2025-04-02 14:57:29 -04:00
|
|
|
// Copyright 2025 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import React from 'react';
|
|
|
|
import type {
|
|
|
|
BackupsSubscriptionType,
|
|
|
|
BackupStatusType,
|
|
|
|
} from '../types/backups';
|
|
|
|
import type { LocalizerType } from '../types/I18N';
|
|
|
|
import { formatTimestamp } from '../util/formatTimestamp';
|
2025-06-04 13:42:00 -07:00
|
|
|
import { SettingsControl as Control, SettingsRow } from './PreferencesUtil';
|
2025-04-02 14:57:29 -04:00
|
|
|
import { missingCaseError } from '../util/missingCaseError';
|
2025-06-04 13:42:00 -07:00
|
|
|
import { Button, ButtonVariant } from './Button';
|
|
|
|
import type { PreferencesBackupPage } from '../types/PreferencesBackupPage';
|
|
|
|
import { Page } from './Preferences';
|
|
|
|
import { I18n } from './I18n';
|
|
|
|
import { PreferencesLocalBackups } from './PreferencesLocalBackups';
|
|
|
|
import type { ShowToastAction } from '../state/ducks/toast';
|
|
|
|
|
|
|
|
export const SIGNAL_BACKUPS_LEARN_MORE_URL =
|
|
|
|
'https://support.signal.org/hc/articles/360007059752-Backup-and-Restore-Messages';
|
2025-04-02 14:57:29 -04:00
|
|
|
|
|
|
|
export function PreferencesBackups({
|
2025-06-04 13:42:00 -07:00
|
|
|
accountEntropyPool,
|
|
|
|
backupKeyViewed,
|
2025-04-02 14:57:29 -04:00
|
|
|
backupSubscriptionStatus,
|
2025-06-04 13:42:00 -07:00
|
|
|
cloudBackupStatus,
|
2025-04-02 14:57:29 -04:00
|
|
|
i18n,
|
|
|
|
locale,
|
2025-06-04 13:42:00 -07:00
|
|
|
localBackupFolder,
|
|
|
|
onBackupKeyViewedChange,
|
|
|
|
pickLocalBackupFolder,
|
|
|
|
page,
|
|
|
|
setPage,
|
|
|
|
showToast,
|
2025-04-02 14:57:29 -04:00
|
|
|
}: {
|
2025-06-04 13:42:00 -07:00
|
|
|
accountEntropyPool: string | undefined;
|
|
|
|
backupKeyViewed: boolean;
|
2025-04-02 14:57:29 -04:00
|
|
|
backupSubscriptionStatus?: BackupsSubscriptionType;
|
2025-06-04 13:42:00 -07:00
|
|
|
cloudBackupStatus?: BackupStatusType;
|
|
|
|
localBackupFolder: string | undefined;
|
2025-04-02 14:57:29 -04:00
|
|
|
i18n: LocalizerType;
|
|
|
|
locale: string;
|
2025-06-04 13:42:00 -07:00
|
|
|
onBackupKeyViewedChange: (keyViewed: boolean) => void;
|
|
|
|
page: PreferencesBackupPage;
|
|
|
|
pickLocalBackupFolder: () => Promise<string | undefined>;
|
|
|
|
setPage: (page: PreferencesBackupPage) => void;
|
|
|
|
showToast: ShowToastAction;
|
2025-04-02 14:57:29 -04:00
|
|
|
}): JSX.Element {
|
2025-06-04 13:42:00 -07:00
|
|
|
if (page === Page.BackupsDetails) {
|
|
|
|
return (
|
|
|
|
<BackupsDetailsPage
|
|
|
|
i18n={i18n}
|
|
|
|
cloudBackupStatus={cloudBackupStatus}
|
|
|
|
backupSubscriptionStatus={backupSubscriptionStatus}
|
|
|
|
locale={locale}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
page === Page.LocalBackups ||
|
|
|
|
page === Page.LocalBackupsKeyReference ||
|
|
|
|
page === Page.LocalBackupsSetupFolder ||
|
|
|
|
page === Page.LocalBackupsSetupKey
|
|
|
|
) {
|
|
|
|
return (
|
|
|
|
<PreferencesLocalBackups
|
|
|
|
accountEntropyPool={accountEntropyPool}
|
|
|
|
backupKeyViewed={backupKeyViewed}
|
|
|
|
i18n={i18n}
|
|
|
|
localBackupFolder={localBackupFolder}
|
|
|
|
onBackupKeyViewedChange={onBackupKeyViewedChange}
|
|
|
|
page={page}
|
|
|
|
pickLocalBackupFolder={pickLocalBackupFolder}
|
|
|
|
setPage={setPage}
|
|
|
|
showToast={showToast}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
|
|
|
<a href={SIGNAL_BACKUPS_LEARN_MORE_URL} rel="noreferrer" target="_blank">
|
|
|
|
{parts}
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
|
|
|
|
const isLocalBackupsSetup = localBackupFolder && backupKeyViewed;
|
|
|
|
|
2025-04-02 14:57:29 -04:00
|
|
|
return (
|
|
|
|
<>
|
2025-06-04 13:42:00 -07:00
|
|
|
<div className="Preferences__padding">
|
|
|
|
<div className="Preferences__description Preferences__description--medium">
|
|
|
|
{i18n('icu:Preferences--backup-section-description')}
|
|
|
|
</div>
|
2025-04-02 14:57:29 -04:00
|
|
|
</div>
|
|
|
|
|
2025-06-04 13:42:00 -07:00
|
|
|
{backupSubscriptionStatus ? (
|
|
|
|
<SettingsRow className="Preferences--BackupsRow">
|
|
|
|
<Control
|
|
|
|
icon="Preferences__BackupsIcon"
|
|
|
|
left={
|
|
|
|
<label>
|
|
|
|
{i18n('icu:Preferences--signal-backups')}{' '}
|
|
|
|
<div className="Preferences__description">
|
|
|
|
{renderBackupsSubscriptionSummary({
|
|
|
|
subscriptionStatus: backupSubscriptionStatus,
|
|
|
|
i18n,
|
|
|
|
locale,
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</label>
|
|
|
|
}
|
|
|
|
right={
|
|
|
|
<Button
|
|
|
|
onClick={() => setPage(Page.BackupsDetails)}
|
|
|
|
variant={ButtonVariant.Secondary}
|
2025-04-02 14:57:29 -04:00
|
|
|
>
|
2025-06-04 13:42:00 -07:00
|
|
|
{i18n('icu:Preferences__button--manage')}
|
|
|
|
</Button>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</SettingsRow>
|
|
|
|
) : (
|
|
|
|
<SettingsRow className="Preferences--BackupsRow">
|
|
|
|
<Control
|
|
|
|
icon="Preferences__BackupsIcon"
|
|
|
|
left={
|
|
|
|
<label>
|
|
|
|
{i18n('icu:Preferences--signal-backups')}{' '}
|
|
|
|
<div className="Preferences--backup-details__value">
|
|
|
|
<I18n
|
|
|
|
id="icu:Preferences--signal-backups-off-description"
|
|
|
|
i18n={i18n}
|
|
|
|
components={{
|
|
|
|
learnMoreLink,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</label>
|
|
|
|
}
|
|
|
|
right={null}
|
|
|
|
/>
|
|
|
|
</SettingsRow>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<SettingsRow
|
|
|
|
className="Preferences--BackupsRow"
|
|
|
|
title={i18n('icu:Preferences__backup-other-ways')}
|
|
|
|
>
|
|
|
|
<Control
|
|
|
|
icon="Preferences__LocalBackupsIcon"
|
|
|
|
left={
|
2025-04-02 14:57:29 -04:00
|
|
|
<label>
|
2025-06-04 13:42:00 -07:00
|
|
|
{i18n('icu:Preferences__local-backups')}{' '}
|
|
|
|
<div className="Preferences__description">
|
|
|
|
{isLocalBackupsSetup
|
|
|
|
? null
|
|
|
|
: i18n('icu:Preferences--local-backups-off-description')}
|
2025-04-02 14:57:29 -04:00
|
|
|
</div>
|
|
|
|
</label>
|
2025-06-04 13:42:00 -07:00
|
|
|
}
|
|
|
|
right={
|
|
|
|
<Button
|
|
|
|
onClick={() => setPage(Page.LocalBackups)}
|
|
|
|
variant={ButtonVariant.Secondary}
|
|
|
|
>
|
|
|
|
{isLocalBackupsSetup
|
|
|
|
? i18n('icu:Preferences__button--manage')
|
|
|
|
: i18n('icu:Preferences__button--set-up')}
|
|
|
|
</Button>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</SettingsRow>
|
2025-04-02 14:57:29 -04:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
2025-06-04 13:42:00 -07:00
|
|
|
|
2025-04-02 14:57:29 -04:00
|
|
|
function getSubscriptionDetails({
|
|
|
|
i18n,
|
|
|
|
subscriptionStatus,
|
|
|
|
locale,
|
|
|
|
}: {
|
|
|
|
i18n: LocalizerType;
|
|
|
|
locale: string;
|
|
|
|
subscriptionStatus: BackupsSubscriptionType;
|
|
|
|
}): JSX.Element | null {
|
|
|
|
if (subscriptionStatus.status === 'active') {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{subscriptionStatus.cost ? (
|
|
|
|
<div className="Preferences--backups-summary__subscription-price">
|
|
|
|
{new Intl.NumberFormat(locale, {
|
|
|
|
style: 'currency',
|
|
|
|
currency: subscriptionStatus.cost.currencyCode,
|
|
|
|
currencyDisplay: 'narrowSymbol',
|
|
|
|
}).format(subscriptionStatus.cost.amount)}{' '}
|
|
|
|
/ month
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
{subscriptionStatus.renewalDate ? (
|
|
|
|
<div className="Preferences--backups-summary__renewal-date">
|
|
|
|
{i18n('icu:Preferences--backup-plan__renewal-date', {
|
|
|
|
date: formatTimestamp(subscriptionStatus.renewalDate.getTime(), {
|
|
|
|
dateStyle: 'medium',
|
|
|
|
}),
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (subscriptionStatus.status === 'pending-cancellation') {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="Preferences--backups-summary__canceled">
|
|
|
|
{i18n('icu:Preferences--backup-plan__canceled')}
|
|
|
|
</div>
|
|
|
|
{subscriptionStatus.expiryDate ? (
|
|
|
|
<div className="Preferences--backups-summary__expiry-date">
|
|
|
|
{i18n('icu:Preferences--backup-plan__expiry-date', {
|
|
|
|
date: formatTimestamp(subscriptionStatus.expiryDate.getTime(), {
|
|
|
|
dateStyle: 'medium',
|
|
|
|
}),
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2025-06-04 13:42:00 -07:00
|
|
|
|
|
|
|
export function renderBackupsSubscriptionDetails({
|
2025-04-02 14:57:29 -04:00
|
|
|
subscriptionStatus,
|
|
|
|
i18n,
|
|
|
|
locale,
|
|
|
|
}: {
|
|
|
|
locale: string;
|
|
|
|
subscriptionStatus?: BackupsSubscriptionType;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
}): JSX.Element | null {
|
|
|
|
if (!subscriptionStatus) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { status } = subscriptionStatus;
|
|
|
|
switch (status) {
|
|
|
|
case 'active':
|
|
|
|
case 'pending-cancellation':
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="Preferences--backups-summary__status-container">
|
|
|
|
<div>
|
|
|
|
<div className="Preferences--backups-summary__type">
|
|
|
|
{i18n('icu:Preferences--backup-media-plan__description')}
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__content">
|
|
|
|
{getSubscriptionDetails({ i18n, locale, subscriptionStatus })}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{subscriptionStatus.status === 'active' ? (
|
|
|
|
<div className="Preferences--backups-summary__icon Preferences--backups-summary__icon--active" />
|
|
|
|
) : (
|
|
|
|
<div className="Preferences--backups-summary__icon Preferences--backups-summary__icon--inactive" />
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__note">
|
|
|
|
{i18n('icu:Preferences--backup-media-plan__note')}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
case 'free':
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="Preferences--backups-summary__status-container">
|
|
|
|
<div>
|
|
|
|
<div className="Preferences--backups-summary__type">
|
|
|
|
{i18n('icu:Preferences--backup-messages-plan__description', {
|
|
|
|
mediaDayCount:
|
|
|
|
subscriptionStatus.mediaIncludedInBackupDurationDays,
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__content">
|
|
|
|
{i18n(
|
|
|
|
'icu:Preferences--backup-messages-plan__cost-description'
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__icon Preferences--backups-summary__icon--active" />
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__note">
|
|
|
|
{i18n('icu:Preferences--backup-messages-plan__note')}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
case 'not-found':
|
|
|
|
case 'expired':
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="Preferences--backups-summary__status-container ">
|
|
|
|
<div className="Preferences--backups-summary__content">
|
|
|
|
{i18n('icu:Preferences--backup-plan-not-found__description')}
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__icon Preferences--backups-summary__icon--inactive" />
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__note">
|
|
|
|
<div className="Preferences--backups-summary__note">
|
|
|
|
{i18n('icu:Preferences--backup-plan__not-found__note')}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
default:
|
|
|
|
throw missingCaseError(status);
|
|
|
|
}
|
|
|
|
}
|
2025-06-04 13:42:00 -07:00
|
|
|
|
|
|
|
export function renderBackupsSubscriptionSummary({
|
|
|
|
subscriptionStatus,
|
|
|
|
i18n,
|
|
|
|
locale,
|
|
|
|
}: {
|
|
|
|
locale: string;
|
|
|
|
subscriptionStatus?: BackupsSubscriptionType;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
}): JSX.Element | null {
|
|
|
|
if (!subscriptionStatus) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { status } = subscriptionStatus;
|
|
|
|
switch (status) {
|
|
|
|
case 'active':
|
|
|
|
case 'pending-cancellation':
|
|
|
|
return (
|
|
|
|
<div className="Preferences--backups-summary__status-container">
|
|
|
|
<div>
|
|
|
|
<div className="Preferences--backups-summary__type">
|
|
|
|
{i18n('icu:Preferences--backup-media-plan__description')}
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__content">
|
|
|
|
{getSubscriptionDetails({ i18n, locale, subscriptionStatus })}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
case 'free':
|
|
|
|
return (
|
|
|
|
<div className="Preferences--backups-summary__status-container">
|
|
|
|
<div>
|
|
|
|
<div className="Preferences--backups-summary__type">
|
|
|
|
{i18n('icu:Preferences--backup-messages-plan__description', {
|
|
|
|
mediaDayCount:
|
|
|
|
subscriptionStatus.mediaIncludedInBackupDurationDays,
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
<div className="Preferences--backups-summary__content">
|
|
|
|
{i18n('icu:Preferences--backup-messages-plan__cost-description')}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
case 'not-found':
|
|
|
|
case 'expired':
|
|
|
|
return (
|
|
|
|
<div className="Preferences--backups-summary__status-container ">
|
|
|
|
<div className="Preferences--backups-summary__content">
|
|
|
|
{i18n('icu:Preferences--backup-plan-not-found__description')}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
default:
|
|
|
|
throw missingCaseError(status);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function BackupsDetailsPage({
|
|
|
|
cloudBackupStatus,
|
|
|
|
backupSubscriptionStatus,
|
|
|
|
i18n,
|
|
|
|
locale,
|
|
|
|
}: {
|
|
|
|
cloudBackupStatus?: BackupStatusType;
|
|
|
|
backupSubscriptionStatus?: BackupsSubscriptionType;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
locale: string;
|
|
|
|
}): JSX.Element {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="Preferences--backups-summary__container">
|
|
|
|
{renderBackupsSubscriptionDetails({
|
|
|
|
subscriptionStatus: backupSubscriptionStatus,
|
|
|
|
i18n,
|
|
|
|
locale,
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{cloudBackupStatus ? (
|
|
|
|
<SettingsRow
|
|
|
|
className="Preferences--backup-details"
|
|
|
|
title={i18n('icu:Preferences--backup-details__header')}
|
|
|
|
>
|
|
|
|
{cloudBackupStatus.createdAt ? (
|
|
|
|
<div className="Preferences--backup-details__row">
|
|
|
|
<label>{i18n('icu:Preferences--backup-created-at__label')}</label>
|
|
|
|
<div
|
|
|
|
id="Preferences--backup-details__value"
|
|
|
|
className="Preferences--backup-details__value"
|
|
|
|
>
|
|
|
|
{/* TODO (DESKTOP-8509) */}
|
|
|
|
{i18n('icu:Preferences--backup-created-by-phone')}
|
|
|
|
<span className="Preferences--backup-details__value-divider" />
|
|
|
|
{formatTimestamp(cloudBackupStatus.createdAt, {
|
|
|
|
dateStyle: 'medium',
|
|
|
|
timeStyle: 'short',
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</SettingsRow>
|
|
|
|
) : null}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|