81 Commits

Author SHA1 Message Date
ac6e837560 docs: describe bug reports instead of telemetry
Update the site privacy policy (no telemetry/analytics, bug-report only, v1.2)
and the README/apple README to reflect that only user-submitted bug reports
remain.
2026-08-02 10:03:29 +02:00
b68d338097 refactor(apple): remove diagnostics opt-in toggle, keep bug reports
Drop the Share-diagnostics preference, its Settings toggle and the
DiagnosticsBuildConfig stub. Bug reporting (NoopBugReportService) and the
diagnostics install id used for bug-report correlation are retained.
2026-08-02 10:02:49 +02:00
232fb125d3 feat(config): shared app.properties for app-wide constants
Add a single source of truth (root app.properties) for public app-wide
constants, injected at build time on both platforms instead of hardcoding.

- Apple: generate-appconfig.sh -> Generated/AppConfig.swift (wired into
  `make apple-app-config`), consumed as AppConfig.privacyPolicyURL.
- KMP: generateAppConfig task -> AppConfig.kt (mirrors DiagnosticsBuildConfig),
  consumed as AppConfig.PRIVACY_POLICY_URL.

Replaces the stale hardcoded privacy-policy URL on both sides with
https://vnidrop.sudosy.fr/privacy/.

Also fix the Apple release core build: disable release LTO in build-core.sh
(Cargo forbids lto in a build-override) to avoid the proc-macro
"mis-aligned LINKEDIT string pool" corruption, so release archives are
compact instead of shipping the debug core.

Update the app icon.

Tests: shell test for the generator (escaping, missing/duplicate key),
plus XCTest and jvmTest asserting the generated value matches app.properties.
2026-08-01 19:56:07 +02:00
fe97c21c7a fix(apple): keep the snackbar above the approval overlay
The earlier approval-modal fix folded SnackbarHost and the approval modal into a
single OverlayLayer child; nested that way the approval host's full-bleed clear
layer covered the toast, so snackbars stopped appearing.

