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
7 changes: 0 additions & 7 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { twMerge } from 'tailwind-merge'
import { fetchTokenPrice } from '@/app/actions/tokens'
import { RequestFulfillmentBankFlowStep, useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext'
import ExternalWalletFulfilManager from '@/components/Request/views/ExternalWalletFulfilManager'
import ActionList from '@/components/Common/ActionList'
import NavHeader from '@/components/Global/NavHeader'
import { ReqFulfillBankFlowManager } from '@/components/Request/views/ReqFulfillBankFlowManager'
Expand Down Expand Up @@ -67,7 +66,6 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
const { isDrawerOpen, selectedTransaction, openTransactionDetails } = useTransactionDetailsDrawer()
const [isLinkCancelling, setisLinkCancelling] = useState(false)
const {
showExternalWalletFulfillMethods,
showRequestFulfilmentBankFlowManager,
setShowRequestFulfilmentBankFlowManager,
setFlowStep: setRequestFulfilmentBankFlowStep,
Expand Down Expand Up @@ -524,11 +522,6 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
)
}

// render external wallet fulfilment methods
if (showExternalWalletFulfillMethods) {
return <ExternalWalletFulfilManager parsedPaymentData={parsedPaymentData as ParsedURL} />
}

// render request fulfilment bank flow manager
if (showRequestFulfilmentBankFlowManager) {
return <ReqFulfillBankFlowManager parsedPaymentData={parsedPaymentData as ParsedURL} />
Expand Down
21 changes: 15 additions & 6 deletions src/components/Common/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import IconStack from '../Global/IconStack'
import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { type ClaimLinkData } from '@/services/sendLinks'
import { formatUnits } from 'viem'
import { useContext, useMemo, useState } from 'react'
import { useContext, useMemo, useState, useRef } from 'react'
import ActionModal from '@/components/Global/ActionModal'
import Divider from '../0_Bruddle/Divider'
import { Button } from '../0_Bruddle'
Expand Down Expand Up @@ -86,7 +86,6 @@ export default function ActionList({
const { addParamStep } = useClaimLink()
const {
setShowRequestFulfilmentBankFlowManager,
setShowExternalWalletFulfillMethods,
setFlowStep: setRequestFulfilmentBankFlowStep,
setFulfillUsingManteca,
setRegionalMethodType: setRequestFulfillmentRegionalMethodType,
Expand All @@ -109,6 +108,8 @@ export default function ActionList({
const { initiatePayment, loadingStep } = usePaymentInitiator()
const { isUserMantecaKycApproved } = useKycStatus()
const isPaymentInProgress = loadingStep !== 'Idle' && loadingStep !== 'Error' && loadingStep !== 'Success'
// ref to store daimo button click handler for triggering from balance modal
const daimoButtonClickRef = useRef<(() => void) | null>(null)

const dispatch = useAppDispatch()

Expand Down Expand Up @@ -255,9 +256,7 @@ export default function ActionList({
setRequestFulfillmentRegionalMethodType(method.id)
setFulfillUsingManteca(true)
break
case 'exchange-or-wallet':
setShowExternalWalletFulfillMethods(true)
break
// 'exchange-or-wallet' case removed - handled by ActionListDaimoPayButton
}
}
}
Expand Down Expand Up @@ -329,6 +328,7 @@ export default function ActionList({
return true // Proceed with Daimo
}}
isDisabled={!isAmountEntered}
clickHandlerRef={daimoButtonClickRef}
/>
</div>
)
Expand Down Expand Up @@ -427,7 +427,16 @@ export default function ActionList({
setIsUsePeanutBalanceModalShown(true)
// Proceed with the method the user originally selected
if (selectedPaymentMethod) {
handleMethodClick(selectedPaymentMethod, true) // true = bypass modal check
// for exchange-or-wallet, trigger daimo button after state updates
if (selectedPaymentMethod.id === 'exchange-or-wallet' && daimoButtonClickRef.current) {
// use setTimeout to ensure state updates are processed before triggering daimo
setTimeout(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not using a useeffect here cuz it will just add unnecessary complexity, setimeout a bit hacky but it makes sure state is updated properly

daimoButtonClickRef.current?.()
}, 0)
} else {
// for other methods, use handleMethodClick
handleMethodClick(selectedPaymentMethod, true) // true = bypass modal check
}
}
setSelectedPaymentMethod(null)
},
Expand Down
18 changes: 17 additions & 1 deletion src/components/Common/ActionListDaimoPayButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ interface ActionListDaimoPayButtonProps {
showConfirmModal: boolean
onBeforeShow?: () => boolean | Promise<boolean>
isDisabled?: boolean
clickHandlerRef?: React.MutableRefObject<(() => void) | null>
}

const ActionListDaimoPayButton = ({
handleContinueWithPeanut,
showConfirmModal,
onBeforeShow,
isDisabled,
clickHandlerRef,
}: ActionListDaimoPayButtonProps) => {
const dispatch = useAppDispatch()
const searchParams = useSearchParams()
Expand Down Expand Up @@ -112,10 +114,18 @@ const ActionListDaimoPayButton = ({
if (chargeDetails) {
dispatch(paymentActions.setIsDaimoPaymentProcessing(true))
try {
// validate and parse destination chain id with proper fallback
// use chargeDetails chainId if it's a valid non-negative integer, otherwise use daimo response
const parsedChainId = Number(chargeDetails.chainId)
const destinationChainId =
Number.isInteger(parsedChainId) && parsedChainId >= 0
? parsedChainId
: Number(daimoPaymentResponse.payment.destination.chainId)

const result = await completeDaimoPayment({
chargeDetails: chargeDetails,
txHash: daimoPaymentResponse.txHash as string,
destinationchainId: daimoPaymentResponse.payment.destination.chainId,
destinationchainId: destinationChainId,
payerAddress: peanutWalletAddress ?? daimoPaymentResponse.payment.source.payerAddress,
sourceChainId: daimoPaymentResponse.payment.source.chainId,
sourceTokenAddress: daimoPaymentResponse.payment.source.tokenAddress,
Expand Down Expand Up @@ -148,6 +158,8 @@ const ActionListDaimoPayButton = ({
<DaimoPayButton
amount={usdAmount ?? '0.10'}
toAddress={parsedPaymentData.recipient.resolvedAddress}
toChainId={parsedPaymentData.chain?.chainId ? Number(parsedPaymentData.chain.chainId) : undefined}
toTokenAddress={parsedPaymentData.token?.address}
onPaymentCompleted={handleCompleteDaimoPayment}
onBeforeShow={async () => {
// First check if parent wants to intercept (e.g. show balance modal)
Expand Down Expand Up @@ -178,6 +190,10 @@ const ActionListDaimoPayButton = ({
{({ onClick, loading }) => {
// Store the onClick function so we can trigger it from elsewhere
daimoPayButtonClickRef.current = onClick
// also store in parent ref if provided (for balance modal in ActionList)
if (clickHandlerRef) {
clickHandlerRef.current = onClick
}

return (
<ActionListCard
Expand Down
10 changes: 8 additions & 2 deletions src/components/Global/DaimoPayButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export interface DaimoPayButtonProps {
amount: string
/** The recipient address */
toAddress: string
/** Target chain ID (defaults to Arbitrum if not specified) */
toChainId?: number
/** Target token address (defaults to USDC on Arbitrum if not specified) */
toTokenAddress?: string
/**
* Render function that receives click handler and other props
* OR React node for backwards compatibility
Expand Down Expand Up @@ -51,6 +55,8 @@ export interface DaimoPayButtonProps {
export const DaimoPayButton = ({
amount,
toAddress,
toChainId,
toTokenAddress,
children,
variant = 'purple',
icon,
Expand Down Expand Up @@ -139,10 +145,10 @@ export const DaimoPayButton = ({
resetOnSuccess // resets the daimo payment state after payment is successfully completed
appId={daimoAppId}
intent="Deposit"
toChain={arbitrum.id}
toChain={toChainId ?? arbitrum.id} // use provided chain or default to arbitrum
toUnits={amount.replace(/,/g, '')}
toAddress={getAddress(toAddress)}
toToken={getAddress(PEANUT_WALLET_TOKEN)} // USDC on arbitrum
toToken={getAddress(toTokenAddress ?? PEANUT_WALLET_TOKEN)} // use provided token or default to usdc on arbitrum
onPaymentCompleted={onPaymentCompleted}
closeOnSuccess
onClose={onClose}
Expand Down
106 changes: 64 additions & 42 deletions src/components/Payment/PaymentForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import FileUploadInput from '@/components/Global/FileUploadInput'
import { type IconName } from '@/components/Global/Icons/Icon'
import NavHeader from '@/components/Global/NavHeader'
import TokenAmountInput from '@/components/Global/TokenAmountInput'
import TokenSelector from '@/components/Global/TokenSelector/TokenSelector'
import UserCard from '@/components/User/UserCard'
import { PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants'
import { PEANUT_WALLET_TOKEN, PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_CHAIN } from '@/constants'
import { tokenSelectorContext } from '@/context'
import { useAuth } from '@/context/authContext'
import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext'
Expand Down Expand Up @@ -86,14 +87,8 @@ export const PaymentForm = ({
attachmentOptions,
currentView,
} = usePaymentStore()
const {
setShowExternalWalletFulfillMethods,
setExternalWalletFulfillMethod,
fulfillUsingManteca,
setFulfillUsingManteca,
triggerPayWithPeanut,
setTriggerPayWithPeanut,
} = useRequestFulfillmentFlow()
const { fulfillUsingManteca, setFulfillUsingManteca, triggerPayWithPeanut, setTriggerPayWithPeanut } =
useRequestFulfillmentFlow()
const recipientUsername = !chargeDetails && recipient?.recipientType === 'USERNAME' ? recipient.identifier : null
const { user: recipientUser } = useUserByUsername(recipientUsername)

Expand Down Expand Up @@ -182,6 +177,9 @@ export const PaymentForm = ({
setInputTokenAmount(amount)
}

// for ADDRESS/ENS recipients, initialize token/chain from URL or defaults
const isExternalRecipient = recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS'

if (chain) {
setSelectedChainID((chain.chainId || requestDetails?.chainId) ?? '')
if (!token && !requestDetails?.tokenAddress) {
Expand All @@ -191,15 +189,37 @@ export const PaymentForm = ({
// Note: decimals automatically derived by useTokenPrice hook
}
}
} else if (isExternalRecipient && !selectedChainID) {
// default to arbitrum for external recipients if no chain specified
setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString())
}

if (token) {
setSelectedTokenAddress((token.address || requestDetails?.tokenAddress) ?? '')
// Note: decimals automatically derived by useTokenPrice hook
} else if (isExternalRecipient && !selectedTokenAddress && selectedChainID) {
// default to USDC for external recipients if no token specified
const chainData = supportedSquidChainsAndTokens[selectedChainID]
const defaultToken = chainData?.tokens.find((t) => t.symbol.toLowerCase() === 'usdc')
if (defaultToken) {
setSelectedTokenAddress(defaultToken.address)
}
}

setInitialSetupDone(true)
}, [chain, token, amount, initialSetupDone, requestDetails, showRequestPotInitialView, isRequestPotLink])
}, [
chain,
token,
amount,
initialSetupDone,
requestDetails,
showRequestPotInitialView,
isRequestPotLink,
recipient?.recipientType,
selectedChainID,
selectedTokenAddress,
supportedSquidChainsAndTokens,
])

// reset error when component mounts or recipient changes
useEffect(() => {
Expand Down Expand Up @@ -244,12 +264,14 @@ export const PaymentForm = ({
}
} else {
// regular send/pay
const isExternalRecipient = recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS'

if (
!showRequestPotInitialView && // don't apply balance check on request pot payment initial view
isActivePeanutWallet &&
areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)
(areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) || !isExternalRecipient)
) {
// peanut wallet payment
// peanut wallet payment (for USERNAME or default token)
const walletNumeric = parseFloat(String(peanutWalletBalance).replace(/,/g, ''))
if (walletNumeric < parsedInputAmount) {
dispatch(paymentActions.setError('Insufficient balance'))
Expand All @@ -274,6 +296,9 @@ export const PaymentForm = ({
} else {
dispatch(paymentActions.setError(null))
}
} else if (isExternalRecipient && isActivePeanutWallet) {
// for external recipients with peanut wallet, balance will be checked via cross-chain route
dispatch(paymentActions.setError(null))
} else {
dispatch(paymentActions.setError(null))
}
Expand Down Expand Up @@ -304,6 +329,7 @@ export const PaymentForm = ({
currentView,
isProcessing,
hasPendingTransactions,
recipient?.recipientType,
])

// Calculate USD value when requested token price is available
Expand Down Expand Up @@ -339,7 +365,12 @@ export const PaymentForm = ({
(!!inputTokenAmount && parseFloat(inputTokenAmount) > 0) || (!!usdValue && parseFloat(usdValue) > 0)
}

const tokenSelected = !!selectedTokenAddress && !!selectedChainID
const isExternalRecipient = recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS'
// for external recipients, token selection is required
// for USERNAME recipients, token is always PEANUT_WALLET_TOKEN
const tokenSelected = isExternalRecipient
? !!selectedTokenAddress && !!selectedChainID
: !!selectedTokenAddress && !!selectedChainID
const recipientExists = !!recipient
const walletConnected = isConnected

Expand Down Expand Up @@ -678,11 +709,7 @@ export const PaymentForm = ({
}, [recipient])

const handleGoBack = () => {
if (isExternalWalletFlow) {
setShowExternalWalletFulfillMethods(true)
setExternalWalletFulfillMethod(null)
return
} else if (window.history.length > 1) {
if (window.history.length > 1) {
router.back()
} else {
router.push('/')
Expand Down Expand Up @@ -809,30 +836,25 @@ export const PaymentForm = ({
defaultSliderSuggestedAmount={defaultSliderValue.suggestedAmount}
/>

{/*
Url request flow (peanut.me/<address>)
If we are paying from peanut wallet we only need to
select a token if it's not included in the url
From other wallets we always need to select a token
*/}
{/* we dont need this as daimo will handle token selection */}
{/* {!(chain && isPeanutWalletConnected) && isConnected && !isAddMoneyFlow && (
<div className="space-y-2">
{!isPeanutWalletUSDC && !selectedTokenAddress && !selectedChainID && (
<div className="text-sm font-bold">Select token and chain to receive</div>
)}
<TokenSelector viewType="req_pay" />
{!isPeanutWalletUSDC && selectedTokenAddress && selectedChainID && (
<div className="pt-1 text-center text-xs text-grey-1">
<span>Use USDC on Arbitrum for free transactions!</span>
</div>
)}
</div>
)} */}

{/* {isExternalWalletConnected && isAddMoneyFlow && (
<TokenSelector viewType="add" disabled={!isExternalWalletConnected && isAddMoneyFlow} />
)} */}
{/* Token selector for external ADDRESS/ENS recipients */}
{!isExternalWalletFlow &&
!showRequestPotInitialView &&
(recipient?.recipientType === 'ADDRESS' || recipient?.recipientType === 'ENS') &&
isConnected && (
<div className="space-y-2">
<TokenSelector viewType="req_pay" />
{selectedTokenAddress &&
selectedChainID &&
!(
areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) &&
selectedChainID === PEANUT_WALLET_CHAIN.id.toString()
) && (
<div className="pt-1 text-center text-xs text-grey-1">
<span>Use USDC on Arbitrum for free transactions!</span>
</div>
)}
</div>
)}

{isDirectUsdPayment && (
<FileUploadInput
Expand Down
Loading
Loading