Skip to content

Gold: faster landing, sturdier KYC polling, CPF check and key cleanup - #1392

Merged
ebma merged 37 commits into
stagingfrom
fix/gold-followups
Oct 1, 2026
Merged

ebma merged 37 commits into
stagingfrom
fix/gold-followups

Conversation

@ebma

@ebma ebma commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Gold follow-ups from #1388 (merged).

  • Landing renders before Privy. The page stayed a spinner until the Privy chunk (about 750 kB gzipped) loaded and Privy reported ready. App now renders outside PrivyProvider, which hands its auth state up through a small bridge, so the landing paints from the entry chunk (305 kB gzipped) and only the sign-in buttons wait. Returning users (a stored Privy session) keep the loader, so the landing does not flash before their dashboard. The Privy subtree is created once; re-rendering it on every auth update looped.
  • KYC polling. One failed status request ended the check; an expired attempt polled for five minutes; a rejection showed Avenia's English reason code (face, tax_id, …) and sent the user back to the dead attempt. Polling now tolerates transient errors and the 404/409 the API returns while it reconciles a new attempt (up to 5 in a row), stops on every final state, explains rejections and expiry in Portuguese and returns to the form for a new attempt (the API accepts a new submission for a retryable attempt). Polling stops when the modal closes.
  • CPF check digits. The KYC form accepted any 11 digits, so a typo created a subaccount bound to the wrong CPF. It now validates the check digits (the API gets the same check in its own PR).
  • Ephemeral keys. Deleted once a ramp completes: completed ramps are swept server-side with presigned cleanup transactions, so only failed ramps' keys can still recover funds.

Review fixes

A deep review of this PR found no blocker; these commits fix everything it raised.

  • Focus kept while typing. The modal re-focused its close button on every app render (the balance poll re-renders every 30 s), so the field being typed in lost focus and the next Space or Enter closed the flow. Pre-existing, fixed here.
  • KYC polling: a status answer that arrives after the modal closed no longer approves a flow that is gone; 404/409s that never clear and a refused new attempt (Avenia did not mark the old one retryable) now end in Portuguese with a support reference instead of the API's English text; the rejection reason scrolls into view as an alert; after approval the step offers "Continuar" instead of an endless spinner when the sell quote still finds the account unapproved.
  • Copy: tax_id means Avenia did not find the CPF; wording aligned with the rest of the app.
  • CPF: 01234567890 rejected like the shared helper; the field error is linked to its input and aligned under it.
  • Privy: the Google sign-in return (privy_oauth_code) no longer flashes the landing; a Privy that fails to initialize now says to reload instead of leaving a disabled button (or an endless loader for returning users); the buttons show "Carregando…" while Privy loads.
  • Key store closes its database even when a write fails.

Not changed: selling still needs ETH for gas (decided to keep for now). Showing the retry option only when Avenia allows a new attempt needs retryable in the API's getKycStatus response (API follow-up).

Test plan

  • bun run test:gold: 40 pass. Each new test fails without its fix; the abort, KYC error and CPF tests were checked against the pre-fix code.
  • Browser (dev build): focus stays in the amount field across forced app re-renders (moved to the close button before the fix); approved KYC view and scrolled-in rejection alert; with an unreachable Privy app the sign-in shows "Carregando…", then the reload message, and a returning user gets the landing instead of an endless loader.
  • bun run build:gold with the production env: the entry chunk contains the landing; Privy stays a separate chunk.
  • Local non-demo run with a dummy Privy app id: the landing renders immediately and stays up, "Continuar com Google" is disabled until Privy is ready, a stored privy:token shows the loader instead.
  • Deploy preview with the real Privy app: sign in, reload while signed in (no landing flash), buy flow reaches the review.
  • A KYC attempt that is rejected (e.g. a blurry selfie) returns to the form with a Portuguese reason, and a second attempt goes through.

🤖 Generated with Claude Code

