From 427b3eec11de1eb5696e402708069a1e3d682ce8 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Jul 2026 11:02:59 +0200 Subject: [PATCH 1/3] fix(ramp): unblock SPEI ramp with separate flag, i18n migration, dead code cleanup (#312) - Split VITE_ENABLE_DEFI_TRADING into two flags: showDefi (DEX/Blend) and showSpeiRamp (SPEI KYC + on/offramp), independently enableable - Migrated KYCScreen hardcoded strings to i18n (en/es) via react-i18next - Removed dead no-op CapApp block from KYCScreen - Marked registerBankAccount as @deprecated (backend route removed June 30) - Updated Explore.tsx to show CETES card when either flag is true Closes #312. --- micopay/frontend/src/App.tsx | 3 ++ micopay/frontend/src/i18n/en.json | 23 +++++++++ micopay/frontend/src/i18n/es.json | 23 +++++++++ micopay/frontend/src/pages/Explore.tsx | 8 ++-- micopay/frontend/src/pages/KYCScreen.tsx | 61 ++++++++++-------------- micopay/frontend/src/services/api.ts | 5 ++ 6 files changed, 83 insertions(+), 40 deletions(-) diff --git a/micopay/frontend/src/App.tsx b/micopay/frontend/src/App.tsx index ff64527..a715128 100644 --- a/micopay/frontend/src/App.tsx +++ b/micopay/frontend/src/App.tsx @@ -530,6 +530,9 @@ function ExploreRoute() { // (platform-key-only simulation on mainnet, audit finding B2) — hidden // until a real user-signed implementation lands. Opt in with // VITE_ENABLE_DEFI_TRADING=true for internal/demo builds. + // SPEI ramp (KYC + onramp + offramp) moves real funds (device keypair) — + // enabled independently via VITE_ENABLE_SPEI_RAMP=true. + showSpeiRamp={import.meta.env.VITE_ENABLE_SPEI_RAMP === 'true'} showDefi={import.meta.env.VITE_ENABLE_DEFI_TRADING === 'true'} /> ); diff --git a/micopay/frontend/src/i18n/en.json b/micopay/frontend/src/i18n/en.json index c88ae4b..ca94fde 100644 --- a/micopay/frontend/src/i18n/en.json +++ b/micopay/frontend/src/i18n/en.json @@ -374,6 +374,29 @@ "receiptLock": "Lock", "receiptRelease": "Release" }, + "kyc": { + "verifyingIdentity": "Verifying identity…", + "verifyingDesc": "This may take a few seconds.", + "identityVerified": "Identity verified", + "identityVerifiedDesc": "Ready! We'll continue with the investment flow.", + "couldNotVerify": "Could not verify", + "couldNotVerifyDesc": "Check the reason and try again.", + "sessionNotAvailable": "Session not available", + "sessionNotAvailableDesc": "We couldn't verify your session. Please log in again and try again.", + "verifyIdentity": "Verify my identity", + "retryVerification": "Retry verification", + "sessionExpires": "Your verification session expires in ~15 minutes.", + "continue": "Continue", + "identityVerification": "Identity verification", + "oneStepWithEtherfuse": "One-step with Etherfuse", + "identityVerifiedWith": "Your identity will be verified with Etherfuse", + "oneTimeProcess": "This is a one-time process done on Etherfuse's hosted page. We do not collect or store sensitive data in this app.", + "reason": "Reason", + "couldNotQuery": "Could not query", + "sessionError": "Session not available. Log in again and try again.", + "openingEtherfuse": "Opening Etherfuse…", + "pollError": "Error checking verification status." + }, "errors": { "network": { "offline": { "title": "No connection", "message": "We couldn't connect. Check your signal and try again.", "action": "Try again once you have signal." }, diff --git a/micopay/frontend/src/i18n/es.json b/micopay/frontend/src/i18n/es.json index 74cdf20..7649343 100644 --- a/micopay/frontend/src/i18n/es.json +++ b/micopay/frontend/src/i18n/es.json @@ -374,6 +374,29 @@ "receiptLock": "Bloqueo", "receiptRelease": "Liberación" }, + "kyc": { + "verifyingIdentity": "Verificando identidad…", + "verifyingDesc": "Esto puede tardar unos segundos.", + "identityVerified": "Identidad verificada", + "identityVerifiedDesc": "¡Listo! Continuaremos con el flujo de inversión.", + "couldNotVerify": "No se pudo verificar", + "couldNotVerifyDesc": "Revisa el motivo y vuelve a intentar.", + "sessionNotAvailable": "Sesión no disponible", + "sessionNotAvailableDesc": "No pudimos verificar tu sesión. Vuelve a iniciar sesión e intenta de nuevo.", + "verifyIdentity": "Verify my identity", + "retryVerification": "Retry verification", + "sessionExpires": "Tu sesión de verificación expira en ~15 minutos.", + "continue": "Continuar", + "identityVerification": "Verificación de identidad", + "oneStepWithEtherfuse": "Paso único con Etherfuse", + "identityVerifiedWith": "Tu identidad será verificada con Etherfuse", + "oneTimeProcess": "Este proceso es una sola vez y se hace en la página hospedada de Etherfuse. No recopilamos ni almacenamos datos sensibles en esta app.", + "reason": "Motivo", + "couldNotQuery": "No se pudo consultar", + "sessionError": "Sesión no disponible. Vuelve a iniciar sesión e intenta de nuevo.", + "openingEtherfuse": "Abriendo Etherfuse…", + "pollError": "Error al consultar el estado de verificación." + }, "errors": { "network": { "offline": { "title": "Sin conexión", "message": "No pudimos conectarnos. Revisa tu señal e intenta otra vez.", "action": "Vuelve a intentar cuando tengas señal." }, diff --git a/micopay/frontend/src/pages/Explore.tsx b/micopay/frontend/src/pages/Explore.tsx index 2040e73..229a396 100644 --- a/micopay/frontend/src/pages/Explore.tsx +++ b/micopay/frontend/src/pages/Explore.tsx @@ -6,9 +6,11 @@ interface ExploreProps { onNavigate?: (page: string) => void; /** When true, DeFi features are hidden (backend reported mock/demo mode). */ showDefi?: boolean; + /** When true, SPEI ramp (KYC + onramp + offramp) is reachable independently. */ + showSpeiRamp?: boolean; } -const Explore = ({ onBack, onNavigate, showDefi = true }: ExploreProps) => { +const Explore = ({ onBack, onNavigate, showDefi = true, showSpeiRamp = false }: ExploreProps) => { const { t } = useTranslation(); return (
@@ -35,8 +37,8 @@ const Explore = ({ onBack, onNavigate, showDefi = true }: ExploreProps) => {
- {/* CETES tokenizados — feature-gated */} - {showDefi && ( + {/* CETES tokenizados — shown if either DeFi or SPEI ramp is enabled */} + {(showDefi || showSpeiRamp) && (
diff --git a/micopay/frontend/src/pages/KYCScreen.tsx b/micopay/frontend/src/pages/KYCScreen.tsx index 419d7af..1651557 100644 --- a/micopay/frontend/src/pages/KYCScreen.tsx +++ b/micopay/frontend/src/pages/KYCScreen.tsx @@ -1,20 +1,21 @@ import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { App as CapApp } from '@capacitor/app'; - import { startKYC, getKYCStatus, type KYCStatus, type KYCStatusResponse } from '../services/api'; import { readJSON, writeJSON } from '../services/secureStorage'; const SECURE_STORAGE_KEY = 'kyc_status'; function StatusLine({ status }: { status: KYCStatus }) { + const { t } = useTranslation(); if (status === 'pending') { return (
hourglass_top
-

Verificando identidad…

-

Esto puede tardar unos segundos.

+

{t('kyc.verifyingIdentity')}

+

{t('kyc.verifyingDesc')}

); @@ -24,8 +25,8 @@ function StatusLine({ status }: { status: KYCStatus }) {
check_circle
-

Identidad verificada

-

¡Listo! Continuaremos con el flujo de inversión.

+

{t('kyc.identityVerified')}

+

{t('kyc.identityVerifiedDesc')}

); @@ -34,8 +35,8 @@ function StatusLine({ status }: { status: KYCStatus }) {
error
-

No se pudo verificar

-

Revisa el motivo y vuelve a intentar.

+

{t('kyc.couldNotVerify')}

+

{t('kyc.couldNotVerifyDesc')}

); @@ -47,6 +48,7 @@ type KYCScreenProps = { }; export default function KYCScreen({ onApproved, token }: KYCScreenProps) { + const { t } = useTranslation(); const [status, setStatus] = useState('pending'); const [reason, setReason] = useState(null); const [loading, setLoading] = useState(false); @@ -72,7 +74,7 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { const handleOpenHostedFlow = async () => { if (!token) { - setStatusPollingError('Sesión no disponible. Vuelve a iniciar sesión e intenta de nuevo.'); + setStatusPollingError(t('kyc.sessionError')); return; } setStatusPollingError(null); @@ -80,18 +82,12 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { setLoading(true); try { - // IMPORTANT: URL expires in ~15 minutes => generate on touch. const { onboardingUrl } = await startKYC(token); startedAtRef.current = Date.now(); setStartingToken(onboardingUrl); setStatus('pending'); - // Open in system browser (not an in-app webview). - if (CapApp && (CapApp as any)) { - // no-op; keeps capacitor import used. - } - // Open in system browser (preferred: Capacitor Browser plugin). // We lazy-load to avoid hard dependency on TS typings. try { @@ -106,10 +102,6 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { // Fallback for web builds / when plugin is not present. window.open(onboardingUrl, '_blank', 'noopener,noreferrer'); } - - - - } finally { setLoading(false); } @@ -134,7 +126,7 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { await applyStatus(res); return res; } catch (e: any) { - const message = e?.message ?? 'Error al consultar el estado de verificación.'; + const message = e?.message ?? t('kyc.pollError'); setStatusPollingError(message); return null; } @@ -153,9 +145,7 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { }, 5000); }; - // Always attempt polling when mounted; will return pending/approved from stub. if (status === 'pending') { - // Kick off immediately void pollOnce().then(() => { if (cancelled) return; startPolling(); @@ -173,7 +163,6 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { cancelled = true; if (intervalId) window.clearInterval(intervalId); (sub as any)?.remove?.(); - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [status, token]); @@ -182,9 +171,9 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { return (
lock -

Sesión no disponible

+

{t('kyc.sessionNotAvailable')}

- No pudimos verificar tu sesión. Vuelve a iniciar sesión e intenta de nuevo. + {t('kyc.sessionNotAvailableDesc')}

); @@ -197,13 +186,13 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) {
-

Verificación de identidad

-

Paso único con Etherfuse

+

{t('kyc.identityVerification')}

+

{t('kyc.oneStepWithEtherfuse')}

@@ -211,10 +200,9 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) {
-

Tu identidad será verificada con Etherfuse

+

{t('kyc.identityVerifiedWith')}

- Este proceso es una sola vez y se hace en la página - hospedada de Etherfuse. No recopilamos ni almacenamos datos sensibles en esta app. + {t('kyc.oneTimeProcess')}

@@ -222,14 +210,14 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { {status === 'rejected' && reason && (
-

Motivo

+

{t('kyc.reason')}

{reason}

)} {statusPollingError && (
-

No se pudo consultar

+

{t('kyc.couldNotQuery')}

{statusPollingError}

)} @@ -244,12 +232,12 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { {loading ? ( <> progress_activity - Abriendo Etherfuse… + {t('kyc.openingEtherfuse')} ) : ( <> verified_user - Verify my identity + {t('kyc.verifyIdentity')} )} @@ -261,16 +249,15 @@ export default function KYCScreen({ onApproved, token }: KYCScreenProps) { className="w-full bg-white border border-error/30 text-error font-bold py-3.5 rounded-2xl flex items-center justify-center gap-2 shadow-sm disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-[0.98]" > refresh - Retry verification + {t('kyc.retryVerification')} )}

- Tu sesión de verificación expira en ~15 minutos. + {t('kyc.sessionExpires')}

); } - diff --git a/micopay/frontend/src/services/api.ts b/micopay/frontend/src/services/api.ts index 0351ed6..e4b462a 100644 --- a/micopay/frontend/src/services/api.ts +++ b/micopay/frontend/src/services/api.ts @@ -717,6 +717,11 @@ export async function regenerateRampOrderTx(orderId: string, token: string): Pro return res.data as RampOrder; } +/** + * @deprecated The backend POST /defi/bank-account route was removed on June 30. + * CLABE is now captured inside the hosted Etherfuse KYC flow. This function + * always returns a 404. Kept only to avoid breaking any remaining references. + */ export async function registerBankAccount( clabe: string, token: string, From 8d347689789853a31bdbf11834e0fa572b089bb6 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 20 Jul 2026 02:44:12 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(ramp):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20isolate=20SPEI=20from=20simulated=20DeFi,=20i18n,=20dead=20c?= =?UTF-8?q?ode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review 4731742623 (ericmt-98, CHANGES_REQUESTED): 1. Thread showDefi/showSpeiRamp into CETESScreen via CetesRoute. In SPEI-only mode (showSpeiRamp && !showDefi) the buy/sell simulated trading tab switcher is hidden and payMethod/receiveMethod default to 'spei', so VITE_ENABLE_SPEI_RAMP=true no longer exposes the platform-key-only DeFi simulation UI. The flag split now actually isolates SPEI flows. 2. Translate the two missed es.json strings: verifyIdentity / retryVerification -> Spanish. 3. Delete dead registerBankAccount() from api.ts (zero callers; backend route removed June 30). Note: backend buyCETES/sellCETES flag-gating remains a follow-up as the reviewer noted (out of scope for this PR). Closes #312 --- micopay/frontend/src/App.tsx | 2 ++ micopay/frontend/src/i18n/es.json | 4 ++-- micopay/frontend/src/pages/CETESScreen.tsx | 16 ++++++++++++---- micopay/frontend/src/services/api.ts | 17 ----------------- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/micopay/frontend/src/App.tsx b/micopay/frontend/src/App.tsx index a715128..165635d 100644 --- a/micopay/frontend/src/App.tsx +++ b/micopay/frontend/src/App.tsx @@ -546,6 +546,8 @@ function CetesRoute() { onBack={() => navigate('/explore')} onBanco={() => navigate('/deposit')} userToken={buyerUser?.token} + showDefi={import.meta.env.VITE_ENABLE_DEFI_TRADING === 'true'} + showSpeiRamp={import.meta.env.VITE_ENABLE_SPEI_RAMP === 'true'} /> ); } diff --git a/micopay/frontend/src/i18n/es.json b/micopay/frontend/src/i18n/es.json index 7649343..6ee62a5 100644 --- a/micopay/frontend/src/i18n/es.json +++ b/micopay/frontend/src/i18n/es.json @@ -383,8 +383,8 @@ "couldNotVerifyDesc": "Revisa el motivo y vuelve a intentar.", "sessionNotAvailable": "Sesión no disponible", "sessionNotAvailableDesc": "No pudimos verificar tu sesión. Vuelve a iniciar sesión e intenta de nuevo.", - "verifyIdentity": "Verify my identity", - "retryVerification": "Retry verification", + "verifyIdentity": "Verificar mi identidad", + "retryVerification": "Reintentar verificación", "sessionExpires": "Tu sesión de verificación expira en ~15 minutos.", "continue": "Continuar", "identityVerification": "Verificación de identidad", diff --git a/micopay/frontend/src/pages/CETESScreen.tsx b/micopay/frontend/src/pages/CETESScreen.tsx index c677a9f..458d08f 100644 --- a/micopay/frontend/src/pages/CETESScreen.tsx +++ b/micopay/frontend/src/pages/CETESScreen.tsx @@ -24,6 +24,8 @@ interface CETESScreenProps { onBack: () => void; onBanco?: () => void; userToken?: string; + showDefi?: boolean; + showSpeiRamp?: boolean; } type Tab = 'buy' | 'sell'; @@ -32,11 +34,15 @@ type ReceiveMethod = 'wallet' | 'spei'; type PayMethod = 'wallet' | 'spei'; type DepositStep = 'quote' | 'instructions' | 'polling'; -const CETESScreen = ({ onBack, onBanco, userToken }: CETESScreenProps) => { +const CETESScreen = ({ onBack, onBanco, userToken, showDefi = true, showSpeiRamp = false }: CETESScreenProps) => { const { t } = useTranslation(); - const [tab, setTab] = useState('buy'); - const [receiveMethod, setReceiveMethod] = useState('wallet'); - const [payMethod, setPayMethod] = useState('wallet'); + // When SPEI ramp is enabled but DeFi trading is NOT, hide the simulated + // buy/sell trading UI and force the SPEI onramp/offramp path. This isolates + // real-funds SPEI flows from the platform-key-only DeFi simulation. + const speiOnlyMode = showSpeiRamp && !showDefi; + const [tab, setTab] = useState(speiOnlyMode ? 'buy' : 'buy'); + const [receiveMethod, setReceiveMethod] = useState(speiOnlyMode ? 'spei' : 'wallet'); + const [payMethod, setPayMethod] = useState(speiOnlyMode ? 'spei' : 'wallet'); const [amount, setAmount] = useState(''); const [sourceAsset, setSourceAsset] = useState('XLM'); @@ -320,6 +326,7 @@ const CETESScreen = ({ onBack, onBanco, userToken }: CETESScreenProps) => {
+ {showDefi && (
{(['buy', 'sell'] as Tab[]).map((tabOption) => ( ))}
+ )} {tab === 'buy' && canDepositSpei && (
diff --git a/micopay/frontend/src/services/api.ts b/micopay/frontend/src/services/api.ts index e4b462a..4c0ff49 100644 --- a/micopay/frontend/src/services/api.ts +++ b/micopay/frontend/src/services/api.ts @@ -717,23 +717,6 @@ export async function regenerateRampOrderTx(orderId: string, token: string): Pro return res.data as RampOrder; } -/** - * @deprecated The backend POST /defi/bank-account route was removed on June 30. - * CLABE is now captured inside the hosted Etherfuse KYC flow. This function - * always returns a 404. Kept only to avoid breaking any remaining references. - */ -export async function registerBankAccount( - clabe: string, - token: string, -): Promise { - try { - const res = await http.post('/defi/bank-account', { clabe }, authHeaders(token)); - return res.data as BankAccountResult; - } catch (e: unknown) { - throw toApiError(extractApiErrorPayload(e)); - } -} - // Global 401 handler: clear the persisted session and bounce to login. http.interceptors.response.use( (response) => response, From 256ea2fb2bc16a754bcea067b182547be7a9bd28 Mon Sep 17 00:00:00 2001 From: GBOYEE Date: Mon, 20 Jul 2026 22:43:43 +0200 Subject: [PATCH 3/3] fix: hide payMethod wallet switcher in SPEI-only mode (#312) In SPEI-only mode (`showSpeiRamp \&\& !showDefi`) the payMethod toggle still rendered the 'Pagar con wallet' option, letting a user reach the simulated DeFi trading UI. Gate the switcher on `showDefi` so the wallet path is unavailable when DeFi is disabled; payMethod stays forced to 'spei'. es.json verifyIdentity/retryVerification already translated; no dead registerBankAccount fn in frontend src (only docs reference it), so no further change needed. --- micopay/frontend/src/pages/CETESScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/micopay/frontend/src/pages/CETESScreen.tsx b/micopay/frontend/src/pages/CETESScreen.tsx index 458d08f..409ee2c 100644 --- a/micopay/frontend/src/pages/CETESScreen.tsx +++ b/micopay/frontend/src/pages/CETESScreen.tsx @@ -342,7 +342,7 @@ const CETESScreen = ({ onBack, onBanco, userToken, showDefi = true, showSpeiRamp
)} - {tab === 'buy' && canDepositSpei && ( + {tab === 'buy' && canDepositSpei && showDefi && (