1 import { useEffect, useRef, useState } from 'react';
3 import { c } from 'ttag';
5 import { Href } from '@proton/atoms';
6 import useApi from '@proton/components/hooks/useApi';
7 import { ping } from '@proton/shared/lib/api/tests';
8 import { HOUR, SECOND } from '@proton/shared/lib/constants';
9 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
10 import { getKnowledgeBaseUrl } from '@proton/shared/lib/helpers/url';
11 import noop from '@proton/utils/noop';
13 import useApiServerTime from '../../hooks/useApiServerTime';
14 import TopBanner from './TopBanner';
16 const isOutOfSync = (serverTime: Date, localTime: Date) => {
17 const timeDifference = Math.abs(serverTime.getTime() - localTime.getTime());
18 // We should allow at least a 14-hour time difference,
19 // because of potential internal clock issues when using dual-boot with Windows and a different OS
20 return timeDifference > 24 * HOUR;
24 * The serverTime update might be stale if e.g. the device has been asleep/idle, and it's processing the update after a delay.
26 const isStaleServerTimeUpdate = (previousUpdateLocalTime: Date, localTime: Date) => {
27 const timeDifference = Math.abs(previousUpdateLocalTime.getTime() - localTime.getTime());
28 // The event loop runs every 30s, so we expect the server time to be updated at least with that frequency
29 // (with a margin of a few seconds)
30 return timeDifference > 35 * SECOND;
33 const TimeOutOfSyncTopBanner = () => {
34 const [ignore, setIgnore] = useState(false);
35 // This is only used to keep track of the time past since the previous re-render, aka server time update.
36 const previousUpdateLocalTime = useRef(new Date());
39 const serverTime = useApiServerTime();
40 const currentUpdateLocalTime = new Date();
41 const isStaleServerTime = isStaleServerTimeUpdate(previousUpdateLocalTime.current, currentUpdateLocalTime);
44 // Check for stale server time every 5s, and ping the server if needed to ensure that we retrieve an updated
45 // server time at most 5s after waking from an idle state: we do not want to wait up to 30s for the event loop
46 // request to be processed, since the stale serverTime() value will be used by the apps in the meantime.
47 const stalePingInterval = setInterval(() => {
48 if (isStaleServerTimeUpdate(previousUpdateLocalTime.current, new Date())) {
49 void api({ ...ping() }).catch(noop); // if it fails, we'll try again in 5s
53 clearInterval(stalePingInterval);
55 }, []); // run only once, on first render
58 previousUpdateLocalTime.current = currentUpdateLocalTime;
59 }); // run at every re-render
61 // We warn the user if the server time is too far off from local time.
62 // We do not want the server to set arbitrary times (either past or future), to avoid signature replay issues and more.
63 const showWarning = !ignore && serverTime && !isStaleServerTime && isOutOfSync(serverTime, currentUpdateLocalTime);
65 // Log warning to have an idea of how many clients might be affected
66 const onceRef = useRef(false);
68 if (!showWarning || onceRef.current) {
72 onceRef.current = true;
73 captureMessage('Client time difference larger than 24 hours', {
76 serverTime: serverTime?.toString(),
77 localTime: currentUpdateLocalTime.toString(),
87 const learnMore = <Href href={getKnowledgeBaseUrl('/device-time-warning')}>{c('Link').t`Learn more`}</Href>;
90 <TopBanner onClose={() => setIgnore(true)} className="bg-warning">
91 {c('Warning').jt`The date and time settings on your device are out of sync. ${learnMore}`}
96 export default TimeOutOfSyncTopBanner;