Skip to content

feat(migration): pwa-sunset surfaces behind PostHog flag (native-app migration) - #2591

Merged
kushagrasarathe merged 33 commits into
devfrom
feat/pwa-sunset-migration
Aug 4, 2026
Merged

feat(migration): pwa-sunset surfaces behind PostHog flag (native-app migration)#2591
kushagrasarathe merged 33 commits into
devfrom
feat/pwa-sunset-migration

Conversation

@kushagrasarathe

@kushagrasarathe kushagrasarathe commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

The PWA→native migration surfaces, all dark behind the pwa-sunset PostHog flag — launch is a flag flip in the PostHog UI, not a deploy. Extracted from the /dev/migration showcase (#2574) and wired into the real flows.

Flag OFF (default, today): no behavior change on any existing surface (pinned by test). For accuracy, the edits that DO ship flag-off: the setup step-filter conditional, the home modal-priority guards, window.posthog exposure (official-snippet parity, enables QA/support console access), overflow-x-clip on the landing shell (fixes a pre-existing mobile horizontal-scroll bug), a one-time notifModalClosedAt migration write for users carrying the legacy dismissal boolean, two lazily-fetched modal chunks on /home (~5-10KB, matches the existing self-gating-modal pattern), and the /app route existing (it 404s until the flag resolves ON).
Flag ON (notice window): post-login "Peanut is becoming an app" modal (3-day snooze, days-to-cutover in copy), get-the-app home-carousel banner (supersedes ios-pwa-install), landing hero CTA → "Download now" (store deep-link on mobile / scan-to-download QR on desktop), setup drops the InstallPWA steps and offers store links, notifications pre-prompt re-asks after 14 days instead of never.
Flag ON + past MIGRATION_CUTOVER_DATE: full-screen sunset block on both the app and setup layouts; ?keep-web=<token> (support-distributed) sets a 90-day bypass cookie.
Native app only: review prompt, once ever, after the user has a transaction — "Love it" → store review page, "Could be better" → support drawer.

Tasks

  • Contributes to TASK-20912 — PostHog feature-flag scaffold: pwa-sunset flag constant + useMigrationFlag() hook (hydration-safe wrapper over the existing useFeatureFlags); everything else in this PR gates on it.
  • Contributes to TASK-20826 — Web download-app modal: post-login "Peanut is becoming an app" prompt on /home, device-aware (store CTA on mobile, inline QR on desktop), 3-day "Remind me later" snooze, top of the home modal-priority chain.
  • Contributes to TASK-20827 — 30-day sunset block: full-screen SunsetScreen replaces the app (and /setup) once MIGRATION_CUTOVER_DATE passes with the flag on; V1 global-date constant as agreed.
  • Contributes to TASK-20828 — Support bypass link: ?keep-web=<token> persists a 90-day cookie via the shared useKeepWebBypass hook and skips the sunset block on every gated layout; FE-only V1 with a static support-distributed token.
  • Contributes to TASK-20600 — Store links on landing + setup: landing hero CTA becomes "Download now" (store deep-link on mobile, scan-to-download QR on desktop) and the setup landing step offers store buttons.
  • Contributes to TASK-20830 — Retire the PWA install step: setup drops pwa-install/android-initial-pwa-install/unsupported-browser and the post-setup iOS force-install screen while the flag is on (latched per setup session to avoid mid-flow resets).
  • Contributes to TASK-20829 — Open-in-app banner: app-install home-carousel CTA (supersedes the ios-pwa-install CTA), store on mobile / QR modal on desktop, with the carousel's existing dismiss-cooldown machinery.
  • Contributes to TASK-20771 — Push opt-in pre-prompt: migration-era "Get money alerts" copy (flag-gated, all 3 locales) and "Not now" now snoozes 14 days instead of dismissing forever (legacy notifModalClosed converts to a snooze on read).
  • Contributes to TASK-20598 — Review nudge: native-only "Loving Peanut so far?" asked once ever after the user has a transaction; "Love it" deep-links the store review page, "Could be better" opens the support drawer so bad reviews never reach the store.
  • Contributes to TASK-20939 — Migration funnel analytics: MODAL_SHOWN/DISMISSED/CTA_CLICKED with new migration_download/app_review types plus migration_sunset_viewed, migration_store_cta_clicked, migration_qr_shown, migration_keep_web_used, all carrying surface/store props — instrumented inside each component.

Risks / launch blockers (not merge blockers)

  • Store URLs are placeholders — real App Store numeric id + Play listing must land in migration.consts.ts before flag-on (iOS ?action=write-review needs the numeric id too).
  • MIGRATION_CUTOVER_DATE is a placeholder constant (2026-12-31) — set the real date before flag-on; moving it requires a deploy (accepted V1 trade-off, upgrade path = flag payload).
  • The pwa-sunset flag must be created OFF in PostHog (project 138913) before merge — an undefined flag fails closed, so even that is safe.
  • Flag-off regression surface: see the accurate list in the Flag OFF paragraph above — behavioral risk concentrates in the setup step filter and home modal guards; the rest are inert or cosmetic-fix changes. Pinned by the gating test (flag-off renders nothing) plus the new shouldShowSunsetBlock matrix tests.

Design notes / accepted trade-offs

  • useMigrationFlag returns false until mounted — posthog serves cached flags synchronously for returning visitors, so render-time reads would hard-fail hydration against flag-off SSR HTML on prerendered surfaces (review finding).

  • The setup step filter latches the flag at its first run instead of reacting to the async flag load — a mid-load flip re-dispatched setSteps and reset a mid-flow user to the landing step (review finding). First-ever visitors in the seconds before flags cache get the legacy flow; returning visitors are correct immediately.

  • The review prompt stamps reviewPromptShownAt on interaction, not on show, so a priority-wrapper unmount can't burn the once-ever ask unseen.

  • The notifications pre-prompt 14-day re-ask is bounded by the flag lifecycle: flag deleted post-launch (feature-gates doctrine) → behavior returns to closed-forever.

  • Post-cutover guest story (V1): a guest hitting a link → setup is blocked with the download screen; after install, re-tapping the original link opens it in-app via universal links (invite code + redirect intact). The polished no-re-tap story is the deferred deep link (feat(native): deferred deep linking through the store install (TASK-20772) #2560) — its deferred-link utils live on main and aren't in dev yet, so the handoff isn't wired. Follow-up after the main→dev back-merge: attach the payload in openStore() (single chokepoint, upgrades every migration surface at once).

  • Reviewer flag (follow-up, not this PR): useNotifications.ts now carries three dismissal representations (legacy notifModalClosed bool + notifModalClosedAt timestamp + flag-conditional snooze) in a historically fix-prone file — standing deep-dive candidate to consolidate once the migration flag is deleted.

  • Cutover date = code constant, not flag payload (one async source, not two).

  • keep-web = one static shared FE token — courtesy escape hatch, not a security control; per-user tokens need a BE endpoint.

  • Review prompt "good moment" V1 = has-a-transaction + home visit (shares HomeHistory's query cache); wiring exact success screens is the upgrade path. Store-page deep link; @capacitor-community/in-app-review if conversion matters.

  • Landing hero override is English-only like the rest of that surface; the permanent label change goes through the content system post-cutover, deleting the override.

  • Legacy notifModalClosed: true converts to a snooze-from-now on read (same trick as getDismissedCTAs).

  • The sunset gate reads Date.now() at render with no timer: a session left open across the exact cutover moment stays usable until its next navigation/reload. Accepted — the block catches everyone within one interaction.

  • /app is flag-gated with an async-flag wait (posthog callback + 4s timeout) so first-time scanners aren't 404'd before their flags arrive; flag-off it renders notFound().

QA

  1. Flag OFF (default): walk setup (PWA-install step present), home (no new modals/banners), landing (normal hero) — zero visible change.
  2. Flag ON via console posthog.featureFlags.overrideFeatureFlags({ flags: { 'pwa-sunset': true } }) (posthog-js 1.3xx needs the flags wrapper; a flat object is silently ignored): download modal on /home, carousel banner, "Download now" hero, setup shows store links and skips PWA steps; desktop CTAs open the QR modal.
  3. Cutover: with the override on, temporarily set MIGRATION_CUTOVER_DATE in the past → sunset block on every route incl. /setup; ?keep-web=walnut-still-cracks bypasses it and survives reload.
  4. Events land in PostHog Activity with surface/store props.

Screenshots: ✅ embedded below (11 states, all flag-on surfaces). Flag-off renders pixel-identical (pinned by test).

Screenshots

Captured on the branch against the local sandbox (harness debug ribbons at the top of logged-in shots are local-dev chrome, not product UI). Assets live on the pr-assets-2591 orphan branch — delete it after merge.

Download prompt (flag ON, notice window)

Desktop — early phase (celebrate) Desktop — final 14 days (urgency) Mobile — store CTA

Sunset block (flag ON + past cutover)

Desktop (50/50 row) Mobile (50/50 stack, pinned CTA)

New-user funnel (flag ON)

Landing hero (desktop: white store pair) Landing hero (mobile: device store CTA) Sticky CTA (mobile)
Setup desktop (signup closed: QR + Log In) Setup mobile (signup closed: store CTA + Log In)

Home banner + smart link

Get-the-app carousel card (mobile) /app smart link (desktop)
/app smart link (mobile, mid-redirect: loading in the CTA) /app smart link (mobile, settled)

Native nudges (native-only gates — captured by temporarily forcing visibility on the real components in a local run; the gating logic itself is unchanged and covered above)

Notifications pre-prompt (flag-on copy) Review prompt (once ever, in-app)

Behavior on real devices (OS permission dialog, store review page) still needs the simulator/device QA pass (TASK-20831).

Guest claim/request flow under the cutover (public paths bypass the sunset gate — captured with flag ON + cutover past; this PR does not modify these pages. The "Join PEANUT" CTA's app-handoff redesign belongs to the deferred-deep-link work, #2560/TASK-20772)

Guest request page (desktop) Guest request page (mobile) Join PEANUT → scan-to-download modal (desktop)

During the migration window the guest CTA no longer routes into the (closed) web signup: desktop opens the QR modal above, phones deep-link straight to their store (useGuestStoreHandoff, wired into both the pay/request and claim-link CTAs). Native-app guests keep the normal in-app flow.

…20912)

everything in the app-migration ships dark behind the pwa-sunset PostHog
flag so launch is a UI toggle, not a deploy. event names land first so
every surface wires the same funnel (TASK-20939).
…ASK-20826/20827/20828)

the notice window and the cutover are both runtime states of the
pwa-sunset flag: flag on shows the post-login download prompt (3-day
snooze), flag on + past MIGRATION_CUTOVER_DATE swaps the app for the
full-screen sunset block. support hands ?keep-web=<token> to users who
can't install; the 90-day cookie bypasses the block. components are
extracted from the /dev/migration showcase (PR #2574) and localized
because the product surface enforces jsx-no-literals.
…get-the-app banner (TASK-20600/20830/20829)

flag-on stops onboarding new users into a product that dies in 30 days:
setup drops the InstallPWA steps and offers the store instead, the
landing hero CTA becomes Download now (store deep-link on mobile,
scan-to-download QR on desktop), and the home carousel leads with a
get-the-app nudge that supersedes the ios-pwa-install CTA. the (setup)
route group gets its own post-cutover sunset gate since it doesn't
share the mobile-ui layout.
…ge (TASK-20771/20598)

the custom notifications pre-prompt exists so we can re-ask — during the
migration window 'Not now' snoozes 14 days instead of dismissing
forever (legacy notifModalClosed converts to a snooze on read). the
review prompt asks once ever, native only, after the user has a
transaction; happy path deep-links the store review page, unhappy path
lands in the support drawer so bad reviews never reach the store.
flag off must render nothing — that's the whole PR's regression
guarantee — plus capacitor/snooze/cutover suppression and the
remind-me-later write.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: Pro

Run ID: 6a6b4a27-162e-43de-b71d-7a27ec2fc32d

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
📝 Walkthrough

Walkthrough

Adds a feature-flagged PWA-to-native migration flow with download prompts, QR/store CTAs, sunset screens, modal prioritization, review prompts, analytics, preference persistence, localized copy, and post-cutover web gating.

Changes

PWA sunset migration

Layer / File(s) Summary
Migration contracts and shared state
src/constants/migration.consts.ts, src/constants/analytics.consts.ts, src/hooks/useMigrationFlag.ts, src/utils/migration.utils.ts, src/utils/general.utils.ts, src/context/ModalsContext.tsx
Adds migration flags, cutover/store configuration, analytics events, preference timestamps, store navigation, and shared get-the-app modal state.
Download prompts and store flows
src/components/Migration/*, src/components/Migration/__tests__/*, eslint.config.js
Adds QR, store-button, scan-modal, and pre-cutover download-modal components with snooze/cutover gating and tests.
Sunset route and setup surfaces
src/app/(mobile-ui)/layout.tsx, src/app/(setup)/layout.tsx, src/components/Migration/SunsetScreen.tsx, src/components/Setup/Views/Landing.tsx, src/hooks/useKeepWebBypass.ts
Adds post-cutover sunset routing, keep-web bypass handling, setup-screen filtering, and migration download controls.
Home migration modal orchestration
src/app/(mobile-ui)/home/page.tsx, src/hooks/useHomeCarouselCTAs.tsx
Adds migration download, scan-to-download, and review prompts to home modal priority handling and replaces the PWA install CTA during migration.
Landing CTA and prompt behavior
src/components/LandingPage/*, src/components/Migration/ReviewPromptModal.tsx, src/hooks/useNotifications.ts, src/components/Notifications/SetupNotificationsModal.tsx, src/i18n/app/messages/*
Adds migration-aware landing CTAs, native review prompting, timed notification dismissal persistence, migration-aware notification copy, and localized migration strings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: innolope-dev, jjramirezn, hugo0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding PWA sunset migration surfaces behind a PostHog feature flag.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pwa-sunset-migration

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

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 4, 2026 3:30pm

Request Review

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2439 ran, 0 failed, 0 skipped, 43.1s

📊 Coverage (unit)

metric %
statements 62.3%
branches 45.2%
functions 51.8%
lines 62.8%
⏱ 10 slowest test cases
time test
3.9s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
📍 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 Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6792.73 → 6885.96 (+93.23)
Findings: +39 net (+146 new, -107 resolved)

🆕 New findings (146)

  • critical complexity — src/components/Global/Icons/Icon.tsx — CC 87, MI 73.79, SLOC 268
  • critical complexity — src/app/(mobile-ui)/home/page.tsx — CC 84, MI 62.92, SLOC 237
  • critical complexity — src/hooks/useHomeCarouselCTAs.tsx — CC 67, MI 57.33, SLOC 297
  • critical complexity — src/components/Claim/Link/SendLinkActionList.tsx — CC 66, MI 55.59, SLOC 211
  • critical complexity — src/app/(setup)/setup/page.tsx — CC 63, MI 56.02, SLOC 229
  • critical complexity — src/hooks/useNotifications.ts — CC 60, MI 56.31, SLOC 275
  • critical complexity — src/app/(mobile-ui)/layout.tsx — CC 59, MI 54.25, SLOC 111
  • high complexity — src/components/LandingPage/LandingPageClient.tsx — CC 48, MI 55.72, SLOC 172
  • high complexity — src/components/Setup/components/SetupWrapper.tsx — CC 43, MI 56.19, SLOC 89
  • high method-complexity — src/app/(mobile-ui)/layout.tsx:40 — CC 39 SLOC 75
  • high hotspot — src/constants/analytics.consts.ts — 38 commits, +339/-8 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/home/page.tsx — 36 commits, +232/-148 lines since 6 months ago
  • high complexity — src/app/app/page.tsx — CC 36, MI 66.21, SLOC 67
  • high complexity — src/components/LandingPage/hero.tsx — CC 34, MI 59.67, SLOC 125
  • high hotspot — src/hooks/useHomeCarouselCTAs.tsx — 34 commits, +405/-254 lines since 6 months ago
  • high method-complexity — src/app/(setup)/setup/page.tsx:78 — CC 31 SLOC 104
  • high hotspot — src/components/LandingPage/hero.tsx — 31 commits, +283/-198 lines since 6 months ago
  • high hotspot — src/utils/general.utils.ts — 30 commits, +182/-201 lines since 6 months ago
  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 32.43, SLOC 181
  • medium react-long-component — src/app/(mobile-ui)/home/page.tsx:60 — Home is 324 lines — split it

…and 126 more.

✅ Resolved (107)

  • src/components/Global/Icons/Icon.tsx — CC 87, MI 73.88, SLOC 266
  • src/app/(mobile-ui)/home/page.tsx — CC 66, MI 62.54, SLOC 209
  • src/components/Claim/Link/SendLinkActionList.tsx — CC 64, MI 56.03, SLOC 203
  • src/app/(setup)/setup/page.tsx — CC 61, MI 56.19, SLOC 226
  • src/app/(mobile-ui)/layout.tsx — CC 58, MI 55.27, SLOC 101
  • src/hooks/useHomeCarouselCTAs.tsx — CC 58, MI 58.32, SLOC 269
  • src/hooks/useNotifications.ts — CC 55, MI 57.29, SLOC 260
  • src/components/LandingPage/LandingPageClient.tsx — CC 39, MI 55.86, SLOC 145
  • src/app/(mobile-ui)/layout.tsx:36 — CC 38 SLOC 65
  • src/components/Setup/components/SetupWrapper.tsx — CC 37, MI 57.22, SLOC 82
  • src/constants/analytics.consts.ts — 35 commits, +322/-7 lines since 6 months ago
  • src/components/LandingPage/hero.tsx — CC 32, MI 59.41, SLOC 118
  • src/app/(mobile-ui)/home/page.tsx — 31 commits, +164/-141 lines since 6 months ago
  • src/hooks/useHomeCarouselCTAs.tsx — 31 commits, +358/-250 lines since 6 months ago
  • src/constants/analytics.consts.ts — CC 1, MI 32.9, SLOC 174
  • src/hooks/useHomeCarouselCTAs.tsx:74 — useHomeCarouselCTAs: MDD 126.9 (uses across many lines from declarations)
  • src/components/Claim/Link/SendLinkActionList.tsx:71 — SendLinkActionList: MDD 103.6 (uses across many lines from declarations)
  • src/components/LandingPage/hero.tsx:116 — Hero: MDD 86.5 (uses across many lines from declarations)
  • src/hooks/useHomeCarouselCTAs.tsx:135 — : MDD 85.0 (uses across many lines from declarations)
  • src/app/(setup)/setup/page.tsx:25 — SetupPageContent: MDD 78.1 (uses across many lines from declarations)

…and 87 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Migration/MigrationDownloadModal.tsx 0.0 9.0 +9.0
src/components/Migration/ReviewPromptModal.tsx 0.0 8.1 +8.1
src/app/app/page.tsx 0.0 7.5 +7.5
src/hooks/useGuestStoreHandoff.tsx 0.0 5.9 +5.9
src/constants/migration.consts.ts 0.0 5.2 +5.2
src/utils/migration.utils.ts 0.0 5.1 +5.1
src/components/Migration/SunsetScreen.tsx 0.0 5.1 +5.1
src/components/Migration/ScanToDownloadModal.tsx 0.0 5.0 +5.0
src/components/Migration/DownloadQR.tsx 0.0 4.9 +4.9
src/hooks/useKeepWebBypass.ts 0.0 4.6 +4.6
src/components/Migration/StoreBadges.tsx 0.0 4.2 +4.2
src/hooks/useMigrationFlag.ts 0.0 4.2 +4.2
src/components/Migration/StoreButtons.tsx 0.0 4.1 +4.1
src/components/Migration/MigrationHero.tsx 0.0 2.9 +2.9
src/app/(mobile-ui)/home/page.tsx 12.2 14.6 +2.4
src/components/Global/Icons/store-brands.tsx 0.0 2.1 +2.1
src/app/(setup)/layout.tsx 8.3 10.1 +1.8
src/components/Setup/Views/Landing.tsx 7.0 8.5 +1.5
src/hooks/useHomeCarouselCTAs.tsx 12.8 13.8 +1.1
src/components/LandingPage/LandingPageClient.tsx 10.2 11.2 +1.0

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(mobile-ui)/home/page.tsx:
- Around line 270-274: Update the BalanceWarningModal render in the home page to
gate its visible prop with !showMigrationModal, while preserving its existing
saved-state condition. This ensures the migration prompt takes priority even
before MigrationDownloadModal reports visibility.

In `@src/app/`(setup)/layout.tsx:
- Around line 92-98: Update the setup layout’s migration gate around
`SunsetScreen` to process the `keep-web` query token before evaluating cutover
access, reusing the mobile layout’s query-to-cookie handling or shared hook.
Base the post-cutover bypass decision on the resolved persisted token state so a
valid token allows direct `/setup?keep-web=<token>` access.

In `@src/components/Migration/__tests__/MigrationDownloadModal.test.tsx`:
- Around line 66-89: Update the notice-window tests around
MigrationDownloadModal to freeze the clock at a date before the migration
cutover, preventing the “shows for a logged-in web user” case from depending on
the current calendar. In the “recent snooze” test, derive the expired timestamp
from DOWNLOAD_PROMPT_SNOOZE_DAYS plus a small margin instead of hardcoding ten
days, and restore the mocked time after each test.

In `@src/components/Migration/MigrationDownloadModal.tsx`:
- Around line 41-48: Update the useEffect eligibility checks in
MigrationDownloadModal so setVisible(false) runs whenever migrationOn is false,
userId is missing, the app is running in Capacitor, the cutover date has passed,
or the prompt is still snoozed; retain the existing visibility and analytics
behavior for eligible users.

In `@src/constants/migration.consts.ts`:
- Around line 31-37: Remove the shared KEEP_WEB_TOKEN constant from the
client-facing migration constants and replace the keep-web bypass flow with
per-user, short-lived signed tokens issued and validated server-side. Update
consumers of KEEP_WEB_COOKIE and the `/home?keep-web` handling to use server
validation without embedding any reusable secret in the client bundle.
- Line 15: Before enabling the migration flag, replace the placeholder cutover
date in MIGRATION_CUTOVER_DATE with the approved launch date, and update the
associated store URL constants to valid production destinations. Ensure the iOS
review URL includes the required numeric App Store ID, then validate all
affected constants used by migration CTAs and “Love it.”

In `@src/hooks/useHomeCarouselCTAs.tsx`:
- Line 234: Update the denied-notification branch in the callback containing
setIsIosPwaInstallModalOpen so that, when migrationOn is true for a web PWA, it
uses the native download flow instead of opening the iOS PWA install modal. Keep
the existing modal behavior for non-migrated cases and preserve the dedicated
carousel CTA condition in the surrounding logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 09166ca6-c4e1-4b13-bc34-9f83c0affd5a

📥 Commits

Reviewing files that changed from the base of the PR and between a0adc20 and 95bfa3b.

📒 Files selected for processing (25)
  • eslint.config.js
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/app/(setup)/layout.tsx
  • src/components/LandingPage/LandingPageClient.tsx
  • src/components/LandingPage/hero.tsx
  • src/components/Migration/DownloadQR.tsx
  • src/components/Migration/MigrationDownloadModal.tsx
  • src/components/Migration/ReviewPromptModal.tsx
  • src/components/Migration/ScanToDownloadModal.tsx
  • src/components/Migration/StoreButtons.tsx
  • src/components/Migration/SunsetScreen.tsx
  • src/components/Migration/__tests__/MigrationDownloadModal.test.tsx
  • src/components/Setup/Views/Landing.tsx
  • src/constants/analytics.consts.ts
  • src/constants/migration.consts.ts
  • src/context/ModalsContext.tsx
  • src/hooks/useHomeCarouselCTAs.tsx
  • src/hooks/useMigrationFlag.ts
  • src/hooks/useNotifications.ts
  • src/i18n/app/messages/en.json
  • src/i18n/app/messages/es-419.json
  • src/i18n/app/messages/pt-BR.json
  • src/utils/general.utils.ts
  • src/utils/migration.utils.ts

Comment thread src/app/(mobile-ui)/home/page.tsx
Comment thread src/app/(setup)/layout.tsx Outdated
Comment thread src/components/Migration/__tests__/MigrationDownloadModal.test.tsx
Comment thread src/components/Migration/MigrationDownloadModal.tsx
Comment thread src/constants/migration.consts.ts
Comment thread src/constants/migration.consts.ts
Comment thread src/hooks/useHomeCarouselCTAs.tsx
- keep-web param now honored on /setup too (shared useKeepWebBypass hook)
- balance-warning modal render-gated behind the migration prompt (effect
  ordering race)
- download modal clears state on every ineligible path (flag flip-off
  mid-session)
- denied-notification fallback routes to the app download during the
  migration window instead of the retiring PWA install modal
- gating tests frozen 30 days pre-cutover and snooze expiry derived from
  the constant (calendar-independent)
…et, prompt burn, flag-off copy leak

- useMigrationFlag returns false until mounted: posthog serves cached
  flags synchronously, so render-time reads disagreed with flag-off SSR
  HTML on prerendered surfaces (landing/setup) and hard-failed hydration
- setup step filter latches the flag at first run instead of reacting to
  the async flag load, which re-dispatched setSteps and yanked mid-flow
  users back to the landing step
- review prompt stamps reviewPromptShownAt on interaction, not on show —
  a priority-wrapper unmount was burning the once-ever ask unseen
- notifications pre-prompt copy is now flag-gated (migrationSetupTitle/
  Description keys); flag off keeps today's copy byte-for-byte
- post-signup manager unmounts while the migration prompt shows, with a
  stuck-visibility clear so lower-priority modals aren't suppressed
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/Migration/ReviewPromptModal.tsx (1)

39-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear the review modal when eligibility is lost.

If migrationOn, userId, native status, or transaction eligibility changes after display, this early return leaves visible true. Mirror MigrationDownloadModal and call setVisible(false) on ineligible paths.

Proposed fix
 useEffect(() => {
-    if (!migrationOn || !userId || !isCapacitor() || !hasTransacted) return
-    if (getUserPreferences(userId)?.reviewPromptShownAt) return
+    if (
+        !migrationOn ||
+        !userId ||
+        !isCapacitor() ||
+        !hasTransacted ||
+        getUserPreferences(userId)?.reviewPromptShownAt
+    ) {
+        setVisible(false)
+        return
+    }
     setVisible(true)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Migration/ReviewPromptModal.tsx` around lines 39 - 44, Update
the eligibility handling in the ReviewPromptModal useEffect so every ineligible
path—migrationOn, userId, native status, or hasTransacted being false—calls
setVisible(false) before returning. Preserve the existing preference check,
display behavior, and analytics capture for eligible users.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/useKeepWebBypass.ts`:
- Around line 19-23: Replace the client-side KEEP_WEB_TOKEN comparison in the
useKeepWebBypass flow with server- or edge-side validation of a signed or opaque
bypass token. Have the validated endpoint set an HttpOnly, Secure bypass cookie,
and ensure protected-route enforcement checks that cookie before serving
content; remove reliance on the bundled KEEP_WEB_TOKEN and client-written
cookie.

---

Outside diff comments:
In `@src/components/Migration/ReviewPromptModal.tsx`:
- Around line 39-44: Update the eligibility handling in the ReviewPromptModal
useEffect so every ineligible path—migrationOn, userId, native status, or
hasTransacted being false—calls setVisible(false) before returning. Preserve the
existing preference check, display behavior, and analytics capture for eligible
users.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: da9dfb88-c779-4369-aacf-89b6fd90f84c

📥 Commits

Reviewing files that changed from the base of the PR and between 95bfa3b and ef7ab3c.

📒 Files selected for processing (14)
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/app/(setup)/layout.tsx
  • src/components/Migration/MigrationDownloadModal.tsx
  • src/components/Migration/ReviewPromptModal.tsx
  • src/components/Migration/__tests__/MigrationDownloadModal.test.tsx
  • src/components/Notifications/SetupNotificationsModal.tsx
  • src/hooks/useHomeCarouselCTAs.tsx
  • src/hooks/useKeepWebBypass.ts
  • src/hooks/useMigrationFlag.ts
  • src/hooks/useNotifications.ts
  • src/i18n/app/messages/en.json
  • src/i18n/app/messages/es-419.json
  • src/i18n/app/messages/pt-BR.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/i18n/app/messages/en.json
  • src/i18n/app/messages/pt-BR.json

Comment thread src/hooks/useKeepWebBypass.ts
@kushagrasarathe
kushagrasarathe marked this pull request as ready for review July 30, 2026 17:05
@kushagrasarathe
kushagrasarathe requested review from Hugo0 and removed request for Hugo0 July 30, 2026 17:05
@notion-workspace

Copy link
Copy Markdown

@notion-workspace

Copy link
Copy Markdown

Web: download-app modal

@notion-workspace

Copy link
Copy Markdown

PWA 30-day sunset block

@notion-workspace

Copy link
Copy Markdown

PWA support bypass link

@notion-workspace

Copy link
Copy Markdown

@notion-workspace

Copy link
Copy Markdown

Retire PWA install step

@notion-workspace

Copy link
Copy Markdown

@notion-workspace

Copy link
Copy Markdown

…e, banner logo fix

- sunset desktop copy centered to match the centered store CTA column
- /app smart link rebuilt on the shared MigrationHero (sunset-screen
  visual language) with proper store buttons and a spinner while
  redirecting, replacing the bare centered-links fallback
- get-the-app carousel card drops the yellow icon-container override so
  the mascot renders without a circle background
- hero extracted to MigrationHero now that two screens share it
…p loads inside the CTA

- setup landing during the window (web, no bypass): Sign up is replaced
  by the store block on every device — don't onboard users into a
  product that shuts in weeks. Log In and wallet recovery stay until
  cutover (the lockout Hugo flagged). ?step=signup / invite-code jumps
  land on the gated landing instead of the signup form, closing the
  claim/invite side door. Native app unaffected.
- /app: the visitor's store button carries the loading state (disabled)
  while the redirect happens and settles to clickable buttons if the
  store never takes over — replaces the floating spinner.
… QR, no title-block gap, centered desktop copy

the 'Get the Peanut app' label stays only on desktop where it explains
the QR; a lone store button explains itself. the wrapper's fixed
md:max-h-48 title block left a large gap above the QR — dropped and
copy centered on desktop, scoped to the sunset landing so every legacy
setup screen keeps its layout.
a preview's QR pointed scanners at prod and a LAN-served dev build at
localhost — window.location.origin is correct everywhere.
…e underline pile

- apple-logo + google-play brand marks added to the icon registry
  (monochrome currentColor, official silhouettes)
- StoreBadges: the classic black badge pair (Download on the App Store /
  GET IT ON Google Play) replaces the two underlined 'Open X' links
  under the QR, so the modal has one text CTA instead of three
- landing hero gets the badge pair under 'Download now' (standard
  native-app LP pattern); sticky mobile bar stays single-button, no room
- every store CTA now carries its brand mark (sunset/setup buttons, the
  download modal, /app)
…ashes

- 'The Peanut app is here!' with the bang; carousel banner reuses the
  modal's copy instead of 'moving to the app'
- 'Maybe later' -> 'I'll download it later'
- sunset support link -> 'Having trouble downloading the app? Chat with
  our support'
- em-dashes stripped from all user-facing migration copy
- unused qr.openIos/openAndroid keys dropped (StoreBadges carries the
  official English badge text)
…f store badges

purple App Store + stroke Google Play with brand icons, matching the
/app pair — the black two-line badges didn't belong to the design
system.
…, white pair on desktop

phones get a single Download now carrying their store's mark and
deep-linking straight to it; desktop drops the redundant primary and
shows two equal white store buttons (the pink-on-pink pair with a
mismatched big button above read badly). the desktop QR modal trigger
goes with it — the pair deep-links directly, QR still lives on setup
and the download modal. sticky bar picks up the brand mark too.
…et gate extracted + tested

- /app: mounted guard kills the React #418 on phones (redirect state was
  derived from useDeviceType at first render, which is WEB on the
  server); flag gate 404s the page until pwa-sunset resolves ON (waits
  for the posthog flag callback with a 4s timeout so first-time
  scanners aren't misjudged); copy moved to the migration i18n
  namespace in all three locales
- migration.downloadNow is finally consumed: hero mobile CTA and the
  sticky bar translate via the app locale (LatAm-first funnel)
- the sunset-block condition is one shouldShowSunsetBlock predicate
  shared by both layouts instead of two hand-rolled copies, and the
  matrix (flag/cutover/public/native/bypass + dev overrides) is pinned
  by unit tests
…to the app

mockup §03/§08 behavior that was missing: during the migration window a
logged-out web visitor tapping Join Peanut / Continue with Peanut on a
claim or request page no longer routes into a signup that's closed —
desktop opens the scan-to-download QR modal, phones deep-link their
store. One useGuestStoreHandoff hook wired into both guest CTAs
(SendWithPeanutCta + SendLinkActionList); native-app guests keep the
normal in-app flow. New guest_flow analytics surface.
…(TASK-20939)

migration_guest_cta_shown fires once per mount when the Join/Continue
CTA is actually shown to a settled logged-out web visitor during the
window, so guest click-through-rate is computable. Caller passes its
settled guest state to avoid counting the pre-auth flash.
@kushagrasarathe
kushagrasarathe merged commit 91840fb into dev Aug 4, 2026
18 of 19 checks passed
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.

2 participants