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.
Platform contract harnesses need monotonic event revisions for at-least-once
dedup and a typed rename API that survives listing.
Co-authored-by: Cursor <cursoragent@cursor.com>
Send to a device now sits alongside the QR code, NFC, and export actions,
since an offer is another way to deliver the same invitation. Picking a
device pushes the existing transfer rather than re-sharing the files.
The picker lists only devices holding a live grant, so nothing offered there
can fail on tap, and it distinguishes accepted from waiting for that device
to open the app.
Also fixes the deprecated SF Symbol and the two Sendable warnings introduced
with the contacts screen: the sections now talk to the model directly rather
than storing view callbacks that a Binding setter has to convert.
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 contact, pairing, and offer models plus the gateway surface the feature
models will use. Contacts and offers are endpoint-scoped events with no
transfer id, so they get their own coalesced signals.
DeviceContact.displayName prefers the local label over the name the peer
claims, and carries a short endpoint fingerprint for telling apart devices
using the same name.
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.
Brings in custom relays, relay connection policies, storage cache clearing, and
receiver-failure reporting. Apple-side conflict resolutions:
- CoreRepository: keep CoreDispatcher, adopt master's relay factory + network
transition guard, drop the now-unused serial queue.
- TransferDetailsView: keep the toolbar-share layout; adopt master's
invitationPresentation-based QR panel and the new .failed receiver case (typed).
- SettingsModel/SettingsScreen: typed L10n titleKey with master's .network case;
relay controls and the Free up space / storage redesign coexist.
- Add the missing transfer_receiver_failed localization key.
- Regenerate l10n from the merged strings.json; keep the Apple catalog untracked.
- Drop the notificationsEnabled test assertion (notifications preference was
intentionally removed on this branch).
The Cancel button on an in-progress receive did nothing. CoreRepository
funnelled every core call through one serial DispatchQueue, but `receive`
is a blocking core call that occupies that queue for the whole transfer.
The tapped `cancelTransfer` was enqueued behind the in-flight `receive` on
the same serial queue, so it could never run until `receive` returned —
which it never would, because it was waiting to be cancelled. A deadlock
the button couldn't escape.
The Rust core is explicitly designed for cancel to arrive from another
thread mid-receive (VnidropCore.block_on uses a shared runtime handle for
exactly this). Extracts the two-lane dispatch into a CoreDispatcher: a
serial lane for ordered calls and a separate concurrent lane for
interrupt-style calls, and routes cancel through the latter so the signal
reaches the core and unblocks the receive.
Adds CoreDispatcherTests, including a regression guard that an interrupt
completes while the serial lane is blocked.
Replaces the stringly-typed transfer-event phase/kind/direction values
throughout the progress-derivation logic with EventPhase, EventKind and
EventDirection enums (String-backed to match the core's wire values).
CoreEventModel keeps the raw wire strings as a faithful boundary DTO but
exposes typed eventPhase/eventKind/eventDirection accessors; all logic —
progressForTransfer/Receiver, humanProgressLabel, aggregateReceiverProgress,
the refresh trigger, and the SendScreen snapshots — now compares enum cases
instead of literals. TransferProgress.phase/kind are the enums directly, so
constructions read `phase: .transfer, kind: .progress`. The two ad-hoc
phase/kind Sets collapse into "is a recognized case" (non-nil) checks.
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
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.