Refactor backup subscription UI

This commit is contained in:
trevor-signal
2025-10-15 11:05:59 -04:00
committed by GitHub
parent dbaf2f5e68
commit 6ec7272d4e
14 changed files with 481 additions and 320 deletions

View File

@@ -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',
},

View File

@@ -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}

View 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);
}
}

View File

@@ -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>
);
}

View File

@@ -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'),