mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 00:14:23 +00:00
fix(push): recreate a browser subscription that was lost
The background re-sync skipped any account whose browser subscription was gone (isWebPushEnabled needs a live one), which is exactly the state that needs repairing: the browser or the user cleared the endpoint while the opt-in and the server registration survived, and push stopped for good. It now runs whenever the user opted in and permission is granted, so the normal enable flow recreates the subscription. It still never prompts. A failed attempt no longer waits a day for the next one.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
disableWebPush,
|
||||
enableWebPush,
|
||||
listPushDevices,
|
||||
isWebPushEnabled,
|
||||
resetWebPushResyncState,
|
||||
resyncWebPush,
|
||||
revokePushDevice,
|
||||
@@ -274,6 +275,40 @@ describe('enableWebPush', () => {
|
||||
// repaired in the background on app start - nobody should have to find the
|
||||
// settings toggle to stop the spam pushes.
|
||||
describe('resyncWebPush', () => {
|
||||
it('repairs a lost browser subscription for an account that previously opted in', async () => {
|
||||
localStorage.setItem(DEVICE_KEY, THIS_DEVICE);
|
||||
localStorage.setItem(SUB_KEY, 'push-old');
|
||||
const { registration } = installPushBrowser();
|
||||
registration.pushManager.getSubscription.mockResolvedValue(null as never);
|
||||
const client = makeClient([sub('push-old', THIS_DEVICE)]);
|
||||
const calls = installFetch({});
|
||||
|
||||
expect(await isWebPushEnabled(ACCOUNT_ID)).toBe(false);
|
||||
expect(await resyncWebPush({ client, relayBaseUrl: RELAY, inboxOnly: false })).toBe(true);
|
||||
expect(registration.pushManager.subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(calls).toContainEqual({ url: `${RELAY}/api/push/register/web`, method: 'POST' });
|
||||
});
|
||||
|
||||
it.each(['default', 'denied'])('does not resubscribe or prompt with %s permission', async (permission) => {
|
||||
localStorage.setItem(SUB_KEY, 'push-old');
|
||||
const { registration } = installPushBrowser();
|
||||
vi.stubGlobal('Notification', { permission, requestPermission: vi.fn() });
|
||||
const client = makeClient([sub('push-old', THIS_DEVICE)]);
|
||||
expect(await resyncWebPush({ client, relayBaseUrl: RELAY, inboxOnly: false })).toBe(false);
|
||||
expect(registration.pushManager.subscribe).not.toHaveBeenCalled();
|
||||
expect(Notification.requestPermission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows retry after a transient relay failure', async () => {
|
||||
localStorage.setItem(SUB_KEY, 'push-old');
|
||||
localStorage.setItem(DEVICE_KEY, THIS_DEVICE);
|
||||
const client = makeClient([sub('push-old', THIS_DEVICE)]);
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('offline')));
|
||||
expect(await resyncWebPush({ client, relayBaseUrl: RELAY, inboxOnly: false })).toBe(false);
|
||||
installFetch({});
|
||||
expect(await resyncWebPush({ client, relayBaseUrl: RELAY, inboxOnly: false })).toBe(true);
|
||||
});
|
||||
|
||||
it('re-syncs an enabled registration and installs the missing filter', async () => {
|
||||
localStorage.setItem(DEVICE_KEY, THIS_DEVICE);
|
||||
localStorage.setItem(SUB_KEY, 'push-old');
|
||||
|
||||
+9
-2
@@ -732,7 +732,7 @@ export interface ResyncWebPushParams {
|
||||
/**
|
||||
* Bring an already-enabled push registration up to date without any user
|
||||
* action: refresh its expiry and install/repair the delivery filter. Nothing
|
||||
* here can prompt - it only runs when push is already on for the account -
|
||||
* here can prompt - it requires a saved opt-in and granted permission -
|
||||
* and every failure is swallowed because the app must not care whether the
|
||||
* background touch-up worked. Returns true when a re-sync actually ran.
|
||||
*/
|
||||
@@ -747,7 +747,12 @@ export async function resyncWebPush(params: ResyncWebPushParams): Promise<boolea
|
||||
const last = lastResyncAt.get(accountId);
|
||||
if (last !== undefined && Date.now() - last < RESYNC_INTERVAL_MS) return false;
|
||||
try {
|
||||
if (!(await isWebPushEnabled(accountId))) return false;
|
||||
// The browser may have lost its endpoint while our saved opt-in and
|
||||
// server registration survived. Recreate it through the normal enable
|
||||
// flow instead of skipping the account in that state, which is what
|
||||
// isWebPushEnabled (it requires a live browser subscription) would do.
|
||||
if (!isWebPushSupported() || Notification.permission !== 'granted'
|
||||
|| !localStorage.getItem(subscriptionIdKey(accountId))) return false;
|
||||
lastResyncAt.set(accountId, Date.now());
|
||||
await enableWebPush({
|
||||
client: params.client,
|
||||
@@ -757,6 +762,8 @@ export async function resyncWebPush(params: ResyncWebPushParams): Promise<boolea
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// A failed attempt must not wait a day for the next one.
|
||||
lastResyncAt.delete(accountId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user