Skip to content

fix(native): OTA beta switch off PostHog, Peanut-only receive, claim settles on CLAIMED - #2956

Merged
innolope-dev merged 6 commits into
devfrom
fix/native-ota-receive-claim-settle
Sep 3, 2026
Merged

fix(native): OTA beta switch off PostHog, Peanut-only receive, claim settles on CLAIMED#2956
innolope-dev merged 6 commits into
devfrom
fix/native-ota-receive-claim-settle

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Why

Three fixes found while investigating native "trouble reaching Peanut" reports.

  1. OTA beta switch never appeared, and the gate it hid behind was the wrong shape. The five-tap switch was gated on a beta-ota-channel PostHog flag that was never created, so isFeatureEnabled returned false on every prod device — the switch stayed hidden and no device could self-assign to the Capgo staging channel. Gating the whole reveal is what made a missing flag invisible; eligibility belongs on the join.
  2. Receive screen offered non-Peanut rails to people who already have Peanut. The claim/receive screen showed bank / mercadopago / pix / exchange-wallet options beside the Peanut button, including to recipients whose device we can already identify as a Peanut user's.
  3. Claim screen stuck on "Processing". A user got the "claimed" push notification while the screen kept spinning: the backend marks the SendLink CLAIMED and sends the notification before the on-chain claim.txHash projects, but the UI only settled once the poll observed that hash.

What

1. OTA beta switch: the gesture earns a badge, and the badge gates the join (6a1e1488a3a3f329411ef89b57)

  • The old gate hid the whole switch behind a beta-ota-channel PostHog flag that was never created, so isFeatureEnabled returned false on every prod device: invisible to its own testers, and indistinguishable from exclusion.
  • The fifth tap now claims PEANUT_TEAM and refetches the user before revealing the card; the card reads that badge as permission to join. No dashboard step to forget — the record is created by the act of tapping.
  • The badge gates joining only. The card renders on any native build, and the off switch stays live whether or not the badge is held, so revoking someone mid-beta cannot strand them on beta code.
  • A failed claim still reveals the card, with copy telling the tester to tap again.
  • The badge is a record and a revoke handle, not an access boundary — anyone who performs the gesture awards it to themselves; Capgo's channel self-assignment setting remains the real one. It buys a list of who opted in and a way to take it back.
  • Never rendered: excluded from the profile badge row, the home feed, and the celebration toast. It says "team" and is handed out on a gesture, so showing it would let any customer wear Peanut staff colours in a payments app.
  • Depends on peanut-api-ts#1518 (POST /badge/team), which must deploy first. (TASK-22248)

2. Peanut-only receive screen for recognised users (d0943dd2, revised in f687f9c01)

  • The alternate rails now render only for a recipient we cannot identify as a Peanut user. Recognition = a live session, passkey credentials from an earlier registration (hasKnownDeviceCredentials), or a stored native session (hasNativeSession — the only branch that fires in the WebView, where the passkey cookie is cross-origin-empty).
  • A recipient without a Peanut account keeps every rail, so the published "claim to SEPA/ACH without a Peanut account" flow is unchanged and no product-source update is needed.
  • Recognition resolves in the new useKnownPeanutDevice hook after mount (both reads touch storage — neither can run during SSR or hydration). The unresolved tick counts as recognised and the geo spinner moved inside the rail block, so the Peanut button paints once and nothing is offered then withdrawn.
  • Guest "Continue with Peanut" button and the devconnect event flow are untouched.

3. Settle the claim screen on CLAIMED (927c18cd)

  • Treat status === CLAIMED as terminal success — the same point the backend marks the claim done and notifies — with or without the txHash.
  • The poll reports the hash when present, null when it has yet to project; the success view tracks a claimConfirmed flag instead of gating on the hash. FAILED/CANCELLED and the give-up fallback are unchanged.

Tests

  • useClaimSuccessPolling: updated the CLAIMED-without-hash case to settle instead of poll on; other cadence/failure/ceiling tests unchanged.
  • SuccessClaimLinkView: added a case for CLAIMED-without-hash rendering the success card.
  • useKnownPeanutDevice: new suite — null before the reads land, each credential source in isolation, and a stripped webAuthnKey (post-logout) reading as unknown.
  • SendLinkActionList: new who gets the alternate rails block (unrecognised keeps every rail, logged-in and known-device get Peanut only, nothing shown while recognition is unresolved). The rail tests skipped in d0943dd2 are un-skipped and green.
  • BetaUpdatesCard: four badge-gating cases — an account without the badge cannot join, is told why, a device already on beta can still leave after the badge is revoked, and a badge-holding account sees no eligibility copy.
  • About.view: the tap earns the badge and refetches before revealing; a failed claim still reveals the card; a web tap earns nothing.
  • Local gate: prettier clean, changed files typecheck-clean, affected suites pass.

