diff --git a/src/components/settings/NotificationSettings.tsx b/src/components/settings/NotificationSettings.tsx index 2b3b1c85..8377b731 100644 --- a/src/components/settings/NotificationSettings.tsx +++ b/src/components/settings/NotificationSettings.tsx @@ -31,7 +31,7 @@ import { revokePushDevice, serverSupportsEmailPush, setStoredRelayBaseUrl, - setupPushNotifications, + enablePushFromSettings, getStoredPushTransport, setStoredPushTransport, type PushDevice, @@ -137,11 +137,13 @@ export function NotificationSettings() { const relayValid = isValidRelayUrl(trimmed); const busy = pushStatus.kind === 'busy'; - const refreshDevices = useCallback(async () => { + // `relay` is the relay an enable run just settled on: the field's state + // has not caught up with it yet. + const refreshDevices = useCallback(async (relay?: string) => { if (!activeAccountId || !client) return; setDevices({ kind: 'loading' }); try { - const list = await listPushDevices({ accountId: activeAccountId, relayBaseUrl: trimmed }); + const list = await listPushDevices({ accountId: activeAccountId, relayBaseUrl: relay ?? trimmed }); setDevices({ kind: 'loaded', devices: list }); } catch { setDevices({ kind: 'error' }); @@ -196,21 +198,19 @@ export function NotificationSettings() { kind: 'busy', message: t('settings.notifications.push.status_busy', 'Working…'), }); - try { - // The setup stores the relay itself, and keeps the one in use when the - // mail server refuses the new one's address. - await setupPushNotifications({ - relayBaseUrl: trimmed, - accountLabel: username ?? undefined, - forceRecreate, - }); - setPushEnabled(true); - setPushStatus({ kind: 'enabled' }); - } catch (error) { - setPushStatus({ kind: 'error', message: describeError(error) }); - } finally { - void refreshDevices(); - } + // The setup stores the relay itself, and keeps the one in use when the + // mail server refuses the new one's address: the field follows it. + const outcome = await enablePushFromSettings({ + relayBaseUrl: trimmed, + accountLabel: username ?? undefined, + forceRecreate, + }); + setRelayUrl(outcome.relayBaseUrl); + setPushEnabled(outcome.enabled); + setPushStatus(outcome.error + ? { kind: 'error', message: describeError(outcome.error) } + : { kind: 'enabled' }); + void refreshDevices(outcome.relayBaseUrl); }; // Switching the transport while push is on re-registers immediately so the diff --git a/src/lib/__tests__/push-notifications.test.ts b/src/lib/__tests__/push-notifications.test.ts index 97c37630..54acc715 100644 --- a/src/lib/__tests__/push-notifications.test.ts +++ b/src/lib/__tests__/push-notifications.test.ts @@ -110,6 +110,7 @@ import { setupPushNotifications, deviceClientIdKey, disablePushForAccount, + enablePushFromSettings, forgetPushServerState, isValidRelayUrl, mailAccountsWithoutPush, @@ -420,6 +421,31 @@ describe('setupPushNotifications subscription shape', () => { expect(fetchMock.mock.calls.some((c) => (c[1] as RequestInit | undefined)?.method === 'DELETE')).toBe(false); }); + it('hands the settings the relay in use back after the server refused a new one', async () => { + await AsyncStorage.setItem('push:relayBaseUrl:v1', RELAY); + await AsyncStorage.setItem(SUB_KEY, 'existing'); + listMock.mockResolvedValue([{ id: 'existing', deviceClientId: OUR_DCID, expires: new Date(Date.now() + 5 * 86400000).toISOString(), types: ['EmailDelivery'] }]); + createMock.mockRejectedValueOnce(new JMAPSetError('invalidProperties', 'Host is not a public domain name', ['url'])); + + const outcome = await enablePushFromSettings({ relayBaseUrl: 'https://relay.home.lan/', forceRecreate: true }); + + expect(outcome.error).toBeInstanceOf(PushSetupError); + expect(outcome.relayBaseUrl).toBe(RELAY); + expect(outcome.enabled).toBe(true); + + // Nothing in use yet: the typed relay stays, so it can be corrected. + await AsyncStorage.clear(); + await AsyncStorage.setItem(deviceClientIdKey(ACCOUNT_ID), OUR_DCID); + listMock.mockResolvedValue([]); + createMock.mockRejectedValueOnce(new JMAPSetError('invalidProperties', 'Host is not a public domain name', ['url'])); + expect(await enablePushFromSettings({ relayBaseUrl: 'https://relay.home.lan/' })) + .toMatchObject({ relayBaseUrl: 'https://relay.home.lan', enabled: false }); + + // A run that works shows the relay it registered with. + expect(await enablePushFromSettings({ relayBaseUrl: 'https://relay.example.com/' })) + .toEqual({ error: null, relayBaseUrl: RELAY, enabled: true }); + }); + it('rejects plain-http relay URLs', async () => { await expect(setupPushNotifications({ relayBaseUrl: 'http://relay.example.com' })).rejects.toMatchObject({ phase: 'relay' }); expect(createMock).not.toHaveBeenCalled(); diff --git a/src/lib/push-notifications.ts b/src/lib/push-notifications.ts index 918af9ab..e0758ffa 100644 --- a/src/lib/push-notifications.ts +++ b/src/lib/push-notifications.ts @@ -1706,6 +1706,40 @@ export async function resyncPushNotifications( return setupPushNotifications(params); } +export interface PushEnableOutcome { + error: unknown; + // The relay the settings should show afterwards. + relayBaseUrl: string; + // Whether the account has a subscription recorded once the run is over. + enabled: boolean; +} + +/** + * The enable and re-register buttons of the notification settings: run the + * setup and say what it left behind. When the mail server refuses a new + * relay, the setup goes back to the one in use; the relay field must show + * that one again rather than the refused address, since the device list asks + * the relay in the field about each device and a revoke goes to it. With no + * relay in use the typed one stays, so the user can correct it. + */ +export async function enablePushFromSettings(params: PushSetupParams): Promise { + const accountId = jmapClient.username && jmapClient.serverUrl + ? generateAccountId(jmapClient.username, jmapClient.serverUrl) + : null; + let error: unknown = null; + try { + await setupPushNotifications(params); + } catch (err) { + error = err; + } + const typed = (params.relayBaseUrl ?? DEFAULT_RELAY_BASE_URL).replace(/\/+$/, ''); + return { + error, + relayBaseUrl: (await getStoredRelayBaseUrl()) ?? typed, + enabled: accountId ? await isPushEnabledForAccount(accountId) : false, + }; +} + /** * Re-apply an account's push types to its subscription after device sync was * turned on or off for it (#34). The account the client serves goes through