Split them: rename OverlayLayer to ApprovalLayer (approval modal only) and hoist
SnackbarHost to a top-most direct child of the root ZStack, observing the live
`graph.messages` directly. The toast now renders above the overlay again.
2026-07-31 12:46:12 +02:00
c670dda0a9 fix(apple): run the notification delegate on the main actor (iOS crash)
Tapping an approval notification while the app was backgrounded crashed on iOS
with "Call must be made on main thread". The UNUserNotificationCenterDelegate
methods are `async` and nonisolated, so their continuation resumes off the main
thread at the return point — where UIKit synchronously runs state-restoration /
snapshot work, tripping the main-thread assertion. (The empty iOS `didReceive`
body didn't matter; even an empty async method returns off-main.)

Isolate NotificationPresenter to `@MainActor` so the delegate returns on the main
thread. `@preconcurrency` on the UNUserNotificationCenterDelegate conformance is
required because those requirements are nonisolated with non-Sendable UN*
parameters, which strict concurrency won't otherwise let a main-actor type
witness. The macOS branch's now-redundant `await MainActor.run { … }` is dropped.
2026-07-31 12:46:05 +02:00
ff391f5502 build(apple): publish prebuilt core bundle in release assets
Bundle the compiled Apple core — vnidrop.xcframework plus the generated UniFFI
bindings (Vnidrop.swift, a source file that lives outside the xcframework) — into
VnidropCore-<version>.zip with a checksum, and attach it to the GitHub Release.
This lets a consumer (e.g. Xcode Cloud, later) use the prebuilt core instead of
installing Rust and running build-core.sh.

No duplicate builds: the release job compiles the core once (build-apple-dmg ->
build-core.sh release), links it into the signed DMG, and package-core.sh only
zips that same output. Package.swift is unchanged (still binaryTarget(path:)).

- apple/scripts/package-core.sh: stage xcframework + Vnidrop.swift and zip them
  with a sha256sum/shasum-compatible checksum sidecar (macOS-native).
- Makefile: package-apple-core target.
- apple-release.yml: run package-apple-core after the DMG and upload the zip +
  checksum in the macOS artifact.
- assemble-release.sh: verify the core zip's checksum, copy it into the final
  assets, and list it in release-manifest.json + SHA256SUMS (+ fixture update).
2026-07-31 11:40:34 +02:00
9079c81409 build(apple): pin ARCHS to arm64 project-wide
The Rust core's macOS slice (vnidrop.xcframework) is built aarch64-apple-darwin
only, so every target is Apple-Silicon-only — not just the Release-Direct build.
Hoist ARCHS: arm64 from the VniDropDirect target into the project-wide base
settings so no configuration attempts a universal link that would fail looking
for x86_64 symbols. Intel Macs are unsupported (EOL with macOS 28).
2026-07-31 11:20:36 +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
224a8e0e7a fix(release): enforce Apple hardened runtime 2026-07-30 21:27:34 +02:00
efacfab213 fix(release): expose Apple notarization failures 2026-07-30 21:03:30 +02:00
c6655da7db refactor(version): derive Apple build numbers 2026-07-28 11:27:45 +02:00
407a0d2d60 feat(release): unify cross-platform versioning 2026-07-28 08:22:58 +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
cbb535d998 chore(apple): stop tracking RELEASE-MACOS.md
Keep the macOS release notes local-only; remove from the index and ignore
so the working copy stays on disk without being committed.
2026-07-27 16:05:35 +02:00
a0ebd7c71b fix(apple): restore macOS approval modal and sandboxed file sharing
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.
2026-07-27 15:01:01 +02:00
cc194f6a7b feat(apple): add direct-download macOS channel (notarized DMG + Sparkle + Homebrew)
Add a second macOS shipping channel alongside the App Store build:

- New VniDropDirect target (Release-Direct config) sharing VniDrop's sources via
  an AppBase target template; links Sparkle behind the DIRECT_DISTRIBUTION flag so
  the App Store binary never bundles a self-updater. arm64-only (core is arm64).
- Sparkle updater (SparkleUpdater.swift) + "Check for Updates" menu, compiled only
  under DIRECT_DISTRIBUTION; Info.plist SUFeedURL points at the GitHub Release
  /latest/download/appcast.xml, non-sandboxed entitlements for Developer ID.
- build-dmg.sh (archive → Developer ID export → DMG → sign → notarize → staple),
  generate-appcast.sh, and ExportOptions-DeveloperID.plist.
- apple-release.yml: on tag v*.*.*, build/notarize the DMG, publish the GitHub
  Release with appcast, and push the Homebrew cask to sudosylabs/homebrew-vnidrop.
  apple.yml gains a PR compile-check of the direct target.
- CFBundleVersion is stamped at build time as a UTC YYMMDD.HHMM timestamp for both
  channels, replacing the hand-maintained build number.
- Docs (RELEASE-MACOS.md, README), cask template + tap README, localized
  updates_check string, Makefile targets, gitignore for dist/ artifacts.
2026-07-27 14:31:36 +02:00
8de190a36e chore(apple): update Icon Composer app icon definition 2026-07-27 12:19:13 +02:00
73bc87d3d1 fix(apple): use TAG NFC reader format for iOS 26 SDK
App Store upload with the iOS 26 SDK rejects the NDEF value (error 90778
"NDEF is disallowed") and requires TAG. NFCNDEFReaderSession keeps working
under the TAG entitlement, so no code changes are needed.
2026-07-27 10:27:22 +02:00
2166aa9ce4 build(apple): ship TestFlight build 7 as Release
Set CURRENT_PROJECT_VERSION to 7 for the next TestFlight upload, and pin the
scheme's Archive/Profile actions to the Release configuration so Product →
Archive can't pick up Debug.
2026-07-27 10:21:54 +02:00
22b93ce94e feat(apple): keep iOS transfers alive in the background
iOS suspends the process on backgrounding, freezing the core's network
threads so in-flight transfers stall and never fire notifications. Hold a
UIApplication background-task assertion (BackgroundActivityController) while
transfers/shares are active so iOS grants its grace window — long enough to
finish and notify. Released on foreground, on completion, or on expiration.
No UIBackgroundModes added (keeps App Store validation clean); macOS is a
no-op since it already runs unfocused.

Add a localized iOS-only Settings notice explaining the platform limit so it
doesn't read as a bug.
2026-07-27 10:02:31 +02:00
f3124371ee fix(apple): resolve App Store validation errors
- Info.plist: drop unused `fetch`/`processing` background modes (no
  BGTaskScheduler implementation exists, which they would require); keep
  remote-notification.
- Info.plist: set ITSAppUsesNonExemptEncryption=false — the app's standard
  end-to-end encryption qualifies for the mass-market export exemption, so no
  compliance code is required.
- project.yml: emit dwarf-with-dsym for Release so archive symbol upload works.

The remaining upload errors (NFC "NDEF is disallowed", Unsupported SDK) are
artifacts of building with a beta Xcode/SDK 27 and clear when archiving with a
release/RC Xcode; NFCNDEFReaderSession legitimately requires the NDEF format
entitlement, so it is kept as-is.
2026-07-25 19:35:59 +02:00
ea2f8b1cc7 feat(apple): adopt Icon Composer app icon
Replace the legacy AppIcon.appiconset with an Icon Composer AppIcon.icon
bundle in the target's resources. ASSETCATALOG_COMPILER_APPICON_NAME already
points at "AppIcon"; the .icon back-deploys to the iOS 18.2 target.
2026-07-25 16:24:04 +02:00
a8a168ffde chore(apple): declare non-exempt encryption use
Add ITSAppUsesNonExemptEncryption=YES to Info.plist so the export-compliance
question is answered once (the app uses standard end-to-end encryption via
iroh). Avoids being re-prompted on every TestFlight/App Store upload.
2026-07-25 16:06:44 +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
f513a6118e feat(apple): notify the sender when a receiver's delivery fails
plannedReceiverNotifications only fired for completed receivers, so a failed
delivery produced no notification. Add a receiverFailed kind wired through the
planner, id, and deliver paths, with localized notifications_receiver_failed_*
strings and a unit test.
2026-07-24 18:45:02 +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
b65bac021f build(apple): enforce typed resources with SwiftLint
Add a focused .swiftlint.yml (custom rules only, no default style noise) flagging
raw String(localized:) / LocalizedStringKey / systemName|systemImage literals, and
wire it as a required pre-build phase that fails the build if SwiftLint is missing
(brew install swiftlint). The phase prepends the Homebrew bin dirs since Xcode runs
scripts with a minimal PATH. Runs clean on the current tree (0 violations).
2026-07-24 18:32:15 +02:00
d2924f7ce6 fix(apple): focus the running instance on notification tap
Add a UNUserNotificationCenterDelegate didReceive handler so tapping a notification
is handled inside the running app — activating and bringing the existing window
forward — instead of falling through to default launch behavior, which on macOS
can surface a second process. The approval/transfer UI is driven by core state, so
activating the window reveals any pending approval.
2026-07-24 18:32:15 +02:00
3042005226 refactor(apple): type the merged relay/network resources
Convert master's raw-string localization keys and SF Symbols in the new
relay/network code to typed accessors, matching this branch's typed-resources
convention: relay mode labels/descriptions, NetworkSettings strings, the endpoint
id and relay-validation messages (now typed L10n functions), and SF Symbols via
SFSafeSymbols. Retype the model's relayApplyErrorKey from a raw String key to
String.LocalizationValue so no loose key literals remain in the settings layer.
2026-07-24 18:15:36 +02:00
9b15a388d8 fix(apple): polish the merged Network settings
Move the relay-mode picker's Network title into a Section header (the inline
picker label rendered as a stray row on iOS) and hide the picker label. Use a
verbatim prompt for the relay URL placeholder so macOS stops markdown-linkifying
the URL-shaped text into a purple link.
2026-07-24 17:54:59 +02:00
c23f7916bb Merge origin/master into feat/apple-typed-resources-and-fixes
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).
2026-07-24 17:48:10 +02:00
aab5f243ca fix(apple): treat a completed receiver event as terminal
progressForReceiver only labelled a receiver Completed when no progress/started
events preceded the completion, so the normal progress→completed sequence fell
through and rendered as Sending despite a .completed kind. Events are newest-first,
so a completed latest event is always terminal — label it Completed. Fixes the
failing ProgressDerivationTests.testReceiverCompletionAfterProgressIsTerminal.
2026-07-24 16:02:06 +02:00
065d57e896 refactor(apple): redesign the composer source buttons
Replace the bare text links under Start sharing with an even row of bordered,
icon-led buttons (Change files, Choose folder, plus Clear on wider layouts).
Single-line labels keep them equal height, and a neutral tint keeps them quiet so
the purple Start sharing reads as the primary action.
2026-07-24 15:14:21 +02:00
c7ebaee15b feat(apple): add context menus to Send and Receive rows
Send rows get a context menu that acts inline without navigating: Share opens the
share panel (QR + delivery) over the list via a dedicated sheet host, Stop sharing
(active shares) and Delete transfer run in place, the latter through a new id-based
SendModel.deleteTransfer and a list-level confirmation alert. Receive rows get a
Delete action mirroring swipe-to-delete (handy on macOS).
2026-07-24 15:08:37 +02:00
425500ecf2 fix(core): report receiver failures to sender 2026-07-24 14:52:09 +02:00
30dfabf8e7 refactor(apple): move share to the toolbar and delete to the bottom
Put a share icon in the transfer-details toolbar (opening the QR/share panel) in
place of the delete button, drop the now-redundant Share row from the list, and
move Delete transfer into the bottom section alongside Stop sharing.
2026-07-24 14:46:44 +02:00
4bd51106e8 feat(apple): cover the window while the core boots
The core initializes asynchronously at launch, so for a moment the transfer lists
look empty and the app feels stalled. Show a full-window overlay (centered spinner
+ "Starting…") as the top layer of the root stack while coreState.isInitialized is
false; it fades out once the core is ready.
2026-07-24 14:41:37 +02:00
6e2c8b4b2d feat(apple): open the share panel right after creating a transfer
After Start sharing succeeds, jump straight to the new transfer's share panel
(QR code + delivery actions) instead of returning to the list and making the user
drill in via the row and the share row. Refresh first so the transfer exists in
state before selecting it; the share panel already handles the brief window before
the ticket is ready.
2026-07-24 14:18:22 +02:00
e22e395efc feat(apple): streamline the Storage screen and fix stuck usage
Redesign the Storage screen for clarity: an "On this device" usage header with a
manual Refresh control, symbol-led action buttons, and a caption under each action
spelling out exactly what it does (Free up space = temp + trash, non-destructive;
Delete all transfers = clears history + cached share content, keeps received
files).

