Skip to content

Prod Release Sprint 157 — mobile app fixes · app-locale sync · peanut-shaper badge (2026-08-24) - #2804

Closed
jjramirezn wants to merge 138 commits into
mainfrom
dev
Closed

Prod Release Sprint 157 — mobile app fixes · app-locale sync · peanut-shaper badge (2026-08-24)#2804
jjramirezn wants to merge 138 commits into
mainfrom
dev

Conversation

@jjramirezn

@jjramirezn jjramirezn commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Release PR: devmain for the Sprint 157 prod release.

Payload (so far):

Migrations: n/a (FE).

Notion release page + full changelog to follow. Hugo merges this PR, after the BE deploy.

Summary by CodeRabbit

  • New Features

    • Added iOS-specific cashback terminology, localized messaging, and a rewards “How it works” explainer.
    • Added improved pull-to-refresh feedback with haptics and animations.
    • Android now opens terms pages in the system browser.
    • Added a cancel-send-link drawer and richer support verification details.
    • Added localized marketing content, improved exchange CTA routing, and automatic locale synchronization.
  • Bug Fixes

    • Improved return navigation, balance hiding, QR-code visibility, route highlighting, and terms acceptance handling.
    • Improved login, invitation, and error reporting behavior.
    • Native releases now automatically publish and verify matching OTA updates when needed.
    • Improved ENS name resolution and landing-page performance.

innolope-dev and others added 30 commits August 18, 2026 16:55
Support agents have no visibility into a user's live verification state, so
they guess where a user is stuck. Adds a support-facing snapshot to the Crisp
agent sidebar (session:data), derived entirely from the two backend read-models
already on /get-user (`capabilities`, `identityVerification`) — no backend
change and no new provider-state interpretation on the client.

New agent-only fields: identity_status, email_on_file, verification_gates,
verification_rails, failure_reason, pending_actions. Threaded through all three
Crisp sinks: web widget (setCrispUserData), the proxy iframe (which receives the
whole CrispUserData over the postMessage handshake), and native Capacitor
(SupportDrawer).

Sidebar only — the user's own composer (message:text) is never touched, so
internal reason codes and rail ids stay out of the user's view.

Closes #2360.
`shouldIgnoreError` only ever inspected `exception.values[0]`. Sentry orders
that array root-cause-first, so for any error carrying a `cause` the wrapper
sits at the end — and `fetchWithSentry` always sets `userError.cause`.

The `alreadyReported` filter added in 10ee160 to stop double-counting fetch
failures has therefore been inert for its own motivating case ever since:
PEANUT-UI-SNP (the ServiceUnavailableError wrapper) kept being reported
alongside PEANUT-UI-QEY (the timeout it wraps). Sentry confirms the shape —
`error.type` on those events reads "Error, ServiceUnavailableError".

Scan every value's type and message, and collect extension stack frames from
every value rather than just the first.

Also suppress Capgo's background-updater chatter, which captureConsoleIntegration
promotes to ~95 events/day on native. `disable_auto_update_under_native` and
checksum mismatches stay reported: those mean OTA is actually broken for a
build, not that one download hiccuped.
Native reported 970 Sentry events in 24h, 691 of them level:info. Almost all
of that is instrumentation written to watch the CapacitorHttp → direct-fetch
switch (PEANUT-UI-R44). That switch has shipped; the probes have not.

- native-canary: five captureMessage calls per app launch (~550 events/day),
  plus five extra API round-trips on every cold start, to answer a question
  we already answered. Deleted.
- legacy-cookie native transport engaged / native http fallback engaged:
  once-per-session census notes for the same migration. Deleted.
- onesignal subscription snapshot: still useful, but it's a state fact, not a
  fault — moved to PostHog as notification_subscription_snapshot. The failure
  variant stays in Sentry, since failing to read the state is a real error.

Transport behaviour is unchanged; the fallback and prefer-native paths still
work exactly as before, and their tests now assert the paths stay silent.
Same shape as the fetch wrappers. useZeroDev classifies the raw WebAuthn
failure, captures it with full context, and throws a curated user-facing
PasskeyError — and for a plain user cancel it deliberately captures nothing
on web.