Notes / risks

  • Money surface. Fetch Chain details from SDK #2 removes bank/mercadopago/exchange as claim destinations for recognised Peanut users only — a logged-in recipient claims into Peanut and withdraws from the app. Recipients with no account are unaffected. An explicit logout clears both credential signals, so that device falls back to the full rail list; passive session expiry does not. hotfix: sdk version update #3 changes when a claim reads as "success" — verified against peanut-api-ts: SendLink.status is set to CLAIMED and processPostClaim (which sends the push) run together, after the claim tx is handled; the failure path writes FAILED via rollbackClaimOnError.
  • The claimed push is sent to the sender; the claimer's screen now settles on the same CLAIMED moment, so both track one signal.
  • Follow-up for backend (not in this PR): processPostClaim awaits the receipt but does not flip FAILED on a revert, so a revert-after-CLAIMED would show success in both the notification and now the UI.

The five-tap beta switch was gated on a `beta-ota-channel` PostHog flag
that was never created, so `isFeatureEnabled` returned false on every prod
device and the switch never appeared — the toast said "Beta updates aren't
enabled for this device" and no device could self-assign to the Capgo
`staging` channel.

The tap gesture already keeps the control off customer devices, and Capgo's
own self-assignment setting is the real access boundary, so the cohort added
nothing but a missing setup step. Drop the flag: five taps now reveals the
switch on any native build. The off switch stays reachable, so a device on
staging can always return to the store bundle.

Remove the now-dead notEnabled toast and its locale strings.
The claim/receive screen offered alternate rails (bank, mercadopago, pix,
exchange/wallet) beside the Peanut button. Hide them so the only way to
receive is on Peanut, matching the app-first direction.

The rail code stays intact behind a SHOW_ALT_RAILS flag (mirrors the file's
existing SHOW_INVITE_MODAL_FOR_DEVCONNECT pattern), so it is a one-line
re-enable. The rail-specific tests are skipped for the same reason, not
deleted. The guest "Continue with Peanut" button and the devconnect event
flow are untouched.
… txHash

The claim success screen stayed on "Processing" until the poll observed the
on-chain claim txHash, but the backend marks the SendLink CLAIMED and sends
the "claimed" notification before that hash projects (and, on native, the
poll GET is often slow to arrive). So a user could get the success push while
the screen kept spinning.

Settle on the same signal that fires the notification: treat CLAIMED as
terminal success, with or without the txHash. The poll reports the hash when
it is already there and null when it has yet to project; the view tracks a
`claimConfirmed` flag instead of gating success on the hash. FAILED/CANCELLED
and the give-up fallback are unchanged.
@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 3, 2026 8:28pm 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: af528f76-a864-44bd-8de8-ec522d29e9dc

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: 7097.51 → 7105.14 (+7.63)
Findings: +1 net (+42 new, -41 resolved)

