Make backup import UI part of install
This commit is contained in:
parent
6bd9502f5c
commit
ee0090bb84
26 changed files with 829 additions and 596 deletions
|
@ -125,6 +125,7 @@ import type { SendStateByConversationId } from './messages/MessageSendState';
|
|||
import { SendStatus } from './messages/MessageSendState';
|
||||
import * as Stickers from './types/Stickers';
|
||||
import * as Errors from './types/errors';
|
||||
import { InstallScreenStep } from './types/InstallScreen';
|
||||
import { SignalService as Proto } from './protobuf';
|
||||
import {
|
||||
onRetryRequest,
|
||||
|
@ -1242,7 +1243,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
window.Whisper.events.on('setupAsNewDevice', () => {
|
||||
window.IPC.readyForUpdates();
|
||||
window.reduxActions.app.openInstaller();
|
||||
window.reduxActions.installer.startInstaller();
|
||||
});
|
||||
|
||||
window.Whisper.events.on('setupAsStandalone', () => {
|
||||
|
@ -1461,13 +1462,13 @@ export async function startApp(): Promise<void> {
|
|||
if (isCoreDataValid && Registration.everDone()) {
|
||||
drop(connect());
|
||||
if (window.storage.get('backupDownloadPath')) {
|
||||
window.reduxActions.app.openBackupImport();
|
||||
window.reduxActions.installer.showBackupImport();
|
||||
} else {
|
||||
window.reduxActions.app.openInbox();
|
||||
}
|
||||
} else {
|
||||
window.IPC.readyForUpdates();
|
||||
window.reduxActions.app.openInstaller();
|
||||
window.reduxActions.installer.startInstaller();
|
||||
}
|
||||
|
||||
const { activeWindowService } = window.SignalContext;
|
||||
|
@ -1518,6 +1519,8 @@ export async function startApp(): Promise<void> {
|
|||
afterStart();
|
||||
}
|
||||
|
||||
const backupReady = explodePromise<void>();
|
||||
|
||||
function afterStart() {
|
||||
strictAssert(messageReceiver, 'messageReceiver must be initialized');
|
||||
strictAssert(server, 'server must be initialized');
|
||||
|
@ -1553,13 +1556,17 @@ export async function startApp(): Promise<void> {
|
|||
drop(messageReceiver?.drain());
|
||||
|
||||
if (hasAppEverBeenRegistered) {
|
||||
if (
|
||||
window.reduxStore.getState().app.appView === AppViewType.Installer
|
||||
) {
|
||||
log.info(
|
||||
'background: offline, but app has been registered before; opening inbox'
|
||||
);
|
||||
window.reduxActions.app.openInbox();
|
||||
const state = window.reduxStore.getState();
|
||||
if (state.app.appView === AppViewType.Installer) {
|
||||
if (state.installer.step === InstallScreenStep.LinkInProgress) {
|
||||
log.info(
|
||||
'background: offline, but app has been registered before; opening inbox'
|
||||
);
|
||||
window.reduxActions.app.openInbox();
|
||||
} else if (state.installer.step === InstallScreenStep.BackupImport) {
|
||||
log.warn('background: offline, but app has needs to import backup');
|
||||
// TODO: DESKTOP-7584
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasInitialLoadCompleted) {
|
||||
|
@ -1580,44 +1587,43 @@ export async function startApp(): Promise<void> {
|
|||
onOffline();
|
||||
}
|
||||
|
||||
if (window.storage.get('backupDownloadPath')) {
|
||||
log.info(
|
||||
'background: not running storage service while downloading backup'
|
||||
);
|
||||
drop(downloadBackup());
|
||||
return;
|
||||
}
|
||||
|
||||
server.registerRequestHandler(messageReceiver);
|
||||
drop(downloadBackup());
|
||||
}
|
||||
|
||||
async function downloadBackup() {
|
||||
strictAssert(server != null, 'server must be initialized');
|
||||
strictAssert(
|
||||
messageReceiver != null,
|
||||
'MessageReceiver must be initialized'
|
||||
);
|
||||
|
||||
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
||||
if (!backupDownloadPath) {
|
||||
log.warn('No backup download path, cannot download backup');
|
||||
log.warn('downloadBackup: no backup download path, skipping');
|
||||
backupReady.resolve();
|
||||
server.registerRequestHandler(messageReceiver);
|
||||
drop(runStorageService());
|
||||
return;
|
||||
}
|
||||
|
||||
const absoluteDownloadPath =
|
||||
window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath);
|
||||
log.info('downloadBackup: downloading to', absoluteDownloadPath);
|
||||
await backupsService.download(absoluteDownloadPath, {
|
||||
log.info('downloadBackup: downloading...');
|
||||
const hasBackup = await backupsService.download(absoluteDownloadPath, {
|
||||
onProgress: (currentBytes, totalBytes) => {
|
||||
window.reduxActions.app.updateBackupImportProgress({
|
||||
window.reduxActions.installer.updateBackupImportProgress({
|
||||
currentBytes,
|
||||
totalBytes,
|
||||
});
|
||||
},
|
||||
});
|
||||
await window.storage.remove('backupDownloadPath');
|
||||
window.reduxActions.app.openInbox();
|
||||
|
||||
log.info(`downloadBackup: done, had backup=${hasBackup}`);
|
||||
|
||||
// Start storage service sync, etc
|
||||
log.info('downloadBackup: processing websocket messages, storage service');
|
||||
strictAssert(server != null, 'server must be initialized');
|
||||
strictAssert(
|
||||
messageReceiver != null,
|
||||
'MessageReceiver must be initialized'
|
||||
);
|
||||
backupReady.resolve();
|
||||
server.registerRequestHandler(messageReceiver);
|
||||
drop(runStorageService());
|
||||
}
|
||||
|
@ -1718,6 +1724,8 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
strictAssert(server !== undefined, 'WebAPI not connected');
|
||||
|
||||
await backupReady.promise;
|
||||
|
||||
try {
|
||||
connectPromise = explodePromise();
|
||||
// Reset the flag and update it below if needed
|
||||
|
@ -1923,9 +1931,8 @@ export async function startApp(): Promise<void> {
|
|||
setIsInitialSync(false);
|
||||
|
||||
// Switch to inbox view even if contact sync is still running
|
||||
if (
|
||||
window.reduxStore.getState().app.appView === AppViewType.Installer
|
||||
) {
|
||||
const state = window.reduxStore.getState();
|
||||
if (state.app.appView === AppViewType.Installer) {
|
||||
log.info('firstRun: opening inbox');
|
||||
window.reduxActions.app.openInbox();
|
||||
} else {
|
||||
|
@ -1961,6 +1968,17 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
log.info('firstRun: done');
|
||||
} else {
|
||||
const state = window.reduxStore.getState();
|
||||
if (
|
||||
state.app.appView === AppViewType.Installer &&
|
||||
state.installer.step === InstallScreenStep.BackupImport
|
||||
) {
|
||||
log.info('notFirstRun: opening inbox after backup import');
|
||||
window.reduxActions.app.openInbox();
|
||||
} else {
|
||||
log.info('notFirstRun: not opening inbox');
|
||||
}
|
||||
}
|
||||
|
||||
window.storage.onready(async () => {
|
||||
|
|
|
@ -8,17 +8,14 @@ import classNames from 'classnames';
|
|||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||
import type { VerificationTransport } from '../types/VerificationTransport';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { type AppStateType, AppViewType } from '../state/ducks/app';
|
||||
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
||||
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||
import { BackupImportScreen } from './BackupImportScreen';
|
||||
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
state: AppStateType;
|
||||
openInbox: () => void;
|
||||
getCaptchaToken: () => Promise<string>;
|
||||
|
@ -53,7 +50,6 @@ type PropsType = {
|
|||
};
|
||||
|
||||
export function App({
|
||||
i18n,
|
||||
state,
|
||||
getCaptchaToken,
|
||||
hasSelectedStoryData,
|
||||
|
@ -96,10 +92,8 @@ export function App({
|
|||
contents = renderInbox();
|
||||
} else if (state.appView === AppViewType.Blank) {
|
||||
contents = undefined;
|
||||
} else if (state.appView === AppViewType.BackupImport) {
|
||||
contents = <BackupImportScreen i18n={i18n} {...state} />;
|
||||
} else {
|
||||
throw missingCaseError(state);
|
||||
throw missingCaseError(state.appView);
|
||||
}
|
||||
|
||||
// This are here so that themes are properly applied to anything that is
|
||||
|
|
|
@ -5,26 +5,17 @@ import type { ComponentProps, ReactElement } from 'react';
|
|||
import React from 'react';
|
||||
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { InstallScreenStep } from '../types/InstallScreen';
|
||||
import { InstallScreenErrorStep } from './installScreen/InstallScreenErrorStep';
|
||||
import { InstallScreenChoosingDeviceNameStep } from './installScreen/InstallScreenChoosingDeviceNameStep';
|
||||
import { InstallScreenLinkInProgressStep } from './installScreen/InstallScreenLinkInProgressStep';
|
||||
import { InstallScreenQrCodeNotScannedStep } from './installScreen/InstallScreenQrCodeNotScannedStep';
|
||||
|
||||
export enum InstallScreenStep {
|
||||
Error,
|
||||
QrCodeNotScanned,
|
||||
ChoosingDeviceName,
|
||||
LinkInProgress,
|
||||
}
|
||||
import { InstallScreenBackupImportStep } from './installScreen/InstallScreenBackupImportStep';
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
|
||||
type PropsType =
|
||||
| {
|
||||
step: InstallScreenStep.Error;
|
||||
screenSpecificProps: ComponentProps<typeof InstallScreenErrorStep>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.QrCodeNotScanned;
|
||||
screenSpecificProps: ComponentProps<
|
||||
|
@ -42,6 +33,14 @@ type PropsType =
|
|||
screenSpecificProps: ComponentProps<
|
||||
typeof InstallScreenLinkInProgressStep
|
||||
>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.BackupImport;
|
||||
screenSpecificProps: ComponentProps<typeof InstallScreenBackupImportStep>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.Error;
|
||||
screenSpecificProps: ComponentProps<typeof InstallScreenErrorStep>;
|
||||
};
|
||||
|
||||
export function InstallScreen(props: Readonly<PropsType>): ReactElement {
|
||||
|
@ -58,6 +57,8 @@ export function InstallScreen(props: Readonly<PropsType>): ReactElement {
|
|||
);
|
||||
case InstallScreenStep.LinkInProgress:
|
||||
return <InstallScreenLinkInProgressStep {...props.screenSpecificProps} />;
|
||||
case InstallScreenStep.BackupImport:
|
||||
return <InstallScreenBackupImportStep {...props.screenSpecificProps} />;
|
||||
default:
|
||||
throw missingCaseError(props);
|
||||
}
|
||||
|
|
|
@ -3,20 +3,20 @@
|
|||
|
||||
import React from 'react';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { PropsType } from './BackupImportScreen';
|
||||
import { BackupImportScreen } from './BackupImportScreen';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { PropsType } from './InstallScreenBackupImportStep';
|
||||
import { InstallScreenBackupImportStep } from './InstallScreenBackupImportStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/BackupImportScreen',
|
||||
title: 'Components/InstallScreenBackupImportStep',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = (args: PropsType) => (
|
||||
<BackupImportScreen {...args} i18n={i18n} />
|
||||
<InstallScreenBackupImportStep {...args} i18n={i18n} />
|
||||
);
|
||||
|
||||
export const NoBytes = Template.bind({});
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { formatFileSize } from '../util/formatFileSize';
|
||||
import { TitlebarDragArea } from './TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './installScreen/InstallScreenSignalLogo';
|
||||
import { ProgressBar } from './ProgressBar';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { formatFileSize } from '../../util/formatFileSize';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { ProgressBar } from '../ProgressBar';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
|
@ -18,7 +18,7 @@ export type PropsType = Readonly<{
|
|||
totalBytes?: number;
|
||||
}>;
|
||||
|
||||
export function BackupImportScreen({
|
||||
export function InstallScreenBackupImportStep({
|
||||
i18n,
|
||||
currentBytes,
|
||||
totalBytes,
|
|
@ -5,6 +5,7 @@ import type { ReactElement } from 'react';
|
|||
import React, { useRef } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { MAX_DEVICE_NAME_LENGTH } from '../../types/InstallScreen';
|
||||
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
|
||||
import { getEnvironment, Environment } from '../../environment';
|
||||
|
||||
|
@ -12,11 +13,6 @@ import { Button, ButtonVariant } from '../Button';
|
|||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
// This is the string's `.length`, which is the number of UTF-16 code points. Instead, we
|
||||
// want this to be either 50 graphemes or 256 encrypted bytes, whichever is smaller. See
|
||||
// DESKTOP-2844.
|
||||
export const MAX_DEVICE_NAME_LENGTH = 50;
|
||||
|
||||
export type PropsType = {
|
||||
deviceName: string;
|
||||
i18n: LocalizerType;
|
||||
|
|
|
@ -5,9 +5,10 @@ import React from 'react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { InstallScreenError } from '../../types/InstallScreen';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { Props } from './InstallScreenErrorStep';
|
||||
import { InstallScreenErrorStep, InstallError } from './InstallScreenErrorStep';
|
||||
import { InstallScreenErrorStep } from './InstallScreenErrorStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -24,21 +25,21 @@ const defaultProps = {
|
|||
export const _TooManyDevices = (): JSX.Element => (
|
||||
<InstallScreenErrorStep
|
||||
{...defaultProps}
|
||||
error={InstallError.TooManyDevices}
|
||||
error={InstallScreenError.TooManyDevices}
|
||||
/>
|
||||
);
|
||||
|
||||
export const _TooOld = (): JSX.Element => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallScreenError.TooOld} />
|
||||
);
|
||||
|
||||
export const __TooOld = (): JSX.Element => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallScreenError.TooOld} />
|
||||
);
|
||||
|
||||
export const _ConnectionFailed = (): JSX.Element => (
|
||||
<InstallScreenErrorStep
|
||||
{...defaultProps}
|
||||
error={InstallError.ConnectionFailed}
|
||||
error={InstallScreenError.ConnectionFailed}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -10,16 +10,10 @@ import { Button, ButtonVariant } from '../Button';
|
|||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
import { LINK_SIGNAL_DESKTOP } from '../../types/support';
|
||||
|
||||
export enum InstallError {
|
||||
TooManyDevices,
|
||||
TooOld,
|
||||
ConnectionFailed,
|
||||
QRCodeFailed,
|
||||
}
|
||||
import { InstallScreenError } from '../../types/InstallScreen';
|
||||
|
||||
export type Props = Readonly<{
|
||||
error: InstallError;
|
||||
error: InstallScreenError;
|
||||
i18n: LocalizerType;
|
||||
quit: () => unknown;
|
||||
tryAgain: () => unknown;
|
||||
|
@ -37,10 +31,10 @@ export function InstallScreenErrorStep({
|
|||
let shouldShowQuitButton = false;
|
||||
|
||||
switch (error) {
|
||||
case InstallError.TooManyDevices:
|
||||
case InstallScreenError.TooManyDevices:
|
||||
errorMessage = i18n('icu:installTooManyDevices');
|
||||
break;
|
||||
case InstallError.TooOld:
|
||||
case InstallScreenError.TooOld:
|
||||
errorMessage = i18n('icu:installTooOld');
|
||||
buttonText = i18n('icu:upgrade');
|
||||
onClickButton = () => {
|
||||
|
@ -48,10 +42,10 @@ export function InstallScreenErrorStep({
|
|||
};
|
||||
shouldShowQuitButton = true;
|
||||
break;
|
||||
case InstallError.ConnectionFailed:
|
||||
case InstallScreenError.ConnectionFailed:
|
||||
errorMessage = i18n('icu:installConnectionFailed');
|
||||
break;
|
||||
case InstallError.QRCodeFailed:
|
||||
case InstallScreenError.QRCodeFailed:
|
||||
buttonText = i18n('icu:Install__learn-more');
|
||||
errorMessage = i18n('icu:installUnknownError');
|
||||
onClickButton = () => {
|
||||
|
|
|
@ -6,14 +6,12 @@ import { action } from '@storybook/addon-actions';
|
|||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import { DialogType } from '../../types/Dialogs';
|
||||
import { InstallScreenQRCodeError } from '../../types/InstallScreen';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
import type { PropsType } from './InstallScreenQrCodeNotScannedStep';
|
||||
import {
|
||||
InstallScreenQrCodeNotScannedStep,
|
||||
LoadError,
|
||||
} from './InstallScreenQrCodeNotScannedStep';
|
||||
import { InstallScreenQrCodeNotScannedStep } from './InstallScreenQrCodeNotScannedStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -40,10 +38,10 @@ export default {
|
|||
function Simulation({
|
||||
finalResult,
|
||||
}: {
|
||||
finalResult: Loadable<string, LoadError>;
|
||||
finalResult: Loadable<string, InstallScreenQRCodeError>;
|
||||
}) {
|
||||
const [provisioningUrl, setProvisioningUrl] = useState<
|
||||
Loadable<string, LoadError>
|
||||
Loadable<string, InstallScreenQRCodeError>
|
||||
>({
|
||||
loadingState: LoadingState.Loading,
|
||||
});
|
||||
|
@ -92,7 +90,7 @@ export function QrCodeFailedToLoad(): JSX.Element {
|
|||
i18n={i18n}
|
||||
provisioningUrl={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: LoadError.Unknown,
|
||||
error: InstallScreenQRCodeError.Unknown,
|
||||
}}
|
||||
updates={DEFAULT_UPDATES}
|
||||
OS="macOS"
|
||||
|
@ -126,7 +124,7 @@ export function SimulatedUnknownError(): JSX.Element {
|
|||
<Simulation
|
||||
finalResult={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: LoadError.Unknown,
|
||||
error: InstallScreenQRCodeError.Unknown,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -137,7 +135,7 @@ export function SimulatedNetworkIssue(): JSX.Element {
|
|||
<Simulation
|
||||
finalResult={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: LoadError.NetworkIssue,
|
||||
error: InstallScreenQRCodeError.NetworkIssue,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -148,7 +146,7 @@ export function SimulatedTimeout(): JSX.Element {
|
|||
<Simulation
|
||||
finalResult={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: LoadError.Timeout,
|
||||
error: InstallScreenQRCodeError.Timeout,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ import React, { useCallback } from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { InstallScreenQRCodeError } from '../../types/InstallScreen';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
|
@ -20,18 +21,12 @@ import { getClassNamesFor } from '../../util/getClassNamesFor';
|
|||
import type { UpdatesStateType } from '../../state/ducks/updates';
|
||||
import { Environment, getEnvironment } from '../../environment';
|
||||
|
||||
export enum LoadError {
|
||||
Timeout = 'Timeout',
|
||||
Unknown = 'Unknown',
|
||||
NetworkIssue = 'NetworkIssue',
|
||||
}
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
provisioningUrl: Loadable<string, LoadError>;
|
||||
provisioningUrl: Loadable<string, InstallScreenQRCodeError>;
|
||||
hasExpired?: boolean;
|
||||
updates: UpdatesStateType;
|
||||
currentVersion: string;
|
||||
|
@ -121,7 +116,7 @@ export function InstallScreenQrCodeNotScannedStep({
|
|||
}
|
||||
|
||||
function InstallScreenQrCode(
|
||||
props: Loadable<string, LoadError> & {
|
||||
props: Loadable<string, InstallScreenQRCodeError> & {
|
||||
i18n: LocalizerType;
|
||||
retryGetQrCode: () => void;
|
||||
}
|
||||
|
@ -135,7 +130,7 @@ function InstallScreenQrCode(
|
|||
break;
|
||||
case LoadingState.LoadFailed:
|
||||
switch (props.error) {
|
||||
case LoadError.Timeout:
|
||||
case InstallScreenQRCodeError.Timeout:
|
||||
contents = (
|
||||
<>
|
||||
<span
|
||||
|
@ -147,7 +142,7 @@ function InstallScreenQrCode(
|
|||
</>
|
||||
);
|
||||
break;
|
||||
case LoadError.Unknown:
|
||||
case InstallScreenQRCodeError.Unknown:
|
||||
contents = (
|
||||
<>
|
||||
<span
|
||||
|
@ -163,7 +158,7 @@ function InstallScreenQrCode(
|
|||
</>
|
||||
);
|
||||
break;
|
||||
case LoadError.NetworkIssue:
|
||||
case InstallScreenQRCodeError.NetworkIssue:
|
||||
contents = (
|
||||
<>
|
||||
<span
|
||||
|
|
|
@ -10,7 +10,7 @@ import { isNumber } from 'lodash';
|
|||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||
|
||||
import { Backups, SignalService } from '../../protobuf';
|
||||
import { DataWriter } from '../../sql/Client';
|
||||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import {
|
||||
AttachmentDownloadSource,
|
||||
type StoryDistributionWithMembersType,
|
||||
|
@ -78,7 +78,7 @@ import type { GroupV2ChangeDetailType } from '../../groups';
|
|||
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
|
||||
import { drop } from '../../util/drop';
|
||||
import { isNotNil } from '../../util/isNotNil';
|
||||
import { isGroup, isGroupV2 } from '../../util/whatTypeOfConversation';
|
||||
import { isGroup } from '../../util/whatTypeOfConversation';
|
||||
import { rgbToHSL } from '../../util/rgbToHSL';
|
||||
import {
|
||||
convertBackupMessageAttachmentToAttachment,
|
||||
|
@ -88,6 +88,7 @@ import { filterAndClean } from '../../types/BodyRange';
|
|||
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../../types/MIME';
|
||||
import { copyFromQuotedMessage } from '../../messages/copyQuote';
|
||||
import { groupAvatarJobQueue } from '../../jobs/groupAvatarJobQueue';
|
||||
import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
CallDirection,
|
||||
|
@ -99,9 +100,10 @@ import {
|
|||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
|
||||
import { fromAdminKeyBytes } from '../../util/callLinks';
|
||||
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||
import { reinitializeRedux } from '../../state/reinitializeRedux';
|
||||
import { getParametersForRedux, loadAll } from '../allLoaders';
|
||||
|
||||
const MAX_CONCURRENCY = 10;
|
||||
|
||||
|
@ -283,11 +285,21 @@ export class BackupImportStream extends Writable {
|
|||
private customColorById = new Map<number, CustomColorDataType>();
|
||||
private releaseNotesRecipientId: Long | undefined;
|
||||
private releaseNotesChatId: Long | undefined;
|
||||
private pendingGroupAvatars = new Map<string, string>();
|
||||
|
||||
constructor() {
|
||||
private constructor() {
|
||||
super({ objectMode: true });
|
||||
}
|
||||
|
||||
public static async create(): Promise<BackupImportStream> {
|
||||
await AttachmentDownloadManager.stop();
|
||||
await DataWriter.removeAllBackupAttachmentDownloadJobs();
|
||||
await window.storage.put('backupAttachmentsSuccessfullyDownloadedSize', 0);
|
||||
await window.storage.put('backupAttachmentsTotalSizeToDownload', 0);
|
||||
|
||||
return new BackupImportStream();
|
||||
}
|
||||
|
||||
override async _write(
|
||||
data: Buffer,
|
||||
_enc: BufferEncoding,
|
||||
|
@ -359,11 +371,10 @@ export class BackupImportStream extends Writable {
|
|||
|
||||
// Schedule group avatar download.
|
||||
await pMap(
|
||||
allConversations.filter(({ attributes: convo }) => {
|
||||
const { avatar } = convo;
|
||||
return isGroupV2(convo) && avatar?.url && !avatar.path;
|
||||
}),
|
||||
convo => groupAvatarJobQueue.add({ conversationId: convo.id }),
|
||||
[...this.pendingGroupAvatars.entries()],
|
||||
([conversationId, newAvatarUrl]) => {
|
||||
return groupAvatarJobQueue.add({ conversationId, newAvatarUrl });
|
||||
},
|
||||
{ concurrency: MAX_CONCURRENCY }
|
||||
);
|
||||
|
||||
|
@ -376,6 +387,16 @@ export class BackupImportStream extends Writable {
|
|||
.map(([, id]) => id)
|
||||
);
|
||||
|
||||
await loadAll();
|
||||
reinitializeRedux(getParametersForRedux());
|
||||
|
||||
await window.storage.put(
|
||||
'backupAttachmentsTotalSizeToDownload',
|
||||
await DataReader.getSizeOfPendingBackupAttachmentDownloadJobs()
|
||||
);
|
||||
|
||||
await AttachmentDownloadManager.start();
|
||||
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
|
@ -843,12 +864,6 @@ export class BackupImportStream extends Writable {
|
|||
// Snapshot
|
||||
name: dropNull(title?.title),
|
||||
description: dropNull(description?.descriptionText),
|
||||
avatar: avatarUrl
|
||||
? {
|
||||
url: avatarUrl,
|
||||
path: '',
|
||||
}
|
||||
: undefined,
|
||||
expireTimer: expirationTimerS
|
||||
? DurationInSeconds.fromSeconds(expirationTimerS)
|
||||
: undefined,
|
||||
|
@ -937,6 +952,9 @@ export class BackupImportStream extends Writable {
|
|||
: undefined,
|
||||
announcementsOnly: dropNull(announcementsOnly),
|
||||
};
|
||||
if (avatarUrl) {
|
||||
this.pendingGroupAvatars.set(attrs.id, avatarUrl);
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ import * as Errors from '../../types/errors';
|
|||
import { HTTPError } from '../../textsecure/Errors';
|
||||
import { constantTimeEqual } from '../../Crypto';
|
||||
import { measureSize } from '../../AttachmentCrypto';
|
||||
import { reinitializeRedux } from '../../state/reinitializeRedux';
|
||||
import { isTestOrMockEnvironment } from '../../environment';
|
||||
import { BackupExportStream } from './export';
|
||||
import { BackupImportStream } from './import';
|
||||
|
@ -39,8 +38,6 @@ import { getKeyMaterial } from './crypto';
|
|||
import { BackupCredentials } from './credentials';
|
||||
import { BackupAPI, type DownloadOptionsType } from './api';
|
||||
import { validateBackup } from './validator';
|
||||
import { getParametersForRedux, loadAll } from '../allLoaders';
|
||||
import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager';
|
||||
|
||||
const IV_LENGTH = 16;
|
||||
|
||||
|
@ -151,7 +148,7 @@ export class BackupsService {
|
|||
public async download(
|
||||
downloadPath: string,
|
||||
{ onProgress }: Omit<DownloadOptionsType, 'downloadOffset'>
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
let downloadOffset = 0;
|
||||
try {
|
||||
({ size: downloadOffset } = await stat(downloadPath));
|
||||
|
@ -187,7 +184,7 @@ export class BackupsService {
|
|||
} catch (error) {
|
||||
// No backup on the server
|
||||
if (error instanceof HTTPError && error.code === 404) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -197,6 +194,8 @@ export class BackupsService {
|
|||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async importBackup(
|
||||
|
@ -209,6 +208,7 @@ export class BackupsService {
|
|||
this.isRunning = true;
|
||||
|
||||
try {
|
||||
const importStream = await BackupImportStream.create();
|
||||
if (backupType === BackupType.Ciphertext) {
|
||||
const { aesKey, macKey } = getKeyMaterial();
|
||||
|
||||
|
@ -237,15 +237,13 @@ export class BackupsService {
|
|||
// Second pass - decrypt (but still check the mac at the end)
|
||||
hmac = createHmac(HashType.size256, macKey);
|
||||
|
||||
await this.prepareForImport();
|
||||
|
||||
await pipeline(
|
||||
createBackupStream(),
|
||||
getMacAndUpdateHmac(hmac, noop),
|
||||
getIvAndDecipher(aesKey),
|
||||
createGunzip(),
|
||||
new DelimitedStream(),
|
||||
new BackupImportStream()
|
||||
importStream
|
||||
);
|
||||
|
||||
strictAssert(
|
||||
|
@ -260,14 +258,12 @@ export class BackupsService {
|
|||
await pipeline(
|
||||
createBackupStream(),
|
||||
new DelimitedStream(),
|
||||
new BackupImportStream()
|
||||
importStream
|
||||
);
|
||||
} else {
|
||||
throw missingCaseError(backupType);
|
||||
}
|
||||
|
||||
await this.resetStateAfterImport();
|
||||
|
||||
log.info('importBackup: finished...');
|
||||
} catch (error) {
|
||||
log.info(`importBackup: failed, error: ${Errors.toLogFormat(error)}`);
|
||||
|
@ -281,27 +277,6 @@ export class BackupsService {
|
|||
}
|
||||
}
|
||||
|
||||
public async prepareForImport(): Promise<void> {
|
||||
await AttachmentDownloadManager.stop();
|
||||
await DataWriter.removeAllBackupAttachmentDownloadJobs();
|
||||
await window.storage.put('backupAttachmentsSuccessfullyDownloadedSize', 0);
|
||||
await window.storage.put('backupAttachmentsTotalSizeToDownload', 0);
|
||||
}
|
||||
|
||||
public async resetStateAfterImport(): Promise<void> {
|
||||
window.ConversationController.reset();
|
||||
await window.ConversationController.load();
|
||||
await loadAll();
|
||||
reinitializeRedux(getParametersForRedux());
|
||||
|
||||
await window.storage.put(
|
||||
'backupAttachmentsTotalSizeToDownload',
|
||||
await DataReader.getSizeOfPendingBackupAttachmentDownloadJobs()
|
||||
);
|
||||
|
||||
await AttachmentDownloadManager.start();
|
||||
}
|
||||
|
||||
public async fetchAndSaveBackupCdnObjectMetadata(): Promise<void> {
|
||||
log.info('fetchAndSaveBackupCdnObjectMetadata: clearing existing metadata');
|
||||
await DataWriter.clearAllBackupCdnObjectMetadata();
|
||||
|
|
|
@ -15,6 +15,7 @@ import { actions as emojis } from './ducks/emojis';
|
|||
import { actions as expiration } from './ducks/expiration';
|
||||
import { actions as globalModals } from './ducks/globalModals';
|
||||
import { actions as inbox } from './ducks/inbox';
|
||||
import { actions as installer } from './ducks/installer';
|
||||
import { actions as items } from './ducks/items';
|
||||
import { actions as lightbox } from './ducks/lightbox';
|
||||
import { actions as linkPreviews } from './ducks/linkPreviews';
|
||||
|
@ -46,6 +47,7 @@ export const actionCreators: ReduxActions = {
|
|||
expiration,
|
||||
globalModals,
|
||||
inbox,
|
||||
installer,
|
||||
items,
|
||||
lightbox,
|
||||
linkPreviews,
|
||||
|
|
|
@ -7,6 +7,12 @@ import type { StateType as RootStateType } from '../reducer';
|
|||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import * as log from '../../logging/log';
|
||||
import {
|
||||
START_INSTALLER,
|
||||
type StartInstallerActionType,
|
||||
SHOW_BACKUP_IMPORT,
|
||||
type ShowBackupImportActionType,
|
||||
} from './installer';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -15,41 +21,19 @@ export enum AppViewType {
|
|||
Inbox = 'Inbox',
|
||||
Installer = 'Installer',
|
||||
Standalone = 'Standalone',
|
||||
BackupImport = 'BackupImport',
|
||||
}
|
||||
|
||||
export type AppStateType = ReadonlyDeep<
|
||||
{
|
||||
hasInitialLoadCompleted: boolean;
|
||||
} & (
|
||||
| {
|
||||
appView: AppViewType.Blank;
|
||||
}
|
||||
| {
|
||||
appView: AppViewType.Inbox;
|
||||
}
|
||||
| {
|
||||
appView: AppViewType.Installer;
|
||||
}
|
||||
| {
|
||||
appView: AppViewType.Standalone;
|
||||
}
|
||||
| {
|
||||
appView: AppViewType.BackupImport;
|
||||
currentBytes?: number;
|
||||
totalBytes?: number;
|
||||
}
|
||||
)
|
||||
>;
|
||||
export type AppStateType = ReadonlyDeep<{
|
||||
hasInitialLoadCompleted: boolean;
|
||||
appView: AppViewType;
|
||||
}>;
|
||||
|
||||
// Actions
|
||||
|
||||
const INITIAL_LOAD_COMPLETE = 'app/INITIAL_LOAD_COMPLETE';
|
||||
const OPEN_INBOX = 'app/OPEN_INBOX';
|
||||
const OPEN_INSTALLER = 'app/OPEN_INSTALLER';
|
||||
export const OPEN_INSTALLER = 'app/OPEN_INSTALLER';
|
||||
const OPEN_STANDALONE = 'app/OPEN_STANDALONE';
|
||||
const OPEN_BACKUP_IMPORT = 'app/OPEN_BACKUP_IMPORT';
|
||||
const UPDATE_BACKUP_IMPORT_PROGRESS = 'app/UPDATE_BACKUP_IMPORT_PROGRESS';
|
||||
|
||||
type InitialLoadCompleteActionType = ReadonlyDeep<{
|
||||
type: typeof INITIAL_LOAD_COMPLETE;
|
||||
|
@ -59,42 +43,18 @@ type OpenInboxActionType = ReadonlyDeep<{
|
|||
type: typeof OPEN_INBOX;
|
||||
}>;
|
||||
|
||||
type OpenInstallerActionType = ReadonlyDeep<{
|
||||
type: typeof OPEN_INSTALLER;
|
||||
}>;
|
||||
|
||||
type OpenStandaloneActionType = ReadonlyDeep<{
|
||||
type: typeof OPEN_STANDALONE;
|
||||
}>;
|
||||
|
||||
type OpenBackupImportActionType = ReadonlyDeep<{
|
||||
type: typeof OPEN_BACKUP_IMPORT;
|
||||
}>;
|
||||
|
||||
type UpdateBackupImportProgressActionType = ReadonlyDeep<{
|
||||
type: typeof UPDATE_BACKUP_IMPORT_PROGRESS;
|
||||
payload: {
|
||||
currentBytes: number;
|
||||
totalBytes: number;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type AppActionType = ReadonlyDeep<
|
||||
| InitialLoadCompleteActionType
|
||||
| OpenInboxActionType
|
||||
| OpenInstallerActionType
|
||||
| OpenStandaloneActionType
|
||||
| OpenBackupImportActionType
|
||||
| UpdateBackupImportProgressActionType
|
||||
InitialLoadCompleteActionType | OpenInboxActionType | OpenStandaloneActionType
|
||||
>;
|
||||
|
||||
export const actions = {
|
||||
initialLoadComplete,
|
||||
openInbox,
|
||||
openInstaller,
|
||||
openStandalone,
|
||||
openBackupImport,
|
||||
updateBackupImportProgress,
|
||||
};
|
||||
|
||||
export const useAppActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
||||
|
@ -123,21 +83,6 @@ function openInbox(): ThunkAction<
|
|||
};
|
||||
}
|
||||
|
||||
function openInstaller(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
OpenInstallerActionType
|
||||
> {
|
||||
return dispatch => {
|
||||
window.IPC.addSetupMenuItems();
|
||||
|
||||
dispatch({
|
||||
type: OPEN_INSTALLER,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function openStandalone(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
|
@ -156,16 +101,6 @@ function openStandalone(): ThunkAction<
|
|||
};
|
||||
}
|
||||
|
||||
function openBackupImport(): OpenBackupImportActionType {
|
||||
return { type: OPEN_BACKUP_IMPORT };
|
||||
}
|
||||
|
||||
function updateBackupImportProgress(
|
||||
payload: UpdateBackupImportProgressActionType['payload']
|
||||
): UpdateBackupImportProgressActionType {
|
||||
return { type: UPDATE_BACKUP_IMPORT_PROGRESS, payload };
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): AppStateType {
|
||||
|
@ -177,7 +112,9 @@ export function getEmptyState(): AppStateType {
|
|||
|
||||
export function reducer(
|
||||
state: Readonly<AppStateType> = getEmptyState(),
|
||||
action: Readonly<AppActionType>
|
||||
action: Readonly<
|
||||
AppActionType | StartInstallerActionType | ShowBackupImportActionType
|
||||
>
|
||||
): AppStateType {
|
||||
if (action.type === OPEN_INBOX) {
|
||||
return {
|
||||
|
@ -193,13 +130,6 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === OPEN_INSTALLER) {
|
||||
return {
|
||||
...state,
|
||||
appView: AppViewType.Installer,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === OPEN_STANDALONE) {
|
||||
return {
|
||||
...state,
|
||||
|
@ -207,22 +137,11 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === OPEN_BACKUP_IMPORT) {
|
||||
// Foreign action
|
||||
if (action.type === START_INSTALLER || action.type === SHOW_BACKUP_IMPORT) {
|
||||
return {
|
||||
...state,
|
||||
appView: AppViewType.BackupImport,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === UPDATE_BACKUP_IMPORT_PROGRESS) {
|
||||
if (state.appView !== AppViewType.BackupImport) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentBytes: action.payload.currentBytes,
|
||||
totalBytes: action.payload.totalBytes,
|
||||
appView: AppViewType.Installer,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
558
ts/state/ducks/installer.ts
Normal file
558
ts/state/ducks/installer.ts
Normal file
|
@ -0,0 +1,558 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import pTimeout, { TimeoutError } from 'p-timeout';
|
||||
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import {
|
||||
InstallScreenStep,
|
||||
InstallScreenError,
|
||||
InstallScreenQRCodeError,
|
||||
} from '../../types/InstallScreen';
|
||||
import * as Errors from '../../types/errors';
|
||||
import { type Loadable, LoadingState } from '../../util/loadable';
|
||||
import { isRecord } from '../../util/isRecord';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { SECOND } from '../../util/durations';
|
||||
import * as Registration from '../../util/registration';
|
||||
import { isBackupEnabled } from '../../util/isBackupEnabled';
|
||||
import { HTTPError } from '../../textsecure/Errors';
|
||||
import {
|
||||
Provisioner,
|
||||
type PrepareLinkDataOptionsType,
|
||||
} from '../../textsecure/Provisioner';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
const SLEEP_ERROR = new TimeoutError();
|
||||
|
||||
const QR_CODE_TIMEOUTS = [10 * SECOND, 20 * SECOND, 30 * SECOND, 60 * SECOND];
|
||||
|
||||
export type BatonType = ReadonlyDeep<{ __installer_baton: never }>;
|
||||
|
||||
const controllerByBaton = new WeakMap<BatonType, AbortController>();
|
||||
const provisionerByBaton = new WeakMap<BatonType, Provisioner>();
|
||||
|
||||
export type InstallerStateType = ReadonlyDeep<
|
||||
| {
|
||||
step: InstallScreenStep.NotStarted;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.QrCodeNotScanned;
|
||||
provisioningUrl: Loadable<string, InstallScreenQRCodeError>;
|
||||
baton: BatonType;
|
||||
attemptCount: number;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.ChoosingDeviceName;
|
||||
deviceName: string;
|
||||
backupFile?: File;
|
||||
baton: BatonType;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.Error;
|
||||
error: InstallScreenError;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.LinkInProgress;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.BackupImport;
|
||||
currentBytes?: number;
|
||||
totalBytes?: number;
|
||||
}
|
||||
>;
|
||||
|
||||
export const START_INSTALLER = 'installer/START_INSTALLER';
|
||||
const SET_PROVISIONING_URL = 'installer/SET_PROVISIONING_URL';
|
||||
const SET_QR_CODE_ERROR = 'installer/SET_QR_CODE_ERROR';
|
||||
const SET_ERROR = 'installer/SET_ERROR';
|
||||
const QR_CODE_SCANNED = 'installer/QR_CODE_SCANNED';
|
||||
const SHOW_LINK_IN_PROGRESS = 'installer/SHOW_LINK_IN_PROGRESS';
|
||||
export const SHOW_BACKUP_IMPORT = 'installer/SHOW_BACKUP_IMPORT';
|
||||
const UPDATE_BACKUP_IMPORT_PROGRESS = 'installer/UPDATE_BACKUP_IMPORT_PROGRESS';
|
||||
|
||||
export type StartInstallerActionType = ReadonlyDeep<{
|
||||
type: typeof START_INSTALLER;
|
||||
payload: BatonType;
|
||||
}>;
|
||||
|
||||
type SetProvisioningUrlActionType = ReadonlyDeep<{
|
||||
type: typeof SET_PROVISIONING_URL;
|
||||
payload: string;
|
||||
}>;
|
||||
|
||||
type SetQRCodeErrorActionType = ReadonlyDeep<{
|
||||
type: typeof SET_QR_CODE_ERROR;
|
||||
payload: InstallScreenQRCodeError;
|
||||
}>;
|
||||
|
||||
type SetErrorActionType = ReadonlyDeep<{
|
||||
type: typeof SET_ERROR;
|
||||
payload: InstallScreenError;
|
||||
}>;
|
||||
|
||||
type QRCodeScannedActionType = ReadonlyDeep<{
|
||||
type: typeof QR_CODE_SCANNED;
|
||||
payload: {
|
||||
deviceName: string;
|
||||
baton: BatonType;
|
||||
};
|
||||
}>;
|
||||
|
||||
type ShowLinkInProgressActionType = ReadonlyDeep<{
|
||||
type: typeof SHOW_LINK_IN_PROGRESS;
|
||||
}>;
|
||||
|
||||
export type ShowBackupImportActionType = ReadonlyDeep<{
|
||||
type: typeof SHOW_BACKUP_IMPORT;
|
||||
}>;
|
||||
|
||||
type UpdateBackupImportProgressActionType = ReadonlyDeep<{
|
||||
type: typeof UPDATE_BACKUP_IMPORT_PROGRESS;
|
||||
payload: {
|
||||
currentBytes: number;
|
||||
totalBytes: number;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type InstallerActionType = ReadonlyDeep<
|
||||
| StartInstallerActionType
|
||||
| SetProvisioningUrlActionType
|
||||
| SetQRCodeErrorActionType
|
||||
| SetErrorActionType
|
||||
| QRCodeScannedActionType
|
||||
| ShowLinkInProgressActionType
|
||||
| ShowBackupImportActionType
|
||||
| UpdateBackupImportProgressActionType
|
||||
>;
|
||||
|
||||
export const actions = {
|
||||
startInstaller,
|
||||
finishInstall,
|
||||
updateBackupImportProgress,
|
||||
showBackupImport,
|
||||
showLinkInProgress,
|
||||
};
|
||||
|
||||
export const useInstallerActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
function startInstaller(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
InstallerActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
// WeakMap key
|
||||
const baton = {} as BatonType;
|
||||
|
||||
window.IPC.addSetupMenuItems();
|
||||
|
||||
dispatch({
|
||||
type: START_INSTALLER,
|
||||
payload: baton,
|
||||
});
|
||||
const { installer: state } = getState();
|
||||
strictAssert(
|
||||
state.step === InstallScreenStep.QrCodeNotScanned,
|
||||
'Unexpected step after START_INSTALLER'
|
||||
);
|
||||
const { attemptCount } = state;
|
||||
|
||||
// Can't retry past attempt count
|
||||
if (attemptCount >= QR_CODE_TIMEOUTS.length - 1) {
|
||||
log.error('InstallScreen/getQRCode: too many tries');
|
||||
dispatch({
|
||||
type: SET_ERROR,
|
||||
payload: InstallScreenError.QRCodeFailed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { server } = window.textsecure;
|
||||
strictAssert(server, 'Expected a server');
|
||||
|
||||
const provisioner = new Provisioner(server);
|
||||
|
||||
const abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
signal.addEventListener('abort', () => {
|
||||
provisioner.close();
|
||||
});
|
||||
|
||||
controllerByBaton.set(baton, abortController);
|
||||
|
||||
// Wait to get QR code
|
||||
try {
|
||||
const qrCodePromise = provisioner.getURL();
|
||||
const sleepMs = QR_CODE_TIMEOUTS[attemptCount];
|
||||
log.info(`installer/getQRCode: race to ${sleepMs}ms`);
|
||||
|
||||
const url = await pTimeout(qrCodePromise, sleepMs, SLEEP_ERROR);
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.IPC.removeSetupMenuItems();
|
||||
dispatch({
|
||||
type: SET_PROVISIONING_URL,
|
||||
payload: url,
|
||||
});
|
||||
} catch (error) {
|
||||
provisioner.close();
|
||||
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.error(
|
||||
'installer: got an error while waiting for QR code',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
|
||||
// Too many attempts, there is probably some issue
|
||||
if (attemptCount >= QR_CODE_TIMEOUTS.length - 1) {
|
||||
log.error('InstallScreen/getQRCode: too many tries');
|
||||
dispatch({
|
||||
type: SET_ERROR,
|
||||
payload: InstallScreenError.QRCodeFailed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Timed out, let user retry
|
||||
if (error === SLEEP_ERROR) {
|
||||
dispatch({
|
||||
type: SET_QR_CODE_ERROR,
|
||||
payload: InstallScreenQRCodeError.Timeout,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof HTTPError && error.code === -1) {
|
||||
if (
|
||||
isRecord(error.cause) &&
|
||||
error.cause.code === 'SELF_SIGNED_CERT_IN_CHAIN'
|
||||
) {
|
||||
dispatch({
|
||||
type: SET_QR_CODE_ERROR,
|
||||
payload: InstallScreenQRCodeError.NetworkIssue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: SET_ERROR,
|
||||
payload: InstallScreenError.ConnectionFailed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_QR_CODE_ERROR,
|
||||
payload: InstallScreenQRCodeError.Unknown,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
log.warn('installer/startInstaller: aborted');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for primary device to scan QR code and get back to us
|
||||
|
||||
try {
|
||||
await provisioner.waitForEnvelope();
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
log.error(
|
||||
'installer: got an error while waiting for envelope code',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SET_ERROR,
|
||||
payload: InstallScreenError.ConnectionFailed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
provisionerByBaton.set(baton, provisioner);
|
||||
|
||||
// Switch to next UI phase
|
||||
dispatch({
|
||||
type: QR_CODE_SCANNED,
|
||||
payload: {
|
||||
deviceName:
|
||||
window.textsecure.storage.user.getDeviceName() ||
|
||||
window.getHostName() ||
|
||||
'',
|
||||
baton,
|
||||
},
|
||||
});
|
||||
|
||||
// And feed it the CI data if present
|
||||
const { SignalCI } = window;
|
||||
if (SignalCI != null) {
|
||||
dispatch(
|
||||
finishInstall({
|
||||
deviceName: SignalCI.deviceName,
|
||||
backupFile: SignalCI.backupData,
|
||||
isPlaintextBackup: SignalCI.isPlaintextBackup,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function finishInstall(
|
||||
options: PrepareLinkDataOptionsType
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
| SetQRCodeErrorActionType
|
||||
| SetErrorActionType
|
||||
| ShowLinkInProgressActionType
|
||||
| ShowBackupImportActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
strictAssert(
|
||||
state.installer.step === InstallScreenStep.ChoosingDeviceName,
|
||||
'Not choosing device name'
|
||||
);
|
||||
|
||||
const { baton } = state.installer;
|
||||
const provisioner = provisionerByBaton.get(baton);
|
||||
strictAssert(
|
||||
provisioner != null,
|
||||
'Provisioner is not waiting for device info'
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
controllerByBaton.delete(baton);
|
||||
provisionerByBaton.delete(baton);
|
||||
|
||||
const accountManager = window.getAccountManager();
|
||||
strictAssert(accountManager, 'Expected an account manager');
|
||||
|
||||
if (isBackupEnabled()) {
|
||||
dispatch({ type: SHOW_BACKUP_IMPORT });
|
||||
} else {
|
||||
dispatch({ type: SHOW_LINK_IN_PROGRESS });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = provisioner.prepareLinkData(options);
|
||||
await accountManager.registerSecondDevice(data);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
switch (error.code) {
|
||||
case 409:
|
||||
dispatch({
|
||||
type: SET_ERROR,
|
||||
payload: InstallScreenError.TooOld,
|
||||
});
|
||||
return;
|
||||
case 411:
|
||||
dispatch({
|
||||
type: SET_ERROR,
|
||||
payload: InstallScreenError.TooManyDevices,
|
||||
});
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_QR_CODE_ERROR,
|
||||
payload: InstallScreenQRCodeError.Unknown,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete all data from the database unless we're in the middle of a re-link.
|
||||
// Without this, the app restarts at certain times and can cause weird things to
|
||||
// happen, like data from a previous light import showing up after a new install.
|
||||
const shouldRetainData = Registration.everDone();
|
||||
if (!shouldRetainData) {
|
||||
try {
|
||||
await window.textsecure.storage.protocol.removeAllData();
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'installer/finishInstall: error clearing database',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function showBackupImport(): ShowBackupImportActionType {
|
||||
return { type: SHOW_BACKUP_IMPORT };
|
||||
}
|
||||
|
||||
function showLinkInProgress(): ShowLinkInProgressActionType {
|
||||
return { type: SHOW_LINK_IN_PROGRESS };
|
||||
}
|
||||
|
||||
function updateBackupImportProgress(
|
||||
payload: UpdateBackupImportProgressActionType['payload']
|
||||
): UpdateBackupImportProgressActionType {
|
||||
return { type: UPDATE_BACKUP_IMPORT_PROGRESS, payload };
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): InstallerStateType {
|
||||
return {
|
||||
step: InstallScreenStep.NotStarted,
|
||||
};
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: Readonly<InstallerStateType> = getEmptyState(),
|
||||
action: Readonly<InstallerActionType>
|
||||
): InstallerStateType {
|
||||
if (action.type === START_INSTALLER) {
|
||||
// Abort previous install
|
||||
if (state.step === InstallScreenStep.QrCodeNotScanned) {
|
||||
const controller = controllerByBaton.get(state.baton);
|
||||
controller?.abort();
|
||||
}
|
||||
|
||||
return {
|
||||
step: InstallScreenStep.QrCodeNotScanned,
|
||||
provisioningUrl: {
|
||||
loadingState: LoadingState.Loading,
|
||||
},
|
||||
baton: action.payload,
|
||||
attemptCount:
|
||||
state.step === InstallScreenStep.QrCodeNotScanned
|
||||
? state.attemptCount + 1
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_PROVISIONING_URL) {
|
||||
if (
|
||||
state.step !== InstallScreenStep.QrCodeNotScanned ||
|
||||
state.provisioningUrl.loadingState !== LoadingState.Loading
|
||||
) {
|
||||
log.warn('ducks/installer: not setting provisioning url', state.step);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
provisioningUrl: {
|
||||
loadingState: LoadingState.Loaded,
|
||||
value: action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_QR_CODE_ERROR) {
|
||||
if (
|
||||
state.step !== InstallScreenStep.QrCodeNotScanned ||
|
||||
state.provisioningUrl.loadingState !== LoadingState.Loading
|
||||
) {
|
||||
log.warn('ducks/installer: not setting qr code error', state.step);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
provisioningUrl: {
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_ERROR) {
|
||||
return {
|
||||
step: InstallScreenStep.Error,
|
||||
error: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === QR_CODE_SCANNED) {
|
||||
if (
|
||||
state.step !== InstallScreenStep.QrCodeNotScanned ||
|
||||
state.provisioningUrl.loadingState !== LoadingState.Loaded
|
||||
) {
|
||||
log.warn('ducks/installer: not setting qr code scanned', state.step);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
step: InstallScreenStep.ChoosingDeviceName,
|
||||
deviceName: action.payload.deviceName,
|
||||
baton: action.payload.baton,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SHOW_LINK_IN_PROGRESS) {
|
||||
if (
|
||||
// Backups not supported
|
||||
state.step !== InstallScreenStep.ChoosingDeviceName &&
|
||||
// No backup available
|
||||
state.step !== InstallScreenStep.BackupImport
|
||||
) {
|
||||
log.warn('ducks/installer: not setting link in progress', state.step);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
step: InstallScreenStep.LinkInProgress,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SHOW_BACKUP_IMPORT) {
|
||||
if (
|
||||
// Downloading backup after linking
|
||||
state.step !== InstallScreenStep.ChoosingDeviceName &&
|
||||
// Restarting backup download on startup
|
||||
state.step !== InstallScreenStep.NotStarted
|
||||
) {
|
||||
log.warn('ducks/installer: not setting backup import', state.step);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
step: InstallScreenStep.BackupImport,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === UPDATE_BACKUP_IMPORT_PROGRESS) {
|
||||
if (state.step !== InstallScreenStep.BackupImport) {
|
||||
log.warn(
|
||||
'ducks/installer: not updating backup import progress',
|
||||
state.step
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
currentBytes: action.payload.currentBytes,
|
||||
totalBytes: action.payload.totalBytes,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -15,6 +15,7 @@ import { getEmptyState as emojiEmptyState } from './ducks/emojis';
|
|||
import { getEmptyState as expirationEmptyState } from './ducks/expiration';
|
||||
import { getEmptyState as globalModalsEmptyState } from './ducks/globalModals';
|
||||
import { getEmptyState as inboxEmptyState } from './ducks/inbox';
|
||||
import { getEmptyState as installerEmptyState } from './ducks/installer';
|
||||
import { getEmptyState as itemsEmptyState } from './ducks/items';
|
||||
import { getEmptyState as lightboxEmptyState } from './ducks/lightbox';
|
||||
import { getEmptyState as linkPreviewsEmptyState } from './ducks/linkPreviews';
|
||||
|
@ -133,6 +134,7 @@ function getEmptyState(): StateType {
|
|||
expiration: expirationEmptyState(),
|
||||
globalModals: globalModalsEmptyState(),
|
||||
inbox: inboxEmptyState(),
|
||||
installer: installerEmptyState(),
|
||||
items: itemsEmptyState(),
|
||||
lightbox: lightboxEmptyState(),
|
||||
linkPreviews: linkPreviewsEmptyState(),
|
||||
|
|
|
@ -42,6 +42,7 @@ export function initializeRedux(data: ReduxInitData): void {
|
|||
window.reduxActions = {
|
||||
accounts: bindActionCreators(actionCreators.accounts, store.dispatch),
|
||||
app: bindActionCreators(actionCreators.app, store.dispatch),
|
||||
installer: bindActionCreators(actionCreators.installer, store.dispatch),
|
||||
audioPlayer: bindActionCreators(actionCreators.audioPlayer, store.dispatch),
|
||||
audioRecorder: bindActionCreators(
|
||||
actionCreators.audioRecorder,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { reducer as emojis } from './ducks/emojis';
|
|||
import { reducer as expiration } from './ducks/expiration';
|
||||
import { reducer as globalModals } from './ducks/globalModals';
|
||||
import { reducer as inbox } from './ducks/inbox';
|
||||
import { reducer as installer } from './ducks/installer';
|
||||
import { reducer as items } from './ducks/items';
|
||||
import { reducer as lightbox } from './ducks/lightbox';
|
||||
import { reducer as linkPreviews } from './ducks/linkPreviews';
|
||||
|
@ -49,6 +50,7 @@ export const reducer = combineReducers({
|
|||
expiration,
|
||||
globalModals,
|
||||
inbox,
|
||||
installer,
|
||||
items,
|
||||
lightbox,
|
||||
linkPreviews,
|
||||
|
|
8
ts/state/selectors/installer.ts
Normal file
8
ts/state/selectors/installer.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { StateType } from '../reducer';
|
||||
import type { InstallerStateType } from '../ducks/installer';
|
||||
|
||||
export const getInstallerState = (state: StateType): InstallerStateType =>
|
||||
state.installer;
|
|
@ -18,7 +18,6 @@ import {
|
|||
getIsMainWindowMaximized,
|
||||
getIsMainWindowFullScreen,
|
||||
getTheme,
|
||||
getIntl,
|
||||
} from '../selectors/user';
|
||||
import { hasSelectedStoryData as getHasSelectedStoryData } from '../selectors/stories';
|
||||
import { useAppActions } from '../ducks/app';
|
||||
|
@ -111,7 +110,6 @@ async function uploadProfile({
|
|||
}
|
||||
|
||||
export const SmartApp = memo(function SmartApp() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const state = useSelector(getApp);
|
||||
const isMaximized = useSelector(getIsMainWindowMaximized);
|
||||
const isFullScreen = useSelector(getIsMainWindowFullScreen);
|
||||
|
@ -126,7 +124,6 @@ export const SmartApp = memo(function SmartApp() {
|
|||
|
||||
return (
|
||||
<App
|
||||
i18n={i18n}
|
||||
state={state}
|
||||
isMaximized={isMaximized}
|
||||
isFullScreen={isFullScreen}
|
||||
|
|
|
@ -2,364 +2,73 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useCallback, useState, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import pTimeout, { TimeoutError } from 'p-timeout';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getUpdatesState } from '../selectors/updates';
|
||||
import { getInstallerState } from '../selectors/installer';
|
||||
import { useInstallerActions } from '../ducks/installer';
|
||||
import { useUpdatesActions } from '../ducks/updates';
|
||||
import { hasExpired as hasExpiredSelector } from '../selectors/expiration';
|
||||
import * as log from '../../logging/log';
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
import { assertDev, strictAssert } from '../../util/assert';
|
||||
import { explodePromise } from '../../util/explodePromise';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import * as Registration from '../../util/registration';
|
||||
import {
|
||||
InstallScreen,
|
||||
InstallScreenStep,
|
||||
} from '../../components/InstallScreen';
|
||||
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
|
||||
import { LoadError } from '../../components/installScreen/InstallScreenQrCodeNotScannedStep';
|
||||
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
|
||||
import { InstallScreen } from '../../components/InstallScreen';
|
||||
import { WidthBreakpoint } from '../../components/_util';
|
||||
import { HTTPError } from '../../textsecure/Errors';
|
||||
import { isRecord } from '../../util/isRecord';
|
||||
import { Provisioner } from '../../textsecure/Provisioner';
|
||||
import * as Errors from '../../types/errors';
|
||||
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
|
||||
import { InstallScreenStep } from '../../types/InstallScreen';
|
||||
import OS from '../../util/os/osMain';
|
||||
import { SECOND } from '../../util/durations';
|
||||
import { BackOff } from '../../util/BackOff';
|
||||
import { drop } from '../../util/drop';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import { fileToBytes } from '../../util/fileToBytes';
|
||||
import * as log from '../../logging/log';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
|
||||
type PropsType = ComponentProps<typeof InstallScreen>;
|
||||
|
||||
type StateType =
|
||||
| {
|
||||
step: InstallScreenStep.Error;
|
||||
error: InstallError;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.QrCodeNotScanned;
|
||||
provisioningUrl: Loadable<string, LoadError>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.ChoosingDeviceName;
|
||||
deviceName: string;
|
||||
backupFile?: File;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.LinkInProgress;
|
||||
};
|
||||
|
||||
const INITIAL_STATE: StateType = {
|
||||
step: InstallScreenStep.QrCodeNotScanned,
|
||||
provisioningUrl: { loadingState: LoadingState.Loading },
|
||||
};
|
||||
|
||||
const qrCodeBackOff = new BackOff([
|
||||
10 * SECOND,
|
||||
20 * SECOND,
|
||||
30 * SECOND,
|
||||
60 * SECOND,
|
||||
]);
|
||||
|
||||
function classifyError(
|
||||
err: unknown
|
||||
): { installError: InstallError } | { loadError: LoadError } {
|
||||
if (err instanceof HTTPError) {
|
||||
switch (err.code) {
|
||||
case -1:
|
||||
if (
|
||||
isRecord(err.cause) &&
|
||||
err.cause.code === 'SELF_SIGNED_CERT_IN_CHAIN'
|
||||
) {
|
||||
return { loadError: LoadError.NetworkIssue };
|
||||
}
|
||||
return { installError: InstallError.ConnectionFailed };
|
||||
case 409:
|
||||
return { installError: InstallError.TooOld };
|
||||
case 411:
|
||||
return { installError: InstallError.TooManyDevices };
|
||||
default:
|
||||
return { loadError: LoadError.Unknown };
|
||||
}
|
||||
}
|
||||
// AccountManager.registerSecondDevice uses this specific "websocket closed"
|
||||
// error message.
|
||||
if (isRecord(err) && err.message === 'websocket closed') {
|
||||
return { installError: InstallError.ConnectionFailed };
|
||||
}
|
||||
return { loadError: LoadError.Unknown };
|
||||
}
|
||||
|
||||
export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||
const i18n = useSelector(getIntl);
|
||||
const installerState = useSelector(getInstallerState);
|
||||
const updates = useSelector(getUpdatesState);
|
||||
const { startInstaller, finishInstall } = useInstallerActions();
|
||||
const { startUpdate } = useUpdatesActions();
|
||||
const hasExpired = useSelector(hasExpiredSelector);
|
||||
|
||||
const chooseDeviceNamePromiseWrapperRef = useRef(explodePromise<string>());
|
||||
const chooseBackupFilePromiseWrapperRef =
|
||||
useRef(explodePromise<File | undefined>());
|
||||
const [deviceName, setDeviceName] = useState<string>('');
|
||||
const [backupFile, setBackupFile] = useState<File | undefined>();
|
||||
|
||||
const [state, setState] = useState<StateType>(INITIAL_STATE);
|
||||
const [retryCounter, setRetryCounter] = useState(0);
|
||||
|
||||
const setProvisioningUrl = useCallback(
|
||||
(value: string) => {
|
||||
setState(currentState => {
|
||||
if (currentState.step !== InstallScreenStep.QrCodeNotScanned) {
|
||||
return currentState;
|
||||
}
|
||||
return {
|
||||
...currentState,
|
||||
provisioningUrl: {
|
||||
loadingState: LoadingState.Loaded,
|
||||
value,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
const onQrCodeScanned = useCallback(() => {
|
||||
setState(currentState => {
|
||||
if (currentState.step !== InstallScreenStep.QrCodeNotScanned) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
return {
|
||||
step: InstallScreenStep.ChoosingDeviceName,
|
||||
deviceName: normalizeDeviceName(
|
||||
window.textsecure.storage.user.getDeviceName() ||
|
||||
window.getHostName() ||
|
||||
''
|
||||
).slice(0, MAX_DEVICE_NAME_LENGTH),
|
||||
};
|
||||
});
|
||||
}, [setState]);
|
||||
|
||||
const setDeviceName = useCallback(
|
||||
(deviceName: string) => {
|
||||
setState(currentState => {
|
||||
if (currentState.step !== InstallScreenStep.ChoosingDeviceName) {
|
||||
return currentState;
|
||||
}
|
||||
return {
|
||||
...currentState,
|
||||
deviceName,
|
||||
};
|
||||
});
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
const setBackupFile = useCallback(
|
||||
(backupFile: File) => {
|
||||
setState(currentState => {
|
||||
if (currentState.step !== InstallScreenStep.ChoosingDeviceName) {
|
||||
return currentState;
|
||||
}
|
||||
return {
|
||||
...currentState,
|
||||
backupFile,
|
||||
};
|
||||
});
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
const onSubmitDeviceName = useCallback(() => {
|
||||
if (state.step !== InstallScreenStep.ChoosingDeviceName) {
|
||||
return;
|
||||
const onSubmitDeviceName = useCallback(async () => {
|
||||
if (backupFile != null) {
|
||||
// This is only for testing so don't bother catching errors
|
||||
finishInstall({ deviceName, backupFile: await fileToBytes(backupFile) });
|
||||
} else {
|
||||
finishInstall({ deviceName, backupFile: undefined });
|
||||
}
|
||||
}, [backupFile, deviceName, finishInstall]);
|
||||
|
||||
let deviceName: string = normalizeDeviceName(state.deviceName);
|
||||
if (!deviceName.length) {
|
||||
// This should be impossible, but we have it here just in case.
|
||||
assertDev(
|
||||
false,
|
||||
'Unexpected empty device name. Falling back to placeholder value'
|
||||
);
|
||||
deviceName = i18n('icu:Install__choose-device-name__placeholder');
|
||||
}
|
||||
chooseDeviceNamePromiseWrapperRef.current.resolve(deviceName);
|
||||
chooseBackupFilePromiseWrapperRef.current.resolve(state.backupFile);
|
||||
|
||||
setState({ step: InstallScreenStep.LinkInProgress });
|
||||
}, [state, i18n]);
|
||||
const suggestedDeviceName =
|
||||
installerState.step === InstallScreenStep.ChoosingDeviceName
|
||||
? installerState.deviceName
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
let hasCleanedUp = false;
|
||||
|
||||
const { server } = window.textsecure;
|
||||
strictAssert(server, 'Expected a server');
|
||||
|
||||
let provisioner = new Provisioner(server);
|
||||
const accountManager = window.getAccountManager();
|
||||
strictAssert(accountManager, 'Expected an account manager');
|
||||
|
||||
async function getQRCode(): Promise<void> {
|
||||
const sleepError = new TimeoutError();
|
||||
try {
|
||||
const qrCodePromise = provisioner.getURL();
|
||||
const sleepMs = qrCodeBackOff.getAndIncrement();
|
||||
log.info(`InstallScreen/getQRCode: race to ${sleepMs}ms`);
|
||||
|
||||
const url = await pTimeout(qrCodePromise, sleepMs, sleepError);
|
||||
if (hasCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.IPC.removeSetupMenuItems();
|
||||
setProvisioningUrl(url);
|
||||
|
||||
await provisioner.waitForEnvelope();
|
||||
onQrCodeScanned();
|
||||
|
||||
let deviceName: string;
|
||||
let backupFileData: Uint8Array | undefined;
|
||||
let isPlaintextBackup = false;
|
||||
if (window.SignalCI) {
|
||||
({
|
||||
deviceName,
|
||||
backupData: backupFileData,
|
||||
isPlaintextBackup = false,
|
||||
} = window.SignalCI);
|
||||
} else {
|
||||
deviceName = await chooseDeviceNamePromiseWrapperRef.current.promise;
|
||||
const backupFile =
|
||||
await chooseBackupFilePromiseWrapperRef.current.promise;
|
||||
|
||||
backupFileData = backupFile
|
||||
? await fileToBytes(backupFile)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (hasCleanedUp) {
|
||||
throw new Error('Cannot confirm number; the component was unmounted');
|
||||
}
|
||||
|
||||
// Delete all data from the database unless we're in the middle of a
|
||||
// re-link. Without this, the app restarts at certain times and can
|
||||
// cause weird things to happen, like data from a previous light
|
||||
// import showing up after a new install.
|
||||
const shouldRetainData = Registration.everDone();
|
||||
if (!shouldRetainData) {
|
||||
try {
|
||||
await window.textsecure.storage.protocol.removeAllData();
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'confirmNumber: error clearing database',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCleanedUp) {
|
||||
throw new Error('Cannot confirm number; the component was unmounted');
|
||||
}
|
||||
|
||||
const data = provisioner.prepareLinkData({
|
||||
deviceName,
|
||||
backupFile: backupFileData,
|
||||
isPlaintextBackup,
|
||||
});
|
||||
await accountManager.registerSecondDevice(data);
|
||||
} catch (error) {
|
||||
provisioner.close();
|
||||
strictAssert(server, 'Expected a server');
|
||||
provisioner = new Provisioner(server);
|
||||
|
||||
log.error(
|
||||
'account.registerSecondDevice: got an error',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
if (hasCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (qrCodeBackOff.isFull()) {
|
||||
log.error('InstallScreen/getQRCode: too many tries');
|
||||
setState({
|
||||
step: InstallScreenStep.Error,
|
||||
error: InstallError.QRCodeFailed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (error === sleepError) {
|
||||
setState({
|
||||
step: InstallScreenStep.QrCodeNotScanned,
|
||||
provisioningUrl: {
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: LoadError.Timeout,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const classifiedError = classifyError(error);
|
||||
if ('installError' in classifiedError) {
|
||||
setState({
|
||||
step: InstallScreenStep.Error,
|
||||
error: classifiedError.installError,
|
||||
});
|
||||
} else {
|
||||
setState({
|
||||
step: InstallScreenStep.QrCodeNotScanned,
|
||||
provisioningUrl: {
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: classifiedError.loadError,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(getQRCode());
|
||||
|
||||
return () => {
|
||||
hasCleanedUp = true;
|
||||
};
|
||||
}, [setProvisioningUrl, retryCounter, onQrCodeScanned]);
|
||||
setDeviceName(suggestedDeviceName ?? '');
|
||||
}, [suggestedDeviceName]);
|
||||
|
||||
let props: PropsType;
|
||||
|
||||
switch (state.step) {
|
||||
case InstallScreenStep.Error:
|
||||
props = {
|
||||
step: InstallScreenStep.Error,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
error: state.error,
|
||||
quit: () => window.IPC.shutdown(),
|
||||
tryAgain: () => {
|
||||
setRetryCounter(count => count + 1);
|
||||
setState(INITIAL_STATE);
|
||||
},
|
||||
},
|
||||
};
|
||||
break;
|
||||
switch (installerState.step) {
|
||||
case InstallScreenStep.NotStarted:
|
||||
log.error('InstallScreen: Installer not started');
|
||||
return null;
|
||||
|
||||
case InstallScreenStep.QrCodeNotScanned:
|
||||
props = {
|
||||
step: InstallScreenStep.QrCodeNotScanned,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
provisioningUrl: state.provisioningUrl,
|
||||
provisioningUrl: installerState.provisioningUrl,
|
||||
hasExpired,
|
||||
updates,
|
||||
currentVersion: window.getVersion(),
|
||||
startUpdate,
|
||||
retryGetQrCode: () => {
|
||||
setRetryCounter(count => count + 1);
|
||||
setState(INITIAL_STATE);
|
||||
},
|
||||
retryGetQrCode: startInstaller,
|
||||
OS: OS.getName(),
|
||||
},
|
||||
};
|
||||
|
@ -369,7 +78,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
|||
step: InstallScreenStep.ChoosingDeviceName,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
deviceName: state.deviceName,
|
||||
deviceName,
|
||||
setDeviceName,
|
||||
setBackupFile,
|
||||
onSubmit: onSubmitDeviceName,
|
||||
|
@ -382,8 +91,29 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
|||
screenSpecificProps: { i18n },
|
||||
};
|
||||
break;
|
||||
case InstallScreenStep.BackupImport:
|
||||
props = {
|
||||
step: InstallScreenStep.BackupImport,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
currentBytes: installerState.currentBytes,
|
||||
totalBytes: installerState.totalBytes,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case InstallScreenStep.Error:
|
||||
props = {
|
||||
step: InstallScreenStep.Error,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
error: installerState.error,
|
||||
quit: () => window.IPC.shutdown(),
|
||||
tryAgain: startInstaller,
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(state);
|
||||
throw missingCaseError(installerState);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -15,6 +15,7 @@ import type { actions as emojis } from './ducks/emojis';
|
|||
import type { actions as expiration } from './ducks/expiration';
|
||||
import type { actions as globalModals } from './ducks/globalModals';
|
||||
import type { actions as inbox } from './ducks/inbox';
|
||||
import type { actions as installer } from './ducks/installer';
|
||||
import type { actions as items } from './ducks/items';
|
||||
import type { actions as lightbox } from './ducks/lightbox';
|
||||
import type { actions as linkPreviews } from './ducks/linkPreviews';
|
||||
|
@ -45,6 +46,7 @@ export type ReduxActions = {
|
|||
expiration: typeof expiration;
|
||||
globalModals: typeof globalModals;
|
||||
inbox: typeof inbox;
|
||||
installer: typeof installer;
|
||||
items: typeof items;
|
||||
lightbox: typeof lightbox;
|
||||
linkPreviews: typeof linkPreviews;
|
||||
|
|
|
@ -8,12 +8,14 @@ import {
|
|||
import { linkDeviceRoute } from '../util/signalRoutes';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { normalizeAci } from '../util/normalizeAci';
|
||||
import { normalizeDeviceName } from '../util/normalizeDeviceName';
|
||||
import { MAX_DEVICE_NAME_LENGTH } from '../types/InstallScreen';
|
||||
import * as Errors from '../types/errors';
|
||||
import {
|
||||
isUntaggedPniString,
|
||||
normalizePni,
|
||||
toTaggedPni,
|
||||
} from '../types/ServiceId';
|
||||
import * as Errors from '../types/errors';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import * as Bytes from '../Bytes';
|
||||
import * as log from '../logging/log';
|
||||
|
@ -204,7 +206,10 @@ export class Provisioner {
|
|||
aciKeyPair,
|
||||
pniKeyPair,
|
||||
profileKey,
|
||||
deviceName,
|
||||
deviceName: normalizeDeviceName(deviceName).slice(
|
||||
0,
|
||||
MAX_DEVICE_NAME_LENGTH
|
||||
),
|
||||
backupFile,
|
||||
isPlaintextBackup,
|
||||
userAgent,
|
||||
|
|
|
@ -3669,6 +3669,7 @@ export function initialize({
|
|||
currentBytes += chunk.byteLength;
|
||||
onProgress(currentBytes, totalBytes);
|
||||
});
|
||||
onProgress(0, totalBytes);
|
||||
}
|
||||
|
||||
return combinedStream;
|
||||
|
|
31
ts/types/InstallScreen.ts
Normal file
31
ts/types/InstallScreen.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum InstallScreenStep {
|
||||
NotStarted = 'NotStarted',
|
||||
QrCodeNotScanned = 'QrCodeNotScanned',
|
||||
ChoosingDeviceName = 'ChoosingDeviceName',
|
||||
Error = 'Error',
|
||||
|
||||
// Either of these two is the final state
|
||||
LinkInProgress = 'LinkInProgress',
|
||||
BackupImport = 'BackupImport',
|
||||
}
|
||||
|
||||
export enum InstallScreenError {
|
||||
TooManyDevices = 'TooManyDevices',
|
||||
TooOld = 'TooOld',
|
||||
ConnectionFailed = 'ConnectionFailed',
|
||||
QRCodeFailed = 'QRCodeFailed',
|
||||
}
|
||||
|
||||
export enum InstallScreenQRCodeError {
|
||||
Timeout = 'Timeout',
|
||||
Unknown = 'Unknown',
|
||||
NetworkIssue = 'NetworkIssue',
|
||||
}
|
||||
|
||||
// This is the string's `.length`, which is the number of UTF-16 code points. Instead, we
|
||||
// want this to be either 50 graphemes or 256 encrypted bytes, whichever is smaller. See
|
||||
// DESKTOP-2844.
|
||||
export const MAX_DEVICE_NAME_LENGTH = 50;
|
|
@ -3088,21 +3088,6 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-08-20T22:14:52.008Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/state/smart/InstallScreen.tsx",
|
||||
"line": " const chooseDeviceNamePromiseWrapperRef = useRef(explodePromise<string>());",
|
||||
"reasonCategory": "testCode",
|
||||
"updated": "2023-11-16T23:39:21.322Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/state/smart/InstallScreen.tsx",
|
||||
"line": " useRef(explodePromise<File | undefined>());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-12-06T23:07:28.947Z",
|
||||
"reasonDetail": "Doesn't touch the DOM."
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/windows/loading/start.ts",
|
||||
|
|
Loading…
Reference in a new issue