mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Backups: subscription info improvements
This commit is contained in:
@@ -6757,7 +6757,7 @@
|
||||
"description": "Feature name for message backups using the Signal service."
|
||||
},
|
||||
"icu:Preferences--signal-backups-off-description": {
|
||||
"messageformat": "Automatic backups with Signal’s secure, end-to-end encrypted storage service. Get started on your phone. <learnMoreLink>Learn more.</learnMoreLink>",
|
||||
"messageformat": "Automatic backups with Signal’s secure, end-to-end encrypted storage service. Get started on your phone. <learnMoreLink>Learn more</learnMoreLink>.",
|
||||
"description": "Description for message backups using the Signal service."
|
||||
},
|
||||
"icu:Preferences--backup-section-description": {
|
||||
|
||||
@@ -778,6 +778,10 @@ $secondary-text-color: light-dark(
|
||||
padding-block: 8px;
|
||||
margin-block-start: 8px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-block-end: 24px;
|
||||
}
|
||||
|
||||
@@ -196,6 +196,7 @@ export default {
|
||||
backupFeatureEnabled: false,
|
||||
backupKeyViewed: false,
|
||||
backupLocalBackupsEnabled: false,
|
||||
backupSubscriptionStatus: { status: 'off' },
|
||||
badge: undefined,
|
||||
blockedCount: 0,
|
||||
customColors: {},
|
||||
@@ -444,9 +445,8 @@ BackupsPaidActive.args = {
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
mediaSize: 539_249_410_039,
|
||||
protoSize: 100_000_000,
|
||||
createdAt: new Date(Date.now() - WEEK).getTime(),
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
backupSubscriptionStatus: {
|
||||
status: 'active',
|
||||
@@ -454,7 +454,7 @@ BackupsPaidActive.args = {
|
||||
amount: 22.99,
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
renewalDate: new Date(Date.now() + 20 * DAY),
|
||||
renewalTimestamp: Date.now() + 20 * DAY,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -464,9 +464,8 @@ BackupsPaidCancelled.args = {
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
cloudBackupStatus: {
|
||||
mediaSize: 539_249_410_039,
|
||||
protoSize: 100_000_000,
|
||||
createdAt: new Date(Date.now() - WEEK).getTime(),
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
backupSubscriptionStatus: {
|
||||
status: 'pending-cancellation',
|
||||
@@ -474,7 +473,7 @@ BackupsPaidCancelled.args = {
|
||||
amount: 22.99,
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
expiryDate: new Date(Date.now() + 20 * DAY),
|
||||
expiryTimestamp: Date.now() + 20 * DAY,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -512,9 +511,8 @@ BackupsSubscriptionNotFound.args = {
|
||||
status: 'not-found',
|
||||
},
|
||||
cloudBackupStatus: {
|
||||
mediaSize: 539_249_410_039,
|
||||
protoSize: 100_000_000,
|
||||
createdAt: new Date(Date.now() - WEEK).getTime(),
|
||||
createdTimestamp: Date.now() - WEEK,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export type PropsDataType = {
|
||||
backupLocalBackupsEnabled: boolean;
|
||||
localBackupFolder: string | undefined;
|
||||
cloudBackupStatus?: BackupStatusType;
|
||||
backupSubscriptionStatus?: BackupsSubscriptionType;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType;
|
||||
blockedCount: number;
|
||||
customColors: Record<string, CustomColorType>;
|
||||
defaultConversationColor: DefaultConversationColorType;
|
||||
@@ -502,8 +502,7 @@ export function Preferences({
|
||||
setSelectedLanguageLocale(localeOverride);
|
||||
}
|
||||
const shouldShowBackupsPage =
|
||||
(backupFeatureEnabled && backupSubscriptionStatus != null) ||
|
||||
backupLocalBackupsEnabled;
|
||||
backupFeatureEnabled || backupLocalBackupsEnabled;
|
||||
|
||||
if (page === Page.Backups && !shouldShowBackupsPage) {
|
||||
setPage(Page.General);
|
||||
@@ -519,13 +518,6 @@ export function Preferences({
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (page === Page.Backups) {
|
||||
refreshCloudBackupStatus();
|
||||
refreshBackupSubscriptionStatus();
|
||||
}
|
||||
}, [page, refreshCloudBackupStatus, refreshBackupSubscriptionStatus]);
|
||||
|
||||
const onZoomSelectChange = useCallback(
|
||||
(value: string) => {
|
||||
const number = parseFloat(value);
|
||||
@@ -2008,6 +2000,8 @@ export function Preferences({
|
||||
pickLocalBackupFolder={pickLocalBackupFolder}
|
||||
page={page}
|
||||
promptOSAuth={promptOSAuth}
|
||||
refreshCloudBackupStatus={refreshCloudBackupStatus}
|
||||
refreshBackupSubscriptionStatus={refreshBackupSubscriptionStatus}
|
||||
setPage={setPage}
|
||||
showToast={showToast}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type {
|
||||
@@ -44,12 +44,14 @@ export function PreferencesBackups({
|
||||
pickLocalBackupFolder,
|
||||
page,
|
||||
promptOSAuth,
|
||||
refreshCloudBackupStatus,
|
||||
refreshBackupSubscriptionStatus,
|
||||
setPage,
|
||||
showToast,
|
||||
}: {
|
||||
accountEntropyPool: string | undefined;
|
||||
backupKeyViewed: boolean;
|
||||
backupSubscriptionStatus?: BackupsSubscriptionType;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType;
|
||||
cloudBackupStatus?: BackupStatusType;
|
||||
localBackupFolder: string | undefined;
|
||||
i18n: LocalizerType;
|
||||
@@ -60,14 +62,29 @@ export function PreferencesBackups({
|
||||
promptOSAuth: (
|
||||
reason: PromptOSAuthReasonType
|
||||
) => Promise<PromptOSAuthResultType>;
|
||||
refreshCloudBackupStatus: () => void;
|
||||
refreshBackupSubscriptionStatus: () => void;
|
||||
setPage: (page: PreferencesBackupPage) => void;
|
||||
showToast: ShowToastAction;
|
||||
}): JSX.Element {
|
||||
}): JSX.Element | null {
|
||||
const [authError, setAuthError] =
|
||||
useState<Omit<PromptOSAuthResultType, 'success'>>();
|
||||
const [isAuthPending, setIsAuthPending] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (page === Page.Backups) {
|
||||
refreshBackupSubscriptionStatus();
|
||||
} else if (page === Page.BackupsDetails) {
|
||||
refreshBackupSubscriptionStatus();
|
||||
refreshCloudBackupStatus();
|
||||
}
|
||||
}, [page, refreshBackupSubscriptionStatus, refreshCloudBackupStatus]);
|
||||
|
||||
if (page === Page.BackupsDetails) {
|
||||
if (backupSubscriptionStatus.status === 'off') {
|
||||
setPage(Page.Backups);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<BackupsDetailsPage
|
||||
i18n={i18n}
|
||||
@@ -116,7 +133,28 @@ export function PreferencesBackups({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{backupSubscriptionStatus ? (
|
||||
{backupSubscriptionStatus.status === 'off' ? (
|
||||
<SettingsRow className="Preferences--BackupsRow">
|
||||
<Control
|
||||
icon="Preferences__BackupsIcon"
|
||||
left={
|
||||
<label>
|
||||
{i18n('icu:Preferences--signal-backups')}{' '}
|
||||
<div className="Preferences--backup-details__value">
|
||||
<I18n
|
||||
id="icu:Preferences--signal-backups-off-description"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
learnMoreLink,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
right={null}
|
||||
/>
|
||||
</SettingsRow>
|
||||
) : (
|
||||
<SettingsRow className="Preferences--BackupsRow">
|
||||
<FlowingControl>
|
||||
<div className="Preferences__two-thirds-flow">
|
||||
@@ -149,27 +187,6 @@ export function PreferencesBackups({
|
||||
</div>
|
||||
</FlowingControl>
|
||||
</SettingsRow>
|
||||
) : (
|
||||
<SettingsRow className="Preferences--BackupsRow">
|
||||
<Control
|
||||
icon="Preferences__BackupsIcon"
|
||||
left={
|
||||
<label>
|
||||
{i18n('icu:Preferences--signal-backups')}{' '}
|
||||
<div className="Preferences--backup-details__value">
|
||||
<I18n
|
||||
id="icu:Preferences--signal-backups-off-description"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
learnMoreLink,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
right={null}
|
||||
/>
|
||||
</SettingsRow>
|
||||
)}
|
||||
|
||||
<SettingsRow
|
||||
@@ -264,10 +281,10 @@ function getSubscriptionDetails({
|
||||
/ month
|
||||
</div>
|
||||
) : null}
|
||||
{subscriptionStatus.renewalDate ? (
|
||||
{subscriptionStatus.renewalTimestamp ? (
|
||||
<div className="Preferences--backups-summary__renewal-date">
|
||||
{i18n('icu:Preferences--backup-plan__renewal-date', {
|
||||
date: formatTimestamp(subscriptionStatus.renewalDate.getTime(), {
|
||||
date: formatTimestamp(subscriptionStatus.renewalTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
}),
|
||||
})}
|
||||
@@ -282,10 +299,10 @@ function getSubscriptionDetails({
|
||||
<div className="Preferences--backups-summary__canceled">
|
||||
{i18n('icu:Preferences--backup-plan__canceled')}
|
||||
</div>
|
||||
{subscriptionStatus.expiryDate ? (
|
||||
{subscriptionStatus.expiryTimestamp ? (
|
||||
<div className="Preferences--backups-summary__expiry-date">
|
||||
{i18n('icu:Preferences--backup-plan__expiry-date', {
|
||||
date: formatTimestamp(subscriptionStatus.expiryDate.getTime(), {
|
||||
date: formatTimestamp(subscriptionStatus.expiryTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
}),
|
||||
})}
|
||||
@@ -313,6 +330,8 @@ export function renderBackupsSubscriptionDetails({
|
||||
|
||||
const { status } = subscriptionStatus;
|
||||
switch (status) {
|
||||
case 'off':
|
||||
return null;
|
||||
case 'active':
|
||||
case 'pending-cancellation':
|
||||
return (
|
||||
@@ -398,6 +417,8 @@ export function renderBackupsSubscriptionSummary({
|
||||
|
||||
const { status } = subscriptionStatus;
|
||||
switch (status) {
|
||||
case 'off':
|
||||
return null;
|
||||
case 'active':
|
||||
case 'pending-cancellation':
|
||||
return (
|
||||
@@ -449,7 +470,7 @@ function BackupsDetailsPage({
|
||||
locale,
|
||||
}: {
|
||||
cloudBackupStatus?: BackupStatusType;
|
||||
backupSubscriptionStatus?: BackupsSubscriptionType;
|
||||
backupSubscriptionStatus: BackupsSubscriptionType;
|
||||
i18n: LocalizerType;
|
||||
locale: string;
|
||||
}): JSX.Element {
|
||||
@@ -468,7 +489,7 @@ function BackupsDetailsPage({
|
||||
className="Preferences--backup-details"
|
||||
title={i18n('icu:Preferences--backup-details__header')}
|
||||
>
|
||||
{cloudBackupStatus.createdAt ? (
|
||||
{cloudBackupStatus.createdTimestamp ? (
|
||||
<div className="Preferences--backup-details__row">
|
||||
<label>{i18n('icu:Preferences--backup-created-at__label')}</label>
|
||||
<div
|
||||
@@ -478,7 +499,7 @@ function BackupsDetailsPage({
|
||||
{/* TODO (DESKTOP-8509) */}
|
||||
{i18n('icu:Preferences--backup-created-by-phone')}
|
||||
<span className="Preferences--backup-details__value-divider" />
|
||||
{formatTimestamp(cloudBackupStatus.createdAt, {
|
||||
{formatTimestamp(cloudBackupStatus.createdTimestamp, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}
|
||||
|
||||
@@ -253,14 +253,14 @@ export class BackupAPI {
|
||||
return {
|
||||
status: 'pending-cancellation',
|
||||
cost,
|
||||
expiryDate: endOfCurrentPeriod,
|
||||
expiryTimestamp: endOfCurrentPeriod?.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'active',
|
||||
cost,
|
||||
renewalDate: endOfCurrentPeriod,
|
||||
renewalTimestamp: endOfCurrentPeriod?.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import { prependStream } from '../../util/prependStream';
|
||||
import { appendMacStream } from '../../util/appendMacStream';
|
||||
import { getMacAndUpdateHmac } from '../../util/getMacAndUpdateHmac';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { DAY, HOUR, MINUTE } from '../../util/durations';
|
||||
import { DAY, HOUR, SECOND } from '../../util/durations';
|
||||
import type { ExplodePromiseResultType } from '../../util/explodePromise';
|
||||
import { explodePromise } from '../../util/explodePromise';
|
||||
import type { RetryBackupImportValue } from '../../state/ducks/installer';
|
||||
@@ -153,11 +153,11 @@ export class BackupsService {
|
||||
public readonly credentials = new BackupCredentials();
|
||||
public readonly api = new BackupAPI(this.credentials);
|
||||
public readonly throttledFetchCloudBackupStatus = throttle(
|
||||
MINUTE,
|
||||
30 * SECOND,
|
||||
this.fetchCloudBackupStatus.bind(this)
|
||||
);
|
||||
public readonly throttledFetchSubscriptionStatus = throttle(
|
||||
MINUTE,
|
||||
30 * SECOND,
|
||||
this.fetchSubscriptionStatus.bind(this)
|
||||
);
|
||||
|
||||
@@ -986,8 +986,7 @@ export class BackupsService {
|
||||
} catch (error) {
|
||||
log.error('Backup: periodic refresh failed', Errors.toLogFormat(error));
|
||||
}
|
||||
drop(this.fetchCloudBackupStatus());
|
||||
drop(this.fetchSubscriptionStatus());
|
||||
await this.refreshBackupAndSubscriptionStatus();
|
||||
}
|
||||
|
||||
async #unlinkAndDeleteAllData() {
|
||||
@@ -1064,24 +1063,15 @@ export class BackupsService {
|
||||
}
|
||||
}
|
||||
|
||||
async #getBackedUpMediaSize(): Promise<number> {
|
||||
const backupInfo = await this.api.getInfo(BackupCredentialType.Media);
|
||||
return backupInfo.usedSpace ?? 0;
|
||||
}
|
||||
|
||||
async fetchCloudBackupStatus(): Promise<BackupStatusType | undefined> {
|
||||
let result: BackupStatusType | undefined;
|
||||
const [backupProtoInfo, mediaSize] = await Promise.all([
|
||||
this.api.getBackupProtoInfo(),
|
||||
this.#getBackedUpMediaSize(),
|
||||
]);
|
||||
const backupProtoInfo = await this.api.getBackupProtoInfo();
|
||||
|
||||
if (backupProtoInfo.backupExists) {
|
||||
const { createdAt, size: protoSize } = backupProtoInfo;
|
||||
result = {
|
||||
createdAt: createdAt.getTime(),
|
||||
createdTimestamp: createdAt.getTime(),
|
||||
protoSize,
|
||||
mediaSize,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1097,6 +1087,10 @@ export class BackupsService {
|
||||
switch (backupTier) {
|
||||
case null:
|
||||
case undefined:
|
||||
result = {
|
||||
status: 'off',
|
||||
};
|
||||
break;
|
||||
case BackupLevel.Free:
|
||||
result = {
|
||||
status: 'free',
|
||||
@@ -1114,6 +1108,13 @@ export class BackupsService {
|
||||
return result;
|
||||
}
|
||||
|
||||
async refreshBackupAndSubscriptionStatus(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.fetchSubscriptionStatus(),
|
||||
this.fetchCloudBackupStatus(),
|
||||
]);
|
||||
}
|
||||
|
||||
hasMediaBackups(): boolean {
|
||||
return window.storage.get('backupTier') === BackupLevel.Paid;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue';
|
||||
import {
|
||||
generateBackupsSubscriberData,
|
||||
saveBackupsSubscriberData,
|
||||
saveBackupTier,
|
||||
} from '../util/backupSubscriptionData';
|
||||
import { getLinkPreviewSetting } from '../types/LinkPreview';
|
||||
import {
|
||||
@@ -1568,7 +1569,7 @@ export async function mergeAccountRecord(
|
||||
}
|
||||
|
||||
await saveBackupsSubscriberData(backupSubscriberData);
|
||||
await window.storage.put('backupTier', backupTier?.toNumber());
|
||||
await saveBackupTier(backupTier?.toNumber());
|
||||
|
||||
await window.storage.put(
|
||||
'displayBadgesOnProfile',
|
||||
|
||||
@@ -685,7 +685,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
availableSpeakers={availableSpeakers}
|
||||
backupFeatureEnabled={backupFeatureEnabled}
|
||||
backupKeyViewed={backupKeyViewed}
|
||||
backupSubscriptionStatus={backupSubscriptionStatus}
|
||||
backupSubscriptionStatus={backupSubscriptionStatus ?? { status: 'off' }}
|
||||
backupLocalBackupsEnabled={backupLocalBackupsEnabled}
|
||||
badge={badge}
|
||||
blockedCount={blockedCount}
|
||||
|
||||
@@ -1414,17 +1414,50 @@ const backupFileHeadersSchema = z.object({
|
||||
|
||||
type BackupFileHeadersType = z.infer<typeof backupFileHeadersSchema>;
|
||||
|
||||
// See: https://docs.stripe.com/currencies?presentment-currency=US
|
||||
const ZERO_DECIMAL_CURRENCIES = new Set([
|
||||
'bif',
|
||||
'clp',
|
||||
'djf',
|
||||
'gnf',
|
||||
'jpy',
|
||||
'kmf',
|
||||
'krw',
|
||||
'mga',
|
||||
'pyg',
|
||||
'rwf',
|
||||
'vnd',
|
||||
'vuv',
|
||||
'xaf',
|
||||
'xof',
|
||||
'xpf',
|
||||
]);
|
||||
const secondsTimestampToDate = z.coerce
|
||||
.number()
|
||||
.transform(sec => new Date(sec * 1_000));
|
||||
|
||||
const subscriptionResponseSchema = z.object({
|
||||
subscription: z
|
||||
.object({
|
||||
level: z.number(),
|
||||
billingCycleAnchor: z.coerce.date().optional(),
|
||||
endOfCurrentPeriod: z.coerce.date().optional(),
|
||||
billingCycleAnchor: secondsTimestampToDate.optional(),
|
||||
endOfCurrentPeriod: secondsTimestampToDate.optional(),
|
||||
active: z.boolean(),
|
||||
cancelAtPeriodEnd: z.boolean().optional(),
|
||||
currency: z.string().optional(),
|
||||
amount: z.number().nonnegative().optional(),
|
||||
})
|
||||
.transform(data => {
|
||||
const result = { ...data };
|
||||
if (result.currency && result.amount) {
|
||||
result.amount = ZERO_DECIMAL_CURRENCIES.has(
|
||||
result.currency.toLowerCase()
|
||||
)
|
||||
? result.amount
|
||||
: result.amount / 100;
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
|
||||
@@ -36,14 +36,13 @@ export type SubscriptionCostType = {
|
||||
};
|
||||
|
||||
export type BackupStatusType = {
|
||||
createdAt?: number;
|
||||
createdTimestamp?: number;
|
||||
protoSize?: number;
|
||||
mediaSize?: number;
|
||||
};
|
||||
|
||||
export type BackupsSubscriptionType =
|
||||
| {
|
||||
status: 'not-found' | 'expired';
|
||||
status: 'off' | 'not-found' | 'expired';
|
||||
}
|
||||
| {
|
||||
status: 'free';
|
||||
@@ -52,12 +51,12 @@ export type BackupsSubscriptionType =
|
||||
| (
|
||||
| {
|
||||
status: 'active';
|
||||
renewalDate?: Date;
|
||||
renewalTimestamp?: number;
|
||||
cost?: SubscriptionCostType;
|
||||
}
|
||||
| {
|
||||
status: 'pending-cancellation';
|
||||
expiryDate?: Date;
|
||||
expiryTimestamp?: number;
|
||||
cost?: SubscriptionCostType;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Long from 'long';
|
||||
import type { Backups, SignalService } from '../protobuf';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { drop } from './drop';
|
||||
|
||||
// These two proto messages (Backups.AccountData.IIAPSubscriberData &&
|
||||
// SignalService.AccountRecord.IIAPSubscriberData) should remain in sync. If they drift,
|
||||
@@ -15,6 +16,12 @@ export async function saveBackupsSubscriberData(
|
||||
| null
|
||||
| undefined
|
||||
): Promise<void> {
|
||||
const previousSubscriberId = window.storage.get('backupsSubscriberId');
|
||||
|
||||
if (previousSubscriberId !== backupsSubscriberData?.subscriberId) {
|
||||
drop(window.Signal.Services.backups.refreshBackupAndSubscriptionStatus());
|
||||
}
|
||||
|
||||
if (backupsSubscriberData == null) {
|
||||
await window.storage.remove('backupsSubscriberId');
|
||||
await window.storage.remove('backupsSubscriberPurchaseToken');
|
||||
@@ -47,6 +54,16 @@ export async function saveBackupsSubscriberData(
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveBackupTier(
|
||||
backupTier: number | undefined
|
||||
): Promise<void> {
|
||||
const previousBackupTier = window.storage.get('backupTier');
|
||||
await window.storage.put('backupTier', backupTier);
|
||||
if (backupTier !== previousBackupTier) {
|
||||
drop(window.Signal.Services.backups.refreshBackupAndSubscriptionStatus());
|
||||
}
|
||||
}
|
||||
|
||||
export function generateBackupsSubscriberData(): Backups.AccountData.IIAPSubscriberData | null {
|
||||
const backupsSubscriberId = window.storage.get('backupsSubscriberId');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user