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;
}
/*