Skip to content

Merge mobile release (native iOS/Android) into main#2393

Merged
Hugo0 merged 191 commits into
devfrom
merge/mobile-release-into-main
Jul 14, 2026
Merged

Merge mobile release (native iOS/Android) into main#2393
Hugo0 merged 191 commits into
devfrom
merge/mobile-release-into-main

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What

Lands feat/mobile-release into dev (for staging validation before the regular dev → main release) via an integration branch: 179 commits of native/mobile work (Capacitor iOS + Android platforms, TestFlight/Play CI release pipelines, native push via OneSignal adapters, demo/reviewer mode, passkey fixes, security hardening) merged with the ~370 commits main has gained since the branches diverged on June 19.

origin/main is already merged into this branch, so the diff below is exactly the mobile-release changes plus conflict resolutions — nothing from main is re-reviewed here.

Conflict resolutions (20 files)

  • useNotifications / OneSignal: kept the branch's platform-adapter architecture (module-level store, web + native adapters). Ported main's notification-click → PostHog UTM attribution into it: new onNotificationClick on the adapter interface, implemented in both web.adapter.ts (react-onesignal) and native.adapter.ts (Capacitor plugin), registered in the hook with main's exact capture logic.
  • Manteca deposit polling (hook + test + service): took main's version — main fixed the vanished-QR bug (settling gates on stage >= 2, not PROCESSING status) after the branch's copy was synced.
  • Card share-asset (4 files): took main's version (pill z-index fix, soft pill pull, PEANUTMAN asset rename) — branch copies were earlier syncs from main.
  • campaign-maps, dev routes regex, transaction-predicates test: took main's newer versions for the same reason.
  • SupportDrawer: kept both sides — the branch's lazy-mount + Capacitor guard (WKWebView memory-crash fix) and main's Crisp-failure retry UI.
  • TokenSelector: main's Rhino-restricted popular list + the branch's never-empty USDC-on-Arbitrum fallback. Note for reviewer: if restrictToRhino is active and the popular list is empty, the fallback offers USDC on Arbitrum — flagging in case that interaction matters.
  • sentry.utils: union — branch's /invites/validate 400 skip + main's qr-payment/init 400/422 skip.
  • setup page, ClientProviders, card page, ActivationCTAs, DynamicBankAccountForm, useSendMoney: both-sides-added unions (imports/state).

Verification

  • pnpm typecheck
  • pnpm test ✅ 129 suites / 1863 tests
  • pnpm lint has ~1048 pre-existing problems (non-blocking in CI per tests.yml); none introduced by the resolutions
  • Production web build: running locally, will mark ready-for-review when green

Merge instructions — please read before merging

Base is dev on purpose: this is a big change set, so it goes through staging first and reaches production through the normal dev → main release train. Both origin/main and origin/dev are merged into this branch, so the diff is exactly the mobile-release changes plus conflict resolutions, and merging it into dev also carries the pending main → dev back-merge commits.

Merge with a merge commit, NOT squash. Squash would break merge tracking: feat/mobile-release keeps living as the TestFlight release branch, and after a squash git no longer knows the histories are joined — every future sync between it and main/dev would re-conflict on the same ~139 overlapping files, over and over. With a true merge commit, git records the join point and future syncs only deal with new changes. The same applies to the dev → main release PR that later ships this: merge commit, not squash.

After this reaches main: merge main back into feat/mobile-release so the branches converge.

Two native passkey reliability fixes behind the May-18 Play rejection
and a known multi-account signing bug.

Silent "Set it up" no-op (rejection cause):
- checkPasskeySupport() now actually queries the native plugin's
  isSupported() in Capacitor instead of assuming support
- SetupPasskey re-checks on tap and always surfaces an actionable
  message — the button can never silently do nothing
- clearer Android NotAllowedError help copy (Google account / Play
  Services vs private-mode/cancel)

