Skip to content

feat: TASK-21460 setup flow on the URL stepper, kills the redux setup slice - #2949

Merged
jjramirezn merged 14 commits into
tech-debtfrom
feat/TASK-21460-setup-url-stepper
Sep 4, 2026
Merged

feat: TASK-21460 setup flow on the URL stepper, kills the redux setup slice#2949
jjramirezn merged 14 commits into
tech-debtfrom
feat/TASK-21460-setup-url-stepper

Conversation

@jjramirezn

@jjramirezn jjramirezn commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lands via the tech-debt integration train (dev is merge-frozen). STACKED on #2917: base is temporarily that PR's branch so this diff shows only the setup delta (the shared stepper and Field are born in #2917). When #2917 merges into tech-debt (delete its branch), GitHub retargets this PR to tech-debt automatically — verify the base after that merge.

Summary

The setup flow's cursor was a redux numeric index into a runtime-filtered step array (PostHog sunset flag filters steps in (setup)/layout.tsx), and previousStep/nextStep clamped silently — the dead-end bug (TASK-21404) and the broken-back bug were the same defect. This PR puts the cursor in the URL as a named screen id and deletes the redux setup slice.

Setup on the shared stepper (TASK-21460):

  • useSetupFlow rebuilt on useFlowStepper from feat: TASK-21816 withdraw on a URL-backed stepper + TASK-21454 Field #2917: ?screen=signup|residence|passkey|… — named ids, never indexes. ?screen=, not ?step=: ?step=signup at /setup entry is an existing contract that skips the invite gate (determineInitialStep), preserved as-is.
  • history: 'push' — a deliberate deviation from the replace default: the signup funnel wants the hardware Back button to walk the steps, which is the contract the old useSetupStepUrlSync pushState mirror established. That hook (132 LoC + 169 LoC of tests) is deleted; the stepper owns back-nav.
  • Point of no return: once a step with showBackButton: false renders (e.g. sign-test-transaction — the passkey is already registered), guards refuse every earlier step and bounce forward, so a history pop cannot re-enter the forms.
  • No silent clamps: end-of-list is explicit in handleNext; an unknown target id is a no-op, not an index write.
  • SetupFlowProvider mounts at the (setup) layout — flow-scoped, same shape as the withdraw provider in feat: TASK-21816 withdraw on a URL-backed stepper + TASK-21454 Field #2917. It holds what the URL cannot: the filtered step list (a runtime decision), the typed username/residence answers, loading/animation direction.

Redux setup slice deleted (the only slice with real structure — 8 read sites re-homed):

  • Step list + typed answers → SetupFlowContext.
  • showIosPwaInstallScreenuseIosPwaInstallGate: a sessionStorage latch behind useSyncExternalStore, because the writer ((setup) layout) and the reader ((mobile-ui) layout) live in different route groups with separate providers. Session-scoped on purpose; storage-unavailable fails closed (wall never arms).
  • inviteCode/inviteTypesrc/utils/invite-stash.ts: the cookie was already the durable store (the hand-off must survive the PWA-install hop); the redux mirror is gone, the cookie is now the single owner. Consumers: claim/pot/invite CTAs stash, useZeroDev registration reads, authContext clears.
  • useSetupStepAnalytics (new, src/features/setup/): the step-view PostHog events the old URL-sync hook fired, extracted unchanged.

Task

