@@ -68,7 +67,6 @@ export const UserDetailsForm = forwardRef<{ handleSubmit: () => void }, UserDeta
type={type}
placeholder={placeholder}
className="h-12 w-full rounded-sm border border-n-1 bg-white px-4 text-sm"
- disabled={isDisabled}
/>
)}
/>
diff --git a/src/components/AddMoney/components/AddMoneyBankDetails.tsx b/src/components/AddMoney/components/AddMoneyBankDetails.tsx
index 4399f2a2d..c177487ba 100644
--- a/src/components/AddMoney/components/AddMoneyBankDetails.tsx
+++ b/src/components/AddMoney/components/AddMoneyBankDetails.tsx
@@ -42,7 +42,7 @@ export default function AddMoneyBankDetails() {
const countryCodeForFlag = useMemo(() => {
const countryId = currentCountryDetails?.id || 'USA'
- const countryCode = countryCodeMap[countryId]
+ const countryCode = countryCodeMap[countryId] || countryId // if countryId is not in countryCodeMap, use countryId because for some countries countryId is of 2 digit and countryCodeMap is a mapping of 3 digit to 2 digit country codes
return countryCode?.toLowerCase() || 'us'
}, [currentCountryDetails])
diff --git a/src/components/AddMoney/consts/index.ts b/src/components/AddMoney/consts/index.ts
index f0cc48767..2401ef3b9 100644
--- a/src/components/AddMoney/consts/index.ts
+++ b/src/components/AddMoney/consts/index.ts
@@ -157,6 +157,13 @@ export const UPDATED_DEFAULT_ADD_MONEY_METHODS: SpecificPaymentMethod[] = [
description: 'Usually in minutes - KYC required',
isSoon: false,
},
+ {
+ id: 'crypto-add',
+ icon: 'wallet-outline' as IconName,
+ title: 'From Crypto',
+ description: 'Usually arrives instantly',
+ isSoon: false,
+ },
{
id: 'mercado-pago-add',
icon: MERCADO_PAGO,
@@ -188,6 +195,24 @@ export const DEFAULT_BANK_WITHDRAW_METHOD: SpecificPaymentMethod = {
isSoon: false,
}
+export const DEFAULT_WITHDRAW_METHODS: SpecificPaymentMethod[] = [
+ {
+ id: 'crypto-withdraw',
+ icon: 'wallet-outline' as IconName,
+ title: 'Crypto',
+ description: 'Withdraw to a wallet address',
+ isSoon: false,
+ path: '/withdraw/crypto',
+ },
+ {
+ id: 'default-bank-withdraw',
+ icon: 'bank' as IconName,
+ title: 'To Bank',
+ description: 'Standard bank withdrawal',
+ isSoon: false,
+ },
+]
+
const countrySpecificWithdrawMethods: Record<
string,
Array<{ title: string; description: string; icon?: IconName | string }>
@@ -2061,6 +2086,14 @@ countryData.forEach((country) => {
})
}
+ const cryptoWithdrawMethod = DEFAULT_WITHDRAW_METHODS.find((m) => m.id === 'crypto-withdraw')
+ if (cryptoWithdrawMethod) {
+ const cryptoExists = withdrawList.some((m) => m.id === 'crypto-withdraw')
+ if (!cryptoExists) {
+ withdrawList.unshift(cryptoWithdrawMethod)
+ }
+ }
+
// filter add methods: include Mercado Pago only for LATAM countries
const currentAddMethods = UPDATED_DEFAULT_ADD_MONEY_METHODS.filter((method) => {
if (method.id === 'mercado-pago-add') {
@@ -2072,6 +2105,9 @@ countryData.forEach((country) => {
if (newMethod.id === 'bank-transfer-add') {
newMethod.path = `/add-money/${country.path}/bank`
newMethod.isSoon = !isCountryEnabledForBankTransfer(countryCode) || countryCode === 'MX'
+ } else if (newMethod.id === 'crypto-add') {
+ newMethod.path = `/add-money/crypto`
+ newMethod.isSoon = false
} else {
newMethod.isSoon = true
}
diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
index b6b7479dc..27ff70324 100644
--- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
+++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
@@ -13,7 +13,7 @@ import EmptyState from '../Global/EmptyStates/EmptyState'
import { useAuth } from '@/context/authContext'
import { useEffect, useRef, useState } from 'react'
import { InitiateKYCModal } from '@/components/Kyc'
-import { DynamicBankAccountForm, FormData } from './DynamicBankAccountForm'
+import { DynamicBankAccountForm, IBankAccountDetails } from './DynamicBankAccountForm'
import { addBankAccount, updateUserById } from '@/app/actions/users'
import { jsonParse, jsonStringify } from '@/utils/general.utils'
import { KYCStatus } from '@/utils/bridge-accounts.utils'
@@ -36,7 +36,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
const { setFromBankSelected } = useOnrampFlow()
const [view, setView] = useState<'list' | 'form'>('list')
const [isKycModalOpen, setIsKycModalOpen] = useState(false)
- const [cachedBankDetails, setCachedBankDetails] = useState
| null>(null)
+ const [cachedBankDetails, setCachedBankDetails] = useState | null>(null)
const formRef = useRef<{ handleSubmit: () => void }>(null)
const [liveKycStatus, setLiveKycStatus] = useState(user?.user?.kycStatus as KYCStatus)
@@ -78,7 +78,10 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
}
}, [user, liveKycStatus, cachedBankDetails])
- const handleFormSubmit = async (payload: AddBankAccountPayload, rawData: FormData): Promise<{ error?: string }> => {
+ const handleFormSubmit = async (
+ payload: AddBankAccountPayload,
+ rawData: IBankAccountDetails
+ ): Promise<{ error?: string }> => {
const currentKycStatus = liveKycStatus || user?.user.kycStatus
const isUserKycVerified = currentKycStatus === 'approved'
@@ -248,7 +251,9 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
backgroundColor:
method.icon === ('bank' as IconName)
? '#FFC900'
- : getColorForUsername(method.title).lightShade,
+ : method.id === 'crypto-add' || method.id === 'crypto-withdraw'
+ ? '#FFC900'
+ : getColorForUsername(method.title).lightShade,
color: method.icon === ('bank' as IconName) ? 'black' : 'black',
}}
/>
diff --git a/src/components/AddWithdraw/AddWithdrawRouterView.tsx b/src/components/AddWithdraw/AddWithdrawRouterView.tsx
index e2b8ed69a..02531d777 100644
--- a/src/components/AddWithdraw/AddWithdrawRouterView.tsx
+++ b/src/components/AddWithdraw/AddWithdrawRouterView.tsx
@@ -260,7 +260,7 @@ export const AddWithdrawRouterView: FC = ({
}))
return (
-
+
Recent methods
@@ -270,7 +270,13 @@ export const AddWithdrawRouterView: FC
= ({
isAllMethodsView={false}
/>
-
setShowAllMethods(true)} shadowSize="4">
+
+
+ setShowAllMethods(true)} shadowSize="4">
Select new method
diff --git a/src/components/AddWithdraw/DynamicBankAccountForm.tsx b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
index 5f39171d1..65a79dca9 100644
--- a/src/components/AddWithdraw/DynamicBankAccountForm.tsx
+++ b/src/components/AddWithdraw/DynamicBankAccountForm.tsx
@@ -10,7 +10,7 @@ import { useParams } from 'next/navigation'
import { validateBankAccount, validateIban, validateBic, isValidRoutingNumber } from '@/utils/bridge-accounts.utils'
import ErrorAlert from '@/components/Global/ErrorAlert'
import { getBicFromIban } from '@/app/actions/ibanToBic'
-import PeanutActionDetailsCard from '../Global/PeanutActionDetailsCard'
+import PeanutActionDetailsCard, { PeanutActionDetailsCardProps } from '../Global/PeanutActionDetailsCard'
import { PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
@@ -18,7 +18,8 @@ const isIBANCountry = (country: string) => {
return countryCodeMap[country.toUpperCase()] !== undefined
}
-export type FormData = {
+export type IBankAccountDetails = {
+ name?: string
firstName: string
lastName: string
email: string
@@ -30,19 +31,24 @@ export type FormData = {
city: string
state: string
postalCode: string
+ iban?: string
+ country: string
}
interface DynamicBankAccountFormProps {
country: string
- onSuccess: (payload: AddBankAccountPayload, rawData: FormData) => Promise<{ error?: string }>
- initialData?: Partial
+ onSuccess: (payload: AddBankAccountPayload, rawData: IBankAccountDetails) => Promise<{ error?: string }>
+ initialData?: Partial
+ flow?: 'claim' | 'withdraw'
+ actionDetailsProps?: Partial
}
export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, DynamicBankAccountFormProps>(
- ({ country, onSuccess, initialData }, ref) => {
+ ({ country, onSuccess, initialData, flow = 'withdraw', actionDetailsProps }, ref) => {
const { user } = useAuth()
const [isSubmitting, setIsSubmitting] = useState(false)
const [submissionError, setSubmissionError] = useState(null)
+ const [showBicField, setShowBicField] = useState(false)
const { country: countryName } = useParams()
const { amountToWithdraw } = useWithdrawFlow()
const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ')
@@ -53,7 +59,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
handleSubmit,
setValue,
formState: { errors, isValid, isValidating, touchedFields },
- } = useForm({
+ } = useForm({
defaultValues: {
firstName: firstName ?? '',
lastName: lastName ?? '',
@@ -75,7 +81,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
handleSubmit: handleSubmit(onSubmit),
}))
- const onSubmit = async (data: FormData) => {
+ const onSubmit = async (data: IBankAccountDetails) => {
setIsSubmitting(true)
setSubmissionError(null)
try {
@@ -119,8 +125,11 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
const result = await onSuccess(payload as AddBankAccountPayload, {
...data,
+ iban: isIban ? data.accountNumber : undefined,
+ country,
firstName: data.firstName.trim(),
lastName: data.lastName.trim(),
+ name: data.name,
})
if (result.error) {
setSubmissionError(result.error)
@@ -137,7 +146,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
const isMx = country.toUpperCase() === 'MX'
const renderInput = (
- name: keyof FormData,
+ name: keyof IBankAccountDetails,
placeholder: string,
rules: any,
type: string = 'text',
@@ -186,6 +195,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
recipientName={country}
amount={amountToWithdraw}
tokenSymbol={PEANUT_WALLET_TOKEN_SYMBOL}
+ {...actionDetailsProps}
/>
@@ -197,13 +207,18 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
}}
className="space-y-4"
>
- {!user?.user?.fullName && (
+ {flow === 'claim' &&
+ renderInput('name', 'Full Name', {
+ required: 'Full name is required',
+ })}
+ {flow !== 'claim' && !user?.user?.fullName && (
{renderInput('firstName', 'First Name', { required: 'First name is required' })}
{renderInput('lastName', 'Last Name', { required: 'Last name is required' })}
)}
- {!user?.user?.email &&
+ {flow !== 'claim' &&
+ !user?.user?.email &&
renderInput('email', 'E-mail', {
required: 'Email is required',
})}
@@ -228,12 +243,17 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
if (!field.value || field.value.trim().length === 0) return
try {
+ setShowBicField(false)
+ setValue('bic', '', { shouldValidate: false })
const bic = await getBicFromIban(field.value.trim())
if (bic) {
setValue('bic', bic, { shouldValidate: true })
+ } else {
+ setShowBicField(true)
}
} catch (error) {
console.warn('Failed to fetch BIC:', error)
+ setShowBicField(true)
}
}
)
@@ -249,6 +269,7 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D
)}
{isIban &&
+ showBicField &&
renderInput('bic', 'BIC', {
required: 'BIC is required',
validate: async (value: string) => (await validateBic(value)) || 'Invalid BIC code',
diff --git a/src/components/Claim/Claim.consts.ts b/src/components/Claim/Claim.consts.ts
index f5deca324..0dde26017 100644
--- a/src/components/Claim/Claim.consts.ts
+++ b/src/components/Claim/Claim.consts.ts
@@ -56,8 +56,6 @@ export interface IClaimScreenProps {
setUserId: (id: string | undefined) => void
initialKYCStep: number
setInitialKYCStep: (step: number) => void
- claimToExternalWallet: boolean
- setClaimToExternalWallet: (claimToExternalWallet: boolean) => void
}
export enum claimLinkStateType {
diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx
index 05ee7de3f..0ed1cae89 100644
--- a/src/components/Claim/Claim.tsx
+++ b/src/components/Claim/Claim.tsx
@@ -24,6 +24,7 @@ import PeanutLoading from '../Global/PeanutLoading'
import * as _consts from './Claim.consts'
import * as genericViews from './Generic'
import FlowManager from './Link/FlowManager'
+import { useGuestFlow } from '@/context/GuestFlowContext'
export const Claim = ({}) => {
const [step, setStep] = useState<_consts.IClaimScreenState>(_consts.INIT_VIEW_STATE)
@@ -51,7 +52,6 @@ export const Claim = ({}) => {
password: '',
recipient: '',
})
- const [claimToExternalWallet, setClaimToExternalWallet] = useState
(false)
const { setSelectedChainID, setSelectedTokenAddress } = useContext(tokenSelectorContext)
const { selectedTransaction, openTransactionDetails } = useTransactionDetailsDrawer()
@@ -272,8 +272,6 @@ export const Claim = ({}) => {
setUserId,
initialKYCStep,
setInitialKYCStep,
- claimToExternalWallet,
- setClaimToExternalWallet,
} as unknown as _consts.IClaimScreenProps
}
/>
diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx
index 90aff2f48..cc7862cdf 100644
--- a/src/components/Claim/Link/Initial.view.tsx
+++ b/src/components/Claim/Link/Initial.view.tsx
@@ -12,13 +12,13 @@ import {
usdcAddressOptimism,
} from '@/components/Offramp/Offramp.consts'
import { ActionType, estimatePoints } from '@/components/utils/utils'
-import * as consts from '@/constants'
import {
PEANUT_WALLET_CHAIN,
PEANUT_WALLET_TOKEN,
PINTA_WALLET_CHAIN,
PINTA_WALLET_TOKEN,
ROUTE_NOT_FOUND_ERROR,
+ SQUID_API_URL,
} from '@/constants'
import { TRANSACTIONS } from '@/constants/query.consts'
import { loadingStateContext, tokenSelectorContext } from '@/context'
@@ -42,32 +42,38 @@ import { useQueryClient } from '@tanstack/react-query'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { formatUnits } from 'viem'
-import * as _consts from '../Claim.consts'
+import { IClaimScreenProps } from '../Claim.consts'
import useClaimLink from '../useClaimLink'
-
-export const InitialClaimLinkView = ({
- onNext,
- claimLinkData,
- setRecipient,
- recipient,
- tokenPrice,
- setClaimType,
- setEstimatedPoints,
- attachment,
- setTransactionHash,
- onCustom,
- selectedRoute,
- setSelectedRoute,
- hasFetchedRoute,
- setHasFetchedRoute,
- recipientType,
- setRecipientType,
- setOfframpForm,
- setUserType,
- setInitialKYCStep,
- setClaimToExternalWallet,
- claimToExternalWallet,
-}: _consts.IClaimScreenProps) => {
+import GuestActionList from '@/components/GuestActions/MethodList'
+import { useGuestFlow } from '@/context/GuestFlowContext'
+import ActionModal from '@/components/Global/ActionModal'
+import { Slider } from '@/components/Slider'
+import Image from 'next/image'
+import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
+import { BankFlowManager } from './views/BankFlowManager.view'
+
+export const InitialClaimLinkView = (props: IClaimScreenProps) => {
+ const {
+ onNext,
+ claimLinkData,
+ setRecipient,
+ recipient,
+ tokenPrice,
+ setClaimType,
+ setEstimatedPoints,
+ attachment,
+ setTransactionHash,
+ onCustom,
+ selectedRoute,
+ setSelectedRoute,
+ hasFetchedRoute,
+ setHasFetchedRoute,
+ recipientType,
+ setRecipientType,
+ setOfframpForm,
+ setUserType,
+ setInitialKYCStep,
+ } = props
const [isValidRecipient, setIsValidRecipient] = useState(false)
const [errorState, setErrorState] = useState<{
showError: boolean
@@ -76,7 +82,17 @@ export const InitialClaimLinkView = ({
const [isXchainLoading, setIsXchainLoading] = useState(false)
const [routes, setRoutes] = useState([])
const [inputChanging, setInputChanging] = useState(false)
+ const [showConfirmationModal, setShowConfirmationModal] = useState(false)
+ const {
+ claimToExternalWallet,
+ resetGuestFlow,
+ showGuestActionsList,
+ guestFlowStep,
+ showVerificationModal,
+ setShowVerificationModal,
+ setClaimToExternalWallet,
+ } = useGuestFlow()
const { setLoadingState, isLoading } = useContext(loadingStateContext)
const {
selectedChainID,
@@ -129,79 +145,88 @@ export const InitialClaimLinkView = ({
}
}, [recipientType, claimLinkData.chainId, isPeanutChain, claimLinkData.tokenAddress])
- const handleClaimLink = useCallback(async () => {
- setLoadingState('Loading')
- setErrorState({
- showError: false,
- errorMessage: '',
- })
+ const handleClaimLink = useCallback(
+ async (bypassModal = false) => {
+ if (!isPeanutWallet && !bypassModal) {
+ setShowConfirmationModal(true)
+ return
+ }
+ setShowConfirmationModal(false)
+
+ setLoadingState('Loading')
+ setErrorState({
+ showError: false,
+ errorMessage: '',
+ })
+
+ if (recipient.address === '') return
- if (recipient.address === '') return
+ try {
+ setLoadingState('Executing transaction')
+ if (isPeanutWallet) {
+ await sendLinksApi.claim(user?.user.username ?? address, claimLinkData.link)
- try {
- setLoadingState('Executing transaction')
- if (isPeanutWallet) {
- await sendLinksApi.claim(user?.user.username ?? address, claimLinkData.link)
-
- setClaimType('claim')
- onCustom('SUCCESS')
- fetchBalance()
- queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
- } else {
- // Check if cross-chain claiming is needed
- const needsXChain =
- selectedChainID !== claimLinkData.chainId ||
- !areEvmAddressesEqual(selectedTokenAddress, claimLinkData.tokenAddress)
-
- let claimTxHash: string
- if (needsXChain) {
- claimTxHash = await claimLinkXchain({
- address: recipient.address,
- link: claimLinkData.link,
- destinationChainId: selectedChainID,
- destinationToken: selectedTokenAddress,
- })
- setClaimType('claimxchain')
- } else {
- claimTxHash = await claimLink({
- address: recipient.address,
- link: claimLinkData.link,
- })
setClaimType('claim')
- }
+ onCustom('SUCCESS')
+ fetchBalance()
+ queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
+ } else {
+ // Check if cross-chain claiming is needed
+ const needsXChain =
+ selectedChainID !== claimLinkData.chainId ||
+ !areEvmAddressesEqual(selectedTokenAddress, claimLinkData.tokenAddress)
+
+ let claimTxHash: string
+ if (needsXChain) {
+ claimTxHash = await claimLinkXchain({
+ address: recipient.address,
+ link: claimLinkData.link,
+ destinationChainId: selectedChainID,
+ destinationToken: selectedTokenAddress,
+ })
+ setClaimType('claimxchain')
+ } else {
+ claimTxHash = await claimLink({
+ address: recipient.address,
+ link: claimLinkData.link,
+ })
+ setClaimType('claim')
+ }
- setTransactionHash(claimTxHash)
- onCustom('SUCCESS')
- queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
+ setTransactionHash(claimTxHash)
+ onCustom('SUCCESS')
+ queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
+ }
+ } catch (error) {
+ const errorString = ErrorHandler(error)
+ setErrorState({
+ showError: true,
+ errorMessage: errorString,
+ })
+ Sentry.captureException(error)
+ } finally {
+ setLoadingState('Idle')
}
- } catch (error) {
- const errorString = ErrorHandler(error)
- setErrorState({
- showError: true,
- errorMessage: errorString,
- })
- Sentry.captureException(error)
- } finally {
- setLoadingState('Idle')
- }
- }, [
- claimLinkData.link,
- claimLinkData.chainId,
- claimLinkData.tokenAddress,
- isPeanutWallet,
- fetchBalance,
- recipient.address,
- user,
- claimLink,
- claimLinkXchain,
- selectedChainID,
- selectedTokenAddress,
- onCustom,
- setLoadingState,
- setClaimType,
- setTransactionHash,
- queryClient,
- ])
+ },
+ [
+ claimLinkData.link,
+ claimLinkData.chainId,
+ claimLinkData.tokenAddress,
+ isPeanutWallet,
+ fetchBalance,
+ recipient.address,
+ user,
+ claimLink,
+ claimLinkXchain,
+ selectedChainID,
+ selectedTokenAddress,
+ onCustom,
+ setLoadingState,
+ setClaimType,
+ setTransactionHash,
+ queryClient,
+ ]
+ )
useEffect(() => {
if (isPeanutWallet) resetSelectedToken()
@@ -285,8 +310,8 @@ export const InitialClaimLinkView = ({
})
if (user?.user.kycStatus === 'approved') {
const account = user.accounts.find(
- (account: any) =>
- account.account_identifier.replaceAll(/\s/g, '').toLowerCase() ===
+ (account) =>
+ account.identifier.replaceAll(/\s/g, '').toLowerCase() ===
recipient.address.replaceAll(/\s/g, '').toLowerCase()
)
@@ -355,7 +380,7 @@ export const InitialClaimLinkView = ({
const isReward = useMemo(() => {
if (!claimLinkData.tokenAddress) return false
- return areEvmAddressesEqual(claimLinkData.tokenAddress, consts.PINTA_WALLET_TOKEN)
+ return areEvmAddressesEqual(claimLinkData.tokenAddress, PINTA_WALLET_TOKEN)
}, [claimLinkData.tokenAddress])
const fetchRoute = useCallback(
@@ -385,7 +410,7 @@ export const InitialClaimLinkView = ({
: claimLinkData.tokenAddress.toLowerCase()
const route = await getSquidRouteRaw({
- squidRouterUrl: `${consts.SQUID_API_URL}/route`,
+ squidRouterUrl: `${SQUID_API_URL}/v2/route`,
fromChain: claimLinkData.chainId.toString(),
fromToken: fromToken,
fromAmount: tokenAmount.toString(),
@@ -433,6 +458,12 @@ export const InitialClaimLinkView = ({
]
)
+ useEffect(() => {
+ if (guestFlowStep?.startsWith('bank-')) {
+ resetSelectedToken()
+ }
+ }, [guestFlowStep, resetSelectedToken])
+
useEffect(() => {
let isMounted = true
if (isReward || !claimLinkData.tokenAddress) {
@@ -517,57 +548,81 @@ export const InitialClaimLinkView = ({
])
const getButtonText = () => {
- if (isPeanutWallet && !isPeanutChain) {
- return 'Review'
+ if (isPeanutWallet) {
+ return (
+
+ )
}
- if ((selectedRoute || (isXChain && hasFetchedRoute)) && !isPeanutChain) {
+ if (selectedRoute || (isXChain && hasFetchedRoute)) {
return 'Review'
}
- if (isLoading) {
- return 'Claiming'
+ if (isLoading && !inputChanging) {
+ return 'Receiving'
}
- return 'Claim'
+ return 'Receive now'
+ }
+
+ const handleClaimAction = () => {
+ if (isPeanutWallet && !isPeanutChain) {
+ setRefetchXchainRoute(true)
+ onNext()
+ } else if (recipientType === 'iban' || recipientType === 'us') {
+ handleIbanRecipient()
+ } else if ((selectedRoute || (isXChain && hasFetchedRoute)) && !isPeanutChain) {
+ onNext()
+ } else {
+ handleClaimLink()
+ }
}
const guestAction = () => {
if (!!user?.user.userId || claimToExternalWallet) return null
return (
-
{
- saveRedirectUrl()
- router.push('/setup')
- }}
- className="w-full"
- >
- Sign In
-
- {!isPeanutClaimOnlyMode && (
+ {!showGuestActionsList && (
setClaimToExternalWallet(true)}
- className="w-full"
+ onClick={() => {
+ saveRedirectUrl()
+ router.push('/setup')
+ }}
+ className="flex w-full items-center gap-1"
>
- Claim to External Wallet
+ Continue with
+
+
+
+
)}
+ {!isPeanutClaimOnlyMode &&
}
)
}
+ if (guestFlowStep?.startsWith('bank-')) {
+ return
+ }
+
return (
- {!!user?.user.userId || claimToExternalWallet ? (
+ {!!user?.user.userId || showGuestActionsList ? (
{
if (claimToExternalWallet) {
setClaimToExternalWallet(false)
+ } else if (showGuestActionsList) {
+ resetGuestFlow()
} else {
router.push('/home')
}
@@ -576,7 +631,7 @@ export const InitialClaimLinkView = ({
) : (
)}
@@ -604,6 +659,7 @@ export const InitialClaimLinkView = ({
recipientType !== 'iban' &&
recipientType !== 'us' &&
!isPeanutClaimOnlyMode &&
+ guestFlowStep !== 'bank-country-selection' &&
!!claimToExternalWallet && (
)}
@@ -651,18 +707,7 @@ export const InitialClaimLinkView = ({
{
- if (isPeanutWallet && !isPeanutChain) {
- setRefetchXchainRoute(true)
- onNext()
- } else if (recipientType === 'iban' || recipientType === 'us') {
- handleIbanRecipient()
- } else if ((selectedRoute || (isXChain && hasFetchedRoute)) && !isPeanutChain) {
- onNext()
- } else {
- handleClaimLink()
- }
- }}
+ onClick={handleClaimAction}
loading={isLoading || isXchainLoading}
disabled={
isLoading ||
@@ -678,6 +723,68 @@ export const InitialClaimLinkView = ({
)}
+ setShowConfirmationModal(false)}
+ title="Is this address compatible?"
+ description={
+
+
Only claim to an address that support the selected network and token.
+
Incorrect transfers may be lost.
+
+ }
+ icon="alert"
+ iconContainerClassName="bg-yellow-400"
+ footer={
+
+ {
+ if (!v) return
+ // for cross-chain claims, advance to the confirm screen first
+ if (isXChain) {
+ setShowConfirmationModal(false)
+ onNext()
+ } else {
+ // direct on-chain claim – initiate immediately
+ handleClaimLink(true)
+ }
+ }}
+ />
+
+ }
+ preventClose={false}
+ modalPanelClassName="max-w-md mx-8"
+ />
+ setShowVerificationModal(false)}
+ title="This method requires verification"
+ description="To receive funds on your bank account, you’ll create a free Peanut Wallet and complete a quick identity check (KYC)."
+ icon="alert"
+ iconContainerClassName="bg-yellow-400"
+ ctaClassName="md:flex-col gap-4"
+ ctas={[
+ {
+ text: 'Start verification',
+ shadowSize: '4',
+ className: 'md:py-2.5',
+ onClick: () => {
+ saveRedirectUrl()
+ router.push('/setup')
+ },
+ },
+ {
+ text: 'Claim with other method',
+ variant: 'transparent',
+ className: 'w-full h-auto underline underline-offset-2',
+ onClick: () => {
+ setShowVerificationModal(false)
+ },
+ },
+ ]}
+ preventClose={false}
+ modalPanelClassName="max-w-md mx-8"
+ />
)
}
diff --git a/src/components/Claim/Link/Onchain/Confirm.view.tsx b/src/components/Claim/Link/Onchain/Confirm.view.tsx
index 175326387..9c3d338d3 100644
--- a/src/components/Claim/Link/Onchain/Confirm.view.tsx
+++ b/src/components/Claim/Link/Onchain/Confirm.view.tsx
@@ -1,6 +1,5 @@
'use client'
import { Button } from '@/components/0_Bruddle'
-import AddressLink from '@/components/Global/AddressLink'
import Card from '@/components/Global/Card'
import DisplayIcon from '@/components/Global/DisplayIcon'
import ErrorAlert from '@/components/Global/ErrorAlert'
@@ -51,12 +50,29 @@ export const ConfirmClaimLinkView = ({
return areEvmAddressesEqual(claimLinkData.tokenAddress, PINTA_WALLET_TOKEN)
}, [claimLinkData.tokenAddress])
+ // Determine which chain/token details to show – prefer the selectedRoute details if present,
+ // otherwise fall back to what the user picked in the token selector.
const { tokenIconUrl, chainIconUrl, resolvedChainName, resolvedTokenSymbol } = useTokenChainIcons({
- chainId: selectedRoute?.route.params.toChain,
- tokenAddress: selectedRoute?.route.estimate.toToken.address,
- tokenSymbol: selectedRoute?.route.estimate.toToken.symbol,
+ chainId: selectedRoute?.route.params.toChain ?? selectedChainID,
+ tokenAddress: selectedRoute?.route.estimate.toToken.address ?? selectedTokenAddress,
+ tokenSymbol: selectedRoute?.route.estimate.toToken.symbol ?? claimLinkData.tokenSymbol,
})
+ // calculate minimum amount the user will receive after slippage
+ const minReceived = useMemo(() => {
+ let amountNumber: number
+
+ // manual 1% slippage calculation based on the deposited token amount
+ amountNumber = Number(formatUnits(BigInt(claimLinkData.amount), claimLinkData.tokenDecimals)) * 0.99 // subtract 1%
+
+ const formattedAmount = formatTokenAmount(amountNumber)
+
+ return `$ ${formattedAmount}`
+ }, [selectedRoute, resolvedTokenSymbol, claimLinkData])
+
+ // Network fee display – always sponsored in this flow
+ const networkFeeDisplay: string = 'Sponsored by Peanut!'
+
const handleOnClaim = async () => {
if (!recipient) {
return
@@ -138,20 +154,21 @@ export const ConfirmClaimLinkView = ({
/>
{!isReward && (
-
- }
- />
-
- {selectedRoute && (
+ {/* Min received row */}
+ {minReceived && (
+
+ )}
+
+ {/* Token & network row */}
+ {
+
)}
- )}
-
- {resolvedTokenSymbol || selectedRoute?.route.estimate.toToken.symbol} on{' '}
-
- {resolvedChainName || selectedRoute?.route.params.toChain}
+
+ {resolvedTokenSymbol || claimLinkData.tokenSymbol} on{' '}
+ {resolvedChainName || selectedChainID}
-
-
- }
- hideBottomBorder={!selectedRoute}
- />
- {selectedRoute && (
- <>
-
-
- >
- )}
+
+ }
+ />
+ }
+
+ {/* Max network fee row */}
+
+
+ {/* Peanut fee row */}
+
)}
@@ -202,7 +212,7 @@ export const ConfirmClaimLinkView = ({
disabled={isLoading}
loading={isLoading}
>
- {isLoading ? 'Claiming' : 'Claim'}
+ Receive now
{errorState.showError &&
}
diff --git a/src/components/Claim/Link/Onchain/Success.view.tsx b/src/components/Claim/Link/Onchain/Success.view.tsx
index 4fbd6232b..32a128f20 100644
--- a/src/components/Claim/Link/Onchain/Success.view.tsx
+++ b/src/components/Claim/Link/Onchain/Success.view.tsx
@@ -3,9 +3,10 @@ import NavHeader from '@/components/Global/NavHeader'
import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard'
import { TRANSACTIONS } from '@/constants/query.consts'
import { useAuth } from '@/context/authContext'
+import { useGuestFlow } from '@/context/GuestFlowContext'
import { useUserStore } from '@/redux/hooks'
import { ESendLinkStatus, sendLinksApi } from '@/services/sendLinks'
-import { getTokenDetails, printableAddress } from '@/utils'
+import { formatTokenAmount, getTokenDetails, printableAddress, shortenAddressLong } from '@/utils'
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo } from 'react'
@@ -18,11 +19,13 @@ export const SuccessClaimLinkView = ({
setTransactionHash,
claimLinkData,
type,
+ tokenPrice,
}: _consts.IClaimScreenProps) => {
const { user: authUser } = useUserStore()
const { fetchUser } = useAuth()
const router = useRouter()
const queryClient = useQueryClient()
+ const { offrampDetails, claimType, bankDetails } = useGuestFlow()
useEffect(() => {
queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })
@@ -72,43 +75,80 @@ export const SuccessClaimLinkView = ({
return tokenDetails
}, [claimLinkData])
+ const maskedAccountNumber = useMemo(() => {
+ if (bankDetails?.iban) {
+ return `to ${shortenAddressLong(bankDetails.iban)}`
+ }
+ if (bankDetails?.clabe) {
+ return `to ${shortenAddressLong(bankDetails.clabe)}`
+ }
+ if (bankDetails?.accountNumber) {
+ return `to ${shortenAddressLong(bankDetails.accountNumber)}`
+ }
+ return 'to your bank account'
+ }, [bankDetails])
+
+ const isBankClaim = claimType === 'claim-bank'
+
+ const navHeaderTitle = 'Receive'
+
+ const cardProps = {
+ viewType: 'SUCCESS' as const,
+ transactionType: (isBankClaim ? 'CLAIM_LINK_BANK_ACCOUNT' : 'CLAIM_LINK') as
+ | 'CLAIM_LINK_BANK_ACCOUNT'
+ | 'CLAIM_LINK',
+ recipientType: isBankClaim ? ('BANK_ACCOUNT' as const) : ('USERNAME' as const),
+ recipientName: isBankClaim
+ ? maskedAccountNumber
+ : (claimLinkData.sender?.username ?? printableAddress(claimLinkData.senderAddress)),
+ amount: isBankClaim
+ ? (formatTokenAmount(
+ Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) * (tokenPrice ?? 0)
+ ) ?? '')
+ : formatUnits(claimLinkData.amount, tokenDetails?.decimals ?? 6),
+ tokenSymbol: isBankClaim ? (offrampDetails?.quote.destination_currency ?? '') : claimLinkData.tokenSymbol,
+ message: isBankClaim
+ ? maskedAccountNumber
+ : `from ${claimLinkData.sender?.username || printableAddress(claimLinkData.senderAddress)}`,
+ title: isBankClaim ? 'You will receive' : 'You claimed',
+ }
+
+ const renderButtons = () => {
+ if (authUser?.user.userId) {
+ return (
+
{
+ if (!isBankClaim) fetchUser()
+ router.push('/home')
+ }}
+ className="w-full"
+ >
+ Back to home
+
+ )
+ }
+ return (
+
router.push('/setup')} shadowSize="4">
+ Create Account
+
+ )
+ }
+
return (
{
router.push('/home')
}}
/>
-
- {!!authUser?.user.userId ? (
-
{
- fetchUser()
- router.push('/home')
- }}
- className="w-full"
- >
- Back to home
-
- ) : (
-
router.push('/setup')} shadowSize="4">
- Create Account
-
- )}
+
+ {renderButtons()}
)
diff --git a/src/components/Claim/Link/views/BankFlowManager.view.tsx b/src/components/Claim/Link/views/BankFlowManager.view.tsx
new file mode 100644
index 000000000..85c22cff7
--- /dev/null
+++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx
@@ -0,0 +1,229 @@
+'use client'
+
+import { IClaimScreenProps } from '../../Claim.consts'
+import { DynamicBankAccountForm, IBankAccountDetails } from '@/components/AddWithdraw/DynamicBankAccountForm'
+import { useGuestFlow } from '@/context/GuestFlowContext'
+import { ClaimCountryListView } from './ClaimCountryList.view'
+import { useCallback, useContext, useState } from 'react'
+import { loadingStateContext } from '@/context'
+import { createBridgeExternalAccountForGuest } from '@/app/actions/external-accounts'
+import { confirmOfframp, createOfframpForGuest } from '@/app/actions/offramp'
+import { Address, formatUnits } from 'viem'
+import { ErrorHandler, formatTokenAmount } from '@/utils'
+import * as Sentry from '@sentry/nextjs'
+import useClaimLink from '../../useClaimLink'
+import { ConfirmBankClaimView } from './Confirm.bank-claim.view'
+import { AddBankAccountPayload } from '@/app/actions/types/users.types'
+import { TCreateOfframpRequest, TCreateOfframpResponse } from '@/services/services.types'
+import { getOfframpCurrencyConfig } from '@/utils/bridge.utils'
+import { getBridgeChainName, getBridgeTokenName } from '@/utils/bridge-accounts.utils'
+import peanut from '@squirrel-labs/peanut-sdk'
+import { getUserById } from '@/app/actions/users'
+import NavHeader from '@/components/Global/NavHeader'
+
+export const BankFlowManager = (props: IClaimScreenProps) => {
+ const { onCustom, claimLinkData, setTransactionHash } = props
+ const { guestFlowStep, setGuestFlowStep, selectedCountry, setClaimType, setBankDetails } = useGuestFlow()
+ const { isLoading, setLoadingState } = useContext(loadingStateContext)
+ const { claimLink } = useClaimLink()
+ const [offrampDetails, setOfframpDetails] = useState
(null)
+ const [localBankDetails, setLocalBankDetails] = useState(null)
+ const [receiverFullName, setReceiverFullName] = useState('')
+ const [error, setError] = useState(null)
+
+ const handleSuccess = async (payload: AddBankAccountPayload, rawData: IBankAccountDetails) => {
+ if (!selectedCountry) {
+ const err = 'Country not selected'
+ setError(err)
+ return { error: err }
+ }
+
+ try {
+ if (!claimLinkData.sender?.userId) return { error: 'Sender details not found' }
+ setLoadingState('Executing transaction')
+ setError(null)
+ const userResponse = await getUserById(claimLinkData.sender?.userId ?? claimLinkData.senderAddress)
+
+ if (!userResponse || ('error' in userResponse && userResponse.error)) {
+ const errorMessage =
+ (userResponse && typeof userResponse.error === 'string' && userResponse.error) ||
+ 'Failed to get user info'
+ setError(errorMessage)
+ return { error: errorMessage }
+ }
+
+ if (userResponse.kycStatus !== 'approved') {
+ setError('User not KYC approved')
+ return { error: 'User not KYC approved' }
+ }
+
+ setReceiverFullName(rawData.name ?? '')
+ sessionStorage.setItem('receiverFullName', rawData.name ?? '')
+
+ const [firstName, ...lastNameParts] = rawData.name?.split(' ') ?? ['', '']
+ const lastName = lastNameParts.join(' ')
+
+ const paymentRail = getBridgeChainName(claimLinkData.chainId)
+ const currency = getBridgeTokenName(claimLinkData.chainId, claimLinkData.tokenAddress)
+
+ if (!paymentRail || !currency) {
+ const err = 'Chain or token not supported for bank withdrawal'
+ setError(err)
+ return { error: err }
+ }
+
+ const payloadWithCountry = {
+ ...payload,
+ country: selectedCountry.id,
+ accountOwnerName: {
+ firstName: firstName,
+ lastName: lastName,
+ },
+ }
+
+ if (!userResponse?.bridgeCustomerId) {
+ setError('Sender details not found')
+ return { error: 'Sender details not found' }
+ }
+
+ const externalAccountResponse = await createBridgeExternalAccountForGuest(
+ userResponse.bridgeCustomerId,
+ payloadWithCountry
+ )
+
+ if ('error' in externalAccountResponse && externalAccountResponse.error) {
+ setError(externalAccountResponse.error)
+ return { error: externalAccountResponse.error }
+ }
+
+ if (!('id' in externalAccountResponse)) {
+ setError('Failed to create external account')
+ return { error: 'Failed to create external account' }
+ }
+
+ // note: we pass peanut contract address to offramp as the funds go from, user -> peanut contract -> bridge
+ const params = peanut.getParamsFromLink(claimLinkData.link)
+ const { address: pubKey } = peanut.generateKeysFromString(params.password)
+ const chainId = params.chainId
+ const contractVersion = params.contractVersion
+ const peanutContractAddress = peanut.getContractAddress(chainId, contractVersion) as Address
+
+ const offrampRequestParams: TCreateOfframpRequest = {
+ amount: formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals),
+ userId: userResponse.userId,
+ sendLinkPubKey: pubKey,
+ source: {
+ paymentRail: paymentRail,
+ currency: currency,
+ fromAddress: peanutContractAddress,
+ },
+ destination: {
+ ...getOfframpCurrencyConfig(selectedCountry.id),
+ externalAccountId: externalAccountResponse.id,
+ },
+ features: {
+ allowAnyFromAddress: true,
+ },
+ }
+
+ const offrampResponse = await createOfframpForGuest(offrampRequestParams)
+
+ if (offrampResponse.error || !offrampResponse.data) {
+ setError(offrampResponse.error || 'Failed to create offramp')
+ return { error: offrampResponse.error || 'Failed to create offramp' }
+ }
+
+ setOfframpDetails(offrampResponse.data as TCreateOfframpResponse)
+
+ setLocalBankDetails(rawData)
+ setBankDetails(rawData)
+ setGuestFlowStep('bank-confirm-claim')
+ return {}
+ } catch (e: any) {
+ const errorString = ErrorHandler(e)
+ setError(errorString)
+ Sentry.captureException(e)
+ return { error: errorString }
+ } finally {
+ setLoadingState('Idle')
+ }
+ }
+
+ const handleConfirmClaim = useCallback(async () => {
+ try {
+ setLoadingState('Executing transaction')
+ setError(null)
+
+ if (!offrampDetails) {
+ throw new Error('Offramp details not available')
+ }
+
+ const claimTx = await claimLink({
+ address: offrampDetails.depositInstructions.toAddress,
+ link: claimLinkData.link,
+ })
+
+ setTransactionHash(claimTx)
+ await confirmOfframp(offrampDetails.transferId, claimTx)
+ if (setClaimType) setClaimType('claim-bank')
+ onCustom('SUCCESS')
+ } catch (e: any) {
+ const errorString = ErrorHandler(e)
+ setError(errorString)
+ Sentry.captureException(e)
+ } finally {
+ setLoadingState('Idle')
+ }
+ }, [
+ offrampDetails,
+ claimLink,
+ claimLinkData.link,
+ setTransactionHash,
+ confirmOfframp,
+ setClaimType,
+ onCustom,
+ setLoadingState,
+ setError,
+ ])
+
+ if (guestFlowStep === 'bank-confirm-claim' && offrampDetails && localBankDetails) {
+ return (
+ {
+ setGuestFlowStep('bank-details-form')
+ setError(null)
+ }}
+ isProcessing={isLoading}
+ error={error}
+ bankDetails={localBankDetails}
+ fullName={receiverFullName}
+ />
+ )
+ }
+
+ if (guestFlowStep === 'bank-country-list' || !selectedCountry) {
+ return
+ }
+
+ return (
+
+
+ setGuestFlowStep('bank-country-list')} />
+
+
+
+ )
+}
diff --git a/src/components/Claim/Link/views/ClaimCountryList.view.tsx b/src/components/Claim/Link/views/ClaimCountryList.view.tsx
new file mode 100644
index 000000000..734ff32bd
--- /dev/null
+++ b/src/components/Claim/Link/views/ClaimCountryList.view.tsx
@@ -0,0 +1,103 @@
+'use client'
+import { countryCodeMap, countryData } from '@/components/AddMoney/consts'
+import EmptyState from '@/components/Global/EmptyStates/EmptyState'
+import NavHeader from '@/components/Global/NavHeader'
+import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard'
+import { SearchInput } from '@/components/SearchUsers/SearchInput'
+import { SearchResultCard } from '@/components/SearchUsers/SearchResultCard'
+import { useGuestFlow } from '@/context/GuestFlowContext'
+import Image from 'next/image'
+import { useMemo, useState } from 'react'
+import { IClaimScreenProps } from '../../Claim.consts'
+import { formatUnits } from 'viem'
+import { formatTokenAmount, printableAddress } from '@/utils/general.utils'
+
+export const ClaimCountryListView = ({ claimLinkData }: Pick) => {
+ const { setGuestFlowStep, setSelectedCountry, resetGuestFlow } = useGuestFlow()
+ const [searchTerm, setSearchTerm] = useState('')
+
+ const supportedCountries = useMemo(() => {
+ const sepaCountries = Object.keys(countryCodeMap)
+ const supported = new Set([...sepaCountries, 'US', 'MX'])
+
+ return countryData.filter((country) => supported.has(country.id))
+ }, [])
+
+ const filteredCountries = useMemo(() => {
+ if (!searchTerm) return supportedCountries
+
+ return supportedCountries.filter(
+ (country) =>
+ country.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ country.currency?.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ }, [searchTerm, supportedCountries])
+
+ const handleCountrySelect = (country: (typeof countryData)[0]) => {
+ setSelectedCountry(country)
+ setGuestFlowStep('bank-details-form')
+ }
+
+ return (
+
+
resetGuestFlow()} />
+
+
+
+
+
Which country do you want to receive to?
+
setSearchTerm(e.target.value)}
+ onClear={() => setSearchTerm('')}
+ placeholder="Search country or currency"
+ />
+
+ {searchTerm && filteredCountries.length === 0 ? (
+
+ ) : (
+
+ {filteredCountries.map((country, index) => {
+ const twoLetterCountryCode =
+ countryCodeMap[country.id.toUpperCase()] ?? country.id.toLowerCase()
+ return (
+
handleCountrySelect(country)}
+ position={'single'}
+ leftIcon={
+
+ {
+ e.currentTarget.style.display = 'none'
+ }}
+ />
+
+ }
+ />
+ )
+ })}
+
+ )}
+
+
+ )
+}
diff --git a/src/components/Claim/Link/views/Confirm.bank-claim.view.tsx b/src/components/Claim/Link/views/Confirm.bank-claim.view.tsx
new file mode 100644
index 000000000..7658d2a2e
--- /dev/null
+++ b/src/components/Claim/Link/views/Confirm.bank-claim.view.tsx
@@ -0,0 +1,112 @@
+'use client'
+
+import { Button } from '@/components/0_Bruddle'
+import { countryCodeMap } from '@/components/AddMoney/consts'
+import Card from '@/components/Global/Card'
+import ErrorAlert from '@/components/Global/ErrorAlert'
+import NavHeader from '@/components/Global/NavHeader'
+import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard'
+import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
+import { IBankAccountDetails } from '@/components/AddWithdraw/DynamicBankAccountForm'
+import { useMemo } from 'react'
+import { ClaimLinkData } from '@/services/sendLinks'
+import { formatUnits } from 'viem'
+import ExchangeRate from '@/components/ExchangeRate'
+import { AccountType } from '@/interfaces'
+
+interface ConfirmBankClaimViewProps {
+ onConfirm: () => void
+ onBack: () => void
+ isProcessing?: boolean
+ error?: string | null
+ bankDetails: IBankAccountDetails
+ fullName: string
+ claimLinkData: ClaimLinkData
+}
+
+export function ConfirmBankClaimView({
+ onConfirm,
+ onBack,
+ isProcessing,
+ error,
+ bankDetails,
+ fullName,
+ claimLinkData,
+}: ConfirmBankClaimViewProps) {
+ const displayedFullName = useMemo(() => {
+ if (fullName) return fullName
+ if (typeof window !== 'undefined') {
+ return sessionStorage.getItem('receiverFullName') ?? ''
+ }
+ return ''
+ }, [fullName])
+
+ const accountType = useMemo(() => {
+ if (bankDetails.iban) return AccountType.IBAN
+ if (bankDetails.clabe) return AccountType.CLABE
+ if (bankDetails.accountNumber && bankDetails.routingNumber) return AccountType.US
+ return AccountType.IBAN // Default or handle error
+ }, [bankDetails])
+
+ const countryCodeForFlag = useMemo(() => {
+ return countryCodeMap[bankDetails.country.toUpperCase()] ?? bankDetails.country.toUpperCase()
+ }, [bankDetails.country])
+
+ return (
+
+
+
+
+
+
+
+
+ {/* todo: take full name from user, this name rn is of senders */}
+
+ {bankDetails.iban && }
+ {bankDetails.bic && }
+
+
+
+
+
+ {error ? (
+
+ Retry
+
+ ) : (
+
+ Receive now
+
+ )}
+
+ {error && }
+
+
+
+ )
+}
diff --git a/src/components/Create/useCreateLink.tsx b/src/components/Create/useCreateLink.tsx
index 0fde487e6..e1b6d266e 100644
--- a/src/components/Create/useCreateLink.tsx
+++ b/src/components/Create/useCreateLink.tsx
@@ -238,48 +238,7 @@ export const useCreateLink = () => {
throw error
}
}
- const submitDirectTransfer = async ({
- txHash,
- chainId,
- senderAddress,
- amountUsd,
- transaction,
- }: {
- txHash: string
- chainId: string
- senderAddress: string
- amountUsd: number
- transaction?: peanutInterfaces.IPeanutUnsignedTransaction
- }) => {
- try {
- const response = await fetchWithSentry('/api/peanut/submit-direct-transfer', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- txHash,
- chainId,
- senderAddress: senderAddress,
- amountUsd,
- transaction: {
- ...transaction,
- value:
- transaction?.value && transaction.value !== BigInt(0)
- ? transaction.value.toString()
- : undefined,
- },
- }),
- })
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`)
- }
- } catch (error) {
- console.error('Failed to publish file (complete):', error)
- return ''
- }
- }
const prepareDirectSendTx = ({
recipient,
tokenValue,
@@ -463,7 +422,6 @@ export const useCreateLink = () => {
submitClaimLinkInit,
submitClaimLinkConfirm,
prepareDirectSendTx,
- submitDirectTransfer,
createLink,
}
}
diff --git a/src/components/ExchangeRate/index.tsx b/src/components/ExchangeRate/index.tsx
index a7c21cbf2..74464f29c 100644
--- a/src/components/ExchangeRate/index.tsx
+++ b/src/components/ExchangeRate/index.tsx
@@ -16,7 +16,6 @@ const ExchangeRate = ({ accountType }: ExchangeRateProps) => {
setIsFetchingRate(true)
try {
const { data, error: rateError } = await getExchangeRate(accountType)
- console.log('data', data)
if (rateError) {
console.error('Failed to fetch exchange rate:', rateError)
@@ -41,18 +40,16 @@ const ExchangeRate = ({ accountType }: ExchangeRateProps) => {
return
}
- if (exchangeRate) {
- return (
-
- )
- }
+ const displayValue = exchangeRate ? `1 USD = ${parseFloat(exchangeRate).toFixed(4)} ${toCurrency}` : '-'
- return null
+ return (
+
+ )
}
export default ExchangeRate
diff --git a/src/components/Global/FAQs/index.tsx b/src/components/Global/FAQs/index.tsx
index 3be551e37..20954f919 100644
--- a/src/components/Global/FAQs/index.tsx
+++ b/src/components/Global/FAQs/index.tsx
@@ -81,7 +81,7 @@ export function FAQsPanel({ heading, questions }: FAQsProps) {
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
- className="mt-1 overflow-hidden leading-6 text-n-1"
+ className="mt-1 overflow-hidden whitespace-pre-line leading-6 text-n-1"
>
{faq.answer}
{faq.calModal && (
diff --git a/src/components/Global/IconStack.tsx b/src/components/Global/IconStack.tsx
new file mode 100644
index 000000000..d08e06de4
--- /dev/null
+++ b/src/components/Global/IconStack.tsx
@@ -0,0 +1,35 @@
+import Image from 'next/image'
+import { twMerge } from 'tailwind-merge'
+
+interface IconStackProps {
+ icons: string[]
+ iconSize?: number
+ iconClassName?: string
+}
+
+const IconStack: React.FC = ({ icons, iconSize = 24, iconClassName = '' }) => {
+ return (
+
+ {icons.map((icon, index) => (
+
+
+
+ ))}
+
+ )
+}
+
+export default IconStack
diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx
index defb7eb56..e00619e56 100644
--- a/src/components/Global/Icons/Icon.tsx
+++ b/src/components/Global/Icons/Icon.tsx
@@ -49,6 +49,7 @@ import { WalletOutlineIcon } from './wallet-outline'
import { BadgeIcon } from './badge'
import { UserIdIcon } from './user-id'
import { ClockIcon } from './clock'
+import { DollarIcon } from './dollar'
// available icon names
export type IconName =
@@ -102,6 +103,7 @@ export type IconName =
| 'badge'
| 'user-id'
| 'clock'
+ | 'dollar'
export interface IconProps extends SVGProps {
name: IconName
@@ -120,6 +122,7 @@ const iconComponents: Record>> =
check: CheckIcon,
'chevron-up': ChevronUpIcon,
download: DownloadIcon,
+ dollar: DollarIcon,
eye: EyeIcon,
'eye-slash': EyeSlashIcon,
exchange: ExchangeIcon,
diff --git a/src/components/Global/Icons/dollar.tsx b/src/components/Global/Icons/dollar.tsx
new file mode 100644
index 000000000..2400b9fd8
--- /dev/null
+++ b/src/components/Global/Icons/dollar.tsx
@@ -0,0 +1,10 @@
+import { FC, SVGProps } from 'react'
+
+export const DollarIcon: FC> = (props) => (
+
+
+
+)
diff --git a/src/components/Global/IframeWrapper/index.tsx b/src/components/Global/IframeWrapper/index.tsx
index 728c3da9a..4ce8c236d 100644
--- a/src/components/Global/IframeWrapper/index.tsx
+++ b/src/components/Global/IframeWrapper/index.tsx
@@ -95,6 +95,7 @@ const IframeWrapper = ({ src, visible, onClose, closeConfirmMessage }: IFrameWra