From 20525d9ce5e711f929f5fdda97b59d024fa2c44e Mon Sep 17 00:00:00 2001 From: Nathan Richards Date: Wed, 10 Jul 2024 19:08:35 +0200 Subject: [PATCH] refactor: use widgets own timer hook --- packages/widget/package.json | 1 - .../widget/src/components/Step/StepTimer.tsx | 3 +- .../widget/src/hooks/timer/useInterval.ts | 21 +++++ packages/widget/src/hooks/timer/useTimer.ts | 91 +++++++++++++++++++ packages/widget/src/hooks/timer/utils.ts | 54 +++++++++++ yarn.lock | 10 -- 6 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 packages/widget/src/hooks/timer/useInterval.ts create mode 100644 packages/widget/src/hooks/timer/useTimer.ts create mode 100644 packages/widget/src/hooks/timer/utils.ts diff --git a/packages/widget/package.json b/packages/widget/package.json index d51379e82..588c0a104 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -68,7 +68,6 @@ "react-i18next": "^14.1.2", "react-intersection-observer": "^9.10.3", "react-router-dom": "^6.24.0", - "react-timer-hook": "^3.0.7", "uuid": "^10.0.0", "viem": "^2.16.2", "wagmi": "^2.10.7", diff --git a/packages/widget/src/components/Step/StepTimer.tsx b/packages/widget/src/components/Step/StepTimer.tsx index de0818673..8e1a0066a 100644 --- a/packages/widget/src/components/Step/StepTimer.tsx +++ b/packages/widget/src/components/Step/StepTimer.tsx @@ -1,8 +1,7 @@ import type { LiFiStepExtended } from '@lifi/sdk'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import * as reactTimerHook from 'react-timer-hook'; -const { useTimer } = reactTimerHook; +import { useTimer } from '../../hooks/timer/useTimer.js'; const getExpiryTimestamp = (step: LiFiStepExtended) => new Date( diff --git a/packages/widget/src/hooks/timer/useInterval.ts b/packages/widget/src/hooks/timer/useInterval.ts new file mode 100644 index 000000000..3c1aac735 --- /dev/null +++ b/packages/widget/src/hooks/timer/useInterval.ts @@ -0,0 +1,21 @@ +import { useEffect, useRef } from 'react'; + +export function useInterval(callback: Function, delay: number) { + const callbacRef = useRef(); + + // update callback function with current render callback that has access to latest props and state + useEffect(() => { + callbacRef.current = callback; + }); + + useEffect(() => { + if (!delay) { + return () => {}; + } + + const interval = setInterval(() => { + callbacRef.current && callbacRef.current(); + }, delay); + return () => clearInterval(interval); + }, [delay]); +} diff --git a/packages/widget/src/hooks/timer/useTimer.ts b/packages/widget/src/hooks/timer/useTimer.ts new file mode 100644 index 000000000..95eb48a73 --- /dev/null +++ b/packages/widget/src/hooks/timer/useTimer.ts @@ -0,0 +1,91 @@ +import { useCallback, useState } from 'react'; +import { useInterval } from './useInterval.js'; +import { + getDelayFromExpiryTimestamp, + getSecondsFromExpiry, + getTimeFromSeconds, + validateOnExpire, +} from './utils.js'; + +const DEFAULT_DELAY = 1000; + +interface UseTimerProps { + expiryTimestamp: Date; + onExpire: Function; + autoStart?: boolean; +} + +// This implementation was taken from the common js project - https://www.npmjs.com/package/react-timer-hook +// modified to work in the Widget codebase with Typescript +export function useTimer({ + expiryTimestamp: expiry, + onExpire, + autoStart = true, +}: UseTimerProps) { + const [expiryTimestamp, setExpiryTimestamp] = useState(expiry); + const [seconds, setSeconds] = useState(getSecondsFromExpiry(expiryTimestamp)); + const [isRunning, setIsRunning] = useState(autoStart); + const [didStart, setDidStart] = useState(autoStart); + const [delay, setDelay] = useState( + getDelayFromExpiryTimestamp(expiryTimestamp, DEFAULT_DELAY), + ); + + const handleExpire = useCallback(() => { + validateOnExpire(onExpire) && onExpire(); + setIsRunning(false); + setDelay(0); + }, [onExpire]); + + const pause = useCallback(() => { + setIsRunning(false); + }, []); + + const restart = useCallback( + (newExpiryTimestamp: Date, newAutoStart = true) => { + setDelay(getDelayFromExpiryTimestamp(newExpiryTimestamp, DEFAULT_DELAY)); + setDidStart(newAutoStart); + setIsRunning(newAutoStart); + setExpiryTimestamp(newExpiryTimestamp); + setSeconds(getSecondsFromExpiry(newExpiryTimestamp)); + }, + [], + ); + + const resume = useCallback(() => { + const time = new Date(); + time.setMilliseconds(time.getMilliseconds() + seconds * 1000); + restart(time); + }, [seconds, restart]); + + const start = useCallback(() => { + if (didStart) { + setSeconds(getSecondsFromExpiry(expiryTimestamp)); + setIsRunning(true); + } else { + resume(); + } + }, [expiryTimestamp, didStart, resume]); + + useInterval( + () => { + if (delay !== DEFAULT_DELAY) { + setDelay(DEFAULT_DELAY); + } + const secondsValue = getSecondsFromExpiry(expiryTimestamp); + setSeconds(secondsValue); + if (secondsValue <= 0) { + handleExpire(); + } + }, + isRunning ? delay : 0, + ); + + return { + ...getTimeFromSeconds(seconds), + start, + pause, + resume, + restart, + isRunning, + }; +} diff --git a/packages/widget/src/hooks/timer/utils.ts b/packages/widget/src/hooks/timer/utils.ts new file mode 100644 index 000000000..44638468f --- /dev/null +++ b/packages/widget/src/hooks/timer/utils.ts @@ -0,0 +1,54 @@ +export function getTimeFromSeconds(secs: number) { + const totalSeconds = Math.ceil(secs); + const days = Math.floor(totalSeconds / (60 * 60 * 24)); + const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60)); + const minutes = Math.floor((totalSeconds % (60 * 60)) / 60); + const seconds = Math.floor(totalSeconds % 60); + + return { + totalSeconds, + seconds, + minutes, + hours, + days, + }; +} + +export function getSecondsFromExpiry(expiry: Date, shouldRound?: boolean) { + const now = new Date().getTime(); + const milliSecondsDistance = expiry.getTime() - now; + if (milliSecondsDistance > 0) { + const val = milliSecondsDistance / 1000; + return shouldRound ? Math.round(val) : val; + } + return 0; +} + +export function validateExpiryTimestamp(expiryTimestamp: Date) { + const isValid = new Date(expiryTimestamp).getTime() > 0; + if (!isValid) { + console.warn('useTimer Invalid expiryTimestamp settings', expiryTimestamp); // eslint-disable-line + } + return isValid; +} + +export function validateOnExpire(onExpire: Function) { + const isValid = onExpire && typeof onExpire === 'function'; + if (onExpire && !isValid) { + console.warn('useTimer Invalid onExpire settings function', onExpire); + } + return isValid; +} + +export function getDelayFromExpiryTimestamp( + expiryTimestamp: Date, + defaultDelay: number, +) { + if (!validateExpiryTimestamp(expiryTimestamp)) { + return 0; + } + + const seconds = getSecondsFromExpiry(expiryTimestamp); + const extraMilliSeconds = Math.floor((seconds - Math.floor(seconds)) * 1000); + return extraMilliSeconds > 0 ? extraMilliSeconds : defaultDelay; +} diff --git a/yarn.lock b/yarn.lock index a6c0b7324..70b26482e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2668,7 +2668,6 @@ __metadata: react-i18next: "npm:^14.1.2" react-intersection-observer: "npm:^9.10.3" react-router-dom: "npm:^6.24.0" - react-timer-hook: "npm:^3.0.7" typescript: "npm:^5.5.2" uuid: "npm:^10.0.0" viem: "npm:^2.16.2" @@ -15368,15 +15367,6 @@ __metadata: languageName: node linkType: hard -"react-timer-hook@npm:^3.0.7": - version: 3.0.7 - resolution: "react-timer-hook@npm:3.0.7" - peerDependencies: - react: ">=16.8.0" - checksum: 10/36807a32245ca7d840805bc5e727951584cb40304331374b27e7b49ba77e820262ecaac90f6d0f1931991f7c9d3b6050cb89384a446812a4463ce890b1547332 - languageName: node - linkType: hard - "react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5"