Multi-account signing (re-applies closed PR #2189):
- createNativeSignMessageCallback pins allowCredentials to the kernel's
  own authenticatorId when the caller supplies none, so a second
  peanut.me passkey on the device can't be substituted. Additive and
  native-only; requires a 2-account real-device smoke test.
Two native passkey reliability fixes behind the May-18 Play rejection
and a known multi-account signing bug.

Silent "Set it up" no-op (rejection cause):
- checkPasskeySupport() now actually queries the native plugin's
  isSupported() in Capacitor instead of assuming support
- SetupPasskey re-checks on tap and always surfaces an actionable
  message — the button can never silently do nothing
- clearer Android NotAllowedError help copy (Google account / Play
  Services vs private-mode/cancel)

Multi-account signing (re-applies closed PR #2189):
- createNativeSignMessageCallback pins allowCredentials to the kernel's
  own authenticatorId when the caller supplies none, so a second
  peanut.me passkey on the device can't be substituted. Additive and
  native-only; requires a 2-account real-device smoke test.
Native-enablement fixes that were sitting uncommitted in the working tree.

- android/build.gradle: force Java 17 across all modules (capacitor-android
  compiles at 21 → "compiled with a more recent Java" Gradle failures). Java 17
  is the Capacitor 8 / AGP baseline.
- card-comparison.ts: drop the `'use server'` directive. Server Actions can't
  exist in a static export (output: 'export') and would break the native build.
  The file only does a public dolarapi.com fetch + arithmetic (no secrets), so
  it's safe to run client-side; its dep getCurrencyPrice is not a server action.
- AndroidManifest + res/xml/network_security_config.xml: permit cleartext ONLY
  to 10.0.2.2 / localhost so the emulator can reach a local backend. The network
  config takes precedence on API 24+, so production https traffic is unaffected.

Note: capacitor.build.gradle / capacitor.settings.gradle / capacitor-passkey.xml
are cap-sync-generated and intentionally not committed here.
Native-enablement fixes that were sitting uncommitted in the working tree.

- android/build.gradle: force Java 17 across all modules (capacitor-android
  compiles at 21 → "compiled with a more recent Java" Gradle failures). Java 17
  is the Capacitor 8 / AGP baseline.
- card-comparison.ts: drop the `'use server'` directive. Server Actions can't
  exist in a static export (output: 'export') and would break the native build.
  The file only does a public dolarapi.com fetch + arithmetic (no secrets), so
  it's safe to run client-side; its dep getCurrencyPrice is not a server action.
- AndroidManifest + res/xml/network_security_config.xml: permit cleartext ONLY
  to 10.0.2.2 / localhost so the emulator can reach a local backend. The network
  config takes precedence on API 24+, so production https traffic is unaffected.

Note: capacitor.build.gradle / capacitor.settings.gradle / capacitor-passkey.xml
are cap-sync-generated and intentionally not committed here.
Audit-driven security fixes (peanut-ui):

- MoreInfo: remove the `html` prop / dangerouslySetInnerHTML sink. The only
  caller (ValidatedInput) passed help-text that's always rendered safely now;
  the sink was a latent XSS vector if user-controlled text ever reached it.
- MermaidRenderer (dev): render caught errors via textContent instead of
  interpolating the (untrusted) diagram source into innerHTML.
- vercel.json: add baseline security headers (nosniff, X-Frame-Options
  SAMEORIGIN, Referrer-Policy, HSTS) and a clickjacking CSP
  (frame-ancestors 'self'; object-src 'none'; base-uri 'self'). A full
  script-src allow-list is deferred to a report-only rollout.
- PeanutDebug: add a NODE_ENV==='production' early return so the hardcoded
  harness private key + localStorage signer writes are dead-code-eliminated
  from prod bundles, not just skipped at runtime.
Audit-driven security fixes (peanut-ui):

- MoreInfo: remove the `html` prop / dangerouslySetInnerHTML sink. The only
  caller (ValidatedInput) passed help-text that's always rendered safely now;
  the sink was a latent XSS vector if user-controlled text ever reached it.
- MermaidRenderer (dev): render caught errors via textContent instead of
  interpolating the (untrusted) diagram source into innerHTML.
- vercel.json: add baseline security headers (nosniff, X-Frame-Options
  SAMEORIGIN, Referrer-Policy, HSTS) and a clickjacking CSP
  (frame-ancestors 'self'; object-src 'none'; base-uri 'self'). A full
  script-src allow-list is deferred to a report-only rollout.
- PeanutDebug: add a NODE_ENV==='production' early return so the hardcoded
  harness private key + localStorage signer writes are dead-code-eliminated
  from prod bundles, not just skipped at runtime.
Fill the edge-to-edge status-bar inset on the setup/onboarding flow with the
brand periwinkle (secondary-3) and pad by env(safe-area-inset-top) so the
feedback ribbon isn't jammed under the status icons on Android 15 (targetSdk 36,
edge-to-edge forced). No-op on web and pre-edge-to-edge Android.
Fill the edge-to-edge status-bar inset on the setup/onboarding flow with the
brand periwinkle (secondary-3) and pad by env(safe-area-inset-top) so the
feedback ribbon isn't jammed under the status icons on Android 15 (targetSdk 36,
edge-to-edge forced). No-op on web and pre-edge-to-edge Android.
Everything needed for Play review beyond the passkey fixes.

