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
33 changes: 18 additions & 15 deletions src/components/Payment/PaymentForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@ import { useAppDispatch, usePaymentStore } from '@/redux/hooks'
import { paymentActions } from '@/redux/slices/payment-slice'
import { walletActions } from '@/redux/slices/wallet-slice'
import { areEvmAddressesEqual, ErrorHandler, formatAmount, formatCurrency, getContributorsFromCharge } from '@/utils'
import { initializeAppKit } from '@/config/wagmi.config'
import { useAppKit, useDisconnect } from '@reown/appkit/react'
import * as Sentry from '@sentry/nextjs'
import { initializeAppKit } from '@/config/wagmi.config'
import Image from 'next/image'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
Expand All @@ -41,6 +40,7 @@ import { invitesApi } from '@/services/invites'
import { EInviteType } from '@/services/services.types'
import ContributorCard from '@/components/Global/Contributors/ContributorCard'
import { getCardPosition } from '@/components/Global/Card'
import * as Sentry from '@sentry/nextjs'

export type PaymentFlowProps = {
isExternalWalletFlow?: boolean
Expand Down Expand Up @@ -237,8 +237,12 @@ export const PaymentForm = ({
}
} else {
// regular send/pay
if (isActivePeanutWallet && areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)) {
// peanut wallet payment - ALWAYS check balance (including request pots)
if (
!showRequestPotInitialView && // don't apply balance check on request pot payment initial view
isActivePeanutWallet &&
areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)
) {
// peanut wallet payment
const walletNumeric = parseFloat(String(peanutWalletBalance).replace(/,/g, ''))
if (walletNumeric < parsedInputAmount) {
dispatch(paymentActions.setError('Insufficient balance'))
Expand Down Expand Up @@ -370,8 +374,8 @@ export const PaymentForm = ({
if (inviteError) {
setInviteError(false)
}
// Handle insufficient balance - redirect to add money
if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) {
// Invites will be handled in the payment page, skip this step for request pots initial view
if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) {
// If the user doesn't have app access, accept the invite before claiming the link
if (recipient.recipientType === 'USERNAME' && !user?.user.hasAppAccess) {
const isAccepted = await handleAcceptInvite()
Expand All @@ -393,7 +397,6 @@ export const PaymentForm = ({
extra: { flow: 'external_wallet_payment' },
})
}
return
}

// skip this step for request pots initial view
Expand Down Expand Up @@ -521,7 +524,10 @@ export const PaymentForm = ({
return 'Send'
}

// Check insufficient balance BEFORE other conditions
if (showRequestPotInitialView) {
return 'Pay'
}

if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) {
return (
<div className="flex items-center gap-1">
Expand All @@ -534,10 +540,6 @@ export const PaymentForm = ({
)
}

if (showRequestPotInitialView) {
return 'Pay'
}

if (isActivePeanutWallet) {
return (
<div className="flex items-center gap-1">
Expand All @@ -554,11 +556,12 @@ export const PaymentForm = ({
}

const getButtonIcon = (): IconName | undefined => {
if (!isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline'
if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline'

if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) return 'arrow-down'
if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow)
return 'arrow-down'

if (!isProcessing && isActivePeanutWallet && !isExternalWalletFlow && !showRequestPotInitialView)
if (!showRequestPotInitialView && !isProcessing && isActivePeanutWallet && !isExternalWalletFlow)
return 'arrow-up-right'

return undefined
Expand Down
45 changes: 13 additions & 32 deletions src/hooks/usePaymentInitiator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants'
import { BALANCE_DECREASE, INITIATE_PAYMENT } from '@/constants/query.consts'
import { tokenSelectorContext } from '@/context'
import { useWallet } from '@/hooks/wallet/useWallet'
import { type ParsedURL } from '@/lib/url-parser/types/payment'
Expand Down Expand Up @@ -27,7 +26,6 @@ import { getRoute, type PeanutCrossChainRoute } from '@/services/swap'
import { estimateTransactionCostUsd } from '@/app/actions/tokens'
import { captureException } from '@sentry/nextjs'
import { useRouter } from 'next/navigation'
import { useQueryClient } from '@tanstack/react-query'

enum ELoadingStep {
IDLE = 'Idle',
Expand Down Expand Up @@ -84,7 +82,6 @@ export const usePaymentInitiator = () => {
const router = useRouter()
const config = useConfig()
const { chain: connectedWalletChain } = useWagmiAccount()
const queryClient = useQueryClient()

const [slippagePercentage, setSlippagePercentage] = useState<number | undefined>(undefined)
const [unsignedTx, setUnsignedTx] = useState<peanutInterfaces.IPeanutUnsignedTransaction | null>(null)
Expand Down Expand Up @@ -605,22 +602,12 @@ export const usePaymentInitiator = () => {
]
)

// @dev Architecture Note: initiatePayment flow and mutation tracking
//
// Current: This async function uses state-based lifecycle (isProcessing, loadingStep)
// rather than TanStack Query mutations. This is INTENTIONAL because:
// 1. It has two phases: charge preparation (no balance change) + payment execution (balance decrease)
// 2. Only the payment execution phase triggers balance-decreasing mutations (via sendMoney/sendTransactions)
// 3. sendMoney already properly wraps mutations with mutationKey: [BALANCE_DECREASE, SEND_MONEY]
// 4. usePendingTransactions tracks ALL balance-decreasing operations globally
//
// Bug fix (2025-10): Added guard at line 648 to prevent premature payment execution when
// fetching existing charges. Previously, fetching an existing charge (chargeCreated=false)
// without skipChargeCreation=true would fall through and trigger sendMoney() prematurely,
// causing optimistic balance updates before user confirmed payment.
//
// Future consideration: Could wrap entire initiatePayment in useMutation, but complexity is HIGH
// due to two-phase flow, Redux integration, and multiple return points. Current architecture works.
// @dev TODO: Refactor to TanStack Query mutation for architectural consistency
// Current: This async function works correctly (protected by isProcessing state)
// but is NOT tracked by usePendingTransactions mutation system.
// Future improvement: Wrap in useMutation for consistency with other balance-decreasing ops.
// mutationKey: [BALANCE_DECREASE, INITIATE_PAYMENT]
// Complexity: HIGH - complex state/Redux integration. Low priority.
//
// initiate and process payments
const initiatePayment = useCallback(
Expand All @@ -641,25 +628,19 @@ export const usePaymentInitiator = () => {
console.log('Proceeding with charge details:', determinedChargeDetails.uuid)

// 2. handle charge state
// Return early if:
// a) Explicitly told to return after charge creation (request pot initial view)
// b) Charge was just created AND needs special handling (external wallet/cross-chain)
// c) Fetching existing charge WITHOUT explicit skipChargeCreation (not from CONFIRM view)
const shouldReturnAfterCharge =
payload.returnAfterChargeCreation ||
if (
payload.returnAfterChargeCreation || // For request pot payment, return after charge creation
(chargeCreated &&
(payload.isExternalWalletFlow ||
!isPeanutWallet ||
(isPeanutWallet &&
(!areEvmAddressesEqual(determinedChargeDetails.tokenAddress, PEANUT_WALLET_TOKEN) ||
determinedChargeDetails.chainId !== PEANUT_WALLET_CHAIN.id.toString())))) ||
// NEW: If charge exists (not created) and we're NOT explicitly skipping charge creation,
// then we're in "prepare" mode and shouldn't execute payment yet
(!chargeCreated && payload.chargeId && !payload.skipChargeCreation)

if (shouldReturnAfterCharge) {
determinedChargeDetails.chainId !== PEANUT_WALLET_CHAIN.id.toString()))))
) {
console.log(
`Charge ready. Returning without payment execution. (chargeCreated: ${chargeCreated}, skipChargeCreation: ${payload.skipChargeCreation})`
`Charge created. Transitioning to Confirm view for: ${
payload.isExternalWalletFlow ? 'Add Money Flow' : 'External Wallet'
}.`
)
setLoadingStep('Charge Created')
return { status: 'Charge Created', charge: determinedChargeDetails, success: false }
Expand Down
Loading