Gold: faster landing, sturdier KYC polling, CPF check and key cleanup - #1392
Merged
Merged
Conversation
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>
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vrtx-dashboard canceled.
|
This was referenced Sep 29, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Gold follow-ups from #1388 (merged).
ready.Appnow renders outsidePrivyProvider, 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.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.Review fixes
A deep review of this PR found no blocker; these commits fix everything it raised.
tax_idmeans Avenia did not find the CPF; wording aligned with the rest of the app.01234567890rejected like the shared helper; the field error is linked to its input and aligned under it.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.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
retryablein the API'sgetKycStatusresponse (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.bun run build:goldwith the production env: the entry chunk contains the landing; Privy stays a separate chunk.privy:tokenshows the loader instead.🤖 Generated with Claude Code