From 12f28448b29d36b01c87cf5e56d94a5584135c4f Mon Sep 17 00:00:00 2001 From: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> Date: Mon, 7 Oct 2024 06:32:31 -0700 Subject: [PATCH] Retry dialog for errors during backup download --- _locales/en/messages.json | 28 +++++++++ .../InstallScreenBackupImportStep.stories.tsx | 8 +++ .../InstallScreenBackupImportStep.tsx | 62 +++++++++++++++++++ ts/services/backups/index.ts | 53 ++++++++++++++-- ts/state/ducks/installer.ts | 57 +++++++++++++++-- ts/state/smart/InstallScreen.tsx | 5 +- 6 files changed, 202 insertions(+), 11 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index db08e9a48b7a..2b9e17d8c5d2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -4723,6 +4723,34 @@ "messageformat": "Cancel transfer", "description": "Text of the confirmation button of the cancel confirmation modal in the backup import screen" }, + "icu:BackupImportScreen__error__title": { + "messageformat": "Error transferring your messages", + "description": "Title of the error modal in the backup import screen" + }, + "icu:BackupImportScreen__error__body": { + "messageformat": "Your messages could not be transferred. Check your internet connection and try again.", + "description": "Body of the error modal in the backup import screen" + }, + "icu:BackupImportScreen__error__confirm": { + "messageformat": "Retry", + "description": "Text of the retry button of the error modal in the backup import screen" + }, + "icu:BackupImportScreen__skip": { + "messageformat": "Skip", + "description": "Text of the Skip button in the error and skip confirmation modals in the backup import screen" + }, + "icu:BackupImportScreen__skip-confirmation__title": { + "messageformat": "Skip message transfer?", + "description": "Title of the cancel confirmation modal in the backup import screen" + }, + "icu:BackupImportScreen__skip-confirmation__body": { + "messageformat": "If you choose to skip, you won’t have access to any of your messages or media on this device. You can start a new transfer after skipping from Settings > Chats > Transfer.", + "description": "Body of the cancel confirmation modal in the backup import screen" + }, + "icu:BackupImportScreen__skip-confirmation__cancel": { + "messageformat": "Cancel", + "description": "Text of the cancel button of the skip confirmation modal in the backup import screen" + }, "icu:BackupMediaDownloadProgress__title-in-progress": { "messageformat": "Restoring media", "description": "Label next to a progress bar showing active media (attachment) download progress after restoring from backup" diff --git a/ts/components/installScreen/InstallScreenBackupImportStep.stories.tsx b/ts/components/installScreen/InstallScreenBackupImportStep.stories.tsx index 06d9470b85c8..92c2dd259184 100644 --- a/ts/components/installScreen/InstallScreenBackupImportStep.stories.tsx +++ b/ts/components/installScreen/InstallScreenBackupImportStep.stories.tsx @@ -21,6 +21,7 @@ const Template: StoryFn = (args: PropsType) => ( {...args} i18n={i18n} onCancel={action('onCancel')} + onRetry={action('onRetry')} /> ); @@ -41,3 +42,10 @@ Full.args = { currentBytes: 1024, totalBytes: 1024, }; + +export const Error = Template.bind({}); +Error.args = { + currentBytes: 500 * 1024, + totalBytes: 1024 * 1024, + hasError: true, +}; diff --git a/ts/components/installScreen/InstallScreenBackupImportStep.tsx b/ts/components/installScreen/InstallScreenBackupImportStep.tsx index 95e7c0ffb3cf..2c848c34f5d4 100644 --- a/ts/components/installScreen/InstallScreenBackupImportStep.tsx +++ b/ts/components/installScreen/InstallScreenBackupImportStep.tsx @@ -18,16 +18,21 @@ export type PropsType = Readonly<{ i18n: LocalizerType; currentBytes?: number; totalBytes?: number; + hasError?: boolean; onCancel: () => void; + onRetry: () => void; }>; export function InstallScreenBackupImportStep({ i18n, currentBytes, totalBytes, + hasError, onCancel, + onRetry, }: PropsType): JSX.Element { const [isConfirmingCancel, setIsConfirmingCancel] = useState(false); + const [isConfirmingSkip, setIsConfirmingSkip] = useState(false); const confirmCancel = useCallback(() => { setIsConfirmingCancel(true); @@ -42,6 +47,24 @@ export function InstallScreenBackupImportStep({ setIsConfirmingCancel(false); }, [onCancel]); + const confirmSkip = useCallback(() => { + setIsConfirmingSkip(true); + }, []); + + const abortSkip = useCallback(() => { + setIsConfirmingSkip(false); + }, []); + + const onSkipWrap = useCallback(() => { + onCancel(); + setIsConfirmingSkip(false); + }, [onCancel]); + + const onRetryWrap = useCallback(() => { + onRetry(); + setIsConfirmingSkip(false); + }, [onRetry]); + let progress: JSX.Element; let isCancelPossible = true; if (currentBytes != null && totalBytes != null) { @@ -79,6 +102,7 @@ export function InstallScreenBackupImportStep({ ); } + return (
@@ -127,6 +151,44 @@ export function InstallScreenBackupImportStep({ {i18n('icu:BackupImportScreen__cancel-confirmation__body')} )} + + {isConfirmingSkip && ( + + {i18n('icu:BackupImportScreen__skip-confirmation__body')} + + )} + + {hasError && !isConfirmingSkip && ( + + {i18n('icu:BackupImportScreen__error__body')} + + )}
); } diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index edc680a7a69d..64d6f91ad5b7 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -39,6 +39,9 @@ import { BackupCredentials } from './credentials'; import { BackupAPI, type DownloadOptionsType } from './api'; import { validateBackup } from './validator'; import { BackupType } from './types'; +import type { ExplodePromiseResultType } from '../../util/explodePromise'; +import { explodePromise } from '../../util/explodePromise'; +import type { RetryBackupImportValue } from '../../state/ducks/installer'; export { BackupType }; @@ -50,6 +53,9 @@ export class BackupsService { private isStarted = false; private isRunning = false; private downloadController: AbortController | undefined; + private downloadRetryPromise: + | ExplodePromiseResultType + | undefined; public readonly credentials = new BackupCredentials(); public readonly api = new BackupAPI(this.credentials); @@ -87,14 +93,50 @@ export class BackupsService { const absoluteDownloadPath = window.Signal.Migrations.getAbsoluteDownloadsPath(backupDownloadPath); + let hasBackup = false; log.info('backups.download: downloading...'); - const hasBackup = await this.doDownload(absoluteDownloadPath, options); + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + // eslint-disable-next-line no-await-in-loop + hasBackup = await this.doDownload(absoluteDownloadPath, options); + } catch (error) { + log.info('backups.download: error, prompting user to retry'); + this.downloadRetryPromise = explodePromise(); + window.reduxActions.installer.updateBackupImportProgress({ + hasError: true, + }); + + // eslint-disable-next-line no-await-in-loop + const nextStep = await this.downloadRetryPromise.promise; + if (nextStep === 'retry') { + continue; + } + + try { + // eslint-disable-next-line no-await-in-loop + await unlink(absoluteDownloadPath); + } catch { + // Best-effort + } + } + break; + } await window.storage.remove('backupDownloadPath'); log.info(`backups.download: done, had backup=${hasBackup}`); } + public retryDownload(): void { + if (!this.downloadRetryPromise) { + return; + } + + this.downloadRetryPromise.resolve('retry'); + } + public async upload(): Promise { const fileName = `backup-${randomBytes(32).toString('hex')}`; const filePath = join(window.BasePaths.temp, fileName); @@ -169,6 +211,9 @@ export class BackupsService { log.warn('importBackup: canceling download'); this.downloadController.abort(); this.downloadController = undefined; + if (this.downloadRetryPromise) { + this.downloadRetryPromise.resolve('cancel'); + } } else { log.error('importBackup: not canceling download, not running'); } @@ -365,11 +410,7 @@ export class BackupsService { return false; } - try { - await unlink(downloadPath); - } catch { - // Best-effort - } + // Other errors bubble up and can be retried throw error; } diff --git a/ts/state/ducks/installer.ts b/ts/state/ducks/installer.ts index ce7dfca47277..0b33489f9914 100644 --- a/ts/state/ducks/installer.ts +++ b/ts/state/ducks/installer.ts @@ -26,6 +26,7 @@ import { import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions'; import * as log from '../../logging/log'; +import { backupsService } from '../../services/backups'; const SLEEP_ERROR = new TimeoutError(); @@ -63,14 +64,18 @@ export type InstallerStateType = ReadonlyDeep< step: InstallScreenStep.BackupImport; currentBytes?: number; totalBytes?: number; + hasError?: boolean; } >; +export type RetryBackupImportValue = ReadonlyDeep<'retry' | 'cancel'>; + 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'; 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'; @@ -103,6 +108,10 @@ type QRCodeScannedActionType = ReadonlyDeep<{ }; }>; +type RetryBackupImportActionType = ReadonlyDeep<{ + type: typeof RETRY_BACKUP_IMPORT; +}>; + type ShowLinkInProgressActionType = ReadonlyDeep<{ type: typeof SHOW_LINK_IN_PROGRESS; }>; @@ -113,10 +122,14 @@ export type ShowBackupImportActionType = ReadonlyDeep<{ type UpdateBackupImportProgressActionType = ReadonlyDeep<{ type: typeof UPDATE_BACKUP_IMPORT_PROGRESS; - payload: { - currentBytes: number; - totalBytes: number; - }; + payload: + | { + currentBytes: number; + totalBytes: number; + } + | { + hasError: boolean; + }; }>; export type InstallerActionType = ReadonlyDeep< @@ -125,6 +138,7 @@ export type InstallerActionType = ReadonlyDeep< | SetQRCodeErrorActionType | SetErrorActionType | QRCodeScannedActionType + | RetryBackupImportActionType | ShowLinkInProgressActionType | ShowBackupImportActionType | UpdateBackupImportProgressActionType @@ -134,6 +148,7 @@ export const actions = { startInstaller, finishInstall, updateBackupImportProgress, + retryBackupImport, showBackupImport, showLinkInProgress, }; @@ -414,6 +429,18 @@ function updateBackupImportProgress( return { type: UPDATE_BACKUP_IMPORT_PROGRESS, payload }; } +function retryBackupImport(): ThunkAction< + void, + RootStateType, + unknown, + RetryBackupImportActionType +> { + return dispatch => { + dispatch({ type: RETRY_BACKUP_IMPORT }); + backupsService.retryDownload(); + }; +} + // Reducer export function getEmptyState(): InstallerStateType { @@ -546,6 +573,13 @@ export function reducer( return state; } + if ('hasError' in action.payload) { + return { + ...state, + hasError: action.payload.hasError, + }; + } + return { ...state, currentBytes: action.payload.currentBytes, @@ -553,5 +587,20 @@ export function reducer( }; } + 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, + hasError: false, + }; + } + return state; } diff --git a/ts/state/smart/InstallScreen.tsx b/ts/state/smart/InstallScreen.tsx index 53e15764dc42..365e55be16b3 100644 --- a/ts/state/smart/InstallScreen.tsx +++ b/ts/state/smart/InstallScreen.tsx @@ -30,7 +30,8 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() { const installerState = useSelector(getInstallerState); const updates = useSelector(getUpdatesState); const { openInbox } = useAppActions(); - const { startInstaller, finishInstall } = useInstallerActions(); + const { startInstaller, finishInstall, retryBackupImport } = + useInstallerActions(); const { startUpdate } = useUpdatesActions(); const hasExpired = useSelector(hasExpiredSelector); @@ -110,7 +111,9 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() { i18n, currentBytes: installerState.currentBytes, totalBytes: installerState.totalBytes, + hasError: installerState.hasError, onCancel: onCancelBackupImport, + onRetry: retryBackupImport, }, }; break;