From dad90f72075a3c527fe52a1d6e258e6946410a4f Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 17:51:17 +0300 Subject: [PATCH 1/2] feat: extract single tab guard to a component and use for ledger --- .../components/guards/onboarding/index.tsx | 6 +- .../onboarding/useOnboardingSingleton.ts | 30 --------- src/app/components/guards/singleTab.tsx | 66 +++++++++++++++++++ .../components/guards/walletResetGuard.tsx | 17 +++++ src/app/routes/index.tsx | 9 ++- .../stores/wallet/actions/actionCreators.ts | 5 +- src/pages/Options/index.tsx | 7 +- 7 files changed, 104 insertions(+), 36 deletions(-) delete mode 100644 src/app/components/guards/onboarding/useOnboardingSingleton.ts create mode 100644 src/app/components/guards/singleTab.tsx create mode 100644 src/app/components/guards/walletResetGuard.tsx 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..511adb788 --- /dev/null +++ b/src/app/components/guards/singleTab.tsx @@ -0,0 +1,66 @@ +import { useEffect } from 'react'; + +type GuardType = 'onboarding' | 'importLedger' | 'resetWallet'; + +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/walletResetGuard.tsx b/src/app/components/guards/walletResetGuard.tsx new file mode 100644 index 000000000..b9a241602 --- /dev/null +++ b/src/app/components/guards/walletResetGuard.tsx @@ -0,0 +1,17 @@ +import { useSingleTabGuard } from '@components/guards/singleTab'; + +type WalletResetGuardProps = { + children?: React.ReactElement | React.ReactElement[]; +}; + +/** + * This guard is used to close any open tabs when the wallet is reset. + * It should only be rendered at the root of the options page. + */ +function WalletResetGuard({ children }: WalletResetGuardProps): React.ReactNode { + useSingleTabGuard('resetWallet', false); + + return children; +} + +export default WalletResetGuard; 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..87aa0b573 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 { @@ -47,6 +48,8 @@ export function setWalletSeedPhraseAction(seedPhrase: string): actions.SetWallet } export function resetWalletAction(): actions.ResetWallet { + // We post the resetWallet action to the guard so that any open tabs will close + PostGuardPing('resetWallet'); return { type: actions.ResetWalletKey, }; diff --git a/src/pages/Options/index.tsx b/src/pages/Options/index.tsx index a58407c04..5cde3054e 100644 --- a/src/pages/Options/index.tsx +++ b/src/pages/Options/index.tsx @@ -1,3 +1,4 @@ +import WalletResetGuard from '@components/guards/walletResetGuard'; 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(); From 407eddb980a1b4e713eceee3c4fa8fe0b31189f2 Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 30 Jun 2023 18:02:36 +0300 Subject: [PATCH 2/2] feat: close all tabs on wallet lock --- src/app/components/guards/singleTab.tsx | 2 +- .../{walletResetGuard.tsx => walletCloseGuard.tsx} | 10 +++++----- src/app/stores/wallet/actions/actionCreators.ts | 6 ++++-- src/pages/Options/index.tsx | 6 +++--- 4 files changed, 13 insertions(+), 11 deletions(-) rename src/app/components/guards/{walletResetGuard.tsx => walletCloseGuard.tsx} (61%) diff --git a/src/app/components/guards/singleTab.tsx b/src/app/components/guards/singleTab.tsx index 511adb788..a519427bc 100644 --- a/src/app/components/guards/singleTab.tsx +++ b/src/app/components/guards/singleTab.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -type GuardType = 'onboarding' | 'importLedger' | 'resetWallet'; +type GuardType = 'onboarding' | 'importLedger' | 'closeWallet'; const getChannelAndPingNames = (guardName: GuardType) => { const channelName = `${guardName}Channel`; diff --git a/src/app/components/guards/walletResetGuard.tsx b/src/app/components/guards/walletCloseGuard.tsx similarity index 61% rename from src/app/components/guards/walletResetGuard.tsx rename to src/app/components/guards/walletCloseGuard.tsx index b9a241602..99df2af9b 100644 --- a/src/app/components/guards/walletResetGuard.tsx +++ b/src/app/components/guards/walletCloseGuard.tsx @@ -1,17 +1,17 @@ import { useSingleTabGuard } from '@components/guards/singleTab'; -type WalletResetGuardProps = { +type WalletCloseGuardProps = { children?: React.ReactElement | React.ReactElement[]; }; /** - * This guard is used to close any open tabs when the wallet is reset. + * 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 WalletResetGuard({ children }: WalletResetGuardProps): React.ReactNode { - useSingleTabGuard('resetWallet', false); +function WalletCloseGuard({ children }: WalletCloseGuardProps): React.ReactNode { + useSingleTabGuard('closeWallet', false); return children; } -export default WalletResetGuard; +export default WalletCloseGuard; diff --git a/src/app/stores/wallet/actions/actionCreators.ts b/src/app/stores/wallet/actions/actionCreators.ts index 87aa0b573..a866b7d22 100644 --- a/src/app/stores/wallet/actions/actionCreators.ts +++ b/src/app/stores/wallet/actions/actionCreators.ts @@ -28,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, }; @@ -48,8 +50,8 @@ export function setWalletSeedPhraseAction(seedPhrase: string): actions.SetWallet } export function resetWalletAction(): actions.ResetWallet { - // We post the resetWallet action to the guard so that any open tabs will close - PostGuardPing('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 5cde3054e..3f51d54a9 100644 --- a/src/pages/Options/index.tsx +++ b/src/pages/Options/index.tsx @@ -1,4 +1,4 @@ -import WalletResetGuard from '@components/guards/walletResetGuard'; +import WalletCloseGuard from '@components/guards/walletCloseGuard'; import { decryptMnemonic } from '@stacks/encryption'; import rootStore from '@stores/index'; import { setWalletSeedPhraseAction } from '@stores/wallet/actions/actionCreators'; @@ -29,9 +29,9 @@ const renderApp = async () => { const container = document.getElementById('app'); const root = createRoot(container!); return root.render( - + - , + , ); };