diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index e94e288ba..1209d64fc 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -78,7 +78,23 @@ export default function QRPayPage() { const { openTransactionDetails, selectedTransaction, isDrawerOpen, closeTransactionDetails } = useTransactionDetailsDrawer() const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext) - const { shouldBlockPay, kycGateState } = useQrKycGate() + + const paymentProcessor: PaymentProcessor | null = useMemo(() => { + 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 + } + }, [qrType]) + + const { shouldBlockPay, kycGateState } = useQrKycGate(paymentProcessor) const queryClient = useQueryClient() const { hasPendingTransactions } = usePendingTransactions() const [isShaking, setIsShaking] = useState(false) @@ -97,21 +113,6 @@ export default function QRPayPage() { const [waitingForMerchantAmount, setWaitingForMerchantAmount] = useState(false) const retryCount = useRef(0) - const paymentProcessor: PaymentProcessor | null = useMemo(() => { - 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 - } - }, [qrType]) - const resetState = () => { setIsSuccess(false) setErrorMessage(null) @@ -301,6 +302,22 @@ export default function QRPayPage() { getCurrencyObject().then(setCurrency) }, [paymentLock?.code, paymentProcessor]) + // Set default currency for SimpleFi USER_SPECIFIED (user will enter amount) + useEffect(() => { + if (paymentProcessor !== 'SIMPLEFI') return + if (simpleFiQrData?.type !== 'SIMPLEFI_USER_SPECIFIED') return + if (currency) return // Already set + + // Default to ARS for SimpleFi payments + getCurrencyPrice('ARS').then((priceData) => { + setCurrency({ + code: 'ARS', + symbol: 'ARS', + price: priceData.sell, + }) + }) + }, [paymentProcessor, simpleFiQrData?.type, currency]) + const isBlockingError = useMemo(() => { return !!errorMessage && errorMessage !== 'Please confirm the transaction.' }, [errorMessage]) diff --git a/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts b/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts index 9aba5276a..70c4406da 100644 --- a/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts +++ b/src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts @@ -321,6 +321,15 @@ describe('recognizeQr', () => { ['www.pagar.simplefi.tech/peanut-test?static=true', 'without protocol'], ['pagar.simplefi.tech/peanut-test?static=true', 'without www and protocol'], ['https://pagar.simplefi.tech/merchant-123/static', 'with numeric merchant slug'], + // New pay.simplefi.tech URLs + ['https://pay.simplefi.tech/peanut-test/static', 'pay subdomain with /static path'], + ['https://www.pay.simplefi.tech/peanut-test/static', 'pay subdomain with www'], + ['http://www.pay.simplefi.tech/peanut-test/static', 'pay subdomain http protocol'], + ['http://www.pay.simplefi.tech/peanut-test/static?stupid=params', 'pay subdomain with query params'], + ['https://pay.simplefi.tech/peanut-test?static=true', 'pay subdomain with static=true param'], + ['www.pay.simplefi.tech/peanut-test?static=true', 'pay subdomain without protocol'], + ['pay.simplefi.tech/peanut-test?static=true', 'pay subdomain without www and protocol'], + ['https://pay.simplefi.tech/merchant-123/static', 'pay subdomain with numeric merchant slug'], ])('should recognize %s (%s)', (data, _description) => { expect(recognizeQr(data)).toBe(EQrType.SIMPLEFI_STATIC) }) @@ -337,18 +346,31 @@ describe('recognizeQr', () => { describe('SIMPLEFI_DYNAMIC', () => { it.each([ - ['https://pagar.simplefi.tech/1234/payment/5678', 'standard dynamic payment'], - ['https://www.pagar.simplefi.tech/merchant-slug/payment/pay-id-123', 'with www'], - ['http://pagar.simplefi.tech/abc/payment/def', 'http protocol'], - ['pagar.simplefi.tech/merchant/payment/payment-id', 'without protocol'], + // Old format with /payment/ (backward compatibility) + ['https://pagar.simplefi.tech/1234/payment/5678', 'old format: standard dynamic payment'], + ['https://www.pagar.simplefi.tech/merchant-slug/payment/pay-id-123', 'old format: with www'], + ['http://pagar.simplefi.tech/abc/payment/def', 'old format: http protocol'], + ['pagar.simplefi.tech/merchant/payment/payment-id', 'old format: without protocol'], + ['https://pay.simplefi.tech/1234/payment/5678', 'old format: pay subdomain'], + ['https://www.pay.simplefi.tech/merchant-slug/payment/pay-id-123', 'old format: pay subdomain with www'], + ['http://pay.simplefi.tech/abc/payment/def', 'old format: pay subdomain http protocol'], + ['pay.simplefi.tech/merchant/payment/payment-id', 'old format: pay subdomain without protocol'], + // New format without /payment/ (current) + ['https://pagar.simplefi.tech/1234/5678', 'new format: pagar subdomain'], + ['https://www.pagar.simplefi.tech/merchant-slug/pay-id-123', 'new format: pagar with www'], + ['http://pagar.simplefi.tech/abc/def', 'new format: pagar http protocol'], + ['pagar.simplefi.tech/merchant/payment-id', 'new format: pagar without protocol'], + ['https://pay.simplefi.tech/1234/5678', 'new format: pay subdomain'], + ['https://www.pay.simplefi.tech/merchant-slug/pay-id-123', 'new format: pay with www'], + ['http://pay.simplefi.tech/abc/def', 'new format: pay http protocol'], + ['pay.simplefi.tech/merchant/payment-id', 'new format: pay without protocol'], ])('should recognize %s (%s)', (data, _description) => { expect(recognizeQr(data)).toBe(EQrType.SIMPLEFI_DYNAMIC) }) it.each([ - ['https://pagar.simplefi.tech/1234/payment', 'missing payment ID'], - ['https://pagar.simplefi.tech/payment/5678', 'missing merchant ID'], - ['https://pagar.simplefi.tech/1234/pay/5678', 'wrong path segment (pay vs payment)'], + ['https://pagar.simplefi.tech/1234/pay/5678/extra', 'too many path segments'], + ['https://pagar.simplefi.tech/merchant', 'only one path segment (should be USER_SPECIFIED)'], ])('should NOT recognize %s as SIMPLEFI_DYNAMIC (%s)', (data, _description) => { expect(recognizeQr(data)).not.toBe(EQrType.SIMPLEFI_DYNAMIC) }) @@ -361,13 +383,21 @@ describe('recognizeQr', () => { ['http://pagar.simplefi.tech/shop-name', 'http protocol'], ['pagar.simplefi.tech/store', 'without protocol'], ['https://pagar.simplefi.tech/merchant-with-dashes', 'merchant with dashes'], + // New pay.simplefi.tech URLs + ['https://pay.simplefi.tech/peanut-test', 'pay subdomain basic merchant slug'], + ['https://www.pay.simplefi.tech/merchant', 'pay subdomain with www'], + ['http://pay.simplefi.tech/shop-name', 'pay subdomain http protocol'], + ['pay.simplefi.tech/store', 'pay subdomain without protocol'], + ['https://pay.simplefi.tech/merchant-with-dashes', 'pay subdomain merchant with dashes'], ])('should recognize %s (%s)', (data, _description) => { expect(recognizeQr(data)).toBe(EQrType.SIMPLEFI_USER_SPECIFIED) }) it.each([ ['https://other-domain.com/merchant', 'wrong domain'], - ['https://simplefi.tech/merchant', 'missing pagar subdomain'], + ['https://simplefi.tech/merchant', 'missing pagar/pay subdomain'], + ['https://pagar.simplefi.tech/merchant/123', 'two path segments (should be DYNAMIC)'], + ['https://pay.simplefi.tech/merchant/456', 'two path segments on pay subdomain (should be DYNAMIC)'], ])('should NOT recognize %s as SIMPLEFI_USER_SPECIFIED (%s)', (data, _description) => { expect(recognizeQr(data)).not.toBe(EQrType.SIMPLEFI_USER_SPECIFIED) }) @@ -376,6 +406,11 @@ describe('recognizeQr', () => { // The regex captures an empty merchant slug with trailing slash expect(recognizeQr('https://pagar.simplefi.tech/')).toBe(EQrType.SIMPLEFI_USER_SPECIFIED) }) + + it('should recognize https://pay.simplefi.tech/ as SIMPLEFI_USER_SPECIFIED (regex matches trailing slash)', () => { + // The regex captures an empty merchant slug with trailing slash + expect(recognizeQr('https://pay.simplefi.tech/')).toBe(EQrType.SIMPLEFI_USER_SPECIFIED) + }) }) describe('URL (generic)', () => { @@ -457,6 +492,26 @@ describe('recognizeQr', () => { expect(recognizeQr(dynamicUrl)).toBe(EQrType.SIMPLEFI_DYNAMIC) }) + it('should prioritize SIMPLEFI_STATIC over SIMPLEFI_USER_SPECIFIED for pay.simplefi.tech', () => { + const staticUrl = 'https://pay.simplefi.tech/merchant/static' + expect(recognizeQr(staticUrl)).toBe(EQrType.SIMPLEFI_STATIC) + }) + + it('should prioritize SIMPLEFI_DYNAMIC over SIMPLEFI_USER_SPECIFIED for pay.simplefi.tech', () => { + const dynamicUrl = 'https://pay.simplefi.tech/merchant/payment/123' + expect(recognizeQr(dynamicUrl)).toBe(EQrType.SIMPLEFI_DYNAMIC) + }) + + it('should prioritize SIMPLEFI_DYNAMIC (new format) over SIMPLEFI_USER_SPECIFIED for pagar.simplefi.tech', () => { + const dynamicUrlNewFormat = 'https://pagar.simplefi.tech/merchant/123' + expect(recognizeQr(dynamicUrlNewFormat)).toBe(EQrType.SIMPLEFI_DYNAMIC) + }) + + it('should prioritize SIMPLEFI_DYNAMIC (new format) over SIMPLEFI_USER_SPECIFIED for pay.simplefi.tech', () => { + const dynamicUrlNewFormat = 'https://pay.simplefi.tech/merchant/456' + expect(recognizeQr(dynamicUrlNewFormat)).toBe(EQrType.SIMPLEFI_DYNAMIC) + }) + it('should prioritize ENS_NAME over URL for valid ENS domains', () => { const ensName = 'vitalik.eth' expect(recognizeQr(ensName)).toBe(EQrType.ENS_NAME) diff --git a/src/components/Global/DirectSendQR/utils.ts b/src/components/Global/DirectSendQR/utils.ts index 1c37a990d..b811c58f7 100644 --- a/src/components/Global/DirectSendQR/utils.ts +++ b/src/components/Global/DirectSendQR/utils.ts @@ -68,12 +68,15 @@ const PIX_REGEX = /^.*000201.*0014br\.gov\.bcb\.pix.*5303986.*5802BR.*$/i * infer the flow type and merchant slug. * * The flow type is static, dynamic or user_specified. + * Supports both pagar.simplefi.tech (legacy) and pay.simplefi.tech (new) URLs. + * Dynamic URLs support both old format (/merchant/payment/123) and new format (/merchant/123). */ export const SIMPLEFI_STATIC_REGEX = - /^(?:https?:\/\/)?(?:www\.)?pagar\.simplefi\.tech\/(?[^\/]*)(\/static|\/?\?.*static\=true.*)/ -export const SIMPLEFI_USER_SPECIFIED_REGEX = /^(?:https?:\/\/)?(?:www\.)?pagar\.simplefi\.tech\/(?[^\/]*)/ + /^(?:https?:\/\/)?(?:www\.)?(?:pagar|pay)\.simplefi\.tech\/(?[^\/]*)(\/static|\/?\?.*static\=true.*)/ +export const SIMPLEFI_USER_SPECIFIED_REGEX = + /^(?:https?:\/\/)?(?:www\.)?(?:pagar|pay)\.simplefi\.tech\/(?[^\/\?]*)(?:\/)?(?:\?.*)?$/ export const SIMPLEFI_DYNAMIC_REGEX = - /^(?:https?:\/\/)?(?:www\.)?pagar\.simplefi\.tech\/(?[^\/]*)\/payment\/(?[^\/]*)/ + /^(?:https?:\/\/)?(?:www\.)?(?:pagar|pay)\.simplefi\.tech\/(?[^\/]*)\/(?:payment\/)?(?[^\/\?]+)(?:\/)?(?:\?.*)?$/ export const PAYMENT_PROCESSOR_REGEXES: { [key in QrType]?: RegExp } = { [EQrType.MERCADO_PAGO]: MP_AR_REGEX, diff --git a/src/hooks/useQrKycGate.ts b/src/hooks/useQrKycGate.ts index 26a8a63e8..e4d175f8b 100644 --- a/src/hooks/useQrKycGate.ts +++ b/src/hooks/useQrKycGate.ts @@ -21,13 +21,22 @@ export interface QrKycGateResult { /** * This hook determines the KYC gate state for the QR pay page. * It checks the user's KYC status and the country of the QR code to determine the appropriate action. + * @param paymentProcessor - The payment processor type ('MANTECA' | 'SIMPLEFI' | null) * @returns {QrKycGateResult} An object with the KYC gate state and a boolean indicating if the user should be blocked from paying. + * + * Note: KYC is only required for MANTECA payments. SimpleFi payments do not require KYC. */ -export function useQrKycGate(): QrKycGateResult { +export function useQrKycGate(paymentProcessor?: 'MANTECA' | 'SIMPLEFI' | null): QrKycGateResult { const { user } = useAuth() const [kycGateState, setKycGateState] = useState(QrKycState.LOADING) const determineKycGateState = useCallback(async () => { + // SimpleFi payments do not require KYC - allow payment immediately + if (paymentProcessor === 'SIMPLEFI') { + setKycGateState(QrKycState.PROCEED_TO_PAY) + return + } + const currentUser = user?.user if (!currentUser) { setKycGateState(QrKycState.REQUIRES_IDENTITY_VERIFICATION) @@ -68,7 +77,7 @@ export function useQrKycGate(): QrKycGateResult { } setKycGateState(QrKycState.REQUIRES_IDENTITY_VERIFICATION) - }, [user?.user]) + }, [user?.user, paymentProcessor]) useEffect(() => { determineKycGateState()