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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@chakra-ui/react": "^2.10.4",
"@chakra-ui/react-context": "^2.1.0",
"@chakra-ui/shared-utils": "^2.0.4",
"@daimo/pay": "^1.15.0",
"@daimo/pay": "^1.16.5",
"@dicebear/collection": "^9.2.2",
"@dicebear/core": "^9.2.2",
"@headlessui/react": "^1.7.19",
Expand Down
582 changes: 235 additions & 347 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions src/app/(mobile-ui)/add-money/crypto/direct/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ export default function AddMoneyCryptoDirectPage() {

// Save deposit txn hash in the backend to track the user's deposit
try {
await trackDaimoDepositTransactionHash(e.txHash, e.payment.source.payerAddress)
await trackDaimoDepositTransactionHash({
txHash: e.txHash,
payerAddress: e.payment.source.payerAddress,
sourceChainId: e.payment.source.chainId,
sourceTokenAddress: e.payment.source.tokenAddress,
})
} catch (error) {
console.error('Error updating depositor address:', error)
} finally {
Expand All @@ -40,9 +45,9 @@ export default function AddMoneyCryptoDirectPage() {
if (isPaymentSuccess) {
return (
<DirectSuccessView
key={`success-add-money}`}
key={`success-add-money`}
headerTitle={'Add Money'}
type="SEND"
type="DEPOSIT"
currencyAmount={`$${inputTokenAmount}`}
isWithdrawFlow={false}
redirectTo={'/add-money'}
Expand Down
8 changes: 5 additions & 3 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
const isDirectPay = flow === 'direct_pay'
const isExternalWalletFlow = flow === 'external_wallet'
const dispatch = useAppDispatch()
const { currentView, parsedPaymentData, chargeDetails, paymentDetails, usdAmount } = usePaymentStore()
const { currentView, parsedPaymentData, chargeDetails, paymentDetails, usdAmount, isDaimoPaymentProcessing } =
usePaymentStore()
const [error, setError] = useState<ValidationErrorViewProps | null>(null)
const [isUrlParsed, setIsUrlParsed] = useState(false)
const [isRequestDetailsFetching, setIsRequestDetailsFetching] = useState(false)
Expand Down Expand Up @@ -391,8 +392,9 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
)
}

// show loading until URL is parsed and req/charge data is loaded
const isLoading = !isUrlParsed || (chargeId && !chargeDetails) || isRequestDetailsFetching
// show loading until URL is parsed and req/charge data is loaded or daimo payment is processing
const isLoading =
!isUrlParsed || (chargeId && !chargeDetails) || isRequestDetailsFetching || isDaimoPaymentProcessing

if (isLoading) {
return (
Expand Down
14 changes: 13 additions & 1 deletion src/app/actions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,17 @@ export async function getUserById(userId: string): Promise<User | null> {
}
}

export async function trackDaimoDepositTransactionHash(txHash: string, payerAddress: string): Promise<{ data?: any }> {
export async function trackDaimoDepositTransactionHash({
txHash,
payerAddress,
sourceChainId,
sourceTokenAddress,
}: {
txHash: string
payerAddress: string
sourceChainId: string
sourceTokenAddress: string
}): Promise<{ data?: any }> {
try {
if (!txHash || !payerAddress) {
throw new Error('Missing required fields: txHash and payerAddress')
Expand All @@ -135,6 +145,8 @@ export async function trackDaimoDepositTransactionHash(txHash: string, payerAddr
body: JSON.stringify({
txHash,
payerAddress,
sourceChainId,
sourceTokenAddress,
}),
})

Expand Down
2 changes: 1 addition & 1 deletion src/components/AddMoney/components/CryptoMethodDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const CryptoMethodDrawer = ({
return (
<>
<Drawer open={isDrawerOpen} onOpenChange={showRiskModal ? undefined : closeDrawer}>
<DrawerContent className="p-5">
<DrawerContent className="p-5 pb-14">
<div className="mx-auto space-y-4 md:max-w-2xl">
<h2 className="text-base font-bold">Select a deposit method</h2>

Expand Down
56 changes: 9 additions & 47 deletions src/components/Common/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
import { SearchResultCard } from '../SearchUsers/SearchResultCard'
import StatusBadge from '../Global/Badges/StatusBadge'
import IconStack from '../Global/IconStack'
import mercadoPagoIcon from '@/assets/payment-apps/mercado-pago.svg'
import binanceIcon from '@/assets/exchanges/binance.svg'
import { METAMASK_LOGO, TRUST_WALLET_SMALL_LOGO } from '@/assets/wallets'
import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { ClaimLinkData } from '@/services/sendLinks'
import { formatUnits } from 'viem'
Expand All @@ -25,42 +22,8 @@ import { ParsedURL } from '@/lib/url-parser/types/payment'
import { usePaymentStore } from '@/redux/hooks'
import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType'
import { GuestVerificationModal } from '../Global/GuestVerificationModal'

export interface Method {
id: string
title: string
description: string
icons: any[]
soon: boolean
}

const ACTION_METHODS: Method[] = [
{
id: 'bank',
title: 'Bank',
description: 'EUR, USD, ARS (more coming soon)',
icons: [
'https://flagcdn.com/w160/ar.png',
'https://flagcdn.com/w160/de.png',
'https://flagcdn.com/w160/us.png',
],
soon: false,
},
{
id: 'mercadopago',
title: 'Mercado Pago',
description: 'Instant transfers',
icons: [mercadoPagoIcon],
soon: true,
},
{
id: 'exchange-or-wallet',
title: 'Exchange or Wallet',
description: 'Binance, Coinbase, Metamask and more',
icons: [binanceIcon, TRUST_WALLET_SMALL_LOGO, METAMASK_LOGO],
soon: false,
},
]
import ActionListDaimoPayButton from './ActionListDaimoPayButton'
import { ACTION_METHODS, PaymentMethod } from '@/constants/actionlist.consts'

interface IActionListProps {
flow: 'claim' | 'request'
Expand Down Expand Up @@ -94,7 +57,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
} = useRequestFulfillmentFlow()
const [isGuestVerificationModalOpen, setIsGuestVerificationModalOpen] = useState(false)

const handleMethodClick = async (method: Method) => {
const handleMethodClick = async (method: PaymentMethod) => {
if (flow === 'claim' && claimLinkData) {
const amountInUsd = parseFloat(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals))
if (method.id === 'bank' && amountInUsd < 1) {
Expand Down Expand Up @@ -122,7 +85,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
break
}
} else if (flow === 'request' && requestLinkData) {
const amountInUsd = parseFloat(usdAmount ?? '0')
const amountInUsd = usdAmount ? parseFloat(usdAmount) : 0
if (method.id === 'bank' && amountInUsd < 1) {
setShowMinAmountError(true)
return
Expand Down Expand Up @@ -179,12 +142,11 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
)}
<Divider text="or" />
<div className="space-y-2">
{ACTION_METHODS.filter((method) => {
if (flow === 'request') {
return method.id !== 'exchange-or-wallet'
{ACTION_METHODS.map((method) => {
if (flow === 'request' && method.id === 'exchange-or-wallet') {
return <ActionListDaimoPayButton key={method.id} />
}
return true
}).map((method) => {

return (
<MethodCard
onClick={() => handleMethodClick(method)}
Expand Down Expand Up @@ -221,7 +183,7 @@ export const MethodCard = ({
onClick,
requiresVerification,
}: {
method: Method
method: PaymentMethod
onClick: () => void
requiresVerification?: boolean
}) => {
Expand Down
154 changes: 154 additions & 0 deletions src/components/Common/ActionListDaimoPayButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { useCallback } from 'react'
import { SearchResultCard } from '../SearchUsers/SearchResultCard'
import IconStack from '../Global/IconStack'
import { useAppDispatch, usePaymentStore } from '@/redux/hooks'
import { paymentActions } from '@/redux/slices/payment-slice'
import { useCurrency } from '@/hooks/useCurrency'
import { useSearchParams } from 'next/navigation'
import { InitiatePaymentPayload, usePaymentInitiator } from '@/hooks/usePaymentInitiator'
import DaimoPayButton from '../Global/DaimoPayButton'
import { ACTION_METHODS } from '@/constants/actionlist.consts'

const ActionListDaimoPayButton = () => {
const dispatch = useAppDispatch()
const searchParams = useSearchParams()
const method = ACTION_METHODS.find((method) => method.id === 'exchange-or-wallet')
const { requestDetails, chargeDetails, attachmentOptions, parsedPaymentData, usdAmount } = usePaymentStore()
const {
code: currencyCode,
symbol: currencySymbol,
price: currencyPrice,
} = useCurrency(searchParams.get('currency'))
const requestId = searchParams.get('id')

const { isProcessing, initiateDaimoPayment, completeDaimoPayment } = usePaymentInitiator()

const handleInitiateDaimoPayment = useCallback(async () => {
if (!usdAmount || parseFloat(usdAmount) <= 0) {
console.error('Invalid amount entered')
dispatch(paymentActions.setError('Invalid amount'))
return false
}

if (!parsedPaymentData) {
console.error('Invalid payment data')
dispatch(paymentActions.setError('Something went wrong. Please try again.'))
return false
}

dispatch(paymentActions.setError(null))
dispatch(paymentActions.setDaimoError(null))
let tokenAmount = usdAmount

const payload: InitiatePaymentPayload = {
recipient: parsedPaymentData?.recipient,
tokenAmount,
isPintaReq: false, // explicitly set to false for non-PINTA requests
requestId: requestId ?? undefined,
chargeId: chargeDetails?.uuid,
currency: currencyCode
? {
code: currencyCode,
symbol: currencySymbol || '',
price: currencyPrice || 0,
}
: undefined,
currencyAmount: usdAmount,
isExternalWalletFlow: false,
transactionType: 'DIRECT_SEND',
attachmentOptions: attachmentOptions,
}

console.log('Initiating Daimo payment', payload)

const result = await initiateDaimoPayment(payload)

if (result.status === 'Charge Created') {
console.log('Charge created!!')
return true
} else if (result.status === 'Error') {
dispatch(paymentActions.setError('Something went wrong. Please try again.'))
console.error('Payment initiation failed:', result)
return false
} else {
console.warn('Unexpected status from usePaymentInitiator:', result.status)
dispatch(paymentActions.setError('Something went wrong. Please try again.'))
return false
}
}, [
usdAmount,
dispatch,
chargeDetails,
requestDetails,
requestId,
parsedPaymentData,
attachmentOptions,
initiateDaimoPayment,
currencyCode,
currencySymbol,
currencyPrice,
])

const handleCompleteDaimoPayment = useCallback(
async (daimoPaymentResponse: any) => {
if (chargeDetails) {
dispatch(paymentActions.setIsDaimoPaymentProcessing(true))
try {
const result = await completeDaimoPayment({
chargeDetails: chargeDetails,
txHash: daimoPaymentResponse.txHash as string,
sourceChainId: daimoPaymentResponse.payment.source.chainId,
payerAddress: daimoPaymentResponse.payment.source.payerAddress,
})

if (result.status === 'Success') {
dispatch(paymentActions.setView('STATUS'))
} else if (result.status === 'Charge Created') {
dispatch(paymentActions.setView('CONFIRM'))
} else if (result.status === 'Error') {
console.error('Payment initiation failed:', result.error)
} else {
console.warn('Unexpected status from usePaymentInitiator:', result.status)
}
} catch (e) {
console.error('Error completing daimo payment:', e)
} finally {
dispatch(paymentActions.setIsDaimoPaymentProcessing(false))
}
}
},
[chargeDetails, completeDaimoPayment, dispatch]
)

if (!method || !parsedPaymentData) return null

return (
<DaimoPayButton
amount={usdAmount ?? '0.10'}
toAddress={parsedPaymentData.recipient.resolvedAddress}
onPaymentCompleted={handleCompleteDaimoPayment}
onBeforeShow={handleInitiateDaimoPayment}
disabled={!usdAmount}
minAmount={0.1}
maxAmount={4000}
loading={isProcessing}
onValidationError={(error) => {
dispatch(paymentActions.setDaimoError(error))
}}
>
{({ onClick, loading }) => (
<SearchResultCard
isDisabled={loading || isProcessing}
position="single"
description={method.description}
descriptionClassName="text-[12px]"
title={method.title}
onClick={onClick}
rightContent={<IconStack icons={method.icons} iconSize={24} />}
/>
)}
</DaimoPayButton>
)
}

export default ActionListDaimoPayButton
2 changes: 1 addition & 1 deletion src/components/Common/CountryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const CountryList = ({ inputTitle, viewMode, onCountryClick, onCryptoClic
<div className="mb-2">
<SearchResultCard
key="crypto"
title="Crypto"
title={flow === 'withdraw' ? 'Crypto' : 'Crypto Deposit'}
description={
flow === 'add'
? 'Use an exchange or your wallet'
Expand Down
Loading
Loading