-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [GSW-2040] AutoDisconnect when session expired
- Loading branch information
Showing
5 changed files
with
318 additions
and
0 deletions.
There are no files selected for viewing
125 changes: 125 additions & 0 deletions
125
packages/web/src/components/common/session-expired-modal/SessionExpiredModal.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} | ||
`; |
47 changes: 47 additions & 0 deletions
47
packages/web/src/components/common/session-expired-modal/SessionExpiredModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
packages/web/src/hooks/wallet/ui/use-session-expired-modal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}; |