diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 3d6175b6f..e9ff34ba2 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -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' @@ -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 @@ -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')) @@ -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() @@ -393,7 +397,6 @@ export const PaymentForm = ({ extra: { flow: 'external_wallet_payment' }, }) } - return } // skip this step for request pots initial view @@ -521,7 +524,10 @@ export const PaymentForm = ({ return 'Send' } - // Check insufficient balance BEFORE other conditions + if (showRequestPotInitialView) { + return 'Pay' + } + if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { return (
@@ -534,10 +540,6 @@ export const PaymentForm = ({ ) } - if (showRequestPotInitialView) { - return 'Pay' - } - if (isActivePeanutWallet) { return (
@@ -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 diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts index 16570630e..b2646ff8a 100644 --- a/src/hooks/usePaymentInitiator.ts +++ b/src/hooks/usePaymentInitiator.ts @@ -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' @@ -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', @@ -84,7 +82,6 @@ export const usePaymentInitiator = () => { const router = useRouter() const config = useConfig() const { chain: connectedWalletChain } = useWagmiAccount() - const queryClient = useQueryClient() const [slippagePercentage, setSlippagePercentage] = useState(undefined) const [unsignedTx, setUnsignedTx] = useState(null) @@ -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( @@ -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 }