Three call sites re-report that wrapper: Landing and JoinWaitlist call
Sentry.captureException on it directly, and GuestLoginModal console.errors it.
The result is a second, context-free event, and LOGIN_CANCELED showing up at
error level despite the deliberate silence — PEANUT-UI-QRW and PEANUT-UI-R20,
19 events yesterday.
useZeroDev classifies a WebAuthn failure, captures the raw error with full
context, and throws a curated PasskeyError for display. For a plain user
cancel it deliberately captures nothing on web — "Cancel saved no state".

Four call sites undid that:

- InvitesPage fired `void handleLoginClick()` with no catch, so cancelling the
  prompt became an unhandled rejection. Now caught, and the curated message is
  surfaced the way every other login entry point surfaces it.
- GuestLoginModal console.error'd the wrapper, which captureConsoleIntegration
  turns into an event.
- Landing and JoinWaitlist called Sentry.captureException on the wrapper —
  a second, context-free copy of an error already reported at the throw site,
  and the reason LOGIN_CANCELED showed up at error level at all.

Landing and JoinWaitlist still report anything that isn't a PasskeyError, so an
unexpected failure in the login path is not silenced.

PEANUT-UI-QRW and PEANUT-UI-R20: 19 events yesterday, all expected outcomes.
…dentity

`capabilities` is optional on /get-user during the capability migration. Deriving
gates over the empty fallback state made every operation read `needs-identity`,
which a support agent cannot tell apart from a genuinely unverified user — the
exact misreading this snapshot exists to prevent. Report an empty `gates` when
the read-model is absent; a read-model that is present but empty still derives
normally, since needs-identity is the truth there.
5343f1d landed in the same function. Both changes keep their meaning:

- Critical-flow captures still bypass every group but userRejected, and the
  early return now sits ahead of the Capgo check so a money-flow event can
  never be dropped by it either.
- The chain scan is narrowed to exception TYPES only. Class names are exact,
  so matching them across the chain can only catch our own wrappers. Scanning
  chained MESSAGES the same way would suppress more, not less — which is the
  failure 5343f1d fixed, where viem's "Details: Failed to fetch" ate real
  payment errors through the networkIssues group. values[0].value keeps the
  matching reach it had before.

Added a guard test for that boundary: a wrapper whose message contains a noise
pattern is still reported.
Android's Capacitor WebView cannot host a third-party subframe.
BridgeWebViewClient.shouldOverrideUrlLoading hands EVERY navigation to
Bridge.launchIntent without checking request.isForMainFrame(), and
launchIntent cancels the load for any host outside the app origin that
isn't listed in server.allowNavigation — which capacitor.config.ts does
not set. The Bridge ToS iframe therefore painted pure white inside our
verification chrome, no signedAgreementId postMessage ever arrived, and
Bridge kept the terms pending. All 39 post-deploy ToS confirmations came
from web sessions; the one native user in the set is the failure. Sumsub
KYC itself is unaffected on native because SumsubKycWrapper routes
Capacitor to the Cordova SDK, not an iframe.

allowNavigation was deliberately NOT the fix: it is baked into the
binary (every installed app stays broken until a store release), and on
android it also registers the host as a WebViewLocalServer authority —
its HTML would be proxied through handleProxyRequest with Capacitor's
bridge JS injected — and widens the androidBridge trusted-origin set.

Android now opens the ToS in the system browser (@capacitor/browser,
already in the binary since April) and treats `browserFinished` as a
`returned` signal — "the user came back", not "the user accepted".
/users/bridge-tos-confirm re-reads has_accepted_terms_of_service from
Bridge, so the detour needs no postMessage; confirmBridgeTosAndAwaitRails
now returns that verdict and the callers use it to tell a real acceptance
from an abandoned one (no phantom completion, no unearned KYC_TOS_ACCEPTED).
When the caller observed no acceptance and Bridge says no, the helper
stops after the confirm retry instead of arming the 30s submission
window and polling rails that will never change.

