mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-05 01:10:49 +00:00
Refactor backup subscription UI
This commit is contained in:
@@ -7513,7 +7513,7 @@
|
||||
"description": "Description of a backup plan that backups all of their messages (text) and media"
|
||||
},
|
||||
"icu:Preferences--backup-plan-not-found__description": {
|
||||
"messageformat": "Your subscription was not found. Renew to continue using Signal Secure Backups.",
|
||||
"messageformat": "Your subscription was not found. Check your phone to view your backup details.",
|
||||
"description": "Description when a backup subscription used to exist but is not active"
|
||||
},
|
||||
"icu:Preferences--backup-subscription-monthly-cost": {
|
||||
|
||||
@@ -232,8 +232,11 @@ export function isEnabled(
|
||||
return get(reduxConfig ?? config, [name, 'enabled'], false);
|
||||
}
|
||||
|
||||
export function getValue(name: ConfigKeyType): string | undefined {
|
||||
return get(config, [name, 'value']);
|
||||
export function getValue(
|
||||
name: ConfigKeyType, // when called from UI component, provide redux config (items.remoteConfig)
|
||||
reduxConfig?: ConfigMapType
|
||||
): string | undefined {
|
||||
return get(reduxConfig ?? config, [name, 'value']);
|
||||
}
|
||||
|
||||
// See isRemoteConfigBucketEnabled in selectors/items.ts
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -224,7 +224,7 @@ export class BackupAPI {
|
||||
public async getSubscriptionInfo(): Promise<BackupsSubscriptionType> {
|
||||
const subscriberId = itemStorage.get('backupsSubscriberId');
|
||||
if (!subscriberId) {
|
||||
log.error('Backups.getSubscriptionInfo: missing subscriberId');
|
||||
log.warn('Backups.getSubscriptionInfo: missing subscriberId');
|
||||
return { status: 'not-found' };
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import { prependStream } from '../../util/prependStream.js';
|
||||
import { appendMacStream } from '../../util/appendMacStream.js';
|
||||
import { getMacAndUpdateHmac } from '../../util/getMacAndUpdateHmac.js';
|
||||
import { missingCaseError } from '../../util/missingCaseError.js';
|
||||
import { DAY, HOUR, SECOND } from '../../util/durations/index.js';
|
||||
import { HOUR, SECOND } from '../../util/durations/index.js';
|
||||
import type { ExplodePromiseResultType } from '../../util/explodePromise.js';
|
||||
import { explodePromise } from '../../util/explodePromise.js';
|
||||
import type { RetryBackupImportValue } from '../../state/ducks/installer.js';
|
||||
@@ -80,7 +80,6 @@ import {
|
||||
import { FileStream } from './util/FileStream.js';
|
||||
import { ToastType } from '../../types/Toast.js';
|
||||
import { isAdhoc, isNightly } from '../../util/version.js';
|
||||
import { getMessageQueueTime } from '../../util/getMessageQueueTime.js';
|
||||
import { isLocalBackupsEnabled } from '../../util/isLocalBackupsEnabled.js';
|
||||
import type { ValidateLocalBackupStructureResultType } from './util/localBackup.js';
|
||||
import {
|
||||
@@ -1096,29 +1095,33 @@ export class BackupsService {
|
||||
async #fetchSubscriptionStatus(): Promise<
|
||||
BackupsSubscriptionType | undefined
|
||||
> {
|
||||
const cachedBackupSubscriptionStatus = itemStorage.get(
|
||||
'backupSubscriptionStatus'
|
||||
);
|
||||
const backupTier = this.#getBackupTierFromStorage();
|
||||
let result: BackupsSubscriptionType;
|
||||
let result: BackupsSubscriptionType | undefined;
|
||||
switch (backupTier) {
|
||||
case null:
|
||||
case undefined:
|
||||
result = {
|
||||
status: 'off',
|
||||
};
|
||||
break;
|
||||
case BackupLevel.Free:
|
||||
result = {
|
||||
status: 'free',
|
||||
mediaIncludedInBackupDurationDays: getMessageQueueTime() / DAY,
|
||||
};
|
||||
result = { status: 'not-found' };
|
||||
break;
|
||||
case BackupLevel.Paid:
|
||||
await itemStorage.put('backupSubscriptionStatus', {
|
||||
...(cachedBackupSubscriptionStatus ?? { status: 'not-found' }),
|
||||
isFetching: true,
|
||||
});
|
||||
result = await this.api.getSubscriptionInfo();
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(backupTier);
|
||||
}
|
||||
|
||||
await itemStorage.put('backupSubscriptionStatus', result);
|
||||
await itemStorage.put('backupSubscriptionStatus', {
|
||||
...result,
|
||||
lastFetchedAtMs: Date.now(),
|
||||
isFetching: false,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ export enum BackupLevel {
|
||||
Paid = 201,
|
||||
}
|
||||
|
||||
export function backupLevelFromNumber(
|
||||
num: number | undefined
|
||||
): BackupLevel | null {
|
||||
if (Object.values(BackupLevel).includes(num as BackupLevel)) {
|
||||
return num as BackupLevel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export type AboutMe = {
|
||||
aci: AciString;
|
||||
pni?: PniString;
|
||||
|
||||
@@ -58,7 +58,7 @@ import { PhoneNumberSharingMode } from '../../types/PhoneNumberSharingMode.js';
|
||||
import { writeProfile } from '../../services/writeProfile.js';
|
||||
import { getConversation } from '../../util/getConversation.js';
|
||||
import { waitForEvent } from '../../shims/events.js';
|
||||
import { MINUTE } from '../../util/durations/index.js';
|
||||
import { DAY, MINUTE } from '../../util/durations/index.js';
|
||||
import { sendSyncRequests } from '../../textsecure/syncRequests.js';
|
||||
import { SmartUpdateDialog } from './UpdateDialog.js';
|
||||
import { Preferences } from '../../components/Preferences.js';
|
||||
@@ -110,6 +110,8 @@ import {
|
||||
} from './PreferencesNotificationProfiles.js';
|
||||
import type { ExternalProps as SmartNotificationProfilesProps } from './PreferencesNotificationProfiles.js';
|
||||
import { getProfiles } from '../selectors/notificationProfiles.js';
|
||||
import { backupLevelFromNumber } from '../../services/backups/types.js';
|
||||
import { getMessageQueueTime } from '../../util/getMessageQueueTime.js';
|
||||
|
||||
const DEFAULT_NOTIFICATION_SETTING = 'message';
|
||||
|
||||
@@ -552,6 +554,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
|
||||
const {
|
||||
backupSubscriptionStatus,
|
||||
backupTier,
|
||||
cloudBackupStatus,
|
||||
localBackupFolder,
|
||||
backupMediaDownloadCompletedBytes,
|
||||
@@ -576,6 +579,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
|
||||
const backupFeatureEnabled = isBackupFeatureEnabled(items.remoteConfig);
|
||||
const backupLocalBackupsEnabled = isLocalBackupsEnabled(items.remoteConfig);
|
||||
const backupFreeMediaDays = getMessageQueueTime(items.remoteConfig) / DAY;
|
||||
|
||||
// Two-way items
|
||||
|
||||
@@ -770,9 +774,11 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
availableSpeakers={availableSpeakers}
|
||||
backupFeatureEnabled={backupFeatureEnabled}
|
||||
backupKeyViewed={backupKeyViewed}
|
||||
backupTier={backupLevelFromNumber(backupTier)}
|
||||
backupSubscriptionStatus={
|
||||
backupSubscriptionStatus ?? { status: 'off' }
|
||||
backupSubscriptionStatus ?? { status: 'not-found' }
|
||||
}
|
||||
backupFreeMediaDays={backupFreeMediaDays}
|
||||
backupMediaDownloadStatus={{
|
||||
completedBytes: backupMediaDownloadCompletedBytes ?? 0,
|
||||
totalBytes: backupMediaDownloadTotalBytes ?? 0,
|
||||
|
||||
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
@@ -232,7 +232,7 @@ export type StorageAccessType = {
|
||||
|
||||
backupTier: number | undefined;
|
||||
cloudBackupStatus: BackupStatusType | undefined;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType | undefined;
|
||||
|
||||
backupKeyViewed: boolean;
|
||||
localBackupFolder: string | undefined;
|
||||
|
||||
@@ -47,13 +47,9 @@ export type BackupMediaDownloadStatusType = {
|
||||
isIdle: boolean;
|
||||
};
|
||||
|
||||
export type BackupsSubscriptionType =
|
||||
export type BackupsSubscriptionType = (
|
||||
| {
|
||||
status: 'off' | 'not-found' | 'expired';
|
||||
}
|
||||
| {
|
||||
status: 'free';
|
||||
mediaIncludedInBackupDurationDays: number;
|
||||
status: 'not-found' | 'expired';
|
||||
}
|
||||
| (
|
||||
| {
|
||||
@@ -66,7 +62,8 @@ export type BackupsSubscriptionType =
|
||||
expiryTimestamp?: number;
|
||||
cost?: SubscriptionCostType;
|
||||
}
|
||||
);
|
||||
)
|
||||
) & { lastFetchedAtMs?: number; isFetching?: boolean };
|
||||
|
||||
export type LocalBackupMetadataVerificationType = {
|
||||
snapshotDir: string;
|
||||
|
||||
@@ -5,11 +5,13 @@ import * as RemoteConfig from '../RemoteConfig.js';
|
||||
import { MONTH, SECOND } from './durations/index.js';
|
||||
import { parseIntWithFallback } from './parseIntWithFallback.js';
|
||||
|
||||
export function getMessageQueueTime(): number {
|
||||
export function getMessageQueueTime(
|
||||
reduxConfig?: RemoteConfig.ConfigMapType
|
||||
): number {
|
||||
return (
|
||||
Math.max(
|
||||
parseIntWithFallback(
|
||||
RemoteConfig.getValue('global.messageQueueTimeInSeconds'),
|
||||
RemoteConfig.getValue('global.messageQueueTimeInSeconds', reduxConfig),
|
||||
MONTH / SECOND
|
||||
),
|
||||
MONTH / SECOND
|
||||
|
||||
Reference in New Issue
Block a user