mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: check push again when the app comes back to settings (bulwarkmail/webmail#1148)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AppState,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -22,7 +23,8 @@ import {
|
||||
DEFAULT_RELAY_BASE_URL,
|
||||
disablePushForAccount,
|
||||
getStoredRelayBaseUrl,
|
||||
isPushEnabledForAccount,
|
||||
isPushActiveForAccount,
|
||||
shouldRecheckPushState,
|
||||
isPushSupported,
|
||||
isValidRelayUrl,
|
||||
listPushDevices,
|
||||
@@ -118,11 +120,25 @@ export function NotificationSettings() {
|
||||
if (!hydrated) void hydrate();
|
||||
}, [hydrated, hydrate]);
|
||||
|
||||
// Read again when the app comes back to the front: notifications may have
|
||||
// been turned off in the phone's settings meanwhile (webmail #1148).
|
||||
const [recheck, setRecheck] = useState(0);
|
||||
const pushStatusKind = React.useRef(pushStatus.kind);
|
||||
pushStatusKind.current = pushStatus.kind;
|
||||
useEffect(() => {
|
||||
const sub = AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active' && shouldRecheckPushState(pushStatusKind.current)) setRecheck((n) => n + 1);
|
||||
});
|
||||
return () => sub.remove();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const stored = await getStoredRelayBaseUrl();
|
||||
const enabled = activeAccountId ? await isPushEnabledForAccount(activeAccountId) : false;
|
||||
const enabled = activeAccountId ? await isPushActiveForAccount(activeAccountId) : false;
|
||||
// An enable or disable that started meanwhile has the final word.
|
||||
if (!shouldRecheckPushState(pushStatusKind.current)) return;
|
||||
if (cancelled) return;
|
||||
if (stored) setRelayUrl(stored);
|
||||
setPushEnabled(enabled);
|
||||
@@ -137,7 +153,7 @@ export function NotificationSettings() {
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeAccountId]);
|
||||
}, [activeAccountId, recheck]);
|
||||
|
||||
const trimmed = relayUrl.trim().replace(/\/+$/, '');
|
||||
const relayValid = isValidRelayUrl(trimmed);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { isPushActiveForAccount, shouldRecheckPushState } from '../push-notifications';
|
||||
|
||||
// Settings > Notifications read the push state once, when it opened, and
|
||||
// read only whether a subscription was stored: notifications turned off in
|
||||
// the phone's settings meanwhile still showed as on (webmail #1148).
|
||||
|
||||
const ACCOUNT = 'ada@mail.example.com';
|
||||
|
||||
beforeEach(async () => {
|
||||
await AsyncStorage.clear();
|
||||
vi.mocked(Notifications.getPermissionsAsync).mockResolvedValue({ granted: true } as never);
|
||||
});
|
||||
|
||||
describe('push state', () => {
|
||||
it('reads as off once notifications are turned off in the system', async () => {
|
||||
await AsyncStorage.setItem(`push:subscriptionId:v2:${ACCOUNT}`, 'sub-1');
|
||||
await expect(isPushActiveForAccount(ACCOUNT)).resolves.toBe(true);
|
||||
vi.mocked(Notifications.getPermissionsAsync).mockResolvedValue({ granted: false } as never);
|
||||
await expect(isPushActiveForAccount(ACCOUNT)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('reads a failed check as off, and an account without push as off', async () => {
|
||||
await expect(isPushActiveForAccount(ACCOUNT)).resolves.toBe(false);
|
||||
await AsyncStorage.setItem(`push:subscriptionId:v2:${ACCOUNT}`, 'sub-1');
|
||||
vi.mocked(Notifications.getPermissionsAsync).mockRejectedValue(new Error('no bridge'));
|
||||
await expect(isPushActiveForAccount(ACCOUNT)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('is read again on return unless an enable or disable runs or just failed', () => {
|
||||
expect(shouldRecheckPushState('enabled')).toBe(true);
|
||||
expect(shouldRecheckPushState('idle')).toBe(true);
|
||||
expect(shouldRecheckPushState('busy')).toBe(false);
|
||||
expect(shouldRecheckPushState('error')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -793,6 +793,31 @@ export async function isPushEnabledForAccount(accountId: string): Promise<boolea
|
||||
return (await AsyncStorage.getItem(subscriptionIdKey(accountId))) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether push works for the account right now: it is set up, and the
|
||||
* system still lets the app show notifications (they can be turned off in
|
||||
* the phone's settings while the app is in the background). A failed check
|
||||
* reads as off (webmail #1148).
|
||||
*/
|
||||
export async function isPushActiveForAccount(accountId: string): Promise<boolean> {
|
||||
try {
|
||||
if (!(await isPushEnabledForAccount(accountId))) return false;
|
||||
const permission = await Notifications.getPermissionsAsync();
|
||||
return permission.granted !== false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Settings > Notifications should read the push state again when
|
||||
* the app comes back: not while an enable or disable runs, nor right after
|
||||
* one failed, which would wipe its message (webmail #1148).
|
||||
*/
|
||||
export function shouldRecheckPushState(status: 'idle' | 'busy' | 'enabled' | 'error'): boolean {
|
||||
return status !== 'busy' && status !== 'error';
|
||||
}
|
||||
|
||||
export async function wasPushPromptDismissed(accountId: string): Promise<boolean> {
|
||||
return (await AsyncStorage.getItem(promptDismissedKey(accountId))) !== null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user