Skip to content

Prod Release — shared FX rate API · support reply badge · WS log hygiene (2026-08-07) - #2642

Draft
Hugo0 wants to merge 23 commits into
mainfrom
dev
Draft

Prod Release — shared FX rate API · support reply badge · WS log hygiene (2026-08-07)#2642
Hugo0 wants to merge 23 commits into
mainfrom
dev

Conversation

@Hugo0

@Hugo0 Hugo0 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Payload

FX — consume the shared backend rate policy (#2607)
Replaces the local conversion implementation with GET /fx/rate, so the wallet and Peanut Split share one contract.

  • Device-clock tolerance widened from 5 min to 6 h. At 5 min a phone whose clock had drifted rejected every response the backend could send, permanently, with no local fallback left. The checks that actually bound staleness compare two backend timestamps and stay tight.
  • 503 added to the Sentry skip list for /fx/rate — peanut-api already reports the upstream cause; reporting it client-side multiplies one incident by every mounted hook and its retries.
  • /fx/rate gets a real demo-api handler; without one a failed passthrough answered 200 {}.

Support — unread badge (#2639) — FE half of peanut-api-ts #1303. Ships together.

Observability (#2637) — stop shipping raw websocket frames to Sentry on a parse error.

CI (#2636) — workflow consolidation. No runtime effect.

Order — merge this AFTER the backend

peanut-api-ts #1308 must be deployed first. This PR drops api.frankfurter.app from CSP, so if the FE ships before /fx/* is live on prod, every rate surface blanks and the old path cannot be hot-restored.

Verification

FX verified against staging (which tracks dev): 43/43 on the shadow-compare, every Manteca pair resolving from Manteca rather than the reference feed.

Summary by CodeRabbit

  • New Features

    • Added live FX rate retrieval with validation, clearer errors, rate-limit handling, and safer fallback behavior.
    • Added unread support indicators in wallet navigation, with localized labels and automatic clearing after support messages are viewed.
    • Added support deep links that open the support panel directly.
    • Added reusable status indicators across profiles, transactions, and promotional areas.
  • Bug Fixes

    • Improved caching and error handling for exchange-rate requests.
    • Prevented sensitive WebSocket message contents from appearing in error logs.
    • Enabled repeated bundle uploads for existing versions.

0xkkonrad and others added 22 commits August 5, 2026 09:31
The parse-error catch logged the whole frame:

    console.error('Error parsing WebSocket message:', error, event.data)

console.error is not local. instrumentation-client.ts and
sentry.client.config.ts both register
captureConsoleIntegration({ levels: ['error', 'warn'] }), so every
console.error becomes a Sentry event. beforeSendHandler scrubs
request.headers, request.data, extra, contexts and breadcrumb data by key
name - it never touches event.message, and key-name redaction does
nothing to a raw serialized blob anyway.

The frames this handler receives are kyc_status_update,
sumsub_kyc_status_update, manteca_kyc_status_update, history_entry,
rain_card_balance_changed and user_rail_status_changed - user KYC state
and financial data. A malformed one carried all of it to Sentry.

Log the byte length instead. That still separates a truncated frame from
a malformed one, which is the only thing this catch ever needed.

The test pins it: it fails against the old line and passes against this
one. CodeQL alert #145 (js/log-injection, medium).
…ation

fix(ci): consolidate UI workflow reliability
Second half of TASK-21141. The backend now writes an in-app notification row
for every support reply; this shows it.

The Support icon in the mobile nav gets a pink dot while support has replied
and the user has not opened the chat. Opening the drawer clears it. The count
is server truth, read from /notifications/unread-count?category=support — the
Crisp widget is a sandboxed iframe on web and an event-less plugin on native,
so the client cannot work this out for itself.

Clearing hangs off isSupportModalOpen, which is the one flag every entry sets
before anything opens — the nav tap, openSupportWithMessage(), the push deep
link and the Capacitor path. One effect covers all four.

SupportDeepLink handles /home?support=open, the link a support push carries.

The pink dot was copy-pasted in three places and the badge would have made a
fourth, so it is now one IndicatorDot component. The three call sites render
the same as before — twMerge resolves the size and animation overrides. The
name is deliberately neutral: on a transaction card the dot means pending, on
the perk carousel it means claimable.

Do not merge before the backend PR is deployed. An old backend ignores the
category param and would light the badge for any unread notification.
jest.fn(async () => …) infers a zero-arg function, so calling it with the
category failed tsc. Local typecheck predated this mock and missed it.
The freshness check compared backend timestamps against the DEVICE clock with
a five-minute tolerance. A phone more than five minutes out rejected every
response the backend could send — permanently, and there is no local fallback
left to catch it. Device-clock comparisons now allow six hours; the checks
that actually bound staleness compare two backend timestamps and stay tight.

Sentry now skips 503 on /fx/rate. It means a provider leg is momentarily
absent, which peanut-api already reports with the upstream cause attached;
reporting it client-side multiplies one incident by every mounted hook and
its retries, and buries the signal that can be acted on.

Also gives /fx/rate a real demo-api handler. Without one a failed passthrough
fell to defaultShape and answered 200 {} — a contract violation dressed as a
success. A canned rate is not possible (handlers never see the query string,
and the validator rejects a mismatched pair), so it answers 503.
Retargeted from main to dev, so dev's locale redirect and native authReady
gate had to land alongside the FX changes.

proxy.ts: kept dev's locale block and Vary reasoning, carried over the
/api/exchange-rate cache exemption.

api-fetch.ts: authReady() now runs only when includeAuth is true. A public
rate read sends no token either way, so making it queue behind auth
hydration on a native cold start would delay it for nothing.
The three migrated call sites are only safe if twMerge wins their size and
animation overrides instead of emitting both. Asserting the resolved class
string pins that more precisely than a screenshot of a 10px dot.
String.length is UTF-16 code units. Getting a true byte count means
running the whole frame through a TextEncoder inside an error path, and
only the magnitude matters for telling a truncated frame from a
malformed one — so relabel rather than pay for the encode.
Five findings from /code-review on the badge lifecycle.

Opening the drawer is not the same as reading the reply. When the Crisp
bundle fails to load, this same component shows the email fallback instead —
the badge used to clear anyway and bury a reply nobody saw. The web path now
waits for CRISP_READY. The native path has no such signal, so it clears right
after openMessenger() instead.

A reply arriving while the drawer is open — the normal case in a live
conversation — used to light the badge with nothing new behind it and leave
it lit until the user opened support again. Clearing now also fires on the
closing edge.

Concurrent refreshes could resurrect a cleared badge: tapping a push fires a
foreground refetch (count 1), the deep link then clears and refetches (count
0), and if the first response lands last it wins. With no polling nothing
corrected it. Responses now carry a request id and stale ones are dropped.

Guests reach this drawer through claim and pay links, and were sending an
unauthenticated mark-read on every open. Gated on a resolved userId.

The nav badge announced nothing: aria-label on a bare span is ignored by
assistive tech and is an aria-prohibited-attr violation. It now carries
role="status" with a translated label. es-AR has no navigation block at all
and falls back, so only the three locales that do were touched.
feat(support): unread badge on the Support nav icon
refactor(fx): consume shared backend rate policy
fix(websocket): stop shipping raw frames to Sentry on a parse error
…o-dev-20260807

chore: back-merge main → dev (pre-release 2026-08-07)
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 7, 2026 5:56pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1b29a21b-69a3-4441-a63c-27c76a0a3768

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

FX rate flow

Layer / File(s) Summary
FX contract and public fetch path
src/types/api.openapi.json, src/utils/fx.utils.ts, src/utils/api-fetch.ts, src/utils/demo-api.ts
FX consumers now use the public /fx/rate contract without authentication. Responses undergo validation and timeout-protected proxying.
FX route, caching, and client failure handling
src/app/api/exchange-rate/route.ts, src/hooks/useExchangeRate.ts, src/proxy.ts, src/utils/sentry.utils.ts, src/**/__tests__/*
The compatibility route applies status-specific cache and retry behavior. Client conversion values clear after terminal errors.
FX rollout documentation
docs/api-types.md, src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/m/[slug]/MerchantLandingPage.tsx
FX endpoint ownership and references now use /fx/rate.

Support unread state

Layer / File(s) Summary
Unread notification state
src/services/notifications.ts, src/hooks/useSupportUnread.ts, src/hooks/__tests__/*, src/i18n/app/messages/*
Support-specific unread counts, mark-read requests, refresh triggers, cleanup, and localized labels were added.
Support interaction and navigation indicator
src/components/Global/SupportDrawer/*, src/components/Global/SupportDeepLink/*, src/app/(mobile-ui)/layout.tsx, src/components/Global/WalletNavigation/*
Support chat lifecycle events clear authenticated badges. Wallet navigation displays the unread state.
Shared status indicator
src/components/Global/IndicatorDot/*, src/components/Home/HomeCarouselCTA/*, src/components/Profile/components/*, src/components/TransactionDetails/*
A reusable IndicatorDot replaces repeated inline status-dot markup.

API contracts and diagnostics

Layer / File(s) Summary
OpenAPI endpoint and schema updates
src/types/api.openapi.json
ENS, consent, capability, legal-document, badge, and public FX contracts were added or extended.
Sanitized WebSocket parse logging
src/services/websocket.ts, src/services/__tests__/websocket-parse-error-pii.test.ts
Parse errors no longer log raw WebSocket frames. Tests verify that sensitive values remain absent.

Release and repository updates

Layer / File(s) Summary
Deployment and pull-request automation
.github/workflows/*
Capgo uploads accept existing versions. Content checks use the paginated pull-request Files API.
Content publication
src/content
The content submodule reference was updated.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ExchangeRateHook
  participant ExchangeRateRoute
  participant FxApi
  Client->>ExchangeRateHook: request exchange rate
  ExchangeRateHook->>FxApi: GET /fx/rate
  FxApi-->>ExchangeRateHook: rate or structured error
  ExchangeRateHook-->>Client: rate or cleared conversion state
  Client->>ExchangeRateRoute: request compatibility route
  ExchangeRateRoute->>FxApi: fetch validated rate
  FxApi-->>ExchangeRateRoute: status and rate data
  ExchangeRateRoute-->>Client: cache or no-store response
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: kushagrasarathe

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main FX, support badge, and WebSocket logging changes in this production release.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands.

Comment thread src/services/websocket.ts
// TextEncoder inside an error path, and only the magnitude
// matters here.
const size = typeof event.data === 'string' ? event.data.length : 'non-string'
console.error('Error parsing WebSocket message:', error, `(frame length: ${size})`)

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/hooks/__tests__/useSupportUnread.test.ts (1)

16-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an out-of-order response regression test.

The suite does not verify latestRequestId. Create two controlled requests. Resolve the newer request with { count: 0 }, then resolve the older request with { count: 1 }. Assert that the hook remains false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/__tests__/useSupportUnread.test.ts` around lines 16 - 72, Add a
regression test for latestRequestId in the useSupportUnread suite using two
controlled unread-count requests. Resolve the newer request with count 0 before
resolving the older request with count 1, then assert the hook remains false so
stale responses cannot update the state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Profile/components/ProfileMenuItem.tsx`:
- Line 59: Use one semantic accessibility contract for labelled indicator dots:
in src/components/Profile/components/ProfileMenuItem.tsx lines 59-59, add an
appropriate role with a localized accessible name or mark the visual dot
decorative; in src/components/Home/HomeCarouselCTA/CarouselCTA.tsx lines 84-84,
place the claimable label on a role-bearing wrapper or IndicatorDot and hide the
other element; in
src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx lines 32-37,
assert the intended role/name or hidden state and cover ProfileMenuItem
integration if required.

---

Nitpick comments:
In `@src/hooks/__tests__/useSupportUnread.test.ts`:
- Around line 16-72: Add a regression test for latestRequestId in the
useSupportUnread suite using two controlled unread-count requests. Resolve the
newer request with count 0 before resolving the older request with count 1, then
assert the hook remains false so stale responses cannot update the state.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 564cd269-dfb5-4ce7-89dd-67d84ced437a

📥 Commits

Reviewing files that changed from the base of the PR and between 9eb694d and 9a4dd66.

⛔ Files ignored due to path filters (1)
  • src/types/api.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (41)
  • .github/workflows/capgo-deploy-ios.yml
  • .github/workflows/capgo-deploy.yml
  • .github/workflows/content-publish-automerge.yml
  • docs/api-types.md
  • next.config.js
  • src/__tests__/proxy.test.ts
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/app/api/exchange-rate/__tests__/route.test.ts
  • src/app/api/exchange-rate/route.ts
  • src/app/m/[slug]/MerchantLandingPage.tsx
  • src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx
  • src/components/Global/IndicatorDot/index.tsx
  • src/components/Global/SupportDeepLink/index.tsx
  • src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx
  • src/components/Global/SupportDrawer/index.tsx
  • src/components/Global/WalletNavigation/index.tsx
  • src/components/Home/HomeCarouselCTA/CarouselCTA.tsx
  • src/components/Profile/components/ProfileMenuItem.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/content
  • src/hooks/__tests__/useExchangeRate.test.tsx
  • src/hooks/__tests__/useSupportUnread.test.ts
  • src/hooks/useExchangeRate.ts
  • src/hooks/useSupportUnread.ts
  • src/i18n/app/messages/en.json
  • src/i18n/app/messages/es-419.json
  • src/i18n/app/messages/pt-BR.json
  • src/proxy.ts
  • src/services/__tests__/websocket-parse-error-pii.test.ts
  • src/services/notifications.ts
  • src/services/websocket.ts
  • src/types/api.openapi.json
  • src/utils/__tests__/api-fetch.test.ts
  • src/utils/__tests__/demo-api.test.ts
  • src/utils/__tests__/fx.utils.test.ts
  • src/utils/__tests__/sentry.utils.test.ts
  • src/utils/api-fetch.ts
  • src/utils/demo-api.ts
  • src/utils/fx.utils.ts
  • src/utils/sentry.utils.ts
💤 Files with no reviewable changes (1)
  • next.config.js

<div className="h-2.5 w-2.5 rounded-full bg-primary-1" />
</div>
)}
{highlight && <IndicatorDot className="animate-pulse" aria-label="highlight-indicator" />}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one semantic accessibility contract for labelled dots.

The carousel and profile call sites preserve aria-label but do not provide a semantic role. The test checks only the attribute. A generic span or div does not expose the intended status to assistive technology.

  • src/components/Profile/components/ProfileMenuItem.tsx#L59-L59: add an appropriate role and localized accessible name, or mark the visual dot as decorative.
  • src/components/Home/HomeCarouselCTA/CarouselCTA.tsx#L84-L84: move the claimable label to a role-bearing wrapper or IndicatorDot, and hide the other element.
  • src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx#L32-L37: assert the intended role/name or hidden state, and cover ProfileMenuItem if integration behavior is required.
📍 Affects 3 files
  • src/components/Profile/components/ProfileMenuItem.tsx#L59-L59 (this comment)
  • src/components/Home/HomeCarouselCTA/CarouselCTA.tsx#L84-L84
  • src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx#L32-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Profile/components/ProfileMenuItem.tsx` at line 59, Use one
semantic accessibility contract for labelled indicator dots: in
src/components/Profile/components/ProfileMenuItem.tsx lines 59-59, add an
appropriate role with a localized accessible name or mark the visual dot
decorative; in src/components/Home/HomeCarouselCTA/CarouselCTA.tsx lines 84-84,
place the claimable label on a role-bearing wrapper or IndicatorDot and hide the
other element; in
src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx lines 32-37,
assert the intended role/name or hidden state and cover ProfileMenuItem
integration if required.

…w skip reason (#2645)

Without this the Explorer shows a balance-held user as 'due now', the exact
opposite of the truth in the tool an operator uses to verify api#1309.
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.

6 participants