Commit Graph

18 Commits

Author SHA1 Message Date
6b6d5f158d fix(apple): make cancelling a targeted send actually cancel
Creating a targeted transfer contacts the peer and only returns once the
offer is answered or its timeouts expire — connection_timeout plus
offer_wait_timeout, so minutes against a device that never answers. The
composer disabled every control for that whole window, including Close,
leaving no way out. Worse, the core records the row before it reaches out
and leaves it in `failed` when the peer never replies, so giving up still
produced a failure notification and a history entry for a send the user
had already called off.

Cancelling now reaches the core while that create is still running. The
`created` lifecycle event carries the transfer id and is emitted before
the peer is contacted, so the id is known in time; the cancel goes out
through the interrupt lane, which exists precisely to reach a core busy
inside another call. The transfer is then deleted, and its id is filtered
out of the published list so a refresh racing the delete cannot leak it
into history or into a notification. If the id has not arrived yet, the
result carries it and the same cleanup runs on return. Picked sources are
released only once the call lands, because the import owns them until
then, and a generation counter keeps a late result from disturbing a
newer send.

Close and Cancel were also the same action under two labels. There is now
one control: the sheet's cancellation item, reading Cancel while a send is
waiting and Close otherwise, never disabled.

Two further fixes from device testing:

- Receive and Resume were gated on transfer state alone, so an approved
  outgoing transfer offered the sender a Receive button — an invitation to
  download the files it was uploading. Both pull into a local folder and
  are now receiver-only.
- Renamed the deprecated `laptopcomputerAndIphone` symbol to
  `macbookAndIphone`; the deployment targets are well past where it was
  introduced, so no availability guard is needed.

Adds a typed `targetedTransferId` accessor on CoreEventModel. This is a
narrow, deliberate exception to the wake-up-only event rule in
DESIGN-DEVICE-HISTORY.md §13: it takes the subject id and never state,
and it exists because no query can answer while the create holds the
serial lane — which is exactly when the user wants to cancel.

Known gap: direction is inferred by comparing endpoint ids, because the
binding does not expose the row's role. After an identity reset, rows
predating it match neither endpoint, so past sends read as incoming from
the device's own retired identity. Fixing that needs `role` on the core's
TargetedTransfer.
2026-08-13 22:33:06 +02:00
2d4fcd78b5 Merge remote-tracking branch 'origin/feat/device-history' into feat/device-history-apple
# Conflicts:
#	apple/VniDrop/App/RootView.swift
#	apple/VniDrop/Features/App/AppModel.swift
2026-08-13 22:31:38 +02:00
0ea9a8e49c fix(core): recover unrecoverable device identity 2026-08-13 20:49:41 +02:00
8bb1442338 feat(apple): saved devices and targeted transfers UI
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.
2026-08-13 19:42:37 +02:00
dac8232324 fix(apple): realign UI layer with regenerated core bindings
- map the new VnidropError cases (DeviceUnavailable, OfferTimeout,
  RelayPolicyIncompatible, ProtocolIncompatible) to existing catalog keys
- drop the stale .map(\.share) now that sharePickedFiles returns Share
- remove the duplicate .transfersChanged pattern in the signal switch
- replace the deprecated String(cString:) sysctl decode
- close the CoreGateway protocol declaration
2026-08-12 11:34:49 +02:00
eefedd0cb0 fix(core): harden protected identity startup 2026-08-09 20:25:16 +02:00
225ff9ad22 fix: stop the device picker hanging on an offer
Two causes. The connect step had no timeout, so an unreachable device was
retried indefinitely instead of falling through to hold-for-later; it now
gives up after 15s and holds the offer as designed.

The picker also waited on the whole exchange, which includes a person on the
other device deciding — up to two minutes. It now closes on tap and reports
the outcome as a message, and a decline or an unanswered offer is shown as
information rather than an error, since the offer did arrive.
2026-08-07 10:49:32 +02:00
5424da855e fix(apple): show receiver-approval modal on macOS release builds
The approval modal never appeared for a macOS sender: the receiver request
reached the core and even fired its notification, but the modal stayed hidden.

Root cause was observation, not presentation. `RootView` derived `approvals`
and `messages` as `@ObservedObject` in `init` from a freshly built `AppGraph`.
`init` runs on every view re-creation and each run makes a throwaway graph, so
those observed objects were repointed to a dead `ApprovalCoordinator` that never
receives core events — while the persisted `@StateObject graph` (and the models
wired to it) kept the live one. Debug happened not to re-init the view, so it
stayed on the live instance; release re-inits it, exposing the bug.

Move the snackbar + approval modal into an `OverlayLayer` child view that takes
the coordinator/messages as `@ObservedObject` and is constructed in `body` from
the persisted `graph`, so the subscription is always against the live instances.