TASK-21460 (setup rebuild on the URL stepper, absorbs TASK-21404's dead-end/back bugs). The full redux deletion (store, remaining trivial slices, deps) is TASK-21462 — next PR, not this one.

Deleted

  • src/redux/slices/setup-slice.ts, src/redux/types/setup.types.ts, the setup entries in store.ts/hooks.ts/constants.
  • src/hooks/useSetupStepUrlSync.ts + its test file (the stepper owns the URL now).

Risks / breaking changes

  • Blast radius is the whole signup funnel — the first screens every new user sees. Screen order, entry determination (resolveSetupEntryStep), the existing-session interstitial, invite-gate skip via ?step=signup, and the sunset-flag filtering are preserved; what changed is who owns the cursor.
  • ?screen= is a new URL param; deep-linking into a mid-flow screen without the flow's memory bounces to a live screen via the stepper guards instead of rendering a dead one.
  • The iOS PWA install wall latch moved redux→sessionStorage: a hard refresh mid-session now keeps it (before: lost with the store), a new session re-derives it. Small deliberate improvement.
  • No backend changes.

QA

  • npm run typecheck clean · prettier clean · npm run build green.
  • Jest: 5,415 passing, 1 failing — the documented pre-existing add-money-states › loaded EVM deposit failure (fails identically on pristine dev; see feat: TASK-21816 withdraw on a URL-backed stepper + TASK-21454 Field #2917's QA note).
  • New/rewritten: useSetupFlow (guard bounces, explicit end-of-list, point-of-no-return, direction), useIosPwaInstallGate (cross-layout latch + storage-unavailable), invite stash round-trip via useZeroDev invite-onboarding tests, Residence/SignTestTransaction view tests moved off the redux store, InvitesPage/JoinWaitlistPage/ForceIOSPWAInstall re-pointed.
  • Nutcracker: setup scenarios drive /setup routes this PR preserves; classifications unchanged.

The step cursor is a named screen id in the URL (?screen=signup) on the
shared useFlowStepper — replacing the redux numeric index into the
runtime-filtered step array whose next/previous clamps silently
dead-ended (TASK-21404). history:'push' keeps the deliberate setup
contract: the browser/hardware Back button walks the steps; a step with
showBackButton=false is a point of no return — earlier screens' guards
bounce a history pop back. Entry stays owned by resolveSetupEntryStep
and REPLACES any stale ?screen= on fresh load. The pushState mirror
(useSetupStepUrlSync) is retired; its analytics live on in
useSetupStepAnalytics.

The setup slice dissolves by field:
- steps/direction/isLoading/username/residence → SetupFlowProvider,
  mounted at the (setup) layout (flow-scoped, TASK-21816 pattern).
- inviteCode/inviteType → cookies (invite-stash) — they are written from
  payment/claim/invite surfaces and read at registration, and the cookie
  is the copy that already survived the PWA-install hop.
- showIosPwaInstallScreen → useIosPwaInstallGate (sessionStorage) — the
  cross-layout latch (setup arms it, mobile-ui reads it).
- telegramHandle was dead state: no writer anywhere; dropped.
- useResidenceRestrictions reads the during-signup answer via
  useOptionalSetupFlow (Profile/Home consumers have no provider).

useFlowStepper grows a history option ('replace' default per design.md;
'push' for this flow) and a per-call history override on goTo (entry
replaces). TASK-21460.
@notion-workspace

Copy link
Copy Markdown

@vercel

vercel Bot commented Sep 3, 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 4, 2026 10:56am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 3, 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: ab5d8981-29f8-4f45-b599-aabb4baf637e

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.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7127.63 → 7187.1 (+59.47)
Findings: +12 net (+263 new, -251 resolved)

🆕 New findings (263)

  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 177, MI 51.05, SLOC 688
  • critical complexity — src/components/AddWithdraw/DynamicBankAccountForm.tsx — CC 146, MI 51.99, SLOC 442
  • critical complexity — src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 130, MI 56.08, SLOC 385
  • critical complexity — src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 110, MI 47.03, SLOC 421
  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 105, MI 47.78, SLOC 495
  • critical complexity — src/components/Invites/InvitesPage.tsx — CC 88, MI 55.22, SLOC 211
  • critical complexity — src/features/withdraw/useWithdrawRootFlow.ts — CC 81, MI 51.08, SLOC 311
  • critical complexity — src/components/Invites/JoinWaitlistPage.tsx — CC 78, MI 56.16, SLOC 292
  • critical complexity — src/components/Setup/Views/Residence.tsx — CC 76, MI 60.63, SLOC 240
  • critical complexity — src/features/withdraw/useBridgeOfframpFlow.ts — CC 70, MI 48.43, SLOC 364
  • critical complexity — src/components/Claim/Link/SendLinkActionList.tsx — CC 67, MI 55.4, SLOC 213
  • critical complexity — src/app/(setup)/setup/page.tsx — CC 57, MI 53.64, SLOC 237
  • critical complexity — src/app/(mobile-ui)/dev/ds/_components/nav-config.ts — CC 1, MI 27.52, SLOC 218
  • critical complexity — src/dev/fixtures/registry.ts — CC 1, MI 22.1, SLOC 331
  • high hotspot — src/app/(mobile-ui)/withdraw/manteca/page.tsx — 85 commits, +920/-504 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 68 commits, +653/-864 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/crypto/page.tsx — 58 commits, +740/-341 lines since 6 months ago
  • high hotspot — src/components/AddWithdraw/AddWithdrawCountriesList.tsx — 53 commits, +773/-532 lines since 6 months ago
  • high hotspot — src/context/authContext.tsx — 53 commits, +334/-209 lines since 6 months ago
  • high hotspot — src/hooks/useZeroDev.ts — 53 commits, +648/-392 lines since 6 months ago

…and 243 more.

✅ Resolved (251)

  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 160, MI 51.38, SLOC 620
  • src/components/AddWithdraw/DynamicBankAccountForm.tsx — CC 150, MI 51.87, SLOC 464
  • src/app/(mobile-ui)/withdraw/page.tsx — CC 121, MI 53.12, SLOC 366
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 121, MI 56.24, SLOC 375
  • src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 110, MI 46.95, SLOC 425
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 106, MI 53.06, SLOC 395
  • src/components/AddWithdraw/AddWithdrawRouterView.tsx — CC 98, MI 58.34, SLOC 275
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 95, MI 49.41, SLOC 439
  • src/components/Invites/InvitesPage.tsx — CC 88, MI 54.93, SLOC 217
  • src/components/Invites/JoinWaitlistPage.tsx — CC 78, MI 56.15, SLOC 292
  • src/components/Setup/Views/Residence.tsx — CC 76, MI 60.44, SLOC 244
  • src/components/Claim/Link/SendLinkActionList.tsx — CC 67, MI 55.15, SLOC 218
  • src/app/(setup)/setup/page.tsx — CC 63, MI 53.98, SLOC 255
  • src/app/(mobile-ui)/dev/ds/_components/nav-config.ts — CC 1, MI 27.8, SLOC 213
  • src/dev/fixtures/registry.ts — CC 1, MI 22.57, SLOC 319
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — 79 commits, +742/-443 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 64 commits, +594/-316 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — 53 commits, +567/-292 lines since 6 months ago
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx — 52 commits, +734/-504 lines since 6 months ago
  • src/context/authContext.tsx — 52 commits, +332/-207 lines since 6 months ago

…and 231 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/features/withdraw/useBridgeOfframpFlow.ts 0.0 11.4 +11.4
src/features/withdraw/useWithdrawRootFlow.ts 0.0 9.6 +9.6
src/features/withdraw/views/InitialWithdrawView.tsx 0.0 8.3 +8.3
src/features/withdraw/views/ConfirmWithdrawView.tsx 0.0 7.6 +7.6
src/features/withdraw/views/WithdrawBankReviewView.tsx 0.0 7.5 +7.5
src/features/withdraw/views/PixKeySendView.tsx 0.0 6.9 +6.9
src/features/withdraw/WithdrawFlowContext.tsx 0.0 6.7 +6.7
src/features/withdraw/views/WithdrawMethodView.tsx 0.0 6.7 +6.7
src/features/withdraw/views/WithdrawAmountView.tsx 0.0 6.7 +6.7
src/features/withdraw/useMantecaAmountSeed.ts 0.0 6.2 +6.2
src/features/withdraw/amount-validation.ts 0.0 5.6 +5.6
src/features/withdraw/WithdrawRoot.tsx 0.0 5.3 +5.3
src/hooks/useFlowStepper.ts 0.0 5.0 +5.0
src/features/setup/SetupFlowContext.tsx 0.0 4.8 +4.8
src/app/(mobile-ui)/dev/ds/primitives/field/page.tsx 0.0 4.4 +4.4
src/features/setup/useSetupStepAnalytics.ts 0.0 4.1 +4.1
src/components/0_Bruddle/Field.tsx 0.0 3.9 +3.9
src/features/withdraw/routes.ts 0.0 3.6 +3.6
src/features/withdraw/useWithdrawAmount.ts 0.0 3.3 +3.3
src/hooks/useIosPwaInstallGate.ts 0.0 3.1 +3.1

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 5450 ran, 0 failed, 0 skipped, 1.6m

📊 Coverage (unit)

metric %
statements 74.1%
branches 60.5%
functions 67.0%
lines 75.1%
⏱ 10 slowest test cases
time test
4.0s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.7s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › the change cooldown shows its date and blocks changing to another country, not re-saving
1.6s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › saves the declared residence, refetches, and closes
1.3s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › moving to a country in neither slot leaves the second document alone
1.2s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › swaps from the server value with no device mirror at all
1.2s src/components/Setup/Views/__tests__/Residence.test.tsx › lists sanctioned countries in the selector so residents can answer truthfully
1.1s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › promoting the second document country swaps the pair instead of dropping one
1.0s src/utils/__tests__/crisp.test.ts › resets the native session on logout once support has been opened
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
1.0s src/components/Setup/Views/__tests__/Residence.test.tsx › returns to the selector from a heads-up sub-view
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

1 similar comment
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@jjramirezn
jjramirezn changed the base branch from tech-debt to feat/TASK-21816-withdraw-url-stepper September 3, 2026 16:04
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@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 — changes requested

The URL-stepper migration reintroduces native back-stack bounce behavior and can separate persisted invite codes from their invite type.

Findings

  • BLOCKING · src/hooks/useSetupFlow.ts:60 · Keep native setup transitions out of browser history
    Every setup transition now pushes a WebView history entry, including on native. Native hardware Back is already consumed by useSetupBackHandler, which calls handleBack directly, so these entries are not consumed during the flow; after advancing, backing, and completing setup, stale /setup entries remain and Back from the app can re-enter completed onboarding and bounce to /home. The deleted URL mirror explicitly used replace when isNativeBridge() for this reason. Use replace history on native and push only on web, with a native-history regression test.

  • MAJOR · src/utils/invite-stash.ts:18 · Keep invite code and type on one lifecycle
    The new helper stores inviteCode and inviteType separately, but live paths such as /setup?code, PublicProfile, native/deferred links, and JoinWaitlist cleanup still update only inviteCode; the failed-accept retry also extends only inviteCode for 30 days. Start a PAYMENT_LINK signup, then let the session type cookie expire or replace the code through a direct/native link: readInviteType() defaults to or retains the wrong type and /invites/accept persists a misclassified invite edge. Route every write, clear, and retry through one helper (or one structured cookie), keep both fields on the same lifetime, and test overwrite plus restart-retry cases.

  • MINOR · src/utils/invite-stash.ts:25 · [claude-opus] New persistent inviteType cookie has no test; it can now outlive its flow
    src/utils/invite-stash.ts is new shared, persistent state that feeds invite attribution: readInviteType() supplies the type sent to POST /invites/accept (useZeroDev.ts:72,129), which peanut-api-ts persists on the Invites row (siblings/peanut-api-ts/src/routes/invite.ts:265) and emits on the invite.accepted event (:305). CONTRIBUTING.md:506 — "if code moves money or mutates shared state, it needs a test before merge". There is no test for this module: every spec that touches it (InvitesPage.test.tsx, JoinWaitlistPage.test.tsx, useZeroDev-invite-onboarding.test.tsx, useZeroDev-login-failure.test.tsx) jest.mock('@/utils/invite-stash', ...), so the real read/write path is never exercised.

The exact untested case: invite TYPE used to live in per-session redux and defaulted to DIRECT on every fresh session; it is now a cookie that only stashInvite writes, while five call sites still write the code cookie alone — deferred-link.ts:342, useNativeAppLinks.ts:135, general.utils.ts:787 (inviteFlowUrl on native), PublicProfile.tsx:111, setup/page.tsx:161. So: visit a payment-link invite page (type cookie = PAYMENT_LINK), don't sign up; later arrive through a deferred/deep link that writes only inviteCode, then register — registration now sends type: PAYMENT_LINK for an invite that has nothing to do with that earlier payment link, where the old code sent DIRECT. Nothing catches this today.

Fix: add src/utils/__tests__/invite-stash.test.ts covering the round trip (stash → readInviteCode/readInviteType through the JSON-serialising cookie helpers), the DIRECT fallback for a missing or unrecognised inviteType cookie, and clearInvite() blanking both — and decide whether a code-only cookie write should reset the type (e.g. have the deep-link/deferred writers call stashInvite(code, EInviteType.DIRECT)).

Checked clean

  • Verified the detached worktree head and merge base against the supplied SHAs.
  • Checked exact-head CI; all substantive checks completed successfully.
  • Checked URL entry replacement, invalid-screen fallback, point-of-no-return guards, and setup step analytics.
  • Checked setup context migration for username and residence state and the cross-layout iOS install latch.
  • Checked the sibling API invite contract; the supplied invite type is persisted on the invite edge while inviter validity and app access remain backend-controlled.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

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: 30bf4a38eef4 · Context: repo, sibling · Took 9m (queued 25m)

Comment thread src/hooks/useSetupFlow.ts Outdated
Comment thread src/utils/invite-stash.ts
…d 1)

Native setup transitions now replace instead of push: hardware Back is
consumed by useSetupBackHandler, so pushed WebView entries were never used
during the flow and Back after completing setup popped through stale /setup
screens back into onboarding — the contract the deleted useSetupStepUrlSync
mirror kept via replaceState. Web keeps push. Regression tests pin both
history modes through the nuqs adapter.

The invite code and its type are one value on one lifecycle now: every write
goes through stashInvite (bare-code writers — deferred links, native app
links, /setup?code=, PublicProfile — stash DIRECT, which is what the old
per-session redux default made them mean, and it RESETS a stale type from an
earlier flow), cleanup goes through clearInvite (both cookies), and the
failed-accept retry extends both fields for 30 days instead of the code
alone. inviteFlowUrl is a pure URL builder — its native code-only cookie
write was the drift source, and every caller already stashes with the true
type first. New invite-stash test file runs the real cookie round trip:
stash/read, DIRECT fallback, overwrite-resets-type, retry extension, clear.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

handleNext voids goTo's promise, so the assertion could outrun nuqs's
throttle queue — passed locally, failed on the slower CI runner.
rateLimitFactor 0 + one macrotask makes the write land deterministically.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@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 — changes requested

One blocking setup-entry regression remains: a stale final-screen URL can become locked before entry initialization resets it.

Findings

  • BLOCKING · src/hooks/useSetupFlow.ts:72 · Do not lock a step hidden behind entry loading
    Open /setup?screen=sign-test-transaction in a fresh logged-out session, or reload a copied stale URL. Once the layout supplies steps, this effect records the URL-selected final step as rendered even though SetupPageContent is still returning its loading screen. The entry resolver then tries to replace it with landing/signup, but the newly built point-of-no-return guard rejects every earlier target and rewrites the URL back to sign-test-transaction, leaving the visitor on the terminal screen without prerequisite state or a usable back path. Record the irreversible lock only after the page confirms the step is actually visible (stepRendered), or clear/bypass it during initial entry resolution, and add a stale-final-screen load test.

  • MINOR · src/hooks/useIosPwaInstallGate.ts:25 · [claude-opus] New sessionStorage install-gate latch has no test, and its lifetime changed
    useIosPwaInstallGate replaces the redux showIosPwaInstallScreen field with a sessionStorage latch (peanut.showIosPwaInstallScreen) shared across the (setup) and (mobile-ui) route groups. That is shared mutable state gating a full-screen takeover: src/app/(mobile-ui)/layout.tsx:193 returns <ForceIOSPWAInstall /> instead of the app for every non-public route while the latch is true. The module has no test of its own — src/components/ForceIOSPWAInstall/__tests__/ForceIOSPWAInstall.test.tsx mocks the hook out entirely, so nothing exercises the real read/write.

This is not a pure rename: the storage medium changes the lifetime. Previously the flag lived in redux and was false again on any full page load, so only a live /setup mount could arm it. Now it survives reloads for the whole tab session. The exact untested case: arm the gate from the (setup) layout on iOS/non-PWA (setShowIosPwaInstallScreen(true) in src/app/(setup)/layout.tsx:75), then do a full page load of a (mobile-ui) route — the gate still reads true and the wall renders, where the redux version rendered the app. The complementary case is equally uncovered: setShowIosPwaInstallScreen(false) (ForceIOSPWAInstall's "continue in the browser", and the existing-session disarm at src/app/(setup)/setup/page.tsx:120) must keep the user past the wall for the rest of the session.

Fix: add a small jsdom test over the real module — arm, assert read() is true after a simulated remount, disarm, assert it stays false; plus the private-mode branch where sessionStorage throws and the wall must never arm.

Checked clean

  • P1 is fixed: setup transitions use push history on web and replace history on the native bridge, with exact-head regression tests.
  • P2 is fixed: invite code and type writers, retry extension, and terminal cleanup now use the shared lifecycle helpers.
  • P3 is fixed: invite-stash tests exercise real cookie round trips, overwrite/reset behavior, retry lifetime, and cleanup.
  • Setup context migrations, filtered-step indexing, residence state, iOS PWA latch consumers, and invite-flow call sites were checked for additional regressions.
  • Exact-head CI passed unit, typecheck, eslint, format, design-system lint, and the aggregate ci-success gate.
  • A focused local Jest run could not start because the detached worktree has no installed Jest binary; exact-head CI completed successfully instead.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

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: f9af1c03be22 · Context: repo · Took 7m (queued 7m)

Comment thread src/hooks/useSetupFlow.ts Outdated
The lock derived itself from 'the URL selected this no-back step' as soon as
the layout supplied steps — so a stale terminal URL
(/setup?screen=sign-test-transaction in a fresh session) got locked while the
page still showed its loading screen, and the entry resolver's replacement
bounced off the guard forever. The lock now lives in SetupFlowContext (one
fact across the several useSetupFlow instances) and only the page arms it,
gated on stepRendered — entry resolution done, no interstitial, no modals.
resetSetupFlow disarms it. Tests: armed lock still bounces backward moves,
a stale terminal URL stays replaceable, reset disarms.

Also adds the real-module test for useIosPwaInstallGate: cross-layout
remount hand-off, live-subscriber notify, disarm stickiness, the deliberate
survives-reload lifetime, the documented storage key, and the private-mode
throw branch where the wall must never arm.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@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

All five supplied prior findings are fixed at this head. One minor import-graph regression remains: the shared invite stash reconnects globally mounted native-link handling to general.utils and its heavy catalog graph. Exact-head CI is green.

Findings

  • MINOR · src/utils/invite-stash.ts:2 · Keep the invite stash on the lightweight cookie helper
    ClientProviders mounts useNativeAppLinks on every route, and that hook now imports this stash. Importing the cookie helpers through general.utils reconnects this global/native-startup path to token-registry.consts, wagmi/chains, and the rest of the app utility graph, undoing the explicit cookie-url split that kept the roughly 189 KB catalog graph out. Import getFromCookie and saveToCookie directly from cookie-url.utils.

  • MINOR · src/features/setup/useSetupStepAnalytics.ts:17 · [claude-opus] Signup-funnel analytics lost its only test in the move
    useSetupStepUrlSync.test.tsx was deleted (correctly — the URL-mirroring half is gone), but the analytics half it also covered survives verbatim as useSetupStepAnalytics and now has no test at all: SIGNUP_STEP_VIEWED has only 5 references in the repo, none in a spec. The new useSetupFlow.test.tsx covers the stepper/history/lock contracts, not the capture. The untested case is the nav_type derivation: entering signupresidence must emit nav_type: 'forward', residencesignup must emit 'back', a skip (landingresidence) must emit 'jump', the first render must emit 'initial', and a re-render of the same screenId must emit nothing — the deleted spec asserted every one of these (including the no-double-fire case) and nothing asserts them now. Since the whole flow is a single pageview, this event is the only source for per-screen signup funnels, so a regression here fails silently in PostHog rather than in CI. Fix: port the four analytics assertions from the deleted spec into src/features/setup/__tests__/useSetupStepAnalytics.test.ts. Flagging this as a coverage regression from a deleted passing test, not a new test demand on moved code.

Checked clean

  • P1 is fixed: web setup transitions push history while native bridge transitions replace it, with exact platform-mode regression tests.
  • P2 and P5 are fixed: every invite writer now updates code and type together, retry extends both, terminal cleanup clears both, and the real cookie lifecycle is tested.
  • P3 is fixed: the point-of-no-return lock is context-owned and arms only after the page confirms the no-back step is visible; stale terminal URLs remain replaceable.
  • P4 is fixed: the real sessionStorage install-gate module now covers cross-layout remount, subscriber notification, disarm persistence, and storage-unavailable behavior.
  • Correctness and adversarial passes covered setup entry resolution, filtered-step navigation, point-of-no-return behavior, logout/reset, invite acceptance retry, and iOS install-wall handoff.
  • Security pass found no new auth, secret-exposure, unsafe-navigation, or money-handling defect requiring a dedicated review.
  • Exact-head CI completed green: unit, typecheck, eslint, format, design-system lint, backend baseline, and aggregate ci-success all passed.
  • Focused local execution passed 26 tests across useFlowStepper, useIosPwaInstallGate, and invite-stash; the setup-flow suite could not load from the borrowed dependency tree because lucide-react was absent, while the exact-head CI unit job passed.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

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: 2c9c1072eee1 · Context: repo · Took 15m

Comment thread src/utils/invite-stash.ts Outdated
…atalog graph (Chip round 3)

useSetupStepAnalytics regains the coverage the deleted URL-sync spec carried:
initial/forward/back/jump nav_type derivation and the no-double-fire case —
SIGNUP_STEP_VIEWED is the only per-screen signal in the single-pageview flow.
invite-stash imports the cookie helpers from cookie-url.utils directly so the
globally mounted native-link handler stays off general.utils' token-catalog
graph.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@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 setup-flow, invite-stash, analytics, and iOS gate changes resolve the six supplied findings. Exact-head unit CI still fails because PublicProfile's invite handoff tests were not migrated to the new stashInvite contract.

Findings

  • MINOR · src/components/Profile/components/PublicProfile.tsx:113 · Update the PublicProfile invite-handoff tests
    The unit check fails in all three crediting-door cases because this path now calls stashInvite(code, EInviteType.DIRECT), while PublicProfile.test.tsx still mocks and expects saveToCookie('inviteCode', code). Update that suite to mock @/utils/invite-stash and assert the code/type pair so the changed contract is covered and exact-head CI passes.

Checked clean

  • P1 fixed: setup transitions use push history on web and replace history with a native bridge; focused adapter tests cover both modes.
  • P2 fixed: invite code/type writes, retry extension, and cleanup share the invite-stash lifecycle; no production bare-cookie writers remain.
  • P3 fixed: the no-back lock is armed only after the selected step is visibly rendered, and resetSetupFlow disarms it.
  • P4 fixed: invite-stash imports the lightweight cookie-url helper directly and avoids the general-utils token-catalog graph.
  • P5 fixed: useSetupStepAnalytics has real tests for initial, forward, back, jump, and duplicate suppression.
  • P6 fixed: useIosPwaInstallGate has real tests for remount handoff, live notification, disarm persistence, storage lifetime, and unavailable storage.
  • Reviewed entry-step replacement, runtime-filtered step ordering, browser/native back behavior, point-of-no-return guards, Redux setup-state rehoming, and invite consumers.
  • Exact-head typecheck, ESLint, format, design-system lint, backend baseline, preview, and ownership gates passed; unit failed only in three PublicProfile guest-door assertions tied to this PR's writer migration.
  • The detached worktree had no installed dependencies, so local focused Jest execution was unavailable; exact-head GitHub Actions logs supplied the test evidence.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

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: 6519955346ff · Context: repo · Took 11m

Comment thread src/components/Profile/components/PublicProfile.tsx
…general.utils spy

Round 3 moved invite-stash onto cookie-url.utils directly, so the suite's
general.utils saveToCookie spy stopped seeing the write (caught by CI, not
locally — my full-run summary got truncated). The assertions now target the
semantic API: stashInvite('satoshi','DIRECT') on the crediting paths, not
called on the three no-credit paths.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@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 six supplied findings are fixed at this head. One minor duplicate cleanup remains in logout.

Findings

  • MINOR · src/context/authContext.tsx:300 · Clear the invite stash only once on logout
    Every logout already clears both invite cookies at line 287. Replacing the removed setup reset with another clearInvite() here makes the same two cookie writes a second time, and leaves the surrounding Redux-reset block pretending setup state still exists. Remove this second call and update the block comment to cover only the user and ZeroDev slices.

Checked clean

  • P1 fixed: web setup transitions push while native transitions replace, with per-platform adapter coverage.
  • P2 fixed: production invite-code writers, retry extension, and cleanup now keep code and type on one cookie lifecycle.
  • P3 fixed: the no-back lock is armed by the page only after the selected step is visibly rendered; reset disarms it.
  • P4 fixed: invite-stash imports the lightweight cookie helper directly.
  • P5 fixed: PublicProfile guest-door tests now assert stashInvite for both normal and store-handoff paths.
  • P6 fixed: signup step analytics now has initial, forward, back, jump, and duplicate-suppression coverage.
  • Setup context consumers remain inside the setup provider; the residence hook uses the optional context outside setup.
  • Exact-head unit, typecheck, lint, format, design-system lint, backend-baseline, and bot-approval checks passed; aggregate reporting was still in progress when checked.
  • No new security, privacy, secret, authorization, or money-handling surface was introduced.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

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: 35d18d815c61 · Context: repo · Took 9m

Comment thread src/context/authContext.tsx Outdated
The early clear at the top of logout already wipes both cookies; the second
call was the leftover slot where the setup-slice reset used to live. Comment
now says what the block actually resets.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@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

All six supplied prior findings are fixed at this head, exact-head CI is green, and no new review findings remain.

Checked clean

  • P1 is fixed: setup transitions push browser history on web and replace it on the native bridge, with per-platform adapter coverage.
  • P2 is fixed: invite code and type now share one writer, retry lifetime, and cleanup lifecycle across all production handoff paths.
  • P3 is fixed: the point-of-no-return lock arms only after a no-back step is visibly rendered, and resetSetupFlow disarms it.
  • P4 is fixed: invite-stash imports the lightweight cookie helper directly and keeps the globally mounted native-link path off the token-catalog graph.
  • P5 is fixed: PublicProfile tests mock the invite stash and cover both crediting doors plus all no-credit outcomes.
  • P6 is fixed: logout clears the invite stash once and the Redux-reset comment now matches the surviving user and ZeroDev slices.
  • Correctness and adversarial passes covered entry-step replacement, runtime-filtered step ordering, web/native back behavior, no-back guard recovery, cross-layout iOS install state, invite acceptance retries, and context consumers.
  • The paired API contract remains compatible: telegramHandle is optional and invite acceptance still uses the existing DIRECT and PAYMENT_LINK enum values.
  • Exact-head unit, typecheck, ESLint, format, design-system lint, backend baseline, report, aggregate ci-success, preview, and ownership checks completed successfully.
  • The detached worktree has no installed dependencies, so focused local Jest execution was unavailable; exact-head GitHub Actions supplied the test evidence.

Security review by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only and answers only security, privacy and money, so treat its findings as advice.

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: 82f70823cc40 · Context: repo, peanut-api-ts · Took 9m

@jjramirezn
jjramirezn changed the base branch from feat/TASK-21816-withdraw-url-stepper to tech-debt September 4, 2026 19:02
@jjramirezn
jjramirezn marked this pull request as ready for review September 4, 2026 19:02
@jjramirezn
jjramirezn merged commit 45b4c73 into tech-debt Sep 4, 2026
21 of 22 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.

1 participant