1 import type { ReactNode } from 'react';
2 import { useEffect, useState } from 'react';
3 import { useLocation } from 'react-router';
5 import { c } from 'ttag';
9 HumanVerificationSteps,
10 StandardLoadErrorPage,
13 useThemeQueryParameter,
14 } from '@proton/components';
15 import useInstance from '@proton/hooks/useInstance';
16 import { getApiErrorMessage } from '@proton/shared/lib/api/helpers/apiErrorHelper';
17 import { queryCheckVerificationCode } from '@proton/shared/lib/api/user';
18 import { getGenericErrorPayload } from '@proton/shared/lib/broadcast';
19 import { createOfflineError } from '@proton/shared/lib/fetch/ApiError';
20 import { getBrowserLocale, getClosestLocaleCode, getClosestLocaleMatch } from '@proton/shared/lib/i18n/helper';
21 import { loadDateLocale, loadLocale } from '@proton/shared/lib/i18n/loadLocale';
22 import { setTtagLocales } from '@proton/shared/lib/i18n/locales';
23 import type { HumanVerificationMethodType } from '@proton/shared/lib/interfaces';
24 import { getDarkThemes } from '@proton/shared/lib/themes/themes';
26 import broadcast, { MessageType } from './broadcast';
27 import locales from './locales';
28 import type { VerificationSearchParameters } from './types';
30 import './Verify.scss';
32 setTtagLocales(locales);
34 const windowIsEmbedded = window.location !== window.parent.location;
35 const darkThemes = getDarkThemes();
37 const parseSearch = (search: string) =>
39 [...new URLSearchParams(search).entries()].map(([key, value]) => {
40 if (key === 'methods') {
41 return [key, value.split(',')];
47 const Verify = () => {
48 const [step, setStep] = useState(HumanVerificationSteps.ENTER_DESTINATION);
49 const [loading, setLoading] = useState(true);
50 const [error, setError] = useState(false);
52 const { createNotification } = useNotifications();
53 const location = useLocation();
54 const theme = useThemeQueryParameter();
56 const search = parseSearch(location.search) as VerificationSearchParameters;
57 const { methods, embed, locale, token, vpn, defaultCountry, defaultEmail, defaultPhone } = search;
59 const isEmbedded = windowIsEmbedded || embed;
61 const handleClose = () => {
62 broadcast({ type: MessageType.CLOSE });
65 const handleLoaded = () => {
66 broadcast({ type: MessageType.LOADED });
69 const handleError = (error: unknown) => {
70 broadcast({ type: MessageType.ERROR, payload: getGenericErrorPayload(error) });
74 const browserLocale = getBrowserLocale();
76 const localeCode = getClosestLocaleMatch(locale || '', locales) || getClosestLocaleCode(browserLocale, locales);
78 Promise.all([loadLocale(localeCode, locales), loadDateLocale(localeCode, browserLocale)])
85 handleError(createOfflineError({}));
86 // Also sends out a loaded message for clients that don't handle the error message to display the error screen.
91 document.body.classList.remove('embedded');
95 document.body.classList.add('vpn');
99 const sendHeight = (resizes: ResizeObserverEntry[]) => {
100 const [entry] = resizes;
103 type: MessageType.RESIZE,
104 payload: { height: entry.target.clientHeight },
108 const resizeObserver = useInstance(() => new ResizeObserver(sendHeight));
110 const registerRootRef = (el: HTMLElement) => {
111 if (el && isEmbedded) {
112 resizeObserver.observe(el);
118 resizeObserver.disconnect();
123 const handleSubmit = async (token: string, type: HumanVerificationMethodType) => {
124 if (type !== 'captcha' && type !== 'ownership-sms' && type !== 'ownership-email') {
126 await api({ ...queryCheckVerificationCode(token, type, 1), silence: true });
129 type: MessageType.HUMAN_VERIFICATION_SUCCESS,
130 payload: { token, type },
135 * window.close() will only be allowed to execute should the current window
136 * have been opened programatically, otherwise the following error is thrown:
138 * "Scripts may close only the windows that were opened by it."
145 text: getApiErrorMessage(e) || c('Error').t`Unknown error`,
152 type: MessageType.HUMAN_VERIFICATION_SUCCESS,
153 payload: { token, type },
158 const wrapInMain = (child: ReactNode) => (
159 <main className="hv h-full" ref={registerRootRef}>
161 className="hv-container sm:shadow-lifted shadow-color-primary ui-standard relative overflow-hidden w-full max-w-custom mx-auto"
162 style={{ '--max-w-custom': '30rem' }}
174 return <StandardLoadErrorPage />;
177 if (methods === undefined) {
178 return wrapInMain('You need to specify recovery methods');
181 if (token === undefined) {
182 return wrapInMain('You need to specify a token');
186 <HumanVerificationForm
187 theme={theme && darkThemes.includes(theme) ? 'dark' : 'light'}
189 onChangeStep={setStep}
190 onSubmit={handleSubmit}
191 onLoaded={handleLoaded}
192 onClose={handleClose}
196 // Also sends out a loaded message for clients that don't handle the error message to display the error screen.
201 defaultCountry={defaultCountry}
202 defaultEmail={defaultEmail}
203 defaultPhone={defaultPhone}
204 isEmbedded={isEmbedded}
211 <main className="p-5" ref={registerRootRef}>
217 return wrapInMain(hv);
220 export default Verify;