From f29fbc3b2f4cc07df01582c939bbfc79fdb6e0bf Mon Sep 17 00:00:00 2001 From: Christopher Howard Date: Tue, 22 Aug 2023 17:06:02 -0500 Subject: [PATCH] [BX-944] State Restoration: Persistence (#852) --- src/core/state/popupInstances/index.ts | 133 ++++++++++++++++++ src/core/utils/tabs.ts | 6 + .../background/handlers/handleDisconnect.ts | 12 ++ src/entries/background/index.ts | 2 + src/entries/popup/App.tsx | 2 + src/entries/popup/hooks/send/useSendInputs.ts | 20 ++- src/entries/popup/hooks/send/useSendState.ts | 7 +- src/entries/popup/hooks/swap/useSwapAssets.ts | 20 ++- src/entries/popup/hooks/swap/useSwapInputs.ts | 50 +++++-- .../popup/hooks/swap/useSwapQuoteHandler.ts | 2 +- src/entries/popup/hooks/useExpiryListener.ts | 34 +++++ src/entries/popup/pages/home/index.tsx | 7 +- .../popup/pages/send/ToAddressInput.tsx | 13 +- src/entries/popup/pages/send/index.tsx | 43 +++++- .../swap/SwapReviewSheet/SwapReviewSheet.tsx | 5 +- src/entries/popup/pages/swap/index.tsx | 48 ++++++- 16 files changed, 368 insertions(+), 36 deletions(-) create mode 100644 src/core/state/popupInstances/index.ts create mode 100644 src/entries/background/handlers/handleDisconnect.ts create mode 100644 src/entries/popup/hooks/useExpiryListener.ts diff --git a/src/core/state/popupInstances/index.ts b/src/core/state/popupInstances/index.ts new file mode 100644 index 0000000000..45533c7343 --- /dev/null +++ b/src/core/state/popupInstances/index.ts @@ -0,0 +1,133 @@ +import { Address } from 'wagmi'; +import create from 'zustand'; + +import { ParsedSearchAsset } from '~/core/types/assets'; +import { ChainId } from '~/core/types/chains'; +import { isNativePopup } from '~/core/utils/tabs'; +import { IndependentField } from '~/entries/popup/hooks/swap/useSwapInputs'; +import { Tab } from '~/entries/popup/pages/home'; + +import { createStore } from '../internal/createStore'; + +type SendAddress = Address | 'eth' | ''; + +interface PopupInstance { + activeTab: Tab; + sendAddress: Address | string | null; + sendAmount: string | null; + sendField: 'asset' | 'native'; + sendTokenAddressAndChain: { address: SendAddress; chainId: ChainId } | null; + swapAmount: string | null; + swapField: IndependentField | null; + swapTokenToBuy: ParsedSearchAsset | null; + swapTokenToSell: ParsedSearchAsset | null; +} + +const DEFAULT_POPUP_INSTANCE_VALUES: PopupInstance = { + activeTab: 'tokens', + sendAddress: null, + sendAmount: null, + sendField: 'asset', + sendTokenAddressAndChain: null, + swapAmount: null, + swapField: null, + swapTokenToBuy: null, + swapTokenToSell: null, +}; + +export interface PopupInstanceStore extends PopupInstance { + resetValues: () => Promise; + saveActiveTab: ({ tab }: { tab: Tab }) => Promise; + saveSendAddress: ({ + address, + }: { + address: Address | string; + }) => Promise; + saveSendAmount: ({ amount }: { amount: string }) => Promise; + saveSendField: ({ field }: { field: 'asset' | 'native' }) => Promise; + saveSendTokenAddressAndChain: ({ + address, + chainId, + }: { + address: SendAddress; + chainId: ChainId; + }) => Promise; + saveSwapAmount: ({ amount }: { amount: string }) => Promise; + saveSwapField: ({ field }: { field: IndependentField }) => Promise; + saveSwapTokenToBuy: ({ + token, + }: { + token: ParsedSearchAsset | null; + }) => Promise; + saveSwapTokenToSell: ({ + token, + }: { + token: ParsedSearchAsset | null; + }) => Promise; + setupPort: () => Promise; +} + +export const popupInstanceStore = createStore( + (set) => ({ + ...DEFAULT_POPUP_INSTANCE_VALUES, + resetValues: popupInstanceHandlerFactory(() => + set(DEFAULT_POPUP_INSTANCE_VALUES), + ), + saveActiveTab: popupInstanceHandlerFactory(({ tab }) => { + set({ activeTab: tab }); + }), + saveSendAddress: popupInstanceHandlerFactory(({ address }) => { + set({ sendAddress: address }); + }), + saveSendAmount: popupInstanceHandlerFactory(({ amount }) => { + set({ sendAmount: amount }); + }), + saveSendField: popupInstanceHandlerFactory(({ field }) => { + set({ sendField: field }); + }), + saveSendTokenAddressAndChain: popupInstanceHandlerFactory( + ({ address, chainId }) => { + set({ sendTokenAddressAndChain: { address, chainId } }); + }, + ), + saveSwapAmount: popupInstanceHandlerFactory(({ amount }) => { + set({ swapAmount: amount }); + }), + saveSwapField: popupInstanceHandlerFactory(({ field }) => { + set({ swapField: field }); + }), + saveSwapTokenToBuy: popupInstanceHandlerFactory(({ token }) => { + set({ swapTokenToBuy: token }); + }), + saveSwapTokenToSell: popupInstanceHandlerFactory(({ token }) => { + set({ swapTokenToSell: token }); + }), + setupPort: popupInstanceHandlerFactory(() => { + chrome.runtime.connect({ name: 'popup' }); + }), + }), + { + persist: { + name: 'popupInstance', + version: 0, + }, + }, +); + +export const usePopupInstanceStore = create(popupInstanceStore); + +// creates handlers that only work in popup context and passes through callback types +function popupInstanceHandlerFactory< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + THandler extends (...args: any) => any, +>(handler: THandler) { + type handlerParams = Parameters; + return async ( + ...args: handlerParams + ): Promise> => { + const isPopup = await isNativePopup(); + if (isPopup) { + return handler(...(<[]>args)); + } + }; +} diff --git a/src/core/utils/tabs.ts b/src/core/utils/tabs.ts index 020b1d354b..1fc8c582e0 100644 --- a/src/core/utils/tabs.ts +++ b/src/core/utils/tabs.ts @@ -24,3 +24,9 @@ export const goToNewTab = ({ // Edge sometimes returns `Tab creation is restricted in standalone sidebar mode. } }; + +export const isNativePopup = async () => { + return new Promise((resolve) => { + chrome.tabs?.getCurrent((tab) => resolve(!tab)); + }); +}; diff --git a/src/entries/background/handlers/handleDisconnect.ts b/src/entries/background/handlers/handleDisconnect.ts new file mode 100644 index 0000000000..9ab8f44a3d --- /dev/null +++ b/src/entries/background/handlers/handleDisconnect.ts @@ -0,0 +1,12 @@ +const POPUP_INSTANCE_DATA_EXPIRY = 180000; +export function handleDisconnect() { + chrome.runtime.onConnect.addListener(function (port) { + if (port.name === 'popup') { + port.onDisconnect.addListener(async function () { + await chrome.storage.session.set({ + expiry: Date.now() + POPUP_INSTANCE_DATA_EXPIRY, + }); + }); + } + }); +} diff --git a/src/entries/background/index.ts b/src/entries/background/index.ts index eb2744e208..6a34a2f42c 100644 --- a/src/entries/background/index.ts +++ b/src/entries/background/index.ts @@ -6,6 +6,7 @@ import { initializeSentry } from '~/core/sentry'; import { syncStores } from '~/core/state/internal/syncStores'; import { createWagmiClient } from '~/core/wagmi'; +import { handleDisconnect } from './handlers/handleDisconnect'; import { handleInstallExtension } from './handlers/handleInstallExtension'; import { handleKeepAlive } from './handlers/handleKeepAlive'; import { handleProviderRequest } from './handlers/handleProviderRequest'; @@ -25,6 +26,7 @@ handleProviderRequest({ popupMessenger, inpageMessenger }); handleTabAndWindowUpdates(); handleSetupInpage(); handleWallets(); +handleDisconnect(); syncStores(); uuid4(); initFCM(); diff --git a/src/entries/popup/App.tsx b/src/entries/popup/App.tsx index 4b4e3f7ddb..edee150832 100644 --- a/src/entries/popup/App.tsx +++ b/src/entries/popup/App.tsx @@ -22,6 +22,7 @@ import { HWRequestListener } from './components/HWRequestListener/HWRequestListe import { IdleTimer } from './components/IdleTimer/IdleTimer'; import { OnboardingKeepAlive } from './components/OnboardingKeepAlive'; import { AuthProvider } from './hooks/useAuth'; +import { useExpiryListener } from './hooks/useExpiryListener'; import { useIsFullScreen } from './hooks/useIsFullScreen'; import { PlaygroundComponents } from './pages/_playgrounds'; import { RainbowConnector } from './wagmi/RainbowConnector'; @@ -37,6 +38,7 @@ const wagmiClient = createWagmiClient({ export function App() { const { currentLanguage } = useCurrentLanguageStore(); const { deviceId } = useDeviceIdStore(); + useExpiryListener(); React.useEffect(() => { // Disable analytics & sentry for e2e and dev mode diff --git a/src/entries/popup/hooks/send/useSendInputs.ts b/src/entries/popup/hooks/send/useSendInputs.ts index 4618910185..e7f1f4060d 100644 --- a/src/entries/popup/hooks/send/useSendInputs.ts +++ b/src/entries/popup/hooks/send/useSendInputs.ts @@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { supportedCurrencies } from '~/core/references'; import { useCurrentCurrencyStore } from '~/core/state'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { ParsedAddressAsset } from '~/core/types/assets'; import { GasFeeLegacyParams, GasFeeParams } from '~/core/types/gas'; import { @@ -86,13 +87,22 @@ export const useSendInputs = ({ } }, [asset, currentCurrency, independentAmount, independentField]); - const assetAmount = useMemo( - () => + const { saveSendAmount, saveSendField } = usePopupInstanceStore(); + const assetAmount = useMemo(() => { + const amount = independentField === 'asset' ? independentAmount - : dependentAmountDisplay?.amount, - [dependentAmountDisplay, independentAmount, independentField], - ); + : dependentAmountDisplay?.amount; + saveSendField({ field: independentField }); + saveSendAmount({ amount: independentAmount }); + return amount; + }, [ + dependentAmountDisplay, + independentAmount, + independentField, + saveSendAmount, + saveSendField, + ]); const setInputValue = useCallback((newValue: string) => { if (independentFieldRef.current) { diff --git a/src/entries/popup/hooks/send/useSendState.ts b/src/entries/popup/hooks/send/useSendState.ts index 156cd4b2c6..3823b6d0c9 100644 --- a/src/entries/popup/hooks/send/useSendState.ts +++ b/src/entries/popup/hooks/send/useSendState.ts @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import { Address } from 'wagmi'; import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { ParsedAddressAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; import { isNativeAsset } from '~/core/utils/chains'; @@ -22,6 +23,7 @@ export const useSendState = ({ rawMaxAssetBalanceAmount: string; }) => { const [toAddressOrName, setToAddressOrName] = useState
(''); + const { saveSendAddress } = usePopupInstanceStore(); const { currentCurrency } = useCurrentCurrencyStore(); const [, setAsset] = useState(); @@ -84,6 +86,9 @@ export const useSendState = ({ txToAddress, value, setAsset, - setToAddressOrName, + setToAddressOrName: (address: Address | string) => { + setToAddressOrName(address); + saveSendAddress({ address }); + }, }; }; diff --git a/src/entries/popup/hooks/swap/useSwapAssets.ts b/src/entries/popup/hooks/swap/useSwapAssets.ts index e4cc007234..051d18b0ec 100644 --- a/src/entries/popup/hooks/swap/useSwapAssets.ts +++ b/src/entries/popup/hooks/swap/useSwapAssets.ts @@ -6,6 +6,7 @@ import { selectUserAssetsListByChainId } from '~/core/resources/_selectors/asset import { useAssets, useUserAssets } from '~/core/resources/assets'; import { useCurrentAddressStore, useCurrentCurrencyStore } from '~/core/state'; import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { ParsedAsset, ParsedSearchAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; import { SearchAsset } from '~/core/types/search'; @@ -39,7 +40,7 @@ export const useSwapAssets = () => { const [assetToSell, setAssetToSellState] = useState< ParsedSearchAsset | SearchAsset | null >(null); - const [assetToBuy, setAssetToBuy] = useState< + const [assetToBuy, setAssetToBuyState] = useState< ParsedSearchAsset | SearchAsset | null >(null); @@ -60,6 +61,8 @@ export const useSwapAssets = () => { const debouncedAssetToSellFilter = useDebounce(assetToSellFilter, 200); const debouncedAssetToBuyFilter = useDebounce(assetToBuyFilter, 200); + const { saveSwapTokenToBuy, saveSwapTokenToSell } = usePopupInstanceStore(); + const { data: userAssets = [] } = useUserAssets( { address: currentAddress, @@ -154,6 +157,14 @@ export const useSwapAssets = () => { }); }, [assetToSell, assetToSellWithPrice, userAssets]); + const setAssetToBuy = useCallback( + (asset: ParsedSearchAsset | null) => { + saveSwapTokenToBuy({ token: asset }); + setAssetToBuyState(asset); + }, + [saveSwapTokenToBuy], + ); + const setAssetToSell = useCallback( (asset: ParsedSearchAsset | null) => { if ( @@ -162,12 +173,15 @@ export const useSwapAssets = () => { assetToBuy?.address === asset?.address && assetToBuy?.chainId === asset?.chainId ) { - setAssetToBuy(prevAssetToSell === undefined ? null : prevAssetToSell); + setAssetToBuyState( + prevAssetToSell === undefined ? null : prevAssetToSell, + ); } setAssetToSellState(asset); + saveSwapTokenToSell({ token: asset }); asset?.chainId && setOutputChainId(asset?.chainId); }, - [assetToBuy, prevAssetToSell], + [assetToBuy, prevAssetToSell, saveSwapTokenToSell], ); return { diff --git a/src/entries/popup/hooks/swap/useSwapInputs.ts b/src/entries/popup/hooks/swap/useSwapInputs.ts index 643f500744..3e5465c6f4 100644 --- a/src/entries/popup/hooks/swap/useSwapInputs.ts +++ b/src/entries/popup/hooks/swap/useSwapInputs.ts @@ -1,5 +1,6 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { ParsedSearchAsset } from '~/core/types/assets'; import { GasFeeLegacyParams, GasFeeParams } from '~/core/types/gas'; import { POPUP_DIMENSIONS } from '~/core/utils/dimensions'; @@ -48,6 +49,12 @@ export const useSwapInputs = ({ const [assetToSellNativeValue, setAssetToSellNativeValue] = useState(''); const [assetToBuyValue, setAssetToBuyValue] = useState(''); + const { + saveSwapAmount, + saveSwapField, + swapField: savedSwapField, + } = usePopupInstanceStore(); + const assetToSellInputRef = useRef(null); const assetToSellNativeInputRef = useRef(null); const assetToBuyInputRef = useRef(null); @@ -62,18 +69,30 @@ export const useSwapInputs = ({ return; } if (field === 'buyField' && !assetToBuy) return; + saveSwapField({ field }); setIndependentField(field); }, - [assetToBuy, assetToSell], + [assetToBuy, assetToSell, saveSwapField], ); - const setAssetToSellInputValue = useCallback((value: string) => { - setAssetToSellDropdownClosed(true); - setAssetToSellValue(value); - setIndependentField('sellField'); - setIndependentValue(value); + useEffect(() => { + if (savedSwapField) { + setIndependentField(savedSwapField); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const setAssetToSellInputValue = useCallback( + (value: string) => { + setAssetToSellDropdownClosed(true); + saveSwapAmount({ amount: value }); + setAssetToSellValue(value); + setIndependentFieldIfOccupied('sellField'); + setIndependentValue(value); + }, + [saveSwapAmount, setIndependentFieldIfOccupied], + ); + const setAssetToSellInputNativeValue = useCallback( (value: string) => { setAssetToSellDropdownClosed(true); @@ -89,10 +108,12 @@ export const useSwapInputs = ({ ) : '', ); + saveSwapAmount({ amount: value }); }, [ assetToSell?.decimals, assetToSell?.price?.value, + saveSwapAmount, setIndependentFieldIfOccupied, ], ); @@ -103,8 +124,9 @@ export const useSwapInputs = ({ setAssetToBuyValue(value); setIndependentFieldIfOccupied('buyField'); setIndependentValue(value); + saveSwapAmount({ amount: value }); }, - [setIndependentFieldIfOccupied], + [saveSwapAmount, setIndependentFieldIfOccupied], ); const onAssetToSellInputOpen = useCallback( @@ -158,12 +180,14 @@ export const useSwapInputs = ({ if (isCrosschainSwap) { setAssetToBuyValue(''); setAssetToSellValue(assetToBuyValue); - setIndependentFieldIfOccupied('sellField'); + setIndependentField('sellField'); + saveSwapField({ field: 'sellField' }); focusOnInput(assetToSellInputRef); } else if (independentField === 'buyField') { setAssetToBuyValue(''); setAssetToSellValue(independentValue); - setIndependentFieldIfOccupied('sellField'); + setIndependentField('sellField'); + saveSwapField({ field: 'sellField' }); focusOnInput(assetToSellInputRef); } else if ( independentField === 'sellField' || @@ -177,7 +201,8 @@ export const useSwapInputs = ({ setAssetToBuyValue(tokenValue); setIndependentValue(tokenValue); setAssetToSellNativeValue(''); - setIndependentFieldIfOccupied('buyField'); + setIndependentField('buyField'); + saveSwapField({ field: 'buyField' }); focusOnInput(assetToBuyInputRef); } setAssetToBuy(assetToSell); @@ -191,9 +216,10 @@ export const useSwapInputs = ({ assetToSellValue, independentField, independentValue, + saveSwapField, setAssetToBuy, setAssetToSell, - setIndependentFieldIfOccupied, + setIndependentField, ]); const assetToSellDisplay = useMemo( diff --git a/src/entries/popup/hooks/swap/useSwapQuoteHandler.ts b/src/entries/popup/hooks/swap/useSwapQuoteHandler.ts index 9ec3fa7c98..d6f43c954e 100644 --- a/src/entries/popup/hooks/swap/useSwapQuoteHandler.ts +++ b/src/entries/popup/hooks/swap/useSwapQuoteHandler.ts @@ -28,7 +28,7 @@ export const useSwapQuoteHandler = ({ const prevQuote = usePrevious(quote); useEffect(() => { - if (!(quote as QuoteError)?.error) { + if (quote && !(quote as QuoteError)?.error) { const { sellAmountDisplay, buyAmountDisplay } = (quote || {}) as | Quote | CrosschainQuote; diff --git a/src/entries/popup/hooks/useExpiryListener.ts b/src/entries/popup/hooks/useExpiryListener.ts new file mode 100644 index 0000000000..1b118086f9 --- /dev/null +++ b/src/entries/popup/hooks/useExpiryListener.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; + +import { useCurrentAddressStore } from '~/core/state'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; + +import usePrevious from './usePrevious'; + +export function useExpiryListener() { + const { resetValues, setupPort } = usePopupInstanceStore(); + const { currentAddress } = useCurrentAddressStore(); + const previousAddress = usePrevious(currentAddress); + + const checkExpiry = async () => { + const expiryEntry = await chrome.storage.session.get('expiry'); + const expired = Date.now() > (expiryEntry?.expiry || 0); + if (expired) { + await resetValues(); + } + }; + + useEffect(() => { + // this port's disconnection will let the background know to register a new expiry date + setupPort(); + // reset popup instance data if the expiry date has passed + checkExpiry(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (previousAddress && previousAddress !== currentAddress) { + resetValues(); + } + }, [currentAddress, previousAddress, resetValues]); +} diff --git a/src/entries/popup/pages/home/index.tsx b/src/entries/popup/pages/home/index.tsx index 49d46f3ec5..3701502292 100644 --- a/src/entries/popup/pages/home/index.tsx +++ b/src/entries/popup/pages/home/index.tsx @@ -14,7 +14,6 @@ import { useState, useTransition, } from 'react'; -import { useLocation } from 'react-router-dom'; import { useAccount } from 'wagmi'; import { analytics } from '~/analytics'; @@ -22,6 +21,7 @@ import { event } from '~/analytics/event'; import { identifyWalletTypes } from '~/analytics/identify/walletTypes'; import { shortcuts } from '~/core/references/shortcuts'; import { useCurrentAddressStore } from '~/core/state'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { usePendingRequestStore } from '~/core/state/requests'; import { AccentColorProvider, Box, Inset, Separator } from '~/design-system'; import { useContainerRef } from '~/design-system/components/AnimatedRoute/AnimatedRoute'; @@ -58,8 +58,7 @@ const TAB_BAR_HEIGHT = 34; const TOP_NAV_HEIGHT = 65; function Tabs() { - const { state } = useLocation(); - const [activeTab, setActiveTab] = useState(state?.activeTab || 'tokens'); + const { activeTab, saveActiveTab } = usePopupInstanceStore(); const { trackShortcut } = useKeyboardAnalytics(); const [, startTransition] = useTransition(); @@ -69,7 +68,7 @@ function Tabs() { const onSelectTab = (tab: Tab) => { prevScrollPosition.current = containerRef.current?.scrollTop; startTransition(() => { - setActiveTab(tab); + saveActiveTab({ tab }); }); }; diff --git a/src/entries/popup/pages/send/ToAddressInput.tsx b/src/entries/popup/pages/send/ToAddressInput.tsx index 718be19fd1..fe9f867d9d 100644 --- a/src/entries/popup/pages/send/ToAddressInput.tsx +++ b/src/entries/popup/pages/send/ToAddressInput.tsx @@ -13,6 +13,7 @@ import { Address } from 'wagmi'; import { i18n } from '~/core/languages'; import { useCurrentAddressStore } from '~/core/state'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { useWalletOrderStore } from '~/core/state/walletOrder'; import { truncateAddress } from '~/core/utils/address'; import { @@ -330,12 +331,16 @@ export const ToAddressInput = React.forwardRef( }); const { currentAddress } = useCurrentAddressStore(); const selectableWallets = wallets.filter((a) => a !== currentAddress); + const { sendAddress: savedSendAddress } = usePopupInstanceStore(); useEffect(() => { - setTimeout(() => { - openDropdown(); - }, 200); - }, [openDropdown]); + if (!toAddressOrName && !savedSendAddress) { + setTimeout(() => { + openDropdown(); + }, 200); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( <> diff --git a/src/entries/popup/pages/send/index.tsx b/src/entries/popup/pages/send/index.tsx index 0700a7d5f3..91be1583ee 100644 --- a/src/entries/popup/pages/send/index.tsx +++ b/src/entries/popup/pages/send/index.tsx @@ -20,6 +20,10 @@ import { useGasStore } from '~/core/state'; import { useContactsStore } from '~/core/state/contacts'; import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme'; +import { + popupInstanceStore, + usePopupInstanceStore, +} from '~/core/state/popupInstances'; import { useSelectedTokenStore } from '~/core/state/selectedToken'; import { ChainId } from '~/core/types/chains'; import { @@ -181,12 +185,21 @@ export function Send() { const closeReviewSheet = useCallback(() => setShowReviewSheet(false), []); + const { + sendAddress, + sendAmount, + sendField, + sendTokenAddressAndChain, + saveSendTokenAddressAndChain, + } = usePopupInstanceStore(); + const handleSend = useCallback( async (callback?: () => void) => { if (!config.send_enabled) return; try { const { type } = await getWallet(fromAddress); + const { saveActiveTab } = popupInstanceStore.getState(); // Change the label while we wait for confirmation if (type === 'HardwareWalletKeychain') { @@ -228,7 +241,8 @@ export function Send() { transaction, }); callback?.(); - navigate(ROUTES.HOME, { state: { activeTab: 'activity' } }); + saveActiveTab({ tab: 'activity' }); + navigate(ROUTES.HOME); analytics.track(event.sendSubmitted, { assetSymbol: asset?.symbol, assetName: asset?.name, @@ -267,12 +281,20 @@ export function Send() { const selectAsset = useCallback( (address: Address | typeof ETH_ADDRESS | '', chainId: ChainId) => { selectAssetAddressAndChain(address as Address, chainId); + saveSendTokenAddressAndChain({ + address, + chainId, + }); setIndependentAmount(''); setTimeout(() => { valueInputRef?.current?.focus(); }, 300); }, - [selectAssetAddressAndChain, setIndependentAmount], + [ + saveSendTokenAddressAndChain, + selectAssetAddressAndChain, + setIndependentAmount, + ], ); useEffect(() => { @@ -309,8 +331,23 @@ export function Send() { selectAsset(selectedToken.address, selectedToken.chainId); // clear selected token setSelectedToken(); + } else if (sendTokenAddressAndChain) { + selectAsset( + sendTokenAddressAndChain.address, + sendTokenAddressAndChain.chainId, + ); + } + if (sendAddress && sendAddress.length) { + setToAddressOrName(sendAddress); + } + if (sendField !== independentField) { + switchIndependentField(); + } + if (sendAmount) { + setIndependentAmount(sendAmount); } - }, [selectAsset, selectedToken, setSelectedToken]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const prevToAddressIsSmartContract = usePrevious(toAddressIsSmartContract); useEffect(() => { diff --git a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx index b6b32fd3e5..6f481adc0f 100644 --- a/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx +++ b/src/entries/popup/pages/swap/SwapReviewSheet/SwapReviewSheet.tsx @@ -16,6 +16,7 @@ import { i18n } from '~/core/languages'; import { QuoteTypeMap } from '~/core/raps/references'; import { useGasStore } from '~/core/state'; import { useConnectedToHardhatStore } from '~/core/state/currentSettings/connectedToHardhat'; +import { popupInstanceStore } from '~/core/state/popupInstances'; import { useSwapAssetsToRefreshStore } from '~/core/state/swapAssetsToRefresh'; import { ParsedSearchAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; @@ -303,8 +304,10 @@ const SwapReviewSheetWithQuote = ({ }); } } else { + const { saveActiveTab } = popupInstanceStore.getState(); setSwapAssetsToRefresh({ nonce, assetToBuy, assetToSell }); - navigate(ROUTES.HOME, { state: { activeTab: 'activity' } }); + saveActiveTab({ tab: 'activity' }); + navigate(ROUTES.HOME); } isBridge ? analytics.track(event.bridgeSubmitted, { diff --git a/src/entries/popup/pages/swap/index.tsx b/src/entries/popup/pages/swap/index.tsx index 86d35c38eb..08fa8be8c7 100644 --- a/src/entries/popup/pages/swap/index.tsx +++ b/src/entries/popup/pages/swap/index.tsx @@ -7,6 +7,7 @@ import { i18n } from '~/core/languages'; import { shortcuts } from '~/core/references/shortcuts'; import { useGasStore } from '~/core/state'; import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme'; +import { usePopupInstanceStore } from '~/core/state/popupInstances'; import { useSelectedTokenStore } from '~/core/state/selectedToken'; import { ParsedSearchAsset } from '~/core/types/assets'; import { ChainId } from '~/core/types/chains'; @@ -334,6 +335,17 @@ export function Swap() { [setAssetToBuyInputValue, setAssetToSell, setAssetToSellInputValue], ); + const { + swapAmount: savedAmount, + swapField: savedField, + swapTokenToBuy: savedTokenToBuy, + swapTokenToSell: savedTokenToSell, + } = usePopupInstanceStore(); + + const [didPopulateSavedTokens, setDidPopulateSavedTokens] = useState(false); + const [didPopulateSavedInputValues, setDidPopulateSavedInputValues] = + useState(false); + useEffect(() => { // navigating from token row if (selectedToken) { @@ -348,10 +360,42 @@ export function Swap() { } setInputToOpenOnMount('buy'); } else { - setInputToOpenOnMount('sell'); + if (!didPopulateSavedTokens) { + if (savedTokenToBuy) { + setAssetToBuy(savedTokenToBuy); + } + if (savedTokenToSell) { + setAssetToSell(savedTokenToSell); + } else { + setInputToOpenOnMount('sell'); + } + setDidPopulateSavedTokens(true); + } + if (didPopulateSavedTokens && !didPopulateSavedInputValues) { + const field = savedField || 'sellField'; + if (savedAmount) { + if (field === 'buyField') { + setAssetToBuyInputValue(savedAmount); + } else if (field === 'sellField') { + setAssetToSellInputValue(savedAmount); + } else { + setAssetToSellInputNativeValue(savedAmount); + } + } + setDidPopulateSavedInputValues(true); + + switch (field) { + case 'buyField': + assetToBuyInputRef.current?.focus(); + break; + case 'sellField': + assetToSellInputRef.current?.focus(); + break; + } + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [didPopulateSavedInputValues, didPopulateSavedTokens]); useEffect(() => { return () => {