Skip to content

fix: pwa-sunset follow-ups — /app in-app bounce, snooze tests, invite handoff - #2689

Merged
jjramirezn merged 4 commits into
devfrom
fix/pwa-sunset-followups
Aug 13, 2026
Merged

fix: pwa-sunset follow-ups — /app in-app bounce, snooze tests, invite handoff#2689
jjramirezn merged 4 commits into
devfrom
fix/pwa-sunset-followups

Conversation

@kushagrasarathe

@kushagrasarathe kushagrasarathe commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

The three open items from the PR #2591 review audit (TASK-21145), none of which block the flag-flip individually but item 1 is strongly recommended before it:

  1. /app inside the native app routed installed users out to the store. Universal links (paths: ["*"]) open peanut.me/app inside the app when an installed user scans a download QR — the launch-week cohort exactly. Now: isCapacitor()router.replace('/home'), loading state while it happens, store redirect and 404 suppressed.
  2. useNotifications snooze conversion had no test. New useNotifications.test.ts pins evaluateVisibility's dismissal logic: legacy notifModalClosed bool converts to a notifModalClosedAt timestamp exactly once, flag-off = closed forever, flag-on + expired 14-day snooze = re-ask, flag-on + fresh snooze = closed, granted permission always hides. (The sunset-gate half of this ask shipped in feat(migration): pwa-sunset surfaces behind PostHog flag (native-app migration) #2591 as migration.utils.test.ts.)
  3. Invite interstitial "Claim your spot" took the scenic route. Now wired to useGuestStoreHandoff like the claim/request/pay guest CTAs: desktop gets the scan-to-download QR modal, phones deep-link their store. Also emits the migration_guest_cta_shown impression (TASK-20939 funnel). Invite cookie/queue bookkeeping still runs first so keep-web signups and post-install link re-taps recover the invite context.

Task

TASK-21145 — pwa-sunset follow-ups

Risks / breaking changes

  • All three changes are inert until the pwa-sunset PostHog flag is ON (useGuestStoreHandoff and the /app page both gate on it; the notifications change is test-only). Flag OFF = today's behavior byte-for-byte.
  • Item 1 changes what an installed user sees when a universal link opens /app in-app: home instead of a store bounce. No web-path change.
  • No backend changes, no cross-repo deploy order.

QA

  • npm test — 232 suites / 2979 tests green, including the 7 new dismissal tests and the new interstitial handoff test.
  • npm run typecheck + npm run build clean.
  • Local override QA: localStorage.setItem('pwa-sunset', 'true') then visit an invite link logged-out → CTA hands off to QR modal (desktop) / store (mobile).
  • ⚠️ Item 1 needs a simulator/device check (universal-link open inside the Capacitor app) — pairs with the TASK-20831 device QA pass; not verifiable in a web sandbox.

Screenshots

N/A (no new visuals) — the interstitial handoff reuses ScanToDownloadModal exactly as shipped and screenshotted on the other guest surfaces in #2591; the /app in-app path renders the existing loading spinner during the redirect and needs a device to capture.

Design notes / accepted trade-offs (review round 1)

Applied from the automated review: isNativeBridge() over isCapacitor() on /app (capacitor-flavored web previews bake NEXT_PUBLIC_CAPACITOR_BUILD with no bridge and still need the store page), guest impression armed only when the CTA actually renders (invalid-invite / dead-bare-link views no longer counted — pinned by test), redirect_uri saved before the store handoff, and stays-closed test assertions hardened against the shared module store.

Declined, with reasons:

  • window.open handoff can be silently blocked in some in-app webviews — pre-existing feat(migration): pwa-sunset surfaces behind PostHog flag (native-app migration) #2591 behavior shared by all 3 guest surfaces (openStoreopenExternalUrl); the fix belongs in that one chokepoint as a follow-up, not per-surface here.
  • Move the /app/home remap into mapDeepLinkPath — the mapper only runs for appUrlOpen events; the page-level guard covers every arrival path, and one implementation beats two.
  • Fold the guest check into useGuestStoreHandoff itself — behavior-preserving refactor across 3 call sites, out of this PR's blast radius; follow-up.

Summary by CodeRabbit

  • New Features

    • Native app users are now redirected directly to the home screen with a full-screen loading experience.
    • Guest invite claims can continue through the appropriate app store while preserving invite and campaign details.
    • Invalid invites are handled more consistently, and guest tracking is limited to valid invite content.
  • Bug Fixes

    • Improved notification dismissal, snoozing, and permission-based visibility behavior.