While here:
- Present the approval only after any open share/QR sheet has actually finished
  dismissing (macOS can't stack sheets), driven off the sheet's real
  `onDismiss` completion via a new `AdaptiveDrawer.onDismissed` hook and
  `SendModel.shareSheetsDismissed` — no wall-clock delay.
- Move the list-level share-sheet state (`shareTargetId`) into `SendModel` so the
  approval flow can dismiss every share surface centrally.
- Add a fallback: pending receiver rows in the Receivers panel now offer an
  Approve action (`SendModel.acceptReceiver`) alongside Refuse, for the case the
  modal didn't surface.
2026-07-31 11:19:28 +02:00
4730554c2c refactor(apple): make InvitationError typed and localize NFC prompts
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.
2026-07-27 16:07:19 +02:00
3abd4d0cfd build(apple): flag raw string literals in SwiftUI initializers
The typed-resource rules missed a bare string literal passed as the leading arg of
a view initializer (e.g. Label("send_stop_sharing", …)), which is an implicit
LocalizedStringKey. Add a rule covering Text/Label/Button/Section/Picker/etc.
(empty labels allowed). Fixes the two dynamic-content Text sites it surfaced by
switching them to Text(verbatim:).
2026-07-24 19:42:40 +02:00
35f06a0b6b fix(apple): localize receiver failure reasons
The receiver row showed the core's raw reason code (e.g. destination_exists),
breaking the never-expose-raw-reason-blobs rule. Map the core reason codes to the
existing L10n.Error.* messages via receiverReasonUiText, with a generic fallback so
a raw code is never surfaced.
2026-07-24 18:45:02 +02:00
8b31c0fe78 feat(apple): use the brand purple as the app-wide accent color
The macOS sidebar selection and item icons rendered in the system default
blue because the app had no global accent color — SwiftUI's `.tint` doesn't
reach the AppKit-backed sidebar. Add an AccentColor asset (the exact sRGB of
VniDropColors.brandPurple) and wire ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME
so the accent applies at the OS level everywhere, including the sidebar.
2026-07-24 00:37:49 +02:00
1563bf80d6 feat(apple): type-safe SF Symbols via SFSafeSymbols
Replaces every stringly-typed SF Symbol name with a compile-time-checked
SFSymbol case, mirroring the L10n accessor approach. A mistyped or
OS-unavailable symbol is now a build error instead of a silently blank
glyph at runtime.

Adds the SFSafeSymbols SPM package (project.yml) and migrates all call
sites: Image(systemName:)/Label(systemImage:) -> systemSymbol, and the
five symbol-carrying view properties (AppDestination.systemSymbol,
SettingsRow.icon, AboutPoint.symbol, MethodRow.icon, PolicyOption.icon)
flipped from String to SFSymbol end to end.
2026-07-23 17:40:07 +02:00
ef42875ddb feat(apple): generate type-safe L10n accessors and migrate all key literals
Replaces every stringly-typed localization key in the Apple app with
compile-time-checked accessors generated from localization/strings.json.
A mistyped key is now a build error instead of a silent fallback to the
raw key at runtime. The runtime path is unchanged: plain keys are
String.LocalizationValue constants resolved with String(localized:) and
Apple's String Catalog still does the lookup; keys with arguments become
typed, named functions applying args through String(format:).

Generator: new renderSwiftAccessors emits apple/VniDrop/Generated/L10n.swift,
wired into generate. Renamed generic arg1/arg2 tokens on four keys to
semantic names (receiver, transferName, deviceId) and updated their context
notes; positional output is unchanged so .xcstrings (bar the 4 comments)
and the Android XML regenerate identical.

Migration: every key-carrying value flipped to String.LocalizationValue
end to end, resolved only at the leaf. Zero key literals and zero
LocalizedStringKey remain in app or test code. macOS build passes; iOS
test run pending.
2026-07-23 17:12:43 +02:00
82fb549576 fix(core): preserve typed transfer failures 2026-07-22 20:43:15 +02:00
bfa489def1 feat(apple): storage screen, About content, and fixes
- Settings: add Storage screen (size breakdown + delete-all-transfers) and
  expand About (what it is/isn't, privacy & security, license/source)
- Move Report a bug to a toolbar sheet (cancel-only unless empty)
- Send progress: derive the list-row bar from receiver delivery status so it
  clears once every receiver completes
- Fixes: iPad orientations, onChange(of:) iOS 17 API, weak-self captures,
  invalid SF Symbol, macOS bug-report form labels; bump core build target to
  match the app (18.2/15.0)
2026-07-19 13:58:20 +02:00
ceccfcda71 feat(apple): transfer controls, progress fixes, and project config
- Sender progress: aggregate only in-flight receivers so the bar clears on
  completion and is order-independent ("Sending to N")
- Add Stop sharing and per-receiver Refuse (pending requests) on the sender
- Fix macOS: raise approval modal above the Share sheet; drive foreground
  state off NSApplication so background notifications fire
- Persist app identity (display name, category) and signing team via
  Info.plist / project.yml / gitignored Local.xcconfig
2026-07-19 12:33:58 +02:00
ea376a382b feat(apple): native SwiftUI app for iOS and macOS
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.
2026-07-18 22:06:30 +02:00