Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 2 additions & 2 deletions packages/messaging/src/api/deleteToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { ERROR_FACTORY, ErrorCode } from '../util/errors';

import { MessagingService } from '../messaging-service';
import { deleteTokenInternal } from '../internals/token-manager';
import { revokeRegistrationInternal } from '../internals/token-manager';
import { registerDefaultSw } from '../helpers/registerDefaultSw';

export async function deleteToken(
Expand All @@ -32,5 +32,5 @@ export async function deleteToken(
await registerDefaultSw(messaging);
}

return deleteTokenInternal(messaging);
return revokeRegistrationInternal(messaging);
}
35 changes: 35 additions & 0 deletions packages/messaging/src/api/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,41 @@ describe('register', () => {
expect(updateSwRegStub).to.have.been.calledOnceWith(messaging, swReg);
});

it('saves VAPID key in FID registration metadata', async () => {
messaging.onRegisteredHandler = stub();
const options = {
vapidKey: 'custom-vapid'
};

const dbSetFidRegistrationStub = stub(
idbManager,
'dbSetFidRegistration'
).resolves({
fid: 'FID',
lastRegisterTime: 1_700_000_000_000,
vapidKey: 'custom-vapid'
});

updateVapidKeyStub.callsFake(async (msg, key) => {
if (key) {
msg.vapidKey = key;
}
});

await register(messaging, options);

expect(dbSetFidRegistrationStub).to.have.been.calledOnceWith(
messaging.firebaseDependencies,
{
fid: 'FID',
lastRegisterTime: 1_700_000_000_000,
vapidKey: 'custom-vapid'
}
);

dbSetFidRegistrationStub.restore();
});

it('throws when no onRegistered callback handler is provided or registered', async () => {
messaging.onRegisteredHandler = null;

Expand Down
19 changes: 4 additions & 15 deletions packages/messaging/src/api/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import { RegisterOptions } from '../interfaces/public-types';
import { registerFcmRegistrationWithFid } from '../internals/register-fid';
import {
dbGetFidRegistration,
dbRemove,
dbSetFidRegistration
} from '../internals/idb-manager';
import { notifyOnRegistered } from '../internals/token-manager';

const FID_REGISTRATION_REFRESH_MS = 7 * 24 * 60 * 60 * 1000; // 7 days

Expand Down Expand Up @@ -77,18 +77,11 @@ export async function register(
now >= stored.lastRegisterTime + FID_REGISTRATION_REFRESH_MS;

if (shouldRefresh) {
// Best-effort cleanup of legacy token details, since apps may switch from
// getToken() to the FID-based register() API over time.
try {
await dbRemove(messaging.firebaseDependencies);
} catch {
// Ignore.
}

await registerFcmRegistrationWithFid(messaging, fid);
await dbSetFidRegistration(messaging.firebaseDependencies, {
fid,
lastRegisterTime: now
lastRegisterTime: now,
vapidKey: messaging.vapidKey
});
}

Expand All @@ -97,11 +90,7 @@ export async function register(
throw ERROR_FACTORY.create(ErrorCode.INVALID_ON_REGISTERED_HANDLER);
}

if (typeof handler === 'function') {
handler(fid);
} else {
handler.next(fid);
}
notifyOnRegistered(messaging, fid);
});
return messaging._registerNotifyChain;
}
81 changes: 80 additions & 1 deletion packages/messaging/src/helpers/fid-change-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

import '../testing/setup';

