diff --git a/src/components/MobileWalletDialog/routes/CreateWallet/CreateSuccess.tsx b/src/components/MobileWalletDialog/routes/CreateWallet/CreateSuccess.tsx index c7110c3db06..f515afaee3d 100644 --- a/src/components/MobileWalletDialog/routes/CreateWallet/CreateSuccess.tsx +++ b/src/components/MobileWalletDialog/routes/CreateWallet/CreateSuccess.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Text, VStack } from '@chakra-ui/react' import { useQueryClient } from '@tanstack/react-query' -import { useCallback } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { IoIosCheckmarkCircle } from 'react-icons/io' import { useTranslate } from 'react-polyglot' import type { Location } from 'react-router-dom' @@ -33,82 +33,111 @@ export const CreateSuccess = ({ onClose }: CreateSuccessProps) => { const queryClient = useQueryClient() const { dispatch, getAdapter } = useWallet() const localWallet = useLocalWallet() + const saveAttemptedRef = useRef(false) + const connectionAttemptedRef = useRef(false) + const savedWalletRef = useRef> | null>(null) + const [isConnecting, setIsConnecting] = useState(false) + + const saveWallet = useCallback(async () => { + if (saveAttemptedRef.current) return + saveAttemptedRef.current = true - const saveAndSelectWallet = useCallback(async () => { if (location.state?.vault?.label && location.state?.vault?.mnemonic) { - const wallet = await addWallet({ - label: location.state.vault.label, - mnemonic: location.state.vault.mnemonic, - }) + try { + const wallet = await addWallet({ + label: location.state.vault.label, + mnemonic: location.state.vault.mnemonic, + }) - if (!wallet) { - return + if (wallet) { + savedWalletRef.current = wallet + await queryClient.invalidateQueries({ queryKey: ['listWallets'] }) + } + } catch (e) { + console.error('Failed to save wallet:', e) + saveAttemptedRef.current = false } + } + }, [location.state?.vault, queryClient]) + + const handleWalletConnection = useCallback(async () => { + if (connectionAttemptedRef.current) return + connectionAttemptedRef.current = true + const wallet = savedWalletRef.current + if (!wallet || !location.state?.vault?.mnemonic) { + connectionAttemptedRef.current = false + return + } + + try { const adapter = await getAdapter(KeyManager.Mobile) const deviceId = wallet.id if (adapter && deviceId) { const { name, icon } = MobileConfig - try { - const walletInstance = await adapter.pairDevice(deviceId) - await walletInstance?.loadDevice({ mnemonic: location.state?.vault?.mnemonic ?? '' }) - - if (!(await walletInstance?.isInitialized())) { - await walletInstance?.initialize() - } - dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: walletInstance, - name, - icon, - deviceId, - meta: { label: location.state?.vault?.label }, - connectedType: KeyManager.Mobile, - }, - }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - dispatch({ - type: WalletActions.SET_CONNECTOR_TYPE, - payload: { modalType: KeyManager.Mobile, isMipdProvider: false }, - }) - - localWallet.setLocalWallet({ type: KeyManager.Mobile, deviceId }) - localWallet.setLocalNativeWalletName(location.state?.vault?.label ?? 'label') - } catch (e) { - console.log(e) + const walletInstance = await adapter.pairDevice(deviceId) + await walletInstance?.loadDevice({ mnemonic: location.state?.vault?.mnemonic ?? '' }) + + if (!(await walletInstance?.isInitialized())) { + await walletInstance?.initialize() } + dispatch({ + type: WalletActions.SET_WALLET, + payload: { + wallet: walletInstance, + name, + icon, + deviceId, + meta: { label: location.state?.vault?.label }, + connectedType: KeyManager.Mobile, + }, + }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: KeyManager.Mobile, isMipdProvider: false }, + }) + + localWallet.setLocalWallet({ type: KeyManager.Mobile, deviceId }) + localWallet.setLocalNativeWalletName(location.state?.vault?.label ?? 'label') + appDispatch(setWelcomeModal({ show: true })) } - await queryClient.invalidateQueries({ queryKey: ['listWallets'] }) wallet.revoke() - appDispatch(setWelcomeModal({ show: true })) + savedWalletRef.current = null + } catch (e) { + console.error('Failed to connect wallet:', e) + connectionAttemptedRef.current = false + } + }, [location.state?.vault, dispatch, getAdapter, localWallet, appDispatch, setWelcomeModal]) + + useEffect(() => { + saveWallet() + }, [saveWallet]) + + useEffect(() => { + return () => { + if (savedWalletRef.current) { + savedWalletRef.current.revoke() + savedWalletRef.current = null + } + } + }, []) + + const handleViewWallet = useCallback(async () => { + setIsConnecting(true) + try { + await handleWalletConnection() + onClose() + } finally { + setIsConnecting(false) } - }, [ - location.state?.vault, - dispatch, - getAdapter, - localWallet, - queryClient, - appDispatch, - setWelcomeModal, - ]) - - const handleViewWallet = useCallback(() => { - saveAndSelectWallet() - onClose() - }, [onClose, saveAndSelectWallet]) - - const handleClose = useCallback(() => { - saveAndSelectWallet() - onClose() - appDispatch(setWelcomeModal({ show: true })) - }, [onClose, appDispatch, setWelcomeModal, saveAndSelectWallet]) + }, [onClose, handleWalletConnection]) return ( - + @@ -126,7 +155,14 @@ export const CreateSuccess = ({ onClose }: CreateSuccessProps) => { - diff --git a/src/components/MobileWalletDialog/routes/ImportWallet/ImportSuccess.tsx b/src/components/MobileWalletDialog/routes/ImportWallet/ImportSuccess.tsx index c822e7dfd1a..5e64512380e 100644 --- a/src/components/MobileWalletDialog/routes/ImportWallet/ImportSuccess.tsx +++ b/src/components/MobileWalletDialog/routes/ImportWallet/ImportSuccess.tsx @@ -1,6 +1,7 @@ import { Button, Icon, VStack } from '@chakra-ui/react' import type { NativeHDWallet } from '@shapeshiftoss/hdwallet-native' -import { useCallback } from 'react' +import { useQueryClient } from '@tanstack/react-query' +import { useCallback, useEffect, useRef, useState } from 'react' import { IoIosCheckmarkCircle } from 'react-icons/io' import { useTranslate } from 'react-polyglot' import type { Location } from 'react-router-dom' @@ -32,12 +33,35 @@ export const ImportSuccess = ({ onClose }: ImportSuccessProps) => { const { getAdapter, dispatch } = useWallet() const localWallet = useLocalWallet() const translate = useTranslate() + const queryClient = useQueryClient() + const connectionAttemptedRef = useRef(false) + const refreshAttemptedRef = useRef(false) + const [isConnecting, setIsConnecting] = useState(false) + + const refreshWalletList = useCallback(async () => { + if (refreshAttemptedRef.current) return + refreshAttemptedRef.current = true + try { + await queryClient.invalidateQueries({ queryKey: ['listWallets'] }) + } catch (e) { + console.error('Failed to refresh wallet list:', e) + refreshAttemptedRef.current = false + } + }, [queryClient]) + + useEffect(() => { + refreshWalletList() + }, [refreshWalletList]) const handleWalletConnection = useCallback(async () => { + if (connectionAttemptedRef.current) return if (!location.state?.vault) return - const adapter = await getAdapter(KeyManager.Mobile) - if (!adapter) throw new Error('Native adapter not found') + connectionAttemptedRef.current = true + try { + const adapter = await getAdapter(KeyManager.Mobile) + if (!adapter) throw new Error('Native adapter not found') + const deviceId = location.state.vault.id ?? '' const wallet = (await adapter.pairDevice(deviceId)) as NativeHDWallet const mnemonic = location.state.vault.mnemonic @@ -67,18 +91,29 @@ export const ImportSuccess = ({ onClose }: ImportSuccessProps) => { type: WalletActions.SET_CONNECTOR_TYPE, payload: { modalType: KeyManager.Mobile, isMipdProvider: false }, }) + appDispatch(setWelcomeModal({ show: true })) } } catch (e) { - console.log(e) + console.error('Failed to connect wallet:', e) + connectionAttemptedRef.current = false + } + }, [getAdapter, location.state?.vault, dispatch, localWallet, appDispatch, setWelcomeModal]) + + const handleViewWallet = useCallback(async () => { + setIsConnecting(true) + try { + await handleWalletConnection() + onClose() + setTimeout(() => location.state?.vault?.revoke(), 500) + } finally { + setIsConnecting(false) } - }, [getAdapter, location.state?.vault, dispatch, localWallet]) + }, [onClose, handleWalletConnection, location.state?.vault]) const handleClose = useCallback(() => { - handleWalletConnection() onClose() - appDispatch(setWelcomeModal({ show: true })) setTimeout(() => location.state?.vault?.revoke(), 500) - }, [onClose, appDispatch, setWelcomeModal, handleWalletConnection, location.state?.vault]) + }, [onClose, location.state?.vault]) return ( @@ -105,7 +140,14 @@ export const ImportSuccess = ({ onClose }: ImportSuccessProps) => { - diff --git a/src/pages/ConnectWallet/MobileConnect.tsx b/src/pages/ConnectWallet/MobileConnect.tsx index f9255936c28..a3220cfcc7d 100644 --- a/src/pages/ConnectWallet/MobileConnect.tsx +++ b/src/pages/ConnectWallet/MobileConnect.tsx @@ -14,6 +14,7 @@ import { Stack, } from '@chakra-ui/react' import { keyframes } from '@emotion/react' +import { useQuery as useReactQuery } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' @@ -33,7 +34,6 @@ import { SlideTransitionY } from '@/components/SlideTransitionY' import { RawText, Text } from '@/components/Text' import { WalletActions } from '@/context/WalletProvider/actions' import { listWallets } from '@/context/WalletProvider/MobileWallet/mobileMessageHandlers' -import type { RevocableWallet } from '@/context/WalletProvider/MobileWallet/RevocableWallet' import { useModal } from '@/hooks/useModal/useModal' import { useQuery } from '@/hooks/useQuery/useQuery' import { useWallet } from '@/hooks/useWallet/useWallet' @@ -67,10 +67,7 @@ const BodyText: React.FC = props => ( export const MobileConnect = () => { const { dispatch, state } = useWallet() const translate = useTranslate() - const [wallets, setWallets] = useState([]) - const [error, setError] = useState(null) const [hideWallets, setHideWallets] = useState(false) - const [isLoading, setIsLoading] = useState(true) const scaleFadeAnimation = `${scaleFade} 0.6s cubic-bezier(0.76, 0, 0.24, 1)` const hasWallet = Boolean(state.walletInfo?.deviceId) const navigate = useNavigate() @@ -78,6 +75,32 @@ export const MobileConnect = () => { const mobileWalletDialog = useModal('mobileWalletDialog') const [isWaitingForRedirection, setIsWaitingForRedirection] = useState(false) + const { + isLoading, + data: wallets = [], + error: queryError, + } = useReactQuery({ + queryKey: ['listWallets'], + staleTime: 0, + gcTime: 0, + retry: false, + refetchOnMount: true, + queryFn: listWallets, + }) + + const error = useMemo(() => { + if (queryError) { + if (queryError instanceof Error) { + console.error('Failed to fetch wallets:', queryError.message) + } + return 'walletProvider.shapeShift.load.error.fetchingWallets' + } + if (!wallets.length && !isLoading) { + return 'walletProvider.shapeShift.load.error.noWallet' + } + return null + }, [queryError, wallets.length, isLoading]) + const handleOpenCreateWallet = useCallback(() => { mobileWalletDialog.open({ defaultRoute: MobileWalletDialogRoutes.Create }) }, [mobileWalletDialog]) @@ -122,34 +145,13 @@ export const MobileConnect = () => { } }, [navigate, hasWallet, query, state, dispatch, setIsWaitingForRedirection]) - useEffect(() => { - if (!wallets.length) { - setIsLoading(true) // Set loading state to true when fetching wallets - ;(async () => { - try { - const vaults = await listWallets() - if (!vaults.length) { - setError('walletProvider.shapeShift.load.error.noWallet') - } else { - setWallets(vaults) - } - } catch (e) { - console.log(e) - setError('An error occurred while fetching wallets.') - } finally { - setIsLoading(false) // Set loading state to false when fetching is done - } - })() - } - }, [wallets]) - const handleToggleWallets = useCallback(() => { - setHideWallets(!hideWallets) // allow users with saved wallets to toggle between saved and create/import + setHideWallets(!hideWallets) }, [hideWallets]) useEffect(() => { if (!wallets.length && !isLoading) { - setHideWallets(true) // If they have no wallets, show the default create or import + setHideWallets(true) } }, [isLoading, wallets.length])