Fix the summary sticking on "Calculating…": it loaded only on .onAppear and bailed
when opened before the core finished its async launch, leaving the loading branch
showing with nothing running (only a manual refresh recovered it). loadStorageUsage
now waits for the core to become ready before reading usage, distinguishes a real
failure (retry) from loading, loads via .task, and can be refreshed on demand. Use
plain button styling with explicit tints so pressing an action no longer flips the
label to the white selection highlight.
2026-07-24 14:07:31 +02:00
677c3ce47f feat(apple): add a Free up space action to reclaim leaked storage
Delete all transfers only clears core records, and the blob-store cache is
reclaimed by the core's own timer. Neither touches the app's temporary directory
(leftover picker/staging copies — hundreds of MB on macOS) or the stray .Trash
folders that accumulate in app-owned directories and can't be removed via
Files/Finder. Add a non-destructive Free up space button that empties the temp
directory and removes .Trash folders under the core data dir (and, on iOS, the
fixed Documents receive folder), reporting the bytes reclaimed. Guarded against
running while a transfer is in flight; never touches received files, the core
database, or user-chosen macOS receive folders.
2026-07-24 13:46:27 +02:00
20597e6e88 fix(apple): enforce a single window on macOS and iPadOS
macOS uses a single-instance `Window` scene instead of `WindowGroup`, which
otherwise lets the app open multiple windows (via ⌘N). iPadOS sets
`UIApplicationSupportsMultipleScenes = false` to block a second scene via Stage
Manager / split view. (`LSMultipleInstancesProhibited` only blocks a second
process, not a second window.)
2026-07-24 13:19:26 +02:00
42f569dcf0 feat(apple): allow only one macOS app instance
Set LSMultipleInstancesProhibited so re-launching VniDrop (or opening a
vnidrop URL) activates the running instance instead of spawning a second
copy. iOS ignores the key — it's single-instance already.
2026-07-24 12:27:09 +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
c9ef40f00f refactor(apple): tidy the receive-folder preference row
The "Save received transfers to" section was a gray folder label that read
like a disabled field, stacked above two full-width buttons. Replace it with
the standard macOS "label · value · inline action" row: a folder icon + the
current folder name with a trailing "Choose folder" button, long names
truncated in the middle. "Use default" now shows only when a custom folder is
actually set (hidden when already on the default, where it'd be a no-op).
2026-07-24 00:28:34 +02:00
6180cb95ce fix(apple): stop delete confirmation re-presenting on macOS
Confirming a transfer/history deletion flashed the same confirmation alert a
second time before it went away. The destructive button runs confirmDelete
synchronously (setting isDeleting = true), while the alert's isPresented
dismiss binding fires asynchronously and then no-ops because its
`if !isDeleting` guard is already false — leaving the open flag set, so macOS
re-reads the binding as true and re-presents the alert until the async delete
finally clears it.

Close the confirmation flag synchronously in confirmDeleteTransfer /
confirmHistoryDelete so there's no window for re-presentation. Tests assert
the flag clears immediately, before the async delete completes.
2026-07-23 23:43:05 +02:00
2b2fe93293 build(apple): enable dead-code stripping and missing-localizability analyzer
Adds project-wide build settings (applied to every target) so they persist
in project.yml instead of the gitignored generated .xcodeproj:
  - DEAD_CODE_STRIPPING: strip unreachable code from release binaries
  - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: flag user-facing strings that
    aren't localized (the app ships 9 languages), surfaced during Analyze
2026-07-23 23:30:03 +02:00
3469b122c2 feat(apple): local notifications for transfer lifecycle events
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".
2026-07-23 23:29:40 +02:00
0e0d43bc4d build(apple): stop tracking generated localization outputs
Localizable.xcstrings and Generated/L10n.swift are generated from
localization/strings.json — the single source of truth — yet were committed,
which caused redundant tracking and a spurious ~10k-line diff every time
Xcode reformatted the catalog on build.

Treat them like the (already gitignored) Rust bindings: generate at build
time instead of tracking them. gitignore both; make the `apple-project`
target depend on `localization` so `bun run generate` recreates them before
xcodegen; install Bun in the Apple CI job and trigger it on localization/**.
Android strings.xml stays tracked — it has no reformatting churn and its
build doesn't run the generator.
2026-07-23 18:48:03 +02:00
081b59815c fix(apple): make receive-cancel actually cancel the transfer
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.
2026-07-23 18:30:55 +02:00
3c8267adc5 refactor(apple): type core event phase/kind/direction as enums
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.
2026-07-23 18:00:29 +02:00