From 50572496e6d8d2148b1efa3e7f08758d4cbde5bc Mon Sep 17 00:00:00 2001 From: HillBryan Date: Sat, 15 Nov 2025 20:41:41 -0500 Subject: [PATCH 1/7] Add serviceWorkerRegistration as an optional param to deleteToken. --- packages/messaging/src/api/deleteToken.ts | 12 +++++++----- packages/messaging/src/interfaces/public-types.ts | 11 +++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/messaging/src/api/deleteToken.ts b/packages/messaging/src/api/deleteToken.ts index 5a1725f2f0d..195575ba294 100644 --- a/packages/messaging/src/api/deleteToken.ts +++ b/packages/messaging/src/api/deleteToken.ts @@ -19,18 +19,20 @@ import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { MessagingService } from '../messaging-service'; import { deleteTokenInternal } from '../internals/token-manager'; -import { registerDefaultSw } from '../helpers/registerDefaultSw'; +import { + DeleteTokenOptions, +} from '../interfaces/public-types'; +import { updateSwReg } from '../helpers/updateSwReg'; export async function deleteToken( - messaging: MessagingService + messaging: MessagingService, + options?: DeleteTokenOptions ): Promise { if (!navigator) { throw ERROR_FACTORY.create(ErrorCode.AVAILABLE_IN_WINDOW); } - if (!messaging.swRegistration) { - await registerDefaultSw(messaging); - } + await updateSwReg(messaging, options?.serviceWorkerRegistration); return deleteTokenInternal(messaging); } diff --git a/packages/messaging/src/interfaces/public-types.ts b/packages/messaging/src/interfaces/public-types.ts index b3e33965a9c..3e650a3b401 100644 --- a/packages/messaging/src/interfaces/public-types.ts +++ b/packages/messaging/src/interfaces/public-types.ts @@ -134,6 +134,17 @@ export interface GetTokenOptions { serviceWorkerRegistration?: ServiceWorkerRegistration; } +export interface DeleteTokenOptions { + /** + * The service worker registration for receiving push + * messaging. If the registration is not provided explicitly, you need to have a + * `firebase-messaging-sw.js` at your root location. See + * {@link https://firebase.google.com/docs/cloud-messaging/js/client#access_the_registration_token | Access the registration token} + * for more details. + */ + serviceWorkerRegistration?: ServiceWorkerRegistration; +} + /** * Public interface of the Firebase Cloud Messaging SDK. * From 770e834cfd833c9b658988864298388030ee0447 Mon Sep 17 00:00:00 2001 From: HillBryan Date: Sun, 16 Nov 2025 21:08:30 -0500 Subject: [PATCH 2/7] Fixed api wrapper and added tests. --- packages/messaging/src/api.ts | 6 +- .../messaging/src/api/deleteToken.test.ts | 102 ++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/messaging/src/api/deleteToken.test.ts diff --git a/packages/messaging/src/api.ts b/packages/messaging/src/api.ts index b2982b1ad18..d6660665af0 100644 --- a/packages/messaging/src/api.ts +++ b/packages/messaging/src/api.ts @@ -18,6 +18,7 @@ import { ERROR_FACTORY, ErrorCode } from './util/errors'; import { FirebaseApp, _getProvider, getApp } from '@firebase/app'; import { + DeleteTokenOptions, GetTokenOptions, MessagePayload, Messaging @@ -119,14 +120,15 @@ export async function getToken( * the {@link Messaging} instance from the push subscription. * * @param messaging - The {@link Messaging} instance. + * @param options - Provides an optional vapid key and an optional service worker registration. * * @returns The promise resolves when the token has been successfully deleted. * * @public */ -export function deleteToken(messaging: Messaging): Promise { +export function deleteToken(messaging: Messaging, options?: DeleteTokenOptions): Promise { messaging = getModularInstance(messaging); - return _deleteToken(messaging as MessagingService); + return _deleteToken(messaging as MessagingService, options); } /** diff --git a/packages/messaging/src/api/deleteToken.test.ts b/packages/messaging/src/api/deleteToken.test.ts new file mode 100644 index 00000000000..31fde13ce73 --- /dev/null +++ b/packages/messaging/src/api/deleteToken.test.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../testing/setup'; + +import * as registerModule from '../helpers/registerDefaultSw'; +import * as tokenManagerModule from '../internals/token-manager'; +import { deleteToken } from './deleteToken'; +import { stub, restore } from 'sinon'; +import { expect } from 'chai'; +import { getFakeMessagingService } from '../testing/fakes/messaging-service'; +import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; +import { Stub } from '../testing/sinon-types'; + +describe('deleteToken', () => { + + let messaging: ReturnType; + let fakeServiceWorkerRegistration: FakeServiceWorkerRegistration; + let deleteTokenInternalStub: Stub<(typeof tokenManagerModule)['deleteTokenInternal']>; + let registerDefaultSwStub: Stub<(typeof registerModule)['registerDefaultSw']>; + + beforeEach(() => { + messaging = getFakeMessagingService(); + fakeServiceWorkerRegistration = new FakeServiceWorkerRegistration(); + (globalThis as any).ServiceWorkerRegistration = FakeServiceWorkerRegistration; + + deleteTokenInternalStub = stub(tokenManagerModule, 'deleteTokenInternal').resolves(true); + registerDefaultSwStub = stub(registerModule, 'registerDefaultSw').callsFake( + async (msg: typeof messaging) => { + msg.swRegistration = fakeServiceWorkerRegistration; + } + ); + }); + + it('If navigator is missing, an error is thrown', async () => { + stub(globalThis, 'navigator').value(undefined); + + await expect(deleteToken(messaging)).to.be.rejectedWith( + 'messaging/only-available-in-window' + ); + + expect(registerDefaultSwStub).not.to.have.been.called; + expect(deleteTokenInternalStub).not.to.have.been.called; + + restore(); + }); + + it('If no options are present, the default service should be registered', async () => { + await deleteToken(messaging); + + expect(messaging.swRegistration).to.equal(fakeServiceWorkerRegistration); + expect(registerDefaultSwStub).to.have.been.calledOnceWith(messaging); + expect(deleteTokenInternalStub).to.have.been.calledOnceWith(messaging); + }); + + it('If a service worker is already registered, the registration should not be changed', async () => { + const existing = new FakeServiceWorkerRegistration(); + messaging.swRegistration = existing; + + await deleteToken(messaging); + + expect(messaging.swRegistration).to.equal(existing); + expect(registerDefaultSwStub).not.to.have.been.called; + expect(deleteTokenInternalStub).to.have.been.calledOnceWith(messaging); + }); + + it('If given service worker is not a true service worker, an error should be thrown', async () => { + await expect( + deleteToken(messaging, { serviceWorkerRegistration: ({} as unknown) as ServiceWorkerRegistration }) + ).to.be.rejectedWith('messaging/invalid-sw-registration'); + + expect(messaging.swRegistration).to.equal(undefined); + expect(registerDefaultSwStub).not.to.have.been.called; + expect(deleteTokenInternalStub).not.to.have.been.called; + }); + + it('If the given service worker is defined, it should be used in place of the default service worker', async () => { + const options = { + serviceWorkerRegistration: new FakeServiceWorkerRegistration() + }; + + await deleteToken(messaging, options); + + expect(messaging.swRegistration).to.equal(options.serviceWorkerRegistration); + expect(registerDefaultSwStub).not.to.have.been.called; + expect(deleteTokenInternalStub).to.have.been.calledOnceWith(messaging); + }); +}); From 8d136056747e43adb0dc374a8db0697589f0d02d Mon Sep 17 00:00:00 2001 From: HillBryan Date: Sun, 16 Nov 2025 21:14:04 -0500 Subject: [PATCH 3/7] Minor test clean-up. --- packages/messaging/src/api/deleteToken.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/messaging/src/api/deleteToken.test.ts b/packages/messaging/src/api/deleteToken.test.ts index 31fde13ce73..3a63670e657 100644 --- a/packages/messaging/src/api/deleteToken.test.ts +++ b/packages/messaging/src/api/deleteToken.test.ts @@ -36,7 +36,7 @@ describe('deleteToken', () => { beforeEach(() => { messaging = getFakeMessagingService(); fakeServiceWorkerRegistration = new FakeServiceWorkerRegistration(); - (globalThis as any).ServiceWorkerRegistration = FakeServiceWorkerRegistration; + stub(globalThis as any, 'ServiceWorkerRegistration').value(FakeServiceWorkerRegistration); deleteTokenInternalStub = stub(tokenManagerModule, 'deleteTokenInternal').resolves(true); registerDefaultSwStub = stub(registerModule, 'registerDefaultSw').callsFake( @@ -46,6 +46,10 @@ describe('deleteToken', () => { ); }); + afterEach(() => { + restore(); + }); + it('If navigator is missing, an error is thrown', async () => { stub(globalThis, 'navigator').value(undefined); @@ -55,8 +59,6 @@ describe('deleteToken', () => { expect(registerDefaultSwStub).not.to.have.been.called; expect(deleteTokenInternalStub).not.to.have.been.called; - - restore(); }); it('If no options are present, the default service should be registered', async () => { From a9dba4b75af6d3d22208ca0fe3f57d431e1cf36d Mon Sep 17 00:00:00 2001 From: HillBryan Date: Sun, 16 Nov 2025 21:27:04 -0500 Subject: [PATCH 4/7] Docs and formatting. --- common/api-review/messaging-sw.api.md | 5 +++++ common/api-review/messaging.api.md | 7 +++++- packages/firebase/compat/index.d.ts | 10 ++++++++- packages/messaging/src/api.ts | 5 ++++- .../messaging/src/api/deleteToken.test.ts | 22 ++++++++++++++----- packages/messaging/src/api/deleteToken.ts | 4 +--- 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/common/api-review/messaging-sw.api.md b/common/api-review/messaging-sw.api.md index 74f823de196..9b0289074b9 100644 --- a/common/api-review/messaging-sw.api.md +++ b/common/api-review/messaging-sw.api.md @@ -9,6 +9,11 @@ import { NextFn } from '@firebase/util'; import { Observer } from '@firebase/util'; import { Unsubscribe } from '@firebase/util'; +// @public (undocumented) +export interface DeleteTokenOptions { + serviceWorkerRegistration?: ServiceWorkerRegistration; +} + // @public export function experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging: Messaging, enable: boolean): void; diff --git a/common/api-review/messaging.api.md b/common/api-review/messaging.api.md index b74fc3a69fa..bba88c09851 100644 --- a/common/api-review/messaging.api.md +++ b/common/api-review/messaging.api.md @@ -10,7 +10,12 @@ import { Observer } from '@firebase/util'; import { Unsubscribe } from '@firebase/util'; // @public -export function deleteToken(messaging: Messaging): Promise; +export function deleteToken(messaging: Messaging, options?: DeleteTokenOptions): Promise; + +// @public (undocumented) +export interface DeleteTokenOptions { + serviceWorkerRegistration?: ServiceWorkerRegistration; +} // @public export interface FcmOptions { diff --git a/packages/firebase/compat/index.d.ts b/packages/firebase/compat/index.d.ts index fc2bb09567d..3524e937725 100644 --- a/packages/firebase/compat/index.d.ts +++ b/packages/firebase/compat/index.d.ts @@ -7464,9 +7464,17 @@ declare namespace firebase.messaging { * Deletes the registration token associated with this messaging instance and unsubscribes the * messaging instance from the push subscription. * + * @param options.serviceWorkerRegistration The service worker registration for receiving push + * messaging. If the registration is not provided explicitly, you need to have a + * `firebase-messaging-sw.js` at your root location. See + * {@link https://firebase.google.com/docs/cloud-messaging/js/client#access_the_registration_token | Access the registration token} + * for more details. + * * @return The promise resolves when the token has been successfully deleted. */ - deleteToken(): Promise; + deleteToken(options?: { + serviceWorkerRegistration?: ServiceWorkerRegistration; + }): Promise; /** * Subscribes the messaging instance to push notifications. Returns an FCM registration token diff --git a/packages/messaging/src/api.ts b/packages/messaging/src/api.ts index d6660665af0..300d7e9bd49 100644 --- a/packages/messaging/src/api.ts +++ b/packages/messaging/src/api.ts @@ -126,7 +126,10 @@ export async function getToken( * * @public */ -export function deleteToken(messaging: Messaging, options?: DeleteTokenOptions): Promise { +export function deleteToken( + messaging: Messaging, + options?: DeleteTokenOptions +): Promise { messaging = getModularInstance(messaging); return _deleteToken(messaging as MessagingService, options); } diff --git a/packages/messaging/src/api/deleteToken.test.ts b/packages/messaging/src/api/deleteToken.test.ts index 3a63670e657..2037adab479 100644 --- a/packages/messaging/src/api/deleteToken.test.ts +++ b/packages/messaging/src/api/deleteToken.test.ts @@ -27,18 +27,24 @@ import { FakeServiceWorkerRegistration } from '../testing/fakes/service-worker'; import { Stub } from '../testing/sinon-types'; describe('deleteToken', () => { - let messaging: ReturnType; let fakeServiceWorkerRegistration: FakeServiceWorkerRegistration; - let deleteTokenInternalStub: Stub<(typeof tokenManagerModule)['deleteTokenInternal']>; + let deleteTokenInternalStub: Stub< + (typeof tokenManagerModule)['deleteTokenInternal'] + >; let registerDefaultSwStub: Stub<(typeof registerModule)['registerDefaultSw']>; beforeEach(() => { messaging = getFakeMessagingService(); fakeServiceWorkerRegistration = new FakeServiceWorkerRegistration(); - stub(globalThis as any, 'ServiceWorkerRegistration').value(FakeServiceWorkerRegistration); + stub(globalThis as any, 'ServiceWorkerRegistration').value( + FakeServiceWorkerRegistration + ); - deleteTokenInternalStub = stub(tokenManagerModule, 'deleteTokenInternal').resolves(true); + deleteTokenInternalStub = stub( + tokenManagerModule, + 'deleteTokenInternal' + ).resolves(true); registerDefaultSwStub = stub(registerModule, 'registerDefaultSw').callsFake( async (msg: typeof messaging) => { msg.swRegistration = fakeServiceWorkerRegistration; @@ -82,7 +88,9 @@ describe('deleteToken', () => { it('If given service worker is not a true service worker, an error should be thrown', async () => { await expect( - deleteToken(messaging, { serviceWorkerRegistration: ({} as unknown) as ServiceWorkerRegistration }) + deleteToken(messaging, { + serviceWorkerRegistration: {} as unknown as ServiceWorkerRegistration + }) ).to.be.rejectedWith('messaging/invalid-sw-registration'); expect(messaging.swRegistration).to.equal(undefined); @@ -97,7 +105,9 @@ describe('deleteToken', () => { await deleteToken(messaging, options); - expect(messaging.swRegistration).to.equal(options.serviceWorkerRegistration); + expect(messaging.swRegistration).to.equal( + options.serviceWorkerRegistration + ); expect(registerDefaultSwStub).not.to.have.been.called; expect(deleteTokenInternalStub).to.have.been.calledOnceWith(messaging); }); diff --git a/packages/messaging/src/api/deleteToken.ts b/packages/messaging/src/api/deleteToken.ts index 195575ba294..1cbd543b805 100644 --- a/packages/messaging/src/api/deleteToken.ts +++ b/packages/messaging/src/api/deleteToken.ts @@ -19,9 +19,7 @@ import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { MessagingService } from '../messaging-service'; import { deleteTokenInternal } from '../internals/token-manager'; -import { - DeleteTokenOptions, -} from '../interfaces/public-types'; +import { DeleteTokenOptions } from '../interfaces/public-types'; import { updateSwReg } from '../helpers/updateSwReg'; export async function deleteToken( From ffdd1f5b56e7e53c62f0b7a1cfe09f6d9e5266ad Mon Sep 17 00:00:00 2001 From: HillBryan Date: Sun, 16 Nov 2025 23:15:49 -0500 Subject: [PATCH 5/7] Fixed copy/paste typo --- packages/messaging/src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/messaging/src/api.ts b/packages/messaging/src/api.ts index 300d7e9bd49..dccf49013ca 100644 --- a/packages/messaging/src/api.ts +++ b/packages/messaging/src/api.ts @@ -120,7 +120,7 @@ export async function getToken( * the {@link Messaging} instance from the push subscription. * * @param messaging - The {@link Messaging} instance. - * @param options - Provides an optional vapid key and an optional service worker registration. + * @param options - Provides an optional service worker registration. * * @returns The promise resolves when the token has been successfully deleted. * From 7a59598cecb18edd706e313e39ccf1b00e489e89 Mon Sep 17 00:00:00 2001 From: HillBryan Date: Sun, 16 Nov 2025 23:39:08 -0500 Subject: [PATCH 6/7] Added changeset. --- .changeset/delete-token-feature.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/delete-token-feature.md diff --git a/.changeset/delete-token-feature.md b/.changeset/delete-token-feature.md new file mode 100644 index 00000000000..eda2d27b5b5 --- /dev/null +++ b/.changeset/delete-token-feature.md @@ -0,0 +1,7 @@ +--- +"@firebase/messaging": minor +"firebase": minor +--- + +Add an optional `options` object parameter with a `serviceWorkerRegistration` attribute to `deleteToken()` to support explicit worker selection when removing messaging tokens. This maintains full backwards compatibility while expanding the public API surface. + From 10aae40473bb1be9c7a5b368039e62fd7ca6e15d Mon Sep 17 00:00:00 2001 From: HillBryan Date: Sun, 16 Nov 2025 23:56:17 -0500 Subject: [PATCH 7/7] Remove compat change. --- packages/firebase/compat/index.d.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/firebase/compat/index.d.ts b/packages/firebase/compat/index.d.ts index 3524e937725..fc2bb09567d 100644 --- a/packages/firebase/compat/index.d.ts +++ b/packages/firebase/compat/index.d.ts @@ -7464,17 +7464,9 @@ declare namespace firebase.messaging { * Deletes the registration token associated with this messaging instance and unsubscribes the * messaging instance from the push subscription. * - * @param options.serviceWorkerRegistration The service worker registration for receiving push - * messaging. If the registration is not provided explicitly, you need to have a - * `firebase-messaging-sw.js` at your root location. See - * {@link https://firebase.google.com/docs/cloud-messaging/js/client#access_the_registration_token | Access the registration token} - * for more details. - * * @return The promise resolves when the token has been successfully deleted. */ - deleteToken(options?: { - serviceWorkerRegistration?: ServiceWorkerRegistration; - }): Promise; + deleteToken(): Promise; /** * Subscribes the messaging instance to push notifications. Returns an FCM registration token