Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/delete-token-feature.md
Original file line number Diff line number Diff line change
@@ -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.

5 changes: 5 additions & 0 deletions common/api-review/messaging-sw.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
7 changes: 6 additions & 1 deletion common/api-review/messaging.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { Observer } from '@firebase/util';
import { Unsubscribe } from '@firebase/util';

// @public
export function deleteToken(messaging: Messaging): Promise<boolean>;
export function deleteToken(messaging: Messaging, options?: DeleteTokenOptions): Promise<boolean>;

// @public (undocumented)
export interface DeleteTokenOptions {
serviceWorkerRegistration?: ServiceWorkerRegistration;
}

// @public
export interface FcmOptions {
Expand Down
9 changes: 7 additions & 2 deletions packages/messaging/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { ERROR_FACTORY, ErrorCode } from './util/errors';
import { FirebaseApp, _getProvider, getApp } from '@firebase/app';
import {
DeleteTokenOptions,
GetTokenOptions,
MessagePayload,
Messaging
Expand Down Expand Up @@ -119,14 +120,18 @@ export async function getToken(
* the {@link Messaging} instance from the push subscription.
*
* @param messaging - The {@link Messaging} instance.
* @param options - Provides an optional service worker registration.
*
* @returns The promise resolves when the token has been successfully deleted.
*
* @public
*/
export function deleteToken(messaging: Messaging): Promise<boolean> {
export function deleteToken(
messaging: Messaging,
options?: DeleteTokenOptions
): Promise<boolean> {
messaging = getModularInstance(messaging);
return _deleteToken(messaging as MessagingService);
return _deleteToken(messaging as MessagingService, options);
}

/**
Expand Down
114 changes: 114 additions & 0 deletions packages/messaging/src/api/deleteToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @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<typeof getFakeMessagingService>;
let fakeServiceWorkerRegistration: FakeServiceWorkerRegistration;
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
);

deleteTokenInternalStub = stub(
tokenManagerModule,
'deleteTokenInternal'
).resolves(true);
registerDefaultSwStub = stub(registerModule, 'registerDefaultSw').callsFake(
async (msg: typeof messaging) => {
msg.swRegistration = fakeServiceWorkerRegistration;
}
);
});

afterEach(() => {
restore();
});

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;
});

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);
});
});
10 changes: 5 additions & 5 deletions packages/messaging/src/api/deleteToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ 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<boolean> {
if (!navigator) {
throw ERROR_FACTORY.create(ErrorCode.AVAILABLE_IN_WINDOW);
}

if (!messaging.swRegistration) {
await registerDefaultSw(messaging);
}
await updateSwReg(messaging, options?.serviceWorkerRegistration);

return deleteTokenInternal(messaging);
}
11 changes: 11 additions & 0 deletions packages/messaging/src/interfaces/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading