Skip to content
Draft
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
385 changes: 14 additions & 371 deletions src/app/(mobile-ui)/withdraw/crypto/page.tsx

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ConfirmPaymentView from '@/components/Payment/Views/Confirm.payment.view'
import ValidationErrorView, { ValidationErrorViewProps } from '@/components/Payment/Views/Error.validation.view'
import InitialPaymentView from '@/components/Payment/Views/Initial.payment.view'
import DirectSuccessView from '@/components/Payment/Views/Status.payment.view'
import { RequestPayFlow } from '@/components/Payment/flows/RequestPayFlow'
import PintaReqPaySuccessView from '@/components/PintaReqPay/Views/Success.pinta.view'
import PublicProfile from '@/components/Profile/components/PublicProfile'
import { TransactionDetailsReceipt } from '@/components/TransactionDetails/TransactionDetailsDrawer'
Expand Down Expand Up @@ -393,7 +394,22 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
</div>
)
}
// default payment flow
// modernized payment flow using TanStack Query architecture
if (flow === 'request_pay') {
return (
<div className={twMerge('mx-auto h-full min-h-[inherit] w-full space-y-8 self-center')}>
<RequestPayFlow
recipient={recipient}
onComplete={() => {
// Handle completion - could navigate or reset state
console.log('Request payment flow completed')
}}
Comment on lines +403 to +406
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implement proper completion handler

The onComplete callback only logs to console. Consider implementing actual navigation or state cleanup logic.

                    onComplete={() => {
-                        // Handle completion - could navigate or reset state
-                        console.log('Request payment flow completed')
+                        dispatch(paymentActions.resetPaymentState())
+                        router.push('/home')
                    }}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/[...recipient]/client.tsx around lines 402 to 405, the onComplete
handler only logs to the console; replace that stub with real completion
behavior such as navigating to a confirmation page or resetting/closing the
payment UI and refreshing relevant state. Update the component to use the
Next.js router (useRouter) or a passed-in navigation callback to route to a
success/receipt page (e.g., router.push('/payment/success') with any needed
params), and/or call state setters to clear form data and close any modal and
trigger a data refresh (invalidate queries or call a prop refresh function).
Ensure imports and any required props/hooks are added and error handling is
applied for the navigation/cleanup steps.

/>
</div>
)
}

// legacy flows (add_money, direct_pay, etc.) - keep using old system for now
return (
<div className={twMerge('mx-auto min-h-[inherit] w-full space-y-8 self-center')}>
{!user && parsedPaymentData?.recipient?.recipientType !== 'USERNAME' && (
Expand Down
101 changes: 101 additions & 0 deletions src/app/send/[...username]/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use client'

import { DirectSendFlow } from '@/components/Payment/flows/DirectSendFlow'
import PeanutLoading from '@/components/Global/PeanutLoading'
import { AccountType } from '@/interfaces'
import { ParsedURL } from '@/lib/url-parser/types/payment'
import { usersApi } from '@/services/users'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { isAddress } from 'viem'

interface DirectSendPageProps {
recipient: string[]
}

/**
* Client component for direct send flow
* Handles recipient resolution and renders DirectSendFlow
*/
export default function DirectSendPageClient({ recipient }: DirectSendPageProps) {
const router = useRouter()
const [parsedRecipient, setParsedRecipient] = useState<ParsedURL['recipient'] | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

useEffect(() => {
const resolveRecipient = async () => {
try {
if (!recipient || recipient.length === 0) {
setError('No recipient specified')
return
}

const recipientIdentifier = recipient[0]

let resolvedRecipient: ParsedURL['recipient']

if (isAddress(recipientIdentifier)) {
// It's already a valid address
resolvedRecipient = {
identifier: recipientIdentifier,
resolvedAddress: recipientIdentifier,
recipientType: 'ADDRESS',
}
} else {
// It's a username - resolve it to an address
console.log('🔍 Resolving username:', recipientIdentifier)
const user = await usersApi.getByUsername(recipientIdentifier)

// Find the Peanut wallet account (should be the primary one)
const peanutAccount = user.accounts.find((account) => account.type === AccountType.PEANUT_WALLET)

if (!peanutAccount) {
throw new Error(`User ${recipientIdentifier} does not have a Peanut wallet`)
}

resolvedRecipient = {
identifier: recipientIdentifier,
resolvedAddress: peanutAccount.identifier, // This should be the wallet address
recipientType: 'USERNAME',
}

console.log('✅ Username resolved:', {
username: recipientIdentifier,
address: peanutAccount.identifier,
})
}

setParsedRecipient(resolvedRecipient)
} catch (err) {
console.error('Error resolving recipient:', err)
setError('Failed to resolve recipient')
} finally {
setIsLoading(false)
}
}

resolveRecipient()
}, [recipient])
Comment on lines +26 to +79
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding cleanup and error state handling

The useEffect hook doesn't handle cleanup for the async operation, which could lead to state updates after component unmount. Also, the error messages could be more specific to help users understand what went wrong.

Apply this diff to add proper cleanup and improve error handling:

 useEffect(() => {
+    let isMounted = true
+    
     const resolveRecipient = async () => {
         try {
             if (!recipient || recipient.length === 0) {
                 setError('No recipient specified')
                 return
             }
 
             const recipientIdentifier = recipient[0]
 
             let resolvedRecipient: ParsedURL['recipient']
 
             if (isAddress(recipientIdentifier)) {
                 // It's already a valid address
                 resolvedRecipient = {
                     identifier: recipientIdentifier,
                     resolvedAddress: recipientIdentifier,
                     recipientType: 'ADDRESS',
                 }
             } else {
                 // It's a username - resolve it to an address
                 console.log('🔍 Resolving username:', recipientIdentifier)
                 const user = await usersApi.getByUsername(recipientIdentifier)
 
                 // Find the Peanut wallet account (should be the primary one)
                 const peanutAccount = user.accounts.find((account) => account.type === AccountType.PEANUT_WALLET)
 
                 if (!peanutAccount) {
-                    throw new Error(`User ${recipientIdentifier} does not have a Peanut wallet`)
+                    throw new Error(`User @${recipientIdentifier} does not have a Peanut wallet. Please ask them to create one.`)
                 }
 
                 resolvedRecipient = {
                     identifier: recipientIdentifier,
                     resolvedAddress: peanutAccount.identifier, // This should be the wallet address
                     recipientType: 'USERNAME',
                 }
 
                 console.log('✅ Username resolved:', {
                     username: recipientIdentifier,
                     address: peanutAccount.identifier,
                 })
             }
 
-            setParsedRecipient(resolvedRecipient)
+            if (isMounted) {
+                setParsedRecipient(resolvedRecipient)
+            }
         } catch (err) {
             console.error('Error resolving recipient:', err)
-            setError('Failed to resolve recipient')
+            const errorMessage = err instanceof Error ? err.message : 'Failed to resolve recipient'
+            if (isMounted) {
+                setError(errorMessage)
+            }
         } finally {
-            setIsLoading(false)
+            if (isMounted) {
+                setIsLoading(false)
+            }
         }
     }
 
     resolveRecipient()
+    
+    return () => {
+        isMounted = false
+    }
 }, [recipient])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const resolveRecipient = async () => {
try {
if (!recipient || recipient.length === 0) {
setError('No recipient specified')
return
}
const recipientIdentifier = recipient[0]
let resolvedRecipient: ParsedURL['recipient']
if (isAddress(recipientIdentifier)) {
// It's already a valid address
resolvedRecipient = {
identifier: recipientIdentifier,
resolvedAddress: recipientIdentifier,
recipientType: 'ADDRESS',
}
} else {
// It's a username - resolve it to an address
console.log('🔍 Resolving username:', recipientIdentifier)
const user = await usersApi.getByUsername(recipientIdentifier)
// Find the Peanut wallet account (should be the primary one)
const peanutAccount = user.accounts.find((account) => account.type === AccountType.PEANUT_WALLET)
if (!peanutAccount) {
throw new Error(`User ${recipientIdentifier} does not have a Peanut wallet`)
}
resolvedRecipient = {
identifier: recipientIdentifier,
resolvedAddress: peanutAccount.identifier, // This should be the wallet address
recipientType: 'USERNAME',
}
console.log('✅ Username resolved:', {
username: recipientIdentifier,
address: peanutAccount.identifier,
})
}
setParsedRecipient(resolvedRecipient)
} catch (err) {
console.error('Error resolving recipient:', err)
setError('Failed to resolve recipient')
} finally {
setIsLoading(false)
}
}
resolveRecipient()
}, [recipient])
useEffect(() => {
let isMounted = true
const resolveRecipient = async () => {
try {
if (!recipient || recipient.length === 0) {
setError('No recipient specified')
return
}
const recipientIdentifier = recipient[0]
let resolvedRecipient: ParsedURL['recipient']
if (isAddress(recipientIdentifier)) {
// It's already a valid address
resolvedRecipient = {
identifier: recipientIdentifier,
resolvedAddress: recipientIdentifier,
recipientType: 'ADDRESS',
}
} else {
// It's a username - resolve it to an address
console.log('🔍 Resolving username:', recipientIdentifier)
const user = await usersApi.getByUsername(recipientIdentifier)
// Find the Peanut wallet account (should be the primary one)
const peanutAccount = user.accounts.find(
(account) => account.type === AccountType.PEANUT_WALLET
)
if (!peanutAccount) {
throw new Error(
`User @${recipientIdentifier} does not have a Peanut wallet. Please ask them to create one.`
)
}
resolvedRecipient = {
identifier: recipientIdentifier,
resolvedAddress: peanutAccount.identifier, // This should be the wallet address
recipientType: 'USERNAME',
}
console.log('✅ Username resolved:', {
username: recipientIdentifier,
address: peanutAccount.identifier,
})
}
if (isMounted) {
setParsedRecipient(resolvedRecipient)
}
} catch (err) {
console.error('Error resolving recipient:', err)
const errorMessage =
err instanceof Error ? err.message : 'Failed to resolve recipient'
if (isMounted) {
setError(errorMessage)
}
} finally {
if (isMounted) {
setIsLoading(false)
}
}
}
resolveRecipient()
return () => {
isMounted = false
}
}, [recipient])
🤖 Prompt for AI Agents
In src/app/send/[...username]/client.tsx around lines 26 to 79, the useEffect
spawns an async resolver but doesn't cancel or prevent state updates after
unmount and it uses generic error text; add a mounted flag (or AbortController)
inside the effect and check it before calling setParsedRecipient, setError, or
setIsLoading so no state updates occur after unmount, and ensure finally also
checks mounted; when catching errors, populate setError with a clearer message —
prefer err.message or a mapped user-facing string (e.g., "Username not found",
"User has no Peanut wallet", or "Network error") so users see more specific
feedback; also ensure to clear the mounted flag in the cleanup function and
include any used external references in the dependency array if necessary.


const handleComplete = () => {
router.push('/home')
}

if (isLoading) {
return <PeanutLoading />
}

if (error || !parsedRecipient) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<h2 className="text-red-600 text-xl font-bold">Error</h2>
<p className="mt-2 text-gray-600">{error || 'Invalid recipient'}</p>
</div>
</div>
)
}

return <DirectSendFlow recipient={parsedRecipient} onComplete={handleComplete} />
}
6 changes: 2 additions & 4 deletions src/app/send/[...username]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import PaymentPage from '@/app/[...recipient]/client'
import DirectSendPageClient from './client'
import { generateMetadata as generateBaseMetadata } from '@/app/metadata'
import PageContainer from '@/components/0_Bruddle/PageContainer'
import { Metadata } from 'next'
Expand All @@ -12,11 +12,9 @@ export default function DirectPaymentPage(props: PageProps) {
const params = use(props.params)
const usernameSegments = params.username ?? []

const recipient = usernameSegments

return (
<PageContainer>
<PaymentPage recipient={recipient} flow="direct_pay" />
<DirectSendPageClient recipient={usernameSegments} />
</PageContainer>
)
}
Expand Down
45 changes: 45 additions & 0 deletions src/components/Payment/FlowSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import { useDirectSendFlow, useAddMoneyFlow, useCryptoWithdrawFlow, useRequestPayFlow } from '@/hooks/payment'

export type PaymentFlowType = 'direct_send' | 'add_money' | 'withdraw' | 'request_pay'

/**
* Hook that returns the appropriate payment flow based on flow type
* This replaces the complex conditional logic in the old PaymentForm
*/
export const usePaymentFlow = (flowType: PaymentFlowType, chargeId?: string) => {
const directSendFlow = useDirectSendFlow()
const addMoneyFlow = useAddMoneyFlow()
const withdrawFlow = useCryptoWithdrawFlow()
const requestPayFlow = useRequestPayFlow(chargeId)

switch (flowType) {
case 'direct_send':
return {
...directSendFlow,
execute: directSendFlow.sendDirectly,
type: 'direct_send' as const,
}
case 'add_money':
return {
...addMoneyFlow,
execute: addMoneyFlow.addMoney,
type: 'add_money' as const,
}
case 'withdraw':
return {
...withdrawFlow,
execute: withdrawFlow.withdraw,
type: 'withdraw' as const,
}
case 'request_pay':
return {
...requestPayFlow,
execute: requestPayFlow.payRequest,
type: 'request_pay' as const,
}
default:
throw new Error(`Unknown flow type: ${flowType}`)
}
}
166 changes: 166 additions & 0 deletions src/components/Payment/flows/CryptoWithdrawFlow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
'use client'

import { useCryptoWithdrawFlow } from '@/hooks/payment'
import { ITokenPriceData } from '@/interfaces'
import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk'
import { useState, useCallback, useMemo } from 'react'
import { CryptoWithdrawInitial } from './views/CryptoWithdrawInitial'
import { CryptoWithdrawConfirm } from './views/CryptoWithdrawConfirm'
import { CryptoWithdrawStatus } from './views/CryptoWithdrawStatus'

