mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: renew push for every signed-in account and show each account's push state in the notification settings
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
|||||||
getInitialNotificationTap,
|
getInitialNotificationTap,
|
||||||
getStoredRelayBaseUrl,
|
getStoredRelayBaseUrl,
|
||||||
notificationTapJmapAccountId,
|
notificationTapJmapAccountId,
|
||||||
|
renewOtherPushAccounts,
|
||||||
resyncPushNotifications,
|
resyncPushNotifications,
|
||||||
teardownPushNotificationsForAccount,
|
teardownPushNotificationsForAccount,
|
||||||
type NotificationTapPayload,
|
type NotificationTapPayload,
|
||||||
@@ -672,13 +673,16 @@ export default function App() {
|
|||||||
relayBaseUrl,
|
relayBaseUrl,
|
||||||
accountLabel: client.username ?? undefined,
|
accountLabel: client.username ?? undefined,
|
||||||
});
|
});
|
||||||
if (cancelled) return;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[push] relay setup failed:',
|
'[push] relay setup failed:',
|
||||||
error instanceof Error ? error.message : error,
|
error instanceof Error ? error.message : error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (cancelled) return;
|
||||||
|
// The other signed-in accounts' subscriptions expire too (webmail #1054); each
|
||||||
|
// is renewed through a client of its own.
|
||||||
|
await renewOtherPushAccounts().catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
void doSetup();
|
void doSetup();
|
||||||
|
|||||||
@@ -1041,6 +1041,7 @@
|
|||||||
"sound_channel_hint": "Sound and vibration for mail alerts are controlled by the Android notification channel: long-press a notification or open the system app settings."
|
"sound_channel_hint": "Sound and vibration for mail alerts are controlled by the Android notification channel: long-press a notification or open the system app settings."
|
||||||
},
|
},
|
||||||
"push": {
|
"push": {
|
||||||
|
"accounts_desc_mobile": "Background notifications of each signed-in account on this device. Switch to an account to turn them on or off for it.",
|
||||||
"confirm_disable_message_mobile": "This device will stop receiving new-mail alerts for this account.",
|
"confirm_disable_message_mobile": "This device will stop receiving new-mail alerts for this account.",
|
||||||
"description_mobile": "Receive system notifications for new mail when the app is closed. Delivered via the Bulwark push relay; the relay never sees mail content.",
|
"description_mobile": "Receive system notifications for new mail when the app is closed. Delivered via the Bulwark push relay; the relay never sees mail content.",
|
||||||
"err_android_only": "Push notifications are only available on Android.",
|
"err_android_only": "Push notifications are only available on Android.",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Dialog from '../Dialog';
|
|||||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||||
import { useColors } from '../../theme/colors';
|
import { useColors } from '../../theme/colors';
|
||||||
|
import { useAccountStore } from '../../stores/account-store';
|
||||||
import { useAuthStore } from '../../stores/auth-store';
|
import { useAuthStore } from '../../stores/auth-store';
|
||||||
import { useEmailStore } from '../../stores/email-store';
|
import { useEmailStore } from '../../stores/email-store';
|
||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
@@ -28,12 +29,14 @@ import {
|
|||||||
mailAccountsWithoutPush,
|
mailAccountsWithoutPush,
|
||||||
PushSetupError,
|
PushSetupError,
|
||||||
readPushAccountIds,
|
readPushAccountIds,
|
||||||
|
readPushAccountStates,
|
||||||
revokePushDevice,
|
revokePushDevice,
|
||||||
serverSupportsEmailPush,
|
serverSupportsEmailPush,
|
||||||
setStoredRelayBaseUrl,
|
setStoredRelayBaseUrl,
|
||||||
enablePushFromSettings,
|
enablePushFromSettings,
|
||||||
getStoredPushTransport,
|
getStoredPushTransport,
|
||||||
setStoredPushTransport,
|
setStoredPushTransport,
|
||||||
|
type PushAccountState,
|
||||||
type PushDevice,
|
type PushDevice,
|
||||||
type PushSetupPhase,
|
type PushSetupPhase,
|
||||||
type PushTransport,
|
type PushTransport,
|
||||||
@@ -178,6 +181,22 @@ export function NotificationSettings() {
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [supported, client, mailboxes, pushStatus.kind]);
|
}, [supported, client, mailboxes, pushStatus.kind]);
|
||||||
|
|
||||||
|
// Push is set up per account, and the switch above is the shown account's.
|
||||||
|
// List every signed-in account's state so the others aren't a guess (webmail #1054).
|
||||||
|
const accounts = useAccountStore((s) => s.accounts);
|
||||||
|
const accountIdsKey = accounts.map((a) => a.id).join('\n');
|
||||||
|
const [accountStates, setAccountStates] = useState<Record<string, PushAccountState>>({});
|
||||||
|
useEffect(() => {
|
||||||
|
if (!supported || accounts.length < 2) return;
|
||||||
|
let cancelled = false;
|
||||||
|
void readPushAccountStates(accounts.map((a) => a.id)).then(
|
||||||
|
(states) => { if (!cancelled) setAccountStates(states); },
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [supported, accountIdsKey, pushEnabled, pushStatus.kind]);
|
||||||
|
|
||||||
const describeError = (error: unknown): string => {
|
const describeError = (error: unknown): string => {
|
||||||
if (error instanceof PushSetupError) {
|
if (error instanceof PushSetupError) {
|
||||||
const phase = PHASE_KEYS[error.phase];
|
const phase = PHASE_KEYS[error.phase];
|
||||||
@@ -500,6 +519,34 @@ export function NotificationSettings() {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{supported && accounts.length > 1 && (
|
||||||
|
<SettingsSection
|
||||||
|
title={t('settings.notifications.push_all.title', 'All accounts')}
|
||||||
|
description={t(
|
||||||
|
'settings.notifications.push.accounts_desc_mobile',
|
||||||
|
'Background notifications of each signed-in account on this device. Switch to an account to turn them on or off for it.',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{accounts.map((account, index) => {
|
||||||
|
const on = accountStates[account.id]?.enabled === true;
|
||||||
|
const address = account.email || account.username;
|
||||||
|
const state = on
|
||||||
|
? t('settings.notifications.push_all.state_on', 'notifications on')
|
||||||
|
: t('settings.notifications.push_all.state_off', 'notifications off');
|
||||||
|
return (
|
||||||
|
<SettingItem
|
||||||
|
key={account.id}
|
||||||
|
label={account.displayName || address}
|
||||||
|
description={`${address} · ${state}`}
|
||||||
|
noBorder={index === accounts.length - 1}
|
||||||
|
>
|
||||||
|
{on ? <CheckCircle2 size={16} color={c.success} /> : <View />}
|
||||||
|
</SettingItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={t('settings.notifications.email.title', 'Email Notifications')}
|
title={t('settings.notifications.email.title', 'Email Notifications')}
|
||||||
description={t('settings.notifications.email.description', 'Configure notifications for incoming emails')}
|
description={t('settings.notifications.email.description', 'Configure notifications for incoming emails')}
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ const { DETACHED, SYNC_SESSION } = vi.hoisted(() => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
types: ['EmailDelivery'] as string[],
|
types: ['EmailDelivery'] as string[],
|
||||||
deviceClientId: null as string | null,
|
deviceClientId: null as string | null,
|
||||||
|
// The subscription's expiry, and what an update of it settles on.
|
||||||
|
expires: null as string | null,
|
||||||
|
settles: null as string | null,
|
||||||
log: [] as unknown[],
|
log: [] as unknown[],
|
||||||
session: SYNC_SESSION,
|
session: SYNC_SESSION,
|
||||||
},
|
},
|
||||||
@@ -84,14 +87,29 @@ vi.mock('../../api/jmap-client', () => ({
|
|||||||
DETACHED.log.push(['load', id]);
|
DETACHED.log.push(['load', id]);
|
||||||
return DETACHED.loaded;
|
return DETACHED.loaded;
|
||||||
}
|
}
|
||||||
|
get username() {
|
||||||
|
return 'bob';
|
||||||
|
}
|
||||||
|
get accountId() {
|
||||||
|
return 'bob-jmap';
|
||||||
|
}
|
||||||
async request(calls: Array<[string, Record<string, any>, string]>) {
|
async request(calls: Array<[string, Record<string, any>, string]>) {
|
||||||
const [name, args, id] = calls[0];
|
const methodResponses = calls.map(([name, args, id]) => {
|
||||||
DETACHED.log.push([name, args]);
|
DETACHED.log.push([name, args]);
|
||||||
if (name === 'PushSubscription/get') {
|
if (name === 'PushSubscription/get') {
|
||||||
const listed = { id: args.ids[0], types: DETACHED.types, ...(DETACHED.deviceClientId ? { deviceClientId: DETACHED.deviceClientId } : {}) };
|
const listed = {
|
||||||
return { methodResponses: [[name, { list: [listed] }, id]] };
|
id: args.ids[0],
|
||||||
}
|
types: DETACHED.types,
|
||||||
return { methodResponses: [[name, { updated: { [Object.keys(args.update)[0]]: null } }, id]] };
|
...(DETACHED.deviceClientId ? { deviceClientId: DETACHED.deviceClientId } : {}),
|
||||||
|
...(DETACHED.expires ? { expires: DETACHED.expires } : {}),
|
||||||
|
};
|
||||||
|
return [name, { list: [listed] }, id];
|
||||||
|
}
|
||||||
|
const target = Object.keys(args.update)[0];
|
||||||
|
if (args.update[target].expires && DETACHED.settles) DETACHED.expires = DETACHED.settles;
|
||||||
|
return [name, { updated: { [target]: null } }, id];
|
||||||
|
});
|
||||||
|
return { methodResponses };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -126,7 +144,9 @@ import {
|
|||||||
readPushJmapAccountIds,
|
readPushJmapAccountIds,
|
||||||
PushSetupError,
|
PushSetupError,
|
||||||
pushTypesFor,
|
pushTypesFor,
|
||||||
|
readPushAccountStates,
|
||||||
refreshPushSubscriptionTypes,
|
refreshPushSubscriptionTypes,
|
||||||
|
renewOtherPushAccounts,
|
||||||
resyncPushNotifications,
|
resyncPushNotifications,
|
||||||
revokePushDevice,
|
revokePushDevice,
|
||||||
teardownPushNotificationsForAccount,
|
teardownPushNotificationsForAccount,
|
||||||
@@ -1683,6 +1703,78 @@ describe('resyncPushNotifications', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('renewOtherPushAccounts (#1054)', () => {
|
||||||
|
const OTHER = 'bob@other.example.net';
|
||||||
|
const OTHER_DCID = 'bobsphonebobsphonebobsphonebobsp';
|
||||||
|
const inDays = (days: number) => new Date(Date.now() + days * 86400000).toISOString();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
await AsyncStorage.clear();
|
||||||
|
await AsyncStorage.setItem('push:relayBaseUrl:v1', RELAY);
|
||||||
|
await AsyncStorage.setItem('push:accountIds:v1', JSON.stringify([ACCOUNT_ID, OTHER]));
|
||||||
|
await AsyncStorage.setItem(SUB_KEY, 'existing');
|
||||||
|
await AsyncStorage.setItem(`push:subscriptionId:v2:${OTHER}`, 'b');
|
||||||
|
await AsyncStorage.setItem(deviceClientIdKey(OTHER), OTHER_DCID);
|
||||||
|
Object.assign(DETACHED, { loaded: true, deviceClientId: OTHER_DCID, expires: inDays(2), settles: inDays(7), log: [] });
|
||||||
|
installFetch({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.assign(DETACHED, { deviceClientId: null, expires: null, settles: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extends another account's subscription through a client of its own and records the server's expiry", async () => {
|
||||||
|
const settles = DETACHED.settles;
|
||||||
|
|
||||||
|
await renewOtherPushAccounts();
|
||||||
|
|
||||||
|
const calls = DETACHED.log.map((entry) => (entry as unknown[])[0]);
|
||||||
|
// The loaded account is the launch-time resync's; only the other is touched here.
|
||||||
|
expect(calls).toEqual(['load', 'PushSubscription/get', 'PushSubscription/set', 'PushSubscription/get']);
|
||||||
|
expect(DETACHED.log[0]).toEqual(['load', OTHER]);
|
||||||
|
expect(updateMock).not.toHaveBeenCalled();
|
||||||
|
expect(listMock).not.toHaveBeenCalled();
|
||||||
|
expect(await AsyncStorage.getItem(`push:subscriptionExpires:v1:${OTHER}`)).toBe(settles);
|
||||||
|
// Its relay registration carries this device's current token.
|
||||||
|
const fetchMock = global.fetch as ReturnType<typeof vi.fn>;
|
||||||
|
const registration = fetchMock.mock.calls.find((c) => String(c[0]) === `${RELAY}/api/push/register`);
|
||||||
|
expect(JSON.parse(String((registration![1] as RequestInit).body))).toEqual({
|
||||||
|
subscriptionId: OTHER_DCID,
|
||||||
|
fcmToken: 'fcm-token-xyz',
|
||||||
|
accountLabel: 'bob',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only records the expiry of a subscription with more than a week left', async () => {
|
||||||
|
DETACHED.expires = inDays(30);
|
||||||
|
await renewOtherPushAccounts();
|
||||||
|
expect(DETACHED.log.map((entry) => (entry as unknown[])[0])).toEqual(['load', 'PushSubscription/get']);
|
||||||
|
expect(await AsyncStorage.getItem(`push:subscriptionExpires:v1:${OTHER}`)).toBe(DETACHED.expires);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves alone another client's subscription that reuses the id, and an account push was turned off for", async () => {
|
||||||
|
DETACHED.deviceClientId = 'webmailwebmailwebmailwebmailwebm';
|
||||||
|
await renewOtherPushAccounts();
|
||||||
|
expect(DETACHED.log.map((entry) => (entry as unknown[])[0])).toEqual(['load', 'PushSubscription/get']);
|
||||||
|
|
||||||
|
DETACHED.log = [];
|
||||||
|
await AsyncStorage.setItem(`push:optedOut:v1:${OTHER}`, '1');
|
||||||
|
await renewOtherPushAccounts();
|
||||||
|
expect(DETACHED.log).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tells the settings which accounts have push on this device', async () => {
|
||||||
|
await AsyncStorage.setItem(`push:optedOut:v1:${OTHER}`, '1');
|
||||||
|
await AsyncStorage.removeItem(`push:subscriptionId:v2:${OTHER}`);
|
||||||
|
expect(await readPushAccountStates([ACCOUNT_ID, OTHER, 'carl@example.org'])).toEqual({
|
||||||
|
[ACCOUNT_ID]: { enabled: true, optedOut: false },
|
||||||
|
[OTHER]: { enabled: false, optedOut: true },
|
||||||
|
'carl@example.org': { enabled: false, optedOut: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('teardownPushNotificationsForAccount', () => {
|
describe('teardownPushNotificationsForAccount', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { credentialsKey } from '../api/jmap-client';
|
|||||||
import { apnsEnvironment, removeIosPushAccount, writeIosPushAccount } from './ios-push-shared';
|
import { apnsEnvironment, removeIosPushAccount, writeIosPushAccount } from './ios-push-shared';
|
||||||
import {
|
import {
|
||||||
getUnifiedPushDistributors,
|
getUnifiedPushDistributors,
|
||||||
|
getUnifiedPushEndpoint,
|
||||||
isUnifiedPushSupported,
|
isUnifiedPushSupported,
|
||||||
registerUnifiedPush,
|
registerUnifiedPush,
|
||||||
unregisterUnifiedPush,
|
unregisterUnifiedPush,
|
||||||
@@ -1853,6 +1854,121 @@ export async function refreshPushSubscriptionTypes(accountId: string): Promise<v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep push alive for the signed-in accounts the app is not showing (webmail #1054).
|
||||||
|
* The launch-time resync works on the loaded account only, and Stalwart keeps
|
||||||
|
* a subscription for seven days from its last refresh, so another account's
|
||||||
|
* push stopped about a week after the user last switched to it. Each account
|
||||||
|
* goes through a client of its own, never the singleton: its registration
|
||||||
|
* with the relay is renewed (the device token may have rotated since), its
|
||||||
|
* subscription's expiry pushed forward and the expiry the server settled on
|
||||||
|
* recorded. A subscription the server no longer has - lapsed, or revoked
|
||||||
|
* elsewhere - is left to the account's own resync, which can tell the two
|
||||||
|
* apart and verify a new one; so are its delivery filter and push types.
|
||||||
|
* Best effort: whatever fails is tried again on the next launch.
|
||||||
|
*/
|
||||||
|
export async function renewOtherPushAccounts(): Promise<void> {
|
||||||
|
const username = jmapClient.username;
|
||||||
|
const serverUrl = jmapClient.serverUrl;
|
||||||
|
const served = username && serverUrl ? generateAccountId(username, serverUrl) : null;
|
||||||
|
const relayBaseUrl = await getStoredRelayBaseUrl();
|
||||||
|
for (const accountId of await readPushAccountIds()) {
|
||||||
|
if (accountId === served) continue;
|
||||||
|
try {
|
||||||
|
await renewPushAccount(accountId, relayBaseUrl);
|
||||||
|
} catch (err) {
|
||||||
|
logPhase('renew', `${accountId}: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListedSubscription = { id: string; deviceClientId?: string | null; expires?: string | null };
|
||||||
|
|
||||||
|
async function renewPushAccount(accountId: string, relayBaseUrl: string | null): Promise<void> {
|
||||||
|
if (await AsyncStorage.getItem(optedOutKey(accountId))) return;
|
||||||
|
const subscriptionId = await AsyncStorage.getItem(subscriptionIdKey(accountId));
|
||||||
|
if (!subscriptionId) return;
|
||||||
|
const deviceClientId = await AsyncStorage.getItem(deviceClientIdKey(accountId));
|
||||||
|
const client = new JMAPClient();
|
||||||
|
if (!(await client.loadAccount(accountId))) return;
|
||||||
|
if (relayBaseUrl && deviceClientId) {
|
||||||
|
await registerDeviceWithRelay(relayBaseUrl, deviceClientId, client.username ?? undefined).catch(() => undefined);
|
||||||
|
}
|
||||||
|
const using = [CAPABILITIES.CORE];
|
||||||
|
const properties = ['id', 'deviceClientId', 'expires'];
|
||||||
|
const ownIn = (res: Awaited<ReturnType<JMAPClient['request']>>, callId: string) =>
|
||||||
|
(requireMethodResult(res, callId, 'PushSubscription/get').list as ListedSubscription[] | undefined)
|
||||||
|
?.find((s) => isOwnSubscription(s, subscriptionId, deviceClientId));
|
||||||
|
const current = ownIn(
|
||||||
|
await client.request([['PushSubscription/get', { ids: [subscriptionId], properties }, '0']], using),
|
||||||
|
'0',
|
||||||
|
);
|
||||||
|
if (!current) return;
|
||||||
|
let expires = current.expires ?? null;
|
||||||
|
const remainingMs = Date.parse(expires ?? '') - Date.now();
|
||||||
|
if (!(remainingMs > SUBSCRIPTION_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000)) {
|
||||||
|
const res = await client.request(
|
||||||
|
[
|
||||||
|
['PushSubscription/set', { update: { [subscriptionId]: { expires: expiresFromNow(SUBSCRIPTION_EXPIRES_DAYS) } } }, '0'],
|
||||||
|
// The update's answer doesn't say what the server settled on.
|
||||||
|
['PushSubscription/get', { ids: [subscriptionId], properties }, '1'],
|
||||||
|
],
|
||||||
|
using,
|
||||||
|
);
|
||||||
|
expires = ownIn(res, '1')?.expires ?? expires;
|
||||||
|
}
|
||||||
|
if (expires) await AsyncStorage.setItem(subscriptionExpiresKey(accountId), expires);
|
||||||
|
let jmapAccountId: string | null = null;
|
||||||
|
try {
|
||||||
|
jmapAccountId = client.accountId;
|
||||||
|
} catch {
|
||||||
|
jmapAccountId = null;
|
||||||
|
}
|
||||||
|
await shareAccountWithExtension(accountId, jmapAccountId, client.currentSession?.apiUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an account's device client id with the relay again, with the
|
||||||
|
* device token or endpoint this device has now. Never asks for anything: no
|
||||||
|
* permission prompt, no new UnifiedPush registration.
|
||||||
|
*/
|
||||||
|
async function registerDeviceWithRelay(relayBaseUrl: string, deviceClientId: string, accountLabel?: string): Promise<void> {
|
||||||
|
const transport = await getEffectivePushTransport();
|
||||||
|
if (transport === 'apns') {
|
||||||
|
const token = (await Notifications.getDevicePushTokenAsync()).data;
|
||||||
|
if (typeof token !== 'string' || !token) return;
|
||||||
|
await registerWithRelayApns({ relayBaseUrl, subscriptionId: deviceClientId, deviceToken: token, accountLabel });
|
||||||
|
} else if (transport === 'unifiedpush') {
|
||||||
|
const endpoint = await getUnifiedPushEndpoint();
|
||||||
|
if (!endpoint) return;
|
||||||
|
await registerWithRelayUnifiedPush({ relayBaseUrl, subscriptionId: deviceClientId, endpoint, accountLabel });
|
||||||
|
} else {
|
||||||
|
const token = await getFcmToken();
|
||||||
|
if (!token) return;
|
||||||
|
await registerWithRelay({ relayBaseUrl, subscriptionId: deviceClientId, fcmToken: token, accountLabel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PushAccountState {
|
||||||
|
// A subscription is recorded for the account on this device.
|
||||||
|
enabled: boolean;
|
||||||
|
// The user turned push off for it here (see disablePushForAccount).
|
||||||
|
optedOut: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What this device knows about each account's push, for the settings. */
|
||||||
|
export async function readPushAccountStates(accountIds: string[]): Promise<Record<string, PushAccountState>> {
|
||||||
|
await migrateLegacyPushKeys();
|
||||||
|
const out: Record<string, PushAccountState> = {};
|
||||||
|
for (const accountId of accountIds) {
|
||||||
|
out[accountId] = {
|
||||||
|
enabled: (await AsyncStorage.getItem(subscriptionIdKey(accountId))) !== null,
|
||||||
|
optedOut: (await AsyncStorage.getItem(optedOutKey(accountId))) !== null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tear down push for ALL accounts on this device. Used by the logout-all
|
* Tear down push for ALL accounts on this device. Used by the logout-all
|
||||||
* flow; best-effort because we typically aren't authenticated to every
|
* flow; best-effort because we typically aren't authenticated to every
|
||||||
|
|||||||
Reference in New Issue
Block a user