iOS is untouched — its navigation delegate already gates the same detour
on targetFrame.isMainFrame, so the iframe works there. The web iframe path
is unchanged. Ships over the air; no store release needed.
A fast unmount or visibility change while Browser.addListener was still
resolving removed the listener but still opened the Custom Tab — the ToS
could pop after its owning flow had closed. Late arrivals now drop the
listener and stop.
Before a request exists the QR already encodes the profile payment link
for the entered amount (/{username}/{amount}USDC), so there is no reason
to keep it blurred until the create button is clicked. It now unblurs on
a positive amount and re-blurs when the amount is cleared or zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQHgKufMZzqzwCviP4gXKs
On native the pull refetches via react-query instead of reloading the page,
so nothing on screen blinks and the gesture reads as having done nothing.
Give the indicator the full state sequence instead:

- pulling: the indicator scales in and the arrow rotates toward upright,
  flipping at the release threshold with a light haptic on the crossing
- refreshing: a readable spinner arc (was a thin quarter-circle path)
- done: green checkmark with a pop + success haptic, held briefly, and the
  content fades back in so the screen visibly re-renders

Also restyles the indicator to the app's brutalist look (black border + hard
shadow), guards Element.animate for WebViews that lack it, honours
prefers-reduced-motion for the content fade, and clears pending timeouts on
unmount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zVw46ZCvXYp7b4dpXvJiq
Going /profile → "Exchange rates and fees" → "Try it!" lands the user in the
add-money or withdraw root, depending on their balance and unlocked regions.
Both roots deliberately reset to /home on back rather than calling
router.back(), because their own sub-pages push back to the root and back()
ping-pongs there. That is right for a tab-bar entry, but it strands anyone who
arrived from another screen: back never returns to the widget they came from.

Add a `returnTo` query param the caller sets and the flow roots honour, so the
origin travels with the navigation instead of being guessed at the destination:

- new `withReturnTo` / `readReturnTo` helpers — same-origin only (reusing
  sanitizeRedirectURL), and a target pointing at the current page is dropped,
  since re-pushing the page you are on is a back button that does nothing.
- the exchange-rate CTA passes its own path *and* query string, so back
  restores the currency pair and amount the user was looking at.
- add-money and withdraw check it before falling back to /home. The send-flow
  /send branch and the in-page steps (country list → method selection, amount
  → method selection) keep priority, so back still unwinds one step at a time.

Tests: unit coverage for the helpers (including the off-origin and
self-referential rejections) plus back-navigation cases on both flow roots and
the exchange-rate CTA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbALh4dJvcBUAinjnwQ53q
…-app-fixes

Brings in the pull-to-refresh feedback work (arrow arming, spinner, success
checkmark + haptics, content settle) along with the current main base.
Reverses the iOS gate from 4564b40 and adds an iOS-only copy layer that
presents the referral programme as cashback. Web and Android are
unchanged: every pre-existing catalog string is byte-identical to dev, and
the two new UI elements are behind isIOSNative().

Hiding the programme while the backend kept accruing — and while
referral.reward.earned kept pushing "You earned $X! <name> joined Peanut
with your invite" to the same devices — was the worse position. Guideline
2.3.1 treats hidden-but-discoverable features as grounds for removal. The
programme will be disclosed in Notes for Review instead.

The old citation was also wrong: appStoreCompliance.ts cited 3.1.5(ii),
which is Mining. The clause about offering currency for encouraging
downloads is 3.1.5(v).

appStoreCompliance.ts is deleted along with its seven call sites — the
/rewards and /rewards/invites route guards, the home pill, the profile
row, both invite carousel CTAs, the surprise-claim treatment and the
receipt points row. Removing the route guard also fixes the
referral.reward.earned deep link, which pointed at a guarded route and
bounced iOS users to /home.

