Skip to content

Restore driver-share "Add to RoadFlare" button (roadflared: scheme)#4

Closed
variablefate wants to merge 1 commit into
mainfrom
feat/roadflared-driver-scheme
Closed

Restore driver-share "Add to RoadFlare" button (roadflared: scheme)#4
variablefate wants to merge 1 commit into
mainfrom
feat/roadflared-driver-scheme

Conversation

@variablefate
Copy link
Copy Markdown
Owner

Closes #2. Blocked on variablefate/roadflare-ios#64.

Summary

Adds a primary "Add to RoadFlare" CTA on /share/d/<npub> driver-share pages that deep-links into RoadFlare iOS via the custom roadflared: URL scheme. Only visible on driver shares — rider shares stay untouched (tracked separately in #3).

What ships

404.html:

  • New element: <a id="add-driver-btn" class="btn btn-primary" hidden>Add to RoadFlare</a> placed inside .actions, above .store-buttons.
  • In renderSharePage, when isDriver === true: sets add-driver-btn.href = 'roadflared:' + npub + (name ? '?name=' + encodeURIComponent(name) : '') and unhides it.

Why it's a draft

This button goes to a roadflared: URL. RoadFlare iOS does NOT register that scheme yet. Tapping it before the iOS update ships produces the same "Safari cannot open" error we just removed. Do not mark this PR ready until a roadflare-ios App Store build with #64 resolved is live.

Interaction with the removal PR

This PR is designed to be additive — it introduces new elements / new code paths without touching the old add-btn or its nostr: JS line. The separate removal PR (#TBD) deletes those. The two are non-conflicting regardless of merge order.

Test plan (when ready)

  • App Store build with roadflared: support is live
  • gh pr view <n> → rebase onto main (which no longer has add-btn)
  • Visit /share/d/<real-npub> → button visible, href is roadflared:npub1...?name=... with the display name URL-encoded correctly
  • Tap on a device with RoadFlare installed → Add Driver sheet opens pre-filled
  • Visit /share/r/<npub> → button is NOT visible (rider share)

🤖 Generated with Claude Code

variablefate added a commit that referenced this pull request Apr 20, 2026
The button set href to nostr:<npub>, but RoadFlare iOS does not
register any URL scheme today, so tapping it never opened the app —
best case it errored in Safari, worst case some unrelated Nostr client
handled it. The copy button on Account ID and the QR code already
provide the working paths for adding a driver/rider; the broken CTA
only created confusion.

A driver-only replacement using a future roadflared: scheme is drafted
in #4 (pairs with variablefate/roadflare-ios#64). A rider-only
replacement using roadflarer: is drafted in #5, waiting on a future
iOS driver app. This commit clears the way for both by removing the
placeholder element and its JS handler.

Co-authored-by: variablefate <variablefate@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On /share/d/<npub>, render a primary CTA that deep-links into RoadFlare
iOS via roadflared:<npub>?name=<display-name>. Button stays hidden on
rider shares so this change only affects the driver-share flow.

Blocked on: roadflare-ios registering the roadflared: URL scheme and
handling incoming npubs (variablefate/roadflare-ios#64). Keep this PR
as draft until the matching App Store build ships, then mark ready
and merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@variablefate variablefate force-pushed the feat/roadflared-driver-scheme branch from 4f6b163 to 394801e Compare April 20, 2026 17:39
variablefate added a commit to variablefate/roadflare-ios that referenced this pull request Apr 28, 2026
#66)

* feat(app): register roadflared: URL scheme for driver-share deep links

Tap a roadflared:<npub>?name=... link on a device with RoadFlare
installed → app opens, drivers tab is selected, AddDriverSheet is
presented pre-filled with the parsed npub and display name. Replaces
the previous nostr: scheme attempt, which the app never registered as
a handler.

Routing pattern: app-level .onOpenURL → AppState intent property →
DriversTab observation. AppState gains pendingDriverDeepLink and
handleIncomingURL(_:); DriversTab observes via .onChange + .task and
hands the parsed payload to AddDriverSheet's new prefill: init param.
Pattern is documented in ADR-0012; reuses DriverQRCodeParser via a new
parseRoadflaredURI arm that mirrors the existing parseNostrURI arm.

Also fixes a latent crash in parseNpubWithOptionalQuery: an empty body
(e.g. parse("nostr:") or parse("roadflared:")) split to an empty array
and crashed on parts[0]. Now guarded; both arms get regression tests.

Closes #64. Once this ships in an App Store build, merge the matching
site PR variablefate/roadflare-site#4 to restore the "Add to RoadFlare"
button on /share/d/<npub> pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: address code-review findings on PR #66

Three fixes from the code-review pass:

1. Cross-user state leak — `pendingDriverDeepLink` was missing from
   `AppState.prepareForIdentityReplacement`, so an unconsumed deep link
   could survive into the next user's session. Added the reset alongside
   the analogous `requestRideDriverPubkey` and `selectedTab` resets, plus
   a regression test that exercises the logout path end-to-end.

2. Sheet clobber on rapid second deep link — `DriversTab` previously used
   `.sheet(isPresented:)` with separate `showAddDriver` / `addDriverPrefill`
   state. SwiftUI does not re-evaluate `.sheet(isPresented:)`'s content
   closure when the bound state changes, so a second `roadflared:` URL
   arriving while the first sheet was still presented silently dropped.
   Refactored to `.sheet(item:)` with a private `Identifiable` wrapper
   (`AddDriverPresentation`); a new presentation identity now triggers
   SwiftUI to dismiss-and-re-present with the latest prefill. Also
   consolidated `.onChange` + `.task` into `.onChange(of:initial:)` to
   handle both warm and cold-start paths in one place. Net: 3 state vars
   become 1, two modifier blocks become one.

3. Misleading test name — `rejectsRoadflarerSchemeOnRiderApp` actually
   pinned the *acceptance* of `roadflarer:` via the embedded-npub fallback.
   Renamed to `acceptsRoadflarerSchemeViaEmbeddedNpubFallback` and tightened
   the comment.

ADR-0012 updated to reflect the `.sheet(item:)` pattern and the
`prepareForIdentityReplacement` cleanup contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: cold-start deep link survives onboarding + #70 sync-screen bug

Two fixes folded together since both touch AppState's identity-creation paths.

A. Cold-start regression I introduced in 55c8f83 — the unconditional
   `pendingDriverDeepLink = nil` in `prepareForIdentityReplacement`
   wiped a deep link that arrived before the user had any account.
   `generateNewKey`, `importKey`, AND the new `createWithPasskey` all
   call `prepareForIdentityReplacement` early in their flow, so a
   first-time user tapping a `roadflared:` link from elsewhere on iOS
   would lose the intent the moment they tapped "Generate Key" /
   "Create with Passkey" in the welcome screen. ADR-0012 explicitly
   says this case must work; the pass-1 fix broke it.

   Fix: gate the clear on `keypair != nil`. Cross-user-leak protection
   (the original Finding 1 from pass-1 review) still works because
   logout/identity-replacement is only meaningful when there IS a
   prior keypair to replace. Added regression test for the
   keypair == nil path. The keypair-set path can't be unit tested
   here — RoadFlareTests target lacks Keychain entitlement (-34018) —
   so it lives in the manual test plan.

B. Issue #70 — passkey-create flow showed "Restoring Your Data"
   sync screen for brand-new accounts because `WelcomeView.createWithPasskey()`
   reused `importKey()`, which sets `authState = .syncing` while it
   queries relays for a non-existent prior identity. Fix: add a new
   `AppState.createWithPasskey(_ nsec:)` method mirroring
   `generateNewKey`'s structure (transitions directly to
   `.profileIncomplete`, no .syncing detour). Re-routed
   `WelcomeView.createWithPasskey()` through it. The "Log In with
   Passkey" recover-account flow still uses `importKey` since
   "Restoring Your Data" is correct copy there.

ADR-0012 updated with the conditional clear nuance and a new
"Onboarding interaction" subsection enumerating the three account-
establishment paths and which auth-state route they take.

Closes #70.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: also gate selectedTab/requestRideDriverPubkey on prior keypair

Device test of cold-start preservation revealed the conditional clear
in prepareForIdentityReplacement was incomplete: pendingDriverDeepLink
was correctly preserved (per the prior fix), but `selectedTab = 0` was
still unconditional. After completing onboarding, the user landed on
the Ride tab instead of the Drivers tab, so the AddDriverSheet observer
in DriversTab didn't fire until the user manually navigated there.

Manually navigating to Drivers DID present the sheet — confirming the
deep-link payload survived correctly, just that the tab routing was
clobbered. Move all three navigation intents (selectedTab,
requestRideDriverPubkey, pendingDriverDeepLink) inside the same
keypair-conditional block: cleared on actual identity replacement
(logout, key replacement when a prior keypair exists), preserved on
first-time setup so cold-start state survives the user's first
generateNewKey/createWithPasskey/importKey call.

Test renamed to `navigationIntentSurvivesIdentityReplacementWhenNoKeypair`
and now asserts both the pendingDriverDeepLink and selectedTab survive.
ADR-0012 updated to call out the dual-state preservation explicitly
(without selectedTab preservation, the user lands on the wrong tab and
DriversTab — where the sheet observer lives — is not the first tab to
mount post-`.ready`).

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>
@variablefate
Copy link
Copy Markdown
Owner Author

Superseded by #10 — same intent, rebased onto current main (post-#6 / post-#8) with rationale updated for the post-Universal-Links world. Branch feat/roadflared-driver-scheme can be deleted.

variablefate added a commit that referenced this pull request May 12, 2026
…red:) (#10)

* feat(share): add Add-to-RoadFlare button on driver shares (roadflared:)

Closes #2. Supersedes draft #4.

Re-introduces the in-page "Add to RoadFlare" CTA on `/share/d/<npub>`,
deleted in #6 for being non-functional pre-iOS-1.0. With variablefate/roadflare-ios#66
(roadflared: URL scheme) and #113 (Universal Links) both shipped, the
button is now functional via the custom scheme.

## Why roadflared: and not https://

A button with `href="https://roadflare.app/share/d/<npub>"` on the share
page itself wouldn't fire — Safari treats same-URL clicks as no-ops, so
no Universal Link handoff happens. The custom scheme is the deterministic
way to invoke the app from this surface.

## Why a button at all (vs. Smart App Banner only)

The Smart App Banner from #8 only renders in iOS Safari. This button
covers the surfaces it doesn't:

- Chrome / Firefox / DuckDuckGo on iOS
- Any browser on Android
- Desktop browsers when the page is shared cross-device
- Other Nostr-aware clients (drivestr, ridestr) where the user has the
  RoadFlare app installed but isn't on Safari

`roadflared:` is registered system-wide on iOS once RoadFlare is installed,
so the dispatch works regardless of browser. Browsers without a handler
(desktop, Android-without-app) get the normal "no app to handle this"
behavior — graceful, since the App Store buttons below provide install.

## Visibility

Driver shares only (`/share/d/<npub>`). Rider shares (`/share/r/<npub>`)
remain untouched — no rider-side iOS app registers a handler yet, so
showing the button there would dead-tap. Tracked in #3.

## Test plan

- [ ] Visit `/share/d/<real-npub>` → button visible above store buttons, href is `roadflared:npub1...?name=...` with display name URL-encoded correctly
- [ ] Visit `/share/d/<npub-without-display-name>` → href is `roadflared:npub1...` (no name param)
- [ ] Visit `/share/r/<npub>` → button NOT visible
- [ ] Tap on iPhone with RoadFlare installed → Add Driver sheet opens pre-filled
- [ ] Tap on Android / desktop → graceful no-app dialog (App Store buttons still available below)
- [ ] Smart App Banner from #8 still surfaces independently on iOS Safari

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* fix(share): eliminate horizontal scroll + widen QR on mobile

Several minor fixes to make the share page sit cleanly on a single
mobile viewport without any horizontal scrolling. No layout / design
changes — same vertical order, same components, same colors.

## Horizontal scroll fix

The `.account-id-btn .npub-text` flex item had `white-space: nowrap` +
`text-overflow: ellipsis` but no `min-width: 0`. The flex default of
`min-width: auto` won't shrink a child below its intrinsic content
width — and the npub is 63 characters of unbreakable monospace, so
the button refused to truncate and pushed past the viewport.

`min-width: 0` lets the flex child shrink and the ellipsis kick in.
Pairs with `overflow-x: hidden` on `body` as a safety net so any
future stray-wide element won't reintroduce sideways scroll.

## QR widening

`generateQR` was called with size 160 — visibly tiny on a 390px+
viewport. Bumped to 240 (the canvas `width`/`height` attributes
matched, so initial layout doesn't shift). The canvas now also has
`max-width: 100%; height: auto;` so it scales down gracefully on
iPhone SE (320px viewport, ~272px content area).

## Container padding on very small screens

Container padding stays at `2rem 1.5rem` on all "normal" phones.
For ≤360px viewports (iPhone SE / Mini), reduce horizontal padding
to `1rem` — gains 16px of usable width without visibly changing
margins on larger devices.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* fix(share): use .hidden class on driver button + harden name overflow

Three pass-2 findings on top of the original PR:

## 1. Driver button visible on rider shares (high)

The `<a class="btn btn-primary" hidden>` element used the HTML `hidden`
attribute, which CSS `display: flex` on `.btn` overrides — so the button
was rendered visible on every share page, including rider shares where
the JS never sets the `roadflared:` href. Result: riders saw a primary
"Add to RoadFlare" CTA pointing at `#`.

Switched to the file's existing `.hidden` class convention (used at lines
329, 340, 347, 375, 384) — which lives later in the stylesheet than
`.btn` so source-order wins. JS now toggles via `classList.remove('hidden')`
instead of the broken `.hidden = false` property.

## 2. Long Nostr display names re-trigger horizontal scroll (medium)

`.profile-name` and `.vehicle-text` had no `overflow-wrap` rule. A long
unbroken display_name or vehicle string from a kind-0 event would push
the card past the viewport — defeating the npub-truncation fix from the
previous commit. Added `overflow-wrap: break-word` to both (broader
browser compat than `anywhere`, same behavior for our use case).

## 3. `min-width: 0` looked deletable (low)

Added an inline comment explaining the flex-shrink invariant so a future
maintainer doesn't strip it during a "remove unused rules" pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: variablefate <variablefate@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restore driver-share "Add to RoadFlare" button with roadflared: deep link

1 participant