alexatsatoshi and others added 29 commits September 29, 2026 14:47
The gold unit tests cover quote validation, recovery checkpoints and gas
preflight, but no root script ran them. Wiring them into
.github/workflows/ci.yml is left to a maintainer (see the PR description).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The standalone gold.satoshipay.io release sent nosniff, referrer and
permissions headers; the Netlify-served /pt-br/gold/ sends none and can be
framed by any site while it signs transactions and collects PIX keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hero PNG was 1.96 MB and the wordmark 273 KB on the first screen;
the WebP is 209 KB and the wordmark keeps 3x its largest display size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chart only fetched 42 days, so 1A and Tudo showed six weeks, and the
change badge always showed the 42-day change whatever the period.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One failed status request ended polling and sent the buyer to the support
escalation screen although the ramp kept running server-side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The countdown decremented once per timer tick, and browsers pause timers
while the buyer is in the banking app, so it showed time that had passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fee disclosure and legal notes used 11px #837d74 on #fffdf9 (4.0:1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gold asked /v1/ramp-info whether the user passed KYC, but that route only
accepts API credentials and reports on their owner, never on the OTP
session. Production sets no public key, so every buy stopped after the
e-mail code with "A public or secret API credential is required" and every
sell looped back to the code. /v1/brl/getUser resolves the session user's
approved Avenia account; skip KYC only on CONFIRMED, as the widget does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v1/ramp/:id returns unsignedTxs only with showUnsignedTxs=true and the
SDK status call never sends it, so "Continuar esta venda" always failed with
"Não foi possível recuperar as confirmações" after a declined wallet prompt,
a reload or a receipt timeout, although the copy invites the user to retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both buttons resume the stored operation, and it was only cleared on a
terminal status. The API never expires an unstarted ramp, and a failed
sell was never cleared, so one abandoned PIX or failed sell locked the
wallet out of buying and selling on that device for good.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Status responses have no expiresAt, so a resumed PIX screen invented a
fresh ten-minute countdown; start is refused 15 minutes after creation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gold sent no locale, so the Vortex OTP e-mail arrived in English and the
API reset the user's stored locale to en-US; the template renders
Portuguese for pt-BR, as the widget requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Any refresh error cleared the stored session, so a network blip or a 503
from /v1/auth/refresh logged the user out mid-operation. The security spec
(Supabase OTP rule 9) allows teardown only when the refresh returns 401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API passes country and countryOfTaxId through to Avenia unchanged. The
widget's KYC (packages/kyc) sends "BRA", gold sent "BR". No unit test: the
payload is built inline in KycStep, which the node test runner cannot load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every buy and sell asked for a new e-mail code, even with a valid Vortex
session in the same tab. Like the widget, the session now keeps the e-mail
it was verified for and is reused only for that address; a rejected session
falls back to the code step. "Voltar" no longer leads into a code step the
user may have skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a reusable session the amount step now requests the quote itself, so an
amount typed meanwhile was shown on the review as the purchase value while
the PIX is for the quoted amount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The amount step now shows the flow error, so going back from a wrong e-mail
code or a refreshed quote left that message under the amount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Failed and abandoned operations are now cleared so they stop blocking new
ones, which also removed the only place on the device that showed their
operation code; support needs it when a PIX was debited or a sell stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resuming a sell now signs transactions fetched from the ramp status, and
neither the SDK nor the API checks what the user's wallet signs. Only the
approve of at most the quoted PAXG to Squid's router and the router swap
may reach the wallet; the unused typed-data handler is dropped, so any
other transaction kind fails closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API refuses to record or start a ramp 15 minutes after registration,
so a swap sent later, for example when resuming an old sell, moves the
gold to the ephemeral account of a ramp that can never run. Stop four
minutes before the deadline and release the dead ramp instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One failed status request ended the check, an expired attempt polled for
five minutes, and a rejection showed Avenia's English reason code and sent
the user back to the old attempt. Tolerate transient and reconciliation
errors, stop on every final state, explain it in Portuguese and return to
the form for a new attempt; stop polling when the modal closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form accepted any 11 digits, so a typo created a subaccount bound to
the wrong CPF, and the API is about to reject such CPFs outright.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keys were stored for every ramp and never removed. A completed ramp's
ephemeral accounts are swept with presigned cleanup transactions, so only
failed ramps' keys can still recover funds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page stayed a spinner until the Privy chunk (about 750 kB gzipped)
loaded and Privy reported ready. App now renders outside PrivyProvider,
which hands its auth state up, so the landing paints from the entry chunk
and only the sign-in buttons wait; returning users keep the loader so the
landing does not flash before their dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit 59147ba
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6abcd2c2deb3760008adb562
😎 Deploy Preview https://deploy-preview-1392--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit 59147ba
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6abcd2c2f11dc50008a492e4
😎 Deploy Preview https://deploy-preview-1392--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for vrtx-dashboard canceled.

Name Link
🔨 Latest commit 59147ba
🔍 Latest deploy log https://app.netlify.com/projects/vrtx-dashboard/deploys/6abcd2c29a92b9000777456d

ebma and others added 8 commits September 30, 2026 11:06
The modal re-ran its focus effect whenever onClose changed identity, and
every caller passes a new function on each render. The balance poll
re-renders the app every 30 seconds, so focus jumped from the field being
typed in to the close button, where the next Space or Enter closed the
flow and discarded the form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing the modal aborted the wait between polls but not a status request
already in flight, so a late approval still called onApproved and, in the
sell flow, fetched a new quote for a flow that was gone. The abort path had
no test at all; cover both pollers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 404 or 409 from the KYC status check that never cleared reached the
screen as the API's English text after five polls, and a new attempt the
API refused (Avenia did not mark the old one retryable) showed an English
409 behind a button that only resubmitted it. Both now say in Portuguese
what happened and point to support with the request code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avenia's tax_id reason means the CPF was not found, not that it differs
from the document, and the new messages drifted from the app's wording
("de novo", the mobile-only "toque em", advice Avenia does not give).
Assert every reason's message and that no reason code leaks through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejection returns to the long form with its explanation below the
fields, out of view on a phone and not announced; it now scrolls into
view as an alert. After approval the step no longer stays on the checking
spinner: when the sell quote still finds the account unapproved while
Avenia syncs, the user can continue instead of waiting forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared CPF check rejects digit runs, and 01234567890 is the one run
besides repeated digits whose check digits are valid, so gold accepted it.
The field error now names its input for screen readers and sits under the
field instead of centred with the OTP step's margin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Google sign-in returns to the page with privy_oauth_code before Privy
has stored its session, so the landing flashed by again; the session
check now counts that return and lives in a tested helper. A Privy that
failed to initialize left a disabled button with no explanation, and a
returning user on an endless loader; the error Privy reports now ends the
wait and says to reload. The sign-in buttons show that Privy is loading,
and the Privy subtree is kept in state because useMemo may be discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Storing and deleting ephemeral keys skipped db.close() when the
transaction rejected, and the caller swallows delete errors, so a failure
left a connection open that blocks a later upgrade of the store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ebma
ebma merged commit 591d11f into staging Oct 1, 2026
6 checks passed
@ebma
ebma deleted the fix/gold-followups branch October 1, 2026 17:40
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.

2 participants