🆕 New findings (42)

  • critical complexity — src/components/Home/HomeHistory.tsx — CC 97, MI 56.09, SLOC 300
  • critical complexity — src/components/Claim/Link/SendLinkActionList.tsx — CC 69, MI 55.06, SLOC 219
  • high hotspot — src/components/Home/HomeHistory.tsx — 44 commits, +440/-334 lines since 6 months ago
  • high complexity — src/components/Claim/Link/Onchain/Success.view.tsx — CC 38, MI 59.82, SLOC 160
  • medium high-mdd — src/components/Claim/Link/SendLinkActionList.tsx:72 — SendLinkActionList: MDD 110.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/Home/HomeHistory.tsx:59 — HomeHistory: MDD 103.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/Claim/Link/Onchain/Success.view.tsx:32 — SuccessClaimLinkView: MDD 79.7 (uses across many lines from declarations)
  • medium high-dlt — src/components/Home/HomeHistory.tsx:59 — HomeHistory: DLT 55 (calls 55 distinct functions — high context load)
  • medium high-dlt — src/components/Claim/Link/SendLinkActionList.tsx:72 — SendLinkActionList: DLT 50 (calls 50 distinct functions — high context load)
  • medium high-mdd — src/components/Profile/views/About.view.tsx:23 — AboutView: MDD 40.6 (uses across many lines from declarations)
  • medium high-mdd — src/components/Badges/BadgesRow.tsx:38 — BadgesRow: MDD 37.5 (uses across many lines from declarations)
  • medium high-dlt — src/components/Claim/Link/Onchain/Success.view.tsx:32 — SuccessClaimLinkView: DLT 38 (calls 38 distinct functions — high context load)
  • medium high-mdd — src/components/Profile/components/BetaUpdatesCard.tsx:48 — BetaUpdatesCard: MDD 38.2 (uses across many lines from declarations)
  • medium high-mdd — src/components/Home/HomeHistory.tsx:192 — : MDD 28.9 (uses across many lines from declarations)
  • medium method-complexity — src/components/Home/HomeHistory.tsx:59 — CC 26 SLOC 114
  • medium complexity — src/components/Profile/components/BetaUpdatesCard.tsx — CC 23, MI 58.29, SLOC 99
  • medium complexity — src/components/Badges/BadgesRow.tsx — CC 22, MI 65.58, SLOC 91
  • medium complexity — src/components/Claim/Link/Onchain/useClaimSuccessPolling.ts — CC 22, MI 60.05, SLOC 65
  • medium high-mdd — src/components/Home/HomeHistory.tsx:197 — processEntries: MDD 21.7 (uses across many lines from declarations)
  • medium high-mdd — src/components/Home/HomeHistory.tsx:470 — : MDD 21.6 (uses across many lines from declarations)

…and 22 more.

✅ Resolved (41)

  • src/components/Home/HomeHistory.tsx — CC 96, MI 56.1, SLOC 300
  • src/components/Claim/Link/SendLinkActionList.tsx — CC 67, MI 55.15, SLOC 218
  • src/components/Home/HomeHistory.tsx — 43 commits, +435/-333 lines since 6 months ago
  • src/components/Claim/Link/Onchain/Success.view.tsx — CC 36, MI 60.24, SLOC 155
  • src/components/Claim/Link/SendLinkActionList.tsx:67 — SendLinkActionList: MDD 105.1 (uses across many lines from declarations)
  • src/components/Home/HomeHistory.tsx:58 — HomeHistory: MDD 102.2 (uses across many lines from declarations)
  • src/components/Claim/Link/Onchain/Success.view.tsx:32 — SuccessClaimLinkView: MDD 76.4 (uses across many lines from declarations)
  • src/components/Home/HomeHistory.tsx:58 — HomeHistory: DLT 55 (calls 55 distinct functions — high context load)
  • src/components/Claim/Link/SendLinkActionList.tsx:67 — SendLinkActionList: DLT 49 (calls 49 distinct functions — high context load)
  • src/components/Claim/Link/Onchain/Success.view.tsx:32 — SuccessClaimLinkView: DLT 37 (calls 37 distinct functions — high context load)
  • src/components/Profile/components/BetaUpdatesCard.tsx:41 — BetaUpdatesCard: MDD 36.1 (uses across many lines from declarations)
  • src/components/Badges/BadgesRow.tsx:37 — BadgesRow: MDD 35.4 (uses across many lines from declarations)
  • src/components/Home/HomeHistory.tsx:191 — : MDD 28.1 (uses across many lines from declarations)
  • src/components/Profile/views/About.view.tsx:21 — AboutView: MDD 27.8 (uses across many lines from declarations)
  • src/components/Home/HomeHistory.tsx:58 — CC 26 SLOC 114
  • src/components/Profile/components/BetaUpdatesCard.tsx — CC 24, MI 54.5, SLOC 101
  • src/components/Home/HomeHistory.tsx:466 — : MDD 21.6 (uses across many lines from declarations)
  • src/components/Badges/BadgesRow.tsx — CC 21, MI 65.14, SLOC 88
  • src/components/Home/HomeHistory.tsx:196 — processEntries: MDD 21.2 (uses across many lines from declarations)
  • src/components/Claim/Link/Onchain/useClaimSuccessPolling.ts — CC 20, MI 60.3, SLOC 65

…and 21 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/useKnownPeanutDevice.ts 0.0 3.6 +3.6
src/services/peanut-team-badge.ts 0.0 3.0 +3.0
src/components/Profile/components/BetaUpdatesCard.tsx 8.2 7.3 -0.9

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 5661 ran, 0 failed, 0 skipped, 1.9m