Reviewer/demo mode (entered via the `demo` invite code, native-only):
- utils/reviewer.ts gate; pre-filled balance + history (constants/demo-data.ts)
  overlaid in useWallet / useTransactionHistory
- KYC skipped (useCapabilities, useSumsubKycFlow)
- money-movement simulated at the execution primitives (useSpendBundle,
  useZeroDev) — no chain, no funds; safe on mainnet
- flag set on `demo` invite (services/invites.ts), cleared on logout

Build rot guard + versioning:
- native-build.js fails loudly on server-only routes not covered by the
  disable list (route handlers / force-dynamic)
- android versionCode derives from git commit count (env-overridable),
  versionName from package.json; never below 2

Release flow:
- scripts/native-release.sh (pnpm native:release) derives version and runs
  build -> cap sync -> signed bundleRelease
- docs/NATIVE-RELEASE.md runbook

Native plugins (low-risk polish):
- @capacitor/haptics (success buzz on send), @capacitor/keyboard (resize)

Setup status-bar safe zone:
- (setup)/layout.tsx fills the edge-to-edge status-bar inset with the brand
  periwinkle so the feedback ribbon isn't jammed under the status icons
Everything needed for Play review beyond the passkey fixes.

Reviewer/demo mode (entered via the `demo` invite code, native-only):
- utils/reviewer.ts gate; pre-filled balance + history (constants/demo-data.ts)
  overlaid in useWallet / useTransactionHistory
- KYC skipped (useCapabilities, useSumsubKycFlow)
- money-movement simulated at the execution primitives (useSpendBundle,
  useZeroDev) — no chain, no funds; safe on mainnet
- flag set on `demo` invite (services/invites.ts), cleared on logout

Build rot guard + versioning:
- native-build.js fails loudly on server-only routes not covered by the
  disable list (route handlers / force-dynamic)
- android versionCode derives from git commit count (env-overridable),
  versionName from package.json; never below 2

Release flow:
- scripts/native-release.sh (pnpm native:release) derives version and runs
  build -> cap sync -> signed bundleRelease
- docs/NATIVE-RELEASE.md runbook

Native plugins (low-risk polish):
- @capacitor/haptics (success buzz on send), @capacitor/keyboard (resize)

Setup status-bar safe zone:
- (setup)/layout.tsx fills the edge-to-edge status-bar inset with the brand
  periwinkle so the feedback ribbon isn't jammed under the status icons
- rename reviewer.ts→demo.ts; isReviewerMode→isDemoMode, etc.
- hard-stop in kernelClient: in demo mode sendUserOperation returns a fake
  hash and never submits — covers every flow (send/recover/session-key), not
  just the simulated primitives. Guarantees no real transaction in demo mode.
- trim verbose comments.
- rename reviewer.ts→demo.ts; isReviewerMode→isDemoMode, etc.
- hard-stop in kernelClient: in demo mode sendUserOperation returns a fake
  hash and never submits — covers every flow (send/recover/session-key), not
  just the simulated primitives. Guarantees no real transaction in demo mode.
- trim verbose comments.
…setup

The synthetic demo user resolves async via useUserQuery, so there is a brief
window where user is null. Three redirect paths bounced the session back to
setup before DEMO_USER settled. Guard each with isDemoMode():
- (mobile-ui) layout: skip the /setup redirect (loading gate holds instead)
- useAccountSetupRedirect: never redirect a demo session to /setup/finish
- LandingPageCapacitorGate: cold-start routes a demo session to /home
…setup