universal links (paths: ["*"]) open peanut.me/app inside the app when an
installed user scans a download QR — the page then bounced them out to the
store they came from. exactly the launch-week cohort.
the pre-prompt carries three dismissal representations (legacy bool,
timestamp, flag-conditional 14-day snooze) and none of it was tested —
the last open item from the #2591 review rounds.
'Claim your spot' pushed /setup?step=signup during the migration window —
correct end state via the gated landing, but one extra hop and inconsistent
with the claim/request/pay guest CTAs. also gives the invite surface the
migration_guest_cta_shown impression event for the TASK-20939 funnel.
@vercel

vercel Bot commented Aug 13, 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 13, 2026 2:22pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5a6063f7-aee9-4a4e-8e33-f33dbc4183cf

📥 Commits

Reviewing files that changed from the base of the PR and between d670eb7 and 635e6c7.

📒 Files selected for processing (4)
  • src/app/app/page.tsx
  • src/components/Invites/InvitesPage.test.tsx
  • src/components/Invites/InvitesPage.tsx
  • src/hooks/__tests__/useNotifications.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/app/page.tsx
  • src/components/Invites/InvitesPage.tsx
  • src/components/Invites/InvitesPage.test.tsx

📝 Walkthrough

Walkthrough

The app redirects native Capacitor sessions to /home. The invites page intercepts guest claims for store handoff while preserving attribution. Notification tests now cover dismissal, snooze, migration, flags, and permissions.

Changes

Native store flows

Layer / File(s) Summary
Native app route redirect
src/app/app/page.tsx
The app detects native Capacitor sessions, redirects to /home, skips store redirection, and shows a loading state.
Invite CTA store handoff
src/components/Invites/InvitesPage.tsx, src/components/Invites/InvitesPage.test.tsx
Guest claims preserve attribution and invoke store handoff interception. Invalid invite views disable guest impression tracking. Tests verify intercepted claims and invalid invite behavior.

Notification visibility coverage

Layer / File(s) Summary
Notification dismissal and snooze evaluation
src/hooks/__tests__/useNotifications.test.ts
Tests cover default visibility, legacy dismissal conversion, snooze expiry, feature flags, and permission-based suppression.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: ⚪ Minimal · up to 635e6

The changes are targeted to the PWA sunset flow and are covered by added tests and reported validation; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Guest
  participant InvitesPage
  participant useGuestStoreHandoff
  participant Signup
  Guest->>InvitesPage: Claim your spot
  InvitesPage->>InvitesPage: Save invite attribution
  InvitesPage->>useGuestStoreHandoff: Intercept guest CTA
  useGuestStoreHandoff-->>InvitesPage: Return handled state
  InvitesPage-->>Guest: Show handoff modal
  InvitesPage--xSignup: Do not navigate to signup
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: hugo0, jjramirezn

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 three main changes: the native /app redirect, notification snooze tests, and invite handoff.
✨ 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 fix/pwa-sunset-followups

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

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7159.11 → 7161.17 (+2.06)
Findings: +2 net (+22 new, -20 resolved)

🆕 New findings (22)

  • critical complexity — src/components/Invites/InvitesPage.tsx — CC 84, MI 54.71, SLOC 209
  • high complexity — src/app/app/page.tsx — CC 41, MI 64.41, SLOC 78
  • medium high-mdd — src/components/Invites/InvitesPage.tsx:38 — InvitePageContent: MDD 106.0 (uses across many lines from declarations)
  • medium high-dlt — src/components/Invites/InvitesPage.tsx:38 — InvitePageContent: DLT 47 (calls 47 distinct functions — high context load)
  • medium hotspot — src/components/Invites/InvitesPage.tsx — 27 commits, +534/-341 lines since 6 months ago
  • medium high-mdd — src/app/app/page.tsx:36 — SmartStoreRedirect: MDD 26.0 (uses across many lines from declarations)
  • medium method-complexity — src/components/Invites/InvitesPage.tsx:38 — InvitePageContent CC 22 SLOC 94
  • medium method-complexity — src/app/app/page.tsx:36 — SmartStoreRedirect CC 17 SLOC 36
  • medium react-effect-derives-state — src/app/app/page.tsx:48 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/app/app/page.tsx:56 — useEffect with empty deps + setState — derived state anti-pattern
  • low high-mdd — src/components/Invites/InvitesPage.tsx:142 — : MDD 18.9 (uses across many lines from declarations)
  • low high-dlt — src/app/app/page.tsx:36 — SmartStoreRedirect: DLT 18 (calls 18 distinct functions — high context load)
  • low structural-dup — components/Invites/InvitesPage.test.tsx:178 — 17 duplicate lines / 61 tokens with components/Invites/InvitesPage.test.tsx:399
  • low structural-dup — components/Invites/InvitesPage.test.tsx:471 — 17 duplicate lines / 74 tokens with components/Invites/InvitesPage.test.tsx:611
  • low structural-dup — components/Invites/InvitesPage.test.tsx:176 — 16 duplicate lines / 60 tokens with components/Invites/InvitesPage.test.tsx:207
  • low high-dlt — src/components/Invites/InvitesPage.tsx:142 — : DLT 15 (calls 15 distinct functions — high context load)
  • low high-mdd — src/components/Invites/InvitesPage.tsx:158 — : MDD 12.6 (uses across many lines from declarations)
  • low structural-dup — components/Invites/InvitesPage.test.tsx:423 — 12 duplicate lines / 51 tokens with components/Invites/InvitesPage.test.tsx:659
  • low structural-dup — components/Invites/InvitesPage.test.tsx:339 — 11 duplicate lines / 60 tokens with components/Invites/InvitesPage.test.tsx:383
  • low structural-dup — components/Invites/InvitesPage.test.tsx:116 — 7 duplicate lines / 53 tokens with components/Invites/JoinWaitlistPage.test.tsx:61

