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.
Add strict custom Iroh relay profiles with safe restart and rollback across the Rust core, Compose apps, and Apple apps. Preserve multi-relay invitations and fail closed on configuration or recovery mismatches.
- 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
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.