diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 22d7c6fb75..045ff0e0de 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -9192,13 +9192,29 @@ "messageformat": "Update Required", "description": "The title of update dialog on install screen when app update is required before proceeding with backup import" }, + "icu:InstallScreenUpdateDialog--checking-for-updates": { + "messageformat": "Checking for updates", + "description": "Text shown with an indeterminate spinner while Signal Desktop checks the internet for newer versions" + }, + "icu:InstallScreenUpdateDialog--updating-signal": { + "messageformat": "Updating Signal", + "description": "Text shown with a progress bar as Signal Desktop is downloading an updated version" + }, + "icu:InstallScreenUpdateDialog--download-progress": { + "messageformat": "Downloading {currentBytes} of {totalBytes} ({percentage, number, percent})...", + "description": "Shown below progress bar to provide the details of download. currentBytes and totalBytes will be like 5 GB or 10 GB." + }, "icu:InstallScreenUpdateDialog--update-required__body": { - "messageformat": "To complete syncing your messages, update Signal desktop now.", - "description": "The body of update dialog on install screen when app update is required before proceeding with backup import" + "messageformat": "To continue, update Signal Desktop.", + "description": "The body of update dialog on install screen when app update is required before proceeding" }, "icu:InstallScreenUpdateDialog--update-required__action-update": { "messageformat": "Update", - "description": "The update action of update dialog on install screen when app update is required before proceeding with backup import" + "description": "The update action of update dialog on install screen when app update is required" + }, + "icu:InstallScreenUpdateDialog--update-required__action-update__mas": { + "messageformat": "Go to the App Store", + "description": "The update action of update dialog on install screen when update is required, but app was installed via Mac App Store" }, "icu:InstallScreenUpdateDialog--unsupported-os__title": { "messageformat": "Update Required", @@ -10835,15 +10851,15 @@ "description": "Toast message when Signal PIN reminder was completed successfully." }, "icu:UnsupportedOSWarningDialog__body": { - "messageformat": "Signal desktop will no longer support your computer’s version of {OS} soon. To keep using Signal, update your computer’s operating system by {expirationDate}. Learn more", + "messageformat": "Signal Desktop will no longer support your computer’s version of {OS} soon. To keep using Signal, update your computer’s operating system by {expirationDate}. Learn more", "description": "Body of a dialog displayed on unsupported operating systems" }, "icu:UnsupportedOSErrorDialog__body": { - "messageformat": "Signal desktop no longer works on this computer. To use Signal desktop again, update your computer’s version of {OS}. Learn more", + "messageformat": "Signal Desktop no longer works on this computer. To use Signal Desktop again, update your computer’s version of {OS}. Learn more", "description": "Body of a dialog displayed on unsupported operating systems" }, "icu:UnsupportedOSErrorToast": { - "messageformat": "Signal desktop no longer works on this computer. To use Signal desktop again, update your computer’s version of {OS}.", + "messageformat": "Signal Desktop no longer works on this computer. To use Signal Desktop again, update your computer’s version of {OS}.", "description": "Body of a dialog displayed on unsupported operating systems" }, "icu:MessageMetadata__edited": { diff --git a/stylesheets/components/InstallScreenUpdateDialog.scss b/stylesheets/components/InstallScreenUpdateDialog.scss index 7d1d136435..76a1ff3326 100644 --- a/stylesheets/components/InstallScreenUpdateDialog.scss +++ b/stylesheets/components/InstallScreenUpdateDialog.scss @@ -1,50 +1,20 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -@use '../mixins'; -@use '../variables'; - .InstallScreenUpdateDialog { + position: relative; + display: flex; + + width: 100%; + height: 100vh; + + flex-direction: column; + align-items: center; + &__download-size { font-weight: 400; } - &__progress { - &--container { - @include mixins.light-theme() { - background-color: variables.$color-gray-15; - } - @include mixins.dark-theme() { - background-color: variables.$color-gray-65; - } - & { - border-radius: 2px; - height: 4px; - overflow: hidden; - width: 100%; - margin-block: 16px; - margin-inline: 0; - } - } - - &--bar { - background-color: variables.$color-ultramarine; - border-radius: 2px; - display: block; - height: 100%; - width: 100%; - &:dir(ltr) { - /* stylelint-disable-next-line declaration-property-value-disallowed-list */ - transform: translateX(-100%); - } - &:dir(rtl) { - /* stylelint-disable-next-line declaration-property-value-disallowed-list */ - transform: translateX(100%); - } - transition: transform 500ms ease-out; - } - } - a { // Prevent breaking the text display: inline-block; diff --git a/ts/background.preload.ts b/ts/background.preload.ts index a30b7eb78d..b1555f526c 100644 --- a/ts/background.preload.ts +++ b/ts/background.preload.ts @@ -1623,11 +1623,17 @@ async function startApp(): Promise { if ( registrationPartialState === PartialRegistrationType.EXISTING__PIN ) { + StorageService.disableStorageService( + 'EXISTING__PIN: Need to get PIN before we turn it on' + ); window.reduxActions.standaloneInstaller.goToVerifyPINStage(); window.reduxActions.app.openStandalone(startFromBeginning); } else if ( registrationPartialState === PartialRegistrationType.EXISTING__PROFILE ) { + StorageService.disableStorageService( + 'EXISTING__PROFILE: Need to get PIN before we turn it on' + ); const hasPin = true; window.reduxActions.standaloneInstaller.goToProfileEntryStage( hasPin, diff --git a/ts/components/installScreen/InstallScreenBackupImportStep.dom.stories.tsx b/ts/components/installScreen/InstallScreenBackupImportStep.dom.stories.tsx index 2fdd733480..03aa17c92c 100644 --- a/ts/components/installScreen/InstallScreenBackupImportStep.dom.stories.tsx +++ b/ts/components/installScreen/InstallScreenBackupImportStep.dom.stories.tsx @@ -1,10 +1,9 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { useState, useCallback, useEffect, type JSX } from 'react'; +import { useState, useEffect, type JSX } from 'react'; import type { Meta, StoryFn } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { sleep } from '../../util/sleep.std.ts'; import { InstallScreenBackupStep, InstallScreenBackupError, @@ -15,46 +14,18 @@ import { InstallScreenBackupImportStep } from './InstallScreenBackupImportStep.d const { i18n } = window.SignalContext; -const DEFAULT_UPDATES = { +const updates = { dialogType: DialogType.None, didSnooze: false, isCheckingForUpdates: false, showEventsCount: 0, - downloadSize: 42 * 1024 * 1024, }; export default { - title: 'Components/InstallScreenBackupImportStep', + title: 'Components/InstallScreen/InstallScreenBackupImportStep', } satisfies Meta; const Template: StoryFn = (args: PropsType) => { - const [updates, setUpdates] = useState(DEFAULT_UPDATES); - const forceUpdate = useCallback(async () => { - setUpdates(state => ({ - ...state, - isCheckingForUpdates: true, - })); - await sleep(500); - setUpdates(state => ({ - ...state, - isCheckingForUpdates: false, - dialogType: DialogType.Downloading, - downloadSize: 100, - downloadedSize: 0, - version: 'v7.7.7', - })); - await sleep(500); - setUpdates(state => ({ - ...state, - downloadedSize: 50, - })); - await sleep(500); - setUpdates(state => ({ - ...state, - downloadedSize: 100, - })); - }, [setUpdates]); - return ( = (args: PropsType) => { currentVersion="v6.0.0" OS="macOS" startUpdate={action('startUpdate')} - forceUpdate={forceUpdate} + forceCheck={action('forceCheck')} onCancel={action('onCancel')} onRetry={action('onRetry')} /> @@ -94,11 +65,11 @@ export function FullFlow(): JSX.Element { return ( void; - forceUpdate: () => void; + forceCheck: () => void; } & ( | { backupStep: InstallScreenBackupStep.WaitForBackup; @@ -63,7 +62,7 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element { currentVersion, OS, startUpdate, - forceUpdate, + forceCheck, } = props; const [isConfirmingCancel, setIsConfirmingCancel] = useState(false); @@ -92,13 +91,12 @@ export function InstallScreenBackupImportStep(props: PropsType): JSX.Element { if (error == null || error === InstallScreenBackupError.Canceled) { // no-op } else if (error === InstallScreenBackupError.UnsupportedVersion) { - errorElem = ( + return ( = { updates: DEFAULT_UPDATES, OS: 'macOS', startUpdate: action('startUpdate'), - forceUpdate: action('forceUpdate'), + forceCheck: action('forceCheck'), currentVersion: 'v6.0.0', retryGetQrCode: action('retryGetQrCode'), isConfirmingDataDeletion: false, diff --git a/ts/components/installScreen/InstallScreenQrCodeNotScannedStep.dom.tsx b/ts/components/installScreen/InstallScreenQrCodeNotScannedStep.dom.tsx index 118969117a..d450c750ae 100644 --- a/ts/components/installScreen/InstallScreenQrCodeNotScannedStep.dom.tsx +++ b/ts/components/installScreen/InstallScreenQrCodeNotScannedStep.dom.tsx @@ -7,10 +7,7 @@ import classNames from 'classnames'; import lodash from 'lodash'; import type { LocalizerType } from '../../types/Util.std.ts'; -import { - InstallScreenStep, - InstallScreenQRCodeError, -} from '../../types/InstallScreen.std.ts'; +import { InstallScreenQRCodeError } from '../../types/InstallScreen.std.ts'; import { DialogType } from '../../types/Dialogs.std.ts'; import { missingCaseError } from '../../util/missingCaseError.std.ts'; import type { Loadable } from '../../util/loadable.std.ts'; @@ -43,7 +40,7 @@ export type PropsType = Readonly<{ isStaging: boolean; retryGetQrCode: () => void; startUpdate: () => void; - forceUpdate: () => void; + forceCheck: () => void; isConfirmingDataDeletion: boolean; continueInstallWithDataDeletion: () => void; restartInstall: () => void; @@ -65,30 +62,31 @@ export function InstallScreenQrCodeNotScannedStep({ provisioningUrl, retryGetQrCode, startUpdate, - forceUpdate, + forceCheck, isConfirmingDataDeletion, restartInstall, continueInstallWithDataDeletion, updates, }: Readonly): ReactElement { + if (hasExpired || updates.dialogType === DialogType.Downloading) { + return ( + + ); + } + return (
- {(hasExpired || updates.dialogType === DialogType.Downloading) && ( - - )} -
; +} +export function _2IsCheckingForUpdates(): ReactNode { return ( - + ); +} + +export function _3ErrorUnsupportedOS(): ReactNode { + return ( + ); } -export function UpdateAvailableNotReady(): ReactNode { +export function _3ErrorMASUpdate(): ReactNode { return ( - ); } -export function UpdateAvailableAndReady(): ReactNode { +export function _3DownloadReady(): ReactNode { return ( - ); } -export function UpdateDownloading(): ReactNode { - return ; -} - -export function UpdateDownloaded(): ReactNode { +export function _3FullDownloadReady(): ReactNode { return ( - ); } -export function UnsupportedOS(): ReactNode { +export function _3AutoUpdate(): ReactNode { return ( - - ); -} - -export function CannotUpdate(): ReactNode { - return ( - ); } -export function CannotUpdateMacOsReadOnly(): ReactNode { +export function _4Downloading(): ReactNode { return ( - + + ); +} + +export function _5DownloadedUpdate(): ReactNode { + return ( + + ); +} + +export function _6ErrorCannotUpdate(): ReactNode { + return ( + + ); +} + +export function _6ErrorCannot_Update_Require_Manual(): ReactNode { + return ( + + ); +} + +export function _6ErrorMacosReadOnly(): ReactNode { + return ( + + ); +} + +export function Flow(): ReactNode { + const [updates, setUpdates] = useState({ + dialogType: DialogType.None, + didSnooze: false, + isCheckingForUpdates: false, + showEventsCount: 0, + downloadSize: 42 * 1024 * 1024, + }); + const forceCheck = useCallback(async () => { + setUpdates(state => ({ + ...state, + isCheckingForUpdates: true, + })); + await sleep(2000); + setUpdates(state => ({ + ...state, + isCheckingForUpdates: false, + dialogType: DialogType.DownloadReady, + downloadSize: 30_000_000, + downloadedSize: 0, + version: 'v7.7.7', + })); + }, [setUpdates]); + + const startUpdate = useCallback(async () => { + if (updates.dialogType === DialogType.DownloadedUpdate) { + // oxlint-disable-next-line no-console + console.log('Restarting!'); + return; + } + + setUpdates(state => ({ + ...state, + dialogType: DialogType.Downloading, + downloadSize: 30_000_000, + downloadedSize: 0, + version: 'v7.7.7', + })); + await sleep(1000); + setUpdates(state => ({ + ...state, + downloadedSize: 15_000_000, + })); + await sleep(1000); + setUpdates(state => ({ + ...state, + downloadedSize: 30_000_000, + })); + await sleep(500); + setUpdates(state => ({ + ...state, + dialogType: DialogType.DownloadedUpdate, + })); + }, [updates, setUpdates]); + + return ( + ); } diff --git a/ts/components/installScreen/InstallScreenUpdateDialog.dom.tsx b/ts/components/installScreen/InstallScreenUpdateDialog.dom.tsx index 5a71515151..27dd7e0d1e 100644 --- a/ts/components/installScreen/InstallScreenUpdateDialog.dom.tsx +++ b/ts/components/installScreen/InstallScreenUpdateDialog.dom.tsx @@ -2,8 +2,9 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { JSX, ReactNode } from 'react'; +import { isNumber } from 'lodash'; + import { DialogType } from '../../types/Dialogs.std.ts'; -import { InstallScreenStep } from '../../types/InstallScreen.std.ts'; import type { LocalizerType } from '../../types/Util.std.ts'; import { PRODUCTION_DOWNLOAD_URL, @@ -17,14 +18,15 @@ import { roundFractionForProgressBar } from '../../util/numbers.std.ts'; import { I18n } from '../I18n.dom.tsx'; import { formatFileSize } from '../../util/formatFileSize.std.ts'; import { AxoConfirmDialog } from '../../axo/AxoConfirmDialog.dom.tsx'; -import { AxoDialog } from '../../axo/AxoDialog.dom.tsx'; import { tw } from '../../axo/tw.dom.tsx'; +import { TitlebarDragArea } from '../TitlebarDragArea.dom.tsx'; +import { InstallScreenSignalLogo } from './InstallScreenSignalLogo.dom.tsx'; +import { ProgressBar } from '../ProgressBar.dom.tsx'; export type PropsType = UpdatesStateType & Readonly<{ i18n: LocalizerType; - step: InstallScreenStep; - forceUpdate: () => void; + forceCheck: () => void; startUpdate: () => void; currentVersion: string; OS: string; @@ -33,66 +35,72 @@ export type PropsType = UpdatesStateType & export function InstallScreenUpdateDialog({ i18n, - step, dialogType, isCheckingForUpdates, downloadSize, downloadedSize, - forceUpdate, + forceCheck, startUpdate, currentVersion, OS, onClose = () => null, }: PropsType): JSX.Element | null { - if (dialogType === DialogType.None) { - if (step === InstallScreenStep.BackupImport) { - if (isCheckingForUpdates) { - return ; - } + let modal: ReactNode | undefined = undefined; + let inlineElement: ReactNode | undefined = undefined; - return ( + if (dialogType === DialogType.None) { + if (isCheckingForUpdates) { + inlineElement = ( + +
+
+ {i18n('icu:InstallScreenUpdateDialog--checking-for-updates')} +
+ +
+
+ ); + } else { + modal = ( ); } - - return null; - } - - if (dialogType === DialogType.UnsupportedOS) { - return ; - } - - if (dialogType === DialogType.MASUpdate) { - return ( + } else if (dialogType === DialogType.UnsupportedOS) { + modal = ; + } else if (dialogType === DialogType.MASUpdate) { + modal = ( ); - } - - if (dialogType === DialogType.DownloadedUpdate) { - return ( + } else if (dialogType === DialogType.DownloadedUpdate) { + modal = ( ); - } - - if ( + } else if ( dialogType === DialogType.AutoUpdate || // Manual update with an action button dialogType === DialogType.DownloadReady || dialogType === DialogType.FullDownloadReady ) { - return ( + modal = ( ); - } - - if (dialogType === DialogType.Downloading) { - const fractionComplete = roundFractionForProgressBar( - (downloadedSize || 0) / (downloadSize || 1) + } else if (dialogType === DialogType.Downloading) { + const fractionComplete = downloadSize + ? roundFractionForProgressBar((downloadedSize || 0) / downloadSize) + : null; + inlineElement = ( + +
+ {i18n('icu:InstallScreenUpdateDialog--updating-signal')} +
+
+ +
+ {isNumber(fractionComplete) ? ( +
+ {i18n('icu:InstallScreenUpdateDialog--download-progress', { + currentBytes: formatFileSize(downloadedSize ?? 0), + totalBytes: formatFileSize(downloadSize ?? 1), + percentage: fractionComplete, + })} +
+ ) : undefined} +
); - return ( - - ); - } - - if ( + } else if ( dialogType === DialogType.Cannot_Update || dialogType === DialogType.Cannot_Update_Require_Manual ) { - return ( + modal = ( ); + } else if (dialogType === DialogType.MacOS_Read_Only) { + modal = ; + } else { + throw missingCaseError(dialogType); } - if (dialogType === DialogType.MacOS_Read_Only) { - return ; - } - - throw missingCaseError(dialogType); + return ( +
+ + + {modal} + {inlineElement} +
+ ); } -/** @testexport */ -export function UpdateRequiredModal(props: { +function CenteredElement({ children }: { children: ReactNode }): JSX.Element { + return ( +
+ {children} +
+ ); +} + +function UpdateRequiredModal(props: { + isMas: boolean; i18n: LocalizerType; onClose: () => void; onAction: () => void; @@ -157,14 +199,19 @@ export function UpdateRequiredModal(props: { variant="strong-primary" onClick={props.onAction} > - {i18n('icu:InstallScreenUpdateDialog--update-required__action-update')} + {props.isMas + ? i18n( + 'icu:InstallScreenUpdateDialog--update-required__action-update__mas' + ) + : i18n( + 'icu:InstallScreenUpdateDialog--update-required__action-update' + )} ); } -/** @testexport */ -export function UpdateAvailableModal(props: { +function UpdateAvailableModal(props: { i18n: LocalizerType; onClose: () => void; onStartUpdate: () => void; @@ -206,38 +253,7 @@ export function UpdateAvailableModal(props: { ); } -/** @testexport */ -export function UpdateDownloadingModal(props: { - i18n: LocalizerType; - progress: number; -}): ReactNode { - const { i18n } = props; - return ( - - - - - {i18n('icu:DialogUpdate__downloading')} - - - - -
-
-
- - - - - - ); -} - -/** @testexport */ -export function UpdateDownloadedModal(props: { +function UpdateDownloadedModal(props: { i18n: LocalizerType; onClose: () => void; onStartUpdate: () => void; @@ -275,8 +291,7 @@ const learnMoreLink = (parts: Array) => ( ); -/** @testexport */ -export function UnsupportedOSModal(props: { +function UnsupportedOSModal(props: { i18n: LocalizerType; OS: string; onClose: () => void; @@ -301,8 +316,7 @@ export function UnsupportedOSModal(props: { ); } -/** @testexport */ -export function CannotUpdateModal(props: { +function CannotUpdateModal(props: { i18n: LocalizerType; onClose: () => void; currentVersion: string; @@ -354,8 +368,7 @@ export function CannotUpdateModal(props: { ); } -/** @testexport */ -export function CannotUpdateMacOsReadOnlyModal(props: { +function CannotUpdateMacOsReadOnlyModal(props: { i18n: LocalizerType; onClose: () => void; }): ReactNode { diff --git a/ts/components/standaloneRegistration/StandaloneRegistration.dom.stories.tsx b/ts/components/standaloneRegistration/StandaloneRegistration.dom.stories.tsx index a1cb8c7978..193482d16a 100644 --- a/ts/components/standaloneRegistration/StandaloneRegistration.dom.stories.tsx +++ b/ts/components/standaloneRegistration/StandaloneRegistration.dom.stories.tsx @@ -26,6 +26,7 @@ import { getDefaultAvatars } from '../../types/Avatar.std.ts'; import { MINUTE, SECOND } from '../../util/durations/constants.std.ts'; import { VerificationTransport } from '../../types/VerificationTransport.std.ts'; import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability.std.ts'; +import { DialogType } from '../../types/Dialogs.std.ts'; const i18n = setupI18n('en', messages); @@ -88,6 +89,19 @@ export const StartingScreen: Story = { }, }, + // Updates + currentVersion: 'v1.0.0', + forceCheck: action('forceCheck'), + kickOffForcedUpgrade: action('kickOffForcedUpgrade'), + OS: 'windows', + startUpdate: action('startUpdate'), + updates: { + dialogType: DialogType.None, + didSnooze: false, + showEventsCount: 0, + isCheckingForUpdates: false, + }, + // AvatarEditor support deleteAvatarFromDisk: action('deleteAvatarFromDisk'), replaceAvatar: action('replaceAvatar'), @@ -779,6 +793,21 @@ export const AccountLocked: Story = { }, }; +export const UpdateRequiredWithDownloadingState: Story = { + args: { + ...StartingScreen.args, + workflow: { + stage: RegistrationStage.UPDATE_REQUIRED, + }, + updates: { + ...StartingScreen.args.updates, + dialogType: DialogType.Downloading, + downloadedSize: 50_000_000, + downloadSize: 60_000_000, + }, + }, +}; + export function ProgressionWithVerify(): React.JSX.Element { const [workflow, setWorkflow] = useState({ stage: RegistrationStage.PHONE_NUMBER, diff --git a/ts/components/standaloneRegistration/StandaloneRegistration.dom.tsx b/ts/components/standaloneRegistration/StandaloneRegistration.dom.tsx index 35f2534936..8fbd73975e 100644 --- a/ts/components/standaloneRegistration/StandaloneRegistration.dom.tsx +++ b/ts/components/standaloneRegistration/StandaloneRegistration.dom.tsx @@ -25,6 +25,7 @@ import { AccountLockedScreen } from './stages/AccountLocked.dom.tsx'; import { Spacer } from './util/StepComponents.dom.tsx'; import { CONTACT_SUPPORT_URL } from '../../util/contactSupport.dom.tsx'; import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser.dom.ts'; +import { InstallScreenUpdateDialog } from '../installScreen/InstallScreenUpdateDialog.dom.tsx'; import type { LocalizerType } from '../../types/I18N.std.ts'; import type { ActionCreator } from '../../state/types.std.ts'; @@ -54,6 +55,7 @@ import type { verifyPIN as doVerifyPIN, } from '../../state/ducks/standaloneInstaller.preload.ts'; import type { CountryDataType } from '../../util/getCountryData.dom.ts'; +import type { UpdatesStateType } from '../../state/ducks/updates.preload.ts'; const SPRING = { type: 'spring' as const, @@ -89,6 +91,14 @@ export type PropsType = Readonly<{ verifyPIN: ActionCreator; workflow: RegistrationWorkflow; + // Updates + currentVersion: string; + forceCheck: () => unknown; + kickOffForcedUpgrade: () => unknown; + OS: string; + startUpdate: () => unknown; + updates: UpdatesStateType; + // AvatarEditor support deleteAvatarFromDisk: DeleteAvatarFromDiskActionType; replaceAvatar: ReplaceAvatarActionType; @@ -122,6 +132,14 @@ export function StandaloneRegistration({ verifyPIN, workflow, + // Updates + currentVersion, + forceCheck, + kickOffForcedUpgrade, + OS, + startUpdate, + updates, + // AvatarEditor support deleteAvatarFromDisk, replaceAvatar, @@ -236,6 +254,17 @@ export function StandaloneRegistration({ body = ( ); + } else if (workflow.stage === RegistrationStage.UPDATE_REQUIRED) { + return ( + + ); } else { throw missingCaseError(workflow); } @@ -297,9 +326,7 @@ export function StandaloneRegistration({ { - // TODO: kick off update process - how to represent this during updates? - }} + onClick={() => kickOffForcedUpgrade()} > {i18n('icu:StandaloneRegistration--UpdateRequired--update')} diff --git a/ts/shims/updateIpc.preload.ts b/ts/shims/updateIpc.preload.ts index 17be8d90cf..7edc974f3e 100644 --- a/ts/shims/updateIpc.preload.ts +++ b/ts/shims/updateIpc.preload.ts @@ -7,6 +7,6 @@ export function startUpdate(): Promise { return ipcRenderer.invoke('start-update'); } -export function forceUpdate(): Promise { - return ipcRenderer.invoke('updater/force-update'); +export function forceCheck(): Promise { + return ipcRenderer.invoke('updater/force-check'); } diff --git a/ts/state/ducks/standaloneInstaller.preload.ts b/ts/state/ducks/standaloneInstaller.preload.ts index 5db5643867..53efcccafb 100644 --- a/ts/state/ducks/standaloneInstaller.preload.ts +++ b/ts/state/ducks/standaloneInstaller.preload.ts @@ -69,6 +69,7 @@ import type { RawTimings, RegistrationWorkflow, Timings, + UpdateRequiredStage, VerificationCodeStage, VerifyPINStage, } from '../../types/StandaloneRegistration.std.ts'; @@ -277,6 +278,14 @@ export function moveToVerificationStage({ reason: 'standalone registration', }); + const afterCaptchaWorkflow = getState().standaloneInstaller.workflow; + if (afterCaptchaWorkflow?.stage !== RegistrationStage.CAPTCHA) { + log.warn( + `${logId}: Captcha challenge returned, but workflow is now at stage ${afterCaptchaWorkflow?.stage}` + ); + return; + } + try { workflow = { ...workflow, @@ -324,7 +333,7 @@ export function moveToVerificationStage({ if ( error instanceof LibSignalErrorBase && (error.is(ErrorCode.RegistrationRequestRejected) || - error.is(ErrorCode.RegistrationRequestRejected)) + error.is(ErrorCode.RegistrationRequestInvalid)) ) { workflow = { ...workflow, @@ -667,8 +676,7 @@ export function submitVerificationCode({ PartialRegistrationType.EXISTING__PROFILE ); } else { - // We own storage service; no prior data there. We just turn it on! - enableStorageService(); + enableStorageService(); // submitVerificationCode: no prior data there, just turn it on await itemStorage.put( 'standaloneRegistrationPartialState', PartialRegistrationType.NEW_ACCOUNT__PROFILE @@ -702,9 +710,8 @@ export function submitVerificationCode({ }, avatars: undefined, }; - } - - if (!accountState) { + } else { + enableStorageService(); // submitVerificationCode: Got a random error back from account creation workflow = { ...workflow, status: { @@ -917,12 +924,12 @@ export function verifyPIN({ // Something has really gone wrong - we're in reglock, but SVR has nothing for us dispatch(updateWorkflow(workflow, FatalErrorType.UNEXPECTED)); } else { - // No reglock and nothing in SVR - let's allow the user to create a new PIN - enableStorageService(); + enableStorageService(); // verifyPIN: nothing in SVR; will start afresh with new key await itemStorage.put( 'standaloneRegistrationPartialState', PartialRegistrationType.NEW_ACCOUNT__PIN ); + // No reglock and nothing in SVR - let's allow the user to create a new PIN dispatch(goToCreatePINStage()); } } else { @@ -959,7 +966,7 @@ export function verifyPIN({ toBase64(masterKey) ); await itemStorage.put('standaloneRegistrationPartialState', undefined); - enableStorageService(); + enableStorageService(); // verifyPIN: No reglock, got temporary master key } catch (error) { log.error( `${logId}: error saving data after creating account`, @@ -1014,7 +1021,7 @@ export function verifyPIN({ try { disableStorageService( - 'standaloneInstaller/verifyPIN, about to create account' + 'standaloneInstaller/verifyPIN: Reglock, about to create account' ); await accountManager.registerAsPrimaryDevice({ number: phoneNumber, @@ -1028,6 +1035,12 @@ export function verifyPIN({ toLogFormat(error) ); + workflow = { + ...workflow, + status: { + type: 'ready', + }, + }; dispatch({ type: UPDATE_WORKFLOW, payload: { @@ -1057,20 +1070,14 @@ export function verifyPIN({ ourConversation.set({ avatars }); await DataWriter.updateConversation(ourConversation.attributes); } - - enableStorageService(); } catch (error) { + // The account is created, so we should let the user go to the inbox log.error( `${logId}: error saving data after creating account`, toLogFormat(error) ); - dispatch({ - type: UPDATE_WORKFLOW, - payload: { - workflow, - fatalError: FatalErrorType.UNEXPECTED, - }, - }); + } finally { + enableStorageService(); // verifyPIN: Created account with reglock, tried to set things up } try { @@ -1092,17 +1099,11 @@ export function verifyPIN({ : undefined, }); } catch (error) { + // The account is created, so we should let the user go to the inbox log.error( `${logId}: error queueing important jobs after creating account`, toLogFormat(error) ); - dispatch({ - type: UPDATE_WORKFLOW, - payload: { - workflow, - fatalError: FatalErrorType.UNEXPECTED, - }, - }); } await completeRegistration({ workflow: previousWorkflow })( @@ -1209,6 +1210,16 @@ export function goToAccountLockedStage(): UpdateWorkflowActionType { return updateWorkflow(workflow); } +function goToUpdateRequiredStage(): UpdateWorkflowActionType { + const logId = 'goToUpdateRequiredStage'; + log.info(logId); + + const workflow: UpdateRequiredStage = { + stage: RegistrationStage.UPDATE_REQUIRED, + }; + return updateWorkflow(workflow); +} + export function completeRegistration({ workflow: previousWorkflow, }: { @@ -1409,6 +1420,7 @@ export const actions = { goToAccountLockedStage, goToCreatePINStage, goToProfileEntryStage, + goToUpdateRequiredStage, goToVerifyPINStage, moveToCaptchaStage, moveToVerificationStage, @@ -1431,12 +1443,12 @@ export const useStandaloneInstallerActions = (): BoundActionCreatorsMapObject< // Utilities function analyzeError(error: Error): FatalErrorType { - // TODO: might want to do something here for rate limit errors if (error instanceof LibSignalErrorBase) { if ( error.is(ErrorCode.ChatServiceInactive) || error.is(ErrorCode.IoError) || - error.is(ErrorCode.PossibleCaptiveNetwork) + error.is(ErrorCode.PossibleCaptiveNetwork) || + error.is(ErrorCode.RateLimitedError) ) { return FatalErrorType.OFFLINE; } @@ -1496,19 +1508,35 @@ export function reducer( return getEmptyState(); } - if (newWorkflow.stage === RegistrationStage.PHONE_NUMBER) { - log.info( - `UPDATE_WORKFLOW: Transitioning from ${previousWorkflow?.stage ?? ''} to ${newWorkflow.stage}` - ); + const previousOrder = previousWorkflow?.stage + ? StageOrder[previousWorkflow.stage] + : StageOrder[RegistrationStage.PHONE_NUMBER] - 1; + const currentOrder = StageOrder[newWorkflow.stage]; + const direction = + currentOrder >= previousOrder ? Direction.FORWARD : Direction.BACKWARD; + + if (previousWorkflow?.stage === newWorkflow.stage) { return { ...state, workflow: newWorkflow, fatalError, - direction: - previousWorkflow && - previousWorkflow.stage !== RegistrationStage.PHONE_NUMBER - ? Direction.BACKWARD - : Direction.FORWARD, + direction, + }; + } + + if ( + newWorkflow.stage === RegistrationStage.PHONE_NUMBER || + newWorkflow.stage === RegistrationStage.UPDATE_REQUIRED + ) { + log.info( + `UPDATE_WORKFLOW: Transitioning from ${previousWorkflow?.stage ?? ''} to ${newWorkflow.stage}` + ); + + return { + ...state, + workflow: newWorkflow, + fatalError, + direction, }; } @@ -1525,7 +1553,7 @@ export function reducer( ...state, workflow: newWorkflow, fatalError, - direction: Direction.FORWARD, + direction, }; } @@ -1538,15 +1566,6 @@ export function reducer( }; } - if (previousWorkflow.stage === newWorkflow.stage) { - return { - ...state, - workflow: newWorkflow, - fatalError, - direction: Direction.FORWARD, - }; - } - const validNextStages = ValidNextStages[previousWorkflow.stage]; if (!validNextStages.has(newWorkflow.stage)) { log.error( @@ -1562,15 +1581,11 @@ export function reducer( `UPDATE_WORKFLOW: Transitioning from ${previousWorkflow.stage} to ${newWorkflow.stage}` ); - const previousOrder = StageOrder[previousWorkflow.stage]; - const currentOrder = StageOrder[previousWorkflow.stage]; - return { ...state, workflow: newWorkflow, fatalError, - direction: - currentOrder >= previousOrder ? Direction.FORWARD : Direction.BACKWARD, + direction, }; } diff --git a/ts/state/ducks/updates.preload.ts b/ts/state/ducks/updates.preload.ts index 3f99696e80..8073d6ea10 100644 --- a/ts/state/ducks/updates.preload.ts +++ b/ts/state/ducks/updates.preload.ts @@ -148,7 +148,7 @@ function startUpdate(): ThunkAction< }; } -function forceUpdate(): ThunkAction< +function forceCheck(): ThunkAction< void, RootStateType, unknown, @@ -162,7 +162,7 @@ function forceUpdate(): ThunkAction< }); try { - await updateIpc.forceUpdate(); + await updateIpc.forceCheck(); } catch { dispatch({ type: SHOW_UPDATE_DIALOG, @@ -184,7 +184,7 @@ export const actions = { showUpdateDialog, snoozeUpdate, startUpdate, - forceUpdate, + forceCheck, }; export const useUpdatesActions = (): BoundActionCreatorsMapObject< diff --git a/ts/state/smart/InstallScreen.preload.tsx b/ts/state/smart/InstallScreen.preload.tsx index 04da86e1a2..69c8ed7c75 100644 --- a/ts/state/smart/InstallScreen.preload.tsx +++ b/ts/state/smart/InstallScreen.preload.tsx @@ -32,7 +32,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() { const updates = useSelector(getUpdatesState); const { continueInstallWithDataDeletion, startInstaller, retryBackupImport } = useInstallerActions(); - const { startUpdate, forceUpdate } = useUpdatesActions(); + const { startUpdate, forceCheck } = useUpdatesActions(); const hasExpired = useSelector(hasExpiredSelector); const onCancelBackupImport = useCallback((): void => { @@ -56,7 +56,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() { updates, currentVersion: window.getVersion(), startUpdate, - forceUpdate, + forceCheck, retryGetQrCode: startInstaller, isConfirmingDataDeletion: installerState.isConfirmingDataDeletion, restartInstall: startInstaller, @@ -82,7 +82,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() { onRetry: retryBackupImport, updates, currentVersion: window.getVersion(), - forceUpdate, + forceCheck, startUpdate, OS: OS.getName(), }, diff --git a/ts/state/smart/StandaloneRegistration.preload.tsx b/ts/state/smart/StandaloneRegistration.preload.tsx index 8309ba50ba..161900541d 100644 --- a/ts/state/smart/StandaloneRegistration.preload.tsx +++ b/ts/state/smart/StandaloneRegistration.preload.tsx @@ -4,6 +4,7 @@ import { memo } from 'react'; import { useSelector } from 'react-redux'; +import OS from '../../util/os/osMain.node.ts'; import { getDefaultAvatars } from '../../types/Avatar.std.ts'; import { useConversationsActions } from '../ducks/conversations.preload.ts'; import { useStandaloneInstallerActions } from '../ducks/standaloneInstaller.preload.ts'; @@ -18,6 +19,8 @@ import { StandaloneRegistration } from '../../components/standaloneRegistration/ import { trigger } from '../../shims/events.dom.ts'; import { getCountryDataForLocale } from '../../util/getCountryData.dom.ts'; import { RegistrationStage } from '../../types/StandaloneRegistration.std.ts'; +import { getUpdatesState } from '../selectors/updates.std.ts'; +import { useUpdatesActions } from '../ducks/updates.preload.ts'; export const SmartStandaloneRegistration = memo( function SmartStandaloneRegistration() { @@ -29,6 +32,7 @@ export const SmartStandaloneRegistration = memo( finishProfileEntryStage, goToAccountLockedStage, goToCreatePINStage, + goToUpdateRequiredStage, moveToCaptchaStage, moveToVerificationStage, openBrowserForCaptcha, @@ -42,6 +46,14 @@ export const SmartStandaloneRegistration = memo( replaceAvatar: cachedReplaceAvatar, saveAvatarToDisk: cachedSaveAvatarToDisk, } = useStandaloneInstallerActions(); + const updates = useSelector(getUpdatesState); + const { forceCheck, startUpdate } = useUpdatesActions(); + + const currentVersion = window.getVersion(); + const kickOffForcedUpgrade = () => { + forceCheck(); + goToUpdateRequiredStage(); + }; const i18n = useSelector(getIntl); const countries = getCountryDataForLocale(i18n.getLocale()); @@ -92,6 +104,13 @@ export const SmartStandaloneRegistration = memo( submitVerificationCode={submitVerificationCode} verifyPIN={verifyPIN} workflow={workflow} + // Updates + currentVersion={currentVersion} + forceCheck={forceCheck} + kickOffForcedUpgrade={kickOffForcedUpgrade} + OS={OS.getName()} + startUpdate={startUpdate} + updates={updates} // AvatarEditor support deleteAvatarFromDisk={ useCachedAvatarFunctions diff --git a/ts/types/StandaloneRegistration.std.ts b/ts/types/StandaloneRegistration.std.ts index 57979cda23..be91dc4967 100644 --- a/ts/types/StandaloneRegistration.std.ts +++ b/ts/types/StandaloneRegistration.std.ts @@ -26,6 +26,7 @@ export enum RegistrationStage { CREATE_PIN = 'CREATE_PIN', CREATE_PIN_CONFIRM = 'CREATE_PIN_CONFIRM', ACCOUNT_LOCKED = 'ACCOUNT_LOCKED', + UPDATE_REQUIRED = 'UPDATE_REQUIRED', } export const StageOrder: Record = { @@ -37,6 +38,7 @@ export const StageOrder: Record = { [RegistrationStage.CREATE_PIN]: 5, [RegistrationStage.CREATE_PIN_CONFIRM]: 6, [RegistrationStage.ACCOUNT_LOCKED]: 7, + [RegistrationStage.UPDATE_REQUIRED]: 8, }; // A few special-cases are always allowed: @@ -44,6 +46,7 @@ export const StageOrder: Record = { // 2. You can start from the initial PHONE_NUMBER stage, as well as these three // for fixing partial registrations: PROFILE_ENTRY, VERIFY_PIN, CREATE_PIN // 3. You can always make an update to the existing stage +// 4. You can always go to the UPDATE_REQUIRED stage export const ValidNextStages: Record< RegistrationStage, Set @@ -69,6 +72,7 @@ export const ValidNextStages: Record< RegistrationStage.CREATE_PIN, // so we can go back ]), [RegistrationStage.ACCOUNT_LOCKED]: new Set([]), + [RegistrationStage.UPDATE_REQUIRED]: new Set([]), }; // There's no 'complete' stage, just a check we do when we are done @@ -393,6 +397,18 @@ export type AccountLockedStage = { stage: RegistrationStage.ACCOUNT_LOCKED; }; +export type UpdateRequiredStage = { + // Server has told us that we need to update Signal Desktop before we move forward. + // Prerequisites: + // - a new version is available to download + // - the user could potentially choose to do this even if not required + // Behaviors: + // - allow user to download the new version and kick off the install + // - show any errors that happen during the process + // - restart the app to finish the installation + stage: RegistrationStage.UPDATE_REQUIRED; +}; + export type RegistrationWorkflow = | PhoneNumberStage | CaptchaStage @@ -401,4 +417,5 @@ export type RegistrationWorkflow = | VerifyPINStage | CreatePINStage | CreatePINConfirmStage - | AccountLockedStage; + | AccountLockedStage + | UpdateRequiredStage; diff --git a/ts/updater/common.main.ts b/ts/updater/common.main.ts index dcaa2e69f8..748b6d9a8c 100644 --- a/ts/updater/common.main.ts +++ b/ts/updater/common.main.ts @@ -120,9 +120,10 @@ export type UpdaterOptionsType = Readonly<{ sql: MainSQL; }>; -enum CheckType { +export enum CheckType { Normal = 'Normal', AllowSameVersion = 'AllowSameVersion', + ForceCheck = 'ForceCheck', ForceDownload = 'ForceDownload', } @@ -184,7 +185,7 @@ export abstract class Updater { 50 ); - ipcMain.handle('updater/force-update', () => this.force()); + ipcMain.handle('updater/force-check', () => this.forceCheck()); } // @@ -196,6 +197,11 @@ export abstract class Updater { return this.#checkForUpdatesMaybeInstall(CheckType.ForceDownload); } + public async forceCheck(): Promise { + this.#markedCannotUpdate = false; + await this.#checkForUpdatesMaybeInstall(CheckType.ForceCheck); + } + // If the updater was about to restart the app but the user canceled it, show dialog // to let them retry the restart public onRestartCanceled(): void { @@ -228,7 +234,8 @@ export abstract class Updater { protected abstract installUpdate( updateFilePath: string, - isSilent: boolean + isSilent: boolean, + checkType: CheckType ): Promise<() => Promise>; // For Mac App Store @@ -245,11 +252,15 @@ export abstract class Updater { ipcMain.handleOnce('start-update', performUpdateCallback); } - protected checkSystemRequirements(vendor: JSONVendorSchema): boolean { + protected checkSystemRequirements( + vendor: JSONVendorSchema, + checkType: CheckType + ): boolean { if (vendor.requireManualUpdate === 'true') { this.logger.warn('checkSystemRequirements: manual update required'); this.markCannotUpdate( new Error('yaml file has requireManualUpdate flag'), + checkType, DialogType.Cannot_Update_Require_Manual ); return false; @@ -262,6 +273,7 @@ export abstract class Updater { ); this.markCannotUpdate( new Error('yaml file has unsatisfied minOSVersion value'), + checkType, DialogType.UnsupportedOS ); return false; @@ -272,6 +284,7 @@ export abstract class Updater { protected markCannotUpdate( error: Error, + checkType: CheckType, dialogType = DialogType.Cannot_Update ): void { if (this.#markedCannotUpdate) { @@ -296,7 +309,7 @@ export abstract class Updater { this.logger.info('markCannotUpdate: retrying after user action'); this.#markedCannotUpdate = false; - await this.#checkForUpdatesMaybeInstall(CheckType.Normal); + await this.#checkForUpdatesMaybeInstall(checkType); }); } @@ -350,14 +363,19 @@ export abstract class Updater { async #downloadAndInstall( updateInfo: UpdateInformationType, - mode: DownloadMode + mode: DownloadMode, + checkType: CheckType ): Promise { if (this.#activeDownload) { return this.#activeDownload; } try { - this.#activeDownload = this.#doDownloadAndInstall(updateInfo, mode); + this.#activeDownload = this.#doDownloadAndInstall( + updateInfo, + mode, + checkType + ); return await this.#activeDownload; } finally { @@ -367,7 +385,8 @@ export abstract class Updater { async #doDownloadAndInstall( updateInfo: UpdateInformationType, - mode: DownloadMode + mode: DownloadMode, + checkType: CheckType ): Promise { const { logger } = this; @@ -439,7 +458,11 @@ export abstract class Updater { updateInfo.vendor?.requireUserConfirmation !== 'true' && this.#canRunSilently(); - const handler = await this.installUpdate(updateFilePath, isSilent); + const handler = await this.installUpdate( + updateFilePath, + isSilent, + checkType + ); if (isSilent || mode === DownloadMode.ForceUpdate) { await handler(); } else { @@ -471,15 +494,16 @@ export abstract class Updater { logger.error( `downloadAndInstall: fatal error ${Errors.toLogFormat(error)}` ); - this.markCannotUpdate(error); + this.markCannotUpdate(error, checkType); throw error; } } async #checkForUpdatesMaybeInstall(checkType: CheckType): Promise { + const logId = `checkForUpdatesMaybeInstall/${checkType}`; const { logger } = this; - logger.info('checkForUpdatesMaybeInstall: checking for update...'); + logger.info(`${logId}: checking for update...`); const updateInfo = await this.#checkForUpdates(checkType); if (!updateInfo) { return; @@ -488,11 +512,15 @@ export abstract class Updater { const { version: newVersion } = updateInfo; if (checkType === CheckType.ForceDownload) { - await this.#downloadAndInstall(updateInfo, DownloadMode.ForceUpdate); + await this.#downloadAndInstall( + updateInfo, + DownloadMode.ForceUpdate, + checkType + ); return; } - if (checkType === CheckType.Normal) { + if (checkType === CheckType.Normal || checkType === CheckType.ForceCheck) { // Verify that the downloaded version is greater than downloaded if (this.version && !gt(newVersion, this.version)) { return; @@ -507,8 +535,12 @@ export abstract class Updater { } const autoDownloadUpdates = await this.#getAutoDownloadUpdateSetting(); - if (autoDownloadUpdates) { - await this.#downloadAndInstall(updateInfo, DownloadMode.Automatic); + if (autoDownloadUpdates && checkType !== CheckType.ForceCheck) { + await this.#downloadAndInstall( + updateInfo, + DownloadMode.Automatic, + checkType + ); return; } @@ -517,20 +549,25 @@ export abstract class Updater { mode = DownloadMode.DifferentialOnly; } - await this.#offerUpdate(updateInfo, mode, 0); + await this.#offerUpdate(updateInfo, mode, 0, checkType); } async #offerUpdate( updateInfo: UpdateInformationType, mode: DownloadMode, - attempt: number + attempt: number, + checkType: CheckType ): Promise { const { logger } = this; this.setUpdateListener(async () => { logger.info('offerUpdate: have not downloaded update, going to download'); - const didDownload = await this.#downloadAndInstall(updateInfo, mode); + const didDownload = await this.#downloadAndInstall( + updateInfo, + mode, + checkType + ); if (!didDownload && mode === DownloadMode.DifferentialOnly) { this.logger.warn( 'offerUpdate: Failed to download differential update, offering full' @@ -539,7 +576,8 @@ export abstract class Updater { return this.#offerUpdate( updateInfo, DownloadMode.FullOnly, - attempt + 1 + attempt + 1, + checkType ); } @@ -578,10 +616,14 @@ export abstract class Updater { async #checkForUpdates( checkType: CheckType ): Promise { + const logId = `checkForUpdates/${checkType}`; if (isNotUpdatable(packageJson.version)) { this.logger.info( - 'checkForUpdates: not checking for updates, this is not an updatable build' + `${logId}: not checking for updates, this is not an updatable build` ); + if (checkType === CheckType.ForceCheck) { + throw new Error(`${logId}: Not an updatabale build!`); + } return; } @@ -589,31 +631,38 @@ export abstract class Updater { const parsedYaml = parseYaml(yaml); const { vendor } = parsedYaml; - if (vendor && !this.checkSystemRequirements(vendor)) { + if (vendor && !this.checkSystemRequirements(vendor, checkType)) { return; } const version = getVersion(parsedYaml); if (!version) { - this.logger.warn( - 'checkForUpdates: no version extracted from downloaded yaml' - ); - - return; - } - - if (checkType === CheckType.Normal && !isVersionNewer(version)) { - this.logger.info( - `checkForUpdates: ${version} is not newer than ${packageJson.version}; ` + - 'no new update available' - ); + this.logger.warn(`${logId}: no version extracted from downloaded yaml`); + if (checkType === CheckType.ForceCheck) { + throw new Error(`${logId}: No version extracted!`); + } return; } if ( - checkType === CheckType.Normal && + (checkType === CheckType.Normal || checkType === CheckType.ForceCheck) && + !isVersionNewer(version) + ) { + this.logger.info( + `${logId}: ${version} is not newer than ${packageJson.version}; ` + + 'no new update available' + ); + if (checkType === CheckType.ForceCheck) { + throw new Error(`${logId}: No newer version available!`); + } + + return; + } + + if ( + (checkType === CheckType.Normal || checkType === CheckType.ForceCheck) && this.handleUpdateFromThirdParty(version) ) { return; @@ -639,10 +688,7 @@ export abstract class Updater { } } - this.logger.info( - `checkForUpdates: found newer version ${version} ` + - `checkType=${checkType}` - ); + this.logger.info(`${logId}: found newer version ${version}`); const fileName = getUpdateFileName( parsedYaml, @@ -659,9 +705,7 @@ export abstract class Updater { let differentialData: DifferentialDownloadDataType | undefined; if (latestInstaller) { - this.logger.info( - `checkForUpdates: Found local installer ${latestInstaller}` - ); + this.logger.info(`${logId}: Found local installer ${latestInstaller}`); const diffOptions = { oldFile: latestInstaller, @@ -673,7 +717,7 @@ export abstract class Updater { this.cachedDifferentialData && isValidDifferentialData(this.cachedDifferentialData, diffOptions) ) { - this.logger.info('checkForUpdates: using cached differential data'); + this.logger.info(`${logId}: using cached differential data`); differentialData = this.cachedDifferentialData; } else { @@ -683,12 +727,12 @@ export abstract class Updater { this.cachedDifferentialData = differentialData; this.logger.info( - 'checkForUpdates: differential download size', + `${logId}: differential download size`, differentialData.downloadSize ); } catch (error) { this.logger.error( - 'checkForUpdates: Failed to prepare differential update', + `${logId}: Failed to prepare differential update`, Errors.toLogFormat(error) ); this.cachedDifferentialData = undefined; diff --git a/ts/updater/linuxAppImage.main.ts b/ts/updater/linuxAppImage.main.ts index ac7bb7e96c..f639ddaf80 100644 --- a/ts/updater/linuxAppImage.main.ts +++ b/ts/updater/linuxAppImage.main.ts @@ -8,12 +8,14 @@ import config from 'config'; import { app } from 'electron'; import { coerce, lt } from 'semver'; -import type { JSONVendorSchema } from './common.main.ts'; import { Updater } from './common.main.ts'; import { appRelaunch } from '../util/relaunch.main.ts'; import { hexToBinary } from './signature.node.ts'; import { DialogType } from '../types/Dialogs.std.ts'; +import type { JSONVendorSchema } from './common.main.ts'; +import type { CheckType } from './common.main.ts'; + export class LinuxAppImageUpdater extends Updater { #installing = false; @@ -22,7 +24,9 @@ export class LinuxAppImageUpdater extends Updater { } protected async installUpdate( - updateFilePath: string + updateFilePath: string, + _isSilent: boolean, + checkType: CheckType ): Promise<() => Promise> { const { logger } = this; @@ -32,7 +36,7 @@ export class LinuxAppImageUpdater extends Updater { await this.#install(updateFilePath); this.#installing = true; } catch (error) { - this.markCannotUpdate(error); + this.markCannotUpdate(error, checkType); throw error; } @@ -79,7 +83,10 @@ export class LinuxAppImageUpdater extends Updater { await chmod(appImageFile, 0o700); } - override checkSystemRequirements(vendor: JSONVendorSchema): boolean { + override checkSystemRequirements( + vendor: JSONVendorSchema, + checkType: CheckType + ): boolean { const { minGlibcVersion } = vendor; if (minGlibcVersion) { const parsedMinGlibcVersion = coerce(minGlibcVersion); @@ -103,6 +110,7 @@ export class LinuxAppImageUpdater extends Updater { ); this.markCannotUpdate( new Error('system glibc version missing or unparseable'), + checkType, DialogType.UnsupportedOS ); return false; @@ -115,6 +123,7 @@ export class LinuxAppImageUpdater extends Updater { ); this.markCannotUpdate( new Error('yaml file has unsatisfied minGlibcVersion value'), + checkType, DialogType.UnsupportedOS ); return false; diff --git a/ts/updater/macos.main.ts b/ts/updater/macos.main.ts index aa18306ff8..f149155e6d 100644 --- a/ts/updater/macos.main.ts +++ b/ts/updater/macos.main.ts @@ -12,6 +12,8 @@ import { isProduction } from '../util/version.std.ts'; import * as Errors from '../types/errors.std.ts'; import { DialogType } from '../types/Dialogs.std.ts'; +import type { CheckType } from './common.main.ts'; + const APP_ID = '1230208093'; export class MacOSUpdater extends Updater { @@ -20,7 +22,9 @@ export class MacOSUpdater extends Updater { } protected async installUpdate( - updateFilePath: string + updateFilePath: string, + _isSilent: boolean, + checkType: CheckType ): Promise<() => Promise> { const { logger } = this; @@ -32,6 +36,7 @@ export class MacOSUpdater extends Updater { const message: string = error.message || ''; this.markCannotUpdate( error, + checkType, message.includes(readOnly) ? DialogType.MacOS_Read_Only : DialogType.Cannot_Update diff --git a/ts/updater/windows.main.ts b/ts/updater/windows.main.ts index d223774b6c..4b1a42b01d 100644 --- a/ts/updater/windows.main.ts +++ b/ts/updater/windows.main.ts @@ -3,11 +3,13 @@ import { join } from 'node:path'; import type { SpawnOptions } from 'node:child_process'; + import { spawn as spawnEmitter } from 'node:child_process'; import { readdir, unlink } from 'node:fs/promises'; import { app } from 'electron'; import { getAppRootDir } from '../util/appRootDir.main.ts'; import { Updater } from './common.main.ts'; +import type { CheckType } from './common.main.ts'; const IS_EXE = /\.exe$/i; @@ -40,7 +42,8 @@ export class WindowsUpdater extends Updater { } protected async installUpdate( updateFilePath: string, - isSilent: boolean + isSilent: boolean, + checkType: CheckType ): Promise<() => Promise> { const { logger } = this; @@ -50,7 +53,7 @@ export class WindowsUpdater extends Updater { await this.#install(updateFilePath, isSilent); this.#installing = true; } catch (error) { - this.markCannotUpdate(error); + this.markCannotUpdate(error, checkType); throw error; }