diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1e9119a4f7..48ca394b15 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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": { diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 789d232206..4306476204 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -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 diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx index e2e861ea64..2f5441e05e 100644 --- a/ts/components/Preferences.stories.tsx +++ b/ts/components/Preferences.stories.tsx @@ -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', }, diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index db0a7e861e..7b40e3505b 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -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 = ( void; + pauseBackupMediaDownload: () => void; + resumeBackupMediaDownload: () => void; + backupMediaDownloadStatus?: BackupMediaDownloadStatusType; +}): JSX.Element { + const shouldShowMediaProgress = + backupMediaDownloadStatus && + backupMediaDownloadStatus.completedBytes < + backupMediaDownloadStatus.totalBytes; + + return ( + <> +
+ {backupTier === BackupLevel.Paid + ? renderPaidBackupDetailsSummary({ + subscriptionStatus: backupSubscriptionStatus, + i18n, + locale, + }) + : null} + {backupTier === BackupLevel.Free + ? renderFreeBackupDetailsSummary({ + backupFreeMediaDays, + i18n, + }) + : null} +
+ + {cloudBackupStatus || shouldShowMediaProgress ? ( + + {cloudBackupStatus?.createdTimestamp ? ( +
+ +
+ {/* TODO (DESKTOP-8509) */} + {i18n('icu:Preferences--backup-created-by-phone')} + + {formatTimestamp(cloudBackupStatus.createdTimestamp, { + dateStyle: 'medium', + timeStyle: 'short', + })} +
+
+ ) : null} + {shouldShowMediaProgress && backupMediaDownloadStatus ? ( +
+ +
+ ) : null} +
+ ) : null} + + ); +} + +function renderPaidBackupDetailsSummary({ + subscriptionStatus, + i18n, + locale, +}: { + locale: string; + subscriptionStatus?: BackupsSubscriptionType; + i18n: LocalizerType; +}): JSX.Element | null { + return ( + <> +
+
+
+ {i18n('icu:Preferences--backup-media-plan__description')} +
+
+ {subscriptionStatus + ? renderSubscriptionDetails({ i18n, locale, subscriptionStatus }) + : null} +
+
+ {getSubscriptionStatusIcon(subscriptionStatus)} +
+
+ {getSubscriptionNote(i18n, subscriptionStatus)} +
+ + ); +} + +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 ( +
+ ); + case 'pending-cancellation': + case 'not-found': + case 'expired': + case undefined: + return ( +
+ ); + default: + throw missingCaseError(status); + } +} + +function renderFreeBackupDetailsSummary({ + backupFreeMediaDays, + i18n, +}: { + backupFreeMediaDays: number; + i18n: LocalizerType; +}): JSX.Element | null { + return ( + <> +
+
+
+ {i18n('icu:Preferences--backup-messages-plan__description', { + mediaDayCount: backupFreeMediaDays, + })} +
+
+ {i18n('icu:Preferences--backup-messages-plan__cost-description')} +
+
+
+
+
+ {i18n('icu:Preferences--backup-messages-plan__note')} +
+ + ); +} + +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 ( + + ); + } + + switch (status) { + case 'active': + return ( + <> + {subscriptionStatus.cost ? ( +
+ {i18n('icu:Preferences--backup-subscription-monthly-cost', { + cost: new Intl.NumberFormat(locale, { + style: 'currency', + currency: subscriptionStatus.cost.currencyCode, + currencyDisplay: 'narrowSymbol', + }).format(subscriptionStatus.cost.amount), + })} +
+ ) : null} + {subscriptionStatus.renewalTimestamp ? ( +
+ {i18n('icu:Preferences--backup-plan__renewal-date', { + date: formatTimestamp(subscriptionStatus.renewalTimestamp, { + dateStyle: 'medium', + }), + })} +
+ ) : null} + + ); + + case 'pending-cancellation': + return ( + <> +
+ {i18n('icu:Preferences--backup-plan__canceled')} +
+ {subscriptionStatus.expiryTimestamp ? ( +
+ {i18n('icu:Preferences--backup-plan__expiry-date', { + date: formatTimestamp(subscriptionStatus.expiryTimestamp, { + dateStyle: 'medium', + }), + })} +
+ ) : null} + + ); + case 'not-found': + case 'expired': + return ( +
+
+ {i18n('icu:Preferences--backup-plan-not-found__description')} +
+
+ ); + default: + throw missingCaseError(status); + } +} diff --git a/ts/components/PreferencesBackups.tsx b/ts/components/PreferencesBackups.tsx index 2c72f87a1e..a8ed6de232 100644 --- a/ts/components/PreferencesBackups.tsx +++ b/ts/components/PreferencesBackups.tsx @@ -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({ - {backupSubscriptionStatus.status === 'off' ? ( + {backupTier == null ? ( @@ -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 ? ( -
- {i18n('icu:Preferences--backup-subscription-monthly-cost', { - cost: new Intl.NumberFormat(locale, { - style: 'currency', - currency: subscriptionStatus.cost.currencyCode, - currencyDisplay: 'narrowSymbol', - }).format(subscriptionStatus.cost.amount), - })} -
- ) : null} - {subscriptionStatus.renewalTimestamp ? ( -
- {i18n('icu:Preferences--backup-plan__renewal-date', { - date: formatTimestamp(subscriptionStatus.renewalTimestamp, { - dateStyle: 'medium', - }), - })} -
- ) : null} - - ); - } - if (subscriptionStatus.status === 'pending-cancellation') { - return ( - <> -
- {i18n('icu:Preferences--backup-plan__canceled')} -
- {subscriptionStatus.expiryTimestamp ? ( -
- {i18n('icu:Preferences--backup-plan__expiry-date', { - date: formatTimestamp(subscriptionStatus.expiryTimestamp, { - dateStyle: 'medium', - }), - })} -
- ) : 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 ( - <> -
-
-
- {i18n('icu:Preferences--backup-media-plan__description')} -
-
- {getSubscriptionDetails({ i18n, locale, subscriptionStatus })} -
-
- {subscriptionStatus.status === 'active' ? ( -
- ) : ( -
- )} -
-
- {i18n('icu:Preferences--backup-media-plan__note')} -
- - ); - case 'free': - return ( - <> -
-
-
- {i18n('icu:Preferences--backup-messages-plan__description', { - mediaDayCount: - subscriptionStatus.mediaIncludedInBackupDurationDays, - })} -
-
- {i18n( - 'icu:Preferences--backup-messages-plan__cost-description' - )} -
-
-
-
-
- {i18n('icu:Preferences--backup-messages-plan__note')} -
- - ); - case 'not-found': - case 'expired': - return ( - <> -
-
- {i18n('icu:Preferences--backup-plan-not-found__description')} -
-
-
-
-
- {i18n('icu:Preferences--backup-plan__not-found__note')} -
-
- - ); - 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 ( -
-
-
- {i18n('icu:Preferences--backup-media-plan__description')} -
-
- {getSubscriptionDetails({ i18n, locale, subscriptionStatus })} -
-
-
- ); - case 'free': - return ( -
-
-
- {i18n('icu:Preferences--backup-messages-plan__description', { - mediaDayCount: - subscriptionStatus.mediaIncludedInBackupDurationDays, - })} -
-
- {i18n('icu:Preferences--backup-messages-plan__cost-description')} -
-
-
- ); - case 'not-found': - case 'expired': - return ( -
-
- {i18n('icu:Preferences--backup-plan-not-found__description')} -
-
- ); - 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 ( - <> -
- {renderBackupsSubscriptionDetails({ - subscriptionStatus: backupSubscriptionStatus, - i18n, - locale, - })} +
+
+
+ {i18n('icu:Preferences--backup-media-plan__description')} +
+
+ {renderSubscriptionDetails({ i18n, locale, subscriptionStatus })} +
+
+ ); +} - {cloudBackupStatus || shouldShowMediaProgress ? ( - - {cloudBackupStatus?.createdTimestamp ? ( -
- -
- {/* TODO (DESKTOP-8509) */} - {i18n('icu:Preferences--backup-created-by-phone')} - - {formatTimestamp(cloudBackupStatus.createdTimestamp, { - dateStyle: 'medium', - timeStyle: 'short', - })} -
-
- ) : null} - {shouldShowMediaProgress && backupMediaDownloadStatus ? ( -
- -
- ) : null} -
- ) : null} - +export function renderFreeBackupsSummary({ + backupFreeMediaDays, + i18n, +}: { + backupFreeMediaDays: number; + i18n: LocalizerType; +}): JSX.Element | null { + return ( +
+
+
+ {i18n('icu:Preferences--backup-messages-plan__description', { + mediaDayCount: backupFreeMediaDays, + })} +
+
+ {i18n('icu:Preferences--backup-messages-plan__cost-description')} +
+
+
); } diff --git a/ts/components/SpinnerV2.tsx b/ts/components/SpinnerV2.tsx index e536cb9278..d46ed6b24b 100644 --- a/ts/components/SpinnerV2.tsx +++ b/ts/components/SpinnerV2.tsx @@ -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'), diff --git a/ts/services/backups/api.ts b/ts/services/backups/api.ts index 893be02909..540d142419 100644 --- a/ts/services/backups/api.ts +++ b/ts/services/backups/api.ts @@ -224,7 +224,7 @@ export class BackupAPI { public async getSubscriptionInfo(): Promise { const subscriberId = itemStorage.get('backupsSubscriberId'); if (!subscriberId) { - log.error('Backups.getSubscriptionInfo: missing subscriberId'); + log.warn('Backups.getSubscriptionInfo: missing subscriberId'); return { status: 'not-found' }; } diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 3b4458f6d6..4312b9ee53 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -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; } diff --git a/ts/services/backups/types.ts b/ts/services/backups/types.ts index 9930b5ed1b..4dca0ca261 100644 --- a/ts/services/backups/types.ts +++ b/ts/services/backups/types.ts @@ -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; diff --git a/ts/state/smart/Preferences.tsx b/ts/state/smart/Preferences.tsx index 171c14f50b..ef3f17c18b 100644 --- a/ts/state/smart/Preferences.tsx +++ b/ts/state/smart/Preferences.tsx @@ -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, diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index 4f9afbd52e..e8931129a5 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -232,7 +232,7 @@ export type StorageAccessType = { backupTier: number | undefined; cloudBackupStatus: BackupStatusType | undefined; - backupSubscriptionStatus: BackupsSubscriptionType; + backupSubscriptionStatus: BackupsSubscriptionType | undefined; backupKeyViewed: boolean; localBackupFolder: string | undefined; diff --git a/ts/types/backups.ts b/ts/types/backups.ts index e3c522efb2..64cbadc958 100644 --- a/ts/types/backups.ts +++ b/ts/types/backups.ts @@ -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; diff --git a/ts/util/getMessageQueueTime.ts b/ts/util/getMessageQueueTime.ts index d10d6abb86..9794395c5a 100644 --- a/ts/util/getMessageQueueTime.ts +++ b/ts/util/getMessageQueueTime.ts @@ -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