Skip to content

Add App Clip target for share-page → install → driver pre-fill flow #75

@variablefate

Description

@variablefate

Goal

Bridge https://roadflare.app/share/d/<npub> taps from a device without RoadFlare installed → native App Clip preview of the driver → install full app → driver is pre-filled in onboarding's first AddDriverSheet.

This is the App-Store-blessed native solution for "deferred deep linking". The roadflared: scheme (#64, PR #66) and the deferred Universal Links (#63) only work once the app is installed. App Clips are the only native iOS bridge for users tapping a share link before installing.

Background / why this and not alternatives

  • Server-mediated approaches (Branch.io, Adjust, AppsFlyer, deprecated Firebase Dynamic Links) were ruled out due to RoadFlare's privacy posture (no analytics, no fingerprinting, no third-party SDKs).
  • Smart App Banner (variablefate/roadflare-site#7) — complementary, NOT a substitute. The banner surfaces an "OPEN" button to users who already installed and are revisiting the share page, but app-argument is stripped during install, so it only helps the post-install / re-open case. App Clip (this issue) is still required to bridge first-install context.
  • Clipboard / paste-prompt approaches trigger iOS's "pasted from..." privacy banner and require user action.
  • App Clips carry the driver context natively, ~10 MB download, no third-party tracking, GitHub Pages-compatible (only needs an AASA file edit, no backend).

Architecture sketch

Trigger URL

https://roadflare.app/share/d/<npub> — already exists as the SPA-rendered share page. Registered in AASA's appclips section.

App Clip Xcode target

  • New target in RoadFlare.xcodeproj
  • Bundle ID: com.roadflare.RoadFlare.Clip (under team 6Y98438M9X)
  • Constraints: NO background tasks, NO notifications without permission, NO shared Keychain, must use App Group for inter-target state, 10 MB uncompressed binary limit

Code sharing strategy

The clip needs a minimal Nostr client to fetch + display the driver's Kind 0 profile. Two paths:

  1. Link RidestrSDK directly — simplest. Risk: RidestrSDK is broad (relay manager, NIP-44, NIP-19, ride-state machines, fare calculator) and may blow the 10 MB budget when compiled into the clip alongside SwiftUI runtime. Measure first; if size fits, ship.
  2. Extract a RidestrSDK-Lite — keep only NIP19 (npub decode + encode), a stripped relay client, and Kind 0 parsing. Drop NIP44 (no encryption needed in clip), drop ride-state, drop fare calc. Cleaner architecturally; more work.

Recommend: option 1 + measure compiled clip size. Fall back to option 2 if needed.

State handoff (clip → full app post-install)

Both targets entitled to a shared App Group container (group.com.roadflare.RoadFlare):

  1. When the App Clip's "Get RoadFlare" button is tapped:
    • Clip writes the pending driver's npub + cached display name + Kind 0 timestamp to UserDefaults(suiteName: "group.com.roadflare.RoadFlare")
    • Clip presents SKOverlay (or Store.requestAppStoreOverlayPresentation) for the App Store transition
  2. User installs the full app + opens it for the first time:
    • AppState.initialize() (existing entry point — see RoadFlare/RoadFlareCore/ViewModels/AppState.swift) checks the group UserDefaults for a pending driver
    • If present, sets pendingDriverDeepLink = ParsedDriverQRCode(pubkeyInput: npub, scannedName: cachedName)reuses the exact plumbing PR feat(app): register roadflared: URL scheme for driver-share deep links #66 wired up for the roadflared: scheme path
    • Clears the group UserDefaults entry after consuming (one-shot)
  3. Existing onboarding flow takes over: selectedTab is set to 1 (Drivers tab), DriversTab observes pendingDriverDeepLink via .onChange(of:initial:) once it mounts post-.ready, presents AddDriverSheet(prefill:) automatically
  4. Cold-start preservation conditional from PR feat(app): register roadflared: URL scheme for driver-share deep links #66 ensures the deep link survives prepareForIdentityReplacement during the user's first generateNewKey / createWithPasskey / importKey call

This is functionally identical to the roadflared: cold-start path PR #66 already implements; the only new thing is the source of the seed (App Group container vs .onOpenURL).

App Clip UI

Mirror roadflare-site/404.html's driver-share design (visual consistency with the web share page the user already saw):

  • Driver avatar (resolved from Kind 0 picture)
  • Driver display name (resolved from Kind 0 display_name or name)
  • Vehicle description (Kind 0 car_color + car_make + car_model if present — RoadFlare-specific custom fields)
  • Truncated npub with copy-to-clipboard fallback button (in case user dismisses install)
  • Primary CTA: "Get RoadFlare to add [Driver Name]" → triggers SKOverlay
  • Secondary (if Universal Links Add Universal Links support for roadflare.app share pages #63 has shipped): "Already have RoadFlare? Open the app" → universal-link tap

AASA changes (on variablefate/roadflare-site)

.well-known/apple-app-site-association gains an appclips section. Final shape (assuming #63 also lands):

{
  "appclips": {
    "apps": ["6Y98438M9X.com.roadflare.RoadFlare.Clip"]
  },
  "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"]
  }
}

GitHub Pages serves the AASA already; this is one file edit + a roadflare-site PR.

Work breakdown

  1. Xcode target setup — new App Clip target, bundle ID com.roadflare.RoadFlare.Clip, signing under team 6Y98438M9X
  2. Entitlements — Associated Domains (appclips:roadflare.app) on both clip and main app, App Group (group.com.roadflare.RoadFlare) on both
  3. Shared RidestrSDK linkage — link the SDK package into the clip target; measure binary size; if >9 MB compiled, slim to a Lite subset
  4. Clip UI — SwiftUI view rendering the driver Kind 0 fetch, modeled on 404.html
  5. Clip URL parsing — read the invoking URL from userActivity.webpageURL in WindowGroup(for:) or onContinueUserActivity; extract the <npub> segment via the existing DriverQRCodeParser.parse (already accepts https://roadflare.app/share/d/<npub> URLs)
  6. Clip → main app handoff — write pending-driver state to App Group UserDefaults on "Get App" tap; clear after main app consumes
  7. Main app handoff consumptionAppState.initialize() reads App Group UserDefaults and seeds pendingDriverDeepLink via the same path handleIncomingURL already uses; clear the group entry after read
  8. Site PR (variablefate/roadflare-site) — update .well-known/apple-app-site-association. Likely a draft PR similar to PRs Ping feature to notify offline drivers #4 / Add loading animation to launch screen #5 over there.
  9. ADRdecisions/0013-app-clip-deferred-deep-link.md. Document: target boundaries, App Group as state-handoff seam, why we don't use Keychain (clip can't), why state handoff is best-effort (clip eviction)
  10. Manual test plan — across: (a) tap share URL with no app installed → clip downloads → driver shows → install → driver pre-fills, (b) tap with full app installed → Universal Link wins (clip is bypassed), (c) clip evicted between tap and install → graceful: user sees app install but no pre-fill, (d) returning user re-taps share page after install → opens main app via Universal Link

Constraints / risks

  • 10 MB size budget for the App Clip uncompressed binary. RidestrSDK is broad — must measure. SwiftUI runtime alone consumes ~2-3 MB. NIP-44 / secp256k1 adds noticeable size.
  • Notifications / background tasks unavailable in clips — fine for this UX (display + transition only).
  • No shared Keychain between clip and main app — the clip cannot establish or reuse the user's identity. It is purely a pre-install preview, not a partial onboarding. Identity establishment happens fully in the main app post-install.
  • Clip eviction is best-effort — iOS may evict the clip + clear its App Group writes between the user tapping "Get App" and the user actually opening the installed main app. The handoff is therefore opportunistic; if it doesn't survive, the user just sees normal onboarding without pre-fill (graceful degradation, not a crash).
  • App Store review separate from main app review; budget 1-3 days extra ship time.
  • Universal Links Add Universal Links support for roadflare.app share pages #63 should be considered a parallel/prerequisite track. If Add Universal Links support for roadflare.app share pages #63 ships first, the AASA gains applinks; this issue then adds appclips alongside. If this issue ships first, Add Universal Links support for roadflare.app share pages #63 becomes additive.

Out of scope (file separately if desired)

  • Smart App Banner on 404.html — cheap quick-win for users revisiting the share page after install. Tracked separately at variablefate/roadflare-site#7.
  • roadflarer: rider scheme + clip for a future driver app — separate work when that app exists.

Prerequisites

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions