diff --git a/docs/fixes/request-pot-balance-and-navigation-fixes.md b/docs/fixes/request-pot-balance-and-navigation-fixes.md new file mode 100644 index 000000000..308373d03 --- /dev/null +++ b/docs/fixes/request-pot-balance-and-navigation-fixes.md @@ -0,0 +1,333 @@ +# Request Pot Balance Check & Navigation Fixes + +**Date:** October 29, 2025 +**Issues:** Balance check skipped, back button broken, payment methods reshuffling +**Status:** βœ… Fixed + +--- + +## πŸ› Issue #1: Balance Check Was Skipped for Request Pot Payments + +### Problem + +User with $1.00 balance could proceed to pay $25 request without being prompted to add funds. + +### Root Cause + +The previous developer added this comment: + +```typescript +!showRequestPotInitialView && // don't apply balance check on request pot payment initial view +``` + +**Why they added it:** They thought the initial view (where `requestId` is in URL) shouldn't validate balance because the user hasn't selected a payment method yet. They may have intended to validate only after the user clicks "Pay with Peanut." + +**Why it was wrong:** The balance check should ALWAYS run if the user is using Peanut Wallet, regardless of whether they're on the initial view or confirmation view. The button should show "Add funds" if balance is insufficient. + +### The Flow + +``` +requestId in URL β†’ showRequestPotInitialView = true β†’ Initial payment view + ↓ +User clicks "Pay with Peanut" + ↓ +Charge created β†’ URL changes to chargeId β†’ showRequestPotInitialView = false + ↓ +Confirmation view +``` + +### Fix Applied + +**Removed the `!showRequestPotInitialView` guard** from THREE places in `PaymentForm/index.tsx`: + +#### 1. Balance Check Logic (Line 239) + +```typescript +// ❌ BEFORE +if ( + !showRequestPotInitialView && // πŸ‘ˆ WRONG - skips balance check! + isActivePeanutWallet && + areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) +) { + // Check balance... +} + +// βœ… AFTER +if (isActivePeanutWallet && areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)) { + // peanut wallet payment - ALWAYS check balance (including request pots) + if (walletNumeric < parsedInputAmount) { + dispatch(paymentActions.setError('Insufficient balance')) + } +} +``` + +#### 2. Add Money Redirect (Line 375) + +```typescript +// ❌ BEFORE +if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError) { + router.push('/add-money') + return +} + +// βœ… AFTER +if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { + router.push('/add-money') + return +} +``` + +#### 3. Button Text Priority (Line 516) + +```typescript +// ❌ BEFORE - Wrong order +if (showRequestPotInitialView) { + return 'Pay' +} + +if (isActivePeanutWallet && isInsufficientBalanceError) { + return 'Add funds to πŸ₯œ PEANUT' +} + +// βœ… AFTER - Check balance first +if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { + return 'Add funds to πŸ₯œ PEANUT' +} + +if (showRequestPotInitialView) { + return 'Pay' +} +``` + +### Result + +βœ… User with $1.00 trying to pay $25 now sees: + +- Error message: "Insufficient balance" +- Button text: "Add funds to πŸ₯œ PEANUT" +- Button icon: `arrow-down` +- Clicking button β†’ Redirects to `/add-money` + +--- + +## πŸ› Issue #2: Payment Methods Reshuffling After Modal Close + +### Problem + +When user clicks a payment method (e.g., Mercado Pago), the modal shows. When they dismiss the modal, the payment methods reshuffle their order. + +### Root Cause + +The `isMethodUnavailable` callback was **recreated on every render**, causing `useGeoFilteredPaymentOptions` to think dependencies changed and re-sort the array. + +```typescript +// ❌ BEFORE - New function reference every render +useGeoFilteredPaymentOptions({ + isMethodUnavailable: (method) => method.soon || (method.id === 'bank' && requiresVerification), +}) +``` + +**How it caused reshuffling:** + +1. User clicks method β†’ Modal opens β†’ State changes +2. Component re-renders +3. Inline arrow function recreated β†’ **New function reference** +4. `useMemo` in `useGeoFilteredPaymentOptions` sees dependency change +5. Calls `Array.sort()` again +6. JavaScript's `Array.sort()` is unstable β†’ Methods shuffle! + +### Fix Applied + +**Wrapped callback in `useCallback`** to stabilize the reference: + +```typescript +// βœ… AFTER - Stable function reference +const isMethodUnavailable = useCallback( + (method: PaymentMethod) => method.soon || (method.id === 'bank' && requiresVerification), + [requiresVerification] // Only recreate if this actually changes +) + +useGeoFilteredPaymentOptions({ + sortUnavailable: true, + isMethodUnavailable, // Stable reference +}) +``` + +**File:** `src/components/Common/ActionList.tsx` + +### Result + +βœ… Payment methods stay in consistent order +βœ… Modal open/close doesn't trigger re-sort +βœ… Methods only re-sort when `requiresVerification` actually changes + +--- + +## πŸ› Issue #3: Add Money Back Button Goes to Home + +### Problem + +When user navigates from request payment β†’ add money, clicking back button goes to `/home` instead of back to the request payment page. + +### Root Cause + +Hardcoded navigation in `add-money/page.tsx`: + +```typescript +// ❌ BEFORE - Always goes to home +onBackClick={() => router.push('/home')} +``` + +This completely ignores the browser's navigation history, breaking the natural back button behavior. + +### Fix Applied + +**Use `router.back()` with fallback:** + +```typescript +// βœ… AFTER - Respects browser history +const handleBack = () => { + // Check if there's a previous page in history + if (window.history.length > 1) { + router.back() + } else { + // Fallback to home if no history (e.g., direct navigation to /add-money) + router.push('/home') + } +} + +return ( + +) +``` + +**File:** `src/app/(mobile-ui)/add-money/page.tsx` + +### Result + +βœ… User flow: Request payment β†’ Add money β†’ **Back** β†’ Request payment +βœ… Direct navigation: `/add-money` β†’ **Back** β†’ Home (fallback) +βœ… Natural browser history behavior restored + +--- + +## πŸ” Issue #4: Bug Button in Add Money View + +### Investigation + +Searched for bug/support/feedback buttons in add-money flow: + +- ❌ No bug button found in `AddWithdrawRouterView` +- ❌ No bug button in `add-money/page.tsx` +- ❌ No bug button in add-money country/method pages + +**Possible locations where support buttons exist:** + +1. `TransactionDetailsReceipt` - Has "Issues with this transaction?" button +2. `ValidationErrorView` - Has "Talk to support" button +3. `ClaimErrorView` - Has "Talk to support" button +4. Global Crisp Chat widget (bottom right) + +**Question for user:** Where specifically is this bug button? Is it: + +- The Crisp chat widget? +- A "Talk to support" button on an error screen? +- A different button? + +**Need more details to investigate this issue.** + +--- + +## πŸ“‹ Testing Checklist + +### Balance Check + +- [ ] User with $1.00 trying to pay $25 sees "Insufficient balance" +- [ ] Button shows "Add funds to πŸ₯œ PEANUT" +- [ ] Clicking button redirects to `/add-money` +- [ ] After adding funds, payment proceeds normally + +### Method Reshuffling + +- [ ] Open request payment page +- [ ] Click "Mercado Pago" β†’ Modal appears +- [ ] Close modal +- [ ] **Verify methods DON'T reshuffle** +- [ ] Try with Bank, PIX, External wallet +- [ ] All methods should stay in consistent order + +### Back Button + +- [ ] Navigate: Request payment β†’ Add money +- [ ] Click back button +- [ ] **Should return to request payment page** +- [ ] Not to home page + +### Edge Cases + +- [ ] Direct navigation to `/add-money` β†’ Back β†’ Goes to home (fallback) +- [ ] User with sufficient balance doesn't see "Add funds" button +- [ ] Balance check works for both initial view and confirmation view + +--- + +## 🎯 Summary of Changes + +| File | Change | Reason | +| ----------------------- | ------------------------------------------------------ | ----------------------------------------------- | +| `PaymentForm/index.tsx` | Removed `!showRequestPotInitialView` guards (3 places) | Always check balance for Peanut wallet payments | +| `ActionList.tsx` | Wrapped `isMethodUnavailable` in `useCallback` | Prevent function reference instability | +| `add-money/page.tsx` | Changed `router.push('/home')` to `router.back()` | Respect browser history | + +--- + +## 🚨 Breaking Changes + +**None.** All changes are fixes that restore expected behavior: + +- Balance checks that should have been running +- Natural browser navigation that was broken +- Stable sorting that was unstable + +--- + +## πŸ’‘ Key Learnings + +### 1. Always Question Comments + +The comment "don't apply balance check on request pot payment initial view" seemed reasonable but was based on a flawed assumption. **Always validate the reasoning behind defensive code.** + +### 2. Function Reference Stability Matters + +When passing callbacks to hooks with memoization, unstable references cause unnecessary re-renders and re-computations. Use `useCallback` when: + +- Passing functions to child components (prevents re-renders) +- Passing functions as dependencies to `useMemo`/`useEffect` +- Passing functions to custom hooks that memoize based on them + +### 3. Respect Browser History + +Hardcoded navigation (`router.push('/home')`) breaks user expectations. Use `router.back()` unless there's a specific UX reason not to. + +### 4. Check Balance Early and Often + +For payment flows, validate balance: + +- βœ… On initial screen load +- βœ… When user changes amount +- βœ… Before showing payment methods +- βœ… Before submitting payment + +Don't wait until the last momentβ€”give users early feedback! + +--- + +## πŸ“š Related Files + +- `src/components/Payment/PaymentForm/index.tsx` - Main payment form with balance checks +- `src/components/Common/ActionList.tsx` - Payment method selection with modal +- `src/app/(mobile-ui)/add-money/page.tsx` - Add money entry point +- `src/hooks/useGeoFilteredPaymentOptions.ts` - Payment method filtering/sorting +- `src/app/[...recipient]/client.tsx` - Request payment page that sets `showRequestPotInitialView` diff --git a/src/app/(mobile-ui)/add-money/page.tsx b/src/app/(mobile-ui)/add-money/page.tsx index 0b0e6a2df..2c826b9b9 100644 --- a/src/app/(mobile-ui)/add-money/page.tsx +++ b/src/app/(mobile-ui)/add-money/page.tsx @@ -4,6 +4,7 @@ import { AddWithdrawRouterView } from '@/components/AddWithdraw/AddWithdrawRoute import { useOnrampFlow } from '@/context' import { useRouter } from 'next/navigation' import { useEffect } from 'react' +import { checkIfInternalNavigation } from '@/utils' export default function AddMoneyPage() { const router = useRouter() @@ -13,12 +14,23 @@ export default function AddMoneyPage() { resetOnrampFlow() }, []) + const handleBack = () => { + // Check if the referrer is from the same domain (internal navigation) + const isInternalReferrer = checkIfInternalNavigation() + + if (isInternalReferrer && window.history.length > 1) { + router.back() + } else { + router.push('/home') + } + } + return ( router.push('/home')} + onBackClick={handleBack} /> ) } diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index 5cccba256..a9e622352 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -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 { useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import ActionModal from '@/components/Global/ActionModal' import Divider from '../0_Bruddle/Divider' import { Button } from '../0_Bruddle' @@ -87,6 +87,7 @@ export default function ActionList({ const { user } = useAuth() const [isUsePeanutBalanceModalShown, setIsUsePeanutBalanceModalShown] = useState(false) const [showUsePeanutBalanceModal, setShowUsePeanutBalanceModal] = useState(false) + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null) const dispatch = useAppDispatch() @@ -100,17 +101,32 @@ export default function ActionList({ return false }, [claimType, requestType, flow]) + // Memoize the callback to prevent unnecessary re-sorts + const isMethodUnavailable = useCallback( + (method: PaymentMethod) => method.soon || (method.id === 'bank' && requiresVerification), + [requiresVerification] + ) + // use the hook to filter and sort payment methods based on geolocation const { filteredMethods: sortedActionMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({ sortUnavailable: true, - isMethodUnavailable: (method) => method.soon || (method.id === 'bank' && requiresVerification), + isMethodUnavailable, }) // Check if user has enough Peanut balance to pay for the request const amountInUsd = usdAmount ? parseFloat(usdAmount) : 0 const hasSufficientPeanutBalance = user && balance && Number(balance) >= amountInUsd - const handleMethodClick = async (method: PaymentMethod) => { + const handleMethodClick = async (method: PaymentMethod, bypassBalanceModal = false) => { + // For request flow: Check if user has sufficient Peanut balance and hasn't dismissed the modal + if (flow === 'request' && requestLinkData && !bypassBalanceModal) { + if (!isUsePeanutBalanceModalShown && hasSufficientPeanutBalance) { + setSelectedPaymentMethod(method) // Store the method they want to use + setShowUsePeanutBalanceModal(true) + return // Show modal, don't proceed with method yet + } + } + if (flow === 'claim' && claimLinkData) { const amountInUsd = parseFloat(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) if (method.id === 'bank' && amountInUsd < 5) { @@ -152,10 +168,6 @@ export default function ActionList({ return } - if (!isUsePeanutBalanceModalShown && hasSufficientPeanutBalance) { - setShowUsePeanutBalanceModal(true) - return - } switch (method.id) { case 'bank': if (requestType === BankRequestType.GuestKycNeeded) { @@ -235,23 +247,21 @@ export default function ActionList({
{sortedActionMethods.map((method) => { if (flow === 'request' && method.id === 'exchange-or-wallet') { - const shouldShowPeanutBalanceModal = !isUsePeanutBalanceModalShown && hasSufficientPeanutBalance return ( -
{ - if (shouldShowPeanutBalanceModal) { - setShowUsePeanutBalanceModal(true) - } - }} - > - {/* Disable daimo pay button if peanut balance is enough to pay for the request */} -
- -
+
+ { + // Check balance before showing Daimo widget + if (!isUsePeanutBalanceModalShown && hasSufficientPeanutBalance) { + setSelectedPaymentMethod(method) + setShowUsePeanutBalanceModal(true) + return false // Don't show Daimo yet + } + return true // Proceed with Daimo + }} + />
) } @@ -316,6 +326,7 @@ export default function ActionList({ onClose={() => { setShowUsePeanutBalanceModal(false) setIsUsePeanutBalanceModalShown(true) + setSelectedPaymentMethod(null) }} title="Use your Peanut balance instead" description={ @@ -328,9 +339,25 @@ export default function ActionList({ shadowSize: '4', onClick: () => { setShowUsePeanutBalanceModal(false) + setIsUsePeanutBalanceModalShown(true) + setSelectedPaymentMethod(null) setTriggerPayWithPeanut(true) }, }, + { + text: 'Continue', + shadowSize: '4', + variant: 'stroke', + onClick: () => { + setShowUsePeanutBalanceModal(false) + setIsUsePeanutBalanceModalShown(true) + // Proceed with the method the user originally selected + if (selectedPaymentMethod) { + handleMethodClick(selectedPaymentMethod, true) // true = bypass modal check + } + setSelectedPaymentMethod(null) + }, + }, ]} iconContainerClassName="bg-primary-1" preventClose={false} diff --git a/src/components/Common/ActionListDaimoPayButton.tsx b/src/components/Common/ActionListDaimoPayButton.tsx index 347c95a95..29b966e67 100644 --- a/src/components/Common/ActionListDaimoPayButton.tsx +++ b/src/components/Common/ActionListDaimoPayButton.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState, useRef } from 'react' +import { useCallback, useState, useRef, useEffect } from 'react' import IconStack from '../Global/IconStack' import { useAppDispatch, usePaymentStore } from '@/redux/hooks' import { paymentActions } from '@/redux/slices/payment-slice' @@ -14,9 +14,14 @@ import { ActionListCard } from '../ActionListCard' interface ActionListDaimoPayButtonProps { handleContinueWithPeanut: () => void showConfirmModal: boolean + onBeforeShow?: () => boolean | Promise } -const ActionListDaimoPayButton = ({ handleContinueWithPeanut, showConfirmModal }: ActionListDaimoPayButtonProps) => { +const ActionListDaimoPayButton = ({ + handleContinueWithPeanut, + showConfirmModal, + onBeforeShow, +}: ActionListDaimoPayButtonProps) => { const dispatch = useAppDispatch() const searchParams = useSearchParams() const method = ACTION_METHODS.find((method) => method.id === 'exchange-or-wallet') @@ -130,7 +135,7 @@ const ActionListDaimoPayButton = ({ handleContinueWithPeanut, showConfirmModal } } } }, - [chargeDetails, completeDaimoPayment, dispatch] + [chargeDetails, completeDaimoPayment, dispatch, peanutWalletAddress] ) if (!method || !parsedPaymentData) return null @@ -142,11 +147,21 @@ const ActionListDaimoPayButton = ({ handleContinueWithPeanut, showConfirmModal } toAddress={parsedPaymentData.recipient.resolvedAddress} onPaymentCompleted={handleCompleteDaimoPayment} onBeforeShow={async () => { + // First check if parent wants to intercept (e.g. show balance modal) + if (onBeforeShow) { + const shouldProceed = await onBeforeShow() + if (!shouldProceed) { + return false + } + } + + // Then check invite modal if (!confirmLoseInvite && showConfirmModal) { setShowInviteModal(true) return false } - // Don't reset confirmLoseInvite here - let it be reset only when modal is closed or payment is initiated + + // Finally initiate payment return await handleInitiateDaimoPayment() }} disabled={!usdAmount} diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 49061d39c..a70edd939 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -235,12 +235,8 @@ export const PaymentForm = ({ } } else { // regular send/pay - if ( - !showRequestPotInitialView && // don't apply balance check on request pot payment initial view - isActivePeanutWallet && - areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN) - ) { - // peanut wallet payment + if (isActivePeanutWallet && areEvmAddressesEqual(selectedTokenAddress, PEANUT_WALLET_TOKEN)) { + // peanut wallet payment - ALWAYS check balance (including request pots) const walletNumeric = parseFloat(String(peanutWalletBalance).replace(/,/g, '')) if (walletNumeric < parsedInputAmount) { dispatch(paymentActions.setError('Insufficient balance')) @@ -372,8 +368,8 @@ export const PaymentForm = ({ if (inviteError) { setInviteError(false) } - // Invites will be handled in the payment page, skip this step for request pots initial view - if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { + // Handle insufficient balance - redirect to add money + if (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() @@ -514,10 +510,7 @@ export const PaymentForm = ({ return 'Send' } - if (showRequestPotInitialView) { - return 'Pay' - } - + // Check insufficient balance BEFORE other conditions if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) { return (
@@ -530,6 +523,10 @@ export const PaymentForm = ({ ) } + if (showRequestPotInitialView) { + return 'Pay' + } + if (isActivePeanutWallet) { return (
@@ -546,12 +543,11 @@ export const PaymentForm = ({ } const getButtonIcon = (): IconName | undefined => { - if (!showRequestPotInitialView && !isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline' + if (!isExternalWalletConnected && isExternalWalletFlow) return 'wallet-outline' - if (!showRequestPotInitialView && isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) - return 'arrow-down' + if (isActivePeanutWallet && isInsufficientBalanceError && !isExternalWalletFlow) return 'arrow-down' - if (!showRequestPotInitialView && !isProcessing && isActivePeanutWallet && !isExternalWalletFlow) + if (!isProcessing && isActivePeanutWallet && !isExternalWalletFlow && !showRequestPotInitialView) return 'arrow-up-right' return undefined diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts index b2646ff8a..16570630e 100644 --- a/src/hooks/usePaymentInitiator.ts +++ b/src/hooks/usePaymentInitiator.ts @@ -1,4 +1,5 @@ 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' @@ -26,6 +27,7 @@ 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', @@ -82,6 +84,7 @@ 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) @@ -602,12 +605,22 @@ export const usePaymentInitiator = () => { ] ) - // @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. + // @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. // // initiate and process payments const initiatePayment = useCallback( @@ -628,19 +641,25 @@ export const usePaymentInitiator = () => { console.log('Proceeding with charge details:', determinedChargeDetails.uuid) // 2. handle charge state - if ( - payload.returnAfterChargeCreation || // For request pot payment, return after charge creation + // 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 || (chargeCreated && (payload.isExternalWalletFlow || !isPeanutWallet || (isPeanutWallet && (!areEvmAddressesEqual(determinedChargeDetails.tokenAddress, PEANUT_WALLET_TOKEN) || - determinedChargeDetails.chainId !== PEANUT_WALLET_CHAIN.id.toString())))) - ) { + 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) { console.log( - `Charge created. Transitioning to Confirm view for: ${ - payload.isExternalWalletFlow ? 'Add Money Flow' : 'External Wallet' - }.` + `Charge ready. Returning without payment execution. (chargeCreated: ${chargeCreated}, skipChargeCreation: ${payload.skipChargeCreation})` ) setLoadingStep('Charge Created') return { status: 'Charge Created', charge: determinedChargeDetails, success: false }