Commit Graph

269 Commits

Author SHA1 Message Date
c852a68f28 fix(apple): correct the device id row on the detail screen
The fingerprint row reused the no-name placeholder as its label, so it read
"A nearby device — aaab1c58", and it duplicated the device id shown
directly beneath it. Now one selectable full id, which is what someone
comparing devices actually needs.
2026-08-07 10:59:00 +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
677fc3c6d5 fix(apple): make the device detail screen reachable
The Settings stack has a typed path of [SettingsSection], so a
NavigationLink carrying a String could never push onto it: tapping a device
in the list did nothing. Contact detail is now a SettingsSection case, and
the path maps it to the two-level push the way the bug report screen
already does.
2026-08-07 10:33:10 +02:00
3441280599 feat(apple): send a transfer to a device from the share panel
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.
2026-08-07 10:24:05 +02:00
d8587851a3 feat(core): offer an existing share to a remembered device
Another way to deliver an invitation the user already created, alongside the
QR code, rather than a second share of the same files: the ticket handed over
is the stored one and the transfer id is unchanged.

Only an active share can be offered. A stopped one no longer serves its
content, so handing out its ticket would promise nothing.
2026-08-07 10:14:53 +02:00
cba51ad504 docs(design): mark device history as implemented 2026-08-06 19:00:24 +02:00
0f70663263 feat(apple): collect transfers held for this device
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.
2026-08-06 19:00:04 +02:00
94a8ba2103 feat(core): hold undeliverable offers and collect them on demand
An unreachable device is a delay, not a failure: the share stays here and
the ticket waits in held_offers (schema 8 -> 9) until that device comes and
collects it. No server and no push, per the design.

Polling needs no grant proof. iroh has already authenticated the remote
endpoint key, and a device is only handed offers addressed to precisely that
endpoint, so a stranger polling learns nothing. Offers are consumed on
delivery, so polling twice does not re-deliver, and cancelling the transfer
withdraws the waiting ticket.

Polling is rate limited per device: it tells every contact the app was
opened, so it must never become a presence beacon.
2026-08-06 18:47:29 +02:00
268fbf161d feat(apple): send files to a remembered device
Adds the Send files action to the device detail, routing the picked
selection through sendToContact.

Rather than a second picker path, sharePickedFiles now takes a
ShareDestination, so the macOS security-scoped access handling covers both
routes. A contact destination carries no access policy, matching the core's
rule that an offer share is never public.
2026-08-06 18:21:44 +02:00
dc23e87c56 feat(apple): offer to remember a device after a transfer
Closes the loop: until now nothing in the UI could create a contact, so the
list stayed empty unless the peer initiated.

A completed receive names its sender and a completed delivery names its
receiver, so both sides get the suggestion. Declining is persisted, or every
later transfer with the same device would re-ask a question already
answered; pairing deliberately afterwards clears that.

The suggestion sheet ranks below the two other prompts, since nobody is
waiting on the answer.
2026-08-06 18:06:19 +02:00
1369df4578 feat(apple): contacts screen and consent prompts
Device list, detail, and block list under Settings, plus the two sheets:
an incoming offer and a device asking to be remembered. Both are
answer-only, since swiping away would leave the sender waiting.

Accepting an offer routes the released ticket into the ordinary receive
path, so the platform still chooses the destination. The prompt does not
ask a second time; it only falls back to the review sheet when the
destination is unusable.
2026-08-06 17:57:54 +02:00
256ff89423 feat(apple): add the contacts feature model
MVVM model for the device list, its detail, and the two consent prompts.
Accepting an offer is the only path that returns a ticket, matching the
core: a declined offer hands over nothing.

Grant lifetime lives in preferences because the core holds it in memory
only, so it is pushed back on every start rather than silently reverting to
the default.
2026-08-06 17:48:18 +02:00
3c89c34c6e feat(apple): expose device history through the core gateway
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.
2026-08-06 17:40:38 +02:00
178def0629 i18n: add device history strings
Contacts list and detail, pairing consent prompts, incoming offer prompt,
and the grant lifetime setting, in all nine supported languages. Added to
strings.json and regenerated; targeted at both platforms so the Compose
resources exist when the KMP side is built.
2026-08-06 17:29:43 +02:00
5f5f7e0515 chore(apple): drop the unused SwiftPM manifest
Package.swift named its module VniDropApp while all 16 test files import
VniDrop, and it never declared the SFSafeSymbols dependency that project.yml
links, so neither swift build nor swift test worked. The Xcode project is
already the only functioning build definition for the UI and the tests.

Removes a second dependency list that had drifted, and corrects the README,
which documented the CLI path as if it worked.
2026-08-06 17:21:51 +02:00
e041fbeda2 feat(core): send transfers straight to a paired device
Adds SubmitOffer to the contacts ALPN: the sender creates an ordinary share
and pushes the ticket over an authenticated connection, replacing the QR
code without changing the transfer itself.

Only the receiving user is prompted. The sender pre-authorises the target
endpoint before offering, and the approval service now honours an existing
access session, so the handshake the receiver runs next does not ask the
sender to approve a transfer they initiated. An unsolicited ticket receive
still prompts as before.

The ticket leaves the core only when the user accepts; declining yields
nothing. Offer-created shares are never public, one prompt per device is
pending at a time, a decline starts a cooldown, and forgetting a device
clears any prompt it left on screen.
2026-08-06 16:56:52 +02:00
7aa99304b2 feat(core): add the contacts protocol and grant exchange
New /vnidrop/offer/1 ALPN carrying grant delivery and revocation, with a
per-connection challenge so a captured proof cannot be replayed onto another
connection. Unlike the transfer handshake, this serves nobody without a
grant, so an unpaired device cannot raise a prompt on the far side.

