From f09ff30ee18147b12c6d9604ba0aebe4181e198f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Wed, 31 Dec 2025 11:01:34 -0300 Subject: [PATCH 1/4] feat: url as stat poc --- package.json | 1 + pnpm-lock.yaml | 31 ++ .../add-money/[country]/bank/page.tsx | 112 +++---- src/app/(mobile-ui)/points/page.tsx.orig | 278 ++++++++++++++++++ src/app/ClientProviders.tsx | 25 +- .../components/AddMoneyBankDetails.tsx | 11 +- .../AddMoney/components/InputAmountStep.tsx | 2 +- .../AddMoney/components/MantecaAddMoney.tsx | 73 ++++- .../AddMoney/views/RhinoDeposit.view.tsx | 3 +- src/context/OnrampFlowContext.tsx | 32 +- 10 files changed, 475 insertions(+), 93 deletions(-) create mode 100644 src/app/(mobile-ui)/points/page.tsx.orig diff --git a/package.json b/package.json index c5f91a71b..f2a55c3cf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70dc59441..ec4587d8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: next: specifier: 16.0.10 version: 16.0.10(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + nuqs: + specifier: ^2.8.6 + version: 2.8.6(next@16.0.10(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) pulltorefreshjs: specifier: ^0.1.22 version: 0.1.22 @@ -5908,6 +5911,27 @@ packages: nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + nuqs@2.8.6: + resolution: {integrity: sha512-aRxeX68b4ULmhio8AADL2be1FWDy0EPqaByPvIYWrA7Pm07UjlrICp/VPlSnXJNAG0+3MQwv3OporO2sOXMVGA==} + peerDependencies: + '@remix-run/react': '>=2' + '@tanstack/react-router': ^1 + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^5 || ^6 || ^7 + react-router-dom: ^5 || ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@tanstack/react-router': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + nwsapi@2.2.21: resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} @@ -15623,6 +15647,13 @@ snapshots: nullthrows@1.1.1: {} + nuqs@2.8.6(next@16.0.10(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1): + dependencies: + '@standard-schema/spec': 1.0.0 + react: 19.2.1 + optionalDependencies: + next: 16.0.10(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + nwsapi@2.2.21: {} ob1@0.83.2: diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index 1006dc888..d7a3d7b66 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -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('loading') - const [rawTokenAmount, setRawTokenAmount] = useState('') + + // URL state - persisted in query params + // Example: /add-money/mexico/bank?step=inputAmount&amount=500 + const [urlState, setUrlState] = useQueryStates( + { + step: parseAsStringEnum(['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(false) const [isRiskAccepted, setIsRiskAccepted] = useState(false) - const [isKycModalOpen, setIsKycModalOpen] = useState(false) const [liveKycStatus, setLiveKycStatus] = useState(undefined) - const { amountToOnramp: amountFromContext, setAmountToOnramp, setError, error, setOnrampData } = useOnrampFlow() - const formRef = useRef<{ handleSubmit: () => void }>(null) const [isUpdatingUser, setIsUpdatingUser] = useState(false) const [userUpdateError, setUserUpdateError] = useState(null) const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false) + const { setError, error, setOnrampData } = useOnrampFlow() + const formRef = useRef<{ handleSubmit: () => void }>(null) + const { balance } = useWallet() const { user, fetchUser } = useAuth() const { createOnramp, isLoading: isCreatingOnramp, error: onrampError } = useCreateOnramp() @@ -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 => { @@ -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)) { @@ -161,7 +175,6 @@ export default function OnrampBankPage() { }) return } - setAmountToOnramp(rawTokenAmount) setShowWarningModal(false) setIsRiskAccepted(false) try { @@ -172,7 +185,7 @@ export default function OnrampBankPage() { setOnrampData(onrampDataResponse) if (onrampDataResponse.transferId) { - setStep('showDetails') + setUrlState({ step: 'showDetails' }) } else { setError({ showError: true, @@ -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 = () => { @@ -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 } @@ -240,24 +249,22 @@ export default function OnrampBankPage() { } } - const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ') - const lastName = lastNameParts.join(' ') - const initialUserDetails: Partial = 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') { + // Show loading while user is being fetched and no step in URL yet + if (!urlState.step && user === null) { return } @@ -270,7 +277,12 @@ export default function OnrampBankPage() { ) } - if (step === 'collectUserDetails') { + // Still determining initial step + if (!urlState.step) { + return + } + + if (urlState.step === 'collectUserDetails') { return (
@@ -299,7 +311,7 @@ export default function OnrampBankPage() { ) } - if (step === 'kyc') { + if (urlState.step === 'kyc') { return (
} - if (step === 'inputAmount') { + if (urlState.step === 'inputAmount') { return (
diff --git a/src/app/(mobile-ui)/points/page.tsx.orig b/src/app/(mobile-ui)/points/page.tsx.orig new file mode 100644 index 000000000..55c04719b --- /dev/null +++ b/src/app/(mobile-ui)/points/page.tsx.orig @@ -0,0 +1,278 @@ +'use client' + +import PageContainer from '@/components/0_Bruddle/PageContainer' +import Card, { getCardPosition } from '@/components/Global/Card' +import CopyToClipboard from '@/components/Global/CopyToClipboard' +import { Icon } from '@/components/Global/Icons/Icon' +import NavHeader from '@/components/Global/NavHeader' +import NavigationArrow from '@/components/Global/NavigationArrow' +import PeanutLoading from '@/components/Global/PeanutLoading' +import ShareButton from '@/components/Global/ShareButton' +import TransactionAvatarBadge from '@/components/TransactionDetails/TransactionAvatarBadge' +import { VerifiedUserLabel } from '@/components/UserHeader' +import { useAuth } from '@/context/authContext' +import { invitesApi } from '@/services/invites' +import { generateInviteCodeLink, generateInvitesShareText, getInitialsFromName } from '@/utils/general.utils' +import { useQuery } from '@tanstack/react-query' +import { useRouter } from 'next/navigation' +import { STAR_STRAIGHT_ICON, TIER_0_BADGE, TIER_1_BADGE, TIER_2_BADGE, TIER_3_BADGE } from '@/assets' +import Image from 'next/image' +import { pointsApi } from '@/services/points' +import EmptyState from '@/components/Global/EmptyStates/EmptyState' +import { type PointsInvite } from '@/services/services.types' +import { useEffect } from 'react' +import InvitesGraph from '@/components/Global/InvitesGraph' + +const PointsPage = () => { + const router = useRouter() + const { user, fetchUser } = useAuth() + + const getTierBadge = (tier: number) => { + const badges = [TIER_0_BADGE, TIER_1_BADGE, TIER_2_BADGE, TIER_3_BADGE] + return badges[tier] || TIER_0_BADGE + } + const { + data: invites, + isLoading, + isError: isInvitesError, + error: invitesError, + } = useQuery({ + queryKey: ['invites', user?.user.userId], + queryFn: () => invitesApi.getInvites(), + enabled: !!user?.user.userId, + }) + + const { + data: tierInfo, + isLoading: isTierInfoLoading, + isError: isTierInfoError, + error: tierInfoError, + } = useQuery({ + queryKey: ['tierInfo', user?.user.userId], + queryFn: () => pointsApi.getTierInfo(), + enabled: !!user?.user.userId, + }) + + const { data: myGraphResult } = useQuery({ + queryKey: ['myInviteGraph', user?.user.userId], + queryFn: () => pointsApi.getUserInvitesGraph(), + enabled: + !!user?.user.userId && user?.user?.badges?.some((badge) => badge.code === 'SEEDLING_DEVCONNECT_BA_2025'), + }) + const username = user?.user.username + const { inviteCode, inviteLink } = generateInviteCodeLink(username ?? '') + + useEffect(() => { + // Re-fetch user to get the latest invitees list for showing heart Icon + fetchUser() + }, []) + + if (isLoading || isTierInfoLoading || !tierInfo?.data) { + return + } + + if (isInvitesError || isTierInfoError) { + console.error('Error loading points data:', invitesError ?? tierInfoError) + + return ( +
+ +
+ ) + } + + return ( + + router.back()} /> + +
+ +
+ star +

+ {tierInfo.data.totalPoints} {tierInfo.data.totalPoints === 1 ? 'Point' : 'Points'} +

+
+ + {/* Progressive progress bar */} +
+ {`Tier +
+
= 2 + ? 100 + : Math.pow( + Math.min( + 1, + tierInfo.data.nextTierThreshold > 0 + ? tierInfo.data.totalPoints / tierInfo.data.nextTierThreshold + : 0 + ), + 0.6 + ) * 100 + }%`, + }} + /> +
+ {tierInfo?.data.currentTier < 2 && ( + {`Tier + )} +
+ +
+

You're at tier {tierInfo?.data.currentTier}.

+ {tierInfo?.data.currentTier < 2 ? ( +

+ {tierInfo.data.pointsToNextTier}{' '} + {tierInfo.data.pointsToNextTier === 1 ? 'point' : 'points'} needed to level up +

+ ) : ( +

You've reached the max tier!

+ )} +
+ + {user?.invitedBy ? ( +

+ router.push(`/${user.invitedBy}`)} + className="inline-flex cursor-pointer items-center gap-1 font-bold" + > + {user.invitedBy} + {' '} + invited you and earned points. Now it's your turn! Invite friends and get 20% of their points. +

+ ) : ( +
+ +

+ Do stuff on Peanut and get points. Invite friends and pocket 20% of their points, too. +

+
+ )} + +

Invite friends with your code

+
+ +

{`${inviteCode}`}

+ +
+
+ + {invites && invites?.invitees && invites.invitees.length > 0 && ( + <> + Promise.resolve(generateInvitesShareText(inviteLink))} + title="Share your invite link" + > + Share Invite link + +
router.push('/points/invites')} + > +

People you invited

+ +
+ + {/* Invite Graph */} + {myGraphResult?.data && ( + <> + + + +
+ +

+ Experimental. Only available for Seedlings badge holders. +

+
+ + )} + +
+ {invites.invitees?.map((invite: PointsInvite, i: number) => { + const username = invite.username + const fullName = invite.fullName + const isVerified = invite.kycStatus === 'approved' + const pointsEarned = Math.floor(invite.totalPoints * 0.2) + // respect user's showFullName preference for avatar and display name + const displayName = invite.showFullName && fullName ? fullName : username + return ( + router.push(`/${username}`)} + className="cursor-pointer" + > +
+
+ +
+
+ +
+

+ +{pointsEarned} {pointsEarned === 1 ? 'pt' : 'pts'} +

+
+
+ ) + })} +
+ + )} + + {invites?.invitees?.length === 0 && ( + +
+ +
+

No invites yet

+ +

+ Send your invite link to start earning more rewards +

+ Promise.resolve(generateInvitesShareText(inviteLink))} + title="Share your invite link" + > + Share Invite link + +
+ )} +
+
+ ) +} + +export default PointsPage diff --git a/src/app/ClientProviders.tsx b/src/app/ClientProviders.tsx index 0f2e846fb..a6d4959cc 100644 --- a/src/app/ClientProviders.tsx +++ b/src/app/ClientProviders.tsx @@ -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 ( - - - - - - - {children} - - - - + + + + + + + + {children} + + + + + ) } diff --git a/src/components/AddMoney/components/AddMoneyBankDetails.tsx b/src/components/AddMoney/components/AddMoneyBankDetails.tsx index c4eae89c5..d944274c1 100644 --- a/src/components/AddMoney/components/AddMoneyBankDetails.tsx +++ b/src/components/AddMoney/components/AddMoneyBankDetails.tsx @@ -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' @@ -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 { @@ -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(() => { diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx index f91c2a20d..a58122874 100644 --- a/src/components/AddMoney/components/InputAmountStep.tsx +++ b/src/components/AddMoney/components/InputAmountStep.tsx @@ -14,7 +14,7 @@ interface InputAmountStepProps { onSubmit: () => void isLoading: boolean tokenAmount: string - setTokenAmount: React.Dispatch> + setTokenAmount: ((value: string) => void) | React.Dispatch> error: string | null setCurrencyAmount: (amount: string | undefined) => void currencyData?: ICurrency diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx index b80e0db24..87fec1cf1 100644 --- a/src/components/AddMoney/components/MantecaAddMoney.tsx +++ b/src/components/AddMoney/components/MantecaAddMoney.tsx @@ -17,25 +17,45 @@ import useKycStatus from '@/hooks/useKycStatus' import { MAX_MANTECA_DEPOSIT_AMOUNT, MIN_MANTECA_DEPOSIT_AMOUNT } from '@/constants/payment.consts' import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { TRANSACTIONS } from '@/constants/query.consts' +import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs' + +// Step type for URL state +type MantecaStep = 'inputAmount' | 'depositDetails' + +// Currency denomination type for URL state +type CurrencyDenomination = 'USD' | 'ARS' | 'BRL' | 'MXN' | 'EUR' interface MantecaAddMoneyProps { source: 'bank' | 'regionalMethod' } -type stepType = 'inputAmount' | 'depositDetails' - const MantecaAddMoney: FC = ({ source }) => { const params = useParams() const router = useRouter() - const [step, setStep] = useState('inputAmount') + const queryClient = useQueryClient() + + // URL state - persisted in query params + // Example: /add-money/argentina/manteca?step=inputAmount&amount=100¤cy=USD + const [urlState, setUrlState] = useQueryStates( + { + step: parseAsStringEnum(['inputAmount', 'depositDetails']), + amount: parseAsString, + currency: parseAsStringEnum(['USD', 'ARS', 'BRL', 'MXN', 'EUR']), + }, + { history: 'push' } + ) + + // Derive state from URL (with defaults) + const step: MantecaStep = urlState.step ?? 'inputAmount' + const usdAmount = urlState.amount ?? '' + const currentDenomination = urlState.currency ?? 'USD' + + // Local UI state (not URL-appropriate - transient or API responses) const [isCreatingDeposit, setIsCreatingDeposit] = useState(false) const [currencyAmount, setCurrencyAmount] = useState() - const [currentDenomination, setCurrentDenomination] = useState('USD') - const [usdAmount, setUsdAmount] = useState('') const [error, setError] = useState(null) const [depositDetails, setDepositDetails] = useState() const [isKycModalOpen, setIsKycModalOpen] = useState(false) - const queryClient = useQueryClient() const selectedCountryPath = params.country as string const selectedCountry = useMemo(() => { @@ -58,6 +78,7 @@ const MantecaAddMoney: FC = ({ source }) => { }, }) + // Validate amount when it changes useEffect(() => { if (!usdAmount || usdAmount === '0.00') { setError(null) @@ -73,11 +94,12 @@ const MantecaAddMoney: FC = ({ source }) => { } }, [usdAmount]) + // Invalidate transactions query when entering deposit details step useEffect(() => { if (step === 'depositDetails') { queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] }) } - }, [step]) + }, [step, queryClient]) const handleKycCancel = () => { setIsKycModalOpen(false) @@ -86,6 +108,26 @@ const MantecaAddMoney: FC = ({ source }) => { } } + // Handle amount change - sync to URL state + const handleAmountChange = useCallback( + (value: string) => { + setUrlState({ amount: value || null }) // null removes from URL + }, + [setUrlState] + ) + + // Handle currency denomination change - sync to URL state + const handleDenominationChange = useCallback( + (value: string) => { + // Only persist valid currency denominations to URL + const validCurrencies: CurrencyDenomination[] = ['USD', 'ARS', 'BRL', 'MXN', 'EUR'] + if (validCurrencies.includes(value as CurrencyDenomination)) { + setUrlState({ currency: value as CurrencyDenomination }) + } + }, + [setUrlState] + ) + const handleAmountSubmit = useCallback(async () => { if (!selectedCountry?.currency) return if (isCreatingDeposit) return @@ -116,14 +158,23 @@ const MantecaAddMoney: FC = ({ source }) => { return } setDepositDetails(depositData.data) - setStep('depositDetails') + // Update URL state to show deposit details step + setUrlState({ step: 'depositDetails' }) } catch (error) { console.log(error) setError(error instanceof Error ? error.message : String(error)) } finally { setIsCreatingDeposit(false) } - }, [currentDenomination, selectedCountry, usdAmount, currencyAmount]) + }, [ + currentDenomination, + selectedCountry, + usdAmount, + currencyAmount, + isMantecaKycRequired, + isCreatingDeposit, + setUrlState, + ]) // handle verification modal opening useEffect(() => { @@ -139,13 +190,13 @@ const MantecaAddMoney: FC = ({ source }) => { <> {isKycModalOpen && ( (query.state.data?.status === 'completed' ? false : 5000), + refetchInterval: (query: { state: { data?: { status?: string } } }) => + query.state.data?.status === 'completed' ? false : 5000, }) const { containerRef, truncatedAddress } = useAutoTruncatedAddress(depositAddressData?.depositAddress ?? '') diff --git a/src/context/OnrampFlowContext.tsx b/src/context/OnrampFlowContext.tsx index 1b3dd91b6..26b51f737 100644 --- a/src/context/OnrampFlowContext.tsx +++ b/src/context/OnrampFlowContext.tsx @@ -2,8 +2,6 @@ import React, { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from 'react' -export type OnrampView = 'INITIAL' | 'SELECT_METHOD' - export interface InitialViewErrorState { showError: boolean errorMessage: string @@ -28,11 +26,20 @@ export interface IOnrampData { } } +/** + * OnrampFlowContext - Manages transient state for the add-money (onramp) flow. + * + * NOTE: Step and amount are now managed via URL query parameters using nuqs. + * See the useQueryStates usage in: + * - src/app/(mobile-ui)/add-money/[country]/bank/page.tsx + * - src/components/AddMoney/components/MantecaAddMoney.tsx + * + * This context only manages: + * - `error` - Transient error state for form validation + * - `fromBankSelected` - Flag for navigation + * - `onrampData` - API response data (not appropriate for URL) + */ interface OnrampFlowContextType { - amountToOnramp: string - setAmountToOnramp: (amount: string) => void - currentView: OnrampView - setCurrentView: (view: OnrampView) => void error: InitialViewErrorState setError: (error: InitialViewErrorState) => void fromBankSelected: boolean @@ -45,18 +52,17 @@ interface OnrampFlowContextType { const OnrampFlowContext = createContext(undefined) export const OnrampFlowContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [amountToOnramp, setAmountToOnramp] = useState('') - const [currentView, setCurrentView] = useState('INITIAL') + // Transient UI state - not appropriate for URL const [error, setError] = useState({ showError: false, errorMessage: '', }) const [fromBankSelected, setFromBankSelected] = useState(false) + + // API response data - not appropriate for URL const [onrampData, setOnrampData] = useState(null) const resetOnrampFlow = useCallback(() => { - setAmountToOnramp('') - setCurrentView('INITIAL') setError({ showError: false, errorMessage: '', @@ -67,10 +73,6 @@ export const OnrampFlowContextProvider: React.FC<{ children: ReactNode }> = ({ c const value = useMemo( () => ({ - amountToOnramp, - setAmountToOnramp, - currentView, - setCurrentView, error, setError, fromBankSelected, @@ -79,7 +81,7 @@ export const OnrampFlowContextProvider: React.FC<{ children: ReactNode }> = ({ c setOnrampData, resetOnrampFlow, }), - [amountToOnramp, currentView, error, fromBankSelected, onrampData, resetOnrampFlow] + [error, fromBankSelected, onrampData, resetOnrampFlow] ) return {children} From 798b7bbde57b7775ee657abf06828b15d4e2b75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Wed, 31 Dec 2025 18:05:04 -0300 Subject: [PATCH 2/4] fix: fix url state validatiion --- .../add-money/[country]/bank/page.tsx | 7 +- src/app/(mobile-ui)/points/page.tsx.orig | 278 ------------------ .../AddMoney/components/MantecaAddMoney.tsx | 7 +- 3 files changed, 12 insertions(+), 280 deletions(-) delete mode 100644 src/app/(mobile-ui)/points/page.tsx.orig diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index d7a3d7b66..64cb7212a 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -56,7 +56,7 @@ export default function OnrampBankPage() { const [userUpdateError, setUserUpdateError] = useState(null) const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false) - const { setError, error, setOnrampData } = useOnrampFlow() + const { setError, error, setOnrampData, onrampData } = useOnrampFlow() const formRef = useRef<{ handleSubmit: () => void }>(null) const { balance } = useWallet() @@ -326,6 +326,11 @@ export default function OnrampBankPage() { } if (urlState.step === 'showDetails') { + // Validate we have the required data (protects against deep links and back navigation) + if (!onrampData?.transferId) { + setUrlState({ step: 'inputAmount' }) + return + } return } diff --git a/src/app/(mobile-ui)/points/page.tsx.orig b/src/app/(mobile-ui)/points/page.tsx.orig deleted file mode 100644 index 55c04719b..000000000 --- a/src/app/(mobile-ui)/points/page.tsx.orig +++ /dev/null @@ -1,278 +0,0 @@ -'use client' - -import PageContainer from '@/components/0_Bruddle/PageContainer' -import Card, { getCardPosition } from '@/components/Global/Card' -import CopyToClipboard from '@/components/Global/CopyToClipboard' -import { Icon } from '@/components/Global/Icons/Icon' -import NavHeader from '@/components/Global/NavHeader' -import NavigationArrow from '@/components/Global/NavigationArrow' -import PeanutLoading from '@/components/Global/PeanutLoading' -import ShareButton from '@/components/Global/ShareButton' -import TransactionAvatarBadge from '@/components/TransactionDetails/TransactionAvatarBadge' -import { VerifiedUserLabel } from '@/components/UserHeader' -import { useAuth } from '@/context/authContext' -import { invitesApi } from '@/services/invites' -import { generateInviteCodeLink, generateInvitesShareText, getInitialsFromName } from '@/utils/general.utils' -import { useQuery } from '@tanstack/react-query' -import { useRouter } from 'next/navigation' -import { STAR_STRAIGHT_ICON, TIER_0_BADGE, TIER_1_BADGE, TIER_2_BADGE, TIER_3_BADGE } from '@/assets' -import Image from 'next/image' -import { pointsApi } from '@/services/points' -import EmptyState from '@/components/Global/EmptyStates/EmptyState' -import { type PointsInvite } from '@/services/services.types' -import { useEffect } from 'react' -import InvitesGraph from '@/components/Global/InvitesGraph' - -const PointsPage = () => { - const router = useRouter() - const { user, fetchUser } = useAuth() - - const getTierBadge = (tier: number) => { - const badges = [TIER_0_BADGE, TIER_1_BADGE, TIER_2_BADGE, TIER_3_BADGE] - return badges[tier] || TIER_0_BADGE - } - const { - data: invites, - isLoading, - isError: isInvitesError, - error: invitesError, - } = useQuery({ - queryKey: ['invites', user?.user.userId], - queryFn: () => invitesApi.getInvites(), - enabled: !!user?.user.userId, - }) - - const { - data: tierInfo, - isLoading: isTierInfoLoading, - isError: isTierInfoError, - error: tierInfoError, - } = useQuery({ - queryKey: ['tierInfo', user?.user.userId], - queryFn: () => pointsApi.getTierInfo(), - enabled: !!user?.user.userId, - }) - - const { data: myGraphResult } = useQuery({ - queryKey: ['myInviteGraph', user?.user.userId], - queryFn: () => pointsApi.getUserInvitesGraph(), - enabled: - !!user?.user.userId && user?.user?.badges?.some((badge) => badge.code === 'SEEDLING_DEVCONNECT_BA_2025'), - }) - const username = user?.user.username - const { inviteCode, inviteLink } = generateInviteCodeLink(username ?? '') - - useEffect(() => { - // Re-fetch user to get the latest invitees list for showing heart Icon - fetchUser() - }, []) - - if (isLoading || isTierInfoLoading || !tierInfo?.data) { - return - } - - if (isInvitesError || isTierInfoError) { - console.error('Error loading points data:', invitesError ?? tierInfoError) - - return ( -
- -
- ) - } - - return ( - - router.back()} /> - -
- -
- star -

- {tierInfo.data.totalPoints} {tierInfo.data.totalPoints === 1 ? 'Point' : 'Points'} -

-
- - {/* Progressive progress bar */} -
- {`Tier -
-
= 2 - ? 100 - : Math.pow( - Math.min( - 1, - tierInfo.data.nextTierThreshold > 0 - ? tierInfo.data.totalPoints / tierInfo.data.nextTierThreshold - : 0 - ), - 0.6 - ) * 100 - }%`, - }} - /> -
- {tierInfo?.data.currentTier < 2 && ( - {`Tier - )} -
- -
-

You're at tier {tierInfo?.data.currentTier}.

- {tierInfo?.data.currentTier < 2 ? ( -

- {tierInfo.data.pointsToNextTier}{' '} - {tierInfo.data.pointsToNextTier === 1 ? 'point' : 'points'} needed to level up -

- ) : ( -

You've reached the max tier!

- )} -
- - {user?.invitedBy ? ( -

- router.push(`/${user.invitedBy}`)} - className="inline-flex cursor-pointer items-center gap-1 font-bold" - > - {user.invitedBy} - {' '} - invited you and earned points. Now it's your turn! Invite friends and get 20% of their points. -

- ) : ( -
- -

- Do stuff on Peanut and get points. Invite friends and pocket 20% of their points, too. -

-
- )} - -

Invite friends with your code

-
- -

{`${inviteCode}`}

- -
-
- - {invites && invites?.invitees && invites.invitees.length > 0 && ( - <> - Promise.resolve(generateInvitesShareText(inviteLink))} - title="Share your invite link" - > - Share Invite link - -
router.push('/points/invites')} - > -

People you invited

- -
- - {/* Invite Graph */} - {myGraphResult?.data && ( - <> - - - -
- -

- Experimental. Only available for Seedlings badge holders. -

-
- - )} - -
- {invites.invitees?.map((invite: PointsInvite, i: number) => { - const username = invite.username - const fullName = invite.fullName - const isVerified = invite.kycStatus === 'approved' - const pointsEarned = Math.floor(invite.totalPoints * 0.2) - // respect user's showFullName preference for avatar and display name - const displayName = invite.showFullName && fullName ? fullName : username - return ( - router.push(`/${username}`)} - className="cursor-pointer" - > -
-
- -
-
- -
-

- +{pointsEarned} {pointsEarned === 1 ? 'pt' : 'pts'} -

-
-
- ) - })} -
- - )} - - {invites?.invitees?.length === 0 && ( - -
- -
-

No invites yet

- -

- Send your invite link to start earning more rewards -

- Promise.resolve(generateInvitesShareText(inviteLink))} - title="Share your invite link" - > - Share Invite link - -
- )} -
-
- ) -} - -export default PointsPage diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx index 87fec1cf1..1422d433b 100644 --- a/src/components/AddMoney/components/MantecaAddMoney.tsx +++ b/src/components/AddMoney/components/MantecaAddMoney.tsx @@ -217,7 +217,12 @@ const MantecaAddMoney: FC = ({ source }) => { ) } - if (step === 'depositDetails' && depositDetails) { + if (step === 'depositDetails') { + // Validate we have the required data (protects against deep links and back navigation) + if (!depositDetails) { + setUrlState({ step: 'inputAmount' }) + return null + } return ( Date: Wed, 31 Dec 2025 18:23:22 -0300 Subject: [PATCH 3/4] fix: redirect to inputAmount if showDetails is accessed without required data (deep link / back navigation) --- src/app/(mobile-ui)/add-money/[country]/bank/page.tsx | 10 ++++++++-- src/components/AddMoney/components/MantecaAddMoney.tsx | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index 64cb7212a..8ce1ff0e2 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -263,6 +263,13 @@ export default function OnrampBankPage() { } }, [urlState.step]) + // 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 @@ -326,9 +333,8 @@ export default function OnrampBankPage() { } if (urlState.step === 'showDetails') { - // Validate we have the required data (protects against deep links and back navigation) + // Show loading while useEffect redirects if data is missing if (!onrampData?.transferId) { - setUrlState({ step: 'inputAmount' }) return } return diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx index 1422d433b..e34c2dccd 100644 --- a/src/components/AddMoney/components/MantecaAddMoney.tsx +++ b/src/components/AddMoney/components/MantecaAddMoney.tsx @@ -183,6 +183,13 @@ const MantecaAddMoney: FC = ({ source }) => { } }, [isMantecaKycRequired]) + // Redirect to inputAmount if depositDetails is accessed without required data (deep link / back navigation) + useEffect(() => { + if (step === 'depositDetails' && !depositDetails) { + setUrlState({ step: 'inputAmount' }) + } + }, [step, depositDetails, setUrlState]) + if (!selectedCountry) return null if (step === 'inputAmount') { @@ -218,9 +225,8 @@ const MantecaAddMoney: FC = ({ source }) => { } if (step === 'depositDetails') { - // Validate we have the required data (protects against deep links and back navigation) + // Show nothing while useEffect redirects if data is missing if (!depositDetails) { - setUrlState({ step: 'inputAmount' }) return null } return ( From 1b96706fd90f9e3ff3d3bc884d5dc3b6e0f62ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Thu, 1 Jan 2026 16:56:04 -0300 Subject: [PATCH 4/4] fix: misc fixes in add money url state --- .../[country]/[regional-method]/page.tsx | 2 +- .../AddMoney/components/InputAmountStep.tsx | 6 ++ .../AddMoney/components/MantecaAddMoney.tsx | 71 +++++++++---------- .../components/MantecaDepositShareDetails.tsx | 6 +- src/components/Global/AmountInput/index.tsx | 21 +++++- 5 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx b/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx index 448cbb6e3..6374e3224 100644 --- a/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx @@ -15,7 +15,7 @@ export default function AddMoneyRegionalMethodPage() { MantecaSupportedExchanges[countryDetails?.id as keyof typeof MantecaSupportedExchanges] && method === 'manteca' ) { - return + return } return null } diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx index a58122874..e58cec3bc 100644 --- a/src/components/AddMoney/components/InputAmountStep.tsx +++ b/src/components/AddMoney/components/InputAmountStep.tsx @@ -19,6 +19,8 @@ interface InputAmountStepProps { setCurrencyAmount: (amount: string | undefined) => void currencyData?: ICurrency setCurrentDenomination?: (denomination: string) => void + initialDenomination?: string + setDisplayedAmount?: (value: string) => void } const InputAmountStep = ({ @@ -30,6 +32,8 @@ const InputAmountStep = ({ currencyData, setCurrencyAmount, setCurrentDenomination, + initialDenomination, + setDisplayedAmount, }: InputAmountStepProps) => { const router = useRouter() @@ -45,8 +49,10 @@ const InputAmountStep = ({ = ({ source }) => { +const MantecaAddMoney: FC = () => { const params = useParams() const router = useRouter() const queryClient = useQueryClient() // URL state - persisted in query params - // Example: /add-money/argentina/manteca?step=inputAmount&amount=100¤cy=USD + // Example: /add-money/argentina/manteca?step=inputAmount&amount=100¤cy=ARS + // The `amount` is stored in whatever denomination `currency` specifies const [urlState, setUrlState] = useQueryStates( { step: parseAsStringEnum(['inputAmount', 'depositDetails']), @@ -47,12 +44,16 @@ const MantecaAddMoney: FC = ({ source }) => { // Derive state from URL (with defaults) const step: MantecaStep = urlState.step ?? 'inputAmount' - const usdAmount = urlState.amount ?? '' + // Amount from URL - this is in the denomination specified by `currency` + const displayedAmount = urlState.amount ?? '' const currentDenomination = urlState.currency ?? 'USD' - // Local UI state (not URL-appropriate - transient or API responses) + // Local UI state for tracking both amounts (needed for API call and validation) + const [usdAmount, setUsdAmount] = useState('') + const [localCurrencyAmount, setLocalCurrencyAmount] = useState('') + + // Other local UI state (not URL-appropriate - transient or API responses) const [isCreatingDeposit, setIsCreatingDeposit] = useState(false) - const [currencyAmount, setCurrencyAmount] = useState() const [error, setError] = useState(null) const [depositDetails, setDepositDetails] = useState() const [isKycModalOpen, setIsKycModalOpen] = useState(false) @@ -78,7 +79,7 @@ const MantecaAddMoney: FC = ({ source }) => { }, }) - // Validate amount when it changes + // Validate USD amount (for min/max checks which are in USD) useEffect(() => { if (!usdAmount || usdAmount === '0.00') { setError(null) @@ -108,22 +109,29 @@ const MantecaAddMoney: FC = ({ source }) => { } } - // Handle amount change - sync to URL state - const handleAmountChange = useCallback( + // Handle displayed amount change - save to URL + // This is called by AmountInput with the currently DISPLAYED value + const handleDisplayedAmountChange = useCallback( (value: string) => { setUrlState({ amount: value || null }) // null removes from URL }, [setUrlState] ) + // Handle local currency amount change (primary in AmountInput) + const handleLocalCurrencyAmountChange = useCallback((value: string | undefined) => { + setLocalCurrencyAmount(value ?? '') + }, []) + + // Handle USD amount change (secondary in AmountInput) + const handleUsdAmountChange = useCallback((value: string) => { + setUsdAmount(value) + }, []) + // Handle currency denomination change - sync to URL state const handleDenominationChange = useCallback( (value: string) => { - // Only persist valid currency denominations to URL - const validCurrencies: CurrencyDenomination[] = ['USD', 'ARS', 'BRL', 'MXN', 'EUR'] - if (validCurrencies.includes(value as CurrencyDenomination)) { - setUrlState({ currency: value as CurrencyDenomination }) - } + setUrlState({ currency: value as CurrencyDenomination }) }, [setUrlState] ) @@ -147,7 +155,8 @@ const MantecaAddMoney: FC = ({ source }) => { setError(null) setIsCreatingDeposit(true) const isUsdDenominated = currentDenomination === 'USD' - const amount = isUsdDenominated ? usdAmount : currencyAmount + // Use the displayed amount for the API call + const amount = displayedAmount const depositData = await mantecaApi.deposit({ amount: amount!, isUsdDenominated, @@ -166,15 +175,7 @@ const MantecaAddMoney: FC = ({ source }) => { } finally { setIsCreatingDeposit(false) } - }, [ - currentDenomination, - selectedCountry, - usdAmount, - currencyAmount, - isMantecaKycRequired, - isCreatingDeposit, - setUrlState, - ]) + }, [currentDenomination, selectedCountry, displayedAmount, isMantecaKycRequired, isCreatingDeposit, setUrlState]) // handle verification modal opening useEffect(() => { @@ -196,14 +197,16 @@ const MantecaAddMoney: FC = ({ source }) => { return ( <> {isKycModalOpen && ( = ({ source }) => { if (!depositDetails) { return null } - return ( - - ) + return } return null diff --git a/src/components/AddMoney/components/MantecaDepositShareDetails.tsx b/src/components/AddMoney/components/MantecaDepositShareDetails.tsx index d2302b5a0..6f1e2d4af 100644 --- a/src/components/AddMoney/components/MantecaDepositShareDetails.tsx +++ b/src/components/AddMoney/components/MantecaDepositShareDetails.tsx @@ -19,14 +19,10 @@ import { shortenStringLong, formatCurrency } from '@/utils/general.utils' const MantecaDepositShareDetails = ({ depositDetails, - source, currencyAmount, - onBack, }: { depositDetails: MantecaDepositResponseData - source: 'bank' | 'regionalMethod' currencyAmount?: string | undefined - onBack?: () => void }) => { const router = useRouter() const params = useParams() @@ -86,7 +82,7 @@ const MantecaDepositShareDetails = ({ return (
- router.back())} /> +
{/* Amount Display Card */} diff --git a/src/components/Global/AmountInput/index.tsx b/src/components/Global/AmountInput/index.tsx index 499005941..3aae822cf 100644 --- a/src/components/Global/AmountInput/index.tsx +++ b/src/components/Global/AmountInput/index.tsx @@ -13,9 +13,11 @@ const DECIMAL_SCALE = 18 // Max expected decimal places for any denomination interface AmountInputProps { className?: string initialAmount?: string + initialDenomination?: string onSubmit?: () => void setPrimaryAmount: (value: string) => void setSecondaryAmount?: (value: string) => void + setDisplayedAmount?: (value: string) => void onBlur?: () => void disabled?: boolean primaryDenomination?: { symbol: string; price: number; decimals: number } @@ -36,9 +38,11 @@ interface AmountInputProps { const AmountInput = ({ className, initialAmount, + initialDenomination, onSubmit, setPrimaryAmount, setSecondaryAmount, + setDisplayedAmount, onBlur, disabled, primaryDenomination = { symbol: '$', price: 1, decimals: 2 }, @@ -64,7 +68,19 @@ const AmountInput = ({ // Store display value for input field (what user sees when typing) const [displayValue, setDisplayValue] = useState(initialAmount || '') const [exactValue, setExactValue] = useState(Number(initialAmount || '') * 10 ** DECIMAL_SCALE) - const [displaySymbol, setDisplaySymbol] = useState(primaryDenomination.symbol) + // Use initialDenomination if provided and valid, otherwise default to primaryDenomination + const [displaySymbol, setDisplaySymbol] = useState(() => { + if (initialDenomination) { + // Check if initialDenomination matches primary or secondary + if (initialDenomination === primaryDenomination.symbol) { + return primaryDenomination.symbol + } + if (secondaryDenomination && initialDenomination === secondaryDenomination.symbol) { + return secondaryDenomination.symbol + } + } + return primaryDenomination.symbol + }) const denominations = { [primaryDenomination.symbol]: primaryDenomination, @@ -112,6 +128,9 @@ const AmountInput = ({ const rawDisplayValue = displayValue.replace(/,/g, '') const rawAlternativeValue = alternativeDisplayValue.replace(/,/g, '') + // Always call setDisplayedAmount with the currently displayed value + setDisplayedAmount?.(rawDisplayValue) + if (isPrimaryDenomination) { setPrimaryAmount(rawDisplayValue) setSecondaryAmount?.(rawAlternativeValue)