Skip to content

refactor(fx): consume shared backend rate policy - #2607

Merged
Hugo0 merged 9 commits into
devfrom
fix/shared-fx-api
Aug 7, 2026
Merged

refactor(fx): consume shared backend rate policy#2607
Hugo0 merged 9 commits into
devfrom
fix/shared-fx-api

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace the UI's duplicate pair-conversion implementation with Peanut API GET /fx/rate.
  • Keep exact same-currency identity local and preserve the visible provider-both-else-reference-both selection policy.
  • Validate direction, atomic selection/provenance, decimal bounds, generation/effective timestamps, and freshness before using a response.
  • Handle 400/404/429/503 deliberately, honor Retry-After, avoid retrying deterministic/rate-limit outcomes, and keep expected outcomes out of Sentry.
  • Refresh the checked-in OpenAPI snapshot/types from the backend contract.

Why the UI should consume the same API

Split needs the backend contract, but moving the UI too is what makes that contract trustworthy: the production UI dogfoods the exact pair endpoint and validation rules that Split depends on. It removes a second implementation of provider/reference fallback, so a future provider, cache, or policy change cannot silently diverge between products.

This is not intended to change the rate a user sees. The API preserves the UI's existing atomic policy: use provider rates only when both legs are covered; otherwise use reference rates for both legs. PLN→EUR therefore remains reference/reference, with no hybrid source.

The actual operational change is that refresh/cache ownership moves to Peanut API. First-party clients call the public endpoint without cookies or bearer credentials; the legacy /api/exchange-rate wrapper remains for compatibility with a bounded five-minute success cache and no cached failures.

Rollout / review focus

  • Deploy and smoke API #1277 first.
  • Confirm same-currency identity works during an API outage and PLN→EUR validates as reference_pair.
  • Then deploy this PR and Split #4. Content facts are in mono #67.

QA

  • Final focused FX/client/hook/compatibility/Sentry regression: 4 suites, 93 tests passed, including success→failed-refetch fail-closed behavior.
  • Final full Jest: 180 suites, 2,399 passed, 3 skipped, 0 failed. The focused suite and typecheck were also rerun after the final hook change, OpenAPI sync, and latest-main merge.
  • Typecheck, changed production-file ESLint, Prettier, and git diff --check pass. Repository-wide ESLint remains advisory/red at its pre-existing 978-problem baseline; the only touched files it names are unchanged any/CommonJS lines in the existing Sentry utility/test.
  • Backend and UI OpenAPI JSON are byte-identical.
  • The branch contains current main; the final merge was conflict-free.
  • CodeRabbit previously hit its review limit, so this remains draft for human review.

Screenshots: N/A (no intended visible UI change).

@vercel

vercel Bot commented Aug 5, 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 1:01pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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 Plus

Run ID: 19b37d45-a775-4a69-91db-df6b1a82c97a

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

The change moves display-rate fetching to the backend /fx/rate contract. It adds response validation, public unauthenticated transport, route-specific caching and error handling, client integration, observability rules, rollout documentation, and unrelated OpenAPI contract additions.

FX rate rollout

Layer / File(s) Summary
FX contract and rate validation
src/types/api.openapi.json, src/utils/fx.utils.ts, src/utils/__tests__/fx.utils.test.ts
The API defines FX snapshot and pair-rate responses. fetchDisplayRate validates rates, timestamps, identity pairs, sources, and HTTP errors.
Public API transport
src/utils/api-fetch.ts, src/utils/demo-api.ts, src/utils/__tests__/api-fetch.test.ts, src/utils/__tests__/demo-api.test.ts
Unauthenticated requests omit bearer authentication. The /fx/rate passthrough forwards query parameters and applies a 10-second timeout.
Client and route integration
src/hooks/useExchangeRate.ts, src/app/api/exchange-rate/route.ts, src/proxy.ts, src/__tests__/proxy.test.ts, src/app/**/page.tsx, src/app/m/[slug]/MerchantLandingPage.tsx, src/app/api/exchange-rate/__tests__/route.test.ts
The hook uses the shared backend fetch for all clients. The compatibility route maps expected 400/404 errors. The proxy preserves route-owned caching for the exact exchange-rate path.
Observability and rollout support
src/utils/sentry.utils.ts, src/utils/__tests__/sentry.utils.test.ts, docs/api-types.md, next.config.js
Expected public FX 400/404 responses bypass Sentry reporting. Documentation records rollout sequencing and provider precedence. The Frankfurter CSP entry is removed.

API contract expansion