A delivered grant is never stored on arrival: it waits for the local user's
consent, so an unsolicited grant cannot create a contact. Forgetting a
contact revokes locally first and notifies the peer best effort. A blocked
endpoint is refused indistinguishably from any other refusal.

Adds the UniFFI surface for listing, pairing, forgetting, blocking, labels,
and grant lifetime.
2026-08-06 16:35:36 +02:00
9fbcf653e8 feat(core): persist contacts, grants, and the block list
Schema 7 -> 8 adds contacts, grants_issued, grants_held, and
blocked_endpoints. Kept in their own module so repository.rs does not grow
further; the tables migrate with the rest of the schema through the shared
pool.

Revocation tombstones rather than deletes, so a returning peer is answered
Revoked instead of Unknown and can drop its dead entry. Blocking revokes any
outstanding grant, and unblocking does not hand access back.
2026-08-06 16:12:31 +02:00
4cfee786fc feat(core): add grant primitives for device history
Grants are the capability a device issues so a known peer may reach it. The
issuer is the only party that can validate one, which is what makes consent
and revocation enforceable without the peer's cooperation.

Pure module: proof construction and constant-time verification bound to the
challenge and both endpoint ids, idle expiry renewed on use, and secrets
redacted in Debug output.
2026-08-06 16:01:20 +02:00
0388422318 docs(design): specify delivery when the recipient is not running
Sender-held offers with a bounded foreground pull instead of push
infrastructure. Records that APNs is out of scope and that mobile-to-mobile
with both apps closed is unsupported.
2026-08-06 15:51:00 +02:00
7afc7d0892 docs(design): device history and direct offers
Design for remembering devices after a transfer and sending to them without
a new invitation. Grant-based contacts so consent and revocation are
enforceable by the party being remembered. Local network discovery
considered and deferred (Appendix A).
2026-08-06 13:35:02 +02:00
Hammed Abass
e8c3eadfc8 Merge pull request #41 from sudosylabs/feat/shared-app-config
Shared app config + remove telemetry (keep bug reports)
2026-08-02 19:50:49 +02:00
877083a3ed refactor(diagnostics): remove bug report breadcrumbs 2026-08-02 19:18:18 +02:00
7cb2270d56 ci(apple): add Xcode Cloud post-clone script
Xcode Cloud only checks out the repo, so ci_post_clone.sh installs swiftlint,
xcodegen and bun, downloads the prebuilt core (vnidrop.xcframework + Vnidrop.swift)
from the matching GitHub Release asset, and generates the project via localization,
version/app config codegen and xcodegen. Rust is never built on Xcode Cloud.
2026-08-02 10:29:46 +02:00
eb8498168d fix(shared): avoid java accessor shadowing when reading app.properties
In a Gradle Kotlin DSL script `java` resolves to the Java plugin extension
accessor, so `java.util.Properties` failed script compilation with
"Unresolved reference 'util'", breaking the shared/Linux/Windows KMP jobs.
Import java.util.Properties and use it unqualified, matching the root build.
2026-08-02 10:29:45 +02:00
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
3e18378610 refactor(diagnostics-api): drop telemetry and crash ingestion, keep bug reports
Remove the /v1/events and /v1/crashes routes, their normalizers and storage
paths, and simplify retention to the bugs table. Add a migration dropping the
now-unused event_batches and crashes tables, and regenerate worker types.
2026-08-02 10:03:12 +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
b8a002a2ad refactor(shared): remove telemetry and crash reporting, keep bug reports
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.
2026-08-02 10:02:28 +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
Hammed Abass
d52ac52cea Merge pull request #40 from sudosylabs/feat/macos-approval-modal-fix
fix(apple): show macOS approval modal + publish prebuilt core bundle
2026-07-31 17:07:40 +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
56d19014d4 chore(release): prepare 0.2.4 v0.2.4 2026-07-31 04:59:36 +02:00
51bf0abba2 fix(release): configure Store CLI before settings 2026-07-31 04:51:31 +02:00
e0fb84ccb9 chore(release): prepare 0.2.3 v0.2.3 2026-07-30 22:22:11 +02:00
30025a4ebf fix(release): download Play APK media 2026-07-30 22:19:40 +02:00
50e9a6c1cc chore(release): prepare 0.2.2 v0.2.2 2026-07-30 21:54:54 +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
Hammed Abass
0ec7618ce8 Merge pull request #39 from sudosylabs/feat/release-pipeline-fixes
fix(release): repair Apple and Android builds
2026-07-30 20:30:28 +02:00
8b75423b7a fix(release): repair Apple and Android builds 2026-07-30 20:26:27 +02:00
Hammed Abass
7236933b76 Merge pull request #38 from sudosylabs/feat/release-0.2.1
chore(release): prepare 0.2.1
v0.2.1
2026-07-30 19:43:19 +02:00
fc732e1b77 chore(release): prepare 0.2.1 2026-07-30 19:27:57 +02:00
Hammed Abass
d097c82f6a Merge pull request #37 from sudosylabs/feat/microsoft-store-publishing
ci: automate Microsoft Store updates
2026-07-30 19:25:15 +02:00
caaa9a472d ci: automate Microsoft Store updates 2026-07-30 19:12:50 +02:00
Hammed Abass
4ce124da7c Merge pull request #36 from sudosylabs/feat/automated-release-versions
feat(release): automate derived store versions
2026-07-30 17:58:18 +02:00