Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
9 changes: 7 additions & 2 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 @@ -42,7 +47,7 @@ export default function AddMoneyCryptoDirectPage() {
<DirectSuccessView
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 @@ -42,7 +42,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 @@ -390,8 +391,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
14 changes: 7 additions & 7 deletions src/components/Common/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ParsedURL } from '@/lib/url-parser/types/payment'
import { usePaymentStore } from '@/redux/hooks'
import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType'
import { GuestVerificationModal } from '../Global/GuestVerificationModal'
import ActionListDaimoPayButton from './ActionListDaimoPayButton'

export interface Method {
id: string
Expand All @@ -34,7 +35,7 @@ export interface Method {
soon: boolean
}

const ACTION_METHODS: Method[] = [
export const ACTION_METHODS: Method[] = [
{
id: 'bank',
title: 'Bank',
Expand Down Expand Up @@ -122,7 +123,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 +180,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 (method.id === 'exchange-or-wallet') {
return <ActionListDaimoPayButton key={method.id} />
}
return true
}).map((method) => {

return (
<MethodCard
onClick={() => handleMethodClick(method)}
Expand Down
150 changes: 150 additions & 0 deletions src/components/Common/ActionListDaimoPayButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React, { useCallback } from 'react'
import { SearchResultCard } from '../SearchUsers/SearchResultCard'
import { ACTION_METHODS } from './ActionList'
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'

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, setLoadingStep, initiateDaimoPayment, completeDaimoPayment } = usePaymentInitiator()

const handleInitiateDaimoPayment = useCallback(async () => {
if (!usdAmount || parseFloat(usdAmount) <= 0) {
console.error('Invalid amount entered')
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

setLoadingStep('Creating Charge')

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!,
}
: 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,
])

const handleCompleteDaimoPayment = useCallback(
async (daimoPaymentResponse: any) => {
dispatch(paymentActions.setIsDaimoPaymentProcessing(true))
if (chargeDetails) {
setLoadingStep('Confirming Transaction')
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)
}
setLoadingStep('Success')
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}
onClose={() => setLoadingStep('Idle')}
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
82 changes: 54 additions & 28 deletions src/components/Global/DaimoPayButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,26 @@ export interface DaimoPayButtonProps {
amount: string
/** The recipient address */
toAddress: string
/** Button text */
children: React.ReactNode
/** Button variant */
/**
* Render function that receives click handler and other props
* OR React node for backwards compatibility
*/
children:
| React.ReactNode
| ((props: { onClick: () => Promise<void>; disabled: boolean; loading: boolean }) => React.ReactElement | null)
/** Button variant (only used with default button) */
variant?: 'purple' | 'primary-soft' | 'stroke'
/** Button icon */
/** Button icon (only used with default button) */
icon?: IconName
/** Icon size */
/** Icon size (only used with default button) */
iconSize?: number
/** Whether the button is disabled */
disabled?: boolean
/** Whether the button is loading */
loading?: boolean
/** Additional button className */
/** Additional button className (only used with default button) */
className?: string
/** Shadow size for the button */
/** Shadow size for the button (only used with default button) */
shadowSize?: '4' | '6' | '8'
/** Callback when payment is completed */
onPaymentCompleted: (paymentResponse: any) => void
Expand Down Expand Up @@ -69,12 +74,12 @@ export const DaimoPayButton = ({

// Validate amount range if specified
if (minAmount !== undefined && formattedAmount < minAmount) {
onValidationError?.(`Minimum deposit is $${minAmount.toFixed(2)}.`)
onValidationError?.(`Minimum deposit using crypto is $${minAmount.toFixed(2)}.`)
return false
}

if (maxAmount !== undefined && formattedAmount > maxAmount) {
onValidationError?.(`Maximum deposit is $${maxAmount.toFixed(2)}.`)
onValidationError?.(`Maximum deposit using crypto is $${maxAmount.toFixed(2)}.`)
return false
}

Expand Down Expand Up @@ -113,25 +118,46 @@ export const DaimoPayButton = ({
closeOnSuccess
onClose={onClose}
>
{({ show }) => (
<Button
onClick={async () => {
const canShow = await handleClick()
if (canShow) {
show()
}
}}
variant={variant}
shadowSize={shadowSize}
disabled={disabled || amount.length === 0}
loading={loading}
className={className}
icon={icon}
iconSize={iconSize}
>
{children}
</Button>
)}
{({ show }) => {
const handleButtonClick = async () => {
const canShow = await handleClick()
if (canShow) {
show()
}
}

// If children is a function, call it with the necessary props
if (typeof children === 'function') {
const customElement = children({
onClick: handleButtonClick,
disabled: disabled || amount.length === 0,
loading,
})

// Ensure we return a valid React element
if (!customElement) {
return <div />
}

return customElement as React.ReactElement
}

// Otherwise, render the default Button (backwards compatibility)
return (
<Button
onClick={handleButtonClick}
variant={variant}
shadowSize={shadowSize}
disabled={disabled || amount.length === 0}
loading={loading}
className={className}
icon={icon}
iconSize={iconSize}
>
{children}
</Button>
)
}}
</DaimoPayButtonSDK.Custom>
)
}
Expand Down
Loading
Loading