📊 Coverage (unit)

metric %
statements 74.1%
branches 59.6%
functions 67.8%
lines 75.1%
⏱ 10 slowest test cases
time test
🐢 9.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Network failure keeps loading while retries remain, then shows the generic error
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_SOURCE_OVER_MONTHLY_CAP fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_RECENT_REFUND fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › User KYC not approved fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › a refused idempotency key tells the user to scan again, not to contact support
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › routes the KYC rejection on its wire code, and does not retry it
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_USER_NOT_PROVISIONED fails fast with copy that names the real cause
4.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › MANTECA_MERCHANT_VOLUME_NEAR_CAP fails fast with copy that names the real cause
3.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
3.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Going offline blames the connection, and reconnecting clears it for the recovered scan
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🖼 Visual diff — 7 screens moved

14 of 62 shots changed · 48 identical · baseline 367da93 → head 11ef89b

worst % screen widths
10.99% long-full-name 320, 430
10.30% profile-edit 320, 430
7.00% hugo-long-username 320, 430
6.15% profile 320, 430
5.84% home 320, 430
2.44% empty-home 320, 430
0.87% reconsent 320, 430
new screens (2)
  • avatar-picker
  • home-avatar

job summary · before/after/diff images — artifact

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

The OTA and claim-settlement fixes are internally consistent, but hiding every alternate receive rail leaves the published Send Link claim-method contract out of sync with the product.

Findings

  • MAJOR · src/components/Claim/Link/SendLinkActionList.tsx:281 · Align the supported Send Link claim methods
    This gate hides the bank option together with Pix, Mercado Pago, and external wallets. The current Send Links product source still lists Peanut account, SEPA, and ACH as supported claim methods and explicitly allows a recipient to claim to SEPA/ACH without a Peanut account. With this change, a recipient following that published flow no longer has a bank action on the receive screen. If Peanut-only receiving is the approved new contract, update the canonical product source and affected help/content in the same rollout; otherwise keep the supported bank method outside this gate.

  • MINOR · src/components/Profile/components/BetaUpdatesCard.tsx:48 · [moonshotai/kimi-k3] Beta OTA channel join now open to any app user
    BetaUpdatesCard.tsx previously gated the toggle on the beta-ota-channel PostHog cohort plus the five-tap gesture; this PR removes the cohort check, so any production user who discovers the gesture can call setBeta(true) and point their device at the staging Capgo channel, receiving unvetted dev builds. The code comments state the real access control is Capgo's channel self-assignment setting, which cannot be verified from this diff — if that setting is ever enabled (the 'closed' toast implies it currently is not), every prod device becomes eligible for staging code with no allowlist. Fix: either keep a server-side allowlist/cohort check before calling setChannel, or confirm in this PR that Capgo self-assignment is permanently disabled for the staging channel and add a CI/config guard so it cannot be flipped on without re-gating the client.

  • MAJOR · src/components/Claim/Link/SendLinkActionList.tsx:59 · [claude-opus] Peanut-only receive contradicts the documented no-account bank claim (SEPA/ACH)
    SHOW_ALT_RAILS = false (src/components/Claim/Link/SendLinkActionList.tsx:59) hides the bank, mercadopago, pix and exchange/wallet cards, so the receive screen offers only "Continue with Peanut". Product truth says otherwise: /home/chip/mono/product/send-links.md has claim_methods: [peanut-account, sepa, ach], recipient_account_required: false # recipient can claim to bank (SEPA/ACH) without a Peanut account, and a whole "Claim Flow — Without Peanut Account" section (steps: open link → select "Claim to bank account" → choose SEPA or ACH). That promise is already published: content/help/send-money-link/en.md sells it in the page description, in "Without a Peanut account" (lines 32-34) and in the FAQ "Can I send to someone in another country?" ("The recipient can claim to a SEPA bank account (Europe) or ACH bank account (US)"). A recipient who follows that help page after this merges finds no such option — only account creation. Note this goes further than the existing maintenance state: the prior commit only greyed the guest bank rail (BankClaimType.GuestBankClaim, BE 503s /bridge/offramp/create-for-guest); the authenticated self-offramp claim (UserBankClaim) worked and is now hidden too. Either scope the flag so the working bank rail survives, or — if Peanut-only is the intended promise — update product/send-links.md (claim_methods, recipient_account_required, the Claim Flow section, the KYC "even for bank claims" wording) and regenerate the affected help/landing pages, so support and SEO stop promising a flow the app no longer has.

  • MINOR · src/components/Claim/Link/tests/SendLinkActionList.test.tsx:145 · [claude-opus] Peanut-only receive screen ships with zero live tests on SendLinkActionList
    Both suites for the component that routes a claim are now entirely skipped — SendLinkActionList.test.tsx:145 (describe.skip('SendLinkActionList — guest claim-to-bank maintenance')) and SendLinkActionList.verificationReason.test.tsx:120 (describe.skip('guest-verification prompt reason')) — and no test replaces them, leaving the component with no active coverage at all. The exact untested case: a guest opening a claim link renders only the "Continue with Peanut" button, and no bank / mercadopago / pix / exchange-or-wallet MethodCard renders. Nothing pins that; a later edit that drops or flips SHOW_ALT_RAILS would silently put guests back into the claim-to-bank flow the backend currently 503s, and CI would stay green. Add one assertion-level test that renders the list for a guest and expects the Peanut CTA present and queryByText/method cards for bank+pix+mercadopago absent; keep it flag-driven so re-enabling the rails un-skips the old suites in the same change.