The synthetic demo user resolves async via useUserQuery, so there is a brief
window where user is null. Three redirect paths bounced the session back to
setup before DEMO_USER settled. Guard each with isDemoMode():
- (mobile-ui) layout: skip the /setup redirect (loading gate holds instead)
- useAccountSetupRedirect: never redirect a demo session to /setup/finish
- LandingPageCapacitorGate: cold-start routes a demo session to /home
Manage the release ops gaps:
- .github/workflows/android-release.yml: tag/dispatch-triggered job that decodes
  the upload keystore from CI secrets, builds a signed AAB via native:release
  (versionCode = run_number), and uploads to a Play track. Removes the
  one-laptop release dependency; gate via a `production` GitHub Environment.
- docs/NATIVE-RELEASE.md: rewritten for current reality — Node 22 / JDK 17
  toolchain, branch merge order, full local-dev setup (AirPlay :5001, engineering
  stubs, perk key, 10.0.2.2 cleartext, demo invite), keystore secret-manager +
  recovery + assetlinks coupling, Capgo prod channel / staged rollout / rollback,
  and the CI secrets inventory.
- android-release.yml: write the full production NEXT_PUBLIC_* set into
  .env.production.local (identical block to ios-release.yml); PEANUT_API_URL now
  sourced from a repo var. CAPACITOR_BUILD/IS_NATIVE_BUILD/GIT_COMMIT_HASH are
  auto-baked by next.config.native.js, so they're omitted.
- NATIVE-RELEASE.md §11: iOS pipeline now lives on feat/ci-ios; replace the stale
  'in progress' stub with a pointer to docs/NATIVE-RELEASE-IOS.md.
- .github/workflows/ios-release.yml: build the static export, cap sync ios, then
  import the Apple Distribution cert (apple-actions/import-codesign-certs), install
  the provisioning profile from a base64 secret, xcodebuild archive/exportArchive
  with manual signing (CURRENT_PROJECT_VERSION = run number), and upload via
  apple-actions/upload-testflight-build. IPA uploaded as an artifact.
- docs/NATIVE-RELEASE-IOS.md: pipeline overview, one-time signing-material setup,
  cert/profile rotation, manual App Store promotion, and the iOS secrets table.
- Mirrors android-release.yml's no-fastlane style; no Ruby toolchain.

Stacks on feat/native-review-readiness (needs scripts/native-build.js); independent
of the Android CI branch.
cap add ios + plugins via SPM, Info.plist usage strings (camera/photos/FaceID),
associated-domains entitlement (webcredentials/applinks peanut.me), bundle
me.peanut.wallet. Simulator build pending an SPM artifact-cache clear.
The inviter-username gates and the signup field fired their validation
lookup 750ms after any non-empty input, so typing a single character
produced a misleading "No Peanut member with that username" /
"must be at least 4 characters" error before the handle could possibly
be valid.

Add an optional `shouldValidate` predicate to ValidatedInput: when it
fails, the field is held in the neutral "changing" state (no lookup,
no error UI, Next disabled) instead of being marked invalid. Apply it
on both invite gates and the signup field, gated on
`>= USERNAME_MIN_LENGTH` (4, mirroring the backend username regex).
The inviter-username gates and the signup field fired their validation
lookup 750ms after any non-empty input, so typing a single character
produced a misleading "No Peanut member with that username" /
"must be at least 4 characters" error before the handle could possibly
be valid.

Add an optional `shouldValidate` predicate to ValidatedInput: when it
fails, the field is held in the neutral "changing" state (no lookup,
no error UI, Next disabled) instead of being marked invalid. Apply it
on both invite gates and the signup field, gated on
`>= USERNAME_MIN_LENGTH` (4, mirroring the backend username regex).
Comment thread .github/workflows/capgo-deploy-ios.yml Fixed
Comment thread .github/workflows/capgo-deploy.yml Fixed
Comment thread .github/workflows/ios-release.yml Fixed
Comment thread .github/workflows/ios-release.yml Fixed
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Note: how to get the CodeRabbit review running on this PR

CodeRabbit keeps skipping this PR ("Too many files: 172, limit 150"). For anyone wondering, a new PR won't help and excluding images won't help either — here's why and what actually fixes it:

