Skip to content

Commit 265a9d5

Browse files
authored
feat(fcm): Enable fid and deprecate token for Send API (#3145)
* Add FidMessage and deprecate TokenMessage * Add fids to MulticastMessage interface * Add integration tests * Extract API docstring * Change tokens back to required field, add extra docstrings and refactor the messages array construction in MulticastMessage * Fix the lint error * Add FidMulticastMessage and Implement Function Overloads on sendEachForMulticast * Extract API doc and remove hyperlink due to duplicate names from function overload * Update sendEachForMulticast docstring * Add token deprecation documentation * Update integration tests error code for invalid fid target * Register new installation-id-not-registered Error Code * Extract api docstring * Resolve gemini review comments and add a unit test * Add explicit note in FidMulticastMessage interface to avoid confusion
1 parent c3de9df commit 265a9d5

10 files changed

Lines changed: 414 additions & 49 deletions

File tree

etc/firebase-admin.messaging.api.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ export interface FcmOptions {
162162
analyticsLabel?: string;
163163
}
164164

165+
// @public
166+
export interface FidMessage extends BaseMessage {
167+
fid: string;
168+
}
169+
170+
// @public
171+
export interface FidMulticastMessage extends BaseMessage {
172+
fids: string[];
173+
}
174+
165175
// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts
166176
//
167177
// @public
@@ -183,7 +193,7 @@ export interface LightSettings {
183193
}
184194

185195
// @public
186-
export type Message = TokenMessage | TopicMessage | ConditionMessage;
196+
export type Message = FidMessage | TokenMessage | TopicMessage | ConditionMessage;
187197

188198
// @public
189199
export class Messaging {
@@ -192,7 +202,9 @@ export class Messaging {
192202
enableLegacyHttpTransport(): void;
193203
send(message: Message, dryRun?: boolean): Promise<string>;
194204
sendEach(messages: Message[], dryRun?: boolean): Promise<BatchResponse>;
205+
// @deprecated
195206
sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
207+
sendEachForMulticast(message: FidMulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
196208
subscribeToTopic(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
197209
unsubscribeFromTopic(registrationTokenOrTokens: string | string[], topic: string): Promise<MessagingTopicManagementResponse>;
198210
}
@@ -207,6 +219,7 @@ export const MessagingErrorCode: {
207219
readonly INVALID_OPTIONS: "invalid-options";
208220
readonly INVALID_REGISTRATION_TOKEN: "invalid-registration-token";
209221
readonly REGISTRATION_TOKEN_NOT_REGISTERED: "registration-token-not-registered";
222+
readonly INSTALLATION_ID_NOT_REGISTERED: "installation-id-not-registered";
210223
readonly MISMATCHED_CREDENTIAL: "mismatched-credential";
211224
readonly INVALID_PACKAGE_NAME: "invalid-package-name";
212225
readonly DEVICE_MESSAGE_RATE_EXCEEDED: "device-message-rate-exceeded";
@@ -232,9 +245,10 @@ export interface MessagingTopicManagementResponse {
232245
successCount: number;
233246
}
234247

235-
// @public
248+
// @public @deprecated
236249
export interface MulticastMessage extends BaseMessage {
237-
// (undocumented)
250+
fids?: string[];
251+
// @deprecated
238252
tokens: string[];
239253
}
240254

@@ -252,7 +266,7 @@ export interface SendResponse {
252266
success: boolean;
253267
}
254268

255-
// @public (undocumented)
269+
// @public @deprecated (undocumented)
256270
export interface TokenMessage extends BaseMessage {
257271
// (undocumented)
258272
token: string;

src/messaging/error.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const MessagingErrorCode = {
3030
INVALID_OPTIONS: 'invalid-options',
3131
INVALID_REGISTRATION_TOKEN: 'invalid-registration-token',
3232
REGISTRATION_TOKEN_NOT_REGISTERED: 'registration-token-not-registered',
33+
INSTALLATION_ID_NOT_REGISTERED: 'installation-id-not-registered',
3334
MISMATCHED_CREDENTIAL: 'mismatched-credential',
3435
INVALID_PACKAGE_NAME: 'invalid-package-name',
3536
DEVICE_MESSAGE_RATE_EXCEEDED: 'device-message-rate-exceeded',
@@ -91,6 +92,12 @@ export const messagingClientErrorCode: { readonly [K in keyof typeof MessagingEr
9192
'error documentation for more details. Remove this registration token and stop using it to ' +
9293
'send messages.',
9394
},
95+
INSTALLATION_ID_NOT_REGISTERED: {
96+
code: MessagingErrorCode.INSTALLATION_ID_NOT_REGISTERED,
97+
message: 'The provided installation ID is not registered. A previously valid installation ' +
98+
'ID can be unregistered for a variety of reasons. See the error documentation for more ' +
99+
'details. Remove this installation ID and stop using it to send messages.',
100+
},
94101
MISMATCHED_CREDENTIAL: {
95102
code: MessagingErrorCode.MISMATCHED_CREDENTIAL,
96103
message: 'The credential used to authenticate this SDK does not have permission ' +
@@ -202,6 +209,7 @@ const MESSAGING_SERVER_TO_CLIENT_CODE: Record<string, keyof typeof MessagingErro
202209
THIRD_PARTY_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR',
203210
UNAVAILABLE: 'SERVER_UNAVAILABLE',
204211
UNREGISTERED: 'REGISTRATION_TOKEN_NOT_REGISTERED',
212+
UNREGISTERED_FID: 'INSTALLATION_ID_NOT_REGISTERED',
205213
UNSPECIFIED_ERROR: 'UNKNOWN_ERROR',
206214
};
207215

src/messaging/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export {
4242
CriticalSound,
4343
ConditionMessage,
4444
FcmOptions,
45+
FidMessage,
46+
FidMulticastMessage,
4547
LightSettings,
4648
Message,
4749
MessagingTopicManagementResponse,

src/messaging/messaging-api.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ export interface BaseMessage {
2626
fcmOptions?: FcmOptions;
2727
}
2828

29+
/**
30+
* Interface representing a message that targets a Firebase Installation ID (FID).
31+
*/
32+
export interface FidMessage extends BaseMessage {
33+
/**
34+
* The Firebase Installation ID (FID) to which the message should be sent.
35+
*/
36+
fid: string;
37+
}
38+
39+
/**
40+
* @deprecated Use {@link FidMessage} instead.
41+
*/
2942
export interface TokenMessage extends BaseMessage {
3043
token: string;
3144
}
@@ -40,18 +53,44 @@ export interface ConditionMessage extends BaseMessage {
4053

4154
/**
4255
* Payload for the {@link Messaging.send} operation. The payload contains all the fields
43-
* in the BaseMessage type, and exactly one of token, topic or condition.
56+
* in the BaseMessage type, and exactly one of fid, token (deprecated, use fid instead),
57+
* topic or condition.
4458
*/
45-
export type Message = TokenMessage | TopicMessage | ConditionMessage;
59+
export type Message = FidMessage | TokenMessage | TopicMessage | ConditionMessage;
4660

4761
/**
48-
* Payload for the {@link Messaging.sendEachForMulticast} method. The payload contains all the fields
49-
* in the BaseMessage type, and a list of tokens.
62+
* Payload for the `sendEachForMulticast` method.
63+
*
64+
* @deprecated Use {@link FidMulticastMessage} instead.
5065
*/
5166
export interface MulticastMessage extends BaseMessage {
67+
/**
68+
* A list of Firebase Installation IDs (FIDs) to target.
69+
*/
70+
fids?: string[];
71+
/**
72+
* A list of registration tokens to target.
73+
*
74+
* @deprecated Use `fids` in {@link FidMulticastMessage} instead.
75+
*/
5276
tokens: string[];
5377
}
5478

79+
/**
80+
* Payload for the `sendEachForMulticast` method containing only FIDs.
81+
*
82+
* @remarks
83+
* Note: This is a temporary interface. In the next major version, this will be
84+
* renamed back to `MulticastMessage` once the old token-based `MulticastMessage`
85+
* is fully deprecated and removed.
86+
*/
87+
export interface FidMulticastMessage extends BaseMessage {
88+
/**
89+
* A list of Firebase Installation IDs (FIDs) to target.
90+
*/
91+
fids: string[];
92+
}
93+
5594
/**
5695
* A notification that can be included in {@link Message}.
5796
*/
@@ -698,7 +737,7 @@ export interface MessagingTopicManagementResponse {
698737

699738
/**
700739
* Interface representing the server response from the
701-
* {@link Messaging.sendEach} and {@link Messaging.sendEachForMulticast} methods.
740+
* {@link Messaging.sendEach} and `sendEachForMulticast` methods.
702741
*/
703742
export interface BatchResponse {
704743

src/messaging/messaging-errors-internal.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,23 @@ export function createFirebaseError(err: RequestResponseError): FirebaseMessagin
3030
if (err.response.isJson()) {
3131
// For JSON responses, map the server response to a client-side error.
3232
const json = err.response.data;
33-
const errorCode = getErrorCode(json);
33+
let errorCode = getErrorCode(json);
3434
const errorMessage = getErrorMessage(json);
35+
if (errorCode === 'UNREGISTERED' || errorCode === 'NOT_FOUND') {
36+
let requestData = err.response.config && err.response.config.data;
37+
if (requestData && (typeof requestData === 'string' || Buffer.isBuffer(requestData))) {
38+
try {
39+
const strData = typeof requestData === 'string' ? requestData : requestData.toString('utf-8');
40+
requestData = JSON.parse(strData);
41+
} catch (e) {
42+
// Ignore parsing errors.
43+
}
44+
}
45+
const messageObj = requestData && requestData.message;
46+
if (messageObj && typeof messageObj.fid === 'string') {
47+
errorCode = 'UNREGISTERED_FID';
48+
}
49+
}
3550
return FirebaseMessagingError.fromServerError(errorCode, errorMessage, err);
3651
}
3752

src/messaging/messaging-internal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ export function validateMessage(message: Message): void {
5858
}
5959
}
6060

61-
const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition];
61+
const targets = [anyMessage.fid, anyMessage.token, anyMessage.topic, anyMessage.condition];
6262
if (targets.filter((v) => validator.isNonEmptyString(v)).length !== 1) {
6363
throw new FirebaseMessagingError(
6464
messagingClientErrorCode.INVALID_PAYLOAD,
65-
'Exactly one of topic, token or condition is required');
65+
'Exactly one of fid, topic, token or condition is required');
6666
}
6767

6868
validateStringMap(message.data, 'data');

src/messaging/messaging.ts

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { FirebaseMessagingRequestHandler } from './messaging-api-request-interna
2828

2929
import {
3030
BatchResponse,
31+
FidMulticastMessage,
3132
Message,
3233
MessagingTopicManagementResponse,
3334
MulticastMessage,
@@ -274,50 +275,84 @@ export class Messaging {
274275
}
275276

276277
/**
277-
* Sends the given multicast message to all the FCM registration tokens
278+
* Sends the given multicast message to all the FCM registration tokens or fids
278279
* specified in it.
279280
*
280281
* This method uses the {@link Messaging.sendEach} API under the hood to send the given
281282
* message to all the target recipients. The responses list obtained from the
282-
* return value corresponds to the order of tokens in the `MulticastMessage`.
283+
* return value corresponds to the order of tokens/fids in the `MulticastMessage`.
284+
* If both `tokens` and `fids` are provided, `tokens` are processed first, followed by `fids`.
283285
* An error from this method or a `BatchResponse` with all failures indicates a total
284-
* failure, meaning that the messages in the list could be sent. Partial failures or
285-
* failures are only indicated by a `BatchResponse` return value.
286+
* failure, meaning that the messages in the list could not be sent. Partial failures
287+
* are only indicated by a `BatchResponse` return value.
286288
*
287-
* @param message - A multicast message
288-
* containing up to 500 tokens.
289+
* @deprecated Use the overload accepting {@link FidMulticastMessage} instead.
290+
*
291+
* @param message - A multicast message containing up to 500 tokens and/or fids.
289292
* @param dryRun - Whether to send the message in the dry-run
290293
* (validation only) mode.
291294
* @returns A Promise fulfilled with an object representing the result of the
292295
* send operation.
293296
*/
294-
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse> {
295-
const copy: MulticastMessage = deepCopy(message);
297+
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
298+
299+
/**
300+
* Sends the given multicast message to all the FCM fids specified in it.
301+
*
302+
* This method uses the {@link Messaging.sendEach} API under the hood to send the given
303+
* message to all the target recipients. The responses list obtained from the
304+
* return value corresponds to the order of fids in the `FidMulticastMessage`.
305+
* An error from this method or a `BatchResponse` with all failures indicates a total
306+
* failure, meaning that the messages in the list could not be sent. Partial failures
307+
* are only indicated by a `BatchResponse` return value.
308+
*
309+
* @param message - A multicast message containing up to 500 fids.
310+
* @param dryRun - Whether to send the message in the dry-run (validation only) mode.
311+
* @returns A Promise fulfilled with an object representing the result of the send operation.
312+
*/
313+
public sendEachForMulticast(message: FidMulticastMessage, dryRun?: boolean): Promise<BatchResponse>;
314+
315+
public sendEachForMulticast(
316+
message: MulticastMessage | FidMulticastMessage,
317+
dryRun?: boolean,
318+
): Promise<BatchResponse> {
319+
const copy: any = deepCopy(message);
296320
if (!validator.isNonNullObject(copy)) {
297321
throw new FirebaseMessagingError(
298322
messagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object');
299323
}
300-
if (!validator.isNonEmptyArray(copy.tokens)) {
324+
325+
const { tokens, fids, ...baseMessage } = copy;
326+
327+
if (tokens !== undefined && !validator.isArray(tokens)) {
328+
throw new FirebaseMessagingError(
329+
messagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a valid array');
330+
}
331+
if (fids !== undefined && !validator.isArray(fids)) {
332+
throw new FirebaseMessagingError(
333+
messagingClientErrorCode.INVALID_ARGUMENT, 'fids must be a valid array');
334+
}
335+
336+
const tokenList: string[] = tokens || [];
337+
const fidList: string[] = fids || [];
338+
339+
if (tokenList.length === 0 && fidList.length === 0) {
301340
throw new FirebaseMessagingError(
302-
messagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array');
341+
messagingClientErrorCode.INVALID_ARGUMENT, 'Either tokens or fids must be a non-empty array');
303342
}
304-
if (copy.tokens.length > FCM_MAX_BATCH_SIZE) {
343+
344+
const totalLength = tokenList.length + fidList.length;
345+
if (totalLength > FCM_MAX_BATCH_SIZE) {
305346
throw new FirebaseMessagingError(
306347
messagingClientErrorCode.INVALID_ARGUMENT,
307-
`tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
348+
`The total number of tokens and fids must not exceed ${FCM_MAX_BATCH_SIZE}.`);
308349
}
309350

310-
const messages: Message[] = copy.tokens.map((token) => {
311-
return {
312-
token,
313-
android: copy.android,
314-
apns: copy.apns,
315-
data: copy.data,
316-
notification: copy.notification,
317-
webpush: copy.webpush,
318-
fcmOptions: copy.fcmOptions,
319-
};
320-
});
351+
const messages: Message[] = [
352+
...tokenList.map((token) => ({ ...baseMessage, token } as Message)),
353+
...fidList.map((fid) => ({ ...baseMessage, fid } as Message)),
354+
];
355+
321356
return this.sendEach(messages, dryRun);
322357
}
323358

src/utils/api-request.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export interface RequestResponse {
7777
readonly data?: any;
7878
/** For multipart responses, the payloads of individual parts. */
7979
readonly multipart?: Buffer[];
80+
/** Optional request config used to send this request. */
81+
readonly config?: any;
8082
/**
8183
* Indicates if the response content is JSON-formatted or not. If true, data field can be used
8284
* to retrieve the content as a parsed JSON object.
@@ -129,6 +131,7 @@ class DefaultRequestResponse implements RequestResponse {
129131
public readonly status: number;
130132
public readonly headers: any;
131133
public readonly text?: string;
134+
public readonly config?: RequestConfig;
132135

133136
private readonly parsedData: any;
134137
private readonly parseError: Error;
@@ -140,6 +143,7 @@ class DefaultRequestResponse implements RequestResponse {
140143
this.status = resp.status;
141144
this.headers = resp.headers;
142145
this.text = resp.data;
146+
this.config = resp.config;
143147
try {
144148
if (!resp.data) {
145149
throw new FirebaseAppError({ code: AppErrorCode.INTERNAL_ERROR, message: 'HTTP response missing data.' });
@@ -177,11 +181,13 @@ class MultipartRequestResponse implements RequestResponse {
177181
public readonly status: number;
178182
public readonly headers: any;
179183
public readonly multipart?: Buffer[];
184+
public readonly config?: any;
180185

181186
constructor(resp: LowLevelResponse) {
182187
this.status = resp.status;
183188
this.headers = resp.headers;
184189
this.multipart = resp.multipart;
190+
this.config = resp.config;
185191
}
186192

187193
get text(): string {

0 commit comments

Comments
 (0)