Checked clean

  • Exact detached head and merge base matched the supplied SHAs; trusted PR metadata matched innolope-dev targeting dev.
  • Claim polling and success rendering were traced against the sibling API: CLAIMED is persisted only after submitAdminCall receives a successful user-operation receipt, while FAILED and CANCELLED remain terminal failures.
  • Peanut-only receive rendering preserves the logged-in Peanut claim button, guest Peanut handoff, and the explicit Devconnect path.
  • OTA enrollment remains native-only and reports Capgo's closed-channel response; removing the cohort gate is flagged for a dedicated security pass because it broadens staging-channel enrollment.
  • CI at the exact head is green for the code, test, native-export, preview, and screenshot gates. The separate review job failed only because its dispatcher could not reach the review-worker socket, not because of this diff.
  • The Notion Lexicon does not define claim-method availability; the current product/send-links source still lists direct SEPA and ACH claims.

Security review by moonshotai/kimi-k3: 1 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: 2 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: 927c18cd23a8 · Context: repo, sibling-api, product, notion · Took 18m (queued 2m)

Comment thread src/components/Claim/Link/SendLinkActionList.tsx Outdated
The Peanut-only receive screen hid the bank option along with pix,
mercadopago and external wallets. But the published Send Links flow says a
recipient may claim to SEPA/ACH *without* a Peanut account, so hiding the
bank rail unconditionally broke a documented path for the people who need
it most: recipients who have no account to claim into.

Gate on recognition instead of on a constant. The rails render only for a
recipient we cannot place as a Peanut user; a live session, passkey
credentials from an earlier registration (hasKnownDeviceCredentials), or a
stored native session (hasNativeSession — the only branch that fires in the
WebView, where the passkey cookie is cross-origin-empty) collapses the
screen to the Peanut option alone.

Recognition is resolved in useKnownPeanutDevice, after mount: both reads
touch storage, so neither can run during SSR or hydration. The unresolved
tick counts as recognised, and the geo spinner moved inside the rail block,
so the Peanut button paints once and nothing is ever offered and withdrawn.

A logged-in recipient no longer gets a bank rail here — they claim into
Peanut and withdraw from the app. An explicit logout clears both signals,
which returns that device to the full rail list.

@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

One prior minor finding remains: the five-tap OTA control again lets any native user self-assign to the staging channel whenever Capgo permits self-assignment. The Send Link rail and test findings are fixed, and CLAIMED settlement matches the backend's post-receipt state transition.

