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
444 changes: 417 additions & 27 deletions src/app/(mobile-ui)/qr-pay/page.tsx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/assets/payment-apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { default as MERCADO_PAGO } from './mercado-pago.svg'
export { default as PAYPAL } from './paypal.svg'
export { default as SATISPAY } from './satispay.svg'
export { default as PIX } from './pix.svg'
export { default as SIMPLEFI } from './simplefi-logo.svg'

1 change: 1 addition & 0 deletions src/assets/payment-apps/simplefi-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ 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?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],
Expand Down
3 changes: 3 additions & 0 deletions src/components/Global/DirectSendQR/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions src/components/Global/DirectSendQR/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {
Expand All @@ -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}`
Expand Down Expand Up @@ -57,10 +63,24 @@ 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\/(?<merchantSlug>[^\/]*)(\/static$|\?static\=true)/
export const SIMPLEFI_USER_SPECIFIED_REGEX = /^https:\/\/pagar\.simplefi\.tech\/(?<merchantSlug>[^\/]*)$/
export const SIMPLEFI_DYNAMIC_REGEX =
/^https:\/\/pagar\.simplefi\.tech\/(?<merchantId>[^\/]*)\/payment\/(?<paymentId>[^\/]*)$/

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
Expand All @@ -70,6 +90,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,
Expand Down Expand Up @@ -172,3 +195,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
}
5 changes: 3 additions & 2 deletions src/components/Global/TokenAmountInput/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -240,7 +240,8 @@ const TokenAmountInput = ({
{/* Conversion */}
{showConversion && (
<label className={twMerge('text-lg font-bold', !Number(alternativeDisplayValue) && 'text-gray-1')}>
≈ {alternativeDisplayValue} {alternativeDisplaySymbol}
≈ {displayMode === 'TOKEN' ? alternativeDisplayValue : formatCurrency(alternativeDisplayValue)}{' '}
{alternativeDisplaySymbol}
</label>
)}

Expand Down
55 changes: 17 additions & 38 deletions src/components/TransactionDetails/TransactionDetailsReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -261,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
Expand Down
Loading
Loading