Skip to content

Commit cbb1953

Browse files
Doris-Gezwu52
andauthored
implement onSubChange for FID-based registration (#9996)
* implement onSubChange for FID-based registration * Persist VAPID key in FID registration metadata to fix re-registration issue * Enforce mutual exclusivity of token and FID registrations in DB layer * Notify clients in onSubChange on successful FID refresh - Update onSubChange to notify window clients when FID registration is refreshed. Previously it did not notify clients. - Update refreshFidRegistrationIfStored to return the current FID. - Update onSubChange to use the returned FID and only notify if the refresh was successful. - Add unit tests to verify client notification on success, failure, and when not registered. - Fix a lint error in window-listener.ts by adding the FidRegisteredPayload interface. --------- Co-authored-by: Kai Wu <zwu52@users.noreply.github.com>
1 parent 85f6f4e commit cbb1953

13 files changed

Lines changed: 425 additions & 51 deletions

packages/messaging/src/api/deleteToken.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { ERROR_FACTORY, ErrorCode } from '../util/errors';
1919

2020
import { MessagingService } from '../messaging-service';
21-
import { deleteTokenInternal } from '../internals/token-manager';
21+
import { revokeRegistrationInternal } from '../internals/token-manager';
2222
import { registerDefaultSw } from '../helpers/registerDefaultSw';
2323

2424
export async function deleteToken(
@@ -32,5 +32,5 @@ export async function deleteToken(
3232
await registerDefaultSw(messaging);
3333
}
3434

35-
return deleteTokenInternal(messaging);
35+
return revokeRegistrationInternal(messaging);
3636
}

packages/messaging/src/api/register.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,41 @@ describe('register', () => {
121121
expect(updateSwRegStub).to.have.been.calledOnceWith(messaging, swReg);
122122
});
123123

124+
it('saves VAPID key in FID registration metadata', async () => {
125+
messaging.onRegisteredHandler = stub();
126+
const options = {
127+
vapidKey: 'custom-vapid'
128+
};
129+
130+
const dbSetFidRegistrationStub = stub(
131+
idbManager,
132+
'dbSetFidRegistration'
133+
).resolves({
134+
fid: 'FID',
135+
lastRegisterTime: 1_700_000_000_000,
136+
vapidKey: 'custom-vapid'
137+
});
138+
139+
updateVapidKeyStub.callsFake(async (msg, key) => {
140+
if (key) {
141+
msg.vapidKey = key;
142+
}
143+
});
144+
145+
await register(messaging, options);
146+
147+
expect(dbSetFidRegistrationStub).to.have.been.calledOnceWith(
148+
messaging.firebaseDependencies,
149+
{
150+
fid: 'FID',
151+
lastRegisterTime: 1_700_000_000_000,
152+
vapidKey: 'custom-vapid'
153+
}
154+
);
155+
156+
dbSetFidRegistrationStub.restore();
157+
});
158+
124159
it('throws when no onRegistered callback handler is provided or registered', async () => {
125160
messaging.onRegisteredHandler = null;
126161

packages/messaging/src/api/register.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import { RegisterOptions } from '../interfaces/public-types';
2323
import { registerFcmRegistrationWithFid } from '../internals/register-fid';
2424
import {
2525
dbGetFidRegistration,
26-
dbRemove,
2726
dbSetFidRegistration
2827
} from '../internals/idb-manager';
28+
import { notifyOnRegistered } from '../internals/token-manager';
2929

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

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

7979
if (shouldRefresh) {
80-
// Best-effort cleanup of legacy token details, since apps may switch from
81-
// getToken() to the FID-based register() API over time.
82-
try {
83-
await dbRemove(messaging.firebaseDependencies);
84-
} catch {
85-
// Ignore.
86-
}
87-
8880
await registerFcmRegistrationWithFid(messaging, fid);
8981
await dbSetFidRegistration(messaging.firebaseDependencies, {
9082
fid,
91-
lastRegisterTime: now
83+
lastRegisterTime: now,
84+
vapidKey: messaging.vapidKey
9285
});
9386
}
9487

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

100-
if (typeof handler === 'function') {
101-
handler(fid);
102-
} else {
103-
handler.next(fid);
104-
}
93+
notifyOnRegistered(messaging, fid);
10594
});
10695
return messaging._registerNotifyChain;
10796
}

packages/messaging/src/helpers/fid-change-registration.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
import '../testing/setup';
1919

