Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"js-cookie": "^3.0.5",
"jsqr": "^1.4.0",
"next": "16.0.10",
"nuqs": "^2.8.6",
"pulltorefreshjs": "^0.1.22",
"react": "^19.2.1",
"react-dom": "^19.2.1",
Expand Down
31 changes: 31 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function AddMoneyRegionalMethodPage() {
MantecaSupportedExchanges[countryDetails?.id as keyof typeof MantecaSupportedExchanges] &&
method === 'manteca'
) {
return <MantecaAddMoney source="regionalMethod" />
return <MantecaAddMoney />
}
return null
}
123 changes: 73 additions & 50 deletions src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,40 @@ import { getCurrencyConfig, getCurrencySymbol, getMinimumAmount } from '@/utils/
import { OnrampConfirmationModal } from '@/components/AddMoney/components/OnrampConfirmationModal'
import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal'
import InfoCard from '@/components/Global/InfoCard'
import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs'

type AddStep = 'inputAmount' | 'kyc' | 'loading' | 'collectUserDetails' | 'showDetails'
// Step type for URL state
type BridgeBankStep = 'inputAmount' | 'kyc' | 'collectUserDetails' | 'showDetails'

export default function OnrampBankPage() {
const router = useRouter()
const params = useParams()
const [step, setStep] = useState<AddStep>('loading')
const [rawTokenAmount, setRawTokenAmount] = useState<string>('')

// URL state - persisted in query params
// Example: /add-money/mexico/bank?step=inputAmount&amount=500
const [urlState, setUrlState] = useQueryStates(
{
step: parseAsStringEnum<BridgeBankStep>(['inputAmount', 'kyc', 'collectUserDetails', 'showDetails']),
amount: parseAsString,
},
{ history: 'push' }
)

// Amount from URL
const rawTokenAmount = urlState.amount ?? ''

// Local UI state (not URL-appropriate - transient)
const [showWarningModal, setShowWarningModal] = useState<boolean>(false)
const [isRiskAccepted, setIsRiskAccepted] = useState<boolean>(false)

const [isKycModalOpen, setIsKycModalOpen] = useState(false)
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(undefined)
const { amountToOnramp: amountFromContext, setAmountToOnramp, setError, error, setOnrampData } = useOnrampFlow()
const formRef = useRef<{ handleSubmit: () => void }>(null)
const [isUpdatingUser, setIsUpdatingUser] = useState(false)
const [userUpdateError, setUserUpdateError] = useState<string | null>(null)
const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false)

const { setError, error, setOnrampData, onrampData } = useOnrampFlow()
const formRef = useRef<{ handleSubmit: () => void }>(null)

const { balance } = useWallet()
const { user, fetchUser } = useAuth()
const { createOnramp, isLoading: isCreatingOnramp, error: onrampError } = useCreateOnramp()
Expand Down Expand Up @@ -82,32 +97,30 @@ export default function OnrampBankPage() {
return getMinimumAmount(selectedCountry.id)
}, [selectedCountry?.id])

// Determine initial step based on KYC status (only when URL has no step)
useEffect(() => {
if (user === null) return // wait for user to be fetched
if (step === 'loading') {
const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus
const isUserKycVerified = currentKycStatus === 'approved'
// If URL already has a step, respect it (allows deep linking)
if (urlState.step) return

if (!isUserKycVerified) {
setStep('collectUserDetails')
} else {
setStep('inputAmount')
if (amountFromContext && !rawTokenAmount) {
setRawTokenAmount(amountFromContext)
}
}
// Wait for user to be fetched before determining initial step
if (user === null) return

const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus
const isUserKycVerified = currentKycStatus === 'approved'

if (!isUserKycVerified) {
setUrlState({ step: 'collectUserDetails' })
} else {
setUrlState({ step: 'inputAmount' })
}
}, [liveKycStatus, user, step, amountFromContext, rawTokenAmount])
}, [liveKycStatus, user, urlState.step, setUrlState])