The cross-chain withdraw gate is untouched: it lives in
underMaintenance.config.ts, rests on 3.1.5(iii) (Exchanges, which needs
per-region licensing), and multi-chain swaps are the strongest crypto-app
signal we ship.

useAppTranslations wraps useTranslations and prefers an `iosCopy.<key>`
override when one exists, falling back to the base string otherwise.
Overrides sit inside the namespace they belong to, so call sites keep
their existing keys and only the hook name changes. The block is called
iosCopy, not ios, because profile.backup.steps.ios is already content — a
namespace whose own content sat under `ios` would have had every key
silently redirected. Platform is read at render time; the Capacitor bridge
is absent during prerender.

45 overrides per full locale plus voseo deltas for es-AR:

- rewards -> cashback. "Cashback" as a loanword is the standard term in
  both LatAm markets.
- "used Peanut" -> "paid with Peanut", "the more they use" -> "the more
  they pay". Ties the money to a transaction rather than a signup;
  downloading is what 3.1.5(v) names.
- A four-step "How cashback works" card on /rewards. The earn instruction
  used to be a half-sentence on the lifetime total; as its own card it
  reads better and states the process without pinning a dollar to a
  person.
- Dropped "friends & their friends" from qrPay.claim.inviteQrDescription
  and "contribute towards your points forever" from the sticker copy — the
  only two places the UI stated the transitive structure.

Push notification copy is deliberately unchanged. PushChannel targets by
userId and OneSignal fans one notification out to every subscription a
user has, so there is no per-platform copy path and a single notification
cannot say two different things to the same person's devices.

Tests cover both platforms, the fall-through, all four locales, and two
catalog invariants: every override shadows a real base key (a typo would
otherwise silently never resolve), and iosCopy stays distinct from the
existing ios content key.
…o-dev-20260820-2

chore: back-merge main → dev (2026-08-20 release + #2760)
Removing the route guard took the file's only useEffect with it, and
no-unused-vars is an error rather than a warning — the single eslint error
in the run, on top of the 65 pre-existing warnings.
feat(rewards): iOS-only cashback copy, referral surfaces restored
…cel drawer

Four unrelated mobile-app fixes.

**Home tab has no active state.** WalletNavigation compared `usePathname()`
to the nav href with `===`. The native build sets `trailingSlash: true`, so
the pathname is `/home/` there and the comparison never matched — every
active state in the app was silently lost, not just Home. Added
`isSameRoute()` next to the other route helpers and routed the mobile and
desktop nav through it.

**Withdraw's final screen loads twice.** `withdraw/page.tsx` called
`React.lazy()` inside the render body for its two native `?country=` views.
That hands back a fresh, unresolved lazy on every render, so each re-render
re-suspended: React hid the rendered view and swapped in the Suspense
fallback (null) until the import re-resolved a microtask later. The screen
blanked and loaded again — and the success view triggers a re-render itself
when it invalidates the transactions query. Hoisted both to module scope.
The regression test asserts the view isn't display:none straight after a
re-render; it fails against the old code.

**Stray rule under the receipt's last row.** The details card underlines
every row and drops the rule on the last one, but `shouldHideBorder` only
reaches rows the receipt renders itself. `BridgeDepositInstructions` expands
into rows of its own and doesn't take the flag, so the pending bank-deposit
receipt ends on a rule sitting directly on the card border. Rows with a
second runtime gate (a token icon still being fetched, a rate missing from
the payload) fail the same way — the config calls them last, the DOM never
gets them. Let the container decide with `[&>*:last-child]:border-b-0`.

**Cancel-link confirmation is a modal.** Replaced it with a bottom drawer.
It opens from two places — the transaction details drawer and the send-link
success page — so `Drawer` gained a `nested` prop that switches to vaul's
`NestedRoot`; a plain Root inside a Root double-applies the background scale
and fights over the scroll lock. That also retires the `!z-[10]` shuffle the
parent drawer needed to get out of the modal's way. Verified in Chromium
that the content fits without scrolling from 320x568 up, and that the
drawer's `max-h-[80vh] overflow-auto` takes over below that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbALh4dJvcBUAinjnwQ53q
…mobile-app-fixes

Brings in five mobile fixes:
- back navigation after the exchange-rate widget CTA (?returnTo)
- home tab active state on the trailing-slash native build
- withdraw's final screen re-suspending and loading twice
- stray dashed rule under the receipt's last row
- cancel-link confirmation converted from a modal to a drawer
The top strip was forced black in bd9a1b3 so it would stop flipping between
black and beige above the pink beta feedback ribbon. That ribbon is now hidden
on iOS (e56254e), and the underlying inconsistency was really a sizing/source
problem, fixed in 9b3384e by reading Capacitor's natively measured insets.

Recolor only — the safe zone keeps h-safe-top, so the natively measured inset on
Android 15+ and the env() fallback everywhere else are unchanged. Style.Light
pairs dark status-bar icons with the light strip.
fix(mobile): batch of native app fixes
…cation-error-uzgmxr

fix(claim): stop blaming the sender, and offer a retry when one would work (dev)
Artwork and manifest entry for the Help Shape Peanut interview badge
(catalog entry lands in peanut-api-ts). PNG like OFFRAMP_USER — the
vectorized SVG can replace it later without a code change.
TASK-21713
…rame

fix(native): accept Bridge ToS via the system browser on android
feat(badges): PEANUT_SHAPER asset + share line
users.locale on the BE was never written — notification emails could not
know the user's language. LocaleSync (below AppIntlProvider + AuthProvider)
sends the resolved app locale to /update-user, deduped per (user, locale)
via localStorage, synced on startup resolution and on a manual switch in
Settings. Pairs with peanut-api-ts#1387 (locale field + localized
badge-unlocked email).
feat: mirror the user's app locale to the BE for localized emails
The root layout mounts the full app provider stack on every route, so the
landing page pays for the wallet even though it never uses one. Gate the
wallet half on the route and take the analytics init off the critical path.

