mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
Adds background notifications for the "the thing you were waiting for is
done" moments, alongside the existing incoming-approval-request one:
- a receive finished downloading (receive -> done)
- a receive failed / was interrupted (receive -> failed)
- a share you own failed (send -> failed)
- a receiver finished downloading your share (receiver status completed)
A new TransferNotificationCoordinator observes core state + signals and
publishes these; the decision of which moments notify is a pure function
(plannedTransferNotifications / plannedReceiverNotifications), unit-tested
independently. The first state snapshot only primes existing history as seen
so launch doesn't spam.
Notification permission is now the single source of truth. The in-app
notifications toggle and its decoupled UserDefaults preference are gone;
the Settings section shows an "Allow notifications" button that requests the
OS permission (or deep-links to Settings once decided), and notifications
gate purely on `permission == .granted`.
macOS delivery fixes:
- add a UNUserNotificationCenterDelegate so banners present even while the
app is active (the app window is usually open on macOS)
- present-when-active on macOS, suppress-when-foregrounded on iOS
- reserve the notification id before awaiting publish: the CombineLatest
fired several times and re-added the same identifier, which macOS
coalesces into a silent update with no banner
- LocalNotificationService seeds its permission at init so gating can't
race a not-yet-refreshed .notDetermined
Eight localized title/body strings added (apple-only); the shared
notifications_description copy is generalized from "receive requests" to
"transfer activity".
45 lines
2.0 KiB
Swift
45 lines
2.0 KiB
Swift
import XCTest
|
|
@testable import VniDrop
|
|
|
|
@MainActor
|
|
final class TransferNotificationTests: XCTestCase {
|
|
|
|
func testTransferNotificationsFireForTerminalStatesOnly() {
|
|
let transfers = [
|
|
Fixtures.transfer(id: 1, direction: .send, status: .failed),
|
|
Fixtures.transfer(id: 2, direction: .receive, status: .done),
|
|
Fixtures.transfer(id: 3, direction: .receive, status: .failed),
|
|
Fixtures.transfer(id: 4, direction: .receive, status: .receiving), // in-flight, ignored
|
|
Fixtures.transfer(id: 5, direction: .send, status: .sharing), // active share, ignored
|
|
Fixtures.transfer(id: 6, direction: .send, status: .done), // send-done isn't notified
|
|
]
|
|
let planned = plannedTransferNotifications(transfers, published: [])
|
|
XCTAssertEqual(planned.map(\.kind), [.sendFailed, .receiveCompleted, .receiveFailed])
|
|
XCTAssertEqual(planned.map(\.id), ["send-failed-1", "receive-completed-2", "receive-failed-3"])
|
|
XCTAssertEqual(planned.first?.transferName, "Photos")
|
|
}
|
|
|
|
func testTransferNotificationsSkipAlreadyPublished() {
|
|
let transfers = [Fixtures.transfer(id: 2, direction: .receive, status: .done)]
|
|
XCTAssertTrue(plannedTransferNotifications(transfers, published: ["receive-completed-2"]).isEmpty)
|
|
}
|
|
|
|
func testReceiverNotificationsFireOnlyForCompletedReceivers() {
|
|
let requests = [
|
|
Fixtures.request(id: "a", requestedAt: 1, status: .completed),
|
|
Fixtures.request(id: "b", requestedAt: 2, status: .accepted),
|
|
Fixtures.request(id: "c", requestedAt: 3, status: .requested),
|
|
]
|
|
let planned = plannedReceiverNotifications(requests, published: [])
|
|
XCTAssertEqual(planned.map(\.id), ["receiver-completed-a"])
|
|
XCTAssertEqual(planned.first?.kind, .receiverCompleted)
|
|
XCTAssertEqual(planned.first?.receiver, "Peer")
|
|
XCTAssertEqual(planned.first?.transferName, "Photos")
|
|
}
|
|
|
|
func testReceiverNotificationsSkipAlreadyPublished() {
|
|
let requests = [Fixtures.request(id: "a", requestedAt: 1, status: .completed)]
|
|
XCTAssertTrue(plannedReceiverNotifications(requests, published: ["receiver-completed-a"]).isEmpty)
|
|
}
|
|
}
|