Repository navigation
Conversation
…the-app fold, footer block (TASK-21788)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
Code-analysis diffPainscore total: 7251.68 → 7290.46 (+38.78) 🆕 New findings (59)
…and 39 more. ✅ Resolved (40)
…and 20 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Chip review — changes requested
The exact head breaks production and native builds and leaves several migration funnel paths incorrect; exact-head unit, typecheck, format, and eslint checks are green, but required CI is red.
Findings
-
BLOCKING · src/components/LandingPage/sendInSeconds.tsx:10 · Server-only content import breaks both builds
The client-safe LandingPage barrel re-exports this module and is imported by client surfaces such as ShhhhhLandingPage. Adding getLandingContent pulls src/lib/content.ts (and Node's fs module) into that client/native graph. At this SHA both the ds-shots production build and required native-export fail withCan't resolve 'fs'; the base SHA checks are green. Keep this module client-safe by passing the subtext from a server-only caller, or stop exporting a server-bound module through the client barrel. -
MAJOR · src/components/Migration/AppModalProvider.tsx:39 · Desktop rates handoff loses the /send destination
NoFees callsinterceptAppCta(..., { dest: '/send' }), but the WEB branch stores only the surface. The modal then renders the existing DownloadQR, whose URL is plain/app, so a desktop visitor who scans after clicking the rates CTA installs/opens the app without the requested/senddestination. Store the handoff with the modal state and render a payload-aware QR, then assert the desktop modal URL contains the destination. -
MAJOR · src/components/LandingPage/SEOFooter.tsx:57 · SEO footer publishes an unavailable App Store listing — also flagged by claude-opus
These listing links render in every flag state, including today's flag-off landing page.STORE_URL.ioscurrently returns HTTP 404 and this exact SHA declaresIOS_APP_STORE_LISTING_LIVE = false, so merging adds a crawlable broken download link before the migration is enabled. Gate the iOS listing on its published-status constant (and only expose other listings once verified live), or add these anchors when the listings launch. -
MAJOR · e2e/flows/landing-migration.spec.ts:39 · Production E2E cannot turn the migration flag on
The regression config builds and runsnext start, soNODE_ENVis production, whileisPwaSunsetOnhonors this localStorage override only whenIS_DEVis true. Settingpwa-sunsethere therefore cannot select the flag-on branch: the matrix either repeats the live flag-off UI or depends on remote PostHog state, and the final no-/setupassertion becomes nondeterministic. Use a production-valid deterministic test override and assert the flag-on marker before checking layout. -
MAJOR · src/components/Migration/AppStorePair.tsx:54 · New CTA styles fail the required DS ratchet
The exact-headds-lintcheck is red while the base is green: AppStorePair introduces the legacyborder-n-1token and a hover-only file;lg:gap-7andpx-7add two off-scale spacing uses; the phone frame adds two arbitrary radii; and CountriesSignUpCta, AppStorePair, and PhoneAppCta add hover styles without active states. Replace them with allowed semantic color/spacing/radius tokens and paired active states rather than raising the debt baseline. -
MAJOR · src/components/LandingPage/PhoneAppCta.tsx:58 · Other store sends phones back to the same store
On a phone,/appimmediately redirects to the store detected from that same device. An iOS visitor therefore sees an App Store primary button and clickingOther storeopens/app, which redirects to the App Store again; Android has the symmetric failure. Link to the opposite listing, or add an explicit picker/no-auto-redirect mode to/appand cover both platforms. -
MINOR · src/components/Migration/AppQrCode.tsx:70 · QR impressions fire below the stated 50% threshold
IntersectionObserver delivers an initial entry even when the target is below its configured threshold, andisIntersectingbecomes true for any non-zero intersection. If a QR starts only 10% visible, this condition still fires the one-shot impression despite the claimed 50% gate. Requireentry.intersectionRatio >= 0.5and test entries below and above the threshold.
Checked clean
- Pinned worktree HEAD, merge base, trusted author, base ref, and base SHA all match the supplied values; the detached worktree is clean.
- Exact-head CI and failure logs inspected: format, typecheck, eslint, unit, analyze, human-authors, and bot-approval are green; ds-lint, native-export, ds-shots/build, deploy preview, and aggregate CI are red. The corresponding base checks are green.
- Flag-off rendering branches, device selection, store-anchor/openStore behavior, modal ownership, QR payload construction, CTA route substitutions, footer links, and localization changes were traced through surrounding code.
- New focused unit coverage was inspected and the exact-head full unit check is green.
- Direct store endpoints were checked; the iOS 404 matches the repository's unpublished-listing guard.
- Diff whitespace validation is clean. No issue comments or review comments were fetched or read.
Security review: did not run — openrouter-http-402. This review is one reviewer short.
Third opinion by claude-opus: 1 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: a9d551321342 · Context: repo, ci, web · Took 22m
| import { getTranslations } from '@/i18n' | ||
| import { DEFAULT_LOCALE, type Locale } from '@/i18n/types' | ||
| import { landingStrings } from './landingStrings' | ||
| import { getLandingContent } from '@/lib/landingContent' |
There was a problem hiding this comment.
BLOCKING: Server-only content import breaks both builds
The client-safe LandingPage barrel re-exports this module and is imported by client surfaces such as ShhhhhLandingPage. Adding getLandingContent pulls src/lib/content.ts (and Node's fs module) into that client/native graph. At this SHA both the ds-shots production build and required native-export fail with Can't resolve 'fs'; the base SHA checks are green. Keep this module client-safe by passing the subtext from a server-only caller, or stop exporting a server-bound module through the client barrel.
| (nextSurface, handoff) => { | ||
| if (!migrationOn || isCapacitor()) return false | ||
| if (deviceType === DeviceType.WEB) { | ||
| setSurface(nextSurface) |
There was a problem hiding this comment.
MAJOR: Desktop rates handoff loses the /send destination
NoFees calls interceptAppCta(..., { dest: '/send' }), but the WEB branch stores only the surface. The modal then renders the existing DownloadQR, whose URL is plain /app, so a desktop visitor who scans after clicking the rates CTA installs/opens the app without the requested /send destination. Store the handoff with the modal state and render a payload-aware QR, then assert the desktop modal URL contains the destination.
| * column of English siblings, and the store names themselves are constants. | ||
| */ | ||
| const STORE_LISTINGS = [ | ||
| { slug: 'app-store', href: STORE_URL.ios, name: 'Peanut on the App Store' }, |
There was a problem hiding this comment.
MAJOR: SEO footer publishes an unavailable App Store listing — also flagged by claude-opus
These listing links render in every flag state, including today's flag-off landing page. STORE_URL.ios currently returns HTTP 404 and this exact SHA declares IOS_APP_STORE_LISTING_LIVE = false, so merging adds a crawlable broken download link before the migration is enabled. Gate the iOS listing on its published-status constant (and only expose other listings once verified live), or add these anchors when the listings launch.
| await page.addInitScript( | ||
| ([on]) => { | ||
| try { | ||
| if (on) localStorage.setItem('pwa-sunset', 'true') |
There was a problem hiding this comment.
MAJOR: Production E2E cannot turn the migration flag on
The regression config builds and runs next start, so NODE_ENV is production, while isPwaSunsetOn honors this localStorage override only when IS_DEV is true. Setting pwa-sunset here therefore cannot select the flag-on branch: the matrix either repeats the live flag-off UI or depends on remote PostHog state, and the final no-/setup assertion becomes nondeterministic. Use a production-valid deterministic test override and assert the flag-on marker before checking layout.
| icon={store === 'ios' ? 'apple-logo' : 'google-play'} | ||
| className={twMerge( | ||
| 'w-full bg-white px-6 text-button-m hover:bg-white/90 sm:w-52 md:text-button-l', | ||
| isFooter && 'border-n-1 shadow-none hover:shadow-none' |
There was a problem hiding this comment.
MAJOR: New CTA styles fail the required DS ratchet
The exact-head ds-lint check is red while the base is green: AppStorePair introduces the legacy border-n-1 token and a hover-only file; lg:gap-7 and px-7 add two off-scale spacing uses; the phone frame adds two arbitrary radii; and CountriesSignUpCta, AppStorePair, and PhoneAppCta add hover styles without active states. Replace them with allowed semantic color/spacing/radius tokens and paired active states rather than raising the debt baseline.
| </Button> | ||
| </a> | ||
| {showOtherStore && ( | ||
| <Link prefetch={false} href="/app" className="block text-center text-body-s text-n-1 underline"> |
There was a problem hiding this comment.
MAJOR: Other store sends phones back to the same store
On a phone, /app immediately redirects to the store detected from that same device. An iOS visitor therefore sees an App Store primary button and clicking Other store opens /app, which redirects to the App Store again; Android has the symmetric failure. Link to the opposite listing, or add an explicit picker/no-auto-redirect mode to /app and cover both platforms.
| } | ||
| const observer = new IntersectionObserver( | ||
| (entries) => { | ||
| if (shown.current || !entries.some((entry) => entry.isIntersecting)) return |
There was a problem hiding this comment.
MINOR: QR impressions fire below the stated 50% threshold
IntersectionObserver delivers an initial entry even when the target is below its configured threshold, and isIntersecting becomes true for any non-zero intersection. If a QR starts only 10% visible, this condition still fires the one-shot impression despite the claimed 50% gate. Require entry.intersectionRatio >= 0.5 and test entries below and above the threshold.
There was a problem hiding this comment.
Chip review — changes requested
P2 and P4 are fixed, but the exact head still breaks both builds and the required DS ratchet; the unavailable iOS footer link, misleading Other store CTA, QR threshold bug, and a static cutover-timing edge also remain.
Findings
-
BLOCKING · src/components/LandingPage/sendInSeconds.tsx:10 · Server-only content import breaks both builds
sendInSeconds.tsxnow imports the filesystem-backed landing-content reader, whileLandingPage/index.tsstill re-exports this component from the barrel consumed by the client-only merchant and /shhhhh pages. Exact-head CI confirms the resultingfsimport trace through that barrel: both the regular production build inds-shotsandnative-exportfail to compile. Keep this server component out of the client-safe barrel (and import client-safe siblings directly), or pass the already-read subtext into it without adding the server reader to that client graph. -
MAJOR · src/components/LandingPage/SEOFooter.tsx:64 · SEO footer publishes an unavailable App Store listing — also flagged by claude-opus
The date gate delays publication, but at the cutover it still returns both listings unconditionally.IOS_APP_STORE_LISTING_LIVEremains false and its source comment documents that this exact App Store URL 404s, so a cutover with the repository's current availability state gives every marketing page and crawler a dead iOS link. Filter the iOS entry behind the listing-live signal (or only add it once the listing is verifiably public); the cutover date is not an availability check. -
MAJOR · src/components/Migration/AppStorePair.tsx:57 · New CTA styles still fail the required DS ratchet
Exact-headds-lintis red: legacyColorClasses rises 0→1, offScaleSpacing 188→190, offScaleRadius 37→39, and hoverNoActiveFiles 44→47. This line contributes the new legacyborder-n-1and an unpaired hover state; the other new CTA files addpx-7/gap-7, arbitrary 24px/28px radii, and more hover-only files. Replace the new debt with the current semantic border, spacing/radius tokens and paired active states across the touched CTAs instead of raising the baseline. -
MAJOR · src/components/LandingPage/PhoneAppCta.tsx:63 · Other store routes phones back to the detected store
On iOS the primary button already targets the App Store, but theOther storelink goes to/app; that route detects iOS and immediatelylocation.replaces to the App Store again. Android behaves symmetrically, so the alternative CTA cannot reach the other listing (and it also drops the handoff created by the direct store CTA). Point this link at the explicit oppositeSTORE_URLand preserve the normal click tracking/handoff behavior, or make/appaccept an explicit store override. -
MINOR · src/components/Migration/AppQrCode.tsx:74 · QR impressions still fire below 50% visibility
An IntersectionObserver sends an initial callback even when an element is only slightly visible. With a 0.1 intersection ratio,isIntersectingis true, so this guard fires the impression despite the configured 0.5 threshold. Requireentry.intersectionRatio >= 0.5(and add below/at-threshold cases) before capturing. -
MINOR · src/components/LandingPage/SEOFooter.tsx:68 · Cutover date is frozen into prerendered footer HTML
ThisDate.now()runs while the statically generated homepage and localized marketing pages are built, not when a visitor requests them. A production deployment built before 2026-12-31 therefore keeps omitting the store links after cutover until another deploy/regeneration, contrary to the stated from-cutover behavior. Make the release state an explicit build/deploy input with a scheduled cutover deploy, or give these routes a bounded revalidation/runtime source that can actually cross the date.
Checked clean
- Verified the detached worktree head, trusted author, PR number, base ref, base SHA, and merge base exactly match the supplied review target.
- Rechecked P2: AppModalProvider now stores surface and handoff together and threads dest=/send into the desktop modal QR/store paths; its focused unit assertion covers the handoff.
- Rechecked P4: non-production production builds now honor the localStorage flag override, and the Playwright cases positively assert that the expected phone or desktop lockup rendered.
- Traced the landing CTA paths through AppModalProvider, /app smart-store routing, deferred payload helpers, store anchors, and the listing-live constant.
- Reviewed the flag-off branches, scroll-lock cleanup, locale-specific copy plumbing, hero/device rendering, footer scoping, QR analytics, tests, and design-system drift for correctness, security, adversarial failures, and slop.
- Exact-head unit, typecheck, eslint, format, provenance, analysis, and reporting checks passed; required CI is red because ds-lint, the regular production build, native export, visual build, aggregate CI, and preview deployment fail at this head.
Security review: did not run — this change has no security, privacy or money surface, so it was not asked. This review is one reviewer short.
Third opinion by claude-opus: 1 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: e1f80504900c · Context: repo, ci · Took 24m
| import { getTranslations } from '@/i18n' | ||
| import { DEFAULT_LOCALE, type Locale } from '@/i18n/types' | ||
| import { landingStrings } from './landingStrings' | ||
| import { getLandingContent } from '@/lib/landingContent' |
There was a problem hiding this comment.
BLOCKING: Server-only content import breaks both builds
sendInSeconds.tsx now imports the filesystem-backed landing-content reader, while LandingPage/index.ts still re-exports this component from the barrel consumed by the client-only merchant and /shhhhh pages. Exact-head CI confirms the resulting fs import trace through that barrel: both the regular production build in ds-shots and native-export fail to compile. Keep this server component out of the client-safe barrel (and import client-safe siblings directly), or pass the already-read subtext into it without adding the server reader to that client graph.
| * when the flag is off". A date is the one gate the server can answer. | ||
| */ | ||
| const STORE_LISTINGS = [ | ||
| { slug: 'app-store', href: STORE_URL.ios, name: 'Peanut on the App Store' }, |
There was a problem hiding this comment.
MAJOR: SEO footer publishes an unavailable App Store listing — also flagged by claude-opus
The date gate delays publication, but at the cutover it still returns both listings unconditionally. IOS_APP_STORE_LISTING_LIVE remains false and its source comment documents that this exact App Store URL 404s, so a cutover with the repository's current availability state gives every marketing page and crawler a dead iOS link. Filter the iOS entry behind the listing-live signal (or only add it once the listing is verifiably public); the cutover date is not an availability check.
| icon={store === 'ios' ? 'apple-logo' : 'google-play'} | ||
| className={twMerge( | ||
| 'w-full bg-white px-6 text-button-m hover:bg-white/90 sm:w-52 md:text-button-l', | ||
| isFooter && 'border-n-1 shadow-none hover:shadow-none' |
There was a problem hiding this comment.
MAJOR: New CTA styles still fail the required DS ratchet
Exact-head ds-lint is red: legacyColorClasses rises 0→1, offScaleSpacing 188→190, offScaleRadius 37→39, and hoverNoActiveFiles 44→47. This line contributes the new legacy border-n-1 and an unpaired hover state; the other new CTA files add px-7/gap-7, arbitrary 24px/28px radii, and more hover-only files. Replace the new debt with the current semantic border, spacing/radius tokens and paired active states across the touched CTAs instead of raising the baseline.
| </Button> | ||
| </a> | ||
| {showOtherStore && ( | ||
| <Link prefetch={false} href="/app" className="block text-center text-body-s text-n-1 underline"> |
There was a problem hiding this comment.
MAJOR: Other store routes phones back to the detected store
On iOS the primary button already targets the App Store, but the Other store link goes to /app; that route detects iOS and immediately location.replaces to the App Store again. Android behaves symmetrically, so the alternative CTA cannot reach the other listing (and it also drops the handoff created by the direct store CTA). Point this link at the explicit opposite STORE_URL and preserve the normal click tracking/handoff behavior, or make /app accept an explicit store override.
| } | ||
| const observer = new IntersectionObserver( | ||
| (entries) => { | ||
| if (shown.current || !entries.some((entry) => entry.isIntersecting)) return |
There was a problem hiding this comment.
MINOR: QR impressions still fire below 50% visibility
An IntersectionObserver sends an initial callback even when an element is only slightly visible. With a 0.1 intersection ratio, isIntersecting is true, so this guard fires the impression despite the configured 0.5 threshold. Require entry.intersectionRatio >= 0.5 (and add below/at-threshold cases) before capturing.
| { slug: 'google-play', href: STORE_URL.android, name: 'Peanut on Google Play' }, | ||
| ] | ||
|
|
||
| const storeListings = () => (Date.now() >= MIGRATION_CUTOVER_DATE.getTime() ? STORE_LISTINGS : []) |
There was a problem hiding this comment.
MINOR: Cutover date is frozen into prerendered footer HTML
This Date.now() runs while the statically generated homepage and localized marketing pages are built, not when a visitor requests them. A production deployment built before 2026-12-31 therefore keeps omitting the store links after cutover until another deploy/regeneration, contrary to the stated from-cutover behavior. Make the release state an explicit build/deploy input with a scheduled cutover deploy, or give these routes a bounded revalidation/runtime source that can actually cross the date.
|
Superseded by #3018 (all four folded into one PR per Konrad); branch kept until that merges. |
Summary
Turns the landing page into the app-store funnel for the
pwa-sunsetwindow (TASK-21788, PR 2 of 4 — the page). Every download CTA on the page now ends at a store or at a QR that a phone can scan; nothing routes to/setupor/sendwhile the flag is on. With the flag off the page is unchanged: the flag is false until mount, so SSR and the first client render are today's page byte for byte.Depends on PR 1 (mechanism) for
/app's payload branching and the app-links files. The two were built in parallel from the same brief; where PR 1's component API does not exist ondevyet, this PR added its own equivalents rather than editing PR 1's files — see Divergences.What changed
/appwith the deferred payload ands=landing_hero, the scan hint, the white App Store / Google Play pair, and the content-system subtext; horizontal fromlgup, stacked below. Phone: one full-width white "Download now" with the detected store's mark, an href that starts at/appand upgrades to the store deep link on mount, an "Other store" text link to/app, and the same subtext. No two-button row exists on a phone, so nothing can overflow at 320px.mdup and flag-on only (thecompactArtworkprop), the artwork clamp moves fromcalc(100svh-23rem)tocalc(100svh-29.5rem)and the mascot's fromcalc(100svh-28rem)tocalc(100svh-34.5rem). The desktop lockup is ~164px where the single button was ~76px, and that 88px is what the clamp buys back so the marquee stays in the fold at 1440x900 and 1366x768. Belowmdboth keep today's values: the phone CTA barely changes height, so the laptop clamp there only cut hero artwork (up to ~100px on a 320x568 phone) for nothing.ResizeObserveron#heronow re-runsposition().resizealone missed every height change that leaves the window alone — the CTA swapping in on mount, a late webfont, an image decoding — each of which left the mascot floating off the headline.AppModalProviderwraps the whole page insideLandingPageClient, above the server-rendered slots, and owns a singleScanToDownloadModal. ItsinterceptAppCta(surface, handoff)is the contract every re-pointed CTA calls: desktop opens the modal tagged with the caller's surface and carrying the hand-off, phones bounce to their store with the same hand-off, flag-off returns false and the CTA keeps its old behaviour.landing_ratesanddest=/sendbefore the signed-in branch.'use client'child; with the flag on its href is/appand the click opens the modal (landing_countries).store-listing-v2/capture/shots-out/en/01-qr-scan.pngdownscaled to 360px wide and palette-optimised — 12.7 KB. The 3-up ismd:gap-8 lg:gap-14so the row fits the 736px content box at exactly 768px instead of silently shrinking the QR below its specified 192px.FooterGetTheAppblock on top of the footer chrome, opt-in viashowGetTheAppand passed only by the landing page: heading, hint, the store pair in white-on-black with the border kept and the shadow dropped (the LocaleSwitcher's treatment), and a 192px QR on desktop; one download button on a phone. It rendersnullwith the flag off. The SEO footer's Learn More column gains two crawlable store-listing links fromMIGRATION_CUTOVER_DATEon — every other download CTA is a script-driven bounce a crawler cannot follow, and this footer is a server component on every marketing page, so a date is the only gate it can read.HeroAppLockup,PhoneAppCtaandGetTheAppFoldload throughnext/dynamicwithssr: false. None can render before mount, so nothing is lost, andAppQrCode → QRCodeWrapper → react-qr-codeplus the scanner capture leave the landing page's main chunk for every visitor, flag off included.MIGRATION_SURFACESgains the six keys from the brief.MIGRATION_QR_SHOWNfires once per QR, at 50% visibility, with{surface, hasContext}— so the three QRs on the page are counted where they are seen, not where they mount.LANDING_HERO_QR_FLAG = 'landing-hero-qr'is exported and documented; nothing reads it.migration.otherStoreandmigration.getTheAppin the app catalog (en/es-419/pt-BR, marketing subsets regenerated), plus five URL-locale keys in the marketing catalog —migrationGetTheApp,migrationQrTitle,migrationScanHint,migrationDownloadNow,migrationOtherStoreinen/es-419/es-ar/pt-br. The landing folds read the URL-locale ones so /pt-br stays Portuguese on a Spanish phone.Review round 1 — what changed
Fix-then-ship. Every blocker and major addressed, plus all six minors.
isPwaSunsetOnhonoured its localStorage override only underIS_DEV, so againstnext start(which is what the spec andplaywright.regression.config.tsrun) every "flag on" case rendered the flag-OFF page and passed vacuously. The override is now live on every non-production domain (IS_DEV || BASE_URL !== 'https://peanut.me') — the boundaryisFeatureFlagEnabled'snonProdBypassalready draws — and each flag-on case asserts its lockup is visible, so a silently-inert override fails loudly. Both directions pinned inmigration.utils.test.ts.useDeviceTypebranches on the user agent, not the viewport, and the shared project is a Pixel 7 — so the 1440/1366 cases renderedPhoneAppCta, never the QR lockup the re-tuned constants exist for. The laptop and 768 cases now run under a desktop UA via per-describetest.use, and assert the QR lockup before measuring the marquee.AppModalProviderkept only the surface on desktop, sodest=/sendwas phone-only and the modal had no way to accept one. It now keeps{surface, handoff}and threads it intoScanToDownloadModal, which carries it to both the QR payload and the store anchors.#sticky-button-targeton the flag-on branch could stranddocument.body.style.overflow = 'hidden'with no event able to clear it (a visitor already at fold 10 when the flag resolves — scroll restoration on reload or back-navigation). The scroll effect now bails and cleans up when the flag is on.compactArtworkclamp was applied at every width but tuned for the desktop lockup. It ismd:-scoped now, matching its mascot twin.md:gap-8 lg:gap-14on the 3-up plus a 768x1024 gate case;next/dynamicfor the three lockups;showGetTheAppscoping the footer block to the landing page; the hero Log In link kept and re-pointed at/appinstead of removed;AppStorePairback to the lockedmax-w-[26rem].How verified
pnpm typecheck→ 6 errors, all pre-existingTS2307module-resolution failures in four files this branch never touches (src/utils/web-vitals-shim.ts, its test,src/utils/app-review.ts,src/utils/native-settings.ts), caused by the worktree's symlinkednode_modules. Nothing from the diff appears in the output.pnpm test -- src/components/LandingPage src/components/Migration src/i18n src/utils/__tests__/migration.utils.test.ts→ 31 suites, 321 tests, all passing. New this round: the twoisPwaSunsetOnproduction-build cases, the modal hand-off assertion inAppModalProvider.test.tsx, and the store-listing pair inFooter.test.tsxre-cast as before/after the cutover date.pnpm lint --quiet <touched files>→ clean.prettier --checkover the same set → clean.next dev:curlof/and/pt-brwith the flag off contains zeroapps.apple.com/play.google.comhrefs — the flag-off page no longer advertises the listings.e2e/flows/landing-migration.spec.ts— 4 locales x {1440x900, 1366x768, 768x1024, 390x844} x {flag off, flag on}, desktop UA on the first three, asserting the right lockup rendered (and that neither exists with the flag off), no horizontal overflow anywhere, and the marquee above the fold at both laptop sizes; plus one check that no/setupor/sendhref survives with the flag on. Screenshots go toLANDING_SHOTS_OUT. Not executed in this session — see divergence 17: this box could not keepnext devalive with a browser attached (V8 OOM mid-stream, pages never hydrated), so no flag-on capture from here would be honest evidence. CI runs it against a production build, where the override now works.Divergences
dev, so this PR added its own components and touched none of PR 1's files.AppQrCode(bare framed QR, 160/192px, payload + surface in the url, visibility-gated impression) andAppStorePair(row/column/footer) do what the brief specifies forDownloadQR's new props andStorePair. Reconciling the two branches means collapsing them.MIGRATION_SURFACESgains all six keys verbatim, including the two only PR 1 and PR 4 read, so both branches produce an identical hunk instead of two partial lists that conflict.md). Applying them unconditionally changes the flag-off hero wherever the clamp binds (1366x768 does), which breaks "renders identically to today when the flag is off". Rule (a) wins.strings.heroTapScanand the locked table changes only the CTA slot, so it is untouched.RESOURCE_NAME_OVERRIDESdocuments the rule in place), and the store names are untranslated constants already.StickyMobileCTAis left to PR 1, which lists the WEB → both-stores case in its own mechanism section.NoFeesrendersExchangeRateWidget(router,useSearchParams, a rate fetch); the one changed line is covered by the Playwright no-/sendassertion, and the intercept it calls is unit-tested directly.LANDING_SHOTS_OUTrather than hard-coding a scratchpad path into a repo file.pwa-sunsetlocalStorage override now covers every non-production domain, not just dev — the only way to turn the flag on in a build with no PostHog key, which is what the layout gate runs against. peanut.me is unchanged.ScanToDownloadModalgrew ahandoffprop, andstoreAnchorHref/onStoreAnchorClickgained an optionalhandoffargument. With a hand-off the modal renders PR 2's hand-off-aware QR and pair; without one it rendersDownloadQRverbatim, so the home banner, guest flow and dev registry are untouched. Both helper changes are additive and neither PR 1 nor PR 4 rewrites those functions.IntlCorechange far outside this PR. es-AR is voseo, consistent with its siblings.FooterChromeis shared by /careers, /m/[slug], /shhhhh and the marketing layout; the locked structure scopes the block to fold 12, soshowGetTheAppis passed only byLandingPageContent./apprather than removed — it is the only re-entry path a returning logged-out visitor has, and removing it also changed the CTA column's height on mount.next devplus a browser is OOM-killed on this box (a second session holds another dev server); pages never hydrated, so every flag-on capture would have been a disguised flag-off page. Screenshots taken in that state were deleted rather than filed. Static gates and the SSR check above are what this round is verified by.Full text with the reasoning for each:
run/DIVERGENCES-page.md(round 2 entries are numbered 11–17 there).Flag-off impact
None. Every change is inside the
pwa-sunsetbranch, and the one exception from round 1 — the SEO footer's store-listing links, which rendered on every marketing page in every locale regardless of the flag — is now gated onMIGRATION_CUTOVER_DATE, verified by curling the SSR HTML for/and/pt-br.AppModalProviderwraps the tree and returnsfalsefrom its intercept while the flag is off;FooterGetTheAppreturnsnulland is only mounted on the landing page at all; the hero geometry, the fold-10 body and the countries CTA keep their exact current markup in the flag-off path, whichSendInSecondsBody.test.tsx,CountriesSignUpCta.test.tsx,FooterGetTheApp.test.tsxandFooter.test.tsxeach assert.