Findings

  • MINOR · src/components/Profile/components/BetaUpdatesCard.tsx:42 · Keep staging OTA enrollment restricted to internal testers
    On any production native install, five taps reveal this card and the only remaining render gate is supported; toggling it calls setBeta(true), so whenever the staging channel permits self-assignment any customer can opt into the channel that receives every dev merge. Capgo's channel setting allows or denies self-assignment globally—it does not distinguish internal testers—and the gesture controls discoverability rather than eligibility. Restore an account/device eligibility check for joining while still rendering the card for devices already on beta so they can leave.

  • MINOR · src/components/Profile/components/BetaUpdatesCard.tsx:37 · [moonshotai/kimi-k3] OTA beta join now open to any native app user
    Removing the PostHog cohort check makes the join toggle render and stay enabled on every native build, so any user who finds the five-tap gesture can switch their device onto the staging Capgo channel — the channel that every merge to dev publishes to — meaning staged/dev OTA bundles become deliverable to arbitrary customer devices. The code itself concedes the helper was never a security boundary and that the sole remaining control is Capgo's server-side self-assignment setting, which this diff does not constrain and reviewers cannot verify. Fix: restore a real gate (enforced server-side or via a signed internal flag), or explicitly verify and document that the staging channel has self-assignment disabled in Capgo so client joins are refused.

Checked and not raised again

  • MINOR · src/components/Claim/Link/SendLinkActionList.tsx:59 · [claude-opus] Peanut-only receive contradicts the documented no-account bank claim (SEPA/ACH) — this review checked it and does not believe it. No task filed.

Checked clean

  • Verified the detached worktree head, supplied base SHA, merge base, PR author, and base ref.
  • Rechecked the Send Link rail gate and device-recognition sources against the canonical no-account SEPA/ACH product contract.
  • Rechecked active SendLinkActionList coverage for unrecognized, logged-in, known-device, and unresolved-recognition states.
  • Traced CLAIMED handling through the UI poller and the backend claim path; the backend waits for a successful user-operation receipt before writing CLAIMED.
  • Reviewed OTA enrollment code, its introducing history, and the native OTA channel policy; staging is an internal-testing channel populated by every dev merge.
  • All exact-head CI checks completed successfully, including unit, typecheck, eslint, format, native-export, analyze, and ci-success.
  • A local focused Jest run was unavailable because the detached worktree has no installed dependencies; no packages were installed into the read-only tree.
  • git diff --check passed.

Security review by moonshotai/kimi-k3: 1 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: f687f9c01c81 · Context: repo, product, engineering, sibling-backend, ci, history · Took 15m

Comment thread src/components/Profile/components/BetaUpdatesCard.tsx
The five-tap switch let any production install self-assign to the Capgo
`staging` channel, which receives every `dev` merge. The gesture controls
discoverability, not eligibility, and Capgo's self-assignment setting is
global — it cannot tell an internal tester from a customer — so the two
together were the whole boundary whenever self-assignment was open.

Gate the join on the `beta-ota-channel` cohort again, with the failure mode
that made the last one useless removed. The cohort now gates the JOIN only:

- The card renders on every native build, as it does today. A missing or
  false flag can no longer hide the switch, which is how the previous gate
  disabled itself for its own testers and looked identical to exclusion.
- A blocked device says why on screen and names the fix, instead of a
  gesture that silently does nothing.
- The off switch stays live whatever the cohort says. Offboarding someone
  mid-beta must not strand them on beta code with no way back to the store
  bundle.

nonProdBypass keeps staging and preview builds open — they are internal by
construction — so the cohort only has to exist for the production binary.

TASK-22248

@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

Clean at the pinned head. No-account claim rails and internal-only OTA enrollment now match their intended contracts, and CLAIMED is only written after a successful receipt.

Checked and not raised again

  • MINOR · src/components/Claim/Link/SendLinkActionList.tsx:233 · [claude-opus] Peanut-only receive contradicts the documented no-account bank claim (SEPA/ACH) — this review checked it and does not believe it. No task filed.
  • MINOR · src/components/Claim/Link/SendLinkActionList.tsx:281 · [claude-opus] Align the supported Send Link claim methods — this review checked it and does not believe it. No task filed.

Checked clean

  • Confirmed the detached HEAD, supplied base, trusted author, base ref, PR intent, and complete 17-file diff.
  • Rechecked P1/P4 against the live Lexicon and product Send Link source: unrecognised recipients retain the no-account claim rails, while logged-in or known-device recipients get the Peanut path.
  • Rechecked P2/P3/P5: production enrollment fails closed on the beta-ota-channel cohort; an ineligible device cannot join, while a device already on beta can still leave.
  • Rechecked P6: SendLinkActionList now covers unrecognised, logged-in, known-device, unresolved-recognition, guest-bank, and user-bank cases.
  • Validated the CLAIMED contract in the backend policy checkout: the admin submission waits for a successful user-operation receipt before the SendLink status flips; FAILED and CANCELLED remain terminal failures.
  • Reviewed device-credential detection, explicit logout cleanup, native session detection, hydration behavior, feature-flag reactivity, OTA offboarding, and one-shot claim polling.
  • Exact-head CI completed successfully, including unit, typecheck, eslint, format, native export, analysis, design-system checks, and preview deployment.
  • Focused local polling and known-device hook suites passed; four component suites could not resolve next-intl from the auxiliary installed dependency tree, so their successful exact-head unit CI run is authoritative.