Layer / File(s) Summary
ENS, consent, and legal-document contracts
src/types/api.openapi.json
The OpenAPI specification adds ENS reverse lookup, optional ENS chainId, consent endpoints, and accepted-document fields for passkey and Rain card requests.
Badge code contract
src/types/api.openapi.json
The grant-badge code enum adds NITA.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant useExchangeRate
  participant fetchDisplayRate
  participant PeanutAPI
  Client->>useExchangeRate: Request display rate
  useExchangeRate->>fetchDisplayRate: Fetch normalized currency pair
  fetchDisplayRate->>PeanutAPI: GET /fx/rate without authentication
  PeanutAPI-->>fetchDisplayRate: Validated rate or 400/404/503 response
  fetchDisplayRate-->>useExchangeRate: Rate or typed error
  useExchangeRate-->>Client: Display rate result
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: hugo0, abalinda

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: refactoring FX consumers to use the shared backend rate policy.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shared-fx-api

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

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7079 → 7078.73 (-0.27)
Findings: +13 net (+376 new, -363 resolved)

🆕 New findings (376)

  • critical complexity — src/utils/demo-api.ts — CC 109, MI 59.48, SLOC 934
  • high structural-dup — types/api.generated.ts:383 — 83 duplicate lines / 388 tokens with types/api.generated.ts:1096
  • high structural-dup — types/api.generated.ts:393 — 74 duplicate lines / 348 tokens with types/api.generated.ts:1106
  • high structural-dup — types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9701
  • high structural-dup — types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9787
  • high structural-dup — types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9378
  • high structural-dup — types/api.generated.ts:10790 — 61 duplicate lines / 188 tokens with types/api.generated.ts:10892
  • high hotspot — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 60 commits, +588/-497 lines since 6 months ago
  • high structural-dup — types/api.generated.ts:9323 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9475
  • high structural-dup — types/api.generated.ts:9323 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9646
  • high structural-dup — types/api.generated.ts:9323 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9904
  • high complexity — src/proxy.ts — CC 36, MI 57.21, SLOC 116
  • medium high-mdd — src/hooks/useExchangeRate.ts:27 — useExchangeRate: MDD 57.2 (uses across many lines from declarations)
  • medium high-mdd — src/utils/sentry.utils.ts:389 — fetchWithSentry: MDD 53.8 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:3379 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6768
  • medium structural-dup — types/api.generated.ts:3379 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7343
  • medium structural-dup — types/api.generated.ts:704 — 42 duplicate lines / 152 tokens with types/api.generated.ts:2415
  • medium structural-dup — types/api.generated.ts:7420 — 40 duplicate lines / 108 tokens with types/api.generated.ts:7492
  • medium structural-dup — types/api.generated.ts:9338 — 40 duplicate lines / 120 tokens with types/api.generated.ts:9575
  • medium structural-dup — types/api.generated.ts:893 — 39 duplicate lines / 137 tokens with types/api.generated.ts:2801

…and 356 more.

✅ Resolved (363)

  • src/utils/demo-api.ts — CC 107, MI 59.5, SLOC 917
  • types/api.generated.ts:346 — 83 duplicate lines / 384 tokens with types/api.generated.ts:1059
  • types/api.generated.ts:356 — 74 duplicate lines / 344 tokens with types/api.generated.ts:1069
  • types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9524
  • types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9610
  • types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9201
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 59 commits, +587/-496 lines since 6 months ago
  • types/api.generated.ts:9146 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9298
  • types/api.generated.ts:9146 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9469
  • types/api.generated.ts:9146 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9727
  • src/proxy.ts — CC 35, MI 57.26, SLOC 116
  • src/utils/sentry.utils.ts:382 — fetchWithSentry: MDD 53.8 (uses across many lines from declarations)
  • src/hooks/useExchangeRate.ts:28 — useExchangeRate: MDD 49.1 (uses across many lines from declarations)
  • types/api.generated.ts:3207 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6591
  • types/api.generated.ts:3207 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7166
  • types/api.generated.ts:667 — 42 duplicate lines / 152 tokens with types/api.generated.ts:2243
  • types/api.generated.ts:7243 — 40 duplicate lines / 108 tokens with types/api.generated.ts:7315
  • types/api.generated.ts:9161 — 40 duplicate lines / 120 tokens with types/api.generated.ts:9398
  • types/api.generated.ts:856 — 39 duplicate lines / 137 tokens with types/api.generated.ts:2629
  • types/api.generated.ts:7243 — 39 duplicate lines / 109 tokens with types/api.generated.ts:7389

