mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-17 07:13:24 +01:00
Add recovery key changed modal
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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/',
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1468,7 +1468,7 @@ export class BackupsService {
|
||||
await Promise.all([
|
||||
itemStorage.remove('lastLocalBackup'),
|
||||
itemStorage.remove('localBackupFolder'),
|
||||
itemStorage.remove('backupKeyViewed'),
|
||||
itemStorage.remove('backupKeyViewedHash'),
|
||||
]);
|
||||
|
||||
if (deleteExistingBackups) {
|
||||
|
||||
@@ -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 ?? {}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -89,8 +89,6 @@ export enum SettingsPage {
|
||||
PNP = 'PNP',
|
||||
BackupsDetails = 'BackupsDetails',
|
||||
LocalBackups = 'LocalBackups',
|
||||
LocalBackupsSetupFolder = 'LocalBackupsSetupFolder',
|
||||
LocalBackupsSetupKey = 'LocalBackupsSetupKey',
|
||||
LocalBackupsKeyReference = 'LocalBackupsKeyReference',
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user