Security review: did not run — the daily spend cap was reached, so nothing was sent. This review is one reviewer short.

Third opinion by claude-opus: 2 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: a3a3f3294bbc · Context: repo, product, backend · Took 14m

… joining

Replaces the PostHog cohort with the badge the gesture now awards. The cohort
never existed, and a flag that has to be hand-created in a dashboard is the
same setup step that left this switch invisible for months; the badge is
created by the act of tapping, so there is nothing to remember.

The fifth tap claims PEANUT_TEAM and refetches the user before revealing the
card. Revealing first would show a disabled toggle and an "ask for access"
line for a round trip, on the very gesture that just granted access. A failed
claim still reveals the card: a device already on beta needs the off switch,
whatever the network did.

What the badge is: a record of who opted in and a handle to revoke. It is not
an access boundary — anyone who performs the gesture awards it to themselves,
and Capgo's channel self-assignment setting remains the real one. What it
buys over the bare gesture is a list and a way to take it back.

It is never rendered — not in a profile row, not in the home feed, not as a
celebration toast. It says "team" and is handed out on a gesture, so showing
it would let any customer wear Peanut staff colours in a payments app.

Needs peanut-api-ts#PEANUT_TEAM (POST /badge/team) deployed first: without it
the claim fails, the card still reveals, and joining stays blocked with copy
that says to tap again.

TASK-22248

@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

Request changes: the Send Link screen still exposes unsupported direct Pix/Mercado Pago claims and can hide the documented no-account bank path based on another person's device credentials. The self-awarded tester badge also leaves staging OTA enrollment gated by discoverability rather than eligibility.