…and 343 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/app/api/exchange-rate/route.ts 8.1 9.9 +1.9
src/utils/fx.utils.ts 6.0 2.2 -3.8

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2742 ran, 0 failed, 0 skipped, 46.3s

📊 Coverage (unit)

metric %
statements 64.7%
branches 49.0%
functions 54.3%
lines 65.3%
⏱ 10 slowest test cases
time test
3.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/auth-token.test.ts › returns the token hydrated from Preferences after authReady
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/utils/__tests__/auth-token.test.ts › should return null when cookie is not set
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 (2)
src/utils/__tests__/fx.utils.test.ts (1)

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

The implausibly small rate fixture does not isolate MIN_DISPLAY_RATE.

'0.0000000000000000001' has 19 decimal places. PLAIN_DECIMAL allows at most 18, so the payload is rejected by the precision rule before the numeric bound is reached. The lower bound stays untested.

Use a value with 18 decimals that is still below 1e-18.

♻️ Proposed fixture change
-        ['implausibly small rate', { ...validResponse, rate: '0.0000000000000000001' }],
+        // 18 decimals, so PLAIN_DECIMAL passes and MIN_DISPLAY_RATE is the rule under test.
+        ['implausibly small rate', { ...validResponse, rate: '0.000000000000000000' }],

Note: '0.000000000000000000' parses to 0, which is below MIN_DISPLAY_RATE. Pick any 18-decimal string under 1e-18 that suits the intent.

