diff --git a/src/app/components/guards/onboarding/index.tsx b/src/app/components/guards/onboarding/index.tsx index d129a8cc0..b9c70973c 100644 --- a/src/app/components/guards/onboarding/index.tsx +++ b/src/app/components/guards/onboarding/index.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react'; import { Navigate } from 'react-router-dom'; +import { useSingleTabGuard } from '@components/guards/singleTab'; import useHasStateRehydrated from '@hooks/stores/useHasRehydrated'; import useWalletSelector from '@hooks/useWalletSelector'; @@ -9,7 +10,6 @@ import { WalletExistsContextProps, useWalletExistsContext, } from './WalletExistsContext'; -import useOnboardingSingleton from './useOnboardingSingleton'; interface WalletExistsGuardProps { children: React.ReactElement; @@ -17,10 +17,10 @@ interface WalletExistsGuardProps { /** * This guard is used to redirect the user to the wallet exists page if they have a wallet and ensures - * that only 1 onboarding workflow tab exists at a time (via the useOnboardingSingleton hook). + * that only 1 onboarding workflow tab exists at a time (via the useSingleTabGuard hook). */ function OnboardingGuard({ children }: WalletExistsGuardProps): React.ReactElement { - useOnboardingSingleton(); + useSingleTabGuard('onboarding'); const [walletExistsGuardEnabled, setWalletExistsGuardEnabled] = useState(true); diff --git a/src/app/components/guards/onboarding/useOnboardingSingleton.ts b/src/app/components/guards/onboarding/useOnboardingSingleton.ts deleted file mode 100644 index bce89c3f2..000000000 --- a/src/app/components/guards/onboarding/useOnboardingSingleton.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect } from 'react'; - -const ONBOARDING_CHANNEL_NAME = 'onboarding'; -const ONBOARDING_PING = 'pingOnboarding'; - -/** - * This hook is used to ensure that only one onboarding window is open at a time. - */ -const useOnboardingSingleton = (): void => { - useEffect(() => { - const broadcastChannel = new BroadcastChannel(ONBOARDING_CHANNEL_NAME); - - broadcastChannel.onmessage = (message) => { - if (message.data !== ONBOARDING_PING) { - return; - } - - broadcastChannel.close(); - window.close(); - }; - - broadcastChannel.postMessage(ONBOARDING_PING); - - return () => { - broadcastChannel.close(); - }; - }, []); -}; - -export default useOnboardingSingleton; diff --git a/src/app/components/guards/singleTab.tsx b/src/app/components/guards/singleTab.tsx new file mode 100644 index 000000000..a519427bc --- /dev/null +++ b/src/app/components/guards/singleTab.tsx @@ -0,0 +1,66 @@ +import { useEffect } from 'react'; + +type GuardType = 'onboarding' | 'importLedger' | 'closeWallet'; + +const getChannelAndPingNames = (guardName: GuardType) => { + const channelName = `${guardName}Channel`; + const pingName = `${guardName}Ping`; + return { channelName, pingName }; +}; + +// NOTE: This is for calling the guard from a non-component context +// The window that is sending it will also receive it if the channel it is broadcasting to is open +// This is predominantly made for the reset wallet guard to close all tabs +// Use with care. +export const PostGuardPing = (guardName: GuardType): void => { + const { channelName, pingName } = getChannelAndPingNames(guardName); + const broadcastChannel = new BroadcastChannel(channelName); + broadcastChannel.postMessage(pingName); + broadcastChannel.close(); +}; + +/** + * This hook is used to ensure that only one window with the guard name is open at a time. + * It fires off an event only once on its first render and will close the window if it receives + * the event. + */ +export const useSingleTabGuard = (guardName: GuardType, broadcastOnLoad = true): void => { + const { channelName, pingName } = getChannelAndPingNames(guardName); + + useEffect(() => { + const broadcastChannel = new BroadcastChannel(channelName); + + broadcastChannel.onmessage = (message) => { + if (message.data !== pingName) { + return; + } + + broadcastChannel.close(); + window.close(); + }; + + if (broadcastOnLoad) { + broadcastChannel.postMessage(pingName); + } + + return () => { + broadcastChannel.close(); + }; + }, []); +}; + +type SingleTabProps = { + guardName: GuardType; + children?: React.ReactElement | React.ReactElement[]; +}; + +/** + * This guard is used to ensure that only one window with the guard name is open at a time. + * It fires off an event only once on its first render and will close the window if it receives + * the event. + */ +export function SingleTabGuard({ guardName, children }: SingleTabProps): React.ReactNode { + useSingleTabGuard(guardName); + + return children; +} diff --git a/src/app/components/guards/walletCloseGuard.tsx b/src/app/components/guards/walletCloseGuard.tsx new file mode 100644 index 000000000..99df2af9b --- /dev/null +++ b/src/app/components/guards/walletCloseGuard.tsx @@ -0,0 +1,17 @@ +import { useSingleTabGuard } from '@components/guards/singleTab'; + +type WalletCloseGuardProps = { + children?: React.ReactElement | React.ReactElement[]; +}; + +/** + * This guard is used to close any open tabs when the wallet is locked or reset. + * It should only be rendered at the root of the options page. + */ +function WalletCloseGuard({ children }: WalletCloseGuardProps): React.ReactNode { + useSingleTabGuard('closeWallet', false); + + return children; +} + +export default WalletCloseGuard; diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index cd2afa1e9..231267c2f 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -1,6 +1,7 @@ import ExtendedScreenContainer from '@components/extendedScreenContainer'; import AuthGuard from '@components/guards/auth'; import OnboardingGuard from '@components/guards/onboarding'; +import { SingleTabGuard } from '@components/guards/singleTab'; import ScreenContainer from '@components/screenContainer'; import AccountList from '@screens/accountList'; import AuthenticationRequest from '@screens/authenticationRequest'; @@ -79,7 +80,13 @@ const router = createHashRouter([ }, { path: 'import-ledger', - element: , + element: ( + + + + + + ) }, { index: true, diff --git a/src/app/stores/wallet/actions/actionCreators.ts b/src/app/stores/wallet/actions/actionCreators.ts index 653f8f0e4..a866b7d22 100644 --- a/src/app/stores/wallet/actions/actionCreators.ts +++ b/src/app/stores/wallet/actions/actionCreators.ts @@ -1,3 +1,5 @@ +import { PostGuardPing } from '@components/guards/singleTab'; +import { AccountType } from '@secretkeylabs/xverse-core'; import { Account, BaseWallet, @@ -9,7 +11,6 @@ import { TransactionData, } from '@secretkeylabs/xverse-core/types'; import BigNumber from 'bignumber.js'; -import { AccountType } from '@secretkeylabs/xverse-core'; import * as actions from './types'; export function setWalletAction(wallet: BaseWallet): actions.SetWallet { @@ -27,6 +28,8 @@ export function unlockWalletAction(seed: string) { } export function lockWalletAction() { + // We post the closeWallet action to the guard so that any open tabs will close + PostGuardPing('closeWallet'); return { type: actions.LockWalletKey, }; @@ -47,6 +50,8 @@ export function setWalletSeedPhraseAction(seedPhrase: string): actions.SetWallet } export function resetWalletAction(): actions.ResetWallet { + // We post the closeWallet action to the guard so that any open tabs will close + PostGuardPing('closeWallet'); return { type: actions.ResetWalletKey, }; diff --git a/src/pages/Options/index.tsx b/src/pages/Options/index.tsx index a58407c04..3f51d54a9 100644 --- a/src/pages/Options/index.tsx +++ b/src/pages/Options/index.tsx @@ -1,3 +1,4 @@ +import WalletCloseGuard from '@components/guards/walletCloseGuard'; import { decryptMnemonic } from '@stacks/encryption'; import rootStore from '@stores/index'; import { setWalletSeedPhraseAction } from '@stores/wallet/actions/actionCreators'; @@ -27,7 +28,11 @@ const renderApp = async () => { }); const container = document.getElementById('app'); const root = createRoot(container!); - return root.render(); + return root.render( + + + , + ); }; renderApp();