Findings

  • MAJOR · src/components/Claim/Link/SendLinkActionList.tsx:296 · Align the supported Send Link claim methods
    The canonical Send Link contract allows claims to a Peanut account, SEPA, or ACH and explicitly excludes direct Pix and Mercado Pago claims. This loop still renders every geo-filtered ACTION_METHODS entry, including Pix and Mercado Pago, for signed-out recipients. Selecting either survives signup via step=regional-claim and reaches MantecaReviewStep, which claims the link directly to the Manteca deposit address before withdrawing, rather than claiming to the user's Peanut balance and then spending. Remove Pix/Mercado Pago from this claim-method list and route those recipients through a normal Peanut claim, or deliberately change the product contract and expose the direct rail consistently.

  • MINOR · src/components/Claim/Link/SendLinkActionList.tsx:233 · Do not infer the recipient's account from shared device state
    The no-account SEPA/ACH promise is about the recipient, but this gate hides every alternate rail when the browser profile contains any prior Peanut passkey marker or native session. A recipient with no Peanut account who opens a forwarded link after somebody else used the device (or after that person's session expired without an explicit logout) therefore sees only Continue with Peanut and cannot reach Claim to bank account. Gate the collapse on a live session for the current recipient, or keep a bank/not-you escape hatch when only stale device credentials caused the suppression.

  • MINOR · src/components/Profile/views/About.view.tsx:53 · Keep staging OTA enrollment restricted to internal testers
    P2 and P4 remain the same defect at the final head: the later commit makes the fifth tap call POST /badge/team, refetch the user, and then reveal a switch whose join check trusts that badge. Because any authenticated native user who performs the gesture can mint PEANUT_TEAM, the badge is only a record of discovering the gesture, not an internal-tester cohort. Whenever Capgo self-assignment is opened for a supervised test window, any user who knows or discovers the gesture can enroll into staging and receive dev builds. Keep the badge administrator/cohort-assigned, or authorize its claim server-side against the internal tester cohort; the card and off switch can remain visible for safe offboarding.

  • MAJOR · src/services/peanut-team-badge.ts:10 · [claude-opus] POST /badge/team and the PEANUT_TEAM badge code do not exist in peanut-api-ts
    claimPeanutTeamBadge() calls serverFetch('/badge/team', { method: 'POST', body: '{}' }), and useCanJoinBeta() then looks for badges.some(b => b.code === 'PEANUT_TEAM') on the refetched user.

Evidence against the pinned peanut-api-ts checkout (policy branch):

  • src/routes/badge.ts registers only POST /badge/claims and POST /badge/award. There is no /badge/team; grep -rn "'/badge" src returns just those two.
  • PEANUT_TEAM appears nowhere in peanut-api-ts (grep -rn PEANUT_TEAM src is empty). src/acknowledgments/badge-registry.ts is a closed map keyed by badge code (BETA_TESTER, SHHHHH, …), so even a successful award could not surface a code that is not registered.
  • peanut-ui's own mirror, src/types/api.openapi.json, likewise documents /badge/award and /badge/claims only.

Against today's backend the POST 404s, claimPeanutTeamBadge() returns false, the badge never appears on /users/me, and the toggle stays permanently disabled behind "Beta updates are not enabled for this account yet" — the exact silent-exclusion failure the PR description says it is fixing.

This is the shape of a change that ships as a pair, and the backend half is presumably an open peanut-api-ts PR I cannot see from this checkout, so I am filing it major rather than blocking. Fix: confirm the peanut-api-ts PR lands POST /badge/team (or reroute this to the existing POST /badge/award with a campaign tag) and adds PEANUT_TEAM to badge-registry.ts, and merge that side first.

  • MINOR · src/components/Claim/Link/SendLinkActionList.tsx:233 · [claude-opus] A signed-out recipient on a device holding someone else's passkey loses the no-account bank claim
    showAltRails = !isLoggedIn && knownDevice === false withholds the bank/exchange rails from any signed-out visitor whose device carries a Peanut passkey marker — the 90-day web-authn-key cookie or a webAuthnKey in any <userId>:user-preferences entry (hasKnownDeviceCredentials), or a stored native session.

/home/chip/mono/product/send-links.md states recipient_account_required: false # recipient can claim to bank (SEPA/ACH) without a Peanut account, and documents a full "Claim Flow — Without Peanut Account" (open link → claim to bank → SEPA or ACH). The heuristic is device-scoped, not person-scoped: a recipient with no Peanut account opening a link on a household or borrowed phone, or on a phone where a previous owner registered, is offered the Peanut option alone with no "I don't have an account" escape — they cannot reach the documented flow at all short of switching browsers.

The code is the side that is wrong here (or the product doc needs an explicit device-scope caveat). The cheapest fix that keeps the intended behaviour is to leave the rails collapsed by default on a recognised device but keep a link/disclosure that expands them, so the documented no-account bank claim stays reachable.

Checked clean

  • Verified the supplied detached worktree is exactly the requested head, the merge base is the supplied dev base SHA, and the trusted PR metadata matches.
  • Rechecked P1 against the canonical Send Link product source and the restored regional-claim path: Pix and Mercado Pago still perform a direct Manteca claim rather than claim-to-balance then spend.
  • Rechecked P3 against device-credential detection and explicit logout cleanup: passive expiry and shared browser profiles can still suppress the no-account bank rail for a different recipient.
  • Rechecked duplicate P2/P4 at the final commit and represented the surviving OTA eligibility defect once under the latest supplied prior finding P4; the cited replies claim an in-PR fix rather than deferring it.
  • Validated the CLAIMED money-state contract in the backend policy checkout: the admin submission waits for a successful user-operation receipt before SendLink status becomes CLAIMED; FAILED and CANCELLED remain terminal failure states.
  • Reviewed badge visibility filters, badge-claim/refetch behavior, native OTA join/leave handling, claim polling, hydration, failure paths, changed translations, and focused tests for correctness, security, adversarial cases, and slop.
  • Exact-head CI completed successfully, including ci-success, unit, typecheck, eslint, format, native-export, analyze, design-system checks, human-authors, bot-approval, review, and preview deployment. No dependencies were installed into the detached worktree.

Security review: did not run — the daily spend cap was reached, so nothing was sent. This review is one reviewer short.

Third opinion by claude-opus: 2 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: 11ef89b57377 · Context: repo, product, sibling, ci · Took 16m

Comment thread src/components/Claim/Link/SendLinkActionList.tsx
Comment thread src/components/Claim/Link/SendLinkActionList.tsx
Comment thread src/components/Profile/views/About.view.tsx
@innolope-dev
innolope-dev merged commit c13fd55 into dev Sep 3, 2026
24 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