Follow-up to #2788, which shrank the fonts and fixed the hero LCP. The landing
page still scored 45 on mobile afterwards (LCP 4.6s, TBT ~4s), and the weight
left is JavaScript rather than images.

- ContextProvider splits into a core (toast + auth) and AppFlowProviders
  (kernel client, token context, the six transfer-flow contexts), the latter
  loaded as its own chunk. KernelClientProvider statically imports the ZeroDev
  SDK, so mounting it was enough to put the SDK on the landing page.
- WagmiProvider moves behind the same gate; the query client moves to
  config/queryClient so marketing routes keep react-query without wagmi.
- App-only globals (rain-cooldown modal, badge toast, app lock, PeanutDebug)
  move to AppGlobals. Two of them read contexts that now only exist on app
  routes, so they had to move with the providers.
- isMarketingRoute keys on the locale prefix, not the path segment: /withdraw
  is BOTH a marketing page under a locale and the app's withdraw flow under
  (mobile-ui), and matching the segment would have stripped the providers off
  the app route. Unknown paths fall through to the full tree.

The ENS client fallback used @justaname.id/react's usePrimaryName, whose
onChain path is ENS reverse resolution through a public client — viem, already
bundled for wagmi, does the same thing. Dropping the dependency takes siwe and
@ensdomains/ensjs out of the client entirely. normalizeEnsName also moves to
its own module so client components stop importing ens.utils, which pulls the
JustaName SDK for a pure string helper.

Analytics keep every event, just later:
- posthog session recording starts on idle instead of at init, so rrweb's
  recorder is no longer fetched and snapshotted mid-load. The opening moment
  of a replay is no longer captured; everything else is unchanged.
- Sentry.init defers to idle behind a buffer that holds anything thrown before
  the SDK exists and replays it on init, so error coverage is unchanged.
- gtag moves to lazyOnload — next emits a preload for afterInteractive
  scripts, which put 186 KB of gtag.js at High priority ahead of the LCP.
- whenIdle also fires on pagehide so a visitor who leaves early still counts.

