fix(apple): stop delete confirmation re-presenting on macOS

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.
This commit is contained in:
2026-07-23 23:43:05 +02:00
parent 2b2fe93293
commit 6180cb95ce
4 changed files with 13 additions and 0 deletions

View File

@@ -24,6 +24,9 @@ final class ReceiveModelTests: XCTestCase {
XCTAssertEqual(model.state.historyDeleteTarget, .transfer(transferId: 5)) XCTAssertEqual(model.state.historyDeleteTarget, .transfer(transferId: 5))
model.confirmHistoryDelete() 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) } await waitUntil { core.deletedTransfers.contains(5) }
XCTAssertEqual(core.deletedTransfers, [5]) XCTAssertEqual(core.deletedTransfers, [5])
XCTAssertNil(model.state.historyDeleteTarget) XCTAssertNil(model.state.historyDeleteTarget)

View File

@@ -32,6 +32,9 @@ final class SendModelTests: XCTestCase {
XCTAssertTrue(model.state.isDeleteConfirmationOpen) XCTAssertTrue(model.state.isDeleteConfirmationOpen)
model.confirmDeleteTransfer() 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) } await waitUntil { core.deletedTransfers.contains(3) }
XCTAssertEqual(core.deletedTransfers, [3]) XCTAssertEqual(core.deletedTransfers, [3])
XCTAssertNil(model.state.selectedTransferId) XCTAssertNil(model.state.selectedTransferId)

View File

@@ -121,6 +121,10 @@ final class ReceiveModel: ObservableObject {
func confirmHistoryDelete() { func confirmHistoryDelete() {
guard let target = state.historyDeleteTarget, !state.isDeletingHistory else { return } guard let target = state.historyDeleteTarget, !state.isDeletingHistory else { return }
state.isDeletingHistory = true 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 { Task {
let result: Result<Void, Error> let result: Result<Void, Error>
switch target { switch target {

View File

@@ -207,6 +207,9 @@ final class SendModel: ObservableObject {
func confirmDeleteTransfer() { func confirmDeleteTransfer() {
guard let transferId = state.selectedTransferId, !state.isDeleting else { return } guard let transferId = state.selectedTransferId, !state.isDeleting else { return }
state.isDeleting = true 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 { Task {
let result = await repository.delete(transferId: transferId) let result = await repository.delete(transferId: transferId)
switch result { switch result {