From 0d47bb1c04d14eb832817abe633f4c4cc034245a Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 09:38:04 +0000 Subject: [PATCH 01/12] Adjust templates --- supabase/templates/magic_link.html | 6 +++--- supabase/templates/signup.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/supabase/templates/magic_link.html b/supabase/templates/magic_link.html index ea3c14ea1..4b73a1f64 100644 --- a/supabase/templates/magic_link.html +++ b/supabase/templates/magic_link.html @@ -143,8 +143,8 @@

VORTEX

diff --git a/supabase/templates/signup.html b/supabase/templates/signup.html index e81108425..700b9ed90 100644 --- a/supabase/templates/signup.html +++ b/supabase/templates/signup.html @@ -145,8 +145,8 @@

Confirm your email address

From afb5f2481a0417a7d326d374d990904e59e630be Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 10:37:10 +0000 Subject: [PATCH 02/12] Add back button to auth email card --- .../src/components/widget-steps/AuthEmailStep/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx b/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx index ef3374ac8..a82bf12d1 100644 --- a/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx +++ b/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx @@ -5,6 +5,7 @@ import * as yup from "yup"; import { useRampActor } from "../../../contexts/rampState"; import { cn } from "../../../helpers/cn"; import { useQuote } from "../../../stores/quote/useQuoteStore"; +import { MenuButtons } from "../../MenuButtons"; import { QuoteSummary } from "../../QuoteSummary"; const emailSchema = yup.string().email().required(); @@ -43,6 +44,9 @@ export const AuthEmailStep = ({ className }: AuthEmailStepProps) => { return (
+
+ +

{t("components.authEmailStep.title")}

From a07b68f1656a6bb3ecd6aa1eecebb6179e55f27a Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 15:43:40 +0000 Subject: [PATCH 03/12] Add document for the ramp machine widget flow --- docs/architecture/ramp-machine-widget-flow.md | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 docs/architecture/ramp-machine-widget-flow.md diff --git a/docs/architecture/ramp-machine-widget-flow.md b/docs/architecture/ramp-machine-widget-flow.md new file mode 100644 index 000000000..51a2646ca --- /dev/null +++ b/docs/architecture/ramp-machine-widget-flow.md @@ -0,0 +1,146 @@ +# Ramp Machine + Widget Card Flow + +This document maps how the top-level XState machine (`ramp.machine.ts`) drives what the widget renders, and which UI actions send events back into the machine. + +## Source of truth +- Machine: `apps/frontend/src/machines/ramp.machine.ts` +- KYC node: `apps/frontend/src/machines/kyc.states.ts` +- Widget rendering switch: `apps/frontend/src/pages/widget/index.tsx` +- URL/bootstrap events: `apps/frontend/src/hooks/useRampUrlParams.ts` +- Main CTA behavior: `apps/frontend/src/components/RampSubmitButton/RampSubmitButton.tsx` + +## High-level flow (machine) +```mermaid +flowchart TD + A[Idle] -->|SET_QUOTE| B[LoadingQuote] + B -->|quote loaded| C[QuoteReady] + C -->|CONFIRM| D[RampRequested] + + D -->|kycNeeded = true| E[KYC] + D -->|kycNeeded = false and BRL| F[KycComplete] + + E -->|child KYC done| F + E -->|child KYC error| X[KycFailure] + X --> R[Resetting] + + F -->|PROCEED_TO_REGISTRATION| G[RegisterRamp] + F -->|GO_BACK| C + + G --> H[UpdateRamp] + + H -->|SELL onDone| I[StartRamp] + H -->|BUY onDone| H + H -->|PAYMENT_CONFIRMED 'BUY'| I + + I -->|callbackUrl present| J[RedirectCallback] + I -->|no callbackUrl| K[RampFollowUp] + + K -->|FINISH_OFFRAMPING| R + J -->|after 5s + cleanup| A + + C -->|GO_BACK| A + A -->|INITIAL_QUOTE_FETCH_FAILED| Q[InitialFetchFailed] + + R -->|urlCleaner done| A + + A --> AA[CheckAuth] + AA -->|authenticated| C + AA -->|not authenticated| AB[EnterEmail] + AB --> AC[CheckingEmail] + AC --> AD[RequestingOTP] + AD --> AE[EnterOTP] + AE --> AF[VerifyingOTP] + AF -->|success| C + AF -->|error| AE + + G -->|error| Z[Error] + H -->|error| Z + I -->|error| Z +``` + +## Widget card resolution order (important) +`WidgetContent` picks the first matching branch in this order: + +1. `ErrorStep` if machine matches `Error` +2. `RampFollowUpRedirectStep` if machine matches `RedirectCallback` +3. `AuthEmailStep` for `CheckAuth | EnterEmail | CheckingEmail | RequestingOTP` +4. `AuthOTPStep` for `EnterOTP | VerifyingOTP` +5. `MoneriumRedirectStep` if Monerium child actor exists and child state is `Redirect` +6. `SummaryStep` for `KycComplete | RegisterRamp | UpdateRamp | StartRamp` +7. Avenia branch if Avenia child actor exists: + - `AveniaKYBFlow` when CNPJ + `kybUrls` present + - else `AveniaKYBForm` (CNPJ) + - else `AveniaKYCForm` (CPF) +8. `InitialQuoteFailedStep` for `InitialFetchFailed` +9. fallback: `DetailsStep` + +## KYC subflow and cards +```mermaid +flowchart TD + KYC[KYC.Deciding] -->|fiat = BRL| AV[Avenia child machine] + KYC -->|fiat = EURC and BUY| MO[Monerium child machine] + KYC -->|otherwise| ST[Stellar child machine] + + AV -->|done without error| VC[VerificationComplete] + MO -->|done with authToken| VC + ST -->|done with paymentData| VC + + VC --> KC[KycComplete] + + AV -->|error| KF[KycFailure] + MO -->|error| KF + ST -->|error| KF + + MO -. child state Redirect .-> MR[MoneriumRedirectStep card] + + AV -. child exists .-> AC[Avenia cards] + AC --> AKYC[AveniaKYCForm] + AC --> AKYB[AveniaKYBForm] + AC --> AKYBF[AveniaKYBFlow] +``` + +## State-to-card map +| Machine state / condition | Card shown | +|---|---| +| `Error` | `ErrorStep` | +| `RedirectCallback` | `RampFollowUpRedirectStep` | +| `CheckAuth`, `EnterEmail`, `CheckingEmail`, `RequestingOTP` | `AuthEmailStep` | +| `EnterOTP`, `VerifyingOTP` | `AuthOTPStep` | +| Monerium child actor state `Redirect` | `MoneriumRedirectStep` | +| `KycComplete`, `RegisterRamp`, `UpdateRamp`, `StartRamp` | `SummaryStep` | +| Avenia actor exists + CNPJ + `kybUrls` | `AveniaKYBFlow` | +| Avenia actor exists + CNPJ (no `kybUrls`) | `AveniaKYBForm` | +| Avenia actor exists + CPF | `AveniaKYCForm` | +| `InitialFetchFailed` | `InitialQuoteFailedStep` | +| everything else | `DetailsStep` | + +## Key UI -> machine events +- `DetailsStep` submit -> `CONFIRM` (via `useRampSubmission`) and `SET_ADDRESS` +- `RampSubmitButton`: + - in `QuoteReady` -> `CONFIRM` + - in `KycComplete` -> `PROCEED_TO_REGISTRATION` + - default -> `SummaryConfirm` + - in `UpdateRamp` on onramp -> `PAYMENT_CONFIRMED` + - if quote expired -> `RESET_RAMP` +- `AuthEmailStep` -> `ENTER_EMAIL` +- `AuthOTPStep` -> `VERIFY_OTP`, `CHANGE_EMAIL` +- Error/initial-failure/retry actions -> `RESET_RAMP` +- Back button (`StepBackButton`) primarily sends `GO_BACK` (with Avenia-specific child events in document/liveness/KYB sub-steps) + +## URL/bootstrap interactions +`useSetRampUrlParams` seeds machine state at widget load: +- `SET_QUOTE_PARAMS` +- `SET_EXTERNAL_ID` (if provided) +- `SET_QUOTE` (provided quoteId or fetched quote) +- `INITIAL_QUOTE_FETCH_FAILED` on quote fetch failure + +This is why many sessions start in `LoadingQuote`/`QuoteReady` rather than plain `Idle`. + +## Practical reading model +When debugging what card should show, check in this order: +1. Top-level ramp state (`rampActor.getSnapshot().value`) +2. Whether Monerium child is in `Redirect` +3. Whether Avenia child exists and its context (`taxId`, `kybUrls`, `kybStep`) +4. Whether auth gating states (`CheckAuth`...`VerifyingOTP`) currently match + +The render priority order can override expectations from raw machine state (for example, a Monerium `Redirect` child card can appear before generic details/some other fallback views). From b076ed818ec4ed1d133a7b50196b7e83694b522f Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 16:23:18 +0000 Subject: [PATCH 04/12] Fix order of cards and back button behavior --- .../src/hooks/useStepBackNavigation.ts | 9 +- apps/frontend/src/machines/ramp.machine.ts | 246 +++++++++++++++--- apps/frontend/src/machines/types.ts | 1 + 3 files changed, 219 insertions(+), 37 deletions(-) diff --git a/apps/frontend/src/hooks/useStepBackNavigation.ts b/apps/frontend/src/hooks/useStepBackNavigation.ts index 3e336b46a..01be04cd8 100644 --- a/apps/frontend/src/hooks/useStepBackNavigation.ts +++ b/apps/frontend/src/hooks/useStepBackNavigation.ts @@ -15,6 +15,13 @@ export const useStepBackNavigation = () => { const searchParams = useSearch({ strict: false }); const isExternalProviderEntry = !!searchParams.externalSessionId; const hasQuoteIdInUrl = !!searchParams.quoteId; + const isAuthStep = + rampState === "CheckAuth" || + rampState === "EnterEmail" || + rampState === "CheckingEmail" || + rampState === "RequestingOTP" || + rampState === "EnterOTP" || + rampState === "VerifyingOTP"; // When user removes quoteId from URL while in QuoteReady state (and they entered via form), // send GO_BACK to return to Idle/Quote form @@ -50,7 +57,7 @@ export const useStepBackNavigation = () => { const isQuoteReady = rampState === "QuoteReady"; const isIdle = rampState === "Idle"; - if (isQuoteReady || isIdle) { + if (isQuoteReady || isIdle || isAuthStep) { navigate({ replace: true, search: {}, to: "." }); } diff --git a/apps/frontend/src/machines/ramp.machine.ts b/apps/frontend/src/machines/ramp.machine.ts index 68643f006..9b2db57be 100644 --- a/apps/frontend/src/machines/ramp.machine.ts +++ b/apps/frontend/src/machines/ramp.machine.ts @@ -60,6 +60,7 @@ const initialRampContext: RampContext = { isSep24Redo: false, partnerId: undefined, paymentData: undefined, + postAuthTarget: undefined, quote: undefined, quoteId: undefined, quoteLocked: undefined, @@ -337,6 +338,20 @@ export const rampMachine = setup({ states: { CheckAuth: { always: [ + { + actions: assign({ + postAuthTarget: undefined + }), + guard: ({ context }) => context.isAuthenticated && context.postAuthTarget === "RegisterRamp", + target: "RegisterRamp" + }, + { + actions: assign({ + postAuthTarget: undefined + }), + guard: ({ context }) => context.isAuthenticated && context.postAuthTarget === "QuoteReady", + target: "QuoteReady" + }, { guard: ({ context }) => context.isAuthenticated, target: "QuoteReady" @@ -344,7 +359,24 @@ export const rampMachine = setup({ { target: "EnterEmail" } - ] + ], + on: { + GO_BACK: [ + { + guard: ({ context }) => context.postAuthTarget === "RegisterRamp", + target: "KycComplete" + }, + { + actions: assign({ + enteredViaForm: undefined, + postAuthTarget: undefined, + quote: undefined, + quoteId: undefined + }), + target: "Idle" + } + ] + } }, CheckingEmail: { invoke: { @@ -359,6 +391,27 @@ export const rampMachine = setup({ target: "EnterEmail" }, src: "checkEmail" + }, + on: { + GO_BACK: [ + { + actions: assign({ + errorMessage: undefined + }), + guard: ({ context }) => context.postAuthTarget === "RegisterRamp", + target: "KycComplete" + }, + { + actions: assign({ + enteredViaForm: undefined, + errorMessage: undefined, + postAuthTarget: undefined, + quote: undefined, + quoteId: undefined + }), + target: "Idle" + } + ] } }, EnterEmail: { @@ -369,6 +422,25 @@ export const rampMachine = setup({ }), target: "CheckingEmail" }, + GO_BACK: [ + { + actions: assign({ + errorMessage: undefined + }), + guard: ({ context }) => context.postAuthTarget === "RegisterRamp", + target: "KycComplete" + }, + { + actions: assign({ + enteredViaForm: undefined, + errorMessage: undefined, + postAuthTarget: undefined, + quote: undefined, + quoteId: undefined + }), + target: "Idle" + } + ], SET_QUOTE: { actions: assign({ quoteId: ({ event }) => event.quoteId, @@ -401,6 +473,25 @@ export const rampMachine = setup({ }), target: "CheckingEmail" }, + GO_BACK: [ + { + actions: assign({ + errorMessage: undefined + }), + guard: ({ context }) => context.postAuthTarget === "RegisterRamp", + target: "KycComplete" + }, + { + actions: assign({ + enteredViaForm: undefined, + errorMessage: undefined, + postAuthTarget: undefined, + quote: undefined, + quoteId: undefined + }), + target: "Idle" + } + ], VERIFY_OTP: { actions: assign({ errorMessage: undefined }), target: "VerifyingOTP" @@ -431,12 +522,6 @@ export const rampMachine = setup({ } }, Idle: { - always: [ - { - guard: ({ context }) => !context.isAuthenticated, - target: "CheckAuth" - } - ], on: { INITIAL_QUOTE_FETCH_FAILED: { target: "InitialFetchFailed" @@ -470,9 +555,21 @@ export const rampMachine = setup({ GO_BACK: { target: "QuoteReady" }, - PROCEED_TO_REGISTRATION: { - target: "RegisterRamp" - }, + PROCEED_TO_REGISTRATION: [ + { + actions: assign({ + postAuthTarget: () => "RegisterRamp" + }), + guard: ({ context }) => !context.isAuthenticated, + target: "CheckAuth" + }, + { + actions: assign({ + postAuthTarget: undefined + }), + target: "RegisterRamp" + } + ], // This will trigger a quoteRefresher after some seconds REFRESH_FAILED: { actions: [{ type: "refreshQuoteActionWithDelay" }] @@ -513,13 +610,24 @@ export const rampMachine = setup({ input: ({ event, context }) => ({ quoteId: (event as Extract).quoteId || context.quoteId! }), - onDone: { - actions: assign({ - isQuoteExpired: ({ event }) => event.output.isExpired, - quote: ({ event }) => event.output.quote - }), - target: "QuoteReady" - }, + onDone: [ + { + actions: assign({ + isQuoteExpired: ({ event }) => event.output.isExpired, + postAuthTarget: () => "QuoteReady", + quote: ({ event }) => event.output.quote + }), + guard: ({ context }) => !context.isAuthenticated && context.enteredViaForm === true, + target: "CheckAuth" + }, + { + actions: assign({ + isQuoteExpired: ({ event }) => event.output.isExpired, + quote: ({ event }) => event.output.quote + }), + target: "QuoteReady" + } + ], onError: { actions: assign({ isQuoteExpired: true, @@ -640,6 +748,27 @@ export const rampMachine = setup({ target: "EnterEmail" }, src: "requestOTP" + }, + on: { + GO_BACK: [ + { + actions: assign({ + errorMessage: undefined + }), + guard: ({ context }) => context.postAuthTarget === "RegisterRamp", + target: "KycComplete" + }, + { + actions: assign({ + enteredViaForm: undefined, + errorMessage: undefined, + postAuthTarget: undefined, + quote: undefined, + quoteId: undefined + }), + target: "Idle" + } + ] } }, Resetting: { @@ -719,25 +848,49 @@ export const rampMachine = setup({ code: (event as any).code, email: context.userEmail! }), - onDone: { - actions: [ - assign({ - errorMessage: undefined, - isAuthenticated: true, - userId: ({ event }) => event.output.userId - }), - ({ event, context }) => { - // Store tokens in localStorage for session persistence - AuthService.storeTokens({ - accessToken: event.output.accessToken, - refreshToken: event.output.refreshToken, - userEmail: context.userEmail, - userId: event.output.userId - }); - } - ], - target: "QuoteReady" - }, + onDone: [ + { + actions: [ + assign({ + errorMessage: undefined, + isAuthenticated: true, + postAuthTarget: undefined, + userId: ({ event }) => event.output.userId + }), + ({ event, context }) => { + // Store tokens in localStorage for session persistence + AuthService.storeTokens({ + accessToken: event.output.accessToken, + refreshToken: event.output.refreshToken, + userEmail: context.userEmail, + userId: event.output.userId + }); + } + ], + guard: ({ context }) => context.postAuthTarget === "RegisterRamp", + target: "RegisterRamp" + }, + { + actions: [ + assign({ + errorMessage: undefined, + isAuthenticated: true, + postAuthTarget: undefined, + userId: ({ event }) => event.output.userId + }), + ({ event, context }) => { + // Store tokens in localStorage for session persistence + AuthService.storeTokens({ + accessToken: event.output.accessToken, + refreshToken: event.output.refreshToken, + userEmail: context.userEmail, + userId: event.output.userId + }); + } + ], + target: "QuoteReady" + } + ], onError: { actions: assign({ errorMessage: "Invalid OTP code. Please try again." @@ -745,6 +898,27 @@ export const rampMachine = setup({ target: "EnterOTP" }, src: "verifyOTP" + }, + on: { + GO_BACK: [ + { + actions: assign({ + errorMessage: undefined + }), + guard: ({ context }) => context.postAuthTarget === "RegisterRamp", + target: "KycComplete" + }, + { + actions: assign({ + enteredViaForm: undefined, + errorMessage: undefined, + postAuthTarget: undefined, + quote: undefined, + quoteId: undefined + }), + target: "Idle" + } + ] } } } diff --git a/apps/frontend/src/machines/types.ts b/apps/frontend/src/machines/types.ts index 1048c8f31..64a474082 100644 --- a/apps/frontend/src/machines/types.ts +++ b/apps/frontend/src/machines/types.ts @@ -41,6 +41,7 @@ export interface RampContext { userEmail?: string; userId?: string; isAuthenticated: boolean; + postAuthTarget?: "QuoteReady" | "RegisterRamp"; } export type RampMachineEvents = From cae469d75bb4c2832619dcd71042f24a87ef0511 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 16:29:52 +0000 Subject: [PATCH 05/12] Fix accidental 'go back' after Monerium redirect --- apps/frontend/src/hooks/useStepBackNavigation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/hooks/useStepBackNavigation.ts b/apps/frontend/src/hooks/useStepBackNavigation.ts index 01be04cd8..b224398d3 100644 --- a/apps/frontend/src/hooks/useStepBackNavigation.ts +++ b/apps/frontend/src/hooks/useStepBackNavigation.ts @@ -24,12 +24,12 @@ export const useStepBackNavigation = () => { rampState === "VerifyingOTP"; // When user removes quoteId from URL while in QuoteReady state (and they entered via form), - // send GO_BACK to return to Idle/Quote form + // send GO_BACK to return to Idle/Quote form. useEffect(() => { - if (!hasQuoteIdInUrl) { + if (!hasQuoteIdInUrl && rampState === "QuoteReady" && enteredViaForm) { rampActor.send({ type: "GO_BACK" }); } - }, [rampActor, hasQuoteIdInUrl]); + }, [rampActor, hasQuoteIdInUrl, rampState, enteredViaForm]); const shouldHide = rampState === "RampFollowUp" || From 675353b734c763abb5cfb6c53d159c00ff35bdf0 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 16:33:46 +0000 Subject: [PATCH 06/12] Hide HistoryMenuButton when not authenticated --- .../menus/HistoryMenu/HistoryMenuButton/index.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/frontend/src/components/menus/HistoryMenu/HistoryMenuButton/index.tsx b/apps/frontend/src/components/menus/HistoryMenu/HistoryMenuButton/index.tsx index 7550f31bb..6276f924b 100644 --- a/apps/frontend/src/components/menus/HistoryMenu/HistoryMenuButton/index.tsx +++ b/apps/frontend/src/components/menus/HistoryMenu/HistoryMenuButton/index.tsx @@ -1,8 +1,16 @@ import { ClockIcon } from "@heroicons/react/24/outline"; +import { useSelector } from "@xstate/react"; +import { useRampActor } from "../../../../contexts/rampState"; import { useRampHistoryStore } from "../../../../stores/rampHistoryStore"; export function HistoryMenuButton() { + const rampActor = useRampActor(); const { isActive, actions } = useRampHistoryStore(); + const isAuthenticated = useSelector(rampActor, state => state.context.isAuthenticated); + + if (!isAuthenticated) { + return null; + } return ( <> From 9bba451e8c6924040d4fd80808325e2b0df1b652 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 16:46:14 +0000 Subject: [PATCH 07/12] Add back button to auth otp step --- .../widget-steps/AuthOTPStep/index.tsx | 14 +++---- apps/frontend/src/machines/ramp.machine.ts | 38 ++++--------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx b/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx index b603ac541..4f65213f0 100644 --- a/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx +++ b/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx @@ -6,6 +6,7 @@ import { useRampActor } from "../../../contexts/rampState"; import { cn } from "../../../helpers/cn"; import { useQuote } from "../../../stores/quote/useQuoteStore"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "../../InputOTP"; +import { MenuButtons } from "../../MenuButtons"; import { QuoteSummary } from "../../QuoteSummary"; export interface AuthOTPStepProps { @@ -41,6 +42,10 @@ export function AuthOTPStep({ className }: AuthOTPStepProps) { return (
+
+ +
+

{t("components.authOTPStep.title")}

@@ -85,15 +90,6 @@ export function AuthOTPStep({ className }: AuthOTPStepProps) { {isVerifying && (

{t("components.authOTPStep.status.verifying")}

)} - -
diff --git a/apps/frontend/src/machines/ramp.machine.ts b/apps/frontend/src/machines/ramp.machine.ts index 9b2db57be..50528ece7 100644 --- a/apps/frontend/src/machines/ramp.machine.ts +++ b/apps/frontend/src/machines/ramp.machine.ts @@ -478,18 +478,7 @@ export const rampMachine = setup({ actions: assign({ errorMessage: undefined }), - guard: ({ context }) => context.postAuthTarget === "RegisterRamp", - target: "KycComplete" - }, - { - actions: assign({ - enteredViaForm: undefined, - errorMessage: undefined, - postAuthTarget: undefined, - quote: undefined, - quoteId: undefined - }), - target: "Idle" + target: "EnterEmail" } ], VERIFY_OTP: { @@ -900,25 +889,12 @@ export const rampMachine = setup({ src: "verifyOTP" }, on: { - GO_BACK: [ - { - actions: assign({ - errorMessage: undefined - }), - guard: ({ context }) => context.postAuthTarget === "RegisterRamp", - target: "KycComplete" - }, - { - actions: assign({ - enteredViaForm: undefined, - errorMessage: undefined, - postAuthTarget: undefined, - quote: undefined, - quoteId: undefined - }), - target: "Idle" - } - ] + GO_BACK: { + actions: assign({ + errorMessage: undefined + }), + target: "EnterEmail" + } } } } From cf15c74787aa9aa253c5c79782bd5b7459b9a9e7 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 16:52:13 +0000 Subject: [PATCH 08/12] Fix link to wrong T&C --- apps/frontend/src/components/menus/SettingsMenu/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/menus/SettingsMenu/index.tsx b/apps/frontend/src/components/menus/SettingsMenu/index.tsx index 510ef99bb..5d5824006 100644 --- a/apps/frontend/src/components/menus/SettingsMenu/index.tsx +++ b/apps/frontend/src/components/menus/SettingsMenu/index.tsx @@ -34,7 +34,7 @@ const MenuItem = ({ label, onClick, icon, disabled }: MenuItemProps) => { }; export const SettingsMenu = () => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const isOpen = useSettingsMenuState(); const { closeMenu } = useSettingsMenuActions(); const rampActor = useRampActor(); @@ -63,7 +63,7 @@ export const SettingsMenu = () => { }, { label: t("menus.settings.item.termsAndConditions"), - onClick: () => handleExternalLink("https://www.vortexfinance.co/terms-conditions") + onClick: () => handleExternalLink(`/${i18n.language}/terms-and-conditions`) }, { label: t("menus.settings.item.imprint"), From e295f938091f721742bfd55140c9f0d8afc3a7d1 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 16:59:35 +0000 Subject: [PATCH 09/12] Make auth otp input group more responsive --- .../src/components/widget-steps/AuthOTPStep/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx b/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx index 4f65213f0..281316e99 100644 --- a/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx +++ b/apps/frontend/src/components/widget-steps/AuthOTPStep/index.tsx @@ -61,6 +61,7 @@ export function AuthOTPStep({ className }: AuthOTPStepProps) {
- + - - - + - + From 46c3523787cfb27a059df1dc45ea1a4ab29d7a91 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 17:02:13 +0000 Subject: [PATCH 10/12] Update ramp-machine-widget-flow.md --- docs/architecture/ramp-machine-widget-flow.md | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/architecture/ramp-machine-widget-flow.md b/docs/architecture/ramp-machine-widget-flow.md index 51a2646ca..b5d0cb53c 100644 --- a/docs/architecture/ramp-machine-widget-flow.md +++ b/docs/architecture/ramp-machine-widget-flow.md @@ -13,7 +13,8 @@ This document maps how the top-level XState machine (`ramp.machine.ts`) drives w ```mermaid flowchart TD A[Idle] -->|SET_QUOTE| B[LoadingQuote] - B -->|quote loaded| C[QuoteReady] + B -->|quote loaded + unauth + enteredViaForm| AA[CheckAuth] + B -->|quote loaded otherwise| C[QuoteReady] C -->|CONFIRM| D[RampRequested] D -->|kycNeeded = true| E[KYC] @@ -23,7 +24,8 @@ flowchart TD E -->|child KYC error| X[KycFailure] X --> R[Resetting] - F -->|PROCEED_TO_REGISTRATION| G[RegisterRamp] + F -->|PROCEED_TO_REGISTRATION + authenticated| G[RegisterRamp] + F -->|PROCEED_TO_REGISTRATION + unauthenticated| AA[CheckAuth] F -->|GO_BACK| C G --> H[UpdateRamp] @@ -43,14 +45,16 @@ flowchart TD R -->|urlCleaner done| A - A --> AA[CheckAuth] + AA -->|authenticated + postAuthTarget=RegisterRamp| G + AA -->|authenticated + postAuthTarget=QuoteReady| C AA -->|authenticated| C AA -->|not authenticated| AB[EnterEmail] AB --> AC[CheckingEmail] AC --> AD[RequestingOTP] AD --> AE[EnterOTP] AE --> AF[VerifyingOTP] - AF -->|success| C + AF -->|success + postAuthTarget=RegisterRamp| G + AF -->|success otherwise| C AF -->|error| AE G -->|error| Z[Error] @@ -123,7 +127,7 @@ flowchart TD - in `UpdateRamp` on onramp -> `PAYMENT_CONFIRMED` - if quote expired -> `RESET_RAMP` - `AuthEmailStep` -> `ENTER_EMAIL` -- `AuthOTPStep` -> `VERIFY_OTP`, `CHANGE_EMAIL` +- `AuthOTPStep` -> `VERIFY_OTP` - Error/initial-failure/retry actions -> `RESET_RAMP` - Back button (`StepBackButton`) primarily sends `GO_BACK` (with Avenia-specific child events in document/liveness/KYB sub-steps) @@ -136,6 +140,15 @@ flowchart TD This is why many sessions start in `LoadingQuote`/`QuoteReady` rather than plain `Idle`. +## Auth gating change +- The initial `Idle -> CheckAuth` auto-transition was removed. +- For `/widget` entry coming from Quote form (`enteredViaForm`), auth can happen directly after `LoadingQuote` and before `QuoteReady`. +- Auth is also deferred to `KycComplete -> PROCEED_TO_REGISTRATION` when needed. +- `postAuthTarget` tracks whether post-auth continuation should be `QuoteReady` or `RegisterRamp`. +- `GO_BACK` behavior in auth states: + - `CheckAuth`, `EnterEmail`, `CheckingEmail`, `RequestingOTP`: back to `KycComplete` when `postAuthTarget=RegisterRamp`, otherwise reset to `Idle` (Quote form path). + - `EnterOTP`, `VerifyingOTP`: back to `EnterEmail`. + ## Practical reading model When debugging what card should show, check in this order: 1. Top-level ramp state (`rampActor.getSnapshot().value`) From 86eb264d68aeb2fbcc596cad5478b1e46ca2e658 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 17:15:03 +0000 Subject: [PATCH 11/12] Reset error message on GO_BACK action in ramp machine --- apps/frontend/src/machines/ramp.machine.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/frontend/src/machines/ramp.machine.ts b/apps/frontend/src/machines/ramp.machine.ts index 50528ece7..2c91b77be 100644 --- a/apps/frontend/src/machines/ramp.machine.ts +++ b/apps/frontend/src/machines/ramp.machine.ts @@ -363,12 +363,16 @@ export const rampMachine = setup({ on: { GO_BACK: [ { + actions: assign({ + errorMessage: undefined + }), guard: ({ context }) => context.postAuthTarget === "RegisterRamp", target: "KycComplete" }, { actions: assign({ enteredViaForm: undefined, + errorMessage: undefined, postAuthTarget: undefined, quote: undefined, quoteId: undefined From 78323f1f67be5c8b0bc038cf413f800f252ec0ed Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 11 Feb 2026 18:15:41 +0100 Subject: [PATCH 12/12] Update apps/frontend/src/components/menus/SettingsMenu/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/frontend/src/components/menus/SettingsMenu/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/menus/SettingsMenu/index.tsx b/apps/frontend/src/components/menus/SettingsMenu/index.tsx index 5d5824006..294f03719 100644 --- a/apps/frontend/src/components/menus/SettingsMenu/index.tsx +++ b/apps/frontend/src/components/menus/SettingsMenu/index.tsx @@ -63,7 +63,7 @@ export const SettingsMenu = () => { }, { label: t("menus.settings.item.termsAndConditions"), - onClick: () => handleExternalLink(`/${i18n.language}/terms-and-conditions`) + onClick: () => handleExternalLink(`https://www.vortexfinance.co/${i18n.language}/terms-and-conditions`) }, { label: t("menus.settings.item.imprint"),