Skip to content

feat(migration): QR payload hand-off through /app, store pair fixes, app-link coverage (TASK-21788) - #3009

Closed
0xkkonrad wants to merge 2 commits into
devfrom
feat/pwa-sunset-landing-mechanism
Closed

0xkkonrad wants to merge 2 commits into
devfrom
feat/pwa-sunset-landing-mechanism

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR 1 of the peanut.me landing → app-store migration (TASK-21788): the mechanism only, no page layout. It makes the download QR carry deferred context, teaches /app to hand that context to the right store on the right platform, fixes the store-button pair so it lays out correctly on desktop and can't overflow a phone, and claims /app for App Links on both platforms so an already-installed user who scans a QR lands in the app instead of the store page.

Everything here is inert until the pwa-sunset PostHog flag is on, except the App Links files (which are additive) and the StoreBadges → StorePair rename.

What changed

  • DownloadQR takes size?: 160 | 192 | 224 and one context input: either payload (a querystring the caller already holds) or handoff ({dest, invite}), mutually exclusive in the props type. Given handoff it derives the payload itself after mount and passes the same handoff to the StorePair beneath it, so the code and the buttons can never carry different context. The encoded URL is ${origin}/app?${payload}&s=<surface> (or ?s=<surface> alone), so every scan is attributable to the QR that produced it and carries locale / invite / badge campaigns / destination through the install.
  • size is the QR frame width; the module area is 36px narrower (p-4 + border-2): 160 → 124px modules, 192 → 156px, 224 → 188px. Documented on the constant. PR 2 should pass size={192} for the hero and size={224} for the app fold and the footer to hit the brief's stated module areas.
  • MIGRATION_QR_SHOWN moved from mount to a real impression: an IntersectionObserver at 50% visibility, once per mount, with { surface, hasContext }. The app fold and the footer render their QR far below the fold, so counting mounts would have made the scan rate look broken. Falls back to counting the mount where IntersectionObserver is missing (jsdom, old WebViews); in a modal the QR is already in view on open, so that is the open event. The capture waits for the derived payload, so a contextful QR never reports hasContext:false.
  • /app (src/app/app/page.tsx) reads window.location.search after mount and validates it with parseDeferredPayload (marker check):
    • Android + payload → auto-redirects to playStoreUrlWithReferrer(payload) and fires trackDeferredHandoffCreated('android').
    • iOS + payload → no auto-redirect. The clipboard hand-off only succeeds inside a user gesture, so the visitor gets the pink App Store button + stroke Google Play button, and the App Store tap calls copyIOSHandoff(payload) inside the click handler before the anchor navigates.
    • No payload → today's behaviour, unchanged.
    • Inside the native app → applyDeferredPayload(parsed) then router.replace(dest ?? '/home').
    • DEFERRED_LINK_HANDOFF_CREATED is counted once per visit (ref guard shared by the auto-redirect and the tap). Without it, a bounce that never took over — blocked, offline, Play missing — re-enabled the buttons after 4s and a tap counted a second hand-off, deflating the restore match rate on exactly the flaky devices.
    • Store taps report trackStoreClick(store, 'smart_link', hasPayload, qrSurface). The s= tag is read here (validated against MIGRATION_SURFACES) and reported as qr_surface on the click and hand-off events, which is what joins a smart-link click back to the hero / app fold / footer / rates QR that produced the scan. It is still stripped before the payload is re-emitted, so it never rides into the Play install referrer.
  • StoreBadges → StorePair (rename, imports updated). Anchors use storeAnchorHref(store, handoff) + onStoreAnchorClick(store, surface, handoff). Container widths are per-appearance: hero is max-w-[27.5rem] (440px) so two sm:w-52 anchors (208px each) plus gap-3 (12px) = 428px sit on one line at every width ≥ sm, including 1366 and 1440; the compact row keeps max-w-[26rem]. Below sm the hero anchors are w-full and stack by design. The two hrefs are memoized per handoff identity — the android one runs buildDeferredPayload (cookie + badge-campaign + window.location reads) and the hero pair lives inside LandingPageClient, which re-renders every scroll-driven animation frame until PR 3 lands.
  • storeAnchorHref / onStoreAnchorClick take an optional StoreHandoff, matching what openStore already accepted.
  • StickyMobileCTA: deviceType === WEB (desktop-mode phone, or a UA we don't recognise) now renders the compact store pair instead of guessing iOS. Everything else is unchanged.
  • App links: /app + /app/* added to all three appIDs in public/.well-known/apple-app-site-association, and android:path="/app" + android:pathPrefix="/app/" to the verified intent filter. app added to NATIVE_EXPORT_ROOTS, and mapDeepLinkPath collapses the whole /app/* claim onto /app — the wildcard has no route behind it on either the web app or the static export, so a passthrough would open the app onto a missing route (see Divergences).
  • MIGRATION_SURFACES gains SMART_LINK, LANDING_APP_FOLD, LANDING_FOOTER, LANDING_RATES, LANDING_COUNTRIES, LANDING_DOOR; new MIGRATION_SURFACE_PARAM = 's' and isMigrationSurface() are shared by the QR builder and /app.
  • New tests: DownloadQR URL building, handoff-derived payload reaching both channels, sizes, impression threshold and hasContext; StorePair hrefs, handoff, tap tracking and the hero row's one-line arithmetic; /app payload branching (android bounce, iOS no-bounce + clipboard-on-tap, bare visit, native apply-and-route, flag-off 404), qr_surface attribution, an unknown surface tag, and the once-per-visit hand-off count; StickyMobileCTA WEB case; /app/* collapsing in the deep-link mapper; an App Links parity guard (iOS AASA ↔ Android intent filter).

No new user-facing copy — no i18n keys added.

How verified

  • pnpm typecheck → clean on every touched file. 6 errors remain, all TS2307 in files this PR does not touch (src/utils/web-vitals-shim.ts, src/utils/app-review.ts, src/utils/native-settings.ts and the web-vitals test): web-vitals, @capacitor/app-launcher, @capgo/capacitor-in-app-review and capacitor-native-settings are absent from this sandbox's shared node_modules. Same 6 before and after the change.
  • pnpm test -- src/components/Migration/__tests__/DownloadQR.test.tsx src/components/Migration/__tests__/StorePair.test.tsx src/app/app/__tests__/smart-store-link.test.tsx src/utils/__tests__/app-links.test.ts src/utils/__tests__/native-routes.test.ts src/components/LandingPage/__tests__/StickyMobileCTA.test.tsx src/components/Migration/__tests__/MigrationDownloadModal.test.tsx → 7 suites, 246 tests, green. pnpm test -- src/utils/__tests__/deferred-link.test.ts src/utils/__tests__/migration.utils.test.ts → 2 suites, 63 tests, green.
  • pnpm lint --quiet <touched files> → clean. prettier --check <touched files> → clean.
  • Hero row geometry: asserted in StorePair.test.tsx against the resolved values rather than the class string (jsdom has no layout). Tailwind v4 with no --spacing or --breakpoint-sm override in src/styles/globals.css, so w-52 = 13rem = 208px, gap-3 = 0.75rem = 12px, sm = 640px, and the max-w-[27.5rem] = 440px cap clears the 428px the row needs. The previous max-w-[26rem] (416px) wrapped the pair at every width ≥ sm.
  • No dev-server/browser check in this PR by design — the runtime pass (screenshots, QR decode, flag-off pixel diff) runs on the page PR, which is where the hero lockup actually renders.

Divergences

  1. /app also had to become a mappable native route. The brief asked only for the AASA + manifest entries. Adding them immediately failed the existing AASA drift guard, which asserts every claimed root resolves through deepLinkToNativePath. That is not a test technicality: an App Link the mapper drops cold-boots the app to /home, so the brief's own native branch (applyDeferredPayload → router.replace(dest)) could never run. /app is already built by the native static export, so nothing new ships — only the mapper's verdict changed. app was therefore added to NATIVE_EXPORT_ROOTS and removed from WEB_ONLY_EXPORTED in the export-drift test, with both comments updated. Side effect, accepted: an in-app anchor to /app would now push in-app rather than open the browser; there are none in the codebase.
  2. handoff threaded through the store helpers, and it is now the only context channel on DownloadQR (a caller passes payload or handoff, never both; handoff derives the payload internally). PR 2 opens the hoisted modal with dest=/send on fold 4 and destination context on fold 5, and that modal's content is DownloadQR → StorePair. As first written the two props were independent: handoff alone left the QR context-free (and reported hasContext:false) while the buttons beneath it carried the destination, and payload alone did the reverse, with nothing catching it. One input, one derivation. The brief's reason for the caller building the payload — it reads window — is met by deriving it in a mount effect.
  3. size is the QR frame width, not the module area. 160 is exactly the frame width QRCodeWrapper ships today (max-w-[160px]); the modules inside it are 124px. Reading "160 default" as the module area would silently resize every existing QR on merge, so the reading that keeps the stated default a no-op is the frame width. The frame→module mapping is now documented on the constant and a 224 option added, so PR 2 can hit the brief's module areas: 192 for the hero (156px modules), 224 for the app fold and footer (188px).
  4. The /app/* App Links wildcard is collapsed in the deep-link mapper. The claim itself is exactly what the brief asked for on both platforms, but /app/<anything> is not a route on the web app or the static export. mapDeepLinkPath now returns /app for any /app/* deep link, so an installed user opening peanut.me/app/anything lands on the real page instead of the SPA's missing-route → home bounce. Neither the AASA drift guard nor the app-links parity test can see this class of bug — both only compare the two hand-maintained lists to each other.
  5. /app reads the s= surface tag instead of only stripping it. The brief had the QR write the tag and /app strip it before re-emitting the payload. It still strips it (the tag must never ride into the Play install referrer), but it is now also validated against MIGRATION_SURFACES and reported as qr_surface on this page's click and hand-off events. Without that read the tag was costing QR modules for nothing and the landing surfaces were indistinguishable in the smart-link funnel — the one attribution question the param exists to answer.

Flag-off impact

None on the page: every behaviour change is inside a pwa-sunset branch (/app still 404s with the flag off, StickyMobileCTA's flag-off arm is untouched, StorePair/DownloadQR only render on migration surfaces). Three changes are live regardless and are additive by design: the /app App Links claim on both platforms, app joining NATIVE_EXPORT_ROOTS, and the mapper's /app/* collapse.

Release gate — the two halves of the /app claim ship on different vehicles. The AASA is a static file served by the web deploy, so iOS picks up the /app claim for already-installed apps within days of merge. The Android intent-filter lives in the APK and only takes effect in the next Play release. app-links.test.ts asserts the two files agree and stays green through the entire window in which the two platforms do not. So: do not flip pwa-sunset on for Android traffic until an Android build containing this manifest is live in Play, and run the runtime checklist's "Android UA" item against that binary, not against the web deploy. Until then an installed Android user who scans a download QR still lands on web /app (404 with the flag off; store buttons for an app they already have once it is on).

@vercel

vercel Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 7, 2026 9:17am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 069d1108-ad2c-44fe-8c86-f6bc9d1427be

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

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.

@notion-workspace

Copy link
Copy Markdown

Landing Page Changes

@github-actions

github-actions Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7255.9 → 7260.19 (+4.29)
Findings: +1 net (+31 new, -30 resolved)

🆕 New findings (31)

  • critical complexity — src/utils/native-routes.ts — CC 118, MI 53.61, SLOC 322
  • critical complexity — src/app/app/page.tsx — CC 62, MI 62.52, SLOC 143
  • critical complexity — src/utils/deferred-link.ts — CC 61, MI 58.07, SLOC 211
  • high method-complexity — src/utils/native-routes.ts:113 — mapDeepLinkPath CC 44 SLOC 131
  • high complexity — src/utils/migration.utils.ts — CC 36, MI 62.99, SLOC 90
  • medium high-mdd — src/utils/native-routes.ts:113 — mapDeepLinkPath: MDD 59.3 (uses across many lines from declarations)
  • medium high-mdd — src/app/app/page.tsx:69 — SmartStoreRedirect: MDD 43.4 (uses across many lines from declarations)
  • medium high-dlt — src/app/app/page.tsx:69 — SmartStoreRedirect: DLT 36 (calls 36 distinct functions — high context load)
  • medium high-mdd — src/components/LandingPage/StickyMobileCTA.tsx:14 — StickyMobileCTA: MDD 29.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/Migration/StorePair.tsx:20 — StorePair: MDD 24.7 (uses across many lines from declarations)
  • medium complexity — src/components/LandingPage/StickyMobileCTA.tsx — CC 21, MI 65.37, SLOC 74
  • medium complexity — src/components/Migration/StorePair.tsx — CC 20, MI 68.66, SLOC 24
  • medium method-complexity — src/utils/native-routes.ts:439 — rewriteMethodPath CC 20 SLOC 34
  • medium complexity — src/components/Migration/DownloadQR.tsx — CC 18, MI 62.26, SLOC 65
  • medium method-complexity — src/app/app/page.tsx:69 — SmartStoreRedirect CC 17 SLOC 49
  • medium complexity — src/constants/migration.consts.ts — CC 3, MI 56.04, SLOC 37
  • medium react-effect-derives-state — src/app/app/page.tsx:106 — useEffect with empty deps + setState — derived state anti-pattern
  • medium react-effect-derives-state — src/components/LandingPage/StickyMobileCTA.tsx:48 — useEffect with empty deps + setState — derived state anti-pattern
  • low high-dlt — src/components/LandingPage/StickyMobileCTA.tsx:14 — StickyMobileCTA: DLT 19 (calls 19 distinct functions — high context load)
  • low structural-dup — app/shhhhh/ShhhhhLandingPage.tsx:100 — 16 duplicate lines / 84 tokens with components/LandingPage/StickyMobileCTA.tsx:52

…and 11 more.

✅ Resolved (30)

  • src/utils/native-routes.ts — CC 117, MI 53.73, SLOC 319
  • src/utils/deferred-link.ts — CC 61, MI 58.08, SLOC 211
  • src/utils/native-routes.ts:113 — mapDeepLinkPath CC 43 SLOC 128
  • src/app/app/page.tsx — CC 41, MI 64.42, SLOC 78
  • src/utils/migration.utils.ts — CC 35, MI 63.55, SLOC 89
  • src/utils/native-routes.ts:113 — mapDeepLinkPath: MDD 49.7 (uses across many lines from declarations)
  • src/app/app/page.tsx:35 — SmartStoreRedirect: MDD 26.0 (uses across many lines from declarations)
  • src/components/LandingPage/StickyMobileCTA.tsx:13 — StickyMobileCTA: MDD 25.2 (uses across many lines from declarations)
  • src/components/LandingPage/StickyMobileCTA.tsx — CC 20, MI 65.67, SLOC 73
  • src/utils/native-routes.ts:422 — rewriteMethodPath CC 20 SLOC 34
  • src/app/app/page.tsx:35 — SmartStoreRedirect CC 17 SLOC 36
  • src/components/Migration/StoreBadges.tsx — CC 17, MI 70.71, SLOC 16
  • src/app/app/page.tsx:47 — small useEffect that only sets state from deps
  • src/app/app/page.tsx:55 — useEffect with empty deps + setState — derived state anti-pattern
  • src/components/LandingPage/StickyMobileCTA.tsx:43 — useEffect with empty deps + setState — derived state anti-pattern
  • src/constants/migration.consts.ts — CC 1, MI 54.12, SLOC 26
  • src/components/LandingPage/StickyMobileCTA.tsx:13 — StickyMobileCTA: DLT 19 (calls 19 distinct functions — high context load)
  • src/app/app/page.tsx:35 — SmartStoreRedirect: DLT 18 (calls 18 distinct functions — high context load)
  • app/shhhhh/ShhhhhLandingPage.tsx:100 — 16 duplicate lines / 84 tokens with components/LandingPage/StickyMobileCTA.tsx:47
  • src/utils/native-routes.ts:422 — rewriteMethodPath: MDD 13.2 (uses across many lines from declarations)

…and 10 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Migration/StorePair.tsx 0.0 4.7 +4.7
src/components/Migration/DownloadQR.tsx 4.2 6.0 +1.8
src/constants/migration.consts.ts 4.6 5.4 +0.8
src/app/app/page.tsx 8.1 8.7 +0.7
src/components/Migration/StoreBadges.tsx 4.6 0.0 -4.6

@github-actions

github-actions Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • ✅ unit: 6119 ran, 0 failed, 0 skipped, 2.0m

📊 Coverage (unit)

metric %
statements 74.7%
branches 60.7%
functions 68.9%
lines 75.7%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

github-actions Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

🖼 Visual diff — 4 screens moved

5 of 66 shots changed · 61 identical · baseline b7f62ae → head aebf6bb

worst % screen widths
1.80% avatar-picker 430
0.07% badges 320
0.03% empty-accounts 320, 430
0.03% unverified 430

job summary · before/after/diff images — artifact

Fixture screenshots, no backend. Advisory — this check never blocks a merge. Posted from the default branch by ds-shots-comment.yml; the report it renders is untrusted data.

@chip-peanut-bot chip-peanut-bot Bot left a comment

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.

Chip review — no blocking findings — this is not an approval

The deferred store handoff works across platform branches, but its QR-source attribution is discarded, and the new hero width cap prevents the store buttons from ever forming the intended desktop row.

Findings

  • MAJOR · src/app/app/page.tsx:129 · Record the QR source before the store handoff
    For a QR emitted by landing_footer, /app?...&s=landing_footer removes s from the forwarded payload, then manual taps hard-code surface: smart_link here while Android auto-bounces record only the platform. Current context-free QRs therefore auto-bounce without any scan event, and payload QRs cannot be attributed to the surface that produced them, so the per-surface scan/conversion rate this parameter was added for is unmeasurable. Parse and validate s against the known migration surfaces, then capture it on a scan or handoff event before either redirect path runs.

  • MINOR · src/components/Migration/StorePair.tsx:40 · Give the hero pair enough width to stay paired
    At sm and above the two hero anchors are 13rem each; with gap-3 they need 26.75rem (428px), but this wrapper caps itself at 26rem (416px). Flex line breaking therefore puts them on separate rows even on the desktop landing hero, so the documented two-button row can never render. Raise the cap to at least 26.75rem or reduce the child widths/gap, and cover the desktop one-line layout.

Checked clean

  • Verified the detached worktree head, trusted author, exact base ref and SHA, merge base, and PR metadata.
  • Traced deferred payload construction, marker parsing, surface stripping, Play referrer generation, iOS clipboard handoff, and native payload application/routing.
  • Checked bare and payload-bearing /app visits across Android, iOS, desktop, native, and flag-off branches, including redirect fallbacks and destination sanitization.
  • Checked QR impression observation, current DownloadQR and StorePair callers, and the landing sticky CTA device fallback.
  • Checked iOS AASA, Android intent-filter, native export mapping, and the repository's parity/drift guards; the new /app claim maps to a shipped native route.
  • Reviewed relevant file history for the smart-link origin, curated App Links allowlist, and native deep-link handling.
  • Exact-head aggregate CI, unit tests, typecheck, native export, lint, formatting, design-system checks, visual snapshots, provenance, and preview deployment all completed successfully.
  • No earlier findings were supplied for reconciliation; correctness, security/privacy, adversarial, and slop passes found no other reachable defect in the changed behavior.

Security review: did not run — openrouter-http-402. This review is one reviewer short.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: 42946479c4f2 · Context: repo, ci · Took 20m

Comment thread src/app/app/page.tsx Outdated
// must stay synchronous up to the clipboard call: a web clipboard write
// only succeeds inside the user gesture that triggered it
const onStoreTap = (store: StoreKind) => {
trackStoreClick(store, MIGRATION_SURFACES.SMART_LINK, !!payload)

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.

MAJOR: Record the QR source before the store handoff

For a QR emitted by landing_footer, /app?...&s=landing_footer removes s from the forwarded payload, then manual taps hard-code surface: smart_link here while Android auto-bounces record only the platform. Current context-free QRs therefore auto-bounce without any scan event, and payload QRs cannot be attributed to the surface that produced them, so the per-surface scan/conversion rate this parameter was added for is unmeasurable. Parse and validate s against the known migration surfaces, then capture it on a scan or handoff event before either redirect path runs.

Comment thread src/components/Migration/StorePair.tsx Outdated
? 'flex w-full flex-col gap-3'
: // wraps rather than overflows: two 208px hero buttons plus the gap
// need 428px, more than a 390px phone has, so they stack there
'mx-auto flex w-full max-w-[26rem] flex-wrap items-center justify-center gap-3'

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.

MINOR: Give the hero pair enough width to stay paired

At sm and above the two hero anchors are 13rem each; with gap-3 they need 26.75rem (428px), but this wrapper caps itself at 26rem (416px). Flex line breaking therefore puts them on separate rows even on the desktop landing hero, so the documented two-button row can never render. Raise the cap to at least 26.75rem or reduce the child widths/gap, and cover the desktop one-line layout.

@chip-peanut-bot chip-peanut-bot Bot left a comment

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.

Chip review — no blocking findings — this is not an approval

The hero pair now has enough width, and payload-bearing handoffs preserve QR attribution, but the surface-only QRs used by every current caller still auto-redirect without recording their source.

Findings

  • MAJOR · src/app/app/page.tsx:171 · Attribute surface-only QR auto-redirects
    Every current DownloadQR caller passes neither payload nor handoff, so scanning (for example) /app?s=landing_footer leaves payload null. This condition then skips countHandoff, and the immediate location.replace succeeds without onStoreTap ever running; no custom event records qr_surface, so the current per-surface QR-to-store funnel remains unmeasurable. Record a dedicated scan/auto-redirect event (or the equivalent store event with handoff:false) with the validated qrSurface before every surface-tagged redirect, while keeping deferred-handoff counts payload-only.

Checked clean

  • Verified the detached worktree head, exact base ref and SHA, merge base, trusted PR metadata, and changed-file scope.
  • Reconciled both prior findings: P2 is fixed by the 27.5rem hero cap and geometry test; P1 remains for surface-only auto-redirects.
  • Traced deferred payload building, marker and surface parsing, Play referrer generation, iOS clipboard handoff, once-per-visit counting, and native payload application and routing.
  • Checked bare and payload-bearing /app visits across Android, iOS, desktop, native, and flag-off branches, including redirect fallback behavior.
  • Checked QR impression observation, all current DownloadQR and StorePair callers, sticky CTA device fallback, and the hero layout arithmetic.
  • Checked iOS AASA, Android intent-filter parity, native export mapping, and /app wildcard collapsing against the shipped route set.
  • Consulted the canonical Notion Lexicon and linked TASK-21788 task; neither defines an additional conflicting product or analytics contract.
  • Exact-head ci-success, unit, typecheck, native-export, lint, formatting, design-system lint, provenance, analysis, and preview checks passed; advisory ds-shots was still running.
  • A local focused Jest run was unavailable because this detached worktree has no node_modules; exact-head unit CI passed.
  • Correctness, security/privacy, adversarial, and slop passes found no other reachable defect in the changed behavior.

Security review: did not run — openrouter-http-402. This review is one reviewer short.

Third opinion by claude-opus: 0 finding(s), marked with the model name. It answers only product truth, missing tests and the cross-repo contract, so treat its findings as advice.

Exact head: aebf6bbcf8fe · Context: repo, product, ci · Took 15m

Comment thread src/app/app/page.tsx
// can still bounce; so can any device with nothing to hand off.
if (payload && targetStore === 'ios') return
// counted before the navigation, and only once per visit
if (payload && targetStore === 'android') countHandoff('android')

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.

MAJOR: Attribute surface-only QR auto-redirects

Every current DownloadQR caller passes neither payload nor handoff, so scanning (for example) /app?s=landing_footer leaves payload null. This condition then skips countHandoff, and the immediate location.replace succeeds without onStoreTap ever running; no custom event records qr_surface, so the current per-surface QR-to-store funnel remains unmeasurable. Record a dedicated scan/auto-redirect event (or the equivalent store event with handoff:false) with the validated qrSurface before every surface-tagged redirect, while keeping deferred-handoff counts payload-only.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Superseded by #3018 (all four folded into one PR per Konrad); branch kept until that merges.

@0xkkonrad 0xkkonrad closed this Sep 7, 2026

This branch was successfully deployed

1 active deployment
Preview — aebf6bbc Deployed Sep 7, 2026 by vercel[bot]
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.

1 participant