Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 33 additions & 16 deletions src/app/(mobile-ui)/qr-pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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])
Expand Down
71 changes: 63 additions & 8 deletions src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand All @@ -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)
})
Expand All @@ -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)
})
Expand All @@ -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)', () => {
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions src/components/Global/DirectSendQR/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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\/(?<merchantSlug>[^\/]*)(\/static|\/?\?.*static\=true.*)/
export const SIMPLEFI_USER_SPECIFIED_REGEX = /^(?:https?:\/\/)?(?:www\.)?pagar\.simplefi\.tech\/(?<merchantSlug>[^\/]*)/
/^(?:https?:\/\/)?(?:www\.)?(?:pagar|pay)\.simplefi\.tech\/(?<merchantSlug>[^\/]*)(\/static|\/?\?.*static\=true.*)/
export const SIMPLEFI_USER_SPECIFIED_REGEX =
/^(?:https?:\/\/)?(?:www\.)?(?:pagar|pay)\.simplefi\.tech\/(?<merchantSlug>[^\/\?]*)(?:\/)?(?:\?.*)?$/
export const SIMPLEFI_DYNAMIC_REGEX =
/^(?:https?:\/\/)?(?:www\.)?pagar\.simplefi\.tech\/(?<merchantId>[^\/]*)\/payment\/(?<paymentId>[^\/]*)/
/^(?:https?:\/\/)?(?:www\.)?(?:pagar|pay)\.simplefi\.tech\/(?<merchantId>[^\/]*)\/(?:payment\/)?(?<paymentId>[^\/\?]+)(?:\/)?(?:\?.*)?$/

export const PAYMENT_PROCESSOR_REGEXES: { [key in QrType]?: RegExp } = {
[EQrType.MERCADO_PAGO]: MP_AR_REGEX,
Expand Down
13 changes: 11 additions & 2 deletions src/hooks/useQrKycGate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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)
Expand Down Expand Up @@ -68,7 +77,7 @@ export function useQrKycGate(): QrKycGateResult {
}

setKycGateState(QrKycState.REQUIRES_IDENTITY_VERIFICATION)
}, [user?.user])
}, [user?.user, paymentProcessor])

useEffect(() => {
determineKycGateState()
Expand Down
Loading