// Handle KYC completion
useEffect(() => {
if (step === 'kyc' && liveKycStatus === 'approved') {
setStep('inputAmount')
if (amountFromContext && !rawTokenAmount) {
setRawTokenAmount(amountFromContext)
}
if (urlState.step === 'kyc' && liveKycStatus === 'approved') {
setUrlState({ step: 'inputAmount' })
}
}, [liveKycStatus, step, amountFromContext, rawTokenAmount])
}, [liveKycStatus, urlState.step, setUrlState])

const validateAmount = useCallback(
(amountStr: string): boolean => {
Expand All @@ -130,22 +143,23 @@ export default function OnrampBankPage() {
[setError, minimumAmount]
)

// Handle amount change - sync to URL state
const handleTokenAmountChange = useCallback(
(value: string | undefined) => {
setRawTokenAmount(value || '')
const newAmount = value || null // null removes from URL
setUrlState({ amount: newAmount })
},
[setRawTokenAmount]
[setUrlState]
)

// Validate amount when it changes
useEffect(() => {
if (rawTokenAmount === '') {
if (!amountFromContext) {
setError({ showError: false, errorMessage: '' })
}
setError({ showError: false, errorMessage: '' })
} else {
validateAmount(rawTokenAmount)
}
}, [rawTokenAmount, validateAmount, setError, amountFromContext])
}, [rawTokenAmount, validateAmount, setError])