import { subscribeFidChangeRegistration } from './fid-change-registration';
import {
refreshFidRegistrationIfStored,
subscribeFidChangeRegistration
} from './fid-change-registration';
import { MessagingService } from '../messaging-service';
import {
getFakeAnalyticsProvider,
Expand All @@ -33,6 +36,82 @@ import { Stub } from '../testing/sinon-types';
import { _FirebaseInstallationsInternal } from '@firebase/installations';
import { dbDelete } from '../internals/idb-manager';

describe('refreshFidRegistrationIfStored', () => {
let messaging: MessagingService;
let requestCreateRegistrationStub: Stub<
typeof requestsModule.requestCreateRegistration
>;
let dbGetFidRegistrationStub: Stub<typeof idbManager.dbGetFidRegistration>;

beforeEach(() => {
const app = getFakeApp();
messaging = new MessagingService(
app,
{
getId: async () => 'fid-1',
getToken: async () => 'authToken'
},
getFakeAnalyticsProvider()
);
messaging.swRegistration =
new FakeServiceWorkerRegistration() as unknown as ServiceWorkerRegistration;

requestCreateRegistrationStub = stub(
requestsModule,
'requestCreateRegistration'
).callsFake(async () => ({
responseFid: 'fid-1'
})) as Stub<typeof requestsModule.requestCreateRegistration>;

dbGetFidRegistrationStub = stub(
idbManager,
'dbGetFidRegistration'
).resolves(undefined) as Stub<typeof idbManager.dbGetFidRegistration>;
});

afterEach(() => {
requestCreateRegistrationStub.restore();
dbGetFidRegistrationStub.restore();
});

it('re-registers with FCM when FID metadata exists and notifies onRegistered', async () => {
const onRegisteredSpy = stub();
messaging.onRegisteredHandler = onRegisteredSpy;
dbGetFidRegistrationStub.resolves({
fid: 'fid-1',
lastRegisterTime: Date.now()
});

await refreshFidRegistrationIfStored(messaging);

expect(requestCreateRegistrationStub).to.have.been.calledOnce;
expect(onRegisteredSpy).to.have.been.calledOnceWith('fid-1');
});

it('uses stored VAPID key when available', async () => {
const onRegisteredSpy = stub();
messaging.onRegisteredHandler = onRegisteredSpy;
dbGetFidRegistrationStub.resolves({
fid: 'fid-1',
lastRegisterTime: Date.now(),
vapidKey: 'custom-vapid-key'
});

await refreshFidRegistrationIfStored(messaging);

expect(messaging.vapidKey).to.equal('custom-vapid-key');
});

it('no-ops when the app instance was never registered with FCM', async () => {
messaging.onRegisteredHandler = stub();
dbGetFidRegistrationStub.resolves(undefined);

await refreshFidRegistrationIfStored(messaging);

expect(requestCreateRegistrationStub).to.not.have.been.called;
});
});

describe('subscribeFidChangeRegistration', () => {
let messaging: MessagingService;
/** Same object as `messaging.firebaseDependencies.installations`; `getId` tracks simulated rotation. */
Expand Down
35 changes: 34 additions & 1 deletion packages/messaging/src/helpers/fid-change-registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,41 @@ import {
onIdChange
} from '@firebase/installations';
import { register } from '../api/register';
import { dbGetFidRegistration } from '../internals/idb-manager';
import {
dbGetFidRegistration,
dbSetFidRegistration
} from '../internals/idb-manager';
import { registerFcmRegistrationWithFid } from '../internals/register-fid';
import { notifyOnRegistered } from '../internals/token-manager';
import { MessagingService } from '../messaging-service';
import { updateVapidKey } from './updateVapidKey';

/**
* Re-runs FCM FID registration when push subscription keys change (e.g. `pushsubscriptionchange`
* in the service worker). No-op if the app instance was never registered via `register()`.
* Best-effort: callers should catch failures when permission or push may be unavailable.
*/
export async function refreshFidRegistrationIfStored(
messaging: MessagingService
): Promise<void> {
const stored = await dbGetFidRegistration(
messaging.firebaseDependencies
).catch(() => undefined);
if (!stored) {
return;
}

await updateVapidKey(messaging, stored.vapidKey);

const fid = await messaging.firebaseDependencies.installations.getId();
await registerFcmRegistrationWithFid(messaging, fid);
Comment thread
Doris-Ge marked this conversation as resolved.
await dbSetFidRegistration(messaging.firebaseDependencies, {
fid,
lastRegisterTime: Date.now(),
vapidKey: messaging.vapidKey
});
notifyOnRegistered(messaging, fid);
}

/**
* When the Firebase Installation ID changes, re-run `register()` so FCM registration and
Expand Down
32 changes: 32 additions & 0 deletions packages/messaging/src/internals/idb-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,36 @@ describe('idb manager', () => {
await dbDelete();
expect(deleteDbStub).to.have.been.calledOnceWith(DATABASE_NAME);
});

describe('mutual exclusivity', () => {
it('dbSet deletes FID registration if it exists', async () => {
await dbSetFidRegistration(firebaseDependencies, {
fid: 'FID',
lastRegisterTime: Date.now()
});

expect(await dbGetFidRegistration(firebaseDependencies)).to.exist;

await dbSet(firebaseDependencies, tokenDetailsA);

expect(await dbGetFidRegistration(firebaseDependencies)).to.be.undefined;
expect(await dbGet(firebaseDependencies)).to.deep.equal(tokenDetailsA);
});

it('dbSetFidRegistration deletes legacy token if it exists', async () => {
await dbSet(firebaseDependencies, tokenDetailsA);

expect(await dbGet(firebaseDependencies)).to.deep.equal(tokenDetailsA);

await dbSetFidRegistration(firebaseDependencies, {
fid: 'FID',
lastRegisterTime: Date.now()
});

expect(await dbGet(firebaseDependencies)).to.be.undefined;
expect((await dbGetFidRegistration(firebaseDependencies))?.fid).to.equal(
'FID'
);
});
});
});
27 changes: 25 additions & 2 deletions packages/messaging/src/internals/idb-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface MessagingDB extends DBSchema {
export interface FidRegistrationDetails {
fid: string;
lastRegisterTime: number;
vapidKey?: string;
}

interface IdbImpl {
Expand Down Expand Up @@ -177,9 +178,25 @@ export async function dbSet(
): Promise<TokenDetails> {
const key = getKey(firebaseDependencies);
const db = await getDbPromise();
const tx = db.transaction(TOKEN_OBJECT_STORE_NAME, 'readwrite');

const stores: Array<
typeof TOKEN_OBJECT_STORE_NAME | typeof FID_REGISTRATION_OBJECT_STORE_NAME
> = [TOKEN_OBJECT_STORE_NAME];
const hasFidStore = hasObjectStore(
db as unknown as IDBPDatabase<unknown>,
FID_REGISTRATION_OBJECT_STORE_NAME
);
if (hasFidStore) {
stores.push(FID_REGISTRATION_OBJECT_STORE_NAME);
}

const tx = db.transaction(stores, 'readwrite');
await tx.objectStore(TOKEN_OBJECT_STORE_NAME).put(tokenDetails, key);
if (hasFidStore) {
await tx.objectStore(FID_REGISTRATION_OBJECT_STORE_NAME).delete(key);
}
await tx.done;

return tokenDetails;
}

Expand Down Expand Up @@ -212,9 +229,15 @@ export async function dbSetFidRegistration(
const key = getKey(firebaseDependencies);
const db = await getDbPromise();
assertFidRegistrationObjectStore(db);
const tx = db.transaction(FID_REGISTRATION_OBJECT_STORE_NAME, 'readwrite');

const tx = db.transaction(
[TOKEN_OBJECT_STORE_NAME, FID_REGISTRATION_OBJECT_STORE_NAME],
'readwrite'
);
await tx.objectStore(FID_REGISTRATION_OBJECT_STORE_NAME).put(details, key);
await tx.objectStore(TOKEN_OBJECT_STORE_NAME).delete(key);
await tx.done;

return details;
}

Expand Down
16 changes: 8 additions & 8 deletions packages/messaging/src/internals/token-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
dbSetFidRegistration
} from './idb-manager';
import * as idbManager from './idb-manager';
import { deleteTokenInternal, getTokenInternal } from './token-manager';
import { getTokenInternal, revokeRegistrationInternal } from './token-manager';
import {
getFakeAnalyticsProvider,
getFakeApp,
Expand Down Expand Up @@ -204,9 +204,9 @@ describe('Token Manager', () => {
});
});

describe('deleteToken', () => {
describe('revokeRegistrationInternal', () => {
it('returns if there is no token in the db', async () => {
await deleteTokenInternal(messaging);
await revokeRegistrationInternal(messaging);

expect(requestGetTokenStub).not.to.have.been.called;
expect(requestUpdateTokenStub).not.to.have.been.called;
Expand All @@ -223,7 +223,7 @@ describe('Token Manager', () => {
const onUnregisteredSpy = stub();
messaging.onUnregisteredHandler = onUnregisteredSpy;

await deleteTokenInternal(messaging);
await revokeRegistrationInternal(messaging);

expect(requestDeleteTokenStub).not.to.have.been.called;
expect(requestDeleteRegistrationStub).to.have.been.calledOnceWith(
Expand All @@ -245,7 +245,7 @@ describe('Token Manager', () => {
const onUnregisteredSpy = stub();
messaging.onUnregisteredHandler = onUnregisteredSpy;

await expect(deleteTokenInternal(messaging)).to.be.rejectedWith(
await expect(revokeRegistrationInternal(messaging)).to.be.rejectedWith(
'network'
);

Expand All @@ -263,7 +263,7 @@ describe('Token Manager', () => {
});
messaging.onUnregisteredHandler = null;

await deleteTokenInternal(messaging);
await revokeRegistrationInternal(messaging);

expect(requestDeleteRegistrationStub).to.have.been.calledOnceWith(
messaging.firebaseDependencies,
Expand All @@ -278,7 +278,7 @@ describe('Token Manager', () => {
);
await dbSet(messaging.firebaseDependencies, tokenDetails);

await deleteTokenInternal(messaging);
await revokeRegistrationInternal(messaging);

expect(await dbGet(messaging.firebaseDependencies)).to.be.undefined;
expect(requestGetTokenStub).not.to.have.been.called;
Expand All @@ -296,7 +296,7 @@ describe('Token Manager', () => {
'dbRemoveFidRegistration'
).resolves();

await deleteTokenInternal(messaging);
await revokeRegistrationInternal(messaging);

expect(dbRemoveFidStub).to.have.been.calledOnceWith(
messaging.firebaseDependencies
Expand Down
Loading
Loading