From f513a6118e8d33a2f3f19b7a659e0c2da6002ee0 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:45:02 +0200 Subject: [PATCH] feat(apple): notify the sender when a receiver's delivery fails plannedReceiverNotifications only fired for completed receivers, so a failed delivery produced no notification. Add a receiverFailed kind wired through the planner, id, and deliver paths, with localized notifications_receiver_failed_* strings and a unit test. --- apple/Tests/TransferNotificationTests.swift | 7 +++ .../TransferNotificationCoordinator.swift | 20 +++++++-- localization/strings.json | 44 +++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/apple/Tests/TransferNotificationTests.swift b/apple/Tests/TransferNotificationTests.swift index e38e911..931461d 100644 --- a/apple/Tests/TransferNotificationTests.swift +++ b/apple/Tests/TransferNotificationTests.swift @@ -41,4 +41,11 @@ final class TransferNotificationTests: XCTestCase { let requests = [Fixtures.request(id: "a", requestedAt: 1, status: .completed)] XCTAssertTrue(plannedReceiverNotifications(requests, published: ["receiver-completed-a"]).isEmpty) } + + func testReceiverNotificationsFireForFailedReceivers() { + let requests = [Fixtures.request(id: "x", requestedAt: 1, status: .failed)] + let planned = plannedReceiverNotifications(requests, published: []) + XCTAssertEqual(planned.map(\.id), ["receiver-failed-x"]) + XCTAssertEqual(planned.first?.kind, .receiverFailed) + } } diff --git a/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift b/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift index 0b166fc..4c52fb7 100644 --- a/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift +++ b/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift @@ -7,6 +7,7 @@ enum TransferNotificationKind: Equatable { case receiveCompleted // An incoming transfer finished downloading. case receiveFailed // An incoming transfer failed. case receiverCompleted // A receiver finished downloading your shared transfer. + case receiverFailed // A receiver's download of your shared transfer failed. } /// A notification resolved from core state but not yet published. `transferName` @@ -39,11 +40,17 @@ func plannedTransferNotifications(_ transfers: [Transfer], published: Set) -> [PlannedNotification] { requests.compactMap { request in - guard request.status == .completed else { return nil } - let id = "receiver-completed-\(request.id)" + let kind: TransferNotificationKind + let idPrefix: String + switch request.status { + case .completed: kind = .receiverCompleted; idPrefix = "receiver-completed" + case .failed: kind = .receiverFailed; idPrefix = "receiver-failed" + default: return nil + } + let id = "\(idPrefix)-\(request.id)" guard !published.contains(id) else { return nil } return PlannedNotification( - id: id, kind: .receiverCompleted, + id: id, kind: kind, transferName: request.transferName, receiver: request.receiverName ?? request.receiverDeviceName ) @@ -56,6 +63,7 @@ private func transferNotificationId(_ kind: TransferNotificationKind, transferId case .receiveCompleted: return "receive-completed-\(transferId)" case .receiveFailed: return "receive-failed-\(transferId)" case .receiverCompleted: return "receiver-completed-\(transferId)" + case .receiverFailed: return "receiver-failed-\(transferId)" } } @@ -176,6 +184,12 @@ final class TransferNotificationCoordinator: ObservableObject { id: plan.id, title: String(localized: L10n.Notifications.receiverCompletedTitle), body: L10n.Notifications.receiverCompletedBody(receiver: receiver, transferName: name)) + case .receiverFailed: + let receiver = plan.receiver ?? String(localized: L10n.Approval.nearbyDevice) + notification = LocalNotification( + id: plan.id, + title: String(localized: L10n.Notifications.receiverFailedTitle), + body: L10n.Notifications.receiverFailedBody(receiver: receiver, transferName: name)) } if case .failure(let error) = await notifications.publish(notification) { messages.error(error) diff --git a/localization/strings.json b/localization/strings.json index be75e03..6fdd4cb 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -1948,6 +1948,50 @@ "ru": "Передача получена" } }, + "notifications_receiver_failed_body": { + "context": "Notification body shown to the sender when a receiver's download fails. {receiver} = receiver name, {transferName} = transfer name.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "receiver", + "type": "string" + }, + { + "name": "transferName", + "type": "string" + } + ], + "translations": { + "en": "{receiver} couldn't receive “{transferName}”", + "fr": "{receiver} n'a pas pu recevoir « {transferName} »", + "es": "{receiver} no pudo recibir «{transferName}»", + "it": "{receiver} non ha potuto ricevere “{transferName}”", + "de": "{receiver} konnte „{transferName}“ nicht empfangen", + "pt": "{receiver} não conseguiu receber “{transferName}”", + "pl": "{receiver} nie mógł odebrać „{transferName}”", + "nl": "{receiver} kon “{transferName}” niet ontvangen", + "ru": "{receiver} не удалось получить «{transferName}»" + } + }, + "notifications_receiver_failed_title": { + "context": "Notification title shown to the sender when a receiver's download fails.", + "targets": [ + "apple" + ], + "translations": { + "en": "Delivery failed", + "fr": "Échec de l'envoi", + "es": "Error en la entrega", + "it": "Consegna non riuscita", + "de": "Übertragung fehlgeschlagen", + "pt": "Falha na entrega", + "pl": "Dostarczenie nie powiodło się", + "nl": "Levering mislukt", + "ru": "Ошибка доставки" + } + }, "notifications_send_failed_body": { "context": "Notification body shown to the sender when a shared transfer fails. {transferName} = transfer name.", "targets": [