Skip to content

Commit d137151

Browse files
Merge pull request #1602 from peanutprotocol/feat/poc-url-state
feat: url as state poc
2 parents c5d4aa9 + 278a994 commit d137151

12 files changed

Lines changed: 259 additions & 115 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"js-cookie": "^3.0.5",
6262
"jsqr": "^1.4.0",
6363
"next": "16.0.10",
64+
"nuqs": "^2.8.6",
6465
"pulltorefreshjs": "^0.1.22",
6566
"react": "^19.2.1",
6667
"react-dom": "^19.2.1",

pnpm-lock.yaml

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function AddMoneyRegionalMethodPage() {
1515
MantecaSupportedExchanges[countryDetails?.id as keyof typeof MantecaSupportedExchanges] &&
1616
method === 'manteca'
1717
) {
18-
return <MantecaAddMoney source="regionalMethod" />
18+
return <MantecaAddMoney />
1919
}
2020
return null
2121
}

src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,40 @@ import { getCurrencyConfig, getCurrencySymbol, getMinimumAmount } from '@/utils/
2525
import { OnrampConfirmationModal } from '@/components/AddMoney/components/OnrampConfirmationModal'
2626
import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal'
2727
import InfoCard from '@/components/Global/InfoCard'
28+
import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs'
2829

29-
type AddStep = 'inputAmount' | 'kyc' | 'loading' | 'collectUserDetails' | 'showDetails'
30+
// Step type for URL state
31+
type BridgeBankStep = 'inputAmount' | 'kyc' | 'collectUserDetails' | 'showDetails'
3032

3133
export default function OnrampBankPage() {
3234
const router = useRouter()
3335
const params = useParams()
34-
const [step, setStep] = useState<AddStep>('loading')
35-
const [rawTokenAmount, setRawTokenAmount] = useState<string>('')
36+
37+
// URL state - persisted in query params
38+
// Example: /add-money/mexico/bank?step=inputAmount&amount=500
39+
const [urlState, setUrlState] = useQueryStates(
40+
{
41+
step: parseAsStringEnum<BridgeBankStep>(['inputAmount', 'kyc', 'collectUserDetails', 'showDetails']),
42+
amount: parseAsString,
43+
},
44+
{ history: 'push' }
45+
)
46+
47+
// Amount from URL
48+
const rawTokenAmount = urlState.amount ?? ''
49+
50+
// Local UI state (not URL-appropriate - transient)
3651
const [showWarningModal, setShowWarningModal] = useState<boolean>(false)
3752
const [isRiskAccepted, setIsRiskAccepted] = useState<boolean>(false)
38-
3953
const [isKycModalOpen, setIsKycModalOpen] = useState(false)
4054
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(undefined)
41-
const { amountToOnramp: amountFromContext, setAmountToOnramp, setError, error, setOnrampData } = useOnrampFlow()
42-
const formRef = useRef<{ handleSubmit: () => void }>(null)
4355
const [isUpdatingUser, setIsUpdatingUser] = useState(false)
4456
const [userUpdateError, setUserUpdateError] = useState<string | null>(null)
4557
const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false)
4658

59+
const { setError, error, setOnrampData, onrampData } = useOnrampFlow()
60+
const formRef = useRef<{ handleSubmit: () => void }>(null)
61+
4762
const { balance } = useWallet()
4863
const { user, fetchUser } = useAuth()
4964
const { createOnramp, isLoading: isCreatingOnramp, error: onrampError } = useCreateOnramp()
@@ -82,32 +97,30 @@ export default function OnrampBankPage() {
8297
return getMinimumAmount(selectedCountry.id)
8398
}, [selectedCountry?.id])
8499

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

91-
if (!isUserKycVerified) {
92-
setStep('collectUserDetails')
93-
} else {
94-
setStep('inputAmount')
95-
if (amountFromContext && !rawTokenAmount) {
96-
setRawTokenAmount(amountFromContext)
97-
}
98-
}
105+
// Wait for user to be fetched before determining initial step
106+
if (user === null) return
107+
108+
const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus
109+
const isUserKycVerified = currentKycStatus === 'approved'
110+
111+
if (!isUserKycVerified) {
112+
setUrlState({ step: 'collectUserDetails' })
113+
} else {
114+
setUrlState({ step: 'inputAmount' })
99115
}
100-
}, [liveKycStatus, user, step, amountFromContext, rawTokenAmount])
116+
}, [liveKycStatus, user, urlState.step, setUrlState])
101117

