Skip to content

Commit

Permalink
feat: [GSW-2040] AutoDisconnect when session expired
Browse files Browse the repository at this point in the history
  • Loading branch information
tfrg committed Jan 20, 2025
1 parent 068644c commit 5fcc12f
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import styled from "@emotion/styled";
import { fonts } from "@constants/font.constant";
import { media } from "@styles/media";

export const SessionExpiredModalWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 16px;
width: 460px;
padding: 23px;
.modal-body {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 24px;
width: 100%;
.header {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
.close-wrap {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 24px;
height: 24px;
* {
fill: ${({ theme }) => theme.color.icon01};
}
&:hover {
* {
fill: ${({ theme }) => theme.color.icon07};
}
}
}
}
}
.content {
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex-direction: column;
gap: 24px;
width: 100%;
.warning-logo {
margin: auto;
display: block;
}
h5 {
${fonts.body7};
color: ${({ theme }) => theme.color.text02};
text-align: center;
${media.mobile} {
font-size: 16px;
}
}
.detail {
display: flex;
justify-content: center;
flex-direction: column;
gap: 8px;
width: 100%;
.description {
text-align: center;
${fonts.body12};
color: ${({ theme }) => theme.color.text04};
${media.mobile} {
font-size: 13px;
br {
display: none;
}
}
}
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
width: 100%;
button {
height: 57px;
span {
${fonts.body7}
${media.mobile} {
font-size: 16px;
}
}
${media.mobile} {
height: 41px;
}
}
}
}
}
${media.mobile} {
padding: 12px;
width: 328px;
.modal-body {
gap: 12px;
.content {
/* gap: 16px; */
.button-wrapper {
gap: 12px;
}
}
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";

import { SessionExpiredModalWrapper } from "./SessionExpiredModal.styles";
import IconClose from "../icons/IconCancel";
import IconFailed from "../icons/IconFailed";
import Button, { ButtonHierarchy } from "../button/Button";

interface Props {
close: () => void;
}

const SessionExpiredModal = ({ close }: Props) => {
return (
<SessionExpiredModalWrapper>
<div className="modal-body">
<div className="header">
<div className="close-wrap">
<button onClick={close}>
<IconClose className="close-icon" />
</button>
</div>
</div>

<div className="content">
<IconFailed className="warning-logo" />
<div className="detail">
<h5>Session Expired</h5>
<div className="description">
Your session has expired due to inactivity.
<br /> Please log in again to continue.
</div>
</div>
<div className="button-wrapper">
<Button
text="Close"
style={{ hierarchy: ButtonHierarchy.Primary, fullWidth: true }}
onClick={close}
className="button-confirm"
/>
</div>
</div>
</div>
</SessionExpiredModalWrapper>
);
};

export default SessionExpiredModal;
98 changes: 98 additions & 0 deletions packages/web/src/hooks/common/use-auto-disconnect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from "react";

import { useWallet } from "@hooks/wallet/data/use-wallet";
import { useAtom } from "jotai";
import { WalletState } from "@states/index";
import { useSessionExpiredModal } from "@hooks/wallet/ui/use-session-expired-modal";

const INACTIVITY_TIMEOUT_MINUTES = 5 * 60 * 1000; // 5 minutes

/**
*
* Custom hook that manages automatic disconnection of social wallets after inactivity
*
* @description
* Implements an auto-disconnect security feature that monitors user activity and
* disconnects social wallet users after a period of inactivity (1 minute).
* Shows a session expired modal before disconnecting.
*
* @listens {UserEvents} Following user activities reset the inactivity timer:
* - mousedown
* - mousemove
* - keydown
* - scroll
* - touchstart
* - click
*
* @requires
* - walletClient must be SOCIAL_WALLET type
* - account must exist
*
* @dependencies
* - useWallet - For account info and disconnect functionality
* - useSessionExpiredModal - For displaying session expiry notification
* - WalletState - For wallet client information
*
*/
export const useAutoDisconnect = () => {
const [walletClient] = useAtom(WalletState.client);

const { openModal: openSessionExpiredModal } = useSessionExpiredModal();
const { account, disconnectWallet } = useWallet();

const inactivityTimerRef = React.useRef<NodeJS.Timeout>();

/**
* Handles the wallet disconnection process
* Shows the sesion expired modal and disconnects the wallet
*/
const handleDisconnect = () => {
openSessionExpiredModal();
disconnectWallet();
};

/**
* Restarts the inactivity timer when user activity is detected
* Clears existing timer and sets a new one
*/
const restartInactivityTimer = React.useCallback(() => {
if (inactivityTimerRef.current) {
clearTimeout(inactivityTimerRef.current);
}

inactivityTimerRef.current = setTimeout(() => {
handleDisconnect();
}, INACTIVITY_TIMEOUT_MINUTES);
}, []);

/**
* Sets up event listeners for user activity monitoring
*
* @effects
* - Adds event listeners for user activity events
* - Initializes inactivity timer
* - Cleans up listeners and timer on unmount
*/
React.useEffect(() => {
if (!walletClient || walletClient.getWalletType() !== "SOCIAL_WALLET" || !account) {
return;
}

const userActivityEvents = ["mousedown", "mousemove", "keydown", "scroll", "touchstart", "click"];

userActivityEvents.forEach(eventName => {
window.addEventListener(eventName, restartInactivityTimer);
});

restartInactivityTimer();

return () => {
userActivityEvents.forEach(event => {
window.removeEventListener(event, restartInactivityTimer);
});
if (inactivityTimerRef.current) {
clearTimeout(inactivityTimerRef.current);
}
};
}, [restartInactivityTimer, walletClient, account]);
};
18 changes: 18 additions & 0 deletions packages/web/src/hooks/common/use-background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useScrollData from "./use-scroll-data";
import { useLoading } from "./use-loading";
import { useSocialWalletContext } from "./use-social-wallet-context";
import { GNOSWAP_SOCIAL_LOGIN_TYPE_KEY, GNOSWAP_WALLET_TYPE_KEY } from "@states/common";
import { useAutoDisconnect } from "./use-auto-disconnect";

export const useBackground = () => {
const router = useRouter();
Expand Down Expand Up @@ -161,6 +162,23 @@ export const useBackground = () => {
updateWalletEvents(walletClient);
}, [walletClient?.getWalletType(), String(account)]);

/**
*
* Automatically disconnects SOCIAL-WALLET after a period of inacitivity
*
* Purpose:
* Implements an auto-disconnect feature for enhanced security of social wallet users
* Tracks user activity and disconnects the wallet after a period of inactivity
*
* Behavior:
* Only activates for SOCIAL-WALLET type and when account exists
* Monitors user activities: mouse movements, keystrokes, scrolling, touches, clicks
* Resets timer on any user activity
* Disconnects wallet after 5 minutes of inactivity
*
*/
useAutoDisconnect();

useEffect(() => {
if (account?.address && account?.chainId) {
updateBalances();
Expand Down
30 changes: 30 additions & 0 deletions packages/web/src/hooks/wallet/ui/use-session-expired-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import { useAtom } from "jotai";

import { CommonState } from "@states/index";
import SessionExpiredModal from "@components/common/session-expired-modal/SessionExpiredModal";
import { useClearModal } from "@hooks/common/use-clear-modal";

interface ModalControls {
openModal: () => void;
}

export const useSessionExpiredModal = (): ModalControls => {
const [, setOpenedModal] = useAtom(CommonState.openedModal);
const [, setModalContent] = useAtom(CommonState.modalContent);

const clearModal = useClearModal();

const closeModal = React.useCallback(() => {
clearModal();
}, [clearModal]);

const openModal = React.useCallback(() => {
setOpenedModal(true);
setModalContent(<SessionExpiredModal close={closeModal} />);
}, [setOpenedModal, setModalContent]);

return {
openModal,
};
};

0 comments on commit 5fcc12f

Please sign in to comment.