Why it skips. The diff has 201 files; CodeRabbit's default filters already exclude the 29 images/lockfile (!**/*.png etc.), leaving 172 — still over the 150 limit. The overage is generated native scaffolding: 28 non-image files under ios/**, android/**, patches/** (pbxproj, plists, storyboards, Gradle). Excluding those brings the count to ~144, under the limit.

Why the fix in this branch isn't working yet. This branch adds a .coderabbit.yaml with exactly those path filters, but every run reports "Configuration used: Organization UI" — the org-level dashboard config takes precedence, so the repo YAML is ignored.

What to do (needs CodeRabbit dashboard access):

  1. Open the CodeRabbit dashboard → peanutprotocol/peanut-ui settings.
  2. Either enable the repository config file (so .coderabbit.yaml from the branch is honored), or add these path filters directly in the UI:
    • !ios/**
    • !android/**
    • !patches/**
  3. Comment @coderabbitai full review on this PR to re-trigger.

Fallback if repo YAML only loads from the default branch: merge the .coderabbit.yaml to main in a tiny separate PR first, then re-trigger here.

This also permanently keeps future mobile PRs (which always carry Xcode/Gradle churn) under the review limit.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

⚙️ CodeRabbit configuration inspected

Length of output: 5263


@innolope-dev Confirmed your diagnosis by pulling the effective configuration for this repo — it checks out exactly:

  • inheritance: false is set at the Organization UI level. This is the specific field that causes the repository's .coderabbit.yaml to be ignored entirely in favor of the org dashboard config — matching what you saw reported as "Configuration used: Organization UI".
  • reviews.path_filters: [] is currently empty in the effective (org) config, confirming no ios/**/android/**/patches/** exclusions are applied yet.
  • The 150-file review limit itself isn't a configurable field in the schema — it's a platform constant — so path filtering is indeed the only lever here, not something that can be raised via config.

So your two proposed fixes are both correct:

  1. Someone with dashboard access flips inheritance to true in the org settings for peanutprotocol/peanut-ui, so the repo's .coderabbit.yaml (with the !ios/**, !android/**, !patches/** filters) takes effect, or
  2. Add those same three filters directly into reviews.path_filters in the Organization UI config (bypassing the repo YAML entirely).

Once either change is made, re-trigger with @coderabbitai full review and it should get the file count under 150 and proceed.


✏️ Learnings added
Learnt from: innolope-dev
URL: https://github.com/peanutprotocol/peanut-ui/pull/2393

Timestamp: 2026-07-10T09:46:15.511Z
Learning: Repository: peanutprotocol/peanut-ui. This repo's CodeRabbit org-level configuration has `inheritance: false`, which means the repository's `.coderabbit.yaml` file is entirely ignored in favor of the Organization UI config ("Configuration used: Organization UI" is reported on reviews). To apply repo-level settings like `path_filters` (e.g., excluding `ios/**`, `android/**`, `patches/**` for mobile PRs with native scaffolding that push file counts over the 150-file review limit), either flip `inheritance` to `true` in the org dashboard, or add the filters directly to the org-level `path_filters` config.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

…e workflows

Fixes the CodeQL findings on this PR: android/ios/capgo release workflows
ran with default GITHUB_TOKEN permissions (none of them write to the repo,
so contents: read suffices) and pulled third-party actions by mutable tag
(pnpm/action-setup, setup-xcode, import-codesign-certs,
upload-testflight-build), now pinned to commit SHAs.
@innolope-dev
innolope-dev marked this pull request as ready for review July 10, 2026 09:48
…nning

native-webauthn.ts is the most security-critical native file and had no
direct tests. Covers: DER ECDSA signature parsing and low-S malleability
normalization, SPKI public-key coordinate extraction and authenticator-id
hashing, the zerodev response tuple encoding (responseTypeLocation,
RIP-7212 precompile flag), challenge/rpId wiring, message format handling,
and the multi-account credential pinning fix (pin used when no allow-list,
caller list never overridden, empty list falls back to pin).
@innolope-dev
innolope-dev changed the base branch from main to dev July 10, 2026 15:15
test(native): cover the WebAuthn bridge — DER parsing + credential pinning

@kushagrasarathe kushagrasarathe 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.

Review summary: holding this before merge to dev

I did a deep review across the auth/signing, demo-mode, native-infra/OTA/CI, and web-conflict surfaces, looking at security, regressions, and code quality.

Good news first. No web regressions. All 12 documented conflict resolutions are correct, main's fixes all survived, the api-fetch.ts web path is byte-identical to dev, the signing core is sound, and demo mode is correctly native-only and sandboxed. This is solid work.

That said, I'm not merging as-is. The OTA pipeline can push arbitrary JS into the wallet WebView, and right now its gating is weaker than the store-release path sitting next to it. Blockers are below.

Blockers (must fix before merge)

1. OTA bundles have no provenance signing. capacitor.config.ts:27-36

CapacitorUpdater has no publicKey. Capgo's checksum and sessionKey come from the same server response as the bundle URL, so they only verify transport integrity, not authorship. Anyone with CAPGO_API_KEY or access to the Capgo account can push JS that runs inside the wallet, which means full control of the passkey/signing UI, recipient addresses, and amounts, all without going through store review. That's RCE-equivalent for a money app.

Fix: enable Capgo E2E signing (npx @capgo/cli key create, add publicKey, sign bundles on upload).

2. OTA auto-deploys to production on merge to main with no approval gate. .github/workflows/capgo-deploy.yml:2-4

on: push: branches: [main, dev], and main maps to the production channel with no environment: reviewer gate, unlike android-release.yml and ios-release.yml. One bad or malicious merge ships straight to devices. Combined with #1, one merge equals arbitrary code on every install.

Fix: add environment: Production (with required reviewers) to the deploy job. Ideally make the production channel workflow_dispatch-only and leave staging on auto.

3. RECORD_AUDIO permission with no justification. android/app/src/main/AndroidManifest.xml:95

QR scanning is camera-only, and FOREGROUND_SERVICE_MICROPHONE is explicitly removed right beside it. This looks like a leftover from the Capacitor camera template. It triggers Play data-safety scrutiny and widens the attack surface for no reason.

Fix: delete the line.

4. enableDemoMode() on a web-reachable path is missing the isCapacitor() guard. src/services/invites.ts:50-51

This fires from web pages (InvitesPage, JoinWaitlistPage), unlike the native path which gates on isCapacitor(). It's inert today since isDemoMode() re-checks Capacitor, but it still writes the demo localStorage key on web, and it's the one spot where the web-inert guarantee falls apart if Capacitor detection ever regresses.

Fix: if (res.data?.success && isCapacitor() && isDemoInviteCode(inviteCode))

Non-blocking, worth folding in while you're here

  • Sentry native capture at 100% plus console capture, scrub depth 10 to 15 (sentry.utils.ts:256, instrumentation-client.ts). Please confirm isSensitiveKey covers authToken, jwt, sessionKey, privateKey, mnemonic, and Authorization so no logged object leaks a secret.
  • Dead code: resetDemoBalance() (demo-balance.ts:45, no callers) and the unused challenge var (native-webauthn.ts:159).

Please confirm (off-repo, can't verify from the diff)

  • Capgo production channel is device-locked and not yet serving real installs
  • Whether account-level bundle signing is already enabled (would soften #1)
  • CAPGO_API_KEY scope. Prefer an upload-only key, not full-account.
  • Prod web Vercel project does not set NEXT_PUBLIC_CAPACITOR_BUILD=true (the one flag that makes demo mode reachable on web)
  • pnpm typecheck && npm test && npm run build green on the merged tree (the PR claims 1863 tests pass, just want it confirmed in CI)

Once #1 through #4 are addressed and the checklist is cleared, I'm happy to re-review and land it on dev for staging. #2394 (native-webauthn tests) looks good and i have merged it.

- capgo: enable E2E bundle signing (publicKey in capacitor.config, sign CI upload via --key-data-v2)
- capgo-deploy: production is workflow_dispatch-only + Production environment gate; dev auto-ships to staging
- android: drop unjustified RECORD_AUDIO permission
- invites: gate enableDemoMode() behind isCapacitor() on the web-reachable validate path
- demo-balance: self-healing 7-day per-device TTL reset + tests
- native-webauthn: remove unused challenge var and orphaned helper
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review — agree on all four blockers. Here's what I've changed.

Quick context on the two OTA items (#1, #2): that pipeline came in with the original Capgo integration rather than this mobile-release branch, so these are hardening the existing setup rather than anything new introduced here — good catches regardless, and addressed below.

Blockers

1. OTA bundle signing ✅ Enabled Capgo E2E signing. Generated the RSA keypair and added the publicKey to CapacitorUpdater in capacitor.config.ts; the private key stays out of git (.capgo_key_v2 is gitignored) and is passed to the CI upload via --key-data-v2 from a new CAPGO_PRIVATE_KEY secret.

2. OTA auto-deploy gate ✅ Went with the stronger option: dropped main from the push trigger so production is workflow_dispatch-only, and added environment: Production to the deploy job (mirrors android-release.yml / ios-release.yml). dev still auto-ships to staging. A merge to main can no longer reach devices without an explicit, approved run.

3. RECORD_AUDIO ✅ Removed — leftover camera-template noise, QR scanning is camera-only.

4. enableDemoMode() web guard ✅ Added the isCapacitor() guard to the validateInviteCode path in src/services/invites.ts, matching the native entry path.

Non-blocking

5. resetDemoBalance() — I kept this one rather than removing it. It reads as unused today, but it's actually the only thing that can refill the demo wallet, and without it the balance would stay stuck at 0 once spent down (debitDemoBalance floors at 0 and persists). Demo mode is fully on-device, so the balance and its reset are per-device. I've now wired it up as a self-healing 7-day per-device TTL: getDemoBalanceUnits() refills on first read when the stored timestamp is older than the TTL (or absent). I also dropped the misleading "call on demo entry" docstring, and added demo-balance.test.ts covering the floor-at-0 debit, the TTL refill, and fresh-install defaults.

6. Unused challenge var (native-webauthn.ts) ✅ Removed, along with the now-orphaned bufferToBase64URL helper.

7. Sentry — leaving as-is for now; we'll tune the capture/scrub settings in a follow-up.

tsc, eslint, and the touched test suites are green locally.

Match the web's tracesSampleRate on the Capacitor Sentry init; error
capture (sampleRate) stays at 1.0.

@kushagrasarathe kushagrasarathe 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.

approved

one qn, on the capgo workflow before it goes further: environment: Production is set at the job level, so it applies to every run including the dev to staging auto-deploy, not just production. if the Production environment has required reviewers, doesn't that mean staging pushes also pause for manual approval? that seems to contradict the "only dev auto-ships to staging" intent in the comment above it

was that intentional, or should the gate only apply to the production workflow_dispatch run? if the latter, we could make the environment conditional, something like:

environment: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.channel == 'production') && 'Production' || '' }}

…nto-main

# Conflicts:
#	src/components/Global/TokenSelector/TokenSelector.tsx
#	src/hooks/wallet/useSpendBundle.ts
Job-level `environment: Production` applied to every run, so the
dev→staging auto-push would also wait on the environment's required
reviewers. Resolve the environment conditionally — Production only for a
workflow_dispatch run targeting the production channel, empty otherwise —
so staging keeps auto-shipping.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

@kushagrasarathe updated according to your comment

@kushagrasarathe kushagrasarathe 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.

good changes, lgtm

Comment thread src/app/(mobile-ui)/dev/full-graph/page.tsx
@Hugo0
Hugo0 merged commit 2b8bacf into dev Jul 14, 2026
21 of 23 checks passed
VladUXUI pushed a commit to VladUXUI/peanut-ui-vlad that referenced this pull request Jul 17, 2026
Two interacting defects made 'Select new method' unusable when saved
bank accounts exist:

1. Crypto click navigated to /withdraw/crypto before an amount was set.
   That page's no-amount guard bounces straight back to /withdraw, and
   since da943d6 (mobile-release, merged to dev via peanutprotocol#2393) its unmount
   cleanup calls resetWithdrawFlow(), wiping selectedMethod and
   showAllWithdrawMethods — landing the user on the saved-accounts list
   instead of the amount input. Set the method in context only (same
   contract as bank-country selection); the withdraw page navigates to
   /withdraw/crypto after Continue, with the amount set.

2. The default-view effect re-ran on every user refetch (window focus,
   4s pending-rail poll — each dispatches a fresh user object into
   redux) and unconditionally forced the saved-accounts view, yanking
   an open country list back after a few seconds. Latch the default
   view once per mount; refetches now only sync the accounts list.
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.

4 participants