Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / password / SSOAuthModal.tsx
bloba41c4f4b569dce60c39e76bd8f0d381ab26bb97c
1 import { useCallback, useEffect, useRef, useState } from 'react';
3 import { c } from 'ttag';
5 import { Button } from '@proton/atoms';
6 import Prompt, { type PromptProps } from '@proton/components/components/prompt/Prompt';
7 import { ExternalSSOError, handleExternalSSOLogin } from '@proton/components/containers/login/ssoExternalLogin';
8 import useApi from '@proton/components/hooks/useApi';
9 import useConfig from '@proton/components/hooks/useConfig';
10 import useErrorHandler from '@proton/components/hooks/useErrorHandler';
11 import { SCOPE_REAUTH_SSO, getInfo } from '@proton/shared/lib/api/auth';
12 import { getApiError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
13 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
14 import type { SSOInfoResponse } from '@proton/shared/lib/authentication/interface';
15 import { API_CODES, APPS } from '@proton/shared/lib/constants';
16 import { getVpnAccountUrl } from '@proton/shared/lib/helpers/url';
17 import noop from '@proton/utils/noop';
19 import type { OwnAuthModalProps, SSOAuthModalResult } from './interface';
21 type State =
22     | {
23           type: 'error';
24           error: any;
25           extra?: string;
26       }
27     | {
28           type: 'loading';
29       }
30     | {
31           type: 'init';
32       };
34 const initialState = { type: 'init' } as const;
36 export interface SSOAuthModalProps
37     extends Omit<OwnAuthModalProps, 'onSuccess'>,
38         Omit<PromptProps, 'title' | 'buttons' | 'children' | 'onError'> {
39     onSuccess?: (data: SSOAuthModalResult) => Promise<void> | void;
42 const SSOAuthModal = ({ scope, onCancel, onClose, onSuccess, onError, config, ...rest }: SSOAuthModalProps) => {
43     const abortRef = useRef<AbortController | null>(null);
44     const handleError = useErrorHandler();
45     const [state, setState] = useState<State>(initialState);
46     const ssoInfoResponsePromiseRef = useRef<Promise<SSOInfoResponse> | null>(null);
47     const api = useApi();
48     const { APP_NAME } = useConfig();
50     const refresh = useCallback(() => {
51         // The SSO info response is cached so that browsers have an easier time allowing the new tab to be opened
52         ssoInfoResponsePromiseRef.current = api<SSOInfoResponse>(getInfo({ intent: 'SSO', reauthScope: scope }));
53     }, []);
55     const cancelClose = () => {
56         onCancel?.();
57         onClose?.();
58     };
60     useEffect(() => {
61         refresh();
62         return () => {
63             abortRef.current?.abort();
64         };
65     }, []);
67     const handleSubmit = async () => {
68         if (!ssoInfoResponsePromiseRef.current) {
69             return;
70         }
72         const abortController = new AbortController();
73         const silentApi = getSilentApi(api);
75         try {
76             setState({ type: 'loading' });
78             abortRef.current?.abort();
79             abortRef.current = abortController;
81             const ssoInfoResponse = await ssoInfoResponsePromiseRef.current;
82             const { token } = await handleExternalSSOLogin({
83                 signal: abortController.signal,
84                 token: ssoInfoResponse.SSOChallengeToken,
85                 finalRedirectBaseUrl: APP_NAME === APPS.PROTONVPN_SETTINGS ? getVpnAccountUrl() : undefined,
86             });
88             const response: Response = await silentApi({
89                 ...config,
90                 output: 'raw',
91                 data: {
92                     ...config.data,
93                     SsoReauthToken: token,
94                 },
95             });
96             // We want to just keep the modal open until the consumer's promise is finished. Not interested in errors.
97             await onSuccess?.({ type: 'sso', response, credentials: { ssoReauthToken: token } })?.catch(noop);
98             onClose?.();
99         } catch (error) {
100             if (error instanceof ExternalSSOError) {
101                 // Try again
102                 refresh();
103                 setState({ type: 'error', error, extra: c('saml: Error').t`Sign in wasn't successfully completed.` });
104                 return;
105             }
107             const { code } = getApiError(error);
108             // Try again
109             if (code === SCOPE_REAUTH_SSO || code === API_CODES.NOT_FOUND_ERROR) {
110                 refresh();
111                 handleError(error);
112                 setState(initialState);
113                 return;
114             }
116             onError?.(error);
117             cancelClose();
118             return;
119         } finally {
120             abortController.abort();
121         }
122     };
124     const loading = state.type === 'loading';
126     // Don't allow to close this modal if it's loading as it could leave other consumers in an undefined state
127     const handleClose = loading ? noop : cancelClose;
129     return (
130         <Prompt
131             {...rest}
132             title={c('sso').t`Sign in to your organization`}
133             buttons={[
134                 <Button color="norm" onClick={handleSubmit} loading={loading}>
135                     {c('sso').t`Sign in`}
136                 </Button>,
137                 <Button onClick={handleClose}>{c('Action').t`Cancel`}</Button>,
138             ]}
139         >
140             <div>{c('sso').t`You'll be redirected to your third-party SSO provider.`}</div>
141             {state.type === 'error' && state.extra && (
142                 <div className="mt-4">
143                     <div className="color-danger">{state.extra}</div>
144                 </div>
145             )}
146         </Prompt>
147     );
150 export default SSOAuthModal;