const handleAmountContinue = () => {
if (validateAmount(rawTokenAmount)) {
Expand All @@ -161,7 +175,6 @@ export default function OnrampBankPage() {
})
return
}
setAmountToOnramp(rawTokenAmount)
setShowWarningModal(false)
setIsRiskAccepted(false)
try {
Expand All @@ -172,7 +185,7 @@ export default function OnrampBankPage() {
setOnrampData(onrampDataResponse)

if (onrampDataResponse.transferId) {
setStep('showDetails')
setUrlState({ step: 'showDetails' })
} else {
setError({
showError: true,
Expand All @@ -195,13 +208,9 @@ export default function OnrampBankPage() {
setIsRiskAccepted(false)
}

const handleKycModalOpen = () => {
setIsKycModalOpen(true)
}

const handleKycSuccess = () => {
setIsKycModalOpen(false)
setStep('inputAmount')
setUrlState({ step: 'inputAmount' })
}

const handleKycModalClose = () => {
Expand All @@ -222,7 +231,7 @@ export default function OnrampBankPage() {
throw new Error(result.error)
}
await fetchUser()
setStep('kyc')
setUrlState({ step: 'kyc' })
} catch (error: any) {
setUserUpdateError(error.message)
return { error: error.message }
Expand All @@ -240,24 +249,29 @@ export default function OnrampBankPage() {
}
}

const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ')
const lastName = lastNameParts.join(' ')

const initialUserDetails: Partial<UserDetailsFormData> = useMemo(
() => ({
fullName: user?.user.fullName ?? '',
email: user?.user.email ?? '',
}),
[user?.user.fullName, user?.user.email, firstName, lastName]
[user?.user.fullName, user?.user.email]
)

useEffect(() => {
if (step === 'kyc') {
if (urlState.step === 'kyc') {
setIsKycModalOpen(true)
}
}, [step])
}, [urlState.step])

if (step === 'loading') {
// Redirect to inputAmount if showDetails is accessed without required data (deep link / back navigation)
useEffect(() => {
if (urlState.step === 'showDetails' && !onrampData?.transferId) {
setUrlState({ step: 'inputAmount' })
}
}, [urlState.step, onrampData?.transferId, setUrlState])

// Show loading while user is being fetched and no step in URL yet
if (!urlState.step && user === null) {
return <PeanutLoading />
}

Expand All @@ -270,7 +284,12 @@ export default function OnrampBankPage() {
)
}

if (step === 'collectUserDetails') {
// Still determining initial step
if (!urlState.step) {
return <PeanutLoading />
}

if (urlState.step === 'collectUserDetails') {
return (
<div className="flex flex-col justify-start space-y-8">
<NavHeader onPrev={handleBack} title="Identity Verification" />
Expand Down Expand Up @@ -299,7 +318,7 @@ export default function OnrampBankPage() {
)
}

if (step === 'kyc') {
if (urlState.step === 'kyc') {
return (
<div className="flex flex-col justify-start space-y-8">
<InitiateBridgeKYCModal
Expand All @@ -313,11 +332,15 @@ export default function OnrampBankPage() {
)
}

if (step === 'showDetails') {
if (urlState.step === 'showDetails') {
// Show loading while useEffect redirects if data is missing
if (!onrampData?.transferId) {
return <PeanutLoading />
}
return <AddMoneyBankDetails />
}

if (step === 'inputAmount') {
if (urlState.step === 'inputAmount') {
return (
<div className="flex flex-col justify-start space-y-8">
<NavHeader title="Add Money" onPrev={handleBack} />
Expand Down
25 changes: 14 additions & 11 deletions src/app/ClientProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ import { TranslationSafeWrapper } from '@/components/Global/TranslationSafeWrapp
import { PeanutProvider } from '@/config'
import { ContextProvider } from '@/context'
import { FooterVisibilityProvider } from '@/context/footerVisibility'
import { NuqsAdapter } from 'nuqs/adapters/next/app'

export function ClientProviders({ children }: { children: React.ReactNode }) {
return (
<PeanutProvider>
<ContextProvider>
<FooterVisibilityProvider>
<TranslationSafeWrapper>
<ConsoleGreeting />
<ScreenOrientationLocker />
{children}
</TranslationSafeWrapper>
</FooterVisibilityProvider>
</ContextProvider>
</PeanutProvider>
<NuqsAdapter>
<PeanutProvider>
<ContextProvider>
<FooterVisibilityProvider>
<TranslationSafeWrapper>
<ConsoleGreeting />
<ScreenOrientationLocker />
{children}
</TranslationSafeWrapper>
</FooterVisibilityProvider>
</ContextProvider>
</PeanutProvider>
</NuqsAdapter>
)
}
11 changes: 7 additions & 4 deletions src/components/AddMoney/components/AddMoneyBankDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import InfoCard from '@/components/Global/InfoCard'
import CopyToClipboard from '@/components/Global/CopyToClipboard'
import { Button } from '@/components/0_Bruddle/Button'
import { useExchangeRate } from '@/hooks/useExchangeRate'
import { useQueryState, parseAsString } from 'nuqs'

interface IAddMoneyBankDetails {
flow?: 'add-money' | 'request-fulfillment'
Expand All @@ -25,6 +26,9 @@ interface IAddMoneyBankDetails {
export default function AddMoneyBankDetails({ flow = 'add-money' }: IAddMoneyBankDetails) {
const isAddMoneyFlow = flow === 'add-money'

// URL state - read amount from URL query params
const [amountFromUrl] = useQueryState('amount', parseAsString)

// contexts
const onrampContext = useOnrampFlow()
const {
Expand Down Expand Up @@ -72,10 +76,9 @@ export default function AddMoneyBankDetails({ flow = 'add-money' }: IAddMoneyBan
enabled: true,
})

// data from contexts based on flow
const amount = isAddMoneyFlow
? onrampContext.amountToOnramp
: requestFulfilmentOnrampData?.depositInstructions?.amount
// data from URL state (add-money flow) or context (request-fulfillment flow)
// For add-money flow, amount is now in URL state via nuqs
const amount = isAddMoneyFlow ? (amountFromUrl ?? '') : requestFulfilmentOnrampData?.depositInstructions?.amount
const onrampData = isAddMoneyFlow ? onrampContext.onrampData : requestFulfilmentOnrampData

const currencySymbolBasedOnCountry = useMemo(() => {
Expand Down
Loading
Loading