Assets and misc:
- landing-countries.svg run through svgo at precision 1: 362 KB -> 43 KB
  gzipped, pixel-diffed against the original (RMS 2.4, edge antialiasing only).
- Londrina Solid is dead — declared with two weights, its CSS variable is
  referenced nowhere. Removed.
- sniglet and the two knerd faces stop preloading; they render below the fold
  and were competing with the hero image at High priority.
- i18n-iso-countries loads on demand, and the /qr-pay prefetch moves out of the
  root layout so marketing visitors stop paying for it.

Not verified locally: this machine ran out of disk before a build completed, so
there is no post-change Lighthouse run yet — see the PR description.
…ze-dev

fix(fx): stop the add-money and withdraw screens freezing on a rate fetch
…-level

perf(capgo): stop logging transient update failures at error level
…loop-dev

fix(kyc): launch Manteca RFIs via start-action instead of the generic resubmit
innolope-dev and others added 9 commits August 27, 2026 14:14
…unch

Backgrounding a cold start and resuming it crashed the process on the next
frame: SurfaceControl.Transaction.hide() ran against a null starting-window
leash inside ActivityThread.syncTransferSplashscreenViewTransaction.

That handoff only happens because the splash plugin registers an
OnExitAnimationListener whenever launchFadeOutDuration > 0 (default 200),
which asks Android to transfer the system starting window into our process.
Setting it to 0 keeps the listener unregistered and leaves the teardown to
the system, and SplashScreen.hide() now parks until the app is active so the
first frame is never unblocked from the background.

Fixes PEANUT-UI-SVN
A Peanut Wallet P2P claim sends optimisticReturn: true, so POST /claim
answers 202 before broadcasting. When the broadcast then failed, this view
was already mounted: it stopped its poll with a sentinel 'FAILED' hash and
left the success card on screen, telling the recipient they had received
money that was never claimed.

Read the failure the API now persists instead. CHAIN_INFRA_UNAVAILABLE
means the failure was provably pre-submission and the link was rolled
back, so the retry copy plus a way back into the claim flow is honest
advice; anything else keeps the generic contact-support copy and offers
no retry.
appStateChange is never replayed, so a resume landing between a false
getState() and addListener() left the wait promise unresolved — and with
splashHidden already set the hard timeout could not retry, wedging the splash
for the rest of the launch. Register first, read second, and let a failed
registration reject so hideSplash falls through to hiding immediately.

Also annotate the exported return types the analyzer flagged.
…ew state

peanut-api-ts #1423 retires the source-of-funds cap-nudge to an "under
review" state once Sumsub accepts the document, since Sumsub going GREEN
does not raise the Manteca cap.

No renderer yet — capability-gate drops any hint without an effectiveDate
(firstAdvisory, capability-gate.ts:232), which is Bridge-only, so the
cap-nudge never reaches the screen today. Staging the strings so the
placement PR is copy-complete on arrival.

es-AR omitted deliberately: it is a deltas-only overlay on es-419 and
neither string needs an Argentine variant.
…rmed

The failure guard only fires once a poll returns FAILED. Between the
optimistic 202 and that answer — indefinitely, if the poll is slow or
erroring — the view fell through to the success card, the success sound
and the haptic, for a claim that had not happened yet.

Render a processing state while there is no hash and no failure, and fire
the success feedback on a confirmed hash rather than on mount. The
synchronous callers arrive with their hash and are unaffected.
remove() returns Promise<void>, so a bare jest.fn() made every successful
cleanup throw at listener.remove().catch() — swallowed by hideSplash, which
logged a false native-state warning and left the normal cleanup path
unexercised. Assert the clean paths stay quiet so it cannot regress.
The es-419 string uses the tuteo imperative "Verifica", which reaches
Argentine users unchanged through the es-AR fallback. The catalog's rule
is that a new tuteo string needs a voseo override, and every neighbouring
es-AR imperative follows it (Intentá, Probá, Agregá).

Only "description" needs one: "cta" is an infinitive and "underReview"
has no imperative or tú-form, so both are already correct in both dialects.

