Skip to content

feat(pwa): one-QR install + pair + reconnect flow#2821

Open
L0vU3000 wants to merge 4 commits into
pingdotgg:mainfrom
L0vU3000:L0vU3000/mobile-one-qr-onboarding
Open

feat(pwa): one-QR install + pair + reconnect flow#2821
L0vU3000 wants to merge 4 commits into
pingdotgg:mainfrom
L0vU3000:L0vU3000/mobile-one-qr-onboarding

Conversation

@L0vU3000
Copy link
Copy Markdown

@L0vU3000 L0vU3000 commented May 27, 2026

Summary

Adds a /m smart-landing route so a single QR scan from the desktop Mobile Access UI handles all three mobile onboarding states:

  1. First install — browser opens /m?token=..., captures the token to localStorage (survives PWA install), shows an install card. After install, root beforeLoad consumes the token and routes straight to /pair — no second scan.
  2. Update — scan re-opens the existing PWA at /m, auto-dispatches to /pair.
  3. Reconnect — same as update, with a fresh token from a regenerated QR.

Token is in a query param (not hash) so it survives iOS A2HS fragment stripping. Pairing remains single-use; the localStorage wrapper has a 23h safety margin under the server's 24h TTL.

UI uses beforeinstallprompt for one-tap install on Android Chrome, and a 3-step "Add to Home Screen" instruction with the Share-icon walkthrough on iOS Safari.

Also adds a ConnectionStatusPill ("Connected to {Mac name}", mobile-only) inside the sidebar header. Complements ConnectionStatusBanner (which only shows during disconnect) by giving users a positive connected-state indicator after pairing.

Files

New

  • apps/web/src/routes/m.tsx — smart landing route
  • apps/web/src/components/mobile/InstallLandingCard.tsx — install UI (Android prompt + iOS instructions)
  • apps/web/src/components/mobile/ConnectionStatusPill.tsx — connected-state pill
  • apps/web/src/pendingPairingToken.ts — localStorage wrapper, 23h TTL
  • apps/web/src/pendingPairingToken.test.ts — 6 tests

Modified

  • apps/web/src/routes/__root.tsx/m stashes token to localStorage; / cold-start consumes pending token and redirects to /pair
  • apps/web/src/pairingUrl.tssetPairingTokenAsQueryOnUrl helper
  • apps/web/src/components/settings/pairingUrls.tsresolveMobileBootstrapUrl + resolveAdvertisedEndpointMobileBootstrapUrl
  • apps/web/src/components/mobile/MobileAccessSection.tsx — QR now encodes /m?token=...
  • apps/web/src/components/Sidebar.tsx — slots ConnectionStatusPill into the mobile sidebar header

Test plan

  • Fresh install on Galaxy S24: delete existing PWA → scan desktop QR → confirm /m install card appears with "Pairing link captured" badge → tap Install → open from home screen → confirm app lands in chat, paired, no second scan
  • Reconnect (app installed): rescan QR → PWA opens at /m → confirm auto-pair without UI flash
  • Connected pill: open mobile sidebar after pair → confirm "Connected to {Mac name}" pill renders at the top
  • 24h TTL: scan QR, wait or manually expire token in sqlite, open /m → confirm graceful "link captured" but pairing fails cleanly
  • Browser fallback (iOS spot-check): scan QR in Safari → confirm 3-step A2HS instructions render

🤖 Generated with Claude Code


Note

Medium Risk
Touches pairing token handling, auth routing, and PWA/service-worker caching; misconfiguration could affect pairing or stale offline assets, but scope is mostly client-side mobile onboarding.

Overview
Adds mobile PWA onboarding so one desktop QR can install, pair, and reconnect without a second scan.

The desktop Mobile access settings section now exposes Tailscale Serve and encodes /m?token=… (query param, not hash) instead of the old pairing-only URL. Visiting /m stashes the token in localStorage (~23h TTL), strips it from the address bar, and shows InstallLandingCard (Android beforeinstallprompt, iOS Add to Home Screen steps). After install or on standalone open, the app routes to /pair; a root beforeLoad on / also consumes a pending token on cold start.

PWA shell: manifest.webmanifest, Apple web-app meta tags, vite-plugin-pwa (offline shell + API/asset caching), and .ts.net allowed hosts for tailnet dev/preview.

Mobile UX: ConnectionStatusBanner (offline / reconnecting / lost, sm:hidden) on chat, settings, and index; ConnectionStatusPill in the sidebar when connected. WebSocketConnectionSurface resets reconnect backoff when the tab becomes visible again.

Tests cover banner states, mobile access UI states, and pending pairing token storage.

