From 3edbfc2d6669cd1e5d2b4641cdbdd8e2484b8518 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 24 Oct 2025 19:26:50 +0530 Subject: [PATCH 1/3] feat: update send router payment options --- .../index.tsx} | 6 +- .../components/CryptoSourceListCard.tsx | 4 +- .../AddMoney/components/DepositMethodList.tsx | 4 +- .../AddMoney/views/TokenSelection.view.tsx | 4 +- .../AddWithdraw/AddWithdrawCountriesList.tsx | 4 +- src/components/Common/ActionList.tsx | 59 ++--- .../Common/ActionListDaimoPayButton.tsx | 6 +- src/components/Common/CountryList.tsx | 8 +- src/components/Common/CountryListSkeleton.tsx | 4 +- src/components/Common/SavedAccountsView.tsx | 6 +- src/components/Global/Icons/user.tsx | 8 +- src/components/Profile/AvatarWithBadge.tsx | 4 +- .../Request/views/RequestRouter.view.tsx | 11 +- src/components/RouterViewWrapper/index.tsx | 93 -------- .../SearchInput.tsx => SearchInput/index.tsx} | 0 src/components/SearchUsers/SearchResults.tsx | 147 ------------- src/components/Send/views/SendRouter.view.tsx | 201 +++++++++++++++++- src/constants/actionlist.consts.ts | 3 +- src/hooks/useGeoFilteredPaymentOptions.ts | 77 +++++++ src/hooks/useRecentUsers.ts | 10 +- src/hooks/useTranslationViewTransition.ts | 33 --- 21 files changed, 325 insertions(+), 367 deletions(-) rename src/components/{SearchUsers/SearchResultCard.tsx => ActionListCard/index.tsx} (95%) delete mode 100644 src/components/RouterViewWrapper/index.tsx rename src/components/{SearchUsers/SearchInput.tsx => SearchInput/index.tsx} (100%) delete mode 100644 src/components/SearchUsers/SearchResults.tsx create mode 100644 src/hooks/useGeoFilteredPaymentOptions.ts delete mode 100644 src/hooks/useTranslationViewTransition.ts diff --git a/src/components/SearchUsers/SearchResultCard.tsx b/src/components/ActionListCard/index.tsx similarity index 95% rename from src/components/SearchUsers/SearchResultCard.tsx rename to src/components/ActionListCard/index.tsx index cd17039fd..46976c86e 100644 --- a/src/components/SearchUsers/SearchResultCard.tsx +++ b/src/components/ActionListCard/index.tsx @@ -4,7 +4,7 @@ import React from 'react' import { twMerge } from 'tailwind-merge' import { Button } from '../0_Bruddle' -interface SearchResultCardProps { +interface ActionListCardProps { title: string | React.ReactNode description?: string leftIcon?: React.ReactNode @@ -16,7 +16,7 @@ interface SearchResultCardProps { descriptionClassName?: string } -export const SearchResultCard = ({ +export const ActionListCard = ({ title, description, leftIcon, @@ -26,7 +26,7 @@ export const SearchResultCard = ({ rightContent, isDisabled = false, descriptionClassName, -}: SearchResultCardProps) => { +}: ActionListCardProps) => { const handleCardClick = () => { onClick() } diff --git a/src/components/AddMoney/components/CryptoSourceListCard.tsx b/src/components/AddMoney/components/CryptoSourceListCard.tsx index 991457348..ca1ce90f4 100644 --- a/src/components/AddMoney/components/CryptoSourceListCard.tsx +++ b/src/components/AddMoney/components/CryptoSourceListCard.tsx @@ -1,9 +1,9 @@ 'use client' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' -import { SearchResultCard } from '@/components/SearchUsers/SearchResultCard' import Image, { type StaticImageData } from 'next/image' import { twMerge } from 'tailwind-merge' import { type CryptoSource } from '../consts' +import { ActionListCard } from '@/components/ActionListCard' interface CryptoSourceListCardProps { sources: CryptoSource[] @@ -22,7 +22,7 @@ export const CryptoSourceListCard = ({ sources, onItemClick }: CryptoSourceListC return (
{sources.map((source, index) => ( - = ({ headerTitle, on const isDisabled = token.symbol.toLowerCase() !== PEANUT_WALLET_TOKEN_SYMBOL.toLowerCase() return ( - {

{title}

{paymentMethods.map((method, index) => ( - { + if (flow === 'claim') { + return claimType === BankClaimType.GuestKycNeeded || claimType === BankClaimType.ReceiverKycNeeded + } + if (flow === 'request') { + return requestType === BankRequestType.GuestKycNeeded || requestType === BankRequestType.PayerKycNeeded + } + return false + }, [claimType, requestType, flow]) + + // use the hook to filter and sort payment methods based on geolocation + const { filteredMethods: sortedActionMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({ + sortUnavailable: true, + isMethodUnavailable: (method) => method.soon || (method.id === 'bank' && requiresVerification), + }) const handleMethodClick = async (method: PaymentMethod) => { if (flow === 'claim' && claimLinkData) { @@ -156,41 +170,6 @@ export default function ActionList({ } } - const geolocatedMethods = useMemo(() => { - // show pix in brazil and mercado pago in other countries - return ACTION_METHODS.filter((method) => { - if (userGeoLocationCountryCode === 'BR' && method.id === 'mercadopago') { - return false - } - if (userGeoLocationCountryCode !== 'BR' && method.id === 'pix') { - return false - } - return true - }) - }, [userGeoLocationCountryCode]) - - const requiresVerification = useMemo(() => { - if (flow === 'claim') { - return claimType === BankClaimType.GuestKycNeeded || claimType === BankClaimType.ReceiverKycNeeded - } - if (flow === 'request') { - return requestType === BankRequestType.GuestKycNeeded || requestType === BankRequestType.PayerKycNeeded - } - return false - }, [claimType, requestType, flow]) - - const sortedActionMethods = useMemo(() => { - return [...geolocatedMethods].sort((a, b) => { - const aIsUnavailable = a.soon || (a.id === 'bank' && requiresVerification) - const bIsUnavailable = b.soon || (b.id === 'bank' && requiresVerification) - - if (aIsUnavailable === bIsUnavailable) { - return 0 - } - return aIsUnavailable ? 1 : -1 - }) - }, [requiresVerification]) - const handleContinueWithPeanut = () => { if (flow === 'claim') { addParamStep('claim') @@ -320,7 +299,7 @@ export const MethodCard = ({ requiresVerification?: boolean }) => { return ( - void @@ -162,7 +162,7 @@ const ActionListDaimoPayButton = ({ handleContinueWithPeanut, showConfirmModal } daimoPayButtonClickRef.current = onClick return ( - {!searchTerm && viewMode === 'add-withdraw' && onCryptoClick && (
- { {Array.from({ length: 10 }).map((_, index) => { const position = getCardPosition(index, 5) return ( - } position={position} diff --git a/src/components/Common/SavedAccountsView.tsx b/src/components/Common/SavedAccountsView.tsx index ff3bf68cb..ff1c95e5c 100644 --- a/src/components/Common/SavedAccountsView.tsx +++ b/src/components/Common/SavedAccountsView.tsx @@ -1,14 +1,14 @@ 'use client' import { countryData as ALL_METHODS_DATA, ALL_COUNTRIES_ALPHA3_TO_ALPHA2 } from '@/components/AddMoney/consts' -import { shortenStringLong, formatIban } from '@/utils/general.utils' +import { formatIban } from '@/utils/general.utils' import { AccountType, type Account } from '@/interfaces' import Image from 'next/image' import { Icon } from '@/components/Global/Icons/Icon' -import { SearchResultCard } from '@/components/SearchUsers/SearchResultCard' import NavHeader from '../Global/NavHeader' import Divider from '../0_Bruddle/Divider' import { Button } from '../0_Bruddle' +import { ActionListCard } from '../ActionListCard' interface SavedAccountListProps { pageTitle: string @@ -101,7 +101,7 @@ export function SavedAccountsMapping({ const title = account.type === AccountType.IBAN ? formatIban(account.identifier) : account.identifier return ( - > = (props) => ( - + - diff --git a/src/components/Profile/AvatarWithBadge.tsx b/src/components/Profile/AvatarWithBadge.tsx index 348778681..ac50f7aa6 100644 --- a/src/components/Profile/AvatarWithBadge.tsx +++ b/src/components/Profile/AvatarWithBadge.tsx @@ -6,7 +6,7 @@ import { Icon, type IconName } from '../Global/Icons/Icon' import StatusPill, { type StatusPillType } from '../Global/StatusPill' import Image, { type StaticImageData } from 'next/image' -export type AvatarSize = 'extra-small' | 'small' | 'medium' | 'large' +export type AvatarSize = 'tiny' | 'extra-small' | 'small' | 'medium' | 'large' /** * props for the avatarwithbadge component. @@ -41,6 +41,7 @@ const AvatarWithBadge: React.FC = ({ logo, }) => { const sizeClasses: Record = { + tiny: 'h-6 w-6 text-[10px]', 'extra-small': 'h-8 w-8 text-xs', small: 'h-12 w-12 text-sm', medium: 'h-16 w-16 text-2xl', @@ -48,6 +49,7 @@ const AvatarWithBadge: React.FC = ({ } const iconSizeMap: Record = { + tiny: 12, 'extra-small': 16, small: 18, medium: 32, diff --git a/src/components/Request/views/RequestRouter.view.tsx b/src/components/Request/views/RequestRouter.view.tsx index 020430213..d18e5add6 100644 --- a/src/components/Request/views/RequestRouter.view.tsx +++ b/src/components/Request/views/RequestRouter.view.tsx @@ -1,16 +1,9 @@ 'use client' -import RouterViewWrapper from '@/components/RouterViewWrapper' import { useRouter } from 'next/navigation' export const RequestRouterView = () => { const router = useRouter() - return ( - router.push('/request/create')} - onUserSelect={(username) => router.push(`/request/${username}`)} - /> - ) + // this is going to be deprecated in request pots + return
request router view
} diff --git a/src/components/RouterViewWrapper/index.tsx b/src/components/RouterViewWrapper/index.tsx deleted file mode 100644 index d0fe0a79c..000000000 --- a/src/components/RouterViewWrapper/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -'use client' -import NavHeader from '@/components/Global/NavHeader' -import { SearchInput } from '@/components/SearchUsers/SearchInput' -import { SearchResults } from '@/components/SearchUsers/SearchResults' -import { useRecentUsers } from '@/hooks/useRecentUsers' -import { useUserSearch } from '@/hooks/useUserSearch' -import { useUserInteractions } from '@/hooks/useUserInteractions' -import { useRef, useMemo } from 'react' -import { Button } from '../0_Bruddle' -import Divider from '../0_Bruddle/Divider' -import { Icon } from '../Global/Icons/Icon' - -interface RouterViewWrapperProps { - title: string - onPrev?: () => void - linkCardTitle: string - onLinkCardClick: () => void - onUserSelect: (username: string) => void -} - -const RouterViewWrapper = ({ title, onPrev, linkCardTitle, onLinkCardClick, onUserSelect }: RouterViewWrapperProps) => { - const inputRef = useRef(null) - const { searchTerm, setSearchTerm, searchResults, isSearching, error, showMinCharError, showNoResults } = - useUserSearch() - const recentTransactions = useRecentUsers() - - // userids to check for interactions - const userIds = useMemo(() => { - const ids = new Set() - searchResults.forEach((user) => ids.add(user.userId)) - recentTransactions.forEach((user) => ids.add(user.userId)) - return Array.from(ids) - }, [searchResults, recentTransactions]) - - const { interactions } = useUserInteractions(userIds) - - const handleSearchChange = (e: React.ChangeEvent) => { - setSearchTerm(e.target.value) - } - - const handleClearSearch = () => { - setSearchTerm('') - } - - return ( -
- - -
-
- - -
- - - {error &&
{error}
} -
-
- - {!searchTerm && ( - <> - - -
- -
- Works even if they don't use Peanut! -
-
- - )} -
-
- ) -} - -export default RouterViewWrapper diff --git a/src/components/SearchUsers/SearchInput.tsx b/src/components/SearchInput/index.tsx similarity index 100% rename from src/components/SearchUsers/SearchInput.tsx rename to src/components/SearchInput/index.tsx diff --git a/src/components/SearchUsers/SearchResults.tsx b/src/components/SearchUsers/SearchResults.tsx deleted file mode 100644 index 66aed8581..000000000 --- a/src/components/SearchUsers/SearchResults.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { type ApiUser, type RecentUser } from '@/services/users' -import { twMerge } from 'tailwind-merge' -import Card from '../Global/Card' -import EmptyState from '../Global/EmptyStates/EmptyState' -import { Icon } from '../Global/Icons/Icon' -import PeanutLoading from '../Global/PeanutLoading' -import AvatarWithBadge from '../Profile/AvatarWithBadge' -import { VerifiedUserLabel } from '../UserHeader' -import { SearchResultCard } from './SearchResultCard' - -interface SearchResultsProps { - searchTerm: string - results: ApiUser[] - isSearching: boolean - showMinCharError: boolean - showNoResults: boolean - className?: string - recentTransactions?: RecentUser[] - onUserSelect: (username: string) => void - interactions?: Record -} - -export const SearchResults = ({ - searchTerm, - results, - isSearching, - showMinCharError, - showNoResults, - className, - recentTransactions = [], - onUserSelect, - interactions = {}, -}: SearchResultsProps) => { - return ( -
- {showMinCharError && ( -
Enter at least 3 characters to search users
- )} - - {searchTerm && results.length > 0 && ( - <> -

People

-
- {results.map((user, index) => { - const isVerified = user.bridgeKycStatus === 'approved' - const haveSentMoneyToUser = interactions[user.userId] || false - return ( - - } - description={`@${user.username}`} - leftIcon={ - - } - onClick={() => onUserSelect(user.username)} - /> - ) - })} -
- - )} - - {isSearching && ( -
- -
- )} - - {showNoResults && ( - -
-
- -
-
-
No users found
-
Try searching with a different term
-
-
-
- )} - - {!searchTerm && ( - <> -

Recent transactions

- {recentTransactions.length > 0 ? ( -
- {recentTransactions.map((user, index) => { - const isVerified = user.bridgeKycStatus === 'approved' - const haveSentMoneyToUser = interactions[user.userId] || false - return ( - - } - description={`@${user.username}`} - leftIcon={ - - } - onClick={() => onUserSelect(user.username)} - position={ - recentTransactions.length === 1 - ? 'single' - : index === 0 - ? 'first' - : index === recentTransactions.length - 1 - ? 'last' - : 'middle' - } - /> - ) - })} -
- ) : ( - - )} - - )} -
- ) -} diff --git a/src/components/Send/views/SendRouter.view.tsx b/src/components/Send/views/SendRouter.view.tsx index c07840ea2..66c2c75b7 100644 --- a/src/components/Send/views/SendRouter.view.tsx +++ b/src/components/Send/views/SendRouter.view.tsx @@ -1,16 +1,75 @@ 'use client' -import RouterViewWrapper from '@/components/RouterViewWrapper' import { useAppDispatch } from '@/redux/hooks' import { sendFlowActions } from '@/redux/slices/send-flow-slice' import { useRouter, useSearchParams } from 'next/navigation' - +import { MERCADO_PAGO, PIX } from '@/assets' import LinkSendFlowManager from '../link/LinkSendFlowManager' +import NavHeader from '@/components/Global/NavHeader' +import Card from '@/components/Global/Card' +import { Icon } from '@/components/Global/Icons/Icon' +import { Button } from '@/components/0_Bruddle' +import Divider from '@/components/0_Bruddle/Divider' +import { ActionListCard } from '@/components/ActionListCard' +import IconStack from '@/components/Global/IconStack' +import { ACTION_METHODS, type PaymentMethod } from '@/constants/actionlist.consts' +import Image from 'next/image' +import StatusBadge from '@/components/Global/Badges/StatusBadge' +import { useGeoFilteredPaymentOptions } from '@/hooks/useGeoFilteredPaymentOptions' +import { useRecentUsers } from '@/hooks/useRecentUsers' +import { getInitialsFromName } from '@/utils/general.utils' +import { useCallback, useMemo } from 'react' +import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' export const SendRouterView = () => { const router = useRouter() const dispatch = useAppDispatch() const searchParams = useSearchParams() const isSendingByLink = searchParams.get('createLink') === 'true' + const { recentTransactions, isFetchingRecentUsers } = useRecentUsers() + + // fallback initials when no recent transactions + const fallbackInitials = ['AA', 'BB', 'CC'] + + const recentUsersAvatarInitials = useCallback(() => { + // if we have recent transactions, use them (max 3) + if (recentTransactions.length > 0) { + return recentTransactions.slice(0, 3).map((transaction) => { + return getInitialsFromName(transaction.username) + }) + } + // fallback to default initials if no data + return fallbackInitials + }, [recentTransactions]) + + const recentUsersAvatars = useMemo(() => { + // show loading skeleton while fetching + if (isFetchingRecentUsers) { + return ( +
+ {[0, 1, 2].map((index) => ( +
+ ))} +
+ ) + } + + // show avatars (either real data or fallback) + return ( +
+ {recentUsersAvatarInitials().map((initial, index) => { + return ( +
+ +
+ ) + })} +
+ ) + }, [isFetchingRecentUsers, recentUsersAvatarInitials]) const redirectToSendByLink = () => { // Reset send flow state when entering link creation flow @@ -24,19 +83,139 @@ export const SendRouterView = () => { router.back() } + const handleLinkCtaClick = () => { + router.push(`${window.location.pathname}?createLink=false`) // preserve current URL + redirectToSendByLink() + } + + // extend ACTION_METHODS with component-specific identifier icons + const extendedActionMethods = useMemo(() => { + return ACTION_METHODS.map((method) => { + // add identifier icon based on method id + switch (method.id) { + case 'bank': + return { + ...method, + identifierIcon: ( +
+ +
+ ), + } + case 'exchange-or-wallet': + return { + ...method, + identifierIcon: ( +
+ +
+ ), + } + case 'mercadopago': + return { + ...method, + identifierIcon: Mercado Pago, + } + case 'pix': + return { + ...method, + identifierIcon: Pix, + } + default: + return method + } + }) + }, []) + + // filter send options based on geolocation + const { filteredMethods: geoFilteredMethods } = useGeoFilteredPaymentOptions({ + methods: extendedActionMethods, + }) + + // prepend peanut contacts option to the filtered methods + const sendOptions = useMemo(() => { + const peanutContactsOption: PaymentMethod = { + id: 'peanut-contacts', + identifierIcon: ( +
+ +
+ ), + title: 'Peanut contacts', + description: "Peanuts you've interacted with", + icons: [], + soon: false, + } + + return [peanutContactsOption, ...geoFilteredMethods] + }, [geoFilteredMethods]) + if (isSendingByLink) { return } return ( - { - router.push(`${window.location.pathname}?createLink=false`) // preserve current URL - redirectToSendByLink() - }} - onUserSelect={(username) => router.push(`/send/${username}`)} - /> +
+
+ +
+ +
+
+
+ +
+
+
Send money with a link
+
No account needed to receive.
+
+
+ +
+
+ + + +
+ {sendOptions.map((option) => { + // determine right content based on option id + let rightContent + switch (option.id) { + case 'peanut-contacts': + rightContent = recentUsersAvatars + break + case 'mercadopago': + rightContent = + break + default: + rightContent = ( + + ) + break + } + + return ( + {}} + rightContent={rightContent} + /> + ) + })} +
+
+
+
) } diff --git a/src/constants/actionlist.consts.ts b/src/constants/actionlist.consts.ts index 944a2a177..3cc9b24d1 100644 --- a/src/constants/actionlist.consts.ts +++ b/src/constants/actionlist.consts.ts @@ -4,6 +4,7 @@ import binanceIcon from '@/assets/exchanges/binance.svg' export interface PaymentMethod { id: string + identifierIcon?: React.ReactNode title: string description: string icons: any[] @@ -39,7 +40,7 @@ export const ACTION_METHODS: PaymentMethod[] = [ { id: 'exchange-or-wallet', title: 'Exchange or Wallet', - description: 'Binance, Coinbase, Metamask and more', + description: 'Binance, Metamask and more', icons: [binanceIcon, TRUST_WALLET_SMALL_LOGO, METAMASK_LOGO], soon: false, }, diff --git a/src/hooks/useGeoFilteredPaymentOptions.ts b/src/hooks/useGeoFilteredPaymentOptions.ts new file mode 100644 index 000000000..ec69191d1 --- /dev/null +++ b/src/hooks/useGeoFilteredPaymentOptions.ts @@ -0,0 +1,77 @@ +'use client' +import { useMemo } from 'react' +import { useGeoLocation } from './useGeoLocation' +import { ACTION_METHODS, type PaymentMethod } from '@/constants/actionlist.consts' + +/** + * hook to filter and sort payment options based on user's geolocation + * + * filters pix to show only in brazil (BR) and mercado pago to show everywhere except brazil + * optionally sorts payment methods to move unavailable ones to the end of the list + * + * @param options - configuration object + * @param options.methods - array of payment methods to filter (defaults to ACTION_METHODS) + * @param options.sortUnavailable - whether to sort unavailable methods to the end (defaults to false) + * @param options.isMethodUnavailable - optional function to determine if a method is unavailable for custom sorting logic + * @returns object containing filtered and sorted payment methods and loading state + * + * @example + * // basic usage with default ACTION_METHODS + * const { filteredMethods, isLoading } = useGeoFilteredPaymentOptions() + * + * @example + * // with custom methods and sorting + * const { filteredMethods } = useGeoFilteredPaymentOptions({ + * methods: customMethods, + * sortUnavailable: true, + * isMethodUnavailable: (method) => method.soon || method.requiresVerification + * }) + */ +export const useGeoFilteredPaymentOptions = ( + options: { + methods?: PaymentMethod[] + sortUnavailable?: boolean + isMethodUnavailable?: (method: PaymentMethod) => boolean + } = {} +) => { + const { methods = ACTION_METHODS, sortUnavailable = false, isMethodUnavailable } = options + const { countryCode, isLoading } = useGeoLocation() + + const filteredAndSortedMethods = useMemo(() => { + // filter methods based on geolocation + const filtered = methods.filter((method) => { + // show pix only in brazil + if (countryCode === 'BR' && method.id === 'mercadopago') { + return false + } + // show mercado pago everywhere except brazil + if (countryCode !== 'BR' && method.id === 'pix') { + return false + } + return true + }) + + // optionally sort unavailable methods to the end + if (sortUnavailable && isMethodUnavailable) { + return [...filtered].sort((a, b) => { + const aIsUnavailable = isMethodUnavailable(a) + const bIsUnavailable = isMethodUnavailable(b) + + // keep original order if both are available or both are unavailable + if (aIsUnavailable === bIsUnavailable) { + return 0 + } + // move unavailable methods to the end + return aIsUnavailable ? 1 : -1 + }) + } + + return filtered + }, [countryCode, methods, sortUnavailable, isMethodUnavailable]) + + return { + filteredMethods: filteredAndSortedMethods, + isLoading, + countryCode, + } +} diff --git a/src/hooks/useRecentUsers.ts b/src/hooks/useRecentUsers.ts index 6405525da..194803b00 100644 --- a/src/hooks/useRecentUsers.ts +++ b/src/hooks/useRecentUsers.ts @@ -1,11 +1,15 @@ +'use client' import { useMemo } from 'react' import { useTransactionHistory, type HistoryEntry } from '@/hooks/useTransactionHistory' import { type RecentUser } from '@/services/users' export function useRecentUsers() { - const { data } = useTransactionHistory({ mode: 'latest', limit: 20 }) + const { data, isLoading } = useTransactionHistory({ mode: 'latest', limit: 20 }) + const recentTransactions = useMemo(() => { - if (!data) return [] + if (!data) { + return [] + } return data.entries.reduce((acc: RecentUser[], entry: HistoryEntry) => { let account if (entry.userRole === 'SENDER') { @@ -29,5 +33,5 @@ export function useRecentUsers() { }, []) }, [data]) - return recentTransactions + return { recentTransactions, isFetchingRecentUsers: isLoading } } diff --git a/src/hooks/useTranslationViewTransition.ts b/src/hooks/useTranslationViewTransition.ts deleted file mode 100644 index 2438dba2a..000000000 --- a/src/hooks/useTranslationViewTransition.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { usePathname } from 'next/navigation' -import { useEffect, useState } from 'react' - -// handles translation state during view transitions to prevent dom errors -export const useTranslationViewTransition = () => { - const [isTransitioning, setIsTransitioning] = useState(false) - const pathname = usePathname() - - useEffect(() => { - // check if google translate is active on the page - const isTranslated = - document.documentElement.classList.contains('translated-ltr') || - document.documentElement.classList.contains('translated-rtl') - - if (isTranslated) { - // show loading state while translation processes new content - setIsTransitioning(true) - - // wait for translation service to finish - const timer = setTimeout(() => { - // trigger translation service to process new content - const event = new Event('translate-view') - window.dispatchEvent(event) - - setIsTransitioning(false) - }, 500) - - return () => clearTimeout(timer) - } - }, [pathname]) - - return isTransitioning -} From dab324bcbd1da9f877124948edb075e3b23769f3 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:08:26 +0530 Subject: [PATCH 2/3] feat: handle navigation --- .../withdraw/[country]/bank/page.tsx | 10 +- src/app/(mobile-ui)/withdraw/page.tsx | 87 +++++-- .../AddWithdraw/AddWithdrawCountriesList.tsx | 31 ++- .../AddWithdraw/AddWithdrawRouterView.tsx | 16 +- src/components/Send/views/SendRouter.view.tsx | 234 +++++++++++++----- 5 files changed, 284 insertions(+), 94 deletions(-) diff --git a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx index 6d23b9556..0ae0766c4 100644 --- a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx @@ -13,7 +13,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { AccountType, type Account } from '@/interfaces' import { formatIban, shortenStringLong, isTxReverted } from '@/utils/general.utils' import { useParams, useRouter } from 'next/navigation' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import DirectSuccessView from '@/components/Payment/Views/Status.payment.view' import { ErrorHandler, getBridgeChainName } from '@/utils' import { getOfframpCurrencyConfig } from '@/utils/bridge.utils' @@ -24,6 +24,7 @@ import countryCurrencyMappings from '@/constants/countryCurrencyMapping' import { useQuery } from '@tanstack/react-query' import { pointsApi } from '@/services/points' import { PointsAction } from '@/services/services.types' +import { useSearchParams } from 'next/navigation' type View = 'INITIAL' | 'SUCCESS' @@ -32,11 +33,16 @@ export default function WithdrawBankPage() { const { user, fetchUser } = useAuth() const { address, sendMoney } = useWallet() const router = useRouter() + const searchParams = useSearchParams() const [isLoading, setIsLoading] = useState(false) const [view, setView] = useState('INITIAL') const params = useParams() const country = params.country as string + // check if we came from send flow - using method param to detect (only bank goes through this page) + const methodParam = searchParams.get('method') + const fromSendFlow = methodParam === 'bank' + const nonEuroCurrency = countryCurrencyMappings.find( (currency) => country.toLowerCase() === currency.country.toLowerCase() || @@ -222,7 +228,7 @@ export default function WithdrawBankPage() { return (
router.push('/home') : () => router.back()} /> diff --git a/src/app/(mobile-ui)/withdraw/page.tsx b/src/app/(mobile-ui)/withdraw/page.tsx index 0f4a94dd4..edf5ffb53 100644 --- a/src/app/(mobile-ui)/withdraw/page.tsx +++ b/src/app/(mobile-ui)/withdraw/page.tsx @@ -11,7 +11,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { tokenSelectorContext } from '@/context/tokenSelector.context' import { formatAmount } from '@/utils' import { getCountryFromAccount } from '@/utils/bridge.utils' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useMemo, useState, useRef, useContext } from 'react' import { formatUnits } from 'viem' @@ -19,8 +19,15 @@ type WithdrawStep = 'inputAmount' | 'selectMethod' export default function WithdrawPage() { const router = useRouter() + const searchParams = useSearchParams() const { selectedTokenData } = useContext(tokenSelectorContext) + // check if coming from send flow based on method query param + const methodParam = searchParams.get('method') + const isFromSendFlow = !!(methodParam && ['bank', 'crypto'].includes(methodParam)) + const isCryptoFromSend = methodParam === 'crypto' && isFromSendFlow + const isBankFromSend = methodParam === 'bank' && isFromSendFlow + const { amountToWithdraw: amountFromContext, setAmountToWithdraw, @@ -33,10 +40,25 @@ export default function WithdrawPage() { setShowAllWithdrawMethods, } = useWithdrawFlow() - const initialStep: WithdrawStep = selectedMethod ? 'inputAmount' : 'selectMethod' + // only go to input amount if method is selected OR if it's crypto from send (bank needs method selection first) + const initialStep: WithdrawStep = selectedMethod || isCryptoFromSend ? 'inputAmount' : 'selectMethod' const [step, setStep] = useState(initialStep) + // automatically set crypto method when coming from send flow with method=crypto + useEffect(() => { + if (isCryptoFromSend && !selectedMethod) { + setSelectedMethod({ + type: 'crypto', + title: 'Crypto', + countryPath: undefined, + }) + } else if (isBankFromSend && !selectedMethod) { + // for bank, just show all methods - don't auto-select + setShowAllWithdrawMethods(true) + } + }, [isCryptoFromSend, isBankFromSend, selectedMethod, setSelectedMethod, setShowAllWithdrawMethods]) + // flag to know if user has manually entered something const userTypedRef = useRef(false) @@ -68,12 +90,12 @@ export default function WithdrawPage() { }, [setError, amountFromContext]) useEffect(() => { - if (selectedMethod) { + if (selectedMethod || isCryptoFromSend) { setStep('inputAmount') if (amountFromContext && !rawTokenAmount) { setRawTokenAmount(amountFromContext) } - } else { + } else if (!selectedMethod) { setStep('selectMethod') // clear the raw token amount when switching back to method selection if (step !== 'selectMethod') { @@ -81,7 +103,7 @@ export default function WithdrawPage() { setTokenInputKey((k) => k + 1) } } - }, [selectedMethod, amountFromContext, step, rawTokenAmount]) + }, [selectedMethod, isCryptoFromSend, amountFromContext, step, rawTokenAmount]) useEffect(() => { // If amount is available (i.e) user clicked back from select method view, show all methods @@ -116,7 +138,7 @@ export default function WithdrawPage() { // determine message let message = '' if (usdEquivalent < 1) { - message = 'Minimum withdrawal is 1.' + message = isFromSendFlow ? 'Minimum send amount is $1.' : 'Minimum withdrawal is $1.' } else if (amount > maxDecimalAmount) { message = 'Amount exceeds your wallet balance.' } else { @@ -125,7 +147,7 @@ export default function WithdrawPage() { setError({ showError: true, errorMessage: message }) return false }, - [maxDecimalAmount, setError, selectedTokenData?.price] + [maxDecimalAmount, setError, selectedTokenData?.price, isFromSendFlow] ) const handleTokenAmountChange = useCallback( @@ -180,25 +202,35 @@ export default function WithdrawPage() { setUsdAmount(usdVal.toString()) // Route based on selected method type + // preserve method param if coming from send flow + const methodQueryParam = isFromSendFlow ? `method=${methodParam}` : '' + if (selectedBankAccount) { const country = getCountryFromAccount(selectedBankAccount) if (country) { - router.push(`/withdraw/${country.path}/bank`) + const queryParams = isFromSendFlow ? `?${methodQueryParam}` : '' + router.push(`/withdraw/${country.path}/bank${queryParams}`) } else { throw new Error('Failed to get country from bank account') } } else if (selectedMethod.type === 'crypto') { - router.push('/withdraw/crypto') + const queryParams = isFromSendFlow ? `?${methodQueryParam}` : '' + router.push(`/withdraw/crypto${queryParams}`) } else if (selectedMethod.type === 'manteca') { // Route directly to Manteca with method and country params - const methodParam = selectedMethod.title?.toLowerCase().replace(/\s+/g, '-') || 'bank-transfer' - router.push(`/withdraw/manteca?method=${methodParam}&country=${selectedMethod.countryPath}`) + const mantecaMethodParam = selectedMethod.title?.toLowerCase().replace(/\s+/g, '-') || 'bank-transfer' + const additionalParams = isFromSendFlow ? `&${methodQueryParam}` : '' + router.push( + `/withdraw/manteca?method=${mantecaMethodParam}&country=${selectedMethod.countryPath}${additionalParams}` + ) } else if (selectedMethod.type === 'bridge' && selectedMethod.countryPath) { // Bridge countries go to country page for bank account form - router.push(`/withdraw/${selectedMethod.countryPath}`) + const queryParams = isFromSendFlow ? `?${methodQueryParam}` : '' + router.push(`/withdraw/${selectedMethod.countryPath}${queryParams}`) } else if (selectedMethod.countryPath) { // Other countries go to their country pages - router.push(`/withdraw/${selectedMethod.countryPath}`) + const queryParams = isFromSendFlow ? `?${methodQueryParam}` : '' + router.push(`/withdraw/${selectedMethod.countryPath}${queryParams}`) } } } @@ -221,16 +253,24 @@ export default function WithdrawPage() { return (
{ - // Go back to method selection - setSelectedMethod(null) - setStep('selectMethod') + // if crypto from send, go back to send page + if (isCryptoFromSend) { + setSelectedMethod(null) + router.push('/send') + } else { + // otherwise go back to method selection + setSelectedMethod(null) + setStep('selectMethod') + } }} />
-
Amount to withdraw
+
+ {isFromSendFlow ? 'Amount to send' : 'Amount to withdraw'} +
{ - router.push('/home') + // if bank from send flow, go back to send page + if (isBankFromSend) { + router.push('/send') + } else { + router.push('/home') + } }} /> ) diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx index b3257a2c1..36d91921d 100644 --- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx +++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx @@ -7,7 +7,7 @@ import NavHeader from '@/components/Global/NavHeader' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' import { getColorForUsername } from '@/utils/color.utils' import Image, { type StaticImageData } from 'next/image' -import { useParams, useRouter } from 'next/navigation' +import { useParams, useRouter, useSearchParams } from 'next/navigation' import EmptyState from '../Global/EmptyStates/EmptyState' import { useAuth } from '@/context/authContext' import { useEffect, useMemo, useRef, useState } from 'react' @@ -35,6 +35,12 @@ interface AddWithdrawCountriesListProps { const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { const router = useRouter() const params = useParams() + const searchParams = useSearchParams() + + // check if coming from send flow and what type + const methodParam = searchParams.get('method') + const isFromSendFlow = !!(methodParam && ['bank', 'crypto'].includes(methodParam)) + const isBankFromSend = methodParam === 'bank' && isFromSendFlow // hooks const { deviceType } = useDeviceType() @@ -123,7 +129,8 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { } if (currentCountry) { - router.push(`/withdraw/${currentCountry.path}/bank`) + const queryParams = isBankFromSend ? `?method=${methodParam}` : '' + router.push(`/withdraw/${currentCountry.path}/bank${queryParams}`) } return {} } @@ -161,9 +168,14 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { } const handleWithdrawMethodClick = (method: SpecificPaymentMethod) => { + // preserve method param only if coming from bank send flow (not crypto) + const methodQueryParam = isBankFromSend ? `?method=${methodParam}` : '' + if (method.path && method.path.includes('/manteca')) { // Manteca methods route directly (has own amount input) - router.push(method.path) + const separator = method.path.includes('?') ? '&' : '?' + const additionalParams = isBankFromSend ? `${separator}method=${methodParam}` : '' + router.push(`${method.path}${additionalParams}`) } else if (method.id.includes('default-bank-withdraw') || method.id.includes('sepa-instant-withdraw')) { if (isUserBridgeKycUnderReview) { setShowKycStatusModal(true) @@ -177,7 +189,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { currency: currentCountry?.currency, title: method.title, }) - router.push('/withdraw') + router.push(`/withdraw${methodQueryParam}`) return } else if (method.id.includes('crypto-withdraw')) { setSelectedMethod({ @@ -185,10 +197,12 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { countryPath: 'crypto', title: 'Crypto', }) - router.push('/withdraw') + router.push(`/withdraw${methodQueryParam}`) } else if (method.path) { // Other methods with paths - router.push(method.path) + const separator = method.path.includes('?') ? '&' : '?' + const additionalParams = isBankFromSend ? `${separator}method=${methodParam}` : '' + router.push(`${method.path}${additionalParams}`) } } @@ -249,7 +263,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { return (
{ // clear DynamicBankAccountForm data dispatch(bankFormActions.clearFormData()) @@ -347,6 +361,9 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { onPrev={() => { if (flow === 'add') { router.push('/add-money') + } else if (isBankFromSend) { + // if coming from bank send flow, preserve the method param + router.push(`/withdraw?method=${methodParam}`) } else { router.back() } diff --git a/src/components/AddWithdraw/AddWithdrawRouterView.tsx b/src/components/AddWithdraw/AddWithdrawRouterView.tsx index 659d4cc6c..7e9ac2750 100644 --- a/src/components/AddWithdraw/AddWithdrawRouterView.tsx +++ b/src/components/AddWithdraw/AddWithdrawRouterView.tsx @@ -68,6 +68,10 @@ export const AddWithdrawRouterView: FC = ({ const searchParams = useSearchParams() const currencyCode = searchParams.get('currencyCode') + // check if coming from send flow + const methodParam = searchParams.get('method') + const isBankFromSend = methodParam === 'bank' && flow === 'withdraw' + // determine if we should show the full list of methods (countries/crypto) instead of the default view let shouldShowAllMethods = flow === 'withdraw' ? showAllWithdrawMethods : localShowAllMethods const setShouldShowAllMethods = flow === 'withdraw' ? setShowAllWithdrawMethods : setLocalShowAllMethods @@ -201,8 +205,10 @@ export const AddWithdrawRouterView: FC = ({ title: 'To Bank', }) if (account.type === AccountType.MANTECA) { + // preserve method param if coming from send flow + const additionalParams = isBankFromSend ? `&method=${methodParam}` : '' router.push( - `/withdraw/manteca?country=${account.details.countryName}&destination=${account.identifier}` + `/withdraw/manteca?country=${account.details.countryName}&destination=${account.identifier}${additionalParams}` ) } }} @@ -269,7 +275,9 @@ export const AddWithdrawRouterView: FC = ({ inputTitle={mainHeading} viewMode="add-withdraw" onCountryClick={(country) => { - const countryPath = `${baseRoute}/${country.path}` + // preserve method param if coming from send flow + const queryParams = isBankFromSend ? `?method=${methodParam}` : '' + const countryPath = `${baseRoute}/${country.path}${queryParams}` if (flow === 'add' && user) { saveRecentMethod(user.user.userId, country, countryPath) } @@ -283,7 +291,9 @@ export const AddWithdrawRouterView: FC = ({ if (flow === 'add') { setIsDrawerOpen(true) } else { - const cryptoPath = `${baseRoute}/crypto` + // preserve method param if coming from send flow (though crypto shouldn't show this screen) + const queryParams = methodParam ? `?method=${methodParam}` : '' + const cryptoPath = `${baseRoute}/crypto${queryParams}` // Set crypto method and navigate to main page for amount input setSelectedMethod({ type: 'crypto', diff --git a/src/components/Send/views/SendRouter.view.tsx b/src/components/Send/views/SendRouter.view.tsx index 66c2c75b7..3638254a7 100644 --- a/src/components/Send/views/SendRouter.view.tsx +++ b/src/components/Send/views/SendRouter.view.tsx @@ -19,16 +19,18 @@ import { useRecentUsers } from '@/hooks/useRecentUsers' import { getInitialsFromName } from '@/utils/general.utils' import { useCallback, useMemo } from 'react' import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' +import { VerifiedUserLabel } from '@/components/UserHeader' export const SendRouterView = () => { const router = useRouter() const dispatch = useAppDispatch() const searchParams = useSearchParams() - const isSendingByLink = searchParams.get('createLink') === 'true' + const isSendingByLink = searchParams.get('view') === 'link' || searchParams.get('createLink') === 'true' + const isSendingToContacts = searchParams.get('view') === 'contacts' const { recentTransactions, isFetchingRecentUsers } = useRecentUsers() // fallback initials when no recent transactions - const fallbackInitials = ['AA', 'BB', 'CC'] + const fallbackInitials = ['PE', 'AN', 'UT'] const recentUsersAvatarInitials = useCallback(() => { // if we have recent transactions, use them (max 3) @@ -45,7 +47,7 @@ export const SendRouterView = () => { // show loading skeleton while fetching if (isFetchingRecentUsers) { return ( -
+
{[0, 1, 2].map((index) => (
{ }, [isFetchingRecentUsers, recentUsersAvatarInitials]) const redirectToSendByLink = () => { - // Reset send flow state when entering link creation flow + // reset send flow state when entering link creation flow dispatch(sendFlowActions.resetSendFlow()) - router.push(`${window.location.pathname}?createLink=true`) + router.push(`${window.location.pathname}?view=link`) } const handlePrev = () => { - // Reset send flow state when leaving link creation flow + // reset send flow state when leaving link creation flow dispatch(sendFlowActions.resetSendFlow()) router.back() } const handleLinkCtaClick = () => { - router.push(`${window.location.pathname}?createLink=false`) // preserve current URL + router.push(`${window.location.pathname}?view=link`) redirectToSendByLink() } + // handle click on payment method options + const handleMethodClick = (methodId: string) => { + switch (methodId) { + case 'peanut-contacts': + // navigate to contacts/user selection page + router.push('/send?view=contacts') + break + case 'bank': + // navigate to send via bank flow + router.push('/withdraw?method=bank') + break + case 'exchange-or-wallet': + // navigate to external wallet send flow + router.push('/withdraw?method=crypto') + break + case 'mercadopago': + // navigate to mercado pago send flow + router.push('/withdraw/manteca?method=mercado-pago&country=argentina') + break + case 'pix': + // navigate to pix send flow + router.push('/withdraw/manteca?method=pix&country=brazil') + break + default: + console.warn(`Unknown method id: ${methodId}`) + } + } + + // handle user selection from contacts + const handleUserSelect = (username: string) => { + router.push(`/send/${username}`) + } + // extend ACTION_METHODS with component-specific identifier icons const extendedActionMethods = useMemo(() => { return ACTION_METHODS.map((method) => { @@ -154,66 +189,143 @@ export const SendRouterView = () => { return } - return ( -
-
+ // contacts view + if (isSendingToContacts) { + return ( +
-
- -
-
-
- -
-
-
Send money with a link
-
No account needed to receive.
+ + {isFetchingRecentUsers ? ( + // show loading state +
+
Loading contacts...
+
+ ) : recentTransactions.length > 0 ? ( + // show contacts list +
+

Recent activity

+
+ {recentTransactions.map((user, index) => { + const isVerified = user.bridgeKycStatus === 'approved' + return ( + + } + description={`@${user.username}`} + leftIcon={ + + } + onClick={() => handleUserSelect(user.username)} + /> + ) + })} +
+
+ ) : ( + // empty state +
+ +
+
+
+ +
+
+
Send money with a link
+
No account needed to receive.
+
+ +
+
+
+ )} +
+ ) + } + + return ( +
+ +
+ +
+
+
+ +
+
+
Send money with a link
+
No account needed to receive.
-
- + +
+
- + -
- {sendOptions.map((option) => { - // determine right content based on option id - let rightContent - switch (option.id) { - case 'peanut-contacts': - rightContent = recentUsersAvatars - break - case 'mercadopago': - rightContent = - break - default: - rightContent = ( - - ) - break - } - - return ( - {}} - rightContent={rightContent} - /> - ) - })} -
+
+ {sendOptions.map((option) => { + // determine right content based on option id + let rightContent + switch (option.id) { + case 'peanut-contacts': + rightContent = recentUsersAvatars + break + case 'mercadopago': + rightContent = + break + default: + rightContent = ( + + ) + break + } + + return ( + handleMethodClick(option.id)} + rightContent={rightContent} + /> + ) + })}
From 59623d873a6b127126455c73425442573b9caf66 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:12:40 +0530 Subject: [PATCH 3/3] feat: handle supported country list --- src/app/(mobile-ui)/withdraw/page.tsx | 4 +-- .../AddWithdraw/AddWithdrawCountriesList.tsx | 26 +++++++++++++++++-- .../AddWithdraw/AddWithdrawRouterView.tsx | 15 ++++++++++- src/components/Common/CountryList.tsx | 13 ++++++++-- src/components/Send/views/SendRouter.view.tsx | 10 +++++-- 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/app/(mobile-ui)/withdraw/page.tsx b/src/app/(mobile-ui)/withdraw/page.tsx index edf5ffb53..366c8e544 100644 --- a/src/app/(mobile-ui)/withdraw/page.tsx +++ b/src/app/(mobile-ui)/withdraw/page.tsx @@ -54,8 +54,8 @@ export default function WithdrawPage() { countryPath: undefined, }) } else if (isBankFromSend && !selectedMethod) { - // for bank, just show all methods - don't auto-select - setShowAllWithdrawMethods(true) + // for bank from send flow, prefer showing saved accounts first + setShowAllWithdrawMethods(false) } }, [isCryptoFromSend, isBankFromSend, selectedMethod, setSelectedMethod, setShowAllWithdrawMethods]) diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx index 36d91921d..17dc745a3 100644 --- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx +++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx @@ -265,10 +265,26 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { { - // clear DynamicBankAccountForm data + // clear dynamicbankaccountform data dispatch(bankFormActions.clearFormData()) // ensure kyc modal isn't open so late success events don't flip view setIsKycModalOpen(false) + + // if coming from send flow, go back to amount input on /withdraw?method=bank + if (flow === 'withdraw' && isBankFromSend) { + if (currentCountry) { + setSelectedMethod({ + type: 'bridge', + countryPath: currentCountry.path, + currency: currentCountry.currency, + title: 'To Bank', + }) + } + router.push(`/withdraw?method=${methodParam}`) + return + } + + // otherwise go back to list setView('list') }} /> @@ -362,7 +378,13 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { if (flow === 'add') { router.push('/add-money') } else if (isBankFromSend) { - // if coming from bank send flow, preserve the method param + // if coming from bank send flow: set method and go to amount input view + setSelectedMethod({ + type: 'bridge', + countryPath: currentCountry.path, + currency: currentCountry.currency, + title: 'To Bank', + }) router.push(`/withdraw?method=${methodParam}`) } else { router.back() diff --git a/src/components/AddWithdraw/AddWithdrawRouterView.tsx b/src/components/AddWithdraw/AddWithdrawRouterView.tsx index 7e9ac2750..46eed48ca 100644 --- a/src/components/AddWithdraw/AddWithdrawRouterView.tsx +++ b/src/components/AddWithdraw/AddWithdrawRouterView.tsx @@ -274,8 +274,21 @@ export const AddWithdrawRouterView: FC = ({ { - // preserve method param if coming from send flow + // from send flow (bank): set method in context and stay on /withdraw?method=bank + if (flow === 'withdraw' && isBankFromSend) { + // set selected method and let withdraw page move to amount input + setSelectedMethod({ + type: 'bridge', + countryPath: country.path, + currency: country.currency, + title: country.title, + }) + return + } + + // default behaviour: navigate to country page const queryParams = isBankFromSend ? `?method=${methodParam}` : '' const countryPath = `${baseRoute}/${country.path}${queryParams}` if (flow === 'add' && user) { diff --git a/src/components/Common/CountryList.tsx b/src/components/Common/CountryList.tsx index 656e9c467..56a019537 100644 --- a/src/components/Common/CountryList.tsx +++ b/src/components/Common/CountryList.tsx @@ -26,6 +26,9 @@ interface CountryListViewProps { onCryptoClick?: (flow: 'add' | 'withdraw') => void flow?: 'add' | 'withdraw' getRightContent?: (country: CountryData, isSupported: boolean) => ReactNode + // when true and viewMode is 'add-withdraw', disable countries that are not supported + // this is used for the send -> bank flow to prevent selecting unsupported countries + enforceSupportedCountries?: boolean } /** @@ -46,6 +49,7 @@ export const CountryList = ({ onCryptoClick, flow, getRightContent, + enforceSupportedCountries, }: CountryListViewProps) => { const searchParams = useSearchParams() // get currencyCode from search params @@ -142,8 +146,13 @@ export const CountryList = ({ let isSupported = false if (viewMode === 'add-withdraw') { - // all countries supported for claim-request - isSupported = true + // for send->bank flow, enforce only bridge or manteca supported countries + if (enforceSupportedCountries) { + isSupported = isBridgeSupportedCountry + } else { + // otherwise allow all countries + isSupported = true + } } else if (viewMode === 'general-verification') { // all countries can verify even if they cant // withdraw diff --git a/src/components/Send/views/SendRouter.view.tsx b/src/components/Send/views/SendRouter.view.tsx index 3638254a7..8ccea511e 100644 --- a/src/components/Send/views/SendRouter.view.tsx +++ b/src/components/Send/views/SendRouter.view.tsx @@ -80,9 +80,15 @@ export const SendRouterView = () => { } const handlePrev = () => { - // reset send flow state when leaving link creation flow + // reset send flow state and navigate deterministically + // when in sub-views (link or contacts), go back to base send page + // otherwise, go to home dispatch(sendFlowActions.resetSendFlow()) - router.back() + if (isSendingByLink || isSendingToContacts) { + router.push('/send') + } else { + router.push('/home') + } } const handleLinkCtaClick = () => {