The glossary test matches a curated word list that doesn't include this
verb, so it stayed green — caught in review, not by CI.
'Solicitar' reads as filing a formal application, not asking a friend for
money. 'Recibir' matches how the action is described in Spanish and keeps
the nav verb consistent with the rest of the wallet.

Only navigation.request changes. The transaction-type noun stays
'Solicitud' — history rows read as a noun, not an action. es-AR is a
deltas-only overlay on es-419 and carried no override, so it inherits the
new string; the infinitive needs no voseo form.
copy(i18n): use 'Recibir' for the Spanish request action
@jjramirezn
jjramirezn deployed to content-publish August 27, 2026 15:44 — with GitHub Actions Active
…der-review-copy

i18n(limits): stage copy for the Manteca cap-nudge under-review state

@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: Changes requested

Request changes: lazy Sentry reporting discards explicit captures before initialization; the status parser also accepts a value that crashes rendering.

Findings

  • MAJOR · src/utils/sentry-lazy.ts:30 · Initialize Sentry before lazy captures
    On a marketing route, or before the app idle initializer runs, a caught failure such as a cookie-storage error or native-listener setup error reaches this wrapper. loadSentry() only imports the module; it does not call Sentry.init(), so the SDK has no client and discards captureException/captureMessage. Because these failures are caught, the window error buffer never sees them, and marketing routes never mount AppGlobals to initialize later. Make the wrappers await one shared initialization promise before capturing, and cover a pre-init explicit capture in tests.

  • MINOR · src/app/[locale]/(marketing)/status/types.ts:132 · Reject non-numeric status uptime values
    A 200 response with uptimePct: "100" passes isProvider because uptimePct is never validated. StatusBoard then calls uptimePct.toFixed(2) and throws, replacing the intended degraded-feed fallback with a server error. Require null or a finite number here and add the malformed-value case to parseStatusSummary.test.ts.

Checked clean

  • Verified the exact head, base SHA, merge base, trusted author, title, and description without reading issue or review comments.
  • Reviewed release workflows, version resolution, OTA floor handling, concurrency, tagging, and the human-authors gate.
  • Reviewed native routing and deferred links, Bridge/Manteca verification flows, locale sync, account deletion and balance gating, provider-tree splitting, status rendering, and Sentry initialization.
  • Ran 307 focused tests across release scripts and changed flows; all passed.
  • Generated Next route types and ran the TypeScript check locally; the exact head also has successful ci-success, unit, typecheck, eslint, e2e, and CodeQL checks.

Second opinion skipped: openrouter-timeout.

Exact head: 09f8d6827ebc · Context: repo

Comment thread src/utils/sentry-lazy.ts
type CaptureContext = SeverityLevel | Record<string, unknown>

export function captureException(error: unknown, hint?: Record<string, unknown>): void {
void loadSentry().then((S) => (S.captureException as (e: unknown, h?: unknown) => void)(error, hint))

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: Initialize Sentry before lazy captures

On a marketing route, or before the app idle initializer runs, a caught failure such as a cookie-storage error or native-listener setup error reaches this wrapper. loadSentry() only imports the module; it does not call Sentry.init(), so the SDK has no client and discards captureException/captureMessage. Because these failures are caught, the window error buffer never sees them, and marketing routes never mount AppGlobals to initialize later. Make the wrappers await one shared initialization promise before capturing, and cover a pre-init explicit capture in tests.

return (
!!p &&
typeof p.provider === 'string' &&
BUCKET_STATES.includes(p.state) &&

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.

MINOR: Reject non-numeric status uptime values

A 200 response with uptimePct: "100" passes isProvider because uptimePct is never validated. StatusBoard then calls uptimePct.toFixed(2) and throws, replacing the intended degraded-feed fallback with a server error. Require null or a finite number here and add the malformed-value case to parseStatusSummary.test.ts.

…e-crash

fix(native): stop the Android splash handoff crashing on a resumed launch
…l-failure

fix(claim): stop rendering a failed optimistic claim as a success
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.

5 participants