Add recovery key changed modal

This commit is contained in:
trevor-signal
2026-03-27 15:29:13 -04:00
committed by GitHub
parent f1f2055058
commit cb8886ebd9
14 changed files with 231 additions and 93 deletions

View File

@@ -8586,6 +8586,18 @@
"messageformat": "Other ways to back up",
"description": "Heading on the backups settings view for alternative backup methods such as on-device backups."
},
"icu:Preferences__recovery-key-updated__title": {
"messageformat": "Your recovery key has changed",
"description": "Title in modal warning the user that their recovery key (backup key) has been changed"
},
"icu:Preferences__recovery-key-updated__description": {
"messageformat": "Your recovery key has been updated. Any new backups you make can only be restored using your new recovery key.",
"description": "Text in modal warning the user that their recovery key (backup key) has been changed"
},
"icu:Preferences__recovery-key-updated__view-key": {
"messageformat": "View new key",
"description": "Button text to view the new updated recovery key"
},
"icu:Preferences--blocked-count": {
"messageformat": "{num, plural, one {# contact} other {# contacts}}",
"description": "Number of contacts blocked plural"

View File

@@ -75,7 +75,7 @@ export namespace AxoSymbol {
*/
export type IconName = AxoSymbolIconName;
export type IconSize = 12 | 14 | 16 | 18 | 20 | 24 | 48;
export type IconSize = 12 | 14 | 16 | 18 | 20 | 24 | 36 | 48;
type IconSizeConfig = { size: number; fontSize: number };
@@ -86,6 +86,7 @@ export namespace AxoSymbol {
18: { size: 18, fontSize: 16 },
20: { size: 20, fontSize: 18 },
24: { size: 24, fontSize: 22 },
36: { size: 36, fontSize: 34 },
48: { size: 48, fontSize: 44 },
};

View File

@@ -286,6 +286,7 @@ import { itemStorage } from './textsecure/Storage.preload.js';
import { initMessageCleanup } from './services/messageStateCleanup.dom.js';
import { MessageCache } from './services/MessageCache.preload.js';
import { saveAndNotify } from './messages/saveAndNotify.preload.js';
import { getBackupKeyHash } from './services/backups/crypto.preload.js';
const { isNumber, throttle } = lodash;
@@ -1025,6 +1026,20 @@ export async function startApp(): Promise<void> {
await itemStorage.remove('callQualitySurveyCooldownDisabled');
await itemStorage.remove('localDeleteWarningShown');
}
if (
itemStorage.get('backupKeyViewed') === true &&
itemStorage.get('backupKeyViewedHash') == null
) {
const backupKey = itemStorage.get('accountEntropyPool');
if (backupKey) {
await itemStorage.put(
'backupKeyViewedHash',
getBackupKeyHash(backupKey)
);
}
await itemStorage.remove('backupKeyViewed');
}
}
setAppLoadingScreenMessage(i18n('icu:optimizingApplication'), i18n);

View File

@@ -389,8 +389,9 @@ export default {
component: Preferences,
args: {
i18n,
accountEntropyPool:
backupKey:
'uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t',
backupKeyHash: 'backupkeyhash',
autoDownloadAttachment: {
photos: true,
videos: false,
@@ -419,7 +420,6 @@ export default {
availableMicrophones,
availableSpeakers,
backupFreeMediaDays: 45,
backupKeyViewed: false,
backupLocalBackupsEnabled: false,
backupSubscriptionStatus: { status: 'not-found' },
backupTier: null,
@@ -492,6 +492,7 @@ export default {
},
preferredSystemLocales: ['en'],
preferredWidthFromStorage: 300,
previouslyViewedBackupKeyHash: 'hash',
resolvedLocale: 'en',
selectedCamera:
'dfbe6effe70b0611ba0fdc2a9ea3f39f6cb110e6687948f7e5f016c111b7329c',
@@ -557,7 +558,7 @@ export default {
onAutoDownloadAttachmentChange: action('onAutoDownloadAttachmentChange'),
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
onAutoLaunchChange: action('onAutoLaunchChange'),
onBackupKeyViewedChange: action('onBackupKeyViewedChange'),
onBackupKeyViewed: action('onBackupKeyViewed'),
onCallNotificationsChange: action('onCallNotificationsChange'),
onCallRingtoneNotificationChange: action(
'onCallRingtoneNotificationChange'
@@ -1253,7 +1254,7 @@ export const LocalBackups = Template.bind({});
LocalBackups.args = {
settingsLocation: { page: SettingsPage.LocalBackups },
backupLocalBackupsEnabled: true,
backupKeyViewed: true,
previouslyViewedBackupKeyHash: 'hash',
lastLocalBackup: {
timestamp: Date.now() - DAY,
backupsFolder: 'backups',
@@ -1266,20 +1267,20 @@ export const LocalBackupsNeverBackedUp = Template.bind({});
LocalBackupsNeverBackedUp.args = {
settingsLocation: { page: SettingsPage.LocalBackups },
backupLocalBackupsEnabled: true,
backupKeyViewed: true,
previouslyViewedBackupKeyHash: 'hash',
lastLocalBackup: undefined,
localBackupFolder: '/home/signaluser/Signal Backups/',
};
export const LocalBackupsSetupChooseFolder = Template.bind({});
LocalBackupsSetupChooseFolder.args = {
settingsLocation: { page: SettingsPage.LocalBackupsSetupFolder },
settingsLocation: { page: SettingsPage.LocalBackups },
backupLocalBackupsEnabled: true,
};
export const LocalBackupsSetupViewBackupKey = Template.bind({});
LocalBackupsSetupViewBackupKey.args = {
settingsLocation: { page: SettingsPage.LocalBackupsSetupKey },
settingsLocation: { page: SettingsPage.LocalBackups },
backupLocalBackupsEnabled: true,
localBackupFolder: '/home/signaluser/Signal Backups/',
};

View File

@@ -111,10 +111,11 @@ type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
export type PropsDataType = {
// Settings
accountEntropyPool: string | undefined;
backupKey: string | undefined;
backupKeyHash: string | undefined;
autoDownloadAttachment: AutoDownloadAttachmentType;
backupFreeMediaDays: number;
backupKeyViewed: boolean;
previouslyViewedBackupKeyHash: string | undefined;
backupLocalBackupsEnabled: boolean;
backupTier: BackupLevel | null;
lastLocalBackup: LocalBackupExportMetadata | undefined;
@@ -313,7 +314,7 @@ type PropsFunctionType = {
) => unknown;
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
onAutoLaunchChange: CheckboxChangeHandlerType;
onBackupKeyViewedChange: (keyViewed: boolean) => void;
onBackupKeyViewed: ({ backupKeyHash }: { backupKeyHash: string }) => void;
onCallNotificationsChange: CheckboxChangeHandlerType;
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
onContentProtectionChange: CheckboxChangeHandlerType;
@@ -400,7 +401,6 @@ const DEFAULT_ZOOM_FACTORS = [
];
export function Preferences({
accountEntropyPool,
addCustomColor,
autoDownloadAttachment,
availableCameras,
@@ -412,7 +412,8 @@ export function Preferences({
resumeBackupMediaDownload,
cancelBackupMediaDownload,
backupFreeMediaDays,
backupKeyViewed,
backupKey,
backupKeyHash,
backupTier,
backupSubscriptionStatus,
backupLocalBackupsEnabled,
@@ -485,7 +486,7 @@ export function Preferences({
onAutoDownloadAttachmentChange,
onAutoDownloadUpdateChange,
onAutoLaunchChange,
onBackupKeyViewedChange,
onBackupKeyViewed,
onCallNotificationsChange,
onCallRingtoneNotificationChange,
onContentProtectionChange,
@@ -539,6 +540,7 @@ export function Preferences({
renderPreferencesEditChatFolderPage,
openFileInFolder,
osName,
previouslyViewedBackupKeyHash,
promptOSAuth,
resetAllChatColors,
resetDefaultChatColor,
@@ -2275,7 +2277,6 @@ export function Preferences({
pageTitle = i18n('icu:Preferences__local-backups');
}
// Local backups setup page titles intentionally left blank
let backPage: PreferencesBackupPage | undefined;
if (settingsLocation.page === SettingsPage.LocalBackupsKeyReference) {
backPage = SettingsPage.LocalBackups;
@@ -2295,9 +2296,9 @@ export function Preferences({
}
const pageContents = (
<PreferencesBackups
accountEntropyPool={accountEntropyPool}
backupKey={backupKey}
backupKeyHash={backupKeyHash}
backupFreeMediaDays={backupFreeMediaDays}
backupKeyViewed={backupKeyViewed}
backupTier={backupTier}
backupSubscriptionStatus={backupSubscriptionStatus}
backupMediaDownloadStatus={backupMediaDownloadStatus}
@@ -2310,10 +2311,11 @@ export function Preferences({
lastLocalBackup={lastLocalBackup}
locale={resolvedLocale}
localBackupFolder={localBackupFolder}
onBackupKeyViewedChange={onBackupKeyViewedChange}
onBackupKeyViewed={onBackupKeyViewed}
openFileInFolder={openFileInFolder}
osName={osName}
pickLocalBackupFolder={pickLocalBackupFolder}
previouslyViewedBackupKeyHash={previouslyViewedBackupKeyHash}
disableLocalBackups={disableLocalBackups}
settingsLocation={settingsLocation}
promptOSAuth={promptOSAuth}

View File

@@ -39,8 +39,6 @@ export const SIGNAL_BACKUPS_LEARN_MORE_URL =
const LOCAL_BACKUPS_PAGES = new Set([
SettingsPage.LocalBackups,
SettingsPage.LocalBackupsKeyReference,
SettingsPage.LocalBackupsSetupFolder,
SettingsPage.LocalBackupsSetupKey,
]);
function isLocalBackupsPage(page: SettingsPage) {
@@ -48,9 +46,9 @@ function isLocalBackupsPage(page: SettingsPage) {
}
export function PreferencesBackups({
accountEntropyPool,
backupKey,
backupKeyHash,
backupFreeMediaDays,
backupKeyViewed,
backupSubscriptionStatus,
backupTier,
cloudBackupStatus,
@@ -59,7 +57,7 @@ export function PreferencesBackups({
lastLocalBackup,
locale,
localBackupFolder,
onBackupKeyViewedChange,
onBackupKeyViewed,
openFileInFolder,
osName,
pickLocalBackupFolder,
@@ -69,6 +67,7 @@ export function PreferencesBackups({
pauseBackupMediaDownload,
resumeBackupMediaDownload,
settingsLocation,
previouslyViewedBackupKeyHash,
promptOSAuth,
refreshCloudBackupStatus,
refreshBackupSubscriptionStatus,
@@ -76,9 +75,9 @@ export function PreferencesBackups({
showToast,
startLocalBackupExport,
}: {
accountEntropyPool: string | undefined;
backupFreeMediaDays: number;
backupKeyViewed: boolean;
backupKey: string | undefined;
backupKeyHash: string | undefined;
backupSubscriptionStatus: BackupsSubscriptionType;
backupTier: BackupLevel | null;
cloudBackupStatus?: BackupStatusType;
@@ -87,7 +86,7 @@ export function PreferencesBackups({
isLocalBackupsEnabled: boolean;
lastLocalBackup: LocalBackupExportMetadata | undefined;
locale: string;
onBackupKeyViewedChange: (keyViewed: boolean) => void;
onBackupKeyViewed: ({ backupKeyHash }: { backupKeyHash: string }) => void;
openFileInFolder: (path: string) => void;
osName: 'linux' | 'macos' | 'windows' | undefined;
settingsLocation: SettingsLocation;
@@ -101,6 +100,7 @@ export function PreferencesBackups({
pauseBackupMediaDownload: () => void;
resumeBackupMediaDownload: () => void;
pickLocalBackupFolder: () => Promise<string | undefined>;
previouslyViewedBackupKeyHash: string | undefined;
promptOSAuth: (
reason: PromptOSAuthReasonType
) => Promise<PromptOSAuthResultType>;
@@ -152,19 +152,25 @@ export function PreferencesBackups({
}
if (isLocalBackupsPage(settingsLocation.page)) {
if (!backupKey || !backupKeyHash) {
setSettingsLocation({ page: SettingsPage.Backups });
return null;
}
return (
<PreferencesLocalBackups
accountEntropyPool={accountEntropyPool}
backupKeyViewed={backupKeyViewed}
backupKey={backupKey}
backupKeyHash={backupKeyHash}
i18n={i18n}
lastLocalBackup={lastLocalBackup}
localBackupFolder={localBackupFolder}
onBackupKeyViewedChange={onBackupKeyViewedChange}
onBackupKeyViewed={onBackupKeyViewed}
openFileInFolder={openFileInFolder}
osName={osName}
settingsLocation={settingsLocation}
pickLocalBackupFolder={pickLocalBackupFolder}
disableLocalBackups={disableLocalBackups}
previouslyViewedBackupKeyHash={previouslyViewedBackupKeyHash}
promptOSAuth={promptOSAuth}
setSettingsLocation={setSettingsLocation}
showToast={showToast}
@@ -179,7 +185,8 @@ export function PreferencesBackups({
</a>
);
const isLocalBackupsSetup = localBackupFolder && backupKeyViewed;
const isLocalBackupsSetup =
localBackupFolder != null && previouslyViewedBackupKeyHash != null;
function renderRemoteBackups() {
return (

View File

@@ -1,7 +1,7 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChangeEvent } from 'react';
import type { ChangeEvent, JSX } from 'react';
import React, {
useCallback,
useEffect,
@@ -24,7 +24,6 @@ import { SettingsPage } from '../types/Nav.std.js';
import { ToastType } from '../types/Toast.dom.js';
import type { ShowToastAction } from '../state/ducks/toast.preload.js';
import { Modal } from './Modal.dom.js';
import { strictAssert } from '../util/assert.std.js';
import type {
PromptOSAuthReasonType,
PromptOSAuthResultType,
@@ -39,29 +38,31 @@ import { tw } from '../axo/tw.dom.js';
import { createLogger } from '../logging/log.std.js';
import { toLogFormat } from '../types/errors.std.js';
import { AxoAlertDialog } from '../axo/AxoAlertDialog.dom.js';
import { AxoSymbol } from '../axo/AxoSymbol.dom.js';
const { noop } = lodash;
const log = createLogger('PreferencesLocalBackups');
export function PreferencesLocalBackups({
accountEntropyPool,
backupKeyViewed,
backupKey,
backupKeyHash,
disableLocalBackups,
i18n,
lastLocalBackup,
localBackupFolder,
openFileInFolder,
osName,
onBackupKeyViewedChange,
onBackupKeyViewed,
settingsLocation,
pickLocalBackupFolder,
previouslyViewedBackupKeyHash,
promptOSAuth,
setSettingsLocation,
showToast,
startLocalBackupExport,
}: {
accountEntropyPool: string | undefined;
backupKeyViewed: boolean;
backupKey: string;
backupKeyHash: string;
disableLocalBackups: ({
deleteExistingBackups,
}: {
@@ -70,22 +71,25 @@ export function PreferencesLocalBackups({
i18n: LocalizerType;
lastLocalBackup: LocalBackupExportMetadata | undefined;
localBackupFolder: string | undefined;
onBackupKeyViewedChange: (keyViewed: boolean) => void;
onBackupKeyViewed: ({ backupKeyHash }: { backupKeyHash: string }) => void;
openFileInFolder: (path: string) => void;
osName: 'linux' | 'macos' | 'windows' | undefined;
settingsLocation: SettingsLocation;
pickLocalBackupFolder: () => Promise<string | undefined>;
previouslyViewedBackupKeyHash: string | undefined;
promptOSAuth: (
reason: PromptOSAuthReasonType
) => Promise<PromptOSAuthResultType>;
setSettingsLocation: (settingsLocation: SettingsLocation) => void;
showToast: ShowToastAction;
startLocalBackupExport: () => void;
}): React.JSX.Element {
}): React.JSX.Element | null {
const [authError, setAuthError] =
React.useState<Omit<PromptOSAuthResultType, 'success'>>();
const [isAuthPending, setIsAuthPending] = useState<boolean>(false);
const [isDisablePending, setIsDisablePending] = useState<boolean>(false);
const [isShowingBackupKeyChangedModal, setIsShowingBackupKeyChangedModal] =
useState<boolean>(false);
if (!localBackupFolder) {
return (
@@ -98,28 +102,44 @@ export function PreferencesLocalBackups({
const isReferencingBackupKey =
settingsLocation.page === SettingsPage.LocalBackupsKeyReference;
if (!backupKeyViewed || isReferencingBackupKey) {
strictAssert(accountEntropyPool, 'AEP is required for backup key viewer');
if (!previouslyViewedBackupKeyHash || isReferencingBackupKey) {
return (
<LocalBackupsBackupKeyViewer
accountEntropyPool={accountEntropyPool}
backupKey={backupKey}
i18n={i18n}
isReferencing={isReferencingBackupKey}
isReferencing={
isReferencingBackupKey &&
previouslyViewedBackupKeyHash === backupKeyHash
}
onBackupKeyViewed={() => {
if (backupKeyViewed) {
setSettingsLocation({
page: SettingsPage.LocalBackups,
});
} else {
onBackupKeyViewedChange(true);
}
onBackupKeyViewed({ backupKeyHash });
setSettingsLocation({
page: SettingsPage.LocalBackups,
});
}}
showToast={showToast}
/>
);
}
async function showKeyReferenceWithAuth() {
setAuthError(undefined);
try {
setIsAuthPending(true);
const result = await promptOSAuth('view-aep');
if (result === 'success' || result === 'unsupported') {
setSettingsLocation({
page: SettingsPage.LocalBackupsKeyReference,
});
} else {
setAuthError(result);
}
} finally {
setIsAuthPending(false);
}
}
const learnMoreLink = (parts: Array<string | React.JSX.Element>) => (
<a href={SIGNAL_BACKUPS_LEARN_MORE_URL} rel="noreferrer" target="_blank">
{parts}
@@ -170,7 +190,16 @@ export function PreferencesLocalBackups({
<AxoButton.Root
variant="secondary"
size="lg"
onClick={startLocalBackupExport}
onClick={async () => {
if (
!previouslyViewedBackupKeyHash ||
previouslyViewedBackupKeyHash !== backupKeyHash
) {
setIsShowingBackupKeyChangedModal(true);
} else {
startLocalBackupExport();
}
}}
>
{i18n('icu:Preferences__local-backups-backup-now')}
</AxoButton.Root>
@@ -225,20 +254,13 @@ export function PreferencesLocalBackups({
isAuthPending ? { 'aria-label': i18n('icu:loading') } : null
}
onClick={async () => {
setAuthError(undefined);
try {
setIsAuthPending(true);
const result = await promptOSAuth('view-aep');
if (result === 'success' || result === 'unsupported') {
setSettingsLocation({
page: SettingsPage.LocalBackupsKeyReference,
});
} else {
setAuthError(result);
}
} finally {
setIsAuthPending(false);
if (
!previouslyViewedBackupKeyHash ||
previouslyViewedBackupKeyHash !== backupKeyHash
) {
setIsShowingBackupKeyChangedModal(true);
} else {
await showKeyReferenceWithAuth();
}
}}
>
@@ -323,6 +345,48 @@ export function PreferencesLocalBackups({
</AxoAlertDialog.Content>
</AxoAlertDialog.Root>
) : null}
{isShowingBackupKeyChangedModal ? (
<AxoAlertDialog.Root
open
onOpenChange={open => {
if (!open) {
setIsShowingBackupKeyChangedModal(false);
}
}}
>
<div className={tw('p-4')}>
<AxoAlertDialog.Content escape="cancel-is-noop">
<AxoAlertDialog.Body>
<div className={tw('my-3 flex flex-col items-center')}>
<LocalBackupSetupIcon symbol="key" />
<AxoAlertDialog.Title>
<div className={tw('mt-3 type-title-medium')}>
{i18n('icu:Preferences__recovery-key-updated__title')}
</div>
</AxoAlertDialog.Title>
</div>
<AxoAlertDialog.Description>
<div className={tw('mb-3')}>
{i18n('icu:Preferences__recovery-key-updated__description')}
</div>
</AxoAlertDialog.Description>
</AxoAlertDialog.Body>
<AxoAlertDialog.Footer>
<AxoAlertDialog.Cancel>
{i18n('icu:cancel')}
</AxoAlertDialog.Cancel>
<AxoAlertDialog.Action
variant="primary"
onClick={showKeyReferenceWithAuth}
>
{i18n('icu:Preferences__recovery-key-updated__view-key')}
</AxoAlertDialog.Action>
</AxoAlertDialog.Footer>
</AxoAlertDialog.Content>
</div>
</AxoAlertDialog.Root>
) : null}
</>
);
}
@@ -478,13 +542,13 @@ function LocalBackupsSetupFolderPicker({
type BackupKeyStep = 'view' | 'confirm' | 'caution' | 'reference';
function LocalBackupsBackupKeyViewer({
accountEntropyPool,
backupKey,
i18n,
isReferencing,
onBackupKeyViewed,
showToast,
}: {
accountEntropyPool: string;
backupKey: string;
i18n: LocalizerType;
isReferencing: boolean;
onBackupKeyViewed: () => void;
@@ -497,20 +561,23 @@ function LocalBackupsBackupKeyViewer({
);
const isStepViewOrReference = step === 'view' || step === 'reference';
const backupKey = useMemo(() => {
return accountEntropyPool
const backupKeyForDisplay = useMemo(() => {
return backupKey
.replace(/\s/g, '')
.replace(/.{4}(?=.)/g, '$& ')
.toUpperCase();
}, [accountEntropyPool]);
}, [backupKey]);
const onCopyBackupKey = useCallback(
async function handleCopyBackupKey(e: React.MouseEvent) {
e.preventDefault();
await window.SignalClipboard.copyTextTemporarily(backupKey, 45 * SECOND);
window.SignalClipboard.copyTextTemporarily(
backupKeyForDisplay,
45 * SECOND
);
showToast({ toastType: ToastType.CopiedBackupKey });
},
[backupKey, showToast]
[backupKeyForDisplay, showToast]
);
const learnMoreLink = (parts: Array<string | React.JSX.Element>) => (
@@ -621,7 +688,7 @@ function LocalBackupsBackupKeyViewer({
<div className="Preferences--LocalBackupsSetupScreenPane">
<div className="Preferences--LocalBackupsSetupScreenPaneContent">
<LocalBackupsBackupKeyTextarea
backupKey={backupKey}
backupKey={backupKeyForDisplay}
i18n={i18n}
onValidate={(isValid: boolean) => setIsBackupKeyConfirmed(isValid)}
isStepViewOrReference={isStepViewOrReference}
@@ -726,3 +793,16 @@ function getOSAuthErrorString(
return i18n('icu:Preferences__local-backups-auth-error--unavailable');
}
function LocalBackupSetupIcon(props: { symbol: 'key' | 'lock' }): JSX.Element {
return (
<div
className={tw(
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
'inline-flex size-16 items-center justify-center rounded-full bg-[#D2DFFB] text-[#3B45FD]'
)}
>
<AxoSymbol.Icon symbol={props.symbol} size={36} label={null} />
</div>
);
}

View File

@@ -13,6 +13,7 @@ import { strictAssert } from '../../util/assert.std.js';
import type { AciString } from '../../types/ServiceId.std.js';
import { toAciObject } from '../../util/ServiceId.node.js';
import { itemStorage } from '../../textsecure/Storage.preload.js';
import { sha256 } from '../../Crypto.node.js';
const getMemoizedBackupKey = memoizee((accountEntropyPool: string) => {
return AccountEntropyPool.deriveBackupKey(accountEntropyPool);
@@ -25,6 +26,10 @@ export function getBackupKey(): BackupKey {
return getMemoizedBackupKey(accountEntropyPool);
}
export function getBackupKeyHash(backupKey: string): string {
return Buffer.from(sha256(Buffer.from(backupKey))).toString('base64');
}
export function getBackupMediaRootKey(): BackupKey {
const rootKey = itemStorage.get('backupMediaRootKey');
strictAssert(rootKey, 'Media root key not available');

View File

@@ -1468,7 +1468,7 @@ export class BackupsService {
await Promise.all([
itemStorage.remove('lastLocalBackup'),
itemStorage.remove('localBackupFolder'),
itemStorage.remove('backupKeyViewed'),
itemStorage.remove('backupKeyViewedHash'),
]);
if (deleteExistingBackups) {

View File

@@ -303,6 +303,11 @@ export const getBackupMediaDownloadProgress = createSelector(
})
);
export const getBackupKey = createSelector(
getItems,
(state: ItemsStateType) => state.accountEntropyPool
);
export const getServerAlerts = createSelector(
getItems,
(state: ItemsStateType) => state.serverAlerts ?? {}

View File

@@ -1,7 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { StrictMode, useCallback, useEffect } from 'react';
import React, { StrictMode, useCallback, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { AudioDevice } from '@signalapp/ringrtc';
@@ -15,6 +15,7 @@ import {
getOtherTabsUnreadStats,
} from '../selectors/conversations.dom.js';
import {
getBackupKey,
getCustomColors,
getItems,
getNavTabsCollapsed,
@@ -118,6 +119,7 @@ import type { ExternalProps as SmartNotificationProfilesProps } from './Preferen
import { useMegaphonesActions } from '../ducks/megaphones.preload.js';
import type { ZoomFactorType } from '../../types/StorageKeys.std.js';
import { isLocalBackupsEnabled } from '../../util/isLocalBackupsEnabled.preload.js';
import { getBackupKeyHash } from '../../services/backups/crypto.preload.js';
const DEFAULT_NOTIFICATION_SETTING = 'message';
@@ -259,6 +261,14 @@ export function SmartPreferences(): React.JSX.Element | null {
);
const { osName } = useSelector(getUser);
const backupKey = useSelector(getBackupKey);
const backupKeyHash = useMemo(() => {
if (!backupKey) {
return undefined;
}
return getBackupKeyHash(backupKey);
}, [backupKey]);
// The weird ones
const makeSyncRequest = async () => {
@@ -552,6 +562,10 @@ export function SmartPreferences(): React.JSX.Element | null {
await window.IPC.setMediaPermissions(value);
};
const onBackupKeyViewed = (args: { backupKeyHash: string }) => {
onBackupKeyViewedChange(args.backupKeyHash);
};
// Simple, one-way items
const {
@@ -622,10 +636,9 @@ export function SmartPreferences(): React.JSX.Element | null {
'auto-download-attachment',
DEFAULT_AUTO_DOWNLOAD_ATTACHMENT
);
const [backupKeyViewed, onBackupKeyViewedChange] = createItemsAccess(
'backupKeyViewed',
false
);
const [previouslyViewedBackupKeyHash, onBackupKeyViewedChange] =
createItemsAccess('backupKeyViewedHash', undefined);
const [hasAudioNotifications, onAudioNotificationsChange] = createItemsAccess(
'audio-notification',
@@ -816,20 +829,18 @@ export function SmartPreferences(): React.JSX.Element | null {
});
};
const accountEntropyPool = itemStorage.get('accountEntropyPool');
return (
<StrictMode>
<AxoProvider dir={i18n.getLocaleDirection()}>
<Preferences
accountEntropyPool={accountEntropyPool}
backupKey={backupKey}
backupKeyHash={backupKeyHash}
addCustomColor={addCustomColor}
autoDownloadAttachment={autoDownloadAttachment}
availableCameras={availableCameras}
availableLocales={availableLocales}
availableMicrophones={availableMicrophones}
availableSpeakers={availableSpeakers}
backupKeyViewed={backupKeyViewed}
backupTier={backupLevelFromNumber(backupTier)}
backupSubscriptionStatus={
backupSubscriptionStatus ?? { status: 'not-found' }
@@ -919,7 +930,7 @@ export function SmartPreferences(): React.JSX.Element | null {
onAutoDownloadAttachmentChange={onAutoDownloadAttachmentChange}
onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
onAutoLaunchChange={onAutoLaunchChange}
onBackupKeyViewedChange={onBackupKeyViewedChange}
onBackupKeyViewed={onBackupKeyViewed}
onCallNotificationsChange={onCallNotificationsChange}
onCallRingtoneNotificationChange={onCallRingtoneNotificationChange}
onContentProtectionChange={onContentProtectionChange}
@@ -981,6 +992,7 @@ export function SmartPreferences(): React.JSX.Element | null {
renderPreferencesEditChatFolderPage={
renderPreferencesEditChatFolderPage
}
previouslyViewedBackupKeyHash={previouslyViewedBackupKeyHash}
promptOSAuth={promptOSAuth}
resetAllChatColors={resetAllChatColors}
resetDefaultChatColor={resetDefaultChatColor}

View File

@@ -89,8 +89,6 @@ export enum SettingsPage {
PNP = 'PNP',
BackupsDetails = 'BackupsDetails',
LocalBackups = 'LocalBackups',
LocalBackupsSetupFolder = 'LocalBackupsSetupFolder',
LocalBackupsSetupKey = 'LocalBackupsSetupKey',
LocalBackupsKeyReference = 'LocalBackupsKeyReference',
}

View File

@@ -8,9 +8,7 @@ export type PreferencesBackupPage =
| SettingsPage.Backups
| SettingsPage.BackupsDetails
| SettingsPage.LocalBackups
| SettingsPage.LocalBackupsKeyReference
| SettingsPage.LocalBackupsSetupFolder
| SettingsPage.LocalBackupsSetupKey;
| SettingsPage.LocalBackupsKeyReference;
// Should be in sync with PreferencesBackupPage
export function isBackupPage(
@@ -20,8 +18,6 @@ export function isBackupPage(
page === SettingsPage.Backups ||
page === SettingsPage.BackupsDetails ||
page === SettingsPage.LocalBackups ||
page === SettingsPage.LocalBackupsSetupFolder ||
page === SettingsPage.LocalBackupsSetupKey ||
page === SettingsPage.LocalBackupsKeyReference
);
}

View File

@@ -242,9 +242,9 @@ export type StorageAccessType = {
cloudBackupStatus: BackupStatusType | undefined;
backupSubscriptionStatus: BackupsSubscriptionType | undefined;
backupKeyViewed: boolean;
lastLocalBackup: LocalBackupExportMetadata;
localBackupFolder: string | undefined;
backupKeyViewedHash: string | undefined;
// If true Desktop message history was restored from backup
isRestoredFromBackup: boolean;
@@ -317,6 +317,7 @@ export type StorageAccessType = {
backupMediaDownloadIdle: never;
callQualitySurveyCooldownDisabled: never;
localDeleteWarningShown: never;
backupKeyViewed: never;
};
export const STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK = [
@@ -360,6 +361,9 @@ export const STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK = [
'number_id',
'uuid_id',
'pni',
// Local backups
'backupKeyViewedHash',
] as const satisfies ReadonlyArray<keyof StorageAccessType>;
const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [