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();