i18n: Upgrade translations from crowdin (681bfc57). (verify)
[ProtonMail-WebClient.git] / applications / verify / src / app / Verify.tsx
blob31714ea5051b4ca367858b5b3ecb59f67cf02b22
1 import type { ReactNode } from 'react';
2 import { useEffect, useState } from 'react';
3 import { useLocation } from 'react-router';
5 import { c } from 'ttag';
7 import {
8     HumanVerificationForm,
9     HumanVerificationSteps,
10     StandardLoadErrorPage,
11     useApi,
12     useNotifications,
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) =>
38     Object.fromEntries(
39         [...new URLSearchParams(search).entries()].map(([key, value]) => {
40             if (key === 'methods') {
41                 return [key, value.split(',')];
42             }
43             return [key, value];
44         })
45     );
47 const Verify = () => {
48     const [step, setStep] = useState(HumanVerificationSteps.ENTER_DESTINATION);
49     const [loading, setLoading] = useState(true);
50     const [error, setError] = useState(false);
51     const api = useApi();
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 });
63     };
65     const handleLoaded = () => {
66         broadcast({ type: MessageType.LOADED });
67     };
69     const handleError = (error: unknown) => {
70         broadcast({ type: MessageType.ERROR, payload: getGenericErrorPayload(error) });
71     };
73     useEffect(() => {
74         const browserLocale = getBrowserLocale();
76         const localeCode = getClosestLocaleMatch(locale || '', locales) || getClosestLocaleCode(browserLocale, locales);
78         Promise.all([loadLocale(localeCode, locales), loadDateLocale(localeCode, browserLocale)])
79             .then(() => {
80                 setLoading(false);
81             })
82             .catch(() => {
83                 setError(true);
84                 setLoading(false);
85                 handleError(createOfflineError({}));
86                 // Also sends out a loaded message for clients that don't handle the error message to display the error screen.
87                 handleLoaded();
88             });
90         if (!isEmbedded) {
91             document.body.classList.remove('embedded');
92         }
94         if (vpn) {
95             document.body.classList.add('vpn');
96         }
97     }, []);
99     const sendHeight = (resizes: ResizeObserverEntry[]) => {
100         const [entry] = resizes;
102         broadcast({
103             type: MessageType.RESIZE,
104             payload: { height: entry.target.clientHeight },
105         });
106     };
108     const resizeObserver = useInstance(() => new ResizeObserver(sendHeight));
110     const registerRootRef = (el: HTMLElement) => {
111         if (el && isEmbedded) {
112             resizeObserver.observe(el);
113         }
114     };
116     useEffect(
117         () => () => {
118             resizeObserver.disconnect();
119         },
120         []
121     );
123     const handleSubmit = async (token: string, type: HumanVerificationMethodType) => {
124         if (type !== 'captcha' && type !== 'ownership-sms' && type !== 'ownership-email') {
125             try {
126                 await api({ ...queryCheckVerificationCode(token, type, 1), silence: true });
128                 broadcast({
129                     type: MessageType.HUMAN_VERIFICATION_SUCCESS,
130                     payload: { token, type },
131                 });
133                 if (!isEmbedded) {
134                     /*
135                      * window.close() will only be allowed to execute should the current window
136                      * have been opened programatically, otherwise the following error is thrown:
137                      *
138                      * "Scripts may close only the windows that were opened by it."
139                      */
140                     window.close();
141                 }
142             } catch (e: any) {
143                 createNotification({
144                     type: 'error',
145                     text: getApiErrorMessage(e) || c('Error').t`Unknown error`,
146                 });
148                 throw e;
149             }
150         } else {
151             broadcast({
152                 type: MessageType.HUMAN_VERIFICATION_SUCCESS,
153                 payload: { token, type },
154             });
155         }
156     };
158     const wrapInMain = (child: ReactNode) => (
159         <main className="hv h-full" ref={registerRootRef}>
160             <div
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' }}
163             >
164                 {child}
165             </div>
166         </main>
167     );
169     if (loading) {
170         return null;
171     }
173     if (error) {
174         return <StandardLoadErrorPage />;
175     }
177     if (methods === undefined) {
178         return wrapInMain('You need to specify recovery methods');
179     }
181     if (token === undefined) {
182         return wrapInMain('You need to specify a token');
183     }
185     const hv = (
186         <HumanVerificationForm
187             theme={theme && darkThemes.includes(theme) ? 'dark' : 'light'}
188             step={step}
189             onChangeStep={setStep}
190             onSubmit={handleSubmit}
191             onLoaded={handleLoaded}
192             onClose={handleClose}
193             onError={(e) => {
194                 setError(true);
195                 handleError(e);
196                 // Also sends out a loaded message for clients that don't handle the error message to display the error screen.
197                 handleLoaded();
198             }}
199             methods={methods}
200             token={token}
201             defaultCountry={defaultCountry}
202             defaultEmail={defaultEmail}
203             defaultPhone={defaultPhone}
204             isEmbedded={isEmbedded}
205             verifyApp
206         />
207     );
209     if (isEmbedded) {
210         return (
211             <main className="p-5" ref={registerRootRef}>
212                 {hv}
213             </main>
214         );
215     }
217     return wrapInMain(hv);
220 export default Verify;