diff --git a/web/account/log-in-form.react.js b/web/account/log-in-form.react.js --- a/web/account/log-in-form.react.js +++ b/web/account/log-in-form.react.js @@ -167,11 +167,17 @@ state => state.restoreBackupState.status !== 'no_backup', ); - if (fullBackupSupport && (qrAuthInProgress || userDataRestoreStarted)) { + const [errorUIShown, setErrorUIShown] = React.useState(false); + + if ( + fullBackupSupport && + (qrAuthInProgress || userDataRestoreStarted || errorUIShown) + ) { return ( ); } diff --git a/web/account/restoration.css b/web/account/restoration.css --- a/web/account/restoration.css +++ b/web/account/restoration.css @@ -177,6 +177,7 @@ flex-direction: column; align-items: stretch; row-gap: 8px; + margin-top: auto; } .errorButtons button { diff --git a/web/account/restoration.react.js b/web/account/restoration.react.js --- a/web/account/restoration.react.js +++ b/web/account/restoration.react.js @@ -10,8 +10,12 @@ logOutActionTypes, useSecondaryDeviceLogOut, } from 'lib/actions/user-actions.js'; +import { useSecondaryDeviceQRAuthContext } from 'lib/components/secondary-device-qr-auth-context-provider.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; -import { getMessageForException } from 'lib/utils/errors.js'; +import { + BackupIsNewerError, + getMessageForException, +} from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; @@ -21,6 +25,10 @@ import LoadingIndicator from '../loading-indicator.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; +import { + getBackupIsNewerThanAppError, + getVersionUnsupportedError, +} from '../utils/version-utils.js'; type RestorationStep = 'authenticating' | 'restoring'; @@ -118,13 +126,13 @@ ); } -type RestorationErrorProps = { +type RestorationFailedErrorProps = { +error: Error, - +step: RestorationStep, }; - -function RestorationError(props: RestorationErrorProps): React.Node { - const { step, error } = props; +function RestorationFailedError( + props: RestorationFailedErrorProps, +): React.Node { + const { error } = props; const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); @@ -155,7 +163,7 @@ return (
@@ -193,31 +201,59 @@ ); } -export type RestorationProgressProps = { - +qrAuthInProgress: boolean, - +userDataRestoreStarted: boolean, +type SimpleErrorProps = { + +title: string, + +rawErrorMessage?: ?string, + +step: RestorationStep, + +onTryAgain?: () => void, + +children?: React.Node, }; +function SimpleError(props: SimpleErrorProps): React.Node { + const { title, step, children, rawErrorMessage, onTryAgain } = props; -function RestorationProgress(props: RestorationProgressProps): React.Node { - const { qrAuthInProgress, userDataRestoreStarted } = props; + let rawErrorContents; + if (rawErrorMessage) { + rawErrorContents = ( +
+
Error message:
+
{rawErrorMessage}
+
+ ); + } - const restorationError = useSelector(state => - state.restoreBackupState.status === 'user_data_restore_failed' - ? state.restoreBackupState.payload.error - : null, - ); + let tryAgainButton; + if (onTryAgain) { + tryAgainButton = ( + + ); + } - const step: RestorationStep = - qrAuthInProgress && !userDataRestoreStarted - ? 'authenticating' - : 'restoring'; + return ( + +
+
{children}
+ {rawErrorContents} +
+
{tryAgainButton}
+
+ ); +} - if (restorationError) { - return ; - } +export type ProgressViewProps = { + +step: RestorationStep, +}; +function ProgressView(props: ProgressViewProps): React.Node { + const { step } = props; const title = step === 'authenticating' ? 'Authenticating device' : 'Restoring your data'; + return (
@@ -227,4 +263,114 @@ ); } -export default RestorationProgress; +type AuthErrorProps = { + +error: mixed, + +handleRetry: () => void, +}; +function AuthErrorView(props: AuthErrorProps): React.Node { + const { error, handleRetry } = props; + + const rawErrorMessage = React.useMemo( + () => getMessageForException(error), + [error], + ); + + if ( + rawErrorMessage === 'client_version_unsupported' || + rawErrorMessage === 'unsupported_version' + ) { + return ( + + {getVersionUnsupportedError()} + + ); + } else if (rawErrorMessage === 'network_error') { + return ( + + Failed to contact Comm services. Please check your network connection. + + ); + } else { + return ( + + Uhh... try again? + + ); + } +} + +type RestoreErrorProps = { + +error: Error, +}; +function RestoreErrorView(props: RestoreErrorProps): React.Node { + const { error } = props; + + if (error instanceof BackupIsNewerError) { + return ( + + {getBackupIsNewerThanAppError()} + + ); + } else { + return ; + } +} + +export type RestorationViewProps = { + +qrAuthInProgress: boolean, + +userDataRestoreStarted: boolean, + +onErrorUIToggle?: (errorShown: boolean) => void, +}; +function RestorationView(props: RestorationViewProps): React.Node { + const { qrAuthInProgress, userDataRestoreStarted, onErrorUIToggle } = props; + + const { registerErrorListener } = useSecondaryDeviceQRAuthContext(); + const [qrAuthError, setQRAuthError] = React.useState(null); + const userDataError = useSelector(state => + state.restoreBackupState.status === 'user_data_restore_failed' + ? state.restoreBackupState.payload.error + : null, + ); + + React.useEffect(() => { + const subscription = registerErrorListener((error, isUserDataError) => { + if (isUserDataError) { + // user data errors are handled by selector + return; + } + setQRAuthError(error); + onErrorUIToggle?.(true); + }); + + return () => subscription.remove(); + }, [registerErrorListener, onErrorUIToggle]); + + const retryQRAuth = React.useCallback(() => { + setQRAuthError(null); + onErrorUIToggle?.(false); + }, [onErrorUIToggle]); + + if (userDataError) { + return ; + } else if (qrAuthError) { + return ; + } + + const step: RestorationStep = + qrAuthInProgress && !userDataRestoreStarted + ? 'authenticating' + : 'restoring'; + + return ; +} + +export default RestorationView; diff --git a/web/utils/qr-code-utils.js b/web/utils/qr-code-utils.js --- a/web/utils/qr-code-utils.js +++ b/web/utils/qr-code-utils.js @@ -22,6 +22,7 @@ BackupIsNewerError, getMessageForException, } from 'lib/utils/errors.js'; +import { fullBackupSupport } from 'lib/utils/services-utils.js'; import { base64DecodeBuffer, base64EncodeBuffer } from './base64-utils.js'; import { getBackupIsNewerThanAppError } from './version-utils.js'; @@ -68,6 +69,11 @@ return React.useCallback( (error: mixed, isUserDataRestoreError?: boolean) => { console.error('Secondary device log in error:', error); + if (fullBackupSupport) { + // for full backup, errors are handled in the restore UI + return; + } + const messageForException = getMessageForException(error); if ( messageForException === 'client_version_unsupported' ||