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';
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);
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 }));
55 const cancelClose = () => {
63 abortRef.current?.abort();
67 const handleSubmit = async () => {
68 if (!ssoInfoResponsePromiseRef.current) {
72 const abortController = new AbortController();
73 const silentApi = getSilentApi(api);
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,
88 const response: Response = await silentApi({
93 SsoReauthToken: token,
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);
100 if (error instanceof ExternalSSOError) {
103 setState({ type: 'error', error, extra: c('saml: Error').t`Sign in wasn't successfully completed.` });
107 const { code } = getApiError(error);
109 if (code === SCOPE_REAUTH_SSO || code === API_CODES.NOT_FOUND_ERROR) {
112 setState(initialState);
120 abortController.abort();
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;
132 title={c('sso').t`Sign in to your organization`}
134 <Button color="norm" onClick={handleSubmit} loading={loading}>
135 {c('sso').t`Sign in`}
137 <Button onClick={handleClose}>{c('Action').t`Cancel`}</Button>,
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>
150 export default SSOAuthModal;