From 6180cb95ceca0c507b4ba9c1f1c46b1875f6c210 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:43:05 +0200 Subject: [PATCH] fix(apple): stop delete confirmation re-presenting on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirming a transfer/history deletion flashed the same confirmation alert a second time before it went away. The destructive button runs confirmDelete synchronously (setting isDeleting = true), while the alert's isPresented dismiss binding fires asynchronously and then no-ops because its `if !isDeleting` guard is already false — leaving the open flag set, so macOS re-reads the binding as true and re-presents the alert until the async delete finally clears it. Close the confirmation flag synchronously in confirmDeleteTransfer / confirmHistoryDelete so there's no window for re-presentation. Tests assert the flag clears immediately, before the async delete completes. --- apple/Tests/ReceiveModelTests.swift | 3 +++ apple/Tests/SendModelTests.swift | 3 +++ apple/VniDrop/Features/Receive/ReceiveModel.swift | 4 ++++ apple/VniDrop/Features/Send/SendModel.swift | 3 +++ 4 files changed, 13 insertions(+) diff --git a/apple/Tests/ReceiveModelTests.swift b/apple/Tests/ReceiveModelTests.swift index 0a83491..cb4975e 100644 --- a/apple/Tests/ReceiveModelTests.swift +++ b/apple/Tests/ReceiveModelTests.swift @@ -24,6 +24,9 @@ final class ReceiveModelTests: XCTestCase { XCTAssertEqual(model.state.historyDeleteTarget, .transfer(transferId: 5)) model.confirmHistoryDelete() + // Must close immediately (not after the async delete) so the alert can't + // re-present on macOS. + XCTAssertNil(model.state.historyDeleteTarget) await waitUntil { core.deletedTransfers.contains(5) } XCTAssertEqual(core.deletedTransfers, [5]) XCTAssertNil(model.state.historyDeleteTarget) diff --git a/apple/Tests/SendModelTests.swift b/apple/Tests/SendModelTests.swift index 616ae67..12b1801 100644 --- a/apple/Tests/SendModelTests.swift +++ b/apple/Tests/SendModelTests.swift @@ -32,6 +32,9 @@ final class SendModelTests: XCTestCase { XCTAssertTrue(model.state.isDeleteConfirmationOpen) model.confirmDeleteTransfer() + // Must close immediately (not after the async delete) so the alert can't + // re-present on macOS. + XCTAssertFalse(model.state.isDeleteConfirmationOpen) await waitUntil { core.deletedTransfers.contains(3) } XCTAssertEqual(core.deletedTransfers, [3]) XCTAssertNil(model.state.selectedTransferId) diff --git a/apple/VniDrop/Features/Receive/ReceiveModel.swift b/apple/VniDrop/Features/Receive/ReceiveModel.swift index 31c95d2..3927e92 100644 --- a/apple/VniDrop/Features/Receive/ReceiveModel.swift +++ b/apple/VniDrop/Features/Receive/ReceiveModel.swift @@ -121,6 +121,10 @@ final class ReceiveModel: ObservableObject { func confirmHistoryDelete() { guard let target = state.historyDeleteTarget, !state.isDeletingHistory else { return } state.isDeletingHistory = true + // Close synchronously: the alert's dismiss binding runs async and no-ops while + // `isDeletingHistory`, which would otherwise leave the target set and macOS + // re-present it. + state.historyDeleteTarget = nil Task { let result: Result switch target { diff --git a/apple/VniDrop/Features/Send/SendModel.swift b/apple/VniDrop/Features/Send/SendModel.swift index b9d74ed..325fe5c 100644 --- a/apple/VniDrop/Features/Send/SendModel.swift +++ b/apple/VniDrop/Features/Send/SendModel.swift @@ -207,6 +207,9 @@ final class SendModel: ObservableObject { func confirmDeleteTransfer() { guard let transferId = state.selectedTransferId, !state.isDeleting else { return } state.isDeleting = true + // Close synchronously: the alert's dismiss binding runs async and no-ops while + // `isDeleting`, which would otherwise leave the flag true and macOS re-present it. + state.isDeleteConfirmationOpen = false Task { let result = await repository.delete(transferId: transferId) switch result {