20-
import { subscribeFidChangeRegistration } from './fid-change-registration';
20+
import {
21+
refreshFidRegistrationIfStored,
22+
subscribeFidChangeRegistration
23+
} from './fid-change-registration';
2124
import { MessagingService } from '../messaging-service';
2225
import {
2326
getFakeAnalyticsProvider,
@@ -33,6 +36,82 @@ import { Stub } from '../testing/sinon-types';
3336
import { _FirebaseInstallationsInternal } from '@firebase/installations';
3437
import { dbDelete } from '../internals/idb-manager';
3538

39+
describe('refreshFidRegistrationIfStored', () => {
40+
let messaging: MessagingService;
41+
let requestCreateRegistrationStub: Stub<
42+
typeof requestsModule.requestCreateRegistration
43+
>;
44+
let dbGetFidRegistrationStub: Stub<typeof idbManager.dbGetFidRegistration>;
45+
46+
beforeEach(() => {
47+
const app = getFakeApp();
48+
messaging = new MessagingService(
49+
app,
50+
{
51+
getId: async () => 'fid-1',
52+
getToken: async () => 'authToken'
53+
},
54+
getFakeAnalyticsProvider()
55+
);
56+
messaging.swRegistration =
57+
new FakeServiceWorkerRegistration() as unknown as ServiceWorkerRegistration;
58+
59+
requestCreateRegistrationStub = stub(
60+
requestsModule,
61+
'requestCreateRegistration'
62+
).callsFake(async () => ({
63+
responseFid: 'fid-1'
64+
})) as Stub<typeof requestsModule.requestCreateRegistration>;
65+
66+
dbGetFidRegistrationStub = stub(
67+
idbManager,
68+
'dbGetFidRegistration'
69+
).resolves(undefined) as Stub<typeof idbManager.dbGetFidRegistration>;
70+
});
71+
72+
afterEach(() => {
73+
requestCreateRegistrationStub.restore();
74+
dbGetFidRegistrationStub.restore();
75+
});
76+
77+
it('re-registers with FCM when FID metadata exists and notifies onRegistered', async () => {
78+
const onRegisteredSpy = stub();
79+
messaging.onRegisteredHandler = onRegisteredSpy;
80+
dbGetFidRegistrationStub.resolves({
81+
fid: 'fid-1',
82+
lastRegisterTime: Date.now()
83+
});
84+
85+
await refreshFidRegistrationIfStored(messaging);
86+
87+
expect(requestCreateRegistrationStub).to.have.been.calledOnce;
88+
expect(onRegisteredSpy).to.have.been.calledOnceWith('fid-1');
89+
});
90+
91+
it('uses stored VAPID key when available', async () => {
92+
const onRegisteredSpy = stub();
93+
messaging.onRegisteredHandler = onRegisteredSpy;
94+
dbGetFidRegistrationStub.resolves({
95+
fid: 'fid-1',
96+
lastRegisterTime: Date.now(),
97+
vapidKey: 'custom-vapid-key'
98+
});
99+
100+
await refreshFidRegistrationIfStored(messaging);
101+
102+
expect(messaging.vapidKey).to.equal('custom-vapid-key');
103+
});
104+
105+
it('no-ops when the app instance was never registered with FCM', async () => {
106+
messaging.onRegisteredHandler = stub();
107+
dbGetFidRegistrationStub.resolves(undefined);
108+
109+
await refreshFidRegistrationIfStored(messaging);
110+
111+
expect(requestCreateRegistrationStub).to.not.have.been.called;
112+
});
113+
});
114+
36115
describe('subscribeFidChangeRegistration', () => {
37116
let messaging: MessagingService;
38117
/** Same object as `messaging.firebaseDependencies.installations`; `getId` tracks simulated rotation. */

packages/messaging/src/helpers/fid-change-registration.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,42 @@ import {
2121
onIdChange
2222
} from '@firebase/installations';
2323
import { register } from '../api/register';
24-
import { dbGetFidRegistration } from '../internals/idb-manager';
24+
import {
25+
dbGetFidRegistration,
26+
dbSetFidRegistration
27+
} from '../internals/idb-manager';
28+
import { registerFcmRegistrationWithFid } from '../internals/register-fid';
29+
import { notifyOnRegistered } from '../internals/token-manager';
2530
import { MessagingService } from '../messaging-service';
31+
import { updateVapidKey } from './updateVapidKey';
32+
33+
/**
34+
* Re-runs FCM FID registration when push subscription keys change (e.g. `pushsubscriptionchange`
35+
* in the service worker). No-op if the app instance was never registered via `register()`.
36+
* Best-effort: callers should catch failures when permission or push may be unavailable.
37+
*/
38+
export async function refreshFidRegistrationIfStored(
39+
messaging: MessagingService
40+
): Promise<string | undefined> {
41+
const stored = await dbGetFidRegistration(
42+
messaging.firebaseDependencies
43+
).catch(() => undefined);
44+
if (!stored) {
45+
return undefined;
46+
}
47+
48+
await updateVapidKey(messaging, stored.vapidKey);
49+
50+
const fid = await messaging.firebaseDependencies.installations.getId();
51+
await registerFcmRegistrationWithFid(messaging, fid);
52+
await dbSetFidRegistration(messaging.firebaseDependencies, {
53+
fid,
54+
lastRegisterTime: Date.now(),
55+
vapidKey: messaging.vapidKey
56+
});
57+
notifyOnRegistered(messaging, fid);
58+
return fid;
59+
}
2660

2761
/**
2862
* When the Firebase Installation ID changes, re-run `register()` so FCM registration and

packages/messaging/src/interfaces/internal-message-payload.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export interface FcmOptionsInternal {
7777

7878
export enum MessageType {
7979
PUSH_RECEIVED = 'push-received',
80-
NOTIFICATION_CLICKED = 'notification-clicked'
80+
NOTIFICATION_CLICKED = 'notification-clicked',
81+
FID_REGISTERED = 'fid-registered'
8182
}
8283

8384
/** Additional data of a message sent from the FN Console. */
@@ -87,3 +88,9 @@ export interface ConsoleMessageData {
8788
[CONSOLE_CAMPAIGN_NAME]?: string;
8889
[CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]?: '1';
8990
}
91+
92+
export interface FidRegisteredPayload {
93+
isFirebaseMessaging: boolean;
94+
messageType: MessageType.FID_REGISTERED;
95+
fid: string;
96+
}

packages/messaging/src/internals/idb-manager.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,36 @@ describe('idb manager', () => {
259259
await dbDelete();
260260
expect(deleteDbStub).to.have.been.calledOnceWith(DATABASE_NAME);
261261
});
262+
263+
describe('mutual exclusivity', () => {
264+
it('dbSet deletes FID registration if it exists', async () => {
265+
await dbSetFidRegistration(firebaseDependencies, {
266+
fid: 'FID',
267+
lastRegisterTime: Date.now()
268+
});
269+
270+
expect(await dbGetFidRegistration(firebaseDependencies)).to.exist;
271+
272+
await dbSet(firebaseDependencies, tokenDetailsA);
273+
274+
expect(await dbGetFidRegistration(firebaseDependencies)).to.be.undefined;
275+
expect(await dbGet(firebaseDependencies)).to.deep.equal(tokenDetailsA);
276+
});
277+
278+
it('dbSetFidRegistration deletes legacy token if it exists', async () => {
279+
await dbSet(firebaseDependencies, tokenDetailsA);
280+
281+
expect(await dbGet(firebaseDependencies)).to.deep.equal(tokenDetailsA);
282+
283+
await dbSetFidRegistration(firebaseDependencies, {
284+
fid: 'FID',
285+
lastRegisterTime: Date.now()
286+
});
287+
288+
expect(await dbGet(firebaseDependencies)).to.be.undefined;
289+
expect((await dbGetFidRegistration(firebaseDependencies))?.fid).to.equal(
290+
'FID'
291+
);
292+
});
293+
});
262294
});

packages/messaging/src/internals/idb-manager.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ interface MessagingDB extends DBSchema {
4343
export interface FidRegistrationDetails {
4444
fid: string;
4545
lastRegisterTime: number;
46+
vapidKey?: string;
4647
}
4748

4849
interface IdbImpl {
@@ -177,9 +178,25 @@ export async function dbSet(
177178
): Promise<TokenDetails> {
178179
const key = getKey(firebaseDependencies);
179180
const db = await getDbPromise();
180-
const tx = db.transaction(TOKEN_OBJECT_STORE_NAME, 'readwrite');
181+
182+
const stores: Array<
183+
typeof TOKEN_OBJECT_STORE_NAME | typeof FID_REGISTRATION_OBJECT_STORE_NAME
184+
> = [TOKEN_OBJECT_STORE_NAME];
185+
const hasFidStore = hasObjectStore(
186+
db as unknown as IDBPDatabase<unknown>,
187+
FID_REGISTRATION_OBJECT_STORE_NAME
188+
);
189+
if (hasFidStore) {
190+
stores.push(FID_REGISTRATION_OBJECT_STORE_NAME);
191+
}
192+
193+
const tx = db.transaction(stores, 'readwrite');
181194
await tx.objectStore(TOKEN_OBJECT_STORE_NAME).put(tokenDetails, key);
195+
if (hasFidStore) {
196+
await tx.objectStore(FID_REGISTRATION_OBJECT_STORE_NAME).delete(key);
197+
}
182198
await tx.done;
199+
183200
return tokenDetails;
184201
}
185202

@@ -212,9 +229,15 @@ export async function dbSetFidRegistration(
212229
const key = getKey(firebaseDependencies);
213230
const db = await getDbPromise();
214231
assertFidRegistrationObjectStore(db);
215-
const tx = db.transaction(FID_REGISTRATION_OBJECT_STORE_NAME, 'readwrite');
232+
233+
const tx = db.transaction(
234+
[TOKEN_OBJECT_STORE_NAME, FID_REGISTRATION_OBJECT_STORE_NAME],
235+
'readwrite'
236+
);
216237
await tx.objectStore(FID_REGISTRATION_OBJECT_STORE_NAME).put(details, key);
238+
await tx.objectStore(TOKEN_OBJECT_STORE_NAME).delete(key);
217239
await tx.done;
240+
218241
return details;
219242
}
220243

0 commit comments

Comments
 (0)