diff --git a/web/account/log-in-form.css b/web/account/log-in-form.css --- a/web/account/log-in-form.css +++ b/web/account/log-in-form.css @@ -88,11 +88,12 @@ display: flex; flex-direction: column; justify-content: center; - min-width: 350px; + width: 380px; + min-height: 438px; padding: 19px 17px; border-radius: 12px; - background-color: #191723; - border: #272537 solid 1px; + background-color: var(--auth-modal-bg); + border: var(--auth-modal-border-color) solid 1px; } div.new_modal_body h1 { 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 @@ -8,13 +8,20 @@ import ModalOverlay from 'lib/components/modal-overlay.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; -import { useSecondaryDeviceQRAuthURL } from 'lib/components/secondary-device-qr-auth-context-provider.react.js'; +import { + useSecondaryDeviceQRAuthURL, + useSecondaryDeviceQRAuthContext, +} from 'lib/components/secondary-device-qr-auth-context-provider.react.js'; import stores from 'lib/facts/stores.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; -import { useIsRestoreFlowEnabled } from 'lib/utils/services-utils.js'; +import { + useIsRestoreFlowEnabled, + fullBackupSupport, +} from 'lib/utils/services-utils.js'; import HeaderSeparator from './header-separator.react.js'; import css from './log-in-form.css'; +import RestorationProgress from './restoration.react.js'; import SIWEButton from './siwe-button.react.js'; import SIWELoginForm from './siwe-login-form.react.js'; import TraditionalLoginForm from './traditional-login-form.react.js'; @@ -22,6 +29,7 @@ import OrBreak from '../components/or-break.react.js'; import LoadingIndicator from '../loading-indicator.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; +import { useSelector } from '../redux/redux-utils.js'; function LegacyLoginForm() { const { openConnectModal } = useConnectModal(); @@ -84,6 +92,7 @@ function LoginForm() { const qrCodeURL = useSecondaryDeviceQRAuthURL(); + const { qrAuthInProgress } = useSecondaryDeviceQRAuthContext(); const { pushModal, clearModals, popModal } = useModalContext(); @@ -154,6 +163,19 @@ return ; }, [qrCodeURL]); + const userDataRestoreStarted = useSelector( + state => state.restoreBackupState.status !== 'no_backup', + ); + + if (fullBackupSupport && (qrAuthInProgress || userDataRestoreStarted)) { + return ( + + ); + } + return (

Log in to Comm

diff --git a/web/account/restoration.css b/web/account/restoration.css new file mode 100644 --- /dev/null +++ b/web/account/restoration.css @@ -0,0 +1,203 @@ +/* Restoration Modal Base Styles */ +.modalBody { + display: flex; + flex-direction: column; + width: 380px; + min-height: 438px; + padding: 19px 17px; + border-radius: 12px; + background-color: var(--auth-modal-bg); + border: var(--auth-modal-border-color) solid 1px; +} + +.modalBody h1 { + color: var(--fg); + font-size: var(--xl-font-20); + line-height: var(--line-height-display); + font-weight: 700; +} + +.content { + display: flex; + flex: 1; + flex-direction: column; + gap: 14px; +} + +div.content .bottom { + margin-top: auto; +} + +.modalText { + color: var(--fg); + font-size: var(--s-font-14); + font-weight: var(--normal); + line-height: var(--line-height-text); + margin: 5px 0; +} + +/* Header Styles */ +.restorationSubtitle { + color: var(--fg); + font-size: var(--xs-font-12); + font-weight: var(--normal); + margin-top: 4px; + margin-bottom: 8px; +} + +/* Progress Indicator Styles */ +.restorationProgress { + margin-top: 20px; +} + +.progressSteps { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.stepItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + min-width: 70px; +} + +.stepItem span { + font-size: var(--xs-font-12); + color: var(--auth-secondary-text-color); + text-align: center; +} + +.stepIconCompleted { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--auth-step-icon-success-bg); + color: var(--fg); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--xs-font-12); + font-weight: bold; +} + +.stepIconActive { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--auth-step-icon-active-bg); + display: flex; + align-items: center; + justify-content: center; + animation: pulse 2s infinite; + color: var(--fg); + font-weight: bold; + font-size: var(--xs-font-12); +} + +.stepIconError { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--auth-step-icon-error-bg); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--xs-font-12); + font-weight: bold; +} + +.stepIconPending { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--auth-step-icon-pending-bg); + color: var(--auth-secondary-text-color); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--xs-font-12); +} + +.stepConnector { + width: 30px; + height: 2px; + background: var(--border); + margin: 0 4px; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(106, 32, 227, 0.7); + } + 70% { + box-shadow: 0 0 0 8px rgba(106, 32, 227, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(106, 32, 227, 0); + } +} + +/* Error Message Styles */ +.errorMessageContainer { + margin-bottom: 0; +} + +.errorDetailsContainer { + background-color: var(--tool-tip-bg); + padding: 12px; + margin-bottom: 16px; + border-radius: 8px; +} + +.errorDetailsHeader { + font-size: var(--xs-font-12); + font-weight: bold; + color: var(--fg); + margin-bottom: 8px; +} + +.errorDetails { + font-family: 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', + monospace; + font-size: var(--xs-font-12); + color: var(--fg); + line-height: 16px; + text-wrap: nowrap; + overflow: auto; +} + +/* Button Styles */ +.errorButtons { + display: flex; + flex-direction: column; + align-items: stretch; + row-gap: 8px; +} + +.errorButtons button { + font-size: var(--s-font-14); +} + +/* Debug info Styles */ +.debugInfo { + display: flex; + flex-direction: column; +} + +.debugInfo span { + color: var(--auth-secondary-text-color); + font-size: var(--xs-font-12); + line-height: var(--line-height-text); +} + +/* Restoration progress spinner */ +div.restorationSpinnerWrapper { + display: flex; + justify-content: center; + margin: auto; +} diff --git a/web/account/restoration.react.js b/web/account/restoration.react.js new file mode 100644 --- /dev/null +++ b/web/account/restoration.react.js @@ -0,0 +1,206 @@ +// @flow + +import * as React from 'react'; + +import { getMessageForException } from 'lib/utils/errors.js'; + +import HeaderSeparator from './header-separator.react.js'; +import css from './restoration.css'; +import Button, { buttonThemes } from '../components/button.react.js'; +import LoadingIndicator from '../loading-indicator.react.js'; +import { useSelector } from '../redux/redux-utils.js'; +import { useStaffCanSee } from '../utils/staff-utils.js'; + +type RestorationStep = 'authenticating' | 'restoring'; + +type ProgressStepProps = { + +iconText: string, + +state: 'pending' | 'active' | 'completed' | 'errored', + +children?: React.Node, +}; +function ProgressStep(props: ProgressStepProps): React.Node { + const { state, children } = props; + + const [iconText, iconClassName] = React.useMemo(() => { + let content, className; + switch (state) { + case 'completed': + content = '✓'; + className = css.stepIconCompleted; + break; + case 'errored': + content = '✗'; + className = css.stepIconError; + break; + case 'active': + content = props.iconText; + className = css.stepIconActive; + break; + default: + content = props.iconText; + className = css.stepIconPending; + } + return [content, className]; + }, [state, props.iconText]); + + return ( +
+
{iconText}
+ {children} +
+ ); +} + +type ContainerProps = { + +title: string, + +step: RestorationStep, + +isError: boolean, + +children?: React.Node, +}; +function RestorationViewContainer(props: ContainerProps): React.Node { + const { title, step, isError, children } = props; + + const activeStepState = isError ? 'errored' : 'active'; + const authStepState = + step === 'authenticating' ? activeStepState : 'completed'; + const restoringStepState = step === 'restoring' ? activeStepState : 'pending'; + + let debugInfo; + const restoreState = useSelector(state => state.restoreBackupState); + const staffCanSee = useStaffCanSee(); + if (staffCanSee) { + const status = restoreState.status; + const restoreStep = restoreState.payload.step ?? 'N/A'; + debugInfo = ( +
+ + [DEBUG] Restore state: {status} (Step: {restoreStep}) + +
+ ); + } + + return ( +
+

{title}

+ +
+
+
+ + Authentication + +
+ + Data Restoration + +
+ + Complete + +
+
+ {children} + {debugInfo} +
+
+ ); +} + +type RestorationErrorProps = { + +error: Error, + +step: RestorationStep, +}; + +function RestorationError(props: RestorationErrorProps): React.Node { + const { step, error } = props; + + const errorDetails = React.useMemo(() => { + const messageForException = getMessageForException(error); + return messageForException ?? 'unknown error'; + }, [error]); + + const onPressIgnore = React.useCallback(() => { + // TODO: Not implemented + }, []); + + const onPressTryAgain = React.useCallback(() => { + // TODO: Not implemented + }, []); + + return ( + +
+
+ Your backup appears to be corrupt. Be careful with your primary + device, as you may lose data if you log out of it at this time. +
+
+
Error message:
+
{errorDetails}
+
+
+ For help recovering your data, email{' '} + support@comm.app or message + Ashoat on the app. +
+
+
+ + +
+
+ ); +} + +export type RestorationProgressProps = { + +qrAuthInProgress: boolean, + +userDataRestoreStarted: boolean, +}; + +function RestorationProgress(props: RestorationProgressProps): React.Node { + const { qrAuthInProgress, userDataRestoreStarted } = props; + + const restorationError = useSelector(state => + state.restoreBackupState.status === 'user_data_restore_failed' + ? state.restoreBackupState.payload.error + : null, + ); + + const step: RestorationStep = + qrAuthInProgress && !userDataRestoreStarted + ? 'authenticating' + : 'restoring'; + + if (restorationError) { + return ; + } + + const title = + step === 'authenticating' ? 'Authenticating device' : 'Restoring your data'; + return ( + +
+ +
+
+ ); +} + +export default RestorationProgress; diff --git a/web/theme.css b/web/theme.css --- a/web/theme.css +++ b/web/theme.css @@ -229,6 +229,13 @@ --unsaved-changes-modal-text-color: var(--shades-white-60); --modal-secondary-label: var(--shades-black-50); --modal-separator: var(--shades-black-75); + --auth-modal-border-color: #272537; + --auth-modal-bg: #191723; + --auth-step-icon-pending-bg: #3a3a3a; + --auth-step-icon-active-bg: #4caf50; + --auth-step-icon-success-bg: #4caf50; + --auth-step-icon-error-bg: #4caf50; + --auth-secondary-text-color: #8c889a; } /*