fix(native): OTA beta switch off PostHog, Peanut-only receive, claim settles on CLAIMED - #2956
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code-analysis diffPainscore total: 7097.51 → 7105.14 (+7.63) 🆕 New findings (42)
…and 22 more. ✅ Resolved (41)
…and 21 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
🖼 Visual diff — 7 screens moved14 of 62 shots changed · 48 identical · baseline
new screens (2)
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. |
There was a problem hiding this comment.
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 thebeta-ota-channelPostHog cohort plus the five-tap gesture; this PR removes the cohort check, so any production user who discovers the gesture can callsetBeta(true)and point their device at thestagingCapgo 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 hasclaim_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 flipsSHOW_ALT_RAILSwould 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 andqueryByText/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)
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.
There was a problem hiding this comment.
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 issupported; toggling it callssetBeta(true), so whenever the staging channel permits self-assignment any customer can opt into the channel that receives everydevmerge. 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 thestagingCapgo channel — the channel that every merge todevpublishes 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 thestagingchannel 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
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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()callsserverFetch('/badge/team', { method: 'POST', body: '{}' }), anduseCanJoinBeta()then looks forbadges.some(b => b.code === 'PEANUT_TEAM')on the refetched user.
Evidence against the pinned peanut-api-ts checkout (policy branch):
src/routes/badge.tsregisters onlyPOST /badge/claimsandPOST /badge/award. There is no/badge/team;grep -rn "'/badge" srcreturns just those two.PEANUT_TEAMappears nowhere in peanut-api-ts (grep -rn PEANUT_TEAM srcis empty).src/acknowledgments/badge-registry.tsis 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/awardand/badge/claimsonly.
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 === falsewithholds the bank/exchange rails from any signed-out visitor whose device carries a Peanut passkey marker — the 90-dayweb-authn-keycookie or awebAuthnKeyin any<userId>:user-preferencesentry (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
Why
Three fixes found while investigating native "trouble reaching Peanut" reports.
beta-ota-channelPostHog flag that was never created, soisFeatureEnabledreturned false on every prod device — the switch stayed hidden and no device could self-assign to the Capgostagingchannel. Gating the whole reveal is what made a missing flag invisible; eligibility belongs on the join.CLAIMEDand sends the notification before the on-chainclaim.txHashprojects, 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 (
6a1e1488→a3a3f3294→11ef89b57)beta-ota-channelPostHog flag that was never created, soisFeatureEnabledreturned false on every prod device: invisible to its own testers, and indistinguishable from exclusion.PEANUT_TEAMand 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.POST /badge/team), which must deploy first. (TASK-22248)2. Peanut-only receive screen for recognised users (
d0943dd2, revised inf687f9c01)hasKnownDeviceCredentials), or a stored native session (hasNativeSession— the only branch that fires in the WebView, where the passkey cookie is cross-origin-empty).useKnownPeanutDevicehook 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.3. Settle the claim screen on CLAIMED (
927c18cd)status === CLAIMEDas terminal success — the same point the backend marks the claim done and notifies — with or without thetxHash.nullwhen it has yet to project; the success view tracks aclaimConfirmedflag 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 strippedwebAuthnKey(post-logout) reading as unknown.SendLinkActionList: newwho gets the alternate railsblock (unrecognised keeps every rail, logged-in and known-device get Peanut only, nothing shown while recognition is unresolved). The rail tests skipped ind0943dd2are 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.Notes / risks
peanut-api-ts:SendLink.statusis set toCLAIMEDandprocessPostClaim(which sends the push) run together, after the claim tx is handled; the failure path writesFAILEDviarollbackClaimOnError.claimedpush is sent to the sender; the claimer's screen now settles on the sameCLAIMEDmoment, so both track one signal.processPostClaimawaits the receipt but does not flipFAILEDon a revert, so a revert-after-CLAIMED would show success in both the notification and now the UI.