🤖 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/utils/__tests__/fx.utils.test.ts` at line 74, Update the “implausibly
small rate” fixture in the rate validation tests to use a numeric string with
exactly 18 decimal places that remains below MIN_DISPLAY_RATE, so it passes
PLAIN_DECIMAL precision validation and specifically exercises the lower-bound
check.
src/utils/fx.utils.ts (1)

76-80: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout to the FX request.

fetchDisplayRate runs on the Next.js server for /api/exchange-rate and in native clients. apiFetch supports timeoutMs, but this call omits it. If api.peanut.me stalls, the route handler holds the request until the platform limit.

src/utils/demo-api.ts already uses a 10-second timeout for the native FX passthrough. Use the same bound here.

♻️ Proposed fix to bound the outbound call
     const response = await apiFetch(`/fx/rate?${query.toString()}`, {
         method: 'GET',
         includeAuth: false,
         credentials: 'omit',
+        timeoutMs: 10_000,
     })
🤖 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/utils/fx.utils.ts` around lines 76 - 80, Update the apiFetch call in
fetchDisplayRate to pass the timeoutMs option using the existing 10-second bound
from src/utils/demo-api.ts, while preserving the current GET method,
authentication, and credential settings.
🤖 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/proxy.ts`:
- Around line 57-60: Update the exchange-rate route handler in
src/app/api/exchange-rate/route.ts so every 400, 404, and 500 error response
explicitly sets Cache-Control to no-store, while preserving the existing
s-maxage/stale-while-revalidate policy on successful responses.

---

Nitpick comments:
In `@src/utils/__tests__/fx.utils.test.ts`:
- Line 74: Update the “implausibly small rate” fixture in the rate validation
tests to use a numeric string with exactly 18 decimal places that remains below
MIN_DISPLAY_RATE, so it passes PLAIN_DECIMAL precision validation and
specifically exercises the lower-bound check.

In `@src/utils/fx.utils.ts`:
- Around line 76-80: Update the apiFetch call in fetchDisplayRate to pass the
timeoutMs option using the existing 10-second bound from src/utils/demo-api.ts,
while preserving the current GET method, authentication, and credential
settings.
🪄 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 Plus

Run ID: b23aed96-a4b0-4245-93bd-4d9dcb5a89a6

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5cd1e and 5f95fad.

⛔ Files ignored due to path filters (1)
  • src/types/api.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (18)
  • docs/api-types.md
  • next.config.js
  • src/__tests__/proxy.test.ts
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/api/exchange-rate/__tests__/route.test.ts
  • src/app/api/exchange-rate/route.ts
  • src/app/m/[slug]/MerchantLandingPage.tsx
  • src/hooks/useExchangeRate.ts
  • src/proxy.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

Comment thread src/proxy.ts Outdated
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Addressed all CodeRabbit findings in 5ade3d4: error responses are explicitly no-store, FX calls have a 10s timeout, and the lower-bound fixture now passes decimal-shape validation before exercising the numeric bound. Focused 47 tests, typecheck, changed ESLint, and formatting pass.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@abalinda

abalinda commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Read #2607 together with peanut-api-ts#1277, since they ship as a pair. Direction looks right — it finishes the single display-rate path #2601 started, and the policy belongs behind one endpoint. Notes below, roughly in the order I would deal with them. All display-rate, no money movement.

Worth a decision before merge

  1. resolvePair substitutes the reference rate for a missing provider leg (peanut-api-ts/src/fx/pair.ts:64). One Manteca call over the 3s timeout and ARS resolves from Frankfurter — the official rate, not the market rate we transact at. It goes out as a normal 200 with selection: 'reference_pair', cached 5 min plus s-maxage=300. Either flag it on the response so the FE can label it, or skip the substitution for provider-domain currencies.

  2. 404 covers two different things (src/routes/fx/index.ts:168): a pair we do not support, and a pair whose provider leg failed on the last refresh. The FE treats 404 as terminal (no retry, useExchangeRate.ts:98) and suppresses it in Sentry, so the second case stays quiet for up to 5 minutes. Keeping 404 for "unsupported" only would fix both sides.

  3. Clock check allows 5 min of slow-clock skew (peanut-ui/src/utils/fx.utils.ts:72). A device further behind rejects every response the backend can send, permanently, with no fallback left. Widening the past-side allowance costs nothing.

Smaller

  • sentry.utils.ts:21 skips 400/404/429 but not 503 — the status the new route emits on a real outage. With 4 retries and 3 hooks on the merchant page, one incident gets loud.
  • demo-api.ts:21 adds /fx/rate to PASSTHROUGH_GET with no canned handler, so a failed passthrough returns 200 {} from defaultShape and reads as a contract violation. The other two passthrough entries have handlers.
  • manteca/service.ts:319 returns undefined when Manteca answers 200 with an empty body, where response.json() used to throw. The as MantecaPrice cast hides it from tsc.
  • Freshness bounds and the magnitude envelope in fx.utils.ts:15 are hand-copies of peanut-api-ts/src/fx/validation.ts. A backend-only change to either breaks the deployed UI. Deriving them from the generated types, or sending them in the response, keeps the DRY win.
  • Deploy order: UI first blanks every rate surface, and next.config.js drops api.frankfurter.app from CSP so the old path cannot be hot-restored. Backend first.

How this pairs with #1275 / #2601

Two direct overlaps.

  • feat: add manteca claim link #1275 flagged trimming the FE's aspirational MANTECA_CURRENCIES (COP/CRC/PUSD/GTQ/PHP/BOB — /manteca/prices 400s on them today). feat: fulfill request using manteca #1277 adds a third copy of the provider map in src/fx/provider-adapters.ts while peanut-ui/src/app/actions/currency.ts keeps its own, still read by useCurrency for the add-money limits check. Those same six currencies are what produce the 404 in point 2. One backend-owned list closes the feat: add manteca claim link #1275 follow-up and this together.
  • feat: add manteca claim link #1275 chose to fail closed on unusable provider data (500 rather than "NaN"). resolvePair here picks the opposite rule for the same class of problem — substitute quietly, return 200. Worth settling on one.

Rest of the FX module reads well.

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.
@Hugo0
Hugo0 changed the base branch from main to dev August 7, 2026 12:51
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.
@Hugo0
Hugo0 marked this pull request as ready for review August 7, 2026 13:00
@Hugo0

Hugo0 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Peanut team has taken this branch over. Retargeted maindev (policy) and merged dev in — it was cut from main, so proxy.ts and api-fetch.ts conflicted.

Addressed the client-side items from @abalinda's review:

Drifted device clock (review point 3) — this was the sharpest one. The freshness check compared backend timestamps against the device clock with a 5-minute tolerance, so a phone more than 5 minutes out rejected every response the backend could send, permanently, with no local fallback left to catch it. Device-clock comparisons now allow 6 hours. The checks that actually bound staleness compare two backend-stamped times and stay tight, so this costs correctness nothing. Tests cover ±3h.

503 in the Sentry skip list — a 503 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 (three on the merchant page alone) and buries the actionable backend signal.

/fx/rate demo-api handler — without one a failed passthrough fell to defaultShape and answered 200 {}, a contract violation dressed as a success. A canned rate isn't possible (handlers never see the query string, and the validator rejects a mismatched pair), so it answers 503 and the hook fails closed.

Merge resolution worth a look: 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.

Deploy order is not optional — backend first. This PR drops api.frankfurter.app from CSP, so shipping UI ahead of the API blanks every rate surface with no way to hot-restore the old path.

All green, including eslint (merging dev brought in its fixes). 2,739 tests pass, typecheck clean.

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.

3 participants