signal-desktop/ts/state/ducks/installer.ts

626 lines
16 KiB
TypeScript
Raw Normal View History

2024-09-03 19:56:13 -07:00
// 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 type { StateType as RootStateType } from '../reducer';
import {
type InstallScreenBackupError,
InstallScreenBackupStep,
2024-09-03 19:56:13 -07:00
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 * as Registration from '../../util/registration';
import { isBackupEnabled } from '../../util/isBackupEnabled';
2025-01-14 12:14:32 -08:00
import { missingCaseError } from '../../util/missingCaseError';
import { HTTPError } from '../../textsecure/Errors';
2024-09-03 19:56:13 -07:00
import {
Provisioner,
2025-01-14 12:14:32 -08:00
EventKind as ProvisionEventKind,
type EnvelopeType as ProvisionEnvelopeType,
2024-09-03 19:56:13 -07:00
} from '../../textsecure/Provisioner';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
import * as log from '../../logging/log';
import { backupsService } from '../../services/backups';
import OS from '../../util/os/osMain';
2024-09-03 19:56:13 -07:00
export type BatonType = ReadonlyDeep<{ __installer_baton: never }>;
2025-01-14 12:14:32 -08:00
const cancelByBaton = new WeakMap<BatonType, () => void>();
let provisioner: Provisioner | undefined;
2024-09-03 19:56:13 -07:00
export type InstallerStateType = ReadonlyDeep<
| {
step: InstallScreenStep.NotStarted;
}
| {
step: InstallScreenStep.QrCodeNotScanned;
provisioningUrl: Loadable<string, InstallScreenQRCodeError>;
baton: BatonType;
}
| {
step: InstallScreenStep.ChoosingDeviceName;
deviceName: string;
backupFile?: File;
2025-01-14 12:14:32 -08:00
envelope: ProvisionEnvelopeType;
2024-09-03 19:56:13 -07:00
baton: BatonType;
}
| {
step: InstallScreenStep.Error;
error: InstallScreenError;
}
| {
step: InstallScreenStep.LinkInProgress;
}
2024-12-19 15:46:50 -05:00
| ({
2024-09-03 19:56:13 -07:00
step: InstallScreenStep.BackupImport;
backupStep: InstallScreenBackupStep;
error?: InstallScreenBackupError;
2024-12-19 15:46:50 -05:00
} & (
| {
backupStep:
| InstallScreenBackupStep.Download
| InstallScreenBackupStep.Process;
currentBytes: number;
totalBytes: number;
}
| {
backupStep: InstallScreenBackupStep.WaitForBackup;
}
))
2024-09-03 19:56:13 -07:00
>;
export type RetryBackupImportValue = ReadonlyDeep<'retry' | 'cancel'>;
2024-09-03 19:56:13 -07:00
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 RETRY_BACKUP_IMPORT = 'installer/RETRY_BACKUP_IMPORT';
2024-09-03 19:56:13 -07:00
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;
2025-01-14 12:14:32 -08:00
envelope: ProvisionEnvelopeType;
2024-09-03 19:56:13 -07:00
};
}>;
type RetryBackupImportActionType = ReadonlyDeep<{
type: typeof RETRY_BACKUP_IMPORT;
}>;
2024-09-03 19:56:13 -07:00
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:
| {
backupStep: InstallScreenBackupStep;
currentBytes: number;
totalBytes: number;
}
| {
error: InstallScreenBackupError;
};
2024-09-03 19:56:13 -07:00
}>;
export type InstallerActionType = ReadonlyDeep<
| StartInstallerActionType
| SetProvisioningUrlActionType
| SetQRCodeErrorActionType
| SetErrorActionType
| QRCodeScannedActionType
| RetryBackupImportActionType
2024-09-03 19:56:13 -07:00
| ShowLinkInProgressActionType
| ShowBackupImportActionType
| UpdateBackupImportProgressActionType
>;
export const actions = {
startInstaller,
finishInstall,
updateBackupImportProgress,
retryBackupImport,
2024-09-03 19:56:13 -07:00
showBackupImport,
handleMissingBackup,
2024-09-03 19:56:13 -07:00
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 { server } = window.textsecure;
strictAssert(server, 'Expected a server');
2025-01-14 12:14:32 -08:00
if (!provisioner) {
provisioner = new Provisioner({
server,
appVersion: window.getVersion(),
2024-09-03 19:56:13 -07:00
});
2025-01-14 12:14:32 -08:00
}
2024-09-03 19:56:13 -07:00
2025-01-14 12:14:32 -08:00
const cancel = provisioner.subscribe(event => {
if (event.kind === ProvisionEventKind.MaxRotationsError) {
log.warn('InstallScreen/getQRCode: max rotations reached');
2024-09-03 19:56:13 -07:00
dispatch({
type: SET_QR_CODE_ERROR,
2025-01-14 12:14:32 -08:00
payload: InstallScreenQRCodeError.MaxRotations,
2024-09-03 19:56:13 -07:00
});
2025-01-14 12:14:32 -08:00
} else if (event.kind === ProvisionEventKind.TimeoutError) {
if (event.canRetry) {
log.warn('InstallScreen/getQRCode: timed out');
dispatch({
type: SET_QR_CODE_ERROR,
payload: InstallScreenQRCodeError.Timeout,
});
} else {
log.error('InstallScreen/getQRCode: too many tries');
dispatch({
type: SET_ERROR,
payload: InstallScreenError.QRCodeFailed,
});
}
} else if (event.kind === ProvisionEventKind.ConnectError) {
const { error } = event;
log.error(
'installer: got an error while waiting for QR code',
Errors.toLogFormat(error)
);
2024-09-03 19:56:13 -07:00
if (
2025-01-14 12:14:32 -08:00
error instanceof HTTPError &&
error.code === -1 &&
2024-09-03 19:56:13 -07:00
isRecord(error.cause) &&
error.cause.code === 'SELF_SIGNED_CERT_IN_CHAIN'
) {
dispatch({
type: SET_QR_CODE_ERROR,
payload: InstallScreenQRCodeError.NetworkIssue,
});
return;
}
2025-01-14 12:14:32 -08:00
2024-09-03 19:56:13 -07:00
dispatch({
type: SET_ERROR,
payload: InstallScreenError.ConnectionFailed,
});
2025-01-14 12:14:32 -08:00
} else if (event.kind === ProvisionEventKind.EnvelopeError) {
log.error(
'installer: got an error while waiting for envelope',
Errors.toLogFormat(event.error)
);
2024-09-03 19:56:13 -07:00
dispatch({
2025-01-14 12:14:32 -08:00
type: SET_QR_CODE_ERROR,
payload: InstallScreenQRCodeError.Unknown,
});
2025-01-14 12:14:32 -08:00
} else if (event.kind === ProvisionEventKind.URL) {
window.SignalCI?.setProvisioningURL(event.url);
dispatch({
type: SET_PROVISIONING_URL,
payload: event.url,
});
} else if (event.kind === ProvisionEventKind.Envelope) {
const { envelope } = event;
if (event.isLinkAndSync) {
const deviceName = OS.getName() || 'Signal Desktop';
dispatch(
finishInstall({
envelope,
deviceName,
isLinkAndSync: true,
})
);
} else {
const deviceName =
window.textsecure.storage.user.getDeviceName() ||
window.getHostName() ||
2025-01-14 12:14:32 -08:00
'';
2024-09-03 19:56:13 -07:00
2025-01-14 12:14:32 -08:00
// Show screen to choose device name
dispatch({
type: QR_CODE_SCANNED,
payload: {
deviceName,
envelope,
baton,
},
});
// And feed it the CI data if present
const { SignalCI } = window;
if (SignalCI != null) {
dispatch(
finishInstall({
envelope,
deviceName: SignalCI.deviceName,
isLinkAndSync: false,
})
);
}
}
} else {
throw missingCaseError(event);
}
2025-01-14 12:14:32 -08:00
});
cancelByBaton.set(baton, cancel);
2024-09-03 19:56:13 -07:00
};
}
2025-01-14 12:14:32 -08:00
type FinishInstallOptionsType = ReadonlyDeep<{
isLinkAndSync: boolean;
deviceName: string;
envelope?: ProvisionEnvelopeType;
backupFile?: Uint8Array;
}>;
function finishInstall({
isLinkAndSync,
envelope: providedEnvelope,
deviceName,
backupFile,
}: FinishInstallOptionsType): ThunkAction<
2024-09-03 19:56:13 -07:00
void,
RootStateType,
unknown,
| SetQRCodeErrorActionType
| SetErrorActionType
| ShowLinkInProgressActionType
| ShowBackupImportActionType
> {
return async (dispatch, getState) => {
const state = getState();
strictAssert(
provisioner != null,
'Provisioner is not waiting for device info'
);
2025-01-14 12:14:32 -08:00
let envelope: ProvisionEnvelopeType;
if (state.installer.step === InstallScreenStep.QrCodeNotScanned) {
2025-01-14 12:14:32 -08:00
strictAssert(isLinkAndSync, 'Can only skip device naming if link & sync');
strictAssert(
2025-01-14 12:14:32 -08:00
providedEnvelope != null,
'finishInstall: missing required envelope'
);
2025-01-14 12:14:32 -08:00
envelope = providedEnvelope;
} else if (state.installer.step === InstallScreenStep.ChoosingDeviceName) {
({ envelope } = state.installer);
} else {
throw new Error('Wrong step');
}
2024-09-03 19:56:13 -07:00
// Cleanup
2025-01-14 12:14:32 -08:00
const { baton } = state.installer;
2025-01-21 10:36:46 -08:00
cancelByBaton.get(baton)?.();
2025-01-14 12:14:32 -08:00
cancelByBaton.delete(baton);
2024-09-03 19:56:13 -07:00
const accountManager = window.getAccountManager();
strictAssert(accountManager, 'Expected an account manager');
2025-01-14 12:14:32 -08:00
if (isBackupEnabled() || isLinkAndSync) {
2024-09-03 19:56:13 -07:00
dispatch({ type: SHOW_BACKUP_IMPORT });
} else {
dispatch({ type: SHOW_LINK_IN_PROGRESS });
}
try {
2025-01-14 12:14:32 -08:00
await accountManager.registerSecondDevice(
Provisioner.prepareLinkData({
envelope,
deviceName,
backupFile,
})
);
window.IPC.removeSetupMenuItems();
2024-09-03 19:56:13 -07:00
} 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 handleMissingBackup(): ShowLinkInProgressActionType {
// If backup is missing, go to normal link-in-progress view
return { type: SHOW_LINK_IN_PROGRESS };
}
2024-09-03 19:56:13 -07:00
function updateBackupImportProgress(
payload: UpdateBackupImportProgressActionType['payload']
): UpdateBackupImportProgressActionType {
return { type: UPDATE_BACKUP_IMPORT_PROGRESS, payload };
}
function retryBackupImport(): ThunkAction<
void,
RootStateType,
unknown,
RetryBackupImportActionType
> {
return dispatch => {
dispatch({ type: RETRY_BACKUP_IMPORT });
backupsService.retryDownload();
};
}
2024-09-03 19:56:13 -07:00
// 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) {
2025-01-14 12:14:32 -08:00
const cancel = cancelByBaton.get(state.baton);
cancel?.();
} else {
// Reset qr code fetch attempt count when starting from scratch
provisioner?.reset();
2024-09-03 19:56:13 -07:00
}
return {
step: InstallScreenStep.QrCodeNotScanned,
provisioningUrl: {
loadingState: LoadingState.Loading,
},
baton: action.payload,
};
}
if (action.type === SET_PROVISIONING_URL) {
if (
state.step !== InstallScreenStep.QrCodeNotScanned ||
2025-01-14 12:14:32 -08:00
(state.provisioningUrl.loadingState !== LoadingState.Loading &&
// Rotating
state.provisioningUrl.loadingState !== LoadingState.Loaded)
2024-09-03 19:56:13 -07:00
) {
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 ||
2025-01-14 12:14:32 -08:00
!(
state.provisioningUrl.loadingState === LoadingState.Loading ||
// Rotating
state.provisioningUrl.loadingState === LoadingState.Loaded
)
2024-09-03 19:56:13 -07:00
) {
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,
2025-01-14 12:14:32 -08:00
envelope: action.payload.envelope,
2024-09-03 19:56:13 -07:00
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.QrCodeNotScanned &&
2024-09-03 19:56:13 -07:00
// 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,
2024-12-19 15:46:50 -05:00
backupStep: InstallScreenBackupStep.WaitForBackup,
2024-09-03 19:56:13 -07:00
};
}
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;
}
if ('error' in action.payload) {
return {
...state,
error: action.payload.error,
};
}
2024-09-03 19:56:13 -07:00
return {
...state,
backupStep: action.payload.backupStep,
2024-09-03 19:56:13 -07:00
currentBytes: action.payload.currentBytes,
totalBytes: action.payload.totalBytes,
};
}
if (action.type === RETRY_BACKUP_IMPORT) {
if (state.step !== InstallScreenStep.BackupImport) {
log.warn(
'ducks/installer: wrong step, not retrying backup import',
state.step
);
return state;
}
return {
...state,
error: undefined,
};
}
2024-09-03 19:56:13 -07:00
return state;
}