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 FailedRestorationErrorProps = { +error: Error, - +step: RestorationStep, }; - -function RestorationError(props: RestorationErrorProps): React.Node { - const { step, error } = props; +function RestorationFailedError( + props: FailedRestorationErrorProps, +): React.Node { + const { error } = props; const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); @@ -155,7 +163,7 @@ return (
@@ -193,38 +201,165 @@ ); } -export type RestorationProgressProps = { +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; + + return ( + +
+
{children}
+ {rawErrorMessage && ( +
+
Error message:
+
{rawErrorMessage}
+
+ )} +
+
+ {!!onTryAgain && ( + + )} +
+
+ ); +} + +export type ProgressViewProps = { + +step: RestorationStep, +}; +function ProgressView(props: ProgressViewProps): React.Node { + const { step } = props; + + const title = + step === 'authenticating' ? 'Authenticating device' : 'Restoring your data'; + + return ( + +
+ +
+
+ ); +} + +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, }; - -function RestorationProgress(props: RestorationProgressProps): React.Node { +function RestorationView(props: RestorationViewProps): React.Node { const { qrAuthInProgress, userDataRestoreStarted } = props; - const restorationError = useSelector(state => + 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); + }); + + return () => subscription.remove(); + }, [registerErrorListener]); + + const retryQRAuth = React.useCallback(() => { + setQRAuthError(null); + }, []); + + if (userDataError) { + return ; + } else if (qrAuthError) { + return ; + } + const step: RestorationStep = qrAuthInProgress && !userDataRestoreStarted ? 'authenticating' : 'restoring'; - if (restorationError) { - return ; - } - - const title = - step === 'authenticating' ? 'Authenticating device' : 'Restoring your data'; - return ( - -
- -
-
- ); + return ; } -export default RestorationProgress; +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' ||