feat(pwa): one-QR install + pair + reconnect flow#2821
Conversation
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>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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>, | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 93ad00b. Configure here.
|
|
||
| const handleTryOpenApp = () => { | ||
| window.location.assign("/"); | ||
| }; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 93ad00b. Configure here.
ApprovabilityVerdict: 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. |


Summary
Adds a
/msmart-landing route so a single QR scan from the desktop Mobile Access UI handles all three mobile onboarding states:/m?token=..., captures the token tolocalStorage(survives PWA install), shows an install card. After install, rootbeforeLoadconsumes the token and routes straight to/pair— no second scan./m, auto-dispatches to/pair.Token is in a query param (not hash) so it survives iOS A2HS fragment stripping. Pairing remains single-use; the
localStoragewrapper has a 23h safety margin under the server's 24h TTL.UI uses
beforeinstallpromptfor 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. ComplementsConnectionStatusBanner(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 routeapps/web/src/components/mobile/InstallLandingCard.tsx— install UI (Android prompt + iOS instructions)apps/web/src/components/mobile/ConnectionStatusPill.tsx— connected-state pillapps/web/src/pendingPairingToken.ts— localStorage wrapper, 23h TTLapps/web/src/pendingPairingToken.test.ts— 6 testsModified
apps/web/src/routes/__root.tsx—/mstashes token to localStorage;/cold-start consumes pending token and redirects to/pairapps/web/src/pairingUrl.ts—setPairingTokenAsQueryOnUrlhelperapps/web/src/components/settings/pairingUrls.ts—resolveMobileBootstrapUrl+resolveAdvertisedEndpointMobileBootstrapUrlapps/web/src/components/mobile/MobileAccessSection.tsx— QR now encodes/m?token=...apps/web/src/components/Sidebar.tsx— slotsConnectionStatusPillinto the mobile sidebar headerTest plan
/minstall card appears with "Pairing link captured" badge → tap Install → open from home screen → confirm app lands in chat, paired, no second scan/m→ confirm auto-pair without UI flash/m→ confirm graceful "link captured" but pairing fails cleanly🤖 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/mstashes the token inlocalStorage(~23h TTL), strips it from the address bar, and showsInstallLandingCard(Androidbeforeinstallprompt, iOS Add to Home Screen steps). After install or on standalone open, the app routes to/pair; a rootbeforeLoadon/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.netallowed hosts for tailnet dev/preview.Mobile UX:
ConnectionStatusBanner(offline / reconnecting / lost,sm:hidden) on chat, settings, and index;ConnectionStatusPillin the sidebar when connected.WebSocketConnectionSurfaceresets 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
manifest.webmanifestand iOS PWA meta tags to enable installability, plus a Workbox service worker viavite-plugin-pwawithNetworkFirst/CacheFirstruntime caching strategies/mroute that captures a pairing token from the URL intolocalStorageand, in standalone mode, auto-redirects to/pair?token=...or/MobileAccessSectionto connection settings (desktop only) that shows a QR code linking to the/mbootstrap URL with an expiring server pairing credentialInstallLandingCardto guide installation: triggers the PWA install prompt on Android, shows step-by-step instructions on iOS, and shows a success panel after installConnectionStatusBanner(mobile-only fixed top banner) andConnectionStatusPill(sidebar) reflecting WebSocket state; also resets reconnect backoff onvisibilitychangewhen disconnectedMacroscope summarized 93ad00b.