Delete the TelemetryRecorder, CrashReporter, PendingCrashStore and platform
crash hooks along with their models, JSON encoders and the diagnostics opt-in
preference. The DiagnosticsTransport interface is narrowed to sendBugReport, and
DiagnosticsCoordinator now only wires the bug-report service and install id.
Bug reporting, the breadcrumb buffer, log redaction and the diagnostics endpoint
config are kept. Regenerate localization after dropping the diagnostics_* keys.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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.
- 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.
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.
Track the Affinity design master (AppStore.af) with Git LFS to keep repo
history lean, and ignore the large exported JPG screenshots (regenerated from
the source) plus macOS/editor junk.
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.
The merge kept this branch's reworded notifications_description and
storage_delete_transfers_description over master's, but the merged KMP code is
master's, so its FoundationComposeTest assertions (and the shipped KMP copy) expect
master's wording. Restore both to master's committed text (pulling the storage one
from master's XML, since master's own strings.json was stale for it). Verified the
two failing KMP Compose tests pass locally.
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:).