102118
// Handle KYC completion
103119
useEffect(() => {
104-
if (step === 'kyc' && liveKycStatus === 'approved') {
105-
setStep('inputAmount')
106-
if (amountFromContext && !rawTokenAmount) {
107-
setRawTokenAmount(amountFromContext)
108-
}
120+
if (urlState.step === 'kyc' && liveKycStatus === 'approved') {
121+
setUrlState({ step: 'inputAmount' })
109122
}
110-
}, [liveKycStatus, step, amountFromContext, rawTokenAmount])
123+
}, [liveKycStatus, urlState.step, setUrlState])
111124

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

146+
// Handle amount change - sync to URL state
133147
const handleTokenAmountChange = useCallback(
134148
(value: string | undefined) => {
135-
setRawTokenAmount(value || '')
149+
const newAmount = value || null // null removes from URL
150+
setUrlState({ amount: newAmount })
136151
},
137-
[setRawTokenAmount]
152+
[setUrlState]
138153
)
139154

155+
// Validate amount when it changes
140156
useEffect(() => {
141157
if (rawTokenAmount === '') {
142-
if (!amountFromContext) {
143-
setError({ showError: false, errorMessage: '' })
144-
}
158+
setError({ showError: false, errorMessage: '' })
145159
} else {
146160
validateAmount(rawTokenAmount)
147161
}
148-
}, [rawTokenAmount, validateAmount, setError, amountFromContext])
162+
}, [rawTokenAmount, validateAmount, setError])
149163

150164
const handleAmountContinue = () => {
151165
if (validateAmount(rawTokenAmount)) {
@@ -161,7 +175,6 @@ export default function OnrampBankPage() {
161175
})
162176
return
163177
}
164-
setAmountToOnramp(rawTokenAmount)
165178
setShowWarningModal(false)
166179
setIsRiskAccepted(false)
167180
try {
@@ -172,7 +185,7 @@ export default function OnrampBankPage() {
172185
setOnrampData(onrampDataResponse)
173186

174187
if (onrampDataResponse.transferId) {
175-
setStep('showDetails')
188+
setUrlState({ step: 'showDetails' })
176189
} else {
177190
setError({
178191
showError: true,
@@ -195,13 +208,9 @@ export default function OnrampBankPage() {
195208
setIsRiskAccepted(false)
196209
}
197210

198-
const handleKycModalOpen = () => {
199-
setIsKycModalOpen(true)
200-
}
201-
202211
const handleKycSuccess = () => {
203212
setIsKycModalOpen(false)
204-
setStep('inputAmount')
213+
setUrlState({ step: 'inputAmount' })
205214
}
206215

207216
const handleKycModalClose = () => {
@@ -222,7 +231,7 @@ export default function OnrampBankPage() {
222231
throw new Error(result.error)
223232
}
224233
await fetchUser()
225-
setStep('kyc')
234+
setUrlState({ step: 'kyc' })
226235
} catch (error: any) {
227236
setUserUpdateError(error.message)
228237
return { error: error.message }
@@ -240,24 +249,29 @@ export default function OnrampBankPage() {
240249
}
241250
}
242251

243-
const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ')
244-
const lastName = lastNameParts.join(' ')
245-
246252
const initialUserDetails: Partial<UserDetailsFormData> = useMemo(
247253
() => ({
248254
fullName: user?.user.fullName ?? '',
249255
email: user?.user.email ?? '',
250256
}),
251-
[user?.user.fullName, user?.user.email, firstName, lastName]
257+
[user?.user.fullName, user?.user.email]
252258
)
253259

254260
useEffect(() => {
255-
if (step === 'kyc') {
261+
if (urlState.step === 'kyc') {
256262
setIsKycModalOpen(true)
257263
}
258-
}, [step])
264+
}, [urlState.step])
259265

260-
if (step === 'loading') {
266+
// Redirect to inputAmount if showDetails is accessed without required data (deep link / back navigation)
267+
useEffect(() => {
268+
if (urlState.step === 'showDetails' && !onrampData?.transferId) {
269+
setUrlState({ step: 'inputAmount' })
270+
}
271+
}, [urlState.step, onrampData?.transferId, setUrlState])
272+
273+
// Show loading while user is being fetched and no step in URL yet
274+
if (!urlState.step && user === null) {
261275
return <PeanutLoading />
262276
}
263277

@@ -270,7 +284,12 @@ export default function OnrampBankPage() {
270284
)
271285
}
272286

273-
if (step === 'collectUserDetails') {
287+
// Still determining initial step
288+
if (!urlState.step) {
289+
return <PeanutLoading />
290+
}
291+
292+
if (urlState.step === 'collectUserDetails') {
274293
return (
275294
<div className="flex flex-col justify-start space-y-8">
276295
<NavHeader onPrev={handleBack} title="Identity Verification" />
@@ -299,7 +318,7 @@ export default function OnrampBankPage() {
299318
)
300319
}
301320

302-
if (step === 'kyc') {
321+
if (urlState.step === 'kyc') {
303322
return (
304323
<div className="flex flex-col justify-start space-y-8">
305324
<InitiateBridgeKYCModal
@@ -313,11 +332,15 @@ export default function OnrampBankPage() {
313332
)
314333
}
315334

316-
if (step === 'showDetails') {
335+
if (urlState.step === 'showDetails') {
336+
// Show loading while useEffect redirects if data is missing
337+
if (!onrampData?.transferId) {
338+
return <PeanutLoading />
339+
}
317340
return <AddMoneyBankDetails />
318341
}
319342

320-
if (step === 'inputAmount') {
343+
if (urlState.step === 'inputAmount') {
321344
return (
322345
<div className="flex flex-col justify-start space-y-8">
323346
<NavHeader title="Add Money" onPrev={handleBack} />

src/app/ClientProviders.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,22 @@ import { TranslationSafeWrapper } from '@/components/Global/TranslationSafeWrapp
1212
import { PeanutProvider } from '@/config'
1313
import { ContextProvider } from '@/context'
1414
import { FooterVisibilityProvider } from '@/context/footerVisibility'
15+
import { NuqsAdapter } from 'nuqs/adapters/next/app'
1516

1617
export function ClientProviders({ children }: { children: React.ReactNode }) {
1718
return (
18-
<PeanutProvider>
19-
<ContextProvider>
20-
<FooterVisibilityProvider>
21-
<TranslationSafeWrapper>
22-
<ConsoleGreeting />
23-
<ScreenOrientationLocker />
24-
{children}
25-
</TranslationSafeWrapper>
26-
</FooterVisibilityProvider>
27-
</ContextProvider>
28-
</PeanutProvider>
19+
<NuqsAdapter>
20+
<PeanutProvider>
21+
<ContextProvider>
22+
<FooterVisibilityProvider>
23+
<TranslationSafeWrapper>
24+
<ConsoleGreeting />
25+
<ScreenOrientationLocker />
26+
{children}
27+
</TranslationSafeWrapper>
28+
</FooterVisibilityProvider>
29+
</ContextProvider>
30+
</PeanutProvider>
31+
</NuqsAdapter>
2932
)
3033
}

src/components/AddMoney/components/AddMoneyBankDetails.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import InfoCard from '@/components/Global/InfoCard'
1717
import CopyToClipboard from '@/components/Global/CopyToClipboard'
1818
import { Button } from '@/components/0_Bruddle/Button'
1919
import { useExchangeRate } from '@/hooks/useExchangeRate'
20+
import { useQueryState, parseAsString } from 'nuqs'
2021

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

29+
// URL state - read amount from URL query params
30+
const [amountFromUrl] = useQueryState('amount', parseAsString)
31+
2832
// contexts
2933
const onrampContext = useOnrampFlow()
3034
const {
@@ -72,10 +76,9 @@ export default function AddMoneyBankDetails({ flow = 'add-money' }: IAddMoneyBan
7276
enabled: true,
7377
})
7478

75-
// data from contexts based on flow
76-
const amount = isAddMoneyFlow
77-
? onrampContext.amountToOnramp
78-
: requestFulfilmentOnrampData?.depositInstructions?.amount
79+
// data from URL state (add-money flow) or context (request-fulfillment flow)
80+
// For add-money flow, amount is now in URL state via nuqs
81+
const amount = isAddMoneyFlow ? (amountFromUrl ?? '') : requestFulfilmentOnrampData?.depositInstructions?.amount
7982
const onrampData = isAddMoneyFlow ? onrampContext.onrampData : requestFulfilmentOnrampData
8083

8184
const currencySymbolBasedOnCountry = useMemo(() => {

0 commit comments

Comments
 (0)