// Flow-specific types
interface CryptoWithdrawFormData {
amount: string
selectedToken: ITokenPriceData | null
selectedChain: (peanutInterfaces.ISquidChain & { tokens: peanutInterfaces.ISquidToken[] }) | null
recipientAddress: string
isValidRecipient: boolean
}

interface CryptoWithdrawFlowProps {
initialAmount?: string
onComplete?: () => void
}

type CryptoWithdrawView = 'INITIAL' | 'CONFIRM' | 'STATUS'

/**
* CryptoWithdrawFlow Orchestrator
*
* Manages the complete crypto withdraw flow (Peanut → External crypto address):
* INITIAL → CONFIRM → STATUS
*/
export const CryptoWithdrawFlow = ({ initialAmount = '', onComplete }: CryptoWithdrawFlowProps) => {
// Simple UI state (orchestrator manages this)
const [currentView, setCurrentView] = useState<CryptoWithdrawView>('INITIAL')
const [formData, setFormData] = useState<CryptoWithdrawFormData>({
amount: initialAmount,
selectedToken: null,
selectedChain: null,
recipientAddress: '',
isValidRecipient: false,
})

// Complex business logic state
const cryptoWithdrawHook = useCryptoWithdrawFlow()

// Form data updater
const updateFormData = useCallback((updates: Partial<CryptoWithdrawFormData>) => {
setFormData((prev) => ({ ...prev, ...updates }))
}, [])

// Create payload from form data
const currentPayload = useMemo(() => {
if (
!formData.selectedToken ||
!formData.selectedChain ||
!formData.recipientAddress ||
!formData.isValidRecipient ||
!formData.amount ||
parseFloat(formData.amount) <= 0
) {
return null
}

return {
recipient: {
identifier: formData.recipientAddress,
resolvedAddress: formData.recipientAddress,
recipientType: 'ADDRESS' as const,
},
tokenAmount: formData.amount,
toChainId: formData.selectedChain.chainId,
toTokenAddress: formData.selectedToken.address,
}
}, [formData])

// View navigation handlers
const handleNext = useCallback(async () => {
if (currentView === 'INITIAL') {
// Validate form
if (!currentPayload) {
console.error('Form validation failed - missing required fields')
return
}

// Prepare route (synchronous now with TanStack Query!)
console.log('Preparing route for withdrawal...')
cryptoWithdrawHook.prepareRoute(currentPayload)

// Move to confirm immediately - route preparation happens in background
console.log('Moving to confirm view, route will prepare automatically')
setCurrentView('CONFIRM')
} else if (currentView === 'CONFIRM') {
// Execute the crypto withdraw transaction with cached route
console.log('Executing withdrawal with cached route...')
const result = await cryptoWithdrawHook.withdraw()

if (result.success) {
console.log('Withdrawal successful, moving to status view')
setCurrentView('STATUS')
} else {
console.error('Withdrawal failed:', result.error)
// Error will be shown by the hook
}
}
}, [currentView, currentPayload, cryptoWithdrawHook])

const handleBack = useCallback(() => {
if (currentView === 'CONFIRM') {
setCurrentView('INITIAL')
} else if (currentView === 'STATUS') {
// Reset everything and start over
setCurrentView('INITIAL')
setFormData({
amount: initialAmount,
selectedToken: null,
selectedChain: null,
recipientAddress: '',
isValidRecipient: false,
})
cryptoWithdrawHook.reset()
}
}, [currentView, initialAmount, cryptoWithdrawHook.reset])

const handleComplete = useCallback(() => {
onComplete?.()
// Note: Component will unmount and all state will be cleaned up automatically!
}, [onComplete])

// Render the appropriate view
if (currentView === 'INITIAL') {
return (
<CryptoWithdrawInitial
formData={formData}
updateFormDataAction={updateFormData}
onNextAction={handleNext}
isProcessing={cryptoWithdrawHook.isProcessing || cryptoWithdrawHook.isPreparingRoute}
error={cryptoWithdrawHook.displayError}
/>
)
}

if (currentView === 'CONFIRM') {
return (
<CryptoWithdrawConfirm
formData={formData}
cryptoWithdrawHook={cryptoWithdrawHook}
onNextAction={handleNext}
onBackAction={handleBack}
/>
)
}

if (currentView === 'STATUS') {
return (
<CryptoWithdrawStatus
formData={formData}
cryptoWithdrawHook={cryptoWithdrawHook}
onCompleteAction={handleComplete}
onWithdrawAnotherAction={handleBack}
/>
)
}

return null
}
Loading
Loading