Skip to content

TASK-22490: Require supported clients before API compatibility cleanup - #3222

Draft
jjramirezn wants to merge 3 commits into
devfrom
codex/TASK-22490-client-support
Draft

jjramirezn wants to merge 3 commits into
devfrom
codex/TASK-22490-client-support

Conversation

@jjramirezn

@jjramirezn jjramirezn commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Require a supported UI bundle before the wallet opens, so temporary API compatibility code can eventually be removed. Startup and resume check a small public policy generated from a pinned mono registry. Unsupported native bundles use the existing OTA restart/check flow or store recovery; web and PWA clients reload.

A stale cached “supported” result never admits the wallet. A cached block survives network failure. Resume checks keep wallet state behind an inert cover; a confirmed unsupported result unmounts the wallet providers. Returning from marketing starts a fresh check. OTA readiness remains above the gate.

Task and dependencies

TASK-22490 — https://www.notion.so/3d7838117579802d84dec4188d981a60

Pairs with mono #194 and API #1613. Mono owns the seven-day retirement report and owner approvals. Merge mono #194 first: the policy pin must be in mono main before UI CI can pass. If mono is squash-merged, sync the pin from the merged commit, then rerun the UI checks. CI verifies the public policy against the pinned source; only the policy, pin, and digest enter public artifacts.

Review fixes and current gate

Chip's unmerged-pin finding is fixed: CI requires the pinned source to be an ancestor of mono main before it fetches the registry. Nine tests execute the actual workflow check, including divergent pins and failed API calls. Five additional tests exercise the real OTA context's checkNow and its handoff to applyNow.

The current source pin is still unmerged. The UI policy check must stay red until mono #194 merges. This is the required deployment order, not a bypass. The PR remains draft for that dependency and review of the updated commit.

Risk and rollout

All support floors start at zero. No update is required yet and no backend version check or request header is added. The new client depends on reaching the public policy: an outage shows a retry screen and blocks wallet interaction. Publish the web policy before shipping native bundles that require it.

This cannot retrofit the guard into existing installations. Before raising any floor, publish the replacement, test OTA/store recovery and compatible rollback bundles, and resolve legacy-client coverage. Seven quiet days alone do not authorize cleanup.

The existing PostHog platform property now registers before the first pageview. No new personal-data category or processor is added. Customer help updates belong to a separate change before a nonzero floor is activated.

Validation

Focused client-support, OTA, fixture, locale, and provider tests pass, including the stale-cache and return-from-marketing regressions. Typecheck, production web build, and full formatting pass. Lint reports zero errors (68 warnings). Full unit suite: 622 suites and 7,712 tests pass, with 7 existing skips. The actual pinned GitHub registry matches the public zero-floor policy.

Two web fixtures captured at 375×667 with no browser errors: update-required and update-check-failed. Native OTA/store behavior has unit coverage; a device or simulator pass is still required before release.

The advisory Screen library capture currently fails on two existing badge fixtures (36-c-badgedetailmodal and 37-c-badgestatusdrawer): their /badges/first-invite.webp asset is absent from both the PR and base trees. The base English capture logs the same NoFallbackError, but completes; these after captures report a transport failure. Both new client-support fixtures captured successfully. Required CI and the preview deployment passed before the ancestry fix. The source gate now intentionally blocks this PR until mono #194 merges.

Design notes

The two new screen compositions reuse OfflineScreen/BackendErrorScreen primitives and the AppLock cover pattern. They need a shared gate above wallet providers; existing modals sit inside those providers. Figma mapping: code-only ❓, no board yet. No new design-system primitive or dependency is introduced.

Screenshots

Fixture screenshot check passed, including both new recovery screens.

Web fixtures at 375×667. Native device validation remains pending.

Required update Policy unavailable
Required update Policy unavailable

The PR-only pr-assets-3222 branch can be deleted after merge.

@vercel

vercel Bot commented Sep 17, 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 17, 2026 4:30am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7997.19 → 8026.57 (+29.38)
Findings: +15 net (+49 new, -34 resolved)

