Adds the native SwiftUI Saved Devices experience on top of the production
saved-device core, as a top-level destination in the iOS tab bar and the
macOS sidebar.
Core seam:
- App-facing saved-device domain models mirroring core/SavedDeviceModels.kt,
with lifecycle helpers (canReceive/canResume/canCancel/canDelete) so views
never hand-roll state checks.
- 21 gateway methods through CoreGateway/CoreRepository with UniFFI mapping.
cancelTargetedTransfer, forgetSavedDevice and blockDevice run off the serial
lane: each must reach the core while a targeted receive is blocking it.
- Payload-free pairingChanged/targetedTransferChanged signals, dispatched
before the numeric-transferId guard since saved-device events identify
their subject by peer endpoint or a string transfer id.
Experience:
- Screen lists saved devices and outstanding consent requests only; the
global targeted-transfer history stays out, reachable per device.
- Details as a sheet with detents on compact layouts and a native inspector
on macOS, owning Send, label, forget/block and that device's transfers.
- Label editing is transactional: the draft and editor survive a failed
write, conflicting actions are refused while saving, and the editor closes
only after the core confirms.
- Pairing and targeted-offer consent hosted at the app root, answerable from
any tab and suppressed while a transfer approval is up. Dismissing a
pairing prompt suppresses locally without consuming the single-use
eligibility; dismissing an offer declines it, since an unanswered offer
holds a slot in the core's bounded per-sender queue.
- Targeted send reuses the invitation composer's affordances with file,
folder, rename, replace and cleanup parity. Picker copies are released on
replace/remove/clear/cancel and after a successful create, but kept after a
failure so retry does not require re-picking.
- Notifications for pairing requests and offers (withdrawn once answered) and
for terminal targeted transfers. Wording follows direction: on the sending
device the peer finished receiving, not us.
Localization:
- Widens 52 saved-device keys from kmp-only to both platforms.
- Five keys carried a literal %1$s with no declared args, which Compose
renders positionally but the Apple generator emits as a plain constant,
leaking the placeholder into the UI. They now use named args; Compose
output is byte-identical.
- Adds targeted_offer_title/body. Reusing the invitation approval copy stated
the roles backwards, announcing the sender as the receiver.
Also surfaces core startup failures: the startup overlay is drawn above the
snackbar host, so a failed initialize() was indistinguishable from an app
that never finished loading. AppModel now keeps the reason, logs it, and the
overlay shows it with a retry, plus the technical detail in DEBUG builds.
Send and receive between two devices is verified only partially; a missing
endpoint-identity credential currently blocks startup on the test device.
Adds the opt-in foreground check and an explicit Check now, the waiting-to-
be-delivered list on the sender side, and honest reporting when a send could
not be delivered: a closed app is a delay, not a success nobody received.
The setting is off by default and its footer states that checking reveals
app-open times to remembered devices, since that is the reason it is a
setting at all.
Records in the design doc that this shipped as one global toggle rather than
the per-contact opt-in originally specified.
Adds the Send files action to the device detail, routing the picked
selection through sendToContact.
Rather than a second picker path, sharePickedFiles now takes a
ShareDestination, so the macOS security-scoped access handling covers both
routes. A contact destination carries no access policy, matching the core's
rule that an offer share is never public.
Replace the free-form InvitationError.message(String) case with semantic
cases mapped to L10n keys at the UI boundary (Error.uiText), so user-facing
error text is localized instead of substring-matched from English blobs.
.raw(String) remains only for genuinely dynamic system/core messages.
Localize the CoreNFC alertMessage prompts via existing L10n keys, and add
SwiftLint rules (raw_alert_message, raw_invitation_error) to catch raw
alert strings and literal .raw("…") errors going forward.
Approval modal: since the Share/QR sheet auto-opens after creating a transfer,
it is always up when a receiver request arrives, and macOS silently drops a sheet
presented while another is still dismissing — so the approval sheet never appeared.
Drive the approval sheet from explicit state (not a constant binding) and, on
macOS, defer its presentation one dismiss-beat after closing the Share/QR sheet so
the hand-off is serialized. Still a non-dismissable sheet; iOS timing unchanged.
Sandboxed file sharing: the macOS picker released its security scope immediately,
so the core's later import failed with EPERM under the App Store sandbox (the
non-sandboxed .dmg was unaffected). Capture a security-scoped bookmark at pick
time and re-acquire access across shareFiles() — during which the core imports the
bytes — mirroring the receive-folder scoped-access pattern.
- Introduce a CoreGateway protocol so feature models depend on a seam
(CoreRepository conforms); enables faking the core in tests
- Add a VniDropTests target with 42 tests mirroring the KMP suites:
approval coordinator, send/receive/settings/app models, preferences,
file previews, invitation decode, message queue, error mapping
- Add a fake gateway/file-system/device-info and fixtures
- Add .github/workflows/apple.yml: build the Rust core, generate the
project, and run the tests on an iOS Simulator
Enable complete strict concurrency and switch the app target to Swift 6.
- Isolate model dependency protocols to @MainActor
- Make the CoreRepository blocking-FFI bridge race-free: nonisolated(unsafe)
core handle, nonisolated runCore/readSnapshot, @Sendable work block,
Sendable domain models
- Fix Binding method-reference captures; @preconcurrency imports for
CoreNFC/AVFoundation/VnidropCore; isolate the NFC/QR delegate helpers
Add a native SwiftUI VniDrop app (Send/Receive/Settings) talking to the
Rust core via generated UniFFI Swift bindings, plus the uniffi-bindgen
helper crate. iOS uses a TabView, macOS a NavigationSplitView sidebar.