…and 2 more.

✅ Resolved (20)

  • src/components/Invites/InvitesPage.tsx — CC 80, MI 55.27, SLOC 199
  • src/app/app/page.tsx — CC 36, MI 66.21, SLOC 67
  • src/components/Invites/InvitesPage.tsx:37 — InvitePageContent: MDD 101.5 (uses across many lines from declarations)
  • src/components/Invites/InvitesPage.tsx:37 — InvitePageContent: DLT 45 (calls 45 distinct functions — high context load)
  • src/components/Invites/JoinWaitlistPage.tsx — 27 commits, +459/-200 lines since 6 months ago
  • src/app/app/page.tsx:34 — SmartStoreRedirect: MDD 23.3 (uses across many lines from declarations)
  • src/components/Invites/InvitesPage.tsx:37 — InvitePageContent CC 19 SLOC 88
  • src/app/app/page.tsx:34 — SmartStoreRedirect CC 15 SLOC 30
  • src/app/app/page.tsx:39 — useEffect with empty deps + setState — derived state anti-pattern
  • src/components/Invites/InvitesPage.tsx:125 — : MDD 18.9 (uses across many lines from declarations)
  • components/Invites/InvitesPage.test.tsx:169 — 17 duplicate lines / 61 tokens with components/Invites/InvitesPage.test.tsx:390
  • components/Invites/InvitesPage.test.tsx:462 — 17 duplicate lines / 74 tokens with components/Invites/InvitesPage.test.tsx:602
  • components/Invites/InvitesPage.test.tsx:167 — 16 duplicate lines / 60 tokens with components/Invites/InvitesPage.test.tsx:198
  • src/app/app/page.tsx:34 — SmartStoreRedirect: DLT 16 (calls 16 distinct functions — high context load)
  • src/components/Invites/InvitesPage.tsx:125 — : DLT 15 (calls 15 distinct functions — high context load)
  • src/components/Invites/InvitesPage.tsx:141 — : MDD 12.6 (uses across many lines from declarations)
  • components/Invites/InvitesPage.test.tsx:330 — 11 duplicate lines / 60 tokens with components/Invites/InvitesPage.test.tsx:374
  • components/Invites/InvitesPage.test.tsx:108 — 7 duplicate lines / 53 tokens with components/Invites/JoinWaitlistPage.test.tsx:61
  • src/app/app/page.tsx:34 — SmartStoreRedirect: exported fn missing return type annotation
  • src/components/Invites/InvitesPage.tsx:344 — InvitesPage: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/app/app/page.tsx 7.5 8.9 +1.5
src/components/Invites/InvitesPage.tsx 13.7 14.3 +0.6

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2983 ran, 0 failed, 0 skipped, 50.3s

📊 Coverage (unit)

metric %
statements 66.5%
branches 51.7%
functions 56.8%
lines 67.3%
⏱ 10 slowest test cases
time test
3.7s 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.5s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.4s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
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__/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
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/utils/__tests__/auth-token.test.ts › authReady does not park — hydrates the plain token without an unlock
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 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.

- /app guard: isNativeBridge over isCapacitor (capacitor-flavored web
  builds bake NEXT_PUBLIC_CAPACITOR_BUILD with no bridge — those visitors
  need the store page), fold redirect into the mount effect, skip the
  flag subscription in-app, reuse PeanutLoading coverFullScreen
- invites: arm the guest impression only when the CTA actually renders
  (not the invalid-invite view or dead-bare-link redirect) + pin it;
  save redirect_uri before the store handoff so mid-flow destinations
  survive the round-trip
- notif tests: baseline-true-then-refresh so stays-closed assertions
  can't pass on leftover module-store state; snooze fixtures derive from
  NOTIF_PROMPT_SNOOZE_DAYS
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 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.

@jjramirezn
jjramirezn merged commit 5c7547a into dev Aug 13, 2026
20 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