🆕 New findings (49)

  • critical complexity — src/context/OtaUpdateContext.tsx — CC 52, MI 66.31, SLOC 217
  • critical complexity — src/dev/fixtures/registry.ts — CC 7, MI 29.36, SLOC 625
  • high complexity — src/utils/client-support.ts — CC 36, MI 60.42, SLOC 117
  • medium high-mdd — src/context/OtaUpdateContext.tsx:54 — OtaUpdateProvider: MDD 44.5 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/ClientSupportGate/RequiredUpdateScreen.tsx:31 — RequiredUpdateScreen: MDD 38.2 (uses across many lines from declarations)
  • medium high-mdd — src/app/ClientProviders.tsx:62 — ClientProviders: MDD 32.0 (uses across many lines from declarations)
  • medium high-dlt — src/context/OtaUpdateContext.tsx:54 — OtaUpdateProvider: DLT 30 (calls 30 distinct functions — high context load)
  • medium high-mdd — src/utils/capgo-updater.ts:102 — checkAndStageUpdate: MDD 29.9 (uses across many lines from declarations)
  • medium complexity — src/hooks/useClientSupport.ts — CC 27, MI 65.73, SLOC 99
  • medium complexity — src/components/Global/ClientSupportGate/RequiredUpdateScreen.tsx — CC 26, MI 57.74, SLOC 98
  • medium high-mdd — src/context/OtaUpdateContext.tsx:120 — : MDD 25.2 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useClientSupport.ts:38 — useClientSupport: MDD 22.6 (uses across many lines from declarations)
  • medium high-mdd — src/context/OtaUpdateContext.tsx:63 — : MDD 22.1 (uses across many lines from declarations)
  • medium complexity — src/app/ClientProviders.tsx — CC 20, MI 69.37, SLOC 49
  • medium complexity — src/components/Global/ClientSupportGate/index.tsx — CC 18, MI 67.99, SLOC 27
  • medium method-complexity — src/components/Global/ClientSupportGate/RequiredUpdateScreen.tsx:31 — RequiredUpdateScreen CC 18 SLOC 76
  • medium method-complexity — src/utils/capgo-updater.ts:102 — checkAndStageUpdate CC 15 SLOC 65
  • medium react-effect-fetches — src/context/OtaUpdateContext.tsx:63 — useEffect for async data fetching — use TanStack Query / server components
  • low high-dlt — src/hooks/useClientSupport.ts:38 — useClientSupport: DLT 22 (calls 22 distinct functions — high context load)
  • low high-dlt — src/components/Global/ClientSupportGate/RequiredUpdateScreen.tsx:31 — RequiredUpdateScreen: DLT 19 (calls 19 distinct functions — high context load)

…and 29 more.

✅ Resolved (34)

  • src/dev/fixtures/registry.ts — CC 7, MI 29.62, SLOC 611
  • src/context/OtaUpdateContext.tsx — CC 46, MI 66.34, SLOC 188
  • src/context/OtaUpdateContext.tsx:47 — OtaUpdateProvider: MDD 41.7 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:100 — checkAndStageUpdate: MDD 29.9 (uses across many lines from declarations)
  • src/app/ClientProviders.tsx:61 — ClientProviders: MDD 28.0 (uses across many lines from declarations)
  • src/context/OtaUpdateContext.tsx:113 — : MDD 25.2 (uses across many lines from declarations)
  • src/context/OtaUpdateContext.tsx:56 — : MDD 22.1 (uses across many lines from declarations)
  • src/app/ClientProviders.tsx — CC 20, MI 69.43, SLOC 49
  • src/utils/capgo-updater.ts:100 — checkAndStageUpdate CC 15 SLOC 65
  • src/app/sw.ts — CC 10, MI 64.74, SLOC 51
  • src/context/OtaUpdateContext.tsx:56 — useEffect for async data fetching — use TanStack Query / server components
  • src/context/OtaUpdateContext.tsx:47 — OtaUpdateProvider: DLT 29 (calls 29 distinct functions — high context load)
  • src/utils/capgo-updater.ts:100 — checkAndStageUpdate: DLT 17 (calls 17 distinct functions — high context load)
  • src/context/OtaUpdateContext.tsx:56 — : DLT 16 (calls 16 distinct functions — high context load)
  • src/utils/capgo-updater.ts:259 — applyStagedBundle: MDD 14.6 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:655 — leaveBetaOtaChannel: MDD 12.3 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:657 — : MDD 12.3 (uses across many lines from declarations)
  • src/utils/capgo-updater.ts:422 — disarmStagedBundle: MDD 11.4 (uses across many lines from declarations)
  • package.json:81 — unused dependency: @headlessui/tailwindcss
  • package.json:82 — unused dependency: @justaname.id/react

