feat(app): Universal Links for roadflare.app share pages (#63)#113
Merged
Conversation
Closes #63. Add the iOS-side plumbing so `https://roadflare.app/share/d/<npub>` and `/share/r/<npub>` URLs open directly in RoadFlare via Universal Links: - `applinks:roadflare.app` in the Associated Domains entitlement - `.onContinueUserActivity(NSUserActivityTypeBrowsingWeb)` modifier in `RoadFlareApp`, symmetric with the existing `.onOpenURL` modifier - `AppState.handleIncomingUserActivity(_:)` extracts `webpageURL` and dispatches through the existing `handleIncomingURL` → parser → Add Driver intent path from ADR-0012/PR #66 The `DriverQRCodeParser.parseURLOrEmbeddedNpub` arm already accepts the `https://roadflare.app/share/...` URL shape, so no parser changes were needed; this PR is purely the routing layer. Hosting the AASA file at `https://roadflare.app/.well-known/apple-app-site-association` is a prerequisite for the entitlement to take effect in production. See the PR description for the JSON template and roadflare-site cross-link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation PR #66's pass-2 review established the pattern of adding a parallel `navigationIntentSurvivesIdentityReplacementWhenNoKeypair`-style regression test on each new deep-link entry point. The original PR landed coverage only for the `handleIncomingURL` shim — but Universal Links will be the more common production entry path once the AASA ships, so the pre-onboarding survival invariant deserves explicit coverage on `handleIncomingUserActivity` too. The new test mirrors the existing same-named test exactly: arrives via the new entry point with no prior keypair, asserts intent state lands, then runs `logout()` (which exercises `prepareForIdentityReplacement`) and asserts both `pendingDriverDeepLink` and `selectedTab` survive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 11, 2026
variablefate
added a commit
that referenced
this pull request
May 11, 2026
Cleanup miss from PR #113 (`ce0d263`): the parity-regression test added in that PR referenced `ADR-0012` for the roadflared/Universal-Links routing pattern, but the ADR was renumbered to `ADR-0019` in PR #112 (`7c08ac8`) while #113 was in flight. The squash merge auto-resolved existing `ADR-0012` references in this file via 3-way merge, but couldn't catch a brand-new line added on the branch. `decisions/0012-coordinator-ride-identity-cache.md` is now an unrelated ADR, so the stale ref doesn't just point at the wrong number — it points at the wrong subject. Co-authored-by: variablefate <variablefate@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
variablefate
added a commit
to variablefate/roadflare-site
that referenced
this pull request
May 12, 2026
* feat(aasa): add applinks for Universal Links to driver/rider share pages Adds the `applinks` block to the existing AASA file so iOS recognizes `https://roadflare.app/share/d/<npub>` and `/share/r/<npub>` URLs as deep-link targets for the RoadFlare app (App ID 6Y98438M9X.com.roadflare.RoadFlare). The existing `webcredentials` integration (Sign in with Apple Passwords) is preserved unchanged. This is the production prerequisite for [variablefate/roadflare-ios#113](variablefate/roadflare-ios#113) — the `applinks:roadflare.app` entitlement is a no-op until Apple's CDN can fetch a valid AASA from this domain. Once this lands and the next App Store build ships, tapping a roadflare.app share URL anywhere on iOS (Messages, Mail, etc.) opens the app directly with the Add Driver sheet pre-filled, with the share page as the natural fallback when the app isn't installed. ## Path components - `/share/d/*` — driver share. Routes to the iOS `Add Driver` flow via the existing `AppState.handleIncomingUserActivity` handler. - `/share/r/*` — rider share. Same routing today (parser is npub-segment-agnostic); scope reserved here for when a dedicated rider-side iOS surface ships. Other paths on roadflare.app (`/`, `/privacy`, `/terms`, etc.) remain web-only — they are not in the `components` filter and will continue to open in Safari as expected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(aasa): explicitly exclude deeper /share/d|r paths AASA components evaluate first-match-wins in array order. Modern `components` `*` wildcard semantics are ambiguously documented for `/` handling, so be explicit: prepend exclude rules for any path deeper than the single npub segment (e.g. `/share/d/foo/bar`), which the site doesn't render anyway. Defensive — the iOS app's `DriverQRCodeParser` already no-ops on non-npub URLs, so the prior overcapture risk was a graceful silent failure. This makes the AASA spell out the intended scope instead of leaning on Apple's wildcard interpretation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: variablefate <variablefate@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #63.
Summary
Tap
https://roadflare.app/share/d/<npub>or/share/r/<npub>on adevice with RoadFlare installed → app opens with the Add Driver sheet
pre-filled. Falls back to the existing share page (with App Store CTA)
when the app isn't installed — the long-term primary share UX, replacing
the custom
roadflared:scheme from PR #66 as the marketing-link pathonce the AASA file ships.
Architecture
This is the deferred Universal Links work anticipated by ADR-0012
("the
AppState.handleIncomingURLplumbing is reusable when UniversalLinks land"). No new ADR — the routing pattern is the same one the ADR
documents, just with a second SwiftUI scene modifier feeding into it.
Routing pattern:
.onContinueUserActivity→AppState.handleIncomingUserActivity→ existinghandleIncomingURL→ existing parser → existingpendingDriverDeepLink→ existingDriversTabobserver → existingAddDriverSheet(prefill:).Symmetric with the
.onOpenURLmodifier added in PR #66. TheDriverQRCodeParser.parseURLOrEmbeddedNpubarm already accepts thehttps://roadflare.app/share/...URL shape, so this PR is purelyrouting-layer wiring.
What's in the diff
RoadFlare/RoadFlare/RoadFlare.entitlementsapplinks:roadflare.appalongside the existingwebcredentials:entryRoadFlare/RoadFlare/RoadFlareApp.swift.onContinueUserActivity(NSUserActivityTypeBrowsingWeb)modifier mirroring.onOpenURLRoadFlare/RoadFlareCore/ViewModels/AppState.swifthandleIncomingUserActivity(_ activity: NSUserActivity)— thin wrapper that extractswebpageURLand routes throughhandleIncomingURLRoadFlare/RoadFlareTests/AppState/HandleIncomingURLTests.swiftTotal: 4 files, 91 insertions, 0 deletions. All additive — no existing signatures change. No parser changes; no view changes; no SDK changes.
The Associated Domains entitlement is only half of Universal Links.
Apple's CDN must be able to fetch a valid
apple-app-site-associationfile from
https://roadflare.app/.well-known/apple-app-site-associationor this PR is a no-op in production.
The site work is tracked separately at roadflare-site#4 (related draft). The AASA JSON should look like:
{ \"applinks\": { \"details\": [ { \"appIDs\": [\"6Y98438M9X.com.roadflare.RoadFlare\"], \"components\": [ { \"/\": \"/share/d/*\", \"comment\": \"Driver profile share\" }, { \"/\": \"/share/r/*\", \"comment\": \"Rider profile share\" } ] } ] }, \"webcredentials\": { \"apps\": [\"6Y98438M9X.com.roadflare.RoadFlare\"] } }Team ID
6Y98438M9Xand Bundle IDcom.roadflare.RoadFlareconfirmed againstRoadFlare.xcodeproj/project.pbxproj. Thewebcredentialssection preserves the existing Sign in with Apple Passwords integration.Notes for shipping:
Content-Type: application/json(GitHub Pages does this by default for.well-known/static files).Settings → Developer → Associated Domains Developmentto re-fetch during testing.roadflare.appis HTTPS-only).Tests
4 new tests in
HandleIncomingURLTests:universalLinkDriverShareRoutesToDriversTab—https://roadflare.app/share/d/<npub>?name=...populatespendingDriverDeepLinkwith the parsed npub + display name, switchesselectedTabto driversuniversalLinkRiderShareRoutesToDriversTab—/share/r/<npub>goes through the same parser path (parser is segment-agnostic; existingDriverLookupDraftTestsconfirms this is consistent with the rest of the codebase)userActivityWithoutWebpageURLIsDropped—NSUserActivitywithout awebpageURLis a no-opuniversalLinkUnknownPathIsDropped—https://roadflare.app/about(no embedded npub) is dropped silentlyThe tests construct synthetic
NSUserActivityinstances with the browsing-web type and exercise the AppState entry point directly. The SwiftUI.onContinueUserActivitymodifier itself is a one-liner that just callsappState.handleIncomingUserActivity(activity)and isn't unit-testable without a host app + simulator, but it's covered by the manual test plan below.Verification
HandleIncomingURLTestsitself: 11 tests passed (7 pre-existing + 4 new).Manual test checklist (run after merge + AASA is live)
curl -i https://roadflare.app/.well-known/apple-app-site-associationreturns 200 withContent-Type: application/jsonand the JSON abovehttps://roadflare.app/share/d/npub1...link in Messages → "Open in RoadFlare" option appears.onChange(of:initial:)path from ADR-0012 handles thishttps://roadflare.app/aboutlink → opens in Safari (no embedded npub, AASAcomponentsfilter doesn't claim it)roadflared:npub1...flow from PR feat(app): register roadflared: URL scheme for driver-share deep links #66 still works unchangedTwo-app partitioning
Same partitioning notes from PR #66 apply: rider shares (
/share/r/*) currently route to the rider app's Add Driver sheet (consistent with existing parser +DriverLookupDraftbehavior). Once a dedicated iOS driver app exists, the AASAdetailsarray can be split per app — theapple-app-site-associationformat supports multiple appID entries.🤖 Generated with Claude Code