From 96efe00460d0ce98893d22cc01f5950fcfc538db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Wed, 8 Oct 2025 21:10:18 -0300 Subject: [PATCH 1/5] feat: recognize simplefi qrs --- .../DirectSendQR/__tests__/recognizeQr.test.ts | 3 +++ src/components/Global/DirectSendQR/index.tsx | 3 +++ src/components/Global/DirectSendQR/utils.ts | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts b/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts index fcc10968c..8560ef251 100644 --- a/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts +++ b/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts @@ -37,6 +37,9 @@ describe('recognizeQr', () => { ['rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', EQrType.XRP_ADDRESS], ['https://example.com', EQrType.URL], ['http://domain.co.uk/path', EQrType.URL], + ['https://pagar.simplefi.tech/peanut-test/static', EQrType.SIMPLEFI_STATIC], + ['https://pagar.simplefi.tech/peanut-test', EQrType.SIMPLEFI_DYNAMIC], + ['https://pagar.simplefi.tech/1234/payment/5678', EQrType.SIMPLEFI_USER_SPECIFIED], ['random text without any pattern', null], ['123456', null], ['', null], diff --git a/src/components/Global/DirectSendQR/index.tsx b/src/components/Global/DirectSendQR/index.tsx index f0295b164..b1014d2ca 100644 --- a/src/components/Global/DirectSendQR/index.tsx +++ b/src/components/Global/DirectSendQR/index.tsx @@ -293,6 +293,9 @@ export default function DirectSendQr({ case EQrType.MERCADO_PAGO: case EQrType.ARGENTINA_QR3: case EQrType.PIX: + case EQrType.SIMPLEFI_STATIC: + case EQrType.SIMPLEFI_DYNAMIC: + case EQrType.SIMPLEFI_USER_SPECIFIED: { const timestamp = Date.now() // Casing matters, so send original instead of normalized diff --git a/src/components/Global/DirectSendQR/utils.ts b/src/components/Global/DirectSendQR/utils.ts index a45a8e8a1..b25bee845 100644 --- a/src/components/Global/DirectSendQR/utils.ts +++ b/src/components/Global/DirectSendQR/utils.ts @@ -15,6 +15,9 @@ export enum EQrType { TRON_ADDRESS = 'TRON_ADDRESS', SOLANA_ADDRESS = 'SOLANA_ADDRESS', XRP_ADDRESS = 'XRP_ADDRESS', + SIMPLEFI_STATIC = 'SIMPLEFI_STATIC', + SIMPLEFI_DYNAMIC = 'SIMPLEFI_DYNAMIC', + SIMPLEFI_USER_SPECIFIED = 'SIMPLEFI_USER_SPECIFIED', } export const NAME_BY_QR_TYPE: { [key in QrType]?: string } = { @@ -26,6 +29,9 @@ export const NAME_BY_QR_TYPE: { [key in QrType]?: string } = { [EQrType.TRON_ADDRESS]: 'Tron', [EQrType.SOLANA_ADDRESS]: 'Solana', [EQrType.XRP_ADDRESS]: 'Ripple', + [EQrType.SIMPLEFI_STATIC]: 'SimpleFi', + [EQrType.SIMPLEFI_DYNAMIC]: 'SimpleFi', + [EQrType.SIMPLEFI_USER_SPECIFIED]: 'SimpleFi', } export type QrType = `${EQrType}` @@ -57,10 +63,17 @@ const ARGENTINA_QR3_REGEX = /^(?=.*00020101021[12])(?=.*5303032)(?=.*5802AR)/i /* PIX is also a emvco qr code */ const PIX_REGEX = /^.*000201.*0014br\.gov\.bcb\.pix.*5303986.*5802BR.*$/i +export const SIMPLEFI_STATIC_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)\/static$/ +export const SIMPLEFI_DYNAMIC_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)$/ +export const SIMPLEFI_USER_SPECIFIED_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)\/payment\/(?[^\/]*)$/ + export const PAYMENT_PROCESSOR_REGEXES: { [key in QrType]?: RegExp } = { [EQrType.MERCADO_PAGO]: MP_AR_REGEX, [EQrType.PIX]: PIX_REGEX, [EQrType.ARGENTINA_QR3]: ARGENTINA_QR3_REGEX, + [EQrType.SIMPLEFI_STATIC]: SIMPLEFI_STATIC_REGEX, + [EQrType.SIMPLEFI_DYNAMIC]: SIMPLEFI_DYNAMIC_REGEX, + [EQrType.SIMPLEFI_USER_SPECIFIED]: SIMPLEFI_USER_SPECIFIED_REGEX, } const EIP_681_REGEX = /^ethereum:(?:pay-)?([^@/?]+)(?:@([^/?]+))?(?:\/([^?]+))?(?:\?(.*))?$/i @@ -70,6 +83,9 @@ const REGEXES_BY_TYPE: { [key in QrType]?: RegExp } = { //this order is important, first mercadipago, then argentina qr3 [EQrType.MERCADO_PAGO]: MP_AR_REGEX, [EQrType.ARGENTINA_QR3]: ARGENTINA_QR3_REGEX, + [EQrType.SIMPLEFI_STATIC]: SIMPLEFI_STATIC_REGEX, + [EQrType.SIMPLEFI_DYNAMIC]: SIMPLEFI_DYNAMIC_REGEX, + [EQrType.SIMPLEFI_USER_SPECIFIED]: SIMPLEFI_USER_SPECIFIED_REGEX, [EQrType.BITCOIN_ONCHAIN]: /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/, [EQrType.BITCOIN_INVOICE]: /^ln(bc|tb|bcrt)([0-9]{1,}[a-z0-9]+){1}$/, [EQrType.PIX]: PIX_REGEX, From 07b490c5e7901c154f5009a2e58debd40dca49b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Thu, 9 Oct 2025 18:23:35 -0300 Subject: [PATCH 2/5] feat: support simplefi QRs --- src/app/(mobile-ui)/qr-pay/page.tsx | 354 ++++++++++++++++-- src/assets/payment-apps/index.ts | 1 + src/assets/payment-apps/simplefi-logo.svg | 1 + .../__tests__/recognizeQr.test.ts | 5 +- src/components/Global/DirectSendQR/utils.ts | 55 ++- src/services/simplefi.ts | 73 ++++ 6 files changed, 460 insertions(+), 29 deletions(-) create mode 100644 src/assets/payment-apps/simplefi-logo.svg create mode 100644 src/services/simplefi.ts diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index da3c5f142..f43835aab 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -8,8 +8,10 @@ import { Button } from '@/components/0_Bruddle/Button' import { Icon } from '@/components/Global/Icons/Icon' import { mantecaApi } from '@/services/manteca' import type { QrPayment, QrPaymentLock } from '@/services/manteca' +import { simplefiApi } from '@/services/simplefi' +import type { SimpleFiQrPaymentResponse } from '@/services/simplefi' import NavHeader from '@/components/Global/NavHeader' -import { MERCADO_PAGO, PIX } from '@/assets/payment-apps' +import { MERCADO_PAGO, PIX, SIMPLEFI } from '@/assets/payment-apps' import Image from 'next/image' import PeanutLoading from '@/components/Global/PeanutLoading' import TokenAmountInput from '@/components/Global/TokenAmountInput' @@ -27,16 +29,18 @@ import { loadingStateContext } from '@/context' import { getCurrencyPrice } from '@/app/actions/currency' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { captureException } from '@sentry/nextjs' -import { isPaymentProcessorQR } from '@/components/Global/DirectSendQR/utils' +import { isPaymentProcessorQR, parseSimpleFiQr, EQrType } from '@/components/Global/DirectSendQR/utils' +import type { SimpleFiQrData } from '@/components/Global/DirectSendQR/utils' import { QrKycState, useQrKycGate } from '@/hooks/useQrKycGate' import ActionModal from '@/components/Global/ActionModal' import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal' -import { EQrType } from '@/components/Global/DirectSendQR/utils' import { SoundPlayer } from '@/components/Global/SoundPlayer' import { useQueryClient } from '@tanstack/react-query' const MAX_QR_PAYMENT_AMOUNT = '200' +type PaymentProcessor = 'MANTECA' | 'SIMPLEFI' + export default function QRPayPage() { const searchParams = useSearchParams() const router = useRouter() @@ -49,6 +53,9 @@ export default function QRPayPage() { const [balanceErrorMessage, setBalanceErrorMessage] = useState(null) const [errorInitiatingPayment, setErrorInitiatingPayment] = useState(null) const [paymentLock, setPaymentLock] = useState(null) + const [simpleFiPayment, setSimpleFiPayment] = useState(null) + const [simpleFiQrData, setSimpleFiQrData] = useState(null) + const [showOrderNotReadyModal, setShowOrderNotReadyModal] = useState(false) const [isFirstLoad, setIsFirstLoad] = useState(true) const [amount, setAmount] = useState(undefined) const [currencyAmount, setCurrencyAmount] = useState(undefined) @@ -60,12 +67,28 @@ export default function QRPayPage() { const { shouldBlockPay, kycGateState } = useQrKycGate() const queryClient = useQueryClient() + const paymentProcessor: PaymentProcessor | null = useMemo(() => { + if ( + qrType === EQrType.SIMPLEFI_STATIC || + qrType === EQrType.SIMPLEFI_DYNAMIC || + qrType === EQrType.SIMPLEFI_USER_SPECIFIED + ) { + return 'SIMPLEFI' + } else if (qrType === EQrType.MERCADO_PAGO || qrType === EQrType.ARGENTINA_QR3 || qrType === EQrType.PIX) { + return 'MANTECA' + } + return null + }, [qrType]) + const resetState = () => { setIsSuccess(false) setErrorMessage(null) setBalanceErrorMessage(null) setErrorInitiatingPayment(null) setPaymentLock(null) + setSimpleFiPayment(null) + setSimpleFiQrData(null) + setShowOrderNotReadyModal(false) setIsFirstLoad(true) setAmount(undefined) setCurrencyAmount(undefined) @@ -83,21 +106,26 @@ export default function QRPayPage() { return } - // defer until gating computed later in component + if (paymentProcessor === 'SIMPLEFI') { + const parsed = parseSimpleFiQr(qrCode) + setSimpleFiQrData(parsed) + } + setIsFirstLoad(false) - // Trigger on rescan - }, [timestamp]) + }, [timestamp, paymentProcessor, qrCode]) - // Get amount from payment lock + // Get amount from payment lock (Manteca) useEffect(() => { + if (paymentProcessor !== 'MANTECA') return if (!paymentLock) return if (paymentLock.code !== '') { setAmount(paymentLock.paymentAssetAmount) } - }, [paymentLock?.code]) + }, [paymentLock?.code, paymentProcessor]) - // Get currency object from payment lock + // Get currency object from payment lock (Manteca) useEffect(() => { + if (paymentProcessor !== 'MANTECA') return if (!paymentLock) return const getCurrencyObject = async () => { let currencyCode: string @@ -115,20 +143,23 @@ export default function QRPayPage() { } } getCurrencyObject().then(setCurrency) - }, [paymentLock?.code]) + }, [paymentLock?.code, paymentProcessor]) const isBlockingError = useMemo(() => { return !!errorMessage && errorMessage !== 'Please confirm the transaction.' }, [errorMessage]) const usdAmount = useMemo(() => { + if (paymentProcessor === 'SIMPLEFI') { + return simpleFiPayment?.usdAmount || amount + } if (!paymentLock) return null if (paymentLock.code === '') { return amount } else { return paymentLock.paymentAgainstAmount } - }, [paymentLock?.code, paymentLock?.paymentAgainstAmount, amount]) + }, [paymentProcessor, simpleFiPayment, paymentLock?.code, paymentLock?.paymentAgainstAmount, amount]) const methodIcon = useMemo(() => { switch (qrType) { @@ -138,13 +169,67 @@ export default function QRPayPage() { return 'https://flagcdn.com/w160/ar.png' case EQrType.PIX: return PIX + case EQrType.SIMPLEFI_STATIC: + case EQrType.SIMPLEFI_DYNAMIC: + case EQrType.SIMPLEFI_USER_SPECIFIED: + return SIMPLEFI default: return null } }, [qrType]) - // fetch payment lock only when gating allows proceeding and we don't yet have a lock + // Fetch SimpleFi payment details + useEffect(() => { + if (paymentProcessor !== 'SIMPLEFI' || !simpleFiQrData) return + if (!!simpleFiPayment) return + if (kycGateState !== QrKycState.PROCEED_TO_PAY) return + + const fetchSimpleFiPayment = async () => { + setLoadingState('Fetching details') + try { + let response: SimpleFiQrPaymentResponse + + if (simpleFiQrData.type === 'SIMPLEFI_STATIC') { + response = await simplefiApi.initiateQrPayment({ + type: 'STATIC', + merchantSlug: simpleFiQrData.merchantSlug, + }) + } else if (simpleFiQrData.type === 'SIMPLEFI_DYNAMIC') { + response = await simplefiApi.initiateQrPayment({ + type: 'DYNAMIC', + simplefiRequestId: simpleFiQrData.paymentId, + }) + } else { + setLoadingState('Idle') + return + } + + setSimpleFiPayment(response) + setAmount(response.currencyAmount) + setCurrencyAmount(response.currencyAmount) + setCurrency({ + code: 'ARS', + symbol: 'ARS', + price: Number(response.price), + }) + } catch (error) { + const errorMsg = (error as Error).message + if (errorMsg.includes('ready to pay')) { + setShowOrderNotReadyModal(true) + } else { + setErrorInitiatingPayment(errorMsg) + } + } finally { + setLoadingState('Idle') + } + } + + fetchSimpleFiPayment() + }, [kycGateState, simpleFiPayment, simpleFiQrData, paymentProcessor, setLoadingState]) + + // fetch payment lock only when gating allows proceeding and we don't yet have a lock (Manteca) useEffect(() => { + if (paymentProcessor !== 'MANTECA') return if (!qrCode || !isPaymentProcessorQR(qrCode)) return if (!!paymentLock) return if (kycGateState !== QrKycState.PROCEED_TO_PAY) return @@ -155,14 +240,81 @@ export default function QRPayPage() { .then((pl) => setPaymentLock(pl)) .catch((error) => setErrorInitiatingPayment(error.message)) .finally(() => setLoadingState('Idle')) - }, [kycGateState, paymentLock, qrCode, setLoadingState]) + }, [kycGateState, paymentLock, qrCode, setLoadingState, paymentProcessor]) const merchantName = useMemo(() => { + if (paymentProcessor === 'SIMPLEFI') { + if (simpleFiQrData?.type === 'SIMPLEFI_STATIC' || simpleFiQrData?.type === 'SIMPLEFI_USER_SPECIFIED') { + return simpleFiQrData.merchantSlug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + } + return 'SimpleFi Merchant' + } if (!paymentLock) return null return paymentLock.paymentRecipientName - }, [paymentLock]) + }, [paymentProcessor, simpleFiQrData, paymentLock]) - const payQR = useCallback(async () => { + const handleSimpleFiPayment = useCallback(async () => { + if (!simpleFiPayment && !simpleFiQrData) return + + let finalPayment = simpleFiPayment + + if (simpleFiQrData?.type === 'SIMPLEFI_USER_SPECIFIED' && !simpleFiPayment && currencyAmount) { + setLoadingState('Fetching details') + try { + finalPayment = await simplefiApi.initiateQrPayment({ + type: 'USER_SPECIFIED', + merchantSlug: simpleFiQrData.merchantSlug, + currencyAmount: currencyAmount, + currency: 'ARS', + }) + setSimpleFiPayment(finalPayment) + } catch (error) { + captureException(error) + setErrorMessage('Unable to process payment. Please try again') + setIsSuccess(false) + setLoadingState('Idle') + return + } + } + + if (!finalPayment) { + setErrorMessage('Unable to fetch payment details') + setIsSuccess(false) + setLoadingState('Idle') + return + } + + setLoadingState('Preparing transaction') + let userOpHash: Hash + let receipt: TransactionReceipt | null + try { + const result = await sendMoney(finalPayment.address, finalPayment.usdAmount) + userOpHash = result.userOpHash + receipt = result.receipt + } catch (error) { + if ((error as Error).toString().includes('not allowed')) { + setErrorMessage('Please confirm the transaction in your wallet') + } else { + captureException(error) + setErrorMessage('Could not complete the transaction') + setIsSuccess(false) + } + setLoadingState('Idle') + return + } + + if (receipt !== null && isTxReverted(receipt)) { + setErrorMessage('Transaction was rejected by the network') + setLoadingState('Idle') + setIsSuccess(false) + return + } + + setLoadingState('Idle') + setIsSuccess(true) + }, [simpleFiPayment, simpleFiQrData, currencyAmount, sendMoney, setLoadingState]) + + const handleMantecaPayment = useCallback(async () => { if (!paymentLock || !qrCode || !currencyAmount) return let finalPaymentLock = paymentLock if (finalPaymentLock.code === '') { @@ -179,7 +331,6 @@ export default function QRPayPage() { } } if (finalPaymentLock.code === '') { - finalPaymentLock setErrorMessage('Could not fetch qr payment details') setIsSuccess(false) setLoadingState('Idle') @@ -222,7 +373,15 @@ export default function QRPayPage() { } finally { setLoadingState('Idle') } - }, [paymentLock?.code, sendMoney, usdAmount, qrCode, currencyAmount]) + }, [paymentLock?.code, sendMoney, qrCode, currencyAmount, setLoadingState]) + + const payQR = useCallback(async () => { + if (paymentProcessor === 'SIMPLEFI') { + await handleSimpleFiPayment() + } else if (paymentProcessor === 'MANTECA') { + await handleMantecaPayment() + } + }, [paymentProcessor, handleSimpleFiPayment, handleMantecaPayment]) // Check user balance useEffect(() => { @@ -246,6 +405,36 @@ export default function QRPayPage() { } }, [isSuccess]) + const handleOrderNotReadyRetry = useCallback(async () => { + setShowOrderNotReadyModal(false) + if (!simpleFiQrData || simpleFiQrData.type !== 'SIMPLEFI_STATIC') return + + setLoadingState('Fetching details') + try { + const response = await simplefiApi.initiateQrPayment({ + type: 'STATIC', + merchantSlug: simpleFiQrData.merchantSlug, + }) + setSimpleFiPayment(response) + setAmount(response.currencyAmount) + setCurrencyAmount(response.currencyAmount) + setCurrency({ + code: 'ARS', + symbol: 'ARS', + price: Number(response.price), + }) + } catch (error) { + const errorMsg = (error as Error).message + if (errorMsg.includes('ready to pay')) { + setShowOrderNotReadyModal(true) + } else { + setErrorInitiatingPayment(errorMsg) + } + } finally { + setLoadingState('Idle') + } + }, [simpleFiQrData, setLoadingState]) + if (!!errorInitiatingPayment) { return (
@@ -293,7 +482,11 @@ export default function QRPayPage() { onClose={() => router.back()} title="Verify your identity to continue" description="You'll need to verify your identity before paying with a QR code. Don't worry it usually just takes a few minutes." - icon={Mercado Pago} + icon={ + methodIcon ? ( + Payment method + ) : undefined + } ctas={[ { text: 'Verify now', @@ -329,15 +522,42 @@ export default function QRPayPage() { ) } - if (isFirstLoad || !paymentLock || !currency) { + if (showOrderNotReadyModal) { + return ( +
+ +
+

Order Not Ready

+

Please notify the cashier that you're ready to pay, then tap Retry.

+
+
+ +
+ + +
+
+
+ ) + } + + if ( + isFirstLoad || + (paymentProcessor === 'MANTECA' && !paymentLock) || + (paymentProcessor === 'SIMPLEFI' && simpleFiQrData?.type !== 'SIMPLEFI_USER_SPECIFIED' && !simpleFiPayment) || + !currency + ) { return } //Success - if (isSuccess && !qrPayment) { - // This should never happen, if this happens there is dev error + if (isSuccess && paymentProcessor === 'MANTECA' && !qrPayment) { return null - } else if (isSuccess) { + } else if (isSuccess && paymentProcessor === 'MANTECA' && qrPayment) { return (
@@ -420,6 +640,85 @@ export default function QRPayPage() { />
) + } else if (isSuccess && paymentProcessor === 'SIMPLEFI') { + return ( +
+ + +
+ +
+
+ +
+
+ +
+

You paid {merchantName}

+
+ ARS{' '} + {formatNumberForDisplay(simpleFiPayment?.currencyAmount ?? currencyAmount ?? '0', { + maxDecimals: 2, + })} +
+
+ ≈ {formatNumberForDisplay(usdAmount ?? undefined, { maxDecimals: 2 })} USD +
+
+
+
+ + +
+
+ +
+ ) } return ( @@ -434,7 +733,7 @@ export default function QRPayPage() {
Mercado Pago 资源 359 资源 359 资源 359 资源 359 \ No newline at end of file diff --git a/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts b/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts index 8560ef251..b586a0914 100644 --- a/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts +++ b/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts @@ -38,8 +38,9 @@ describe('recognizeQr', () => { ['https://example.com', EQrType.URL], ['http://domain.co.uk/path', EQrType.URL], ['https://pagar.simplefi.tech/peanut-test/static', EQrType.SIMPLEFI_STATIC], - ['https://pagar.simplefi.tech/peanut-test', EQrType.SIMPLEFI_DYNAMIC], - ['https://pagar.simplefi.tech/1234/payment/5678', EQrType.SIMPLEFI_USER_SPECIFIED], + ['https://pagar.simplefi.tech/peanut-test?static=true', EQrType.SIMPLEFI_STATIC], + ['https://pagar.simplefi.tech/peanut-test', EQrType.SIMPLEFI_USER_SPECIFIED], + ['https://pagar.simplefi.tech/1234/payment/5678', EQrType.SIMPLEFI_DYNAMIC], ['random text without any pattern', null], ['123456', null], ['', null], diff --git a/src/components/Global/DirectSendQR/utils.ts b/src/components/Global/DirectSendQR/utils.ts index b25bee845..f3bd4e8bd 100644 --- a/src/components/Global/DirectSendQR/utils.ts +++ b/src/components/Global/DirectSendQR/utils.ts @@ -63,9 +63,11 @@ const ARGENTINA_QR3_REGEX = /^(?=.*00020101021[12])(?=.*5303032)(?=.*5802AR)/i /* PIX is also a emvco qr code */ const PIX_REGEX = /^.*000201.*0014br\.gov\.bcb\.pix.*5303986.*5802BR.*$/i -export const SIMPLEFI_STATIC_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)\/static$/ -export const SIMPLEFI_DYNAMIC_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)$/ -export const SIMPLEFI_USER_SPECIFIED_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)\/payment\/(?[^\/]*)$/ +export const SIMPLEFI_STATIC_REGEX = + /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)(\/static$|\?static\=true)/ +export const SIMPLEFI_USER_SPECIFIED_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)$/ +export const SIMPLEFI_DYNAMIC_REGEX = + /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)\/payment\/(?[^\/]*)$/ export const PAYMENT_PROCESSOR_REGEXES: { [key in QrType]?: RegExp } = { [EQrType.MERCADO_PAGO]: MP_AR_REGEX, @@ -188,3 +190,50 @@ export const parseEip681 = ( return { address } } + +export interface SimpleFiStaticQrData { + type: 'SIMPLEFI_STATIC' + merchantSlug: string +} + +export interface SimpleFiDynamicQrData { + type: 'SIMPLEFI_DYNAMIC' + merchantId: string + paymentId: string +} + +export interface SimpleFiUserSpecifiedQrData { + type: 'SIMPLEFI_USER_SPECIFIED' + merchantSlug: string +} + +export type SimpleFiQrData = SimpleFiStaticQrData | SimpleFiDynamicQrData | SimpleFiUserSpecifiedQrData + +export const parseSimpleFiQr = (data: string): SimpleFiQrData | null => { + const staticMatch = data.match(SIMPLEFI_STATIC_REGEX) + if (staticMatch?.groups?.merchantSlug) { + return { + type: 'SIMPLEFI_STATIC', + merchantSlug: staticMatch.groups.merchantSlug, + } + } + + const dynamicMatch = data.match(SIMPLEFI_DYNAMIC_REGEX) + if (dynamicMatch?.groups?.merchantId && dynamicMatch?.groups?.paymentId) { + return { + type: 'SIMPLEFI_DYNAMIC', + merchantId: dynamicMatch.groups.merchantId, + paymentId: dynamicMatch.groups.paymentId, + } + } + + const userSpecifiedMatch = data.match(SIMPLEFI_USER_SPECIFIED_REGEX) + if (userSpecifiedMatch?.groups?.merchantSlug) { + return { + type: 'SIMPLEFI_USER_SPECIFIED', + merchantSlug: userSpecifiedMatch.groups.merchantSlug, + } + } + + return null +} diff --git a/src/services/simplefi.ts b/src/services/simplefi.ts new file mode 100644 index 000000000..7057171db --- /dev/null +++ b/src/services/simplefi.ts @@ -0,0 +1,73 @@ +import { PEANUT_API_URL } from '@/constants' +import { fetchWithSentry } from '@/utils' +import Cookies from 'js-cookie' +import type { Address } from 'viem' + +export type QrPaymentType = 'STATIC' | 'DYNAMIC' | 'USER_SPECIFIED' + +export interface SimpleFiQrPaymentRequest { + type: QrPaymentType + merchantSlug?: string + currencyAmount?: string + currency?: string + simplefiRequestId?: string +} + +export interface SimpleFiQrPaymentResponse { + id: string + usdAmount: string + currency: string + currencyAmount: string + price: string + address: Address +} + +export type SimpleFiErrorCode = + | 'ORDER_NOT_READY' + | 'PAYMENT_NOT_PENDING' + | 'NO_ARBITRUM_TRANSACTION' + | 'MISSING_MERCHANT_SLUG' + | 'MISSING_REQUEST_ID' + | 'MISSING_PARAMETERS' + | 'NO_MERCHANT_FOUND' + | 'INVALID_TYPE' + | 'UNEXPECTED_ERROR' + +export interface SimpleFiQrPaymentError { + error: SimpleFiErrorCode + message: string +} + +const ERROR_MESSAGES: Record = { + ORDER_NOT_READY: "Please notify the cashier that you're ready to pay", + PAYMENT_NOT_PENDING: 'This payment has expired or been completed', + NO_ARBITRUM_TRANSACTION: 'Payment method not supported', + MISSING_MERCHANT_SLUG: 'Invalid merchant QR code', + MISSING_REQUEST_ID: 'Invalid payment request', + MISSING_PARAMETERS: 'Missing required payment information', + NO_MERCHANT_FOUND: 'Merchant not found. Please use the standard payment option', + INVALID_TYPE: 'Invalid payment type', + UNEXPECTED_ERROR: 'Unable to process payment. Please try again', +} + +export const simplefiApi = { + initiateQrPayment: async (data: SimpleFiQrPaymentRequest): Promise => { + const response = await fetchWithSentry(`${PEANUT_API_URL}/simplefi/qr-pay`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Cookies.get('jwt-token')}`, + }, + body: JSON.stringify(data), + }) + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as SimpleFiQrPaymentError + const errorCode = errorData.error || 'UNEXPECTED_ERROR' + const userMessage = ERROR_MESSAGES[errorCode] || ERROR_MESSAGES.UNEXPECTED_ERROR + throw new Error(userMessage) + } + + return response.json() + }, +} From 662e16692cc3cc5199211b101481a49e8b5c92fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Thu, 9 Oct 2025 19:10:49 -0300 Subject: [PATCH 3/5] feat: add simplefi history --- src/app/(mobile-ui)/qr-pay/page.tsx | 2 +- .../TransactionDetails/TransactionDetailsReceipt.tsx | 1 + .../TransactionDetails/transactionTransformer.ts | 1 + src/utils/history.utils.ts | 7 ++++++- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index f43835aab..c322cd7e7 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -698,7 +698,7 @@ export default function QRPayPage() { date: now, createdAt: now, extraDataForDrawer: { - originalType: EHistoryEntryType.MANTECA_QR_PAYMENT, + originalType: EHistoryEntryType.SIMPLEFI_QR_PAYMENT, originalUserRole: EHistoryUserRole.SENDER, avatarUrl: methodIcon, receipt: { diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index c0211aee1..0cff6de6b 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -214,6 +214,7 @@ export const TransactionDetailsReceipt = ({ if ( [ EHistoryEntryType.MANTECA_QR_PAYMENT, + EHistoryEntryType.SIMPLEFI_QR_PAYMENT, EHistoryEntryType.MANTECA_OFFRAMP, EHistoryEntryType.MANTECA_ONRAMP, ].includes(transaction.extraDataForDrawer!.originalType) diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index e8a1e6d07..a78f06af4 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -304,6 +304,7 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact isPeerActuallyUser = false break case EHistoryEntryType.MANTECA_QR_PAYMENT: + case EHistoryEntryType.SIMPLEFI_QR_PAYMENT: direction = 'qr_payment' transactionCardType = 'pay' nameForDetails = entry.recipientAccount?.identifier || 'Merchant' diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index c2d0d8527..8ddfacb4f 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -1,4 +1,4 @@ -import { MERCADO_PAGO, PIX } from '@/assets/payment-apps' +import { MERCADO_PAGO, PIX, SIMPLEFI } from '@/assets/payment-apps' import { TransactionDetails } from '@/components/TransactionDetails/transactionTransformer' import { getFromLocalStorage } from '@/utils' import { PEANUT_WALLET_TOKEN_DECIMALS, BASE_URL } from '@/constants' @@ -18,6 +18,7 @@ export enum EHistoryEntryType { BRIDGE_ONRAMP = 'BRIDGE_ONRAMP', BANK_SEND_LINK_CLAIM = 'BANK_SEND_LINK_CLAIM', MANTECA_QR_PAYMENT = 'MANTECA_QR_PAYMENT', + SIMPLEFI_QR_PAYMENT = 'SIMPLEFI_QR_PAYMENT', MANTECA_OFFRAMP = 'MANTECA_OFFRAMP', MANTECA_ONRAMP = 'MANTECA_ONRAMP', BRIDGE_GUEST_OFFRAMP = 'BRIDGE_GUEST_OFFRAMP', @@ -132,6 +133,7 @@ export function getReceiptUrl(transaction: TransactionDetails): string | undefin transaction.extraDataForDrawer?.originalType && [ EHistoryEntryType.MANTECA_QR_PAYMENT, + EHistoryEntryType.SIMPLEFI_QR_PAYMENT, EHistoryEntryType.MANTECA_OFFRAMP, EHistoryEntryType.MANTECA_ONRAMP, EHistoryEntryType.SEND_LINK, @@ -160,6 +162,9 @@ export function getAvatarUrl(transaction: TransactionDetails): string | undefine return undefined } } + if (transaction.extraDataForDrawer?.originalType === EHistoryEntryType.SIMPLEFI_QR_PAYMENT) { + return SIMPLEFI + } } /** Returns the sign of the transaction, based on the direction and status of the transaction. */ From 392955de2a87cfacc19640890460ab90bd31964d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Thu, 9 Oct 2025 23:44:51 -0300 Subject: [PATCH 4/5] feat: add websocket update for simplefi --- src/app/(mobile-ui)/qr-pay/page.tsx | 92 ++++++++++++++++++- .../TransactionDetailsReceipt.tsx | 54 ++++------- .../transactionTransformer.ts | 9 ++ src/utils/history.utils.ts | 5 + 4 files changed, 117 insertions(+), 43 deletions(-) diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index c322cd7e7..323f14243 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -36,6 +36,9 @@ import ActionModal from '@/components/Global/ActionModal' import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal' import { SoundPlayer } from '@/components/Global/SoundPlayer' import { useQueryClient } from '@tanstack/react-query' +import { useAuth } from '@/context/authContext' +import { useWebSocket } from '@/hooks/useWebSocket' +import type { HistoryEntry } from '@/hooks/useTransactionHistory' const MAX_QR_PAYMENT_AMOUNT = '200' @@ -66,6 +69,9 @@ export default function QRPayPage() { const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext) const { shouldBlockPay, kycGateState } = useQrKycGate() const queryClient = useQueryClient() + const { user } = useAuth() + const [pendingSimpleFiPaymentId, setPendingSimpleFiPaymentId] = useState(null) + const [isWaitingForWebSocket, setIsWaitingForWebSocket] = useState(false) const paymentProcessor: PaymentProcessor | null = useMemo(() => { if ( @@ -95,8 +101,54 @@ export default function QRPayPage() { setQrPayment(null) setCurrency(undefined) setLoadingState('Idle') + setPendingSimpleFiPaymentId(null) + setIsWaitingForWebSocket(false) } + const handleSimpleFiStatusUpdate = useCallback( + (entry: HistoryEntry) => { + if (!pendingSimpleFiPaymentId || entry.uuid !== pendingSimpleFiPaymentId) { + return + } + + if (entry.type !== EHistoryEntryType.SIMPLEFI_QR_PAYMENT) { + return + } + + console.log('[SimpleFi WebSocket] Received status update:', entry.status) + + setIsWaitingForWebSocket(false) + setPendingSimpleFiPaymentId(null) + + switch (entry.status) { + case 'approved': + setSimpleFiPayment({ + id: entry.uuid, + usdAmount: entry.amount, + currency: entry.currency?.code || 'ARS', + currencyAmount: entry.currency?.amount || '0', + price: simpleFiPayment?.price || '1', + address: simpleFiPayment?.address || ('0x0' as any), + }) + setIsSuccess(true) + setLoadingState('Idle') + break + + case 'expired': + case 'canceled': + case 'refunded': + setErrorMessage('Payment failed or expired. Please try again.') + setIsSuccess(false) + setLoadingState('Idle') + break + + default: + console.log('[SimpleFi WebSocket] Unknown status:', entry.status) + } + }, + [pendingSimpleFiPaymentId, simpleFiPayment, setLoadingState] + ) + // First fetch for qrcode info — only after KYC gating allows proceeding useEffect(() => { resetState() @@ -114,6 +166,29 @@ export default function QRPayPage() { setIsFirstLoad(false) }, [timestamp, paymentProcessor, qrCode]) + useWebSocket({ + username: user?.user.username ?? undefined, + autoConnect: true, + onHistoryEntry: handleSimpleFiStatusUpdate, + }) + + useEffect(() => { + if (!isWaitingForWebSocket || !pendingSimpleFiPaymentId) return + + const timeout = setTimeout( + () => { + console.log('[SimpleFi WebSocket] Timeout after 5 minutes') + setIsWaitingForWebSocket(false) + setPendingSimpleFiPaymentId(null) + setErrorMessage('Payment is taking longer than expected. Please check your transaction history.') + setLoadingState('Idle') + }, + 5 * 60 * 1000 + ) + + return () => clearTimeout(timeout) + }, [isWaitingForWebSocket, pendingSimpleFiPaymentId, setLoadingState]) + // Get amount from payment lock (Manteca) useEffect(() => { if (paymentProcessor !== 'MANTECA') return @@ -310,8 +385,10 @@ export default function QRPayPage() { return } - setLoadingState('Idle') - setIsSuccess(true) + console.log('[SimpleFi] Transaction sent, waiting for WebSocket confirmation...') + setLoadingState('Paying') + setIsWaitingForWebSocket(true) + setPendingSimpleFiPaymentId(finalPayment.id) }, [simpleFiPayment, simpleFiQrData, currencyAmount, sendMoney, setLoadingState]) const handleMantecaPayment = useCallback(async () => { @@ -782,7 +859,7 @@ export default function QRPayPage() { {/* Error State */} diff --git a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx index 0cff6de6b..a2cfd9d1d 100644 --- a/src/components/TransactionDetails/TransactionDetailsReceipt.tsx +++ b/src/components/TransactionDetails/TransactionDetailsReceipt.tsx @@ -262,48 +262,26 @@ export const TransactionDetailsReceipt = ({ if (!transaction) return null - // format data for display with proper handling for different transaction types - let amountDisplay = '' + let usdAmount: number | bigint = 0 if (transactionAmount) { - // if transactionAmount is provided (from TransactionCard), use it - amountDisplay = transactionAmount.replace(/[+-]/g, '').replace(/\$/, '$ ') - } else if ( - (transaction.direction === 'bank_deposit' || - transaction.direction === 'bank_withdraw' || - transaction.direction === 'bank_request_fulfillment') && - transaction.currency?.code && - transaction.currency.code.toUpperCase() !== 'USD' - ) { - // handle bank deposits/withdrawals with non-USD currency - const isCompleted = transaction.status === 'completed' - - if (isCompleted) { - // for completed transactions: show USD amount (amount is already in USD) - const amount = transaction.amount || 0 - const numericAmount = typeof amount === 'bigint' ? Number(amount) : Number(amount) - amountDisplay = `$ ${formatAmount(isNaN(numericAmount) ? 0 : numericAmount)}` - } else { - // for non-completed transactions: show original currency - const currencyAmount = transaction.currency?.amount || transaction.amount.toString() - const numericAmount = Number(currencyAmount) - const currencySymbol = getDisplayCurrencySymbol(transaction.currency.code) - amountDisplay = `${currencySymbol} ${formatAmount(isNaN(numericAmount) ? 0 : numericAmount)}` - } - } else { - // default: use currency amount if provided, otherwise fallback to raw amount - never show token value, only USD - if (transaction.currency?.amount && transaction.currency?.code) { - const numericAmount = Number(transaction.currency.amount) - const amount = isNaN(numericAmount) ? 0 : numericAmount - const currencySymbol = getDisplayCurrencySymbol(transaction.currency.code) - amountDisplay = `${currencySymbol} ${formatAmount(amount)}` - } else { - const amount = transaction.amount || 0 - const numericAmount = typeof amount === 'bigint' ? Number(amount) : Number(amount) - amountDisplay = `$ ${formatAmount(isNaN(numericAmount) ? 0 : numericAmount)}` - } + // if transactionAmount is provided as a string, parse it + const parsed = parseFloat(transactionAmount.replace(/[\+\-\$]/g, '')) + usdAmount = isNaN(parsed) ? 0 : parsed + } else if (transaction.amount !== undefined && transaction.amount !== null) { + // fallback to transaction.amount + usdAmount = transaction.amount + } else if (transaction.currency?.amount) { + // last fallback to currency amount + const parsed = parseFloat(String(transaction.currency.amount)) + usdAmount = isNaN(parsed) ? 0 : parsed } + // ensure we have a valid number for display + const numericAmount = typeof usdAmount === 'bigint' ? Number(usdAmount) : usdAmount + const safeAmount = isNaN(numericAmount) || numericAmount === null || numericAmount === undefined ? 0 : numericAmount + const amountDisplay = `$ ${formatCurrency(Math.abs(safeAmount).toString())}` + const feeDisplay = transaction.fee !== undefined ? formatAmount(transaction.fee as number) : 'N/A' // determine if the qr code and sharing section should be shown diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index a78f06af4..70341662c 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -304,10 +304,16 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact isPeerActuallyUser = false break case EHistoryEntryType.MANTECA_QR_PAYMENT: + direction = 'qr_payment' + transactionCardType = 'pay' + nameForDetails = entry.recipientAccount?.identifier || 'Merchant' + isPeerActuallyUser = false + break case EHistoryEntryType.SIMPLEFI_QR_PAYMENT: direction = 'qr_payment' transactionCardType = 'pay' nameForDetails = entry.recipientAccount?.identifier || 'Merchant' + nameForDetails = nameForDetails.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) isPeerActuallyUser = false break default: @@ -371,10 +377,13 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact case 'SUCCESSFUL': case 'CLAIMED': case 'PAID': + case 'APPROVED': uiStatus = 'completed' break case 'FAILED': case 'ERROR': + case 'CANCELED': + case 'EXPIRED': uiStatus = 'failed' break case 'CANCELLED': diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 8ddfacb4f..47dab3541 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -65,6 +65,11 @@ export enum EHistoryStatus { REFUNDED = 'REFUNDED', CANCELED = 'CANCELED', ERROR = 'ERROR', + approved = 'approved', + pending = 'pending', + refunded = 'refunded', + canceled = 'canceled', + expired = 'expired', } export const FINAL_STATES: HistoryStatus[] = [ From c167d15b50d68bf2804291a59ab68329d1e76303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Mon, 13 Oct 2025 16:26:13 -0300 Subject: [PATCH 5/5] fix: simplefi integration comments --- src/app/(mobile-ui)/qr-pay/page.tsx | 28 ++++++++++--------- src/components/Global/DirectSendQR/utils.ts | 5 ++++ .../Global/TokenAmountInput/index.tsx | 5 ++-- .../transactionTransformer.ts | 2 ++ src/utils/history.utils.ts | 2 +- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index 323f14243..40a82c3bb 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -74,16 +74,18 @@ export default function QRPayPage() { const [isWaitingForWebSocket, setIsWaitingForWebSocket] = useState(false) const paymentProcessor: PaymentProcessor | null = useMemo(() => { - if ( - qrType === EQrType.SIMPLEFI_STATIC || - qrType === EQrType.SIMPLEFI_DYNAMIC || - qrType === EQrType.SIMPLEFI_USER_SPECIFIED - ) { - return 'SIMPLEFI' - } else if (qrType === EQrType.MERCADO_PAGO || qrType === EQrType.ARGENTINA_QR3 || qrType === EQrType.PIX) { - return 'MANTECA' + switch (qrType) { + case EQrType.SIMPLEFI_STATIC: + case EQrType.SIMPLEFI_DYNAMIC: + case EQrType.SIMPLEFI_USER_SPECIFIED: + return 'SIMPLEFI' + case EQrType.MERCADO_PAGO: + case EQrType.ARGENTINA_QR3: + case EQrType.PIX: + return 'MANTECA' + default: + return null } - return null }, [qrType]) const resetState = () => { @@ -125,10 +127,10 @@ export default function QRPayPage() { setSimpleFiPayment({ id: entry.uuid, usdAmount: entry.amount, - currency: entry.currency?.code || 'ARS', - currencyAmount: entry.currency?.amount || '0', - price: simpleFiPayment?.price || '1', - address: simpleFiPayment?.address || ('0x0' as any), + currency: entry.currency!.code, + currencyAmount: entry.currency!.amount, + price: simpleFiPayment!.price, + address: simpleFiPayment!.address, }) setIsSuccess(true) setLoadingState('Idle') diff --git a/src/components/Global/DirectSendQR/utils.ts b/src/components/Global/DirectSendQR/utils.ts index f3bd4e8bd..bcd0d34f2 100644 --- a/src/components/Global/DirectSendQR/utils.ts +++ b/src/components/Global/DirectSendQR/utils.ts @@ -63,6 +63,11 @@ const ARGENTINA_QR3_REGEX = /^(?=.*00020101021[12])(?=.*5303032)(?=.*5802AR)/i /* PIX is also a emvco qr code */ const PIX_REGEX = /^.*000201.*0014br\.gov\.bcb\.pix.*5303986.*5802BR.*$/i +/** Simplefi QR codes are urls, depending on the route and params we can + * infer the flow type and merchant slug. + * + * The flow type is static, dynamic or user_specified. + */ export const SIMPLEFI_STATIC_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)(\/static$|\?static\=true)/ export const SIMPLEFI_USER_SPECIFIED_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?[^\/]*)$/ diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 85f51d2c5..a5917910f 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -1,6 +1,6 @@ import { PEANUT_WALLET_TOKEN_DECIMALS, STABLE_COINS } from '@/constants' import { tokenSelectorContext } from '@/context' -import { formatAmountWithoutComma, formatTokenAmount } from '@/utils' +import { formatAmountWithoutComma, formatTokenAmount, formatCurrency } from '@/utils' import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import Icon from '../Icon' import { twMerge } from 'tailwind-merge' @@ -240,7 +240,8 @@ const TokenAmountInput = ({ {/* Conversion */} {showConversion && ( )} diff --git a/src/components/TransactionDetails/transactionTransformer.ts b/src/components/TransactionDetails/transactionTransformer.ts index 70341662c..cf807116c 100644 --- a/src/components/TransactionDetails/transactionTransformer.ts +++ b/src/components/TransactionDetails/transactionTransformer.ts @@ -313,6 +313,8 @@ export function mapTransactionDataForDrawer(entry: HistoryEntry): MappedTransact direction = 'qr_payment' transactionCardType = 'pay' nameForDetails = entry.recipientAccount?.identifier || 'Merchant' + // We dont have merchant name so we try to prettify the slug, + // replacing dashws with speaces and making the first letter uppercase nameForDetails = nameForDetails.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) isPeerActuallyUser = false break diff --git a/src/utils/history.utils.ts b/src/utils/history.utils.ts index 47dab3541..967669302 100644 --- a/src/utils/history.utils.ts +++ b/src/utils/history.utils.ts @@ -68,7 +68,7 @@ export enum EHistoryStatus { approved = 'approved', pending = 'pending', refunded = 'refunded', - canceled = 'canceled', + canceled = 'canceled', // from simplefi, canceled with only one l expired = 'expired', }