…and 14 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Global/ClientSupportGate/RequiredUpdateScreen.tsx 0.0 6.8 +6.8
src/utils/client-support.ts 0.0 6.3 +6.3
src/hooks/useClientSupport.ts 0.0 6.1 +6.1
src/components/Global/ClientSupportGate/index.tsx 0.0 4.0 +4.0
src/components/Global/ClientSupportGate/PolicyUnavailableScreen.tsx 0.0 3.9 +3.9
src/constants/client-support.consts.ts 0.0 1.8 +1.8

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • ✅ unit: 7719 ran, 0 failed, 0 skipped, 2.8m

📊 Coverage (unit)

metric %
statements 78.8%
branches 66.0%
functions 72.9%
lines 79.9%
⏱ 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_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
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 › 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 › 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 › 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
3.0s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Scan that recovers on the retry lands on the payment screen, not an error
3.0s 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`.

@jjramirezn

Copy link
Copy Markdown
Contributor Author

/chip review

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

🖼 Visual diff — 3 screens moved

4 of 96 shots changed · 92 identical · baseline 404f40e → head c1c98bc

worst % screen widths
12.57% avatar-picker 320, 430
3.43% setup-pending 320
0.36% guest-invite 320
new screens (2)
  • update-check-failed
  • update-required

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

One major finding: the policy gate accepts a pushed but unmerged mono commit, so an unreviewed support floor can pass CI and be deployed.

Findings

  • MAJOR · .github/workflows/tests.yml:333 · Require the policy pin to be on mono main
    The lock controls MONO_SHA, and this job fetches that commit directly without proving it is contained in peanutprotocol/mono's main branch. The exact pin in this head is currently divergent from mono/main. A future UI PR can therefore pin a pushed branch commit with minimumGeneration raised, update the matching lock and snapshot, and make both policy jobs pass even though mono review never approved that floor; deployment would then block every older client. Before deriving the proof, compare the pin with mono/main and require it to be an ancestor (or otherwise keep deployment gated until the mono change merges), and cover a divergent pin in the workflow test.

  • MINOR · src/context/OtaUpdateContext.tsx:201 · [claude-opus] OtaUpdateContext.checkNow ships untested
    checkNow is a new method on the shared OTA context (src/context/OtaUpdateContext.tsx:201). It mutates state the rest of the app depends on: on success it pushes a staged bundle into pendingBundle and can set storeUpdateRequired, and pendingBundle is what applyNow then restarts the app onto and what the Profile OtaUpdateModal renders. src/context/tests/OtaUpdateContext.test.tsx covers essentially every applyNow branch (deadlocking Android plugin, set() rejection, marker retarget, double-tap, off-native) but was not touched by this PR, and a grep for checkNow shows it is only ever reached through a jest mock in RequiredUpdateScreen.test.tsx and ClientSupportGate.test.tsx — the real implementation runs in no test.

Untested cases, concretely: (1) off native, checkNow returns 'unavailable' without importing the updater chunk; (2) on native, it passes onUpdateAvailable/onStoreUpdateRequired through to setPendingBundle/setStoreUpdateRequired so the bundle this check stages is the same one applyNow restarts onto — the comment at line 199 asserts exactly this and nothing verifies it; (3) a failed importWithChunkRetry is swallowed and reported as 'unavailable' rather than throwing into the screen's check().

Fix: add a checkNow block to src/context/tests/OtaUpdateContext.test.tsx alongside the existing applyNow blocks, using the same queueUpdateCheck stub the file already sets up for init, asserting the returned outcome and the resulting pendingBundle/storeUpdateRequired for each of the three cases.

Checked clean

  • Confirmed the detached worktree is at the supplied head and its merge base is the supplied dev base SHA.
  • Reviewed the startup and resume gate, strict policy parsing and storage fallback, marketing-route bypass, service-worker and HTTP cache controls, and native OTA/store plus web reload recovery paths.
  • Compared the pinned mono registry and compatibility runbook; the pinned commit exists but is not an ancestor of mono/main.
  • Required policy, unit, formatting, lint, native-export, and aggregate CI checks were green at this head; visual capture jobs were still running when checked.

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: 2d4dce195995 · Context: repo, mono, ci, official-docs · Took 18m

if (!/^[a-f0-9]{40}$/.test(lock.monoSha)) throw new Error("lock.monoSha is not a full sha")
process.stdout.write(lock.monoSha)
')
GH_TOKEN=$MONO_TOKEN gh api \

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.

MAJOR: Require the policy pin to be on mono main

The lock controls MONO_SHA, and this job fetches that commit directly without proving it is contained in peanutprotocol/mono's main branch. The exact pin in this head is currently divergent from mono/main. A future UI PR can therefore pin a pushed branch commit with minimumGeneration raised, update the matching lock and snapshot, and make both policy jobs pass even though mono review never approved that floor; deployment would then block every older client. Before deriving the proof, compare the pin with mono/main and require it to be an ancestor (or otherwise keep deployment gated until the mono change merges), and cover a divergent pin in the workflow test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c1c98bc. Before fetching the registry, the source job compares the pin with mono main and accepts only ahead/identical with the merge base equal to the pin. Nine tests execute the actual workflow block and reject divergent/behind/malformed/failed responses. The current pin is unmerged, so this PR now stays blocked until mono #194 merges; no bootstrap bypass was added. The minor checkNow finding is also covered by five real-context tests, including the staged-bundle handoff to applyNow. Full local suite: 622 suites, 7,712 tests passed; typecheck and formatting passed.

@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 mono ancestry check is implemented, but its failure is skipped out of the required umbrella gate, so an unmerged policy pin still does not block the PR.

Findings

  • MAJOR · .github/workflows/tests.yml:1199 · Failing ancestry check still passes ci-success
    ci-success depends only on client-support-policy, while that job is skipped whenever its client-support-source dependency fails. The umbrella condition rejects only failure and cancelled, so it treats that skip as success. This exact head demonstrates the path: client-support-source failed on the divergent mono pin, client-support-policy was skipped, and ci-success succeeded. The required gate therefore still permits a policy pin that is not on mono main. Add client-support-source directly to the umbrella dependencies or require every dependency result to equal success, including rejecting skipped.

  • MINOR · next.config.js:466 · [claude-opus] No test pins the /client-support.json CORS and no-store headers
    next.config.js:465-474 adds Access-Control-Allow-Origin: * and Cache-Control: no-store for /client-support.json. This is not decorative: CapacitorHttp is disabled (capacitor.config.ts:76-78), so native requests go direct from the webview under real CORS, and clientSupportPolicyUrl() (src/utils/client-support.ts:101) makes native read it cross-origin from https://peanut.me. If that header is ever dropped, reordered behind a broader rule, or lost in a config refactor, fetchClientSupportPolicy() returns null, resolveClientSupport() yields unavailable, and every iOS/Android user is parked on PolicyUnavailableScreen with no route into the wallet — a silent total native outage. The no-store half is the other guarantee: a cached floor is either a block nobody can lift or a client still running after the floor moved. The repo already has the exact precedent for this test (src/features/payment-network-explorer/__tests__/headers.test.ts loads next.config.js and asserts the rule for /dev/payment-graph). Untested case to add: headers() returns a /client-support.json rule carrying Access-Control-Allow-Origin: * and Cache-Control: no-store, max-age=0, and no later matching rule overrides either key. Worth pairing with an assertion that src/app/sw.ts keeps the NetworkOnly route ahead of defaultCache, which is the same guarantee on the PWA side. Note this is a missing test, not a defect in the shipped header — the rule as written is correct.

Checked clean

  • Exact head and merge base matched the supplied SHAs; reviewed all changed production and workflow paths plus focused tests.
  • Client-support parsing, uncached fetch, cached-block fallback, startup/resume gating, provider placement, and web/native recovery paths.
  • Policy snapshot, lock, proof derivation, exact mono registry shape, and live mono ancestry response.
  • Workflow credential isolation and public proof contents; no private registry fields are emitted.
  • Current CI at this SHA: unit, typecheck, format, eslint, and native export 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: c1c98bc61778 · Context: repo, mono, ci · Took 14m

eslint,
typecheck,
native-export,
client-support-policy,

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.

MAJOR: Failing ancestry check still passes ci-success

ci-success depends only on client-support-policy, while that job is skipped whenever its client-support-source dependency fails. The umbrella condition rejects only failure and cancelled, so it treats that skip as success. This exact head demonstrates the path: client-support-source failed on the divergent mono pin, client-support-policy was skipped, and ci-success succeeded. The required gate therefore still permits a policy pin that is not on mono main. Add client-support-source directly to the umbrella dependencies or require every dependency result to equal success, including rejecting skipped.

This branch was successfully deployed

1 active deployment
Preview — c1c98bc6 Deployed Sep 17, 2026 by vercel[bot]
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