feat(spend): make the one-tap mixed spend reachable — PostHog flag gate + native build gate - #2959
feat(spend): make the one-tap mixed spend reachable — PostHog flag gate + native build gate#2959innolope-dev wants to merge 2 commits into
Conversation
…te + native build gate The ephemeral session-key path that collapses the mixed spend's two passkey sheets into one has been dark since it landed: NEXT_PUBLIC_SESSION_KEY_SPEND was baked into no build, and its only runtime switch was a /dev page that production builds 404. Nobody could turn it on. - Bake NEXT_PUBLIC_SESSION_KEY_SPEND=true into the iOS, Android and Capgo OTA lanes so the path ships in native bundles (still dead code on the web). - Runtime gate is now the PostHog flag session_key_spend OR the per-device opt-in, so rollout goes internal cohort -> % without another release. - /dev/session-key-spend shows flag, device and effective state. - Unit test for the gate matrix.
|
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: 7062.75 → 7063.2 (+0.45) 🆕 New findings (2)
✅ Resolved (2)
📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
The rollout gate and activated spend path are sound, but the new diagnostic page can show stale PostHog flag state.
Findings
-
MINOR · src/app/(mobile-ui)/dev/session-key-spend/page.tsx:30 · Subscribe the diagnostics page to feature-flag loading
On a direct page load before PostHog finishes loading flags, this non-reactive read returns false and nothing here subscribes the component to the later flag update. A targeted tester can therefore keep seeing both the flag and effective state as off even though the spend handler will use the enabled flag. UseuseFeatureFlags()(or another PostHog flag-load subscription) so this rollout-verification page re-renders when flags arrive. -
MAJOR · src/constants/session-key-spend.consts.ts:42 · [claude-opus] One-tap mixed spend becomes reachable in production with no test on the branch
Before this PR, NEXT_PUBLIC_SESSION_KEY_SPEND was set in no build, soSESSION_KEY_SPEND_BUILD_ENABLEDwas false everywhere and the ephemeral-key spend was dropped by the bundler — literally unreachable. This PR bakesNEXT_PUBLIC_SESSION_KEY_SPEND=trueinto all three native lanes (ios-release.yml:193, android-release.yml:139, capgo-deploy.yml:131 — and that OTA lane's own header comment says "Production OTA reaches every shipped iOS and Android install"), and adds|| isFeatureFlagEnabled(SESSION_KEY_SPEND_FLAG)so a server-side PostHog flip alone now routes real spends downtryMixedEphemeralSpend.
The gate itself is well covered by the new session-key-spend.consts.test.ts. The money path it unlocks is not covered at all. Searching src and e2e, no test references tryMixedEphemeralSpend, mixedEphemeralSpend or useSpendBundle; src/utils/tests/ephemeralSpendKey.test.ts only exercises derivePermissions (6 cases, all pure scoping) and never the orchestration; and mono/engineering/qa (Nutcracker) has no session-key/one-tap scenario either — its only "ephemeral" hits are an unrelated DB TODO and a harness-label line.
The two specific untested cases, both of which move money:
- Success:
useSpendBundle.ts:301-321— whenattempt.ok, the flow stamps the Rain withdrawal with the resolved tx hash and returnsintentId: prep.preparationId, skipping thewithCeremonyPurpose('admin_eip712')passkey signature entirely. Nothing asserts that the stamp fires with the ephemeral userOp's hash, or thatCARD_WITHDRAW_SUCCEEDEDcarriesengine: 'session-key'. - Fallback:
useSpendBundle.ts:323-331— whenattempt.ok === false(including the post-broadcast ambiguous case handled at mixedEphemeralSpend.ts:135-149), the passkey path must run against the SAMEprep. The whole no-double-spend argument in mixedEphemeralSpend.ts:18-23 rests on the adminNonce being shared, i.e. onprepareWithdrawalnot being called a second time. No test pins that: a future refactor that re-preps on fallback would silently produce two independently-executable withdrawals, and CI would stay green.
Fix: add a Jest test around the mixed branch of useSpendBundle with sessionKeySpendEnabled() mocked true and tryMixedEphemeralSpend mocked — one case asserting the ok path stamps once and never calls signTypedData, one asserting the not-ok path falls through to the passkey signature with rainApi.prepareWithdrawal having been called exactly once. That is cheap and does not need a chain. A Nutcracker scenario for the on-chain ERC-1271 ordering can follow before the flag is widened past internal users, as the consts comment already plans.
Checked clean
- Verified the detached worktree head, trusted author, dev base ref, and exact base/head SHAs.
- Reviewed all six changed files plus the activated mixed-spend, ephemeral-key permission, feature-flag, and production dev-route guards.
- Checked build-time env injection across Android, iOS, and Capgo native release lanes.
- Verified fail-closed PostHog behavior and the build-off gate matrix; exact-head unit, typecheck, lint, format, native-export, and analysis checks are green.
- Local focused Jest execution was unavailable because this detached worktree has no installed Jest binary; the exact-head unit check completed successfully in CI.
- Confirmed the diff is whitespace-clean and left the detached worktree unchanged.
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: 6da1adb68250 · Context: repo, engineering · Took 6m
| const [, setTick] = useState(0) | ||
| const runtimeOn = sessionKeySpendEnabled() | ||
| const deviceOn = sessionKeySpendDeviceOptIn() | ||
| const flagOn = isFeatureFlagEnabled(SESSION_KEY_SPEND_FLAG) |
There was a problem hiding this comment.
MINOR: Subscribe the diagnostics page to feature-flag loading
On a direct page load before PostHog finishes loading flags, this non-reactive read returns false and nothing here subscribes the component to the later flag update. A targeted tester can therefore keep seeing both the flag and effective state as off even though the spend handler will use the enabled flag. Use useFeatureFlags() (or another PostHog flag-load subscription) so this rollout-verification page re-renders when flags arrive.
🖼 Visual diff — 1 screen moved1 of 62 shots changed · 61 identical · baseline
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. |
PostHog delivers flags after page load; a one-shot isFeatureFlagEnabled read showed a targeted tester "off" until a reload. useFeatureFlags re-renders on flag load, and the effective state is derived from the same reactive read.
There was a problem hiding this comment.
Chip review — no blocking findings — this is not an approval
The diagnostics page now updates when PostHog flags load, but the newly production-reachable mixed-spend branch still lacks a regression test.
Findings
-
MAJOR · src/constants/session-key-spend.consts.ts:42 · Cover the production-enabled mixed-spend branch
With the native build gate on and the PostHog flag true, this now sends mixed spends through the previously dark ephemeral signer and fallback branch. The added tests only exercise this boolean gate; none entersuseSpendBundlewith a mixed spend. A regression that treats a reverted ephemeral UserOp as success or prepares a second withdrawal during fallback could therefore pass unit CI while producing a false success or a second money-moving attempt. Add branch-level tests for ephemeral success and reverted/post-broadcast fallback, asserting that fallback reuses the same preparation and that the returned/stamped transaction is the settled one. -
MAJOR · src/constants/session-key-spend.consts.ts:42 · [claude-opus] One-tap mixed spend goes live with the spend path itself untested
This PR is what makes the ephemeral-session-key mixed spend reachable in production: the three native release lanes now bake NEXT_PUBLIC_SESSION_KEY_SPEND=true (.github/workflows/android-release.yml:139, ios-release.yml:193, capgo-deploy.yml:131) and sessionKeySpendEnabled() now turns on from a PostHog flag that can be flipped without a release (src/constants/session-key-spend.consts.ts:42). Until this PR the build gate was off in every shipped build, so the path was dead code and the gap was academic.
The new test file covers only the gate constant (build-off wins, flag-on, device-opt-in, localStorage throw). The code the gate now switches money through has no test at all: tryMixedEphemeralSpend (src/hooks/wallet/mixedEphemeralSpend.ts) has no test file, and the session-key branch in src/hooks/wallet/useSpendBundle.ts:282-326 is never exercised — every suite that touches useSpendBundle (useSendMoney.test.tsx:39, useWalletSpendable.test.tsx:66, qr-pay-states.test.tsx:128) mocks the whole hook. src/utils/tests/ephemeralSpendKey.test.ts covers permission scoping only, not the spend.
The exact untested cases, both of which move real USDC out of Rain collateral:
tryMixedEphemeralSpendreturns{ok:false}for a mined-but-reverted UserOp (mixedEphemeralSpend.ts:135-137) and for a rescued-but-reverted one (:146-148), and useSpendBundle then completes the spend exactly once via the passkey fallback reusing the SAME prep. Nothing asserts that a reverted ephemeral attempt reports failure rather than success — a wrong branch here stamps the withdrawal with a userOp hash for a spend that never happened.tryMixedEphemeralSpendreturns{ok:true}and useSpendBundle returns immediately without falling through to the admin-EIP-712 passkey path (useSpendBundle.ts:301-321). Nothing asserts the fallback does not also run after a successful on-chain spend; the adminNonce argument in the file comment is the only thing standing between that and a double withdrawal, and it is asserted nowhere.
Fix: add a unit test for tryMixedEphemeralSpend with a stubbed session client covering success, success:false receipt, and throw-then-rescue-reverted; plus a useSpendBundle mixed-strategy test with sessionKeySpendEnabled mocked true asserting exactly one of the two paths signs and that a false return still settles the spend once.
Checked clean
- Confirmed the detached worktree HEAD, trusted author, dev base ref, and exact base/head SHAs match the supplied review target.
- Reviewed the feature-gate matrix, native workflow build flags, diagnostics-page reactivity, mixed-spend invocation, ephemeral permission scope, receipt rescue, and passkey fallback path.
- Exact-head CI reports unit, typecheck, lint, format, analyze, native-export, provenance, and aggregate ci-success checks successful; visual screenshots and preview deployment were still in progress at the final check.
- Local focused tests could not run because this detached read-only worktree has no node_modules; exact-head unit CI succeeded.
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: bfff29b7030a · Context: repo · Took 5m
|
|
||
| export function sessionKeySpendEnabled(): boolean { | ||
| if (!SESSION_KEY_SPEND_BUILD_ENABLED) return false | ||
| return sessionKeySpendDeviceOptIn() || isFeatureFlagEnabled(SESSION_KEY_SPEND_FLAG) |
There was a problem hiding this comment.
MAJOR: Cover the production-enabled mixed-spend branch
With the native build gate on and the PostHog flag true, this now sends mixed spends through the previously dark ephemeral signer and fallback branch. The added tests only exercise this boolean gate; none enters useSpendBundle with a mixed spend. A regression that treats a reverted ephemeral UserOp as success or prepares a second withdrawal during fallback could therefore pass unit CI while producing a false success or a second money-moving attempt. Add branch-level tests for ephemeral success and reverted/post-broadcast fallback, asserting that fallback reuses the same preparation and that the returned/stamped transaction is the settled one.
Why
The flow Send → amount → button → passkey → loading → second passkey is the mixed spend (smart balance topped up from card collateral): Rain admin EIP-712 sheet, security-verification overlay, UserOp sheet. Ceremony telemetry (#2870) confirms every mixed
link_createis exactly those two sheets, and that a cancelled second sheet plus retry reads as four.The one-tap path built for exactly this spot (
ephemeralSpendKey.ts/mixedEphemeralSpend.ts) has been dark since it landed:NEXT_PUBLIC_SESSION_KEY_SPENDis baked into no workflow and no GitHub variable, so the bundler drops the whole path./dev/session-key-spend, which production builds 404 (shouldBlockDevRoute).session_key_spend_attemptedevent.TASK-21612 was closed on the telemetry PR; this is the PR that actually reaches for one tap.
What
NEXT_PUBLIC_SESSION_KEY_SPEND=trueinios-release.yml,android-release.yml,capgo-deploy.yml. Web builds unchanged (still dead code) until the Vercel env is set.session_key_spendOR device opt-in. Rollout becomes internal cohort → % in the PostHog UI, no release. Prod fails closed when PostHog is unavailable (isFeatureFlagEnableddefault)./dev/session-key-spendshows flag / device / effective state.No change to the spend path itself. The fallback invariant stands: any ephemeral-path failure retries the passkey path with the same Rain prep, so double-spend is structurally impossible.
Rollout (after merge + OTA)
session_key_spend, targeting internal users only, 0% otherwise.session_key_spend_attemptedwith nosession_key_spend_fallback.webauthn_ceremony_flow(link_create,strategy=mixed) forceremony_countdropping to 1.Verification
pnpm typecheckclean.session-key-spend.consts,ephemeralSpendKey,useGrantSessionKey,useSignSpendBundlesuites: 5 suites, 21 tests green.prettier --checkclean on touched files.