mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-18 23:49:20 +01:00
Refactor backup subscription UI
This commit is contained in:
@@ -12,7 +12,7 @@ import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors.js';
|
||||
import { PhoneNumberSharingMode } from '../types/PhoneNumberSharingMode.js';
|
||||
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability.js';
|
||||
import { EmojiSkinTone } from './fun/data/emojis.js';
|
||||
import { DAY, DurationInSeconds, WEEK } from '../util/durations/index.js';
|
||||
import { DAY, DurationInSeconds, HOUR, WEEK } from '../util/durations/index.js';
|
||||
import { DialogUpdate } from './DialogUpdate.js';
|
||||
import { DialogType } from '../types/Dialogs.js';
|
||||
import { ThemeType } from '../types/Util.js';
|
||||
@@ -53,6 +53,7 @@ import type { SmartPreferencesEditChatFolderPageProps } from '../state/smart/Pre
|
||||
import { CurrentChatFolders } from '../types/CurrentChatFolders.js';
|
||||
import type { ExternalProps as SmartNotificationProfilesProps } from '../state/smart/PreferencesNotificationProfiles.js';
|
||||
import type { NotificationProfileIdString } from '../types/NotificationProfile.js';
|
||||
import { BackupLevel } from '../services/backups/types.js';
|
||||
|
||||
const { shuffle } = lodash;
|
||||
|
||||
@@ -403,9 +404,11 @@ export default {
|
||||
availableMicrophones,
|
||||
availableSpeakers,
|
||||
backupFeatureEnabled: false,
|
||||
backupFreeMediaDays: 45,
|
||||
backupKeyViewed: false,
|
||||
backupLocalBackupsEnabled: false,
|
||||
backupSubscriptionStatus: { status: 'off' },
|
||||
backupSubscriptionStatus: { status: 'not-found' },
|
||||
backupTier: null,
|
||||
badge: undefined,
|
||||
blockedCount: 0,
|
||||
currentChatFoldersCount: 0,
|
||||
@@ -965,8 +968,8 @@ PNPDiscoverabilityDisabled.args = {
|
||||
settingsLocation: { page: SettingsPage.PNP },
|
||||
};
|
||||
|
||||
export const BackupsMediaDownloadActive = Template.bind({});
|
||||
BackupsMediaDownloadActive.args = {
|
||||
export const BackupDetailsMediaDownloadActive = Template.bind({});
|
||||
BackupDetailsMediaDownloadActive.args = {
|
||||
settingsLocation: { page: SettingsPage.BackupsDetails },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
@@ -974,6 +977,7 @@ BackupsMediaDownloadActive.args = {
|
||||
protoSize: 100_000_000,
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
backupTier: BackupLevel.Paid,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'active',
|
||||
cost: {
|
||||
@@ -989,8 +993,8 @@ BackupsMediaDownloadActive.args = {
|
||||
isIdle: false,
|
||||
},
|
||||
};
|
||||
export const BackupsMediaDownloadPaused = Template.bind({});
|
||||
BackupsMediaDownloadPaused.args = {
|
||||
export const BackupDetailsMediaDownloadPaused = Template.bind({});
|
||||
BackupDetailsMediaDownloadPaused.args = {
|
||||
settingsLocation: { page: SettingsPage.BackupsDetails },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
@@ -998,6 +1002,7 @@ BackupsMediaDownloadPaused.args = {
|
||||
protoSize: 100_000_000,
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
backupTier: BackupLevel.Paid,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'active',
|
||||
cost: {
|
||||
@@ -1014,9 +1019,26 @@ BackupsMediaDownloadPaused.args = {
|
||||
},
|
||||
};
|
||||
|
||||
export const BackupDetailsFree = Template.bind({});
|
||||
BackupDetailsFree.args = {
|
||||
settingsLocation: { page: SettingsPage.BackupsDetails },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
protoSize: 100_000_000,
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
backupTier: BackupLevel.Free,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'not-found',
|
||||
lastFetchedAtMs: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
export const BackupsPaidActive = Template.bind({});
|
||||
BackupsPaidActive.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupTier: BackupLevel.Paid,
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
@@ -1033,11 +1055,50 @@ BackupsPaidActive.args = {
|
||||
},
|
||||
};
|
||||
|
||||
export const BackupsPaidLoadingSubscription = Template.bind({});
|
||||
BackupsPaidLoadingSubscription.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupTier: BackupLevel.Paid,
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
protoSize: 100_000_000,
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
backupSubscriptionStatus: {
|
||||
status: 'active',
|
||||
cost: {
|
||||
amount: 22.99,
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
renewalTimestamp: Date.now() + 20 * DAY,
|
||||
isFetching: true,
|
||||
lastFetchedAtMs: Date.now() - HOUR,
|
||||
},
|
||||
};
|
||||
|
||||
export const BackupsPaidLoadingFirstTime = Template.bind({});
|
||||
BackupsPaidLoadingFirstTime.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupTier: BackupLevel.Paid,
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
protoSize: 100_000_000,
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
backupSubscriptionStatus: {
|
||||
status: 'not-found',
|
||||
isFetching: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const BackupsPaidCanceled = Template.bind({});
|
||||
BackupsPaidCanceled.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
backupTier: BackupLevel.Paid,
|
||||
cloudBackupStatus: {
|
||||
protoSize: 100_000_000,
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
@@ -1055,22 +1116,16 @@ BackupsPaidCanceled.args = {
|
||||
export const BackupsFree = Template.bind({});
|
||||
BackupsFree.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupTier: BackupLevel.Free,
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'free',
|
||||
mediaIncludedInBackupDurationDays: 30,
|
||||
},
|
||||
};
|
||||
export const BackupsFreeNoLocal = Template.bind({});
|
||||
BackupsFreeNoLocal.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: false,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'free',
|
||||
mediaIncludedInBackupDurationDays: 30,
|
||||
},
|
||||
backupTier: BackupLevel.Free,
|
||||
};
|
||||
|
||||
export const BackupsOff = Template.bind({});
|
||||
@@ -1078,6 +1133,7 @@ BackupsOff.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
backupTier: null,
|
||||
};
|
||||
|
||||
export const BackupsLocalBackups = Template.bind({});
|
||||
@@ -1094,14 +1150,15 @@ BackupsRemoteEnabledLocalDisabled.args = {
|
||||
backupLocalBackupsEnabled: false,
|
||||
};
|
||||
|
||||
export const BackupsSubscriptionNotFound = Template.bind({});
|
||||
BackupsSubscriptionNotFound.args = {
|
||||
export const BackupsPaidSubscriptionNotFound = Template.bind({});
|
||||
BackupsPaidSubscriptionNotFound.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'not-found',
|
||||
},
|
||||
backupTier: BackupLevel.Paid,
|
||||
cloudBackupStatus: {
|
||||
protoSize: 100_000_000,
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
@@ -1113,6 +1170,7 @@ BackupsSubscriptionExpired.args = {
|
||||
settingsLocation: { page: SettingsPage.Backups },
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
backupTier: null,
|
||||
backupSubscriptionStatus: {
|
||||
status: 'expired',
|
||||
},
|
||||
|
||||
@@ -15,6 +15,8 @@ import classNames from 'classnames';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import type { RowType } from '@signalapp/sqlcipher';
|
||||
import type { BackupLevel } from '@signalapp/libsignal-client/zkgroup.js';
|
||||
|
||||
import { Button, ButtonVariant } from './Button.js';
|
||||
import { ChatColorPicker } from './ChatColorPicker.js';
|
||||
import { Checkbox } from './Checkbox.js';
|
||||
@@ -111,8 +113,10 @@ export type PropsDataType = {
|
||||
accountEntropyPool: string | undefined;
|
||||
autoDownloadAttachment: AutoDownloadAttachmentType;
|
||||
backupFeatureEnabled: boolean;
|
||||
backupFreeMediaDays: number;
|
||||
backupKeyViewed: boolean;
|
||||
backupLocalBackupsEnabled: boolean;
|
||||
backupTier: BackupLevel | null;
|
||||
localBackupFolder: string | undefined;
|
||||
currentChatFoldersCount: number;
|
||||
cloudBackupStatus?: BackupStatusType;
|
||||
@@ -383,7 +387,9 @@ export function Preferences({
|
||||
pauseBackupMediaDownload,
|
||||
resumeBackupMediaDownload,
|
||||
cancelBackupMediaDownload,
|
||||
backupFreeMediaDays,
|
||||
backupKeyViewed,
|
||||
backupTier,
|
||||
backupSubscriptionStatus,
|
||||
backupLocalBackupsEnabled,
|
||||
badge,
|
||||
@@ -2188,7 +2194,9 @@ export function Preferences({
|
||||
const pageContents = (
|
||||
<PreferencesBackups
|
||||
accountEntropyPool={accountEntropyPool}
|
||||
backupFreeMediaDays={backupFreeMediaDays}
|
||||
backupKeyViewed={backupKeyViewed}
|
||||
backupTier={backupTier}
|
||||
backupSubscriptionStatus={backupSubscriptionStatus}
|
||||
backupMediaDownloadStatus={backupMediaDownloadStatus}
|
||||
cancelBackupMediaDownload={cancelBackupMediaDownload}
|
||||
|
||||
287
ts/components/PreferencesBackupDetails.tsx
Normal file
287
ts/components/PreferencesBackupDetails.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {
|
||||
BackupMediaDownloadStatusType,
|
||||
BackupsSubscriptionType,
|
||||
BackupStatusType,
|
||||
} from '../types/backups.js';
|
||||
import type { LocalizerType } from '../types/I18N.js';
|
||||
import { formatTimestamp } from '../util/formatTimestamp.js';
|
||||
import { SettingsRow } from './PreferencesUtil.js';
|
||||
import { missingCaseError } from '../util/missingCaseError.js';
|
||||
import { BackupMediaDownloadProgressSettings } from './BackupMediaDownloadProgressSettings.js';
|
||||
import { BackupLevel } from '../services/backups/types.js';
|
||||
import { SpinnerV2 } from './SpinnerV2.js';
|
||||
import { MINUTE } from '../util/durations/constants.js';
|
||||
import { isOlderThan } from '../util/timestamp.js';
|
||||
|
||||
// We'll show a loading spinner if we are fetching fresh data and cached data is older
|
||||
// than this duration
|
||||
const SUBSCRIPTION_STATUS_STALE_TIME_FOR_UI = 5 * MINUTE;
|
||||
|
||||
export function BackupsDetailsPage({
|
||||
cloudBackupStatus,
|
||||
backupFreeMediaDays,
|
||||
backupSubscriptionStatus,
|
||||
backupTier,
|
||||
i18n,
|
||||
locale,
|
||||
cancelBackupMediaDownload,
|
||||
pauseBackupMediaDownload,
|
||||
resumeBackupMediaDownload,
|
||||
backupMediaDownloadStatus,
|
||||
}: {
|
||||
cloudBackupStatus?: BackupStatusType;
|
||||
backupFreeMediaDays: number;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType;
|
||||
backupTier: BackupLevel | null;
|
||||
i18n: LocalizerType;
|
||||
locale: string;
|
||||
cancelBackupMediaDownload: () => void;
|
||||
pauseBackupMediaDownload: () => void;
|
||||
resumeBackupMediaDownload: () => void;
|
||||
backupMediaDownloadStatus?: BackupMediaDownloadStatusType;
|
||||
}): JSX.Element {
|
||||
const shouldShowMediaProgress =
|
||||
backupMediaDownloadStatus &&
|
||||
backupMediaDownloadStatus.completedBytes <
|
||||
backupMediaDownloadStatus.totalBytes;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="Preferences--backups-summary__container">
|
||||
{backupTier === BackupLevel.Paid
|
||||
? renderPaidBackupDetailsSummary({
|
||||
subscriptionStatus: backupSubscriptionStatus,
|
||||
i18n,
|
||||
locale,
|
||||
})
|
||||
: null}
|
||||
{backupTier === BackupLevel.Free
|
||||
? renderFreeBackupDetailsSummary({
|
||||
backupFreeMediaDays,
|
||||
i18n,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{cloudBackupStatus || shouldShowMediaProgress ? (
|
||||
<SettingsRow
|
||||
className="Preferences--backup-details"
|
||||
title={i18n('icu:Preferences--backup-details__header')}
|
||||
>
|
||||
{cloudBackupStatus?.createdTimestamp ? (
|
||||
<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.createdTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{shouldShowMediaProgress && backupMediaDownloadStatus ? (
|
||||
<div className="Preferences--backup-details__row">
|
||||
<BackupMediaDownloadProgressSettings
|
||||
{...backupMediaDownloadStatus}
|
||||
handleCancel={cancelBackupMediaDownload}
|
||||
handlePause={pauseBackupMediaDownload}
|
||||
handleResume={resumeBackupMediaDownload}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</SettingsRow>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPaidBackupDetailsSummary({
|
||||
subscriptionStatus,
|
||||
i18n,
|
||||
locale,
|
||||
}: {
|
||||
locale: string;
|
||||
subscriptionStatus?: BackupsSubscriptionType;
|
||||
i18n: LocalizerType;
|
||||
}): JSX.Element | null {
|
||||
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">
|
||||
{subscriptionStatus
|
||||
? renderSubscriptionDetails({ i18n, locale, subscriptionStatus })
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
{getSubscriptionStatusIcon(subscriptionStatus)}
|
||||
</div>
|
||||
<div className="Preferences--backups-summary__note">
|
||||
{getSubscriptionNote(i18n, subscriptionStatus)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getSubscriptionNote(
|
||||
i18n: LocalizerType,
|
||||
subscriptionStatus: BackupsSubscriptionType | undefined
|
||||
) {
|
||||
const status = subscriptionStatus?.status;
|
||||
switch (status) {
|
||||
case 'active':
|
||||
case 'pending-cancellation':
|
||||
return i18n('icu:Preferences--backup-media-plan__note');
|
||||
case 'not-found':
|
||||
case 'expired':
|
||||
case undefined:
|
||||
return i18n('icu:Preferences--backup-plan__not-found__note');
|
||||
default:
|
||||
throw missingCaseError(status);
|
||||
}
|
||||
}
|
||||
|
||||
function getSubscriptionStatusIcon(
|
||||
subscriptionStatus: BackupsSubscriptionType | undefined
|
||||
) {
|
||||
const status = subscriptionStatus?.status;
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return (
|
||||
<div className="Preferences--backups-summary__icon Preferences--backups-summary__icon--active" />
|
||||
);
|
||||
case 'pending-cancellation':
|
||||
case 'not-found':
|
||||
case 'expired':
|
||||
case undefined:
|
||||
return (
|
||||
<div className="Preferences--backups-summary__icon Preferences--backups-summary__icon--inactive" />
|
||||
);
|
||||
default:
|
||||
throw missingCaseError(status);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFreeBackupDetailsSummary({
|
||||
backupFreeMediaDays,
|
||||
i18n,
|
||||
}: {
|
||||
backupFreeMediaDays: number;
|
||||
i18n: LocalizerType;
|
||||
}): JSX.Element | null {
|
||||
return (
|
||||
<>
|
||||
<div className="Preferences--backups-summary__status-container">
|
||||
<div>
|
||||
<div className="Preferences--backups-summary__type">
|
||||
{i18n('icu:Preferences--backup-messages-plan__description', {
|
||||
mediaDayCount: backupFreeMediaDays,
|
||||
})}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSubscriptionDetails({
|
||||
i18n,
|
||||
subscriptionStatus,
|
||||
locale,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
locale: string;
|
||||
subscriptionStatus: BackupsSubscriptionType;
|
||||
}): JSX.Element | null {
|
||||
const { status } = subscriptionStatus;
|
||||
if (
|
||||
subscriptionStatus.isFetching &&
|
||||
isOlderThan(
|
||||
subscriptionStatus.lastFetchedAtMs ?? 0,
|
||||
SUBSCRIPTION_STATUS_STALE_TIME_FOR_UI
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<SpinnerV2 variant="no-background-light" size={24} strokeWidth={3} />
|
||||
);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return (
|
||||
<>
|
||||
{subscriptionStatus.cost ? (
|
||||
<div className="Preferences--backups-summary__subscription-price">
|
||||
{i18n('icu:Preferences--backup-subscription-monthly-cost', {
|
||||
cost: new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: subscriptionStatus.cost.currencyCode,
|
||||
currencyDisplay: 'narrowSymbol',
|
||||
}).format(subscriptionStatus.cost.amount),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{subscriptionStatus.renewalTimestamp ? (
|
||||
<div className="Preferences--backups-summary__renewal-date">
|
||||
{i18n('icu:Preferences--backup-plan__renewal-date', {
|
||||
date: formatTimestamp(subscriptionStatus.renewalTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
case 'pending-cancellation':
|
||||
return (
|
||||
<>
|
||||
<div className="Preferences--backups-summary__canceled">
|
||||
{i18n('icu:Preferences--backup-plan__canceled')}
|
||||
</div>
|
||||
{subscriptionStatus.expiryTimestamp ? (
|
||||
<div className="Preferences--backups-summary__expiry-date">
|
||||
{i18n('icu:Preferences--backup-plan__expiry-date', {
|
||||
date: formatTimestamp(subscriptionStatus.expiryTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,12 @@ import type {
|
||||
BackupStatusType,
|
||||
} from '../types/backups.js';
|
||||
import type { LocalizerType } from '../types/I18N.js';
|
||||
import { formatTimestamp } from '../util/formatTimestamp.js';
|
||||
import {
|
||||
SettingsControl as Control,
|
||||
FlowingSettingsControl as FlowingControl,
|
||||
LightIconLabel,
|
||||
SettingsRow,
|
||||
} from './PreferencesUtil.js';
|
||||
import { missingCaseError } from '../util/missingCaseError.js';
|
||||
import { Button, ButtonVariant } from './Button.js';
|
||||
import type { SettingsLocation } from '../types/Nav.js';
|
||||
import { SettingsPage } from '../types/Nav.js';
|
||||
@@ -29,7 +27,11 @@ import type {
|
||||
PromptOSAuthResultType,
|
||||
} from '../util/os/promptOSAuthMain.js';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.js';
|
||||
import { BackupMediaDownloadProgressSettings } from './BackupMediaDownloadProgressSettings.js';
|
||||
import { BackupLevel } from '../services/backups/types.js';
|
||||
import {
|
||||
BackupsDetailsPage,
|
||||
renderSubscriptionDetails,
|
||||
} from './PreferencesBackupDetails.js';
|
||||
|
||||
export const SIGNAL_BACKUPS_LEARN_MORE_URL =
|
||||
'https://support.signal.org/hc/articles/360007059752-Backup-and-Restore-Messages';
|
||||
@@ -50,8 +52,10 @@ function isRemoteBackupsPage(page: SettingsPage) {
|
||||
}
|
||||
export function PreferencesBackups({
|
||||
accountEntropyPool,
|
||||
backupFreeMediaDays,
|
||||
backupKeyViewed,
|
||||
backupSubscriptionStatus,
|
||||
backupTier,
|
||||
cloudBackupStatus,
|
||||
i18n,
|
||||
isLocalBackupsEnabled,
|
||||
@@ -72,8 +76,10 @@ export function PreferencesBackups({
|
||||
showToast,
|
||||
}: {
|
||||
accountEntropyPool: string | undefined;
|
||||
backupFreeMediaDays: number;
|
||||
backupKeyViewed: boolean;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType;
|
||||
backupTier: BackupLevel | null;
|
||||
cloudBackupStatus?: BackupStatusType;
|
||||
localBackupFolder: string | undefined;
|
||||
i18n: LocalizerType;
|
||||
@@ -123,7 +129,7 @@ export function PreferencesBackups({
|
||||
}
|
||||
|
||||
if (settingsLocation.page === SettingsPage.BackupsDetails) {
|
||||
if (backupSubscriptionStatus.status === 'off') {
|
||||
if (backupTier == null) {
|
||||
setSettingsLocation({ page: SettingsPage.Backups });
|
||||
return null;
|
||||
}
|
||||
@@ -131,6 +137,8 @@ export function PreferencesBackups({
|
||||
<BackupsDetailsPage
|
||||
i18n={i18n}
|
||||
cloudBackupStatus={cloudBackupStatus}
|
||||
backupTier={backupTier}
|
||||
backupFreeMediaDays={backupFreeMediaDays}
|
||||
backupSubscriptionStatus={backupSubscriptionStatus}
|
||||
backupMediaDownloadStatus={backupMediaDownloadStatus}
|
||||
cancelBackupMediaDownload={cancelBackupMediaDownload}
|
||||
@@ -169,7 +177,7 @@ export function PreferencesBackups({
|
||||
function renderRemoteBackups() {
|
||||
return (
|
||||
<>
|
||||
{backupSubscriptionStatus.status === 'off' ? (
|
||||
{backupTier == null ? (
|
||||
<SettingsRow className="Preferences--BackupsRow">
|
||||
<Control
|
||||
icon="Preferences__BackupsIcon"
|
||||
@@ -196,13 +204,21 @@ export function PreferencesBackups({
|
||||
<div className="Preferences__two-thirds-flow">
|
||||
<LightIconLabel icon="Preferences__BackupsIcon">
|
||||
<label>
|
||||
{i18n('icu:Preferences--signal-backups')}{' '}
|
||||
{i18n('icu:Preferences--signal-backups')}
|
||||
<div className="Preferences__description">
|
||||
{renderBackupsSubscriptionSummary({
|
||||
subscriptionStatus: backupSubscriptionStatus,
|
||||
i18n,
|
||||
locale,
|
||||
})}
|
||||
{backupTier === BackupLevel.Paid
|
||||
? renderPaidBackupsSummary({
|
||||
subscriptionStatus: backupSubscriptionStatus,
|
||||
i18n,
|
||||
locale,
|
||||
})
|
||||
: null}
|
||||
{backupTier === BackupLevel.Free
|
||||
? renderFreeBackupsSummary({
|
||||
i18n,
|
||||
backupFreeMediaDays,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</label>
|
||||
</LightIconLabel>
|
||||
@@ -317,281 +333,49 @@ export function PreferencesBackups({
|
||||
);
|
||||
}
|
||||
|
||||
function getSubscriptionDetails({
|
||||
i18n,
|
||||
export function renderPaidBackupsSummary({
|
||||
subscriptionStatus,
|
||||
i18n,
|
||||
locale,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
locale: string;
|
||||
subscriptionStatus: BackupsSubscriptionType;
|
||||
}): JSX.Element | null {
|
||||
if (subscriptionStatus.status === 'active') {
|
||||
return (
|
||||
<>
|
||||
{subscriptionStatus.cost ? (
|
||||
<div className="Preferences--backups-summary__subscription-price">
|
||||
{i18n('icu:Preferences--backup-subscription-monthly-cost', {
|
||||
cost: new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: subscriptionStatus.cost.currencyCode,
|
||||
currencyDisplay: 'narrowSymbol',
|
||||
}).format(subscriptionStatus.cost.amount),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{subscriptionStatus.renewalTimestamp ? (
|
||||
<div className="Preferences--backups-summary__renewal-date">
|
||||
{i18n('icu:Preferences--backup-plan__renewal-date', {
|
||||
date: formatTimestamp(subscriptionStatus.renewalTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (subscriptionStatus.status === 'pending-cancellation') {
|
||||
return (
|
||||
<>
|
||||
<div className="Preferences--backups-summary__canceled">
|
||||
{i18n('icu:Preferences--backup-plan__canceled')}
|
||||
</div>
|
||||
{subscriptionStatus.expiryTimestamp ? (
|
||||
<div className="Preferences--backups-summary__expiry-date">
|
||||
{i18n('icu:Preferences--backup-plan__expiry-date', {
|
||||
date: formatTimestamp(subscriptionStatus.expiryTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function renderBackupsSubscriptionDetails({
|
||||
subscriptionStatus,
|
||||
i18n,
|
||||
locale,
|
||||
}: {
|
||||
locale: string;
|
||||
subscriptionStatus?: BackupsSubscriptionType;
|
||||
i18n: LocalizerType;
|
||||
}): JSX.Element | null {
|
||||
if (!subscriptionStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { status } = subscriptionStatus;
|
||||
switch (status) {
|
||||
case 'off':
|
||||
return null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 'off':
|
||||
return null;
|
||||
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,
|
||||
cancelBackupMediaDownload,
|
||||
pauseBackupMediaDownload,
|
||||
resumeBackupMediaDownload,
|
||||
backupMediaDownloadStatus,
|
||||
}: {
|
||||
cloudBackupStatus?: BackupStatusType;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType;
|
||||
i18n: LocalizerType;
|
||||
locale: string;
|
||||
cancelBackupMediaDownload: () => void;
|
||||
pauseBackupMediaDownload: () => void;
|
||||
resumeBackupMediaDownload: () => void;
|
||||
backupMediaDownloadStatus?: BackupMediaDownloadStatusType;
|
||||
}): JSX.Element {
|
||||
const shouldShowMediaProgress =
|
||||
backupMediaDownloadStatus &&
|
||||
backupMediaDownloadStatus.completedBytes <
|
||||
backupMediaDownloadStatus.totalBytes;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="Preferences--backups-summary__container">
|
||||
{renderBackupsSubscriptionDetails({
|
||||
subscriptionStatus: backupSubscriptionStatus,
|
||||
i18n,
|
||||
locale,
|
||||
})}
|
||||
<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">
|
||||
{renderSubscriptionDetails({ i18n, locale, subscriptionStatus })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{cloudBackupStatus || shouldShowMediaProgress ? (
|
||||
<SettingsRow
|
||||
className="Preferences--backup-details"
|
||||
title={i18n('icu:Preferences--backup-details__header')}
|
||||
>
|
||||
{cloudBackupStatus?.createdTimestamp ? (
|
||||
<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.createdTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{shouldShowMediaProgress && backupMediaDownloadStatus ? (
|
||||
<div className="Preferences--backup-details__row">
|
||||
<BackupMediaDownloadProgressSettings
|
||||
{...backupMediaDownloadStatus}
|
||||
handleCancel={cancelBackupMediaDownload}
|
||||
handlePause={pauseBackupMediaDownload}
|
||||
handleResume={resumeBackupMediaDownload}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</SettingsRow>
|
||||
) : null}
|
||||
</>
|
||||
export function renderFreeBackupsSummary({
|
||||
backupFreeMediaDays,
|
||||
i18n,
|
||||
}: {
|
||||
backupFreeMediaDays: number;
|
||||
i18n: LocalizerType;
|
||||
}): JSX.Element | null {
|
||||
return (
|
||||
<div className="Preferences--backups-summary__status-container">
|
||||
<div>
|
||||
<div className="Preferences--backups-summary__type">
|
||||
{i18n('icu:Preferences--backup-messages-plan__description', {
|
||||
mediaDayCount: backupFreeMediaDays,
|
||||
})}
|
||||
</div>
|
||||
<div className="Preferences--backups-summary__content">
|
||||
{i18n('icu:Preferences--backup-messages-plan__cost-description')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ const SpinnerVariants = {
|
||||
bg: tw('stroke-none'),
|
||||
fg: tw('stroke-label-primary'),
|
||||
},
|
||||
'no-background-light': {
|
||||
bg: tw('stroke-none'),
|
||||
fg: tw('stroke-border-primary'),
|
||||
},
|
||||
brand: {
|
||||
bg: tw('stroke-fill-secondary'),
|
||||
fg: tw('stroke-border-selected'),
|
||||
|
||||
Reference in New Issue
Block a user