Reviewed by Cursor Bugbot for commit 93ad00b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add PWA install, QR pairing, and mobile reconnect flow

  • Adds a manifest.webmanifest and iOS PWA meta tags to enable installability, plus a Workbox service worker via vite-plugin-pwa with NetworkFirst/CacheFirst runtime caching strategies
  • Introduces a /m route that captures a pairing token from the URL into localStorage and, in standalone mode, auto-redirects to /pair?token=... or /
  • Adds MobileAccessSection to connection settings (desktop only) that shows a QR code linking to the /m bootstrap URL with an expiring server pairing credential
  • Adds InstallLandingCard to guide installation: triggers the PWA install prompt on Android, shows step-by-step instructions on iOS, and shows a success panel after install
  • Adds ConnectionStatusBanner (mobile-only fixed top banner) and ConnectionStatusPill (sidebar) reflecting WebSocket state; also resets reconnect backoff on visibilitychange when disconnected

Macroscope summarized 93ad00b.

mintrose and others added 4 commits May 26, 2026 17:17
Wire up vite-plugin-pwa to make apps/web installable on mobile devices via
"Add to Home Screen". Adds a Workbox-generated service worker (NetworkFirst
for API routes, CacheFirst for hashed assets) and a manifest with 192/512px
icons derived from the production logo.

Also adds .ts.net to Vite's allowedHosts so the production build can be
tested over Tailscale Serve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Mobile Access section to Connections settings that toggles Tailscale
Serve and, when active, auto-generates a single-use pairing credential and
renders it as a QR code. Users scan with their phone camera to install T3
Code as a PWA and complete pairing in one step.

Reuses the existing `setTailscaleServeEnabled` IPC, `createServerPairingCredential`,
and `QRCodeSvg`. Exports `isTailscaleHttpsEndpoint` and
`resolveAdvertisedEndpointPairingUrl` from ConnectionsSettings so the new
component can share the same pairing-URL resolution logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires online/offline/focus/visibilitychange listeners so the PWA
reconnects automatically after phone sleep, tab backgrounding, or
network loss, without requiring a manual refresh.  Adds
ConnectionStatusBanner (mobile-only, sm:hidden) with offline/
reconnecting/error states, and slots it into ChatView, the chat index
route, and the settings route.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a /m smart landing route that handles all three onboarding states
from a single QR scan: first-time install, update, and reconnect. The
QR encoded in MobileAccessSection now points at /m?token=... (query
param so it survives iOS A2HS, which strips URL fragments). The /m
route detects display mode and either dispatches into /pair (PWA
standalone) or shows an install card (browser).

A localStorage wrapper (pendingPairingToken, 23h safety TTL) survives
the install boundary so the pairing token captured in-browser is
available to the freshly-installed PWA on its first launch. The root
beforeLoad consumes it and redirects to /pair, completing pairing
without a second scan.

Android Chrome flow uses beforeinstallprompt for a single-tap install;
iOS Safari shows a 3-step "Add to Home Screen" instruction with the
Share icon walkthrough.

Adds ConnectionStatusPill ("Connected to {Mac name}", mobile-only)
inside the sidebar header to give users a positive connected-state
indicator after pairing, complementing the existing
ConnectionStatusBanner which only shows during disconnection.

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

coderabbitai Bot commented May 27, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 815ea517-a0d6-42d5-a579-87c1ba9f65ca

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels May 27, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 93ad00b. Configure here.

to: "/pair",
search: { token: pending } as Record<string, string>,
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale token hijacks home route

Medium Severity

Root beforeLoad on / always calls consumePendingPairingToken() and redirects to /pair whenever localStorage still holds a pending token, without checking whether the session is already authenticated. The /pair route then sends authenticated users straight back to /, so the token is removed from storage and never submitted, wasting a single-use credential and causing an extra redirect on each visit to / until the pending entry is gone.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 93ad00b. Configure here.

Comment thread apps/web/src/routes/m.tsx

const handleTryOpenApp = () => {
window.location.assign("/");
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open app link consumes token

High Severity

handleTryOpenApp uses window.location.assign("/") from the browser install landing page. That hits root beforeLoad, which consumes the pending pairing token and starts pairing in the browser tab. It does not launch the installed PWA, and on platforms where storage or sessions differ between Safari and the standalone app, the consumed token can leave the installed app unable to complete pairing after a rescan.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 93ad00b. Configure here.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 27, 2026

Approvability

Verdict: Needs human review

This PR introduces a complete new PWA mobile onboarding feature with new routes, components, service worker configuration, and pairing credential handling. The scope of new functionality combined with open high-severity review comments about token consumption bugs warrants careful human review.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant