Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32103541
D14920.1765013216.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
D14920.1765013216.diff
View Options
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,7 +88,8 @@
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;
@@ -134,6 +135,15 @@
height: 195px;
}
+div.new_modal_body .restorationSpinner {
+ padding: 5px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 195px;
+ height: 195px;
+}
+
div.new_modal_body .buttons {
display: flex;
flex-direction: column;
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 <LoadingIndicator status="loading" size="large" color="black" />;
}, [qrCodeURL]);
+ const userDataRestoreStarted = useSelector(
+ state => state.restoreBackupState.status !== 'no_backup',
+ );
+
+ if (fullBackupSupport && (qrAuthInProgress || userDataRestoreStarted)) {
+ return (
+ <RestorationProgress
+ qrAuthInProgress={qrAuthInProgress}
+ userDataRestoreStarted={userDataRestoreStarted}
+ />
+ );
+ }
+
return (
<div className={css.new_modal_body}>
<h1>Log in to Comm</h1>
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,211 @@
+/* Restoration Modal Base Styles */
+.modalBody {
+ display: flex;
+ flex-direction: column;
+ /* justify-content: center; */
+ width: 380px;
+ min-height: 438px;
+ padding: 19px 17px;
+ border-radius: 12px;
+ background-color: #191723;
+ border: #272537 solid 1px;
+}
+
+.modalBody h1 {
+ color: white;
+ 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: white;
+ font-size: var(--s-font-14);
+ font-weight: var(--normal);
+ line-height: var(--line-height-text);
+ margin: 5px 0;
+}
+
+/* Header Styles */
+.restorationSubtitle {
+ color: white;
+ 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: #8c889a;
+ text-align: center;
+}
+
+.stepIconCompleted {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: #4caf50;
+ color: white;
+ 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: #6a20e3;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: pulse 2s infinite;
+ /* for text */
+ color: white;
+ font-size: var(--xs-font-12);
+}
+
+.stepIconError {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: #f44336;
+ 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: #3a3a3a;
+ color: #8c889a;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--xs-font-12);
+}
+
+.stepConnector {
+ width: 30px;
+ height: 2px;
+ background: #3a3a3a;
+ 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: #3a3a3a;
+ padding: 12px;
+ margin-bottom: 16px;
+ border-radius: 8px;
+}
+
+.errorDetailsHeader {
+ font-size: var(--xs-font-12);
+ font-weight: bold;
+ color: white;
+ margin-bottom: 8px;
+}
+
+.errorDetails {
+ font-family: 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New',
+ monospace;
+ font-size: var(--xs-font-12);
+ color: white;
+ line-height: 16px;
+ text-wrap: nowrap;
+ overflow: auto;
+}
+
+/* Button Styles */
+.errorButtons {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ row-gap: 8px;
+
+ /* NOTE: For horizontal placement */
+ /* align-items: center; */
+ /* justify-content: flex-end; */
+ /* column-gap: 16px; */
+ /* padding: 16px 32px; */
+ /* margin: 0 -17px -19px -17px; */
+}
+
+.errorButtons button {
+ font-size: var(--s-font-14);
+}
+
+/* Debug info Styles */
+.debugInfo {
+ display: flex;
+ flex-direction: column;
+}
+
+.debugInfo span {
+ color: #8c889a;
+ 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,208 @@
+// @flow
+
+import * as React from 'react';
+
+import { isDev } from 'lib/utils/dev-utils.js';
+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';
+
+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 (
+ <div className={css.stepItem}>
+ <div className={iconClassName}>{iconText}</div>
+ <span>{children}</span>
+ </div>
+ );
+}
+
+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;
+ if (isDev) {
+ // This hook inside condition is okay, since `isDev` doesn't
+ // change during the whole app runtime
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const restoreState = useSelector(state => state.restoreBackupState);
+ const status = restoreState.status;
+ const restoreStep = restoreState.payload.step ?? 'N/A';
+ debugInfo = (
+ <div className={css.debugInfo}>
+ <span>
+ [DEBUG] Restore state: {status} (Step: {restoreStep})
+ </span>
+ </div>
+ );
+ }
+
+ return (
+ <div className={css.modalBody}>
+ <h1>{title}</h1>
+ <HeaderSeparator />
+ <div className={css.content}>
+ <div className={css.restorationProgress}>
+ <div className={css.progressSteps}>
+ <ProgressStep iconText="1" state={authStepState}>
+ Authentication
+ </ProgressStep>
+ <div className={css.stepConnector}></div>
+ <ProgressStep iconText="2" state={restoringStepState}>
+ Data Restoration
+ </ProgressStep>
+ <div className={css.stepConnector}></div>
+ <ProgressStep iconText="3" state="pending">
+ Complete
+ </ProgressStep>
+ </div>
+ </div>
+ {children}
+ {debugInfo}
+ </div>
+ </div>
+ );
+}
+
+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 (
+ <RestorationViewContainer
+ title="Restoration failed"
+ step={step}
+ isError={true}
+ >
+ <div className={css.errorMessageContainer}>
+ <div className={css.modalText}>
+ 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.
+ </div>
+ <div className={css.errorDetailsContainer}>
+ <div className={css.errorDetailsHeader}>Error message:</div>
+ <div className={css.errorDetails}>{errorDetails}</div>
+ </div>
+ <div className={css.modalText}>
+ For help recovering your data, email{' '}
+ <a href="mailto:support@comm.app">support@comm.app</a> or message
+ Ashoat on the app.
+ </div>
+ </div>
+ <div className={css.errorButtons}>
+ <Button
+ variant="outline"
+ buttonColor={buttonThemes.outline}
+ onClick={onPressIgnore}
+ >
+ Log in without restoring
+ </Button>
+ <Button
+ variant="filled"
+ buttonColor={buttonThemes.primary}
+ onClick={onPressTryAgain}
+ >
+ Try again
+ </Button>
+ </div>
+ </RestorationViewContainer>
+ );
+}
+
+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 <RestorationError step={step} error={restorationError} />;
+ }
+
+ const title =
+ step === 'authenticating' ? 'Authenticating device' : 'Restoring your data';
+ return (
+ <RestorationViewContainer title={title} step={step} isError={false}>
+ <div className={css.restorationSpinnerWrapper}>
+ <LoadingIndicator status="loading" size="x-large" />
+ </div>
+ </RestorationViewContainer>
+ );
+}
+
+export default RestorationProgress;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Dec 6, 9:26 AM (7 h, 29 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5835851
Default Alt Text
D14920.1765013216.diff (13 KB)
Attached To
Mode
D14920: [web] Add UI for tracking restoration progress
Attached
Detach File
Event Timeline
Log In to Comment