@@ -41,10 +41,49 @@ import { Http2SessionHandler, RequestResponseError } from '../utils/api-request'
4141
4242// FCM endpoints
4343const FCM_SEND_HOST = 'fcm.googleapis.com' ;
44+ const FCM_TOPIC_MANAGEMENT_HOST = 'iid.googleapis.com' ;
45+ const FCM_TOPIC_MANAGEMENT_ADD_PATH = '/iid/v1:batchAdd' ;
46+ const FCM_TOPIC_MANAGEMENT_REMOVE_PATH = '/iid/v1:batchRemove' ;
4447
4548// Maximum messages that can be included in a batch request.
4649const FCM_MAX_BATCH_SIZE = 500 ;
4750
51+ /**
52+ * Maps a raw FCM server response to a `MessagingTopicManagementResponse` object.
53+ *
54+ * @param response - The raw FCM server response to map.
55+ *
56+ * @returns The mapped `MessagingTopicManagementResponse` object.
57+ */
58+ function mapRawResponseToTopicManagementResponse ( response : object ) : MessagingTopicManagementResponse {
59+ // Add the success and failure counts.
60+ const result : MessagingTopicManagementResponse = {
61+ successCount : 0 ,
62+ failureCount : 0 ,
63+ errors : [ ] ,
64+ } ;
65+
66+ if ( 'results' in response ) {
67+ ( response as any ) . results . forEach ( ( tokenManagementResult : any , index : number ) => {
68+ // Map the FCM server's error strings to actual error objects.
69+ if ( 'error' in tokenManagementResult ) {
70+ result . failureCount += 1 ;
71+ const newError = FirebaseMessagingError . fromTopicManagementServerError (
72+ tokenManagementResult . error , /* message */ undefined , tokenManagementResult . error ,
73+ ) ;
74+
75+ result . errors . push ( {
76+ index,
77+ error : newError ,
78+ } ) ;
79+ } else {
80+ result . successCount += 1 ;
81+ }
82+ } ) ;
83+ }
84+ return result ;
85+ }
86+
4887
4988/**
5089 * Messaging service bound to the provided app.
@@ -341,7 +380,6 @@ export class Messaging {
341380 registrationTokenOrTokens ,
342381 topic ,
343382 'subscribeToTopic' ,
344- '' ,
345383 ) ;
346384 }
347385
@@ -368,7 +406,56 @@ export class Messaging {
368406 registrationTokenOrTokens ,
369407 topic ,
370408 'unsubscribeFromTopic' ,
371- '' ,
409+ ) ;
410+ }
411+
412+ /**
413+ * Subscribes a device or list of devices to an FCM topic using the legacy Instance ID API.
414+ * Served as a backup/legacy endpoint.
415+ *
416+ * @param registrationTokenOrTokens - A token or array of registration tokens
417+ * for the devices to subscribe to the topic.
418+ * @param topic - The topic to which to subscribe.
419+ *
420+ * @returns A promise fulfilled with the server's response after the device has been
421+ * subscribed to the topic.
422+ *
423+ * @deprecated Use {@link Messaging.subscribeToTopic} instead.
424+ */
425+ public subscribeToTopicLegacy (
426+ registrationTokenOrTokens : string | string [ ] ,
427+ topic : string ,
428+ ) : Promise < MessagingTopicManagementResponse > {
429+ return this . sendTopicManagementRequestLegacy (
430+ registrationTokenOrTokens ,
431+ topic ,
432+ 'subscribeToTopicLegacy' ,
433+ FCM_TOPIC_MANAGEMENT_ADD_PATH ,
434+ ) ;
435+ }
436+
437+ /**
438+ * Unsubscribes a device or list of devices from an FCM topic using the legacy Instance ID API.
439+ * Served as a backup/legacy endpoint.
440+ *
441+ * @param registrationTokenOrTokens - A device registration token or an array of
442+ * device registration tokens to unsubscribe from the topic.
443+ * @param topic - The topic from which to unsubscribe.
444+ *
445+ * @returns A promise fulfilled with the server's response after the device has been
446+ * unsubscribed from the topic.
447+ *
448+ * @deprecated Use {@link Messaging.unsubscribeFromTopic} instead.
449+ */
450+ public unsubscribeFromTopicLegacy (
451+ registrationTokenOrTokens : string | string [ ] ,
452+ topic : string ,
453+ ) : Promise < MessagingTopicManagementResponse > {
454+ return this . sendTopicManagementRequestLegacy (
455+ registrationTokenOrTokens ,
456+ topic ,
457+ 'unsubscribeFromTopicLegacy' ,
458+ FCM_TOPIC_MANAGEMENT_REMOVE_PATH ,
372459 ) ;
373460 }
374461
@@ -401,7 +488,6 @@ export class Messaging {
401488 * registration tokens to unsubscribe from the topic.
402489 * @param topic - The topic to which to subscribe.
403490 * @param methodName - The name of the original method called.
404- * @param path - The endpoint path to use for the request.
405491 *
406492 * @returns A Promise fulfilled with the parsed server
407493 * response.
@@ -410,7 +496,6 @@ export class Messaging {
410496 registrationTokenOrTokens : string | string [ ] ,
411497 topic : string ,
412498 methodName : string ,
413- _path : string ,
414499 ) : Promise < MessagingTopicManagementResponse > {
415500 this . validateRegistrationTokensType ( registrationTokenOrTokens , methodName ) ;
416501 this . validateTopicType ( topic , methodName ) ;
@@ -434,7 +519,7 @@ export class Messaging {
434519 return utils . findProjectId ( this . app ) . then ( ( projectId ) => {
435520 if ( ! validator . isNonEmptyString ( projectId ) ) {
436521 throw new FirebaseMessagingError (
437- MessagingClientErrorCode . INVALID_ARGUMENT ,
522+ messagingClientErrorCode . INVALID_ARGUMENT ,
438523 'Failed to determine project ID for Messaging. Initialize the '
439524 + 'SDK with service account credentials or set project ID as an app option. '
440525 + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.' ,
@@ -447,75 +532,116 @@ export class Messaging {
447532
448533 const http2SessionHandler = new Http2SessionHandler ( 'https://fcm.googleapis.com' ) ;
449534
450- let settledPromise : Promise < PromiseSettledResult < any > [ ] > ;
451- return new Promise < MessagingTopicManagementResponse > ( ( resolve , reject ) => {
452- http2SessionHandler . invoke ( ) . catch ( ( error ) => {
453- reject ( new FirebaseMessagingSessionError ( error , undefined , undefined ) ) ;
454- } ) ;
535+ const requests = registrationTokensArray . map ( ( registrationId ) => {
536+ let requestPath = `/v1/projects/${ projectId } /registrations/${ registrationId } /topicSubscriptions` ;
537+ if ( isSubscribe ) {
538+ requestPath += `?topic_name=${ topicName } ` ;
539+ } else {
540+ requestPath += `/${ topicName } ?allow_missing=true` ;
541+ }
542+ return this . messagingRequestHandler . invokeHttp2RequestHandler (
543+ 'fcm.googleapis.com' ,
544+ requestPath ,
545+ httpMethod ,
546+ isSubscribe ? { } : undefined ,
547+ http2SessionHandler
548+ ) ;
549+ } ) ;
455550
456- const requests = registrationTokensArray . map ( async ( registrationId ) => {
457- let requestPath = `/v1/projects/${ projectId } /registrations/${ registrationId } /topicSubscriptions` ;
458- if ( isSubscribe ) {
459- requestPath += `?topic_name=${ topicName } ` ;
551+ return Promise . allSettled ( requests ) . then ( ( results ) => {
552+ if ( results . length > 0 && results . every ( ( r ) => r . status === 'rejected' ) ) {
553+ const firstReason = ( results [ 0 ] as PromiseRejectedResult ) . reason ;
554+ if ( firstReason instanceof RequestResponseError ) {
555+ throw createFirebaseError ( firstReason ) ;
460556 } else {
461- requestPath += `/ ${ topicName } ?allow_missing=true` ;
557+ throw firstReason ;
462558 }
463- return this . messagingRequestHandler . invokeHttp2RequestHandler (
464- 'fcm.googleapis.com' ,
465- requestPath ,
466- httpMethod ,
467- isSubscribe ? { } : undefined ,
468- http2SessionHandler
469- ) ;
470- } ) ;
559+ }
471560
472- settledPromise = Promise . allSettled ( requests ) ;
473- settledPromise . then ( ( results ) => {
474- if ( results . length > 0 && results . every ( ( r ) => r . status === 'rejected' ) ) {
475- const firstReason = ( results [ 0 ] as PromiseRejectedResult ) . reason ;
476- if ( firstReason instanceof RequestResponseError ) {
477- reject ( createFirebaseError ( firstReason ) ) ;
478- } else {
479- reject ( firstReason ) ;
480- }
481- return ;
482- }
561+ const response : MessagingTopicManagementResponse = {
562+ successCount : 0 ,
563+ failureCount : 0 ,
564+ errors : [ ] ,
565+ } ;
483566
484- const response : MessagingTopicManagementResponse = {
485- successCount : 0 ,
486- failureCount : 0 ,
487- errors : [ ] ,
488- } ;
489-
490- results . forEach ( ( result , index ) => {
491- if ( result . status === 'fulfilled' ) {
492- response . successCount += 1 ;
493- } else {
494- response . failureCount += 1 ;
495- const err = result . reason ;
496- const errorCode = err . response ?. isJson ( ) ? getErrorCode ( err . response . data ) : null ;
497- const errorMessage = err . response ?. isJson ( ) ? err . response . data ?. error ?. message : err . message ;
498- const newError = FirebaseMessagingError . fromTopicManagementServerError (
499- errorCode || 'UNKNOWN' ,
500- errorMessage ,
501- err . response ?. isJson ( ) ? err . response . data : undefined
502- ) ;
503- response . errors . push ( {
504- index,
505- error : newError ,
506- } ) ;
507- }
508- } ) ;
509-
510- resolve ( response ) ;
567+ results . forEach ( ( result , index ) => {
568+ if ( result . status === 'fulfilled' ) {
569+ response . successCount += 1 ;
570+ } else {
571+ response . failureCount += 1 ;
572+ const err = ( result as PromiseRejectedResult ) . reason ;
573+ const errorCode = err . response ?. isJson ( ) ? getErrorCode ( err . response . data ) : null ;
574+ const errorMessage = err . response ?. isJson ( ) ? err . response . data ?. error ?. message : err . message ;
575+ const newError = FirebaseMessagingError . fromTopicManagementServerError (
576+ errorCode || 'UNKNOWN' ,
577+ errorMessage ,
578+ err . response ?. isJson ( ) ? err . response . data : undefined
579+ ) ;
580+ response . errors . push ( {
581+ index,
582+ error : newError ,
583+ } ) ;
584+ }
511585 } ) ;
586+
587+ return response ;
512588 } ) . finally ( ( ) => {
513589 http2SessionHandler . close ( ) ;
514590 } ) ;
515591 } ) ;
516592 } ) ;
517593 }
518594
595+ /**
596+ * Helper method which sends and handles topic subscription management requests using the legacy Instance ID API.
597+ *
598+ * @param registrationTokenOrTokens - The registration token or an array of
599+ * registration tokens to unsubscribe from the topic.
600+ * @param topic - The topic to which to subscribe.
601+ * @param methodName - The name of the original method called.
602+ * @param path - The endpoint path to use for the request.
603+ *
604+ * @returns A Promise fulfilled with the parsed server response.
605+ */
606+ private sendTopicManagementRequestLegacy (
607+ registrationTokenOrTokens : string | string [ ] ,
608+ topic : string ,
609+ methodName : string ,
610+ path : string ,
611+ ) : Promise < MessagingTopicManagementResponse > {
612+ this . validateRegistrationTokensType ( registrationTokenOrTokens , methodName ) ;
613+ this . validateTopicType ( topic , methodName ) ;
614+
615+ // Prepend the topic with /topics/ if necessary.
616+ topic = this . normalizeTopic ( topic ) ;
617+
618+ return Promise . resolve ( )
619+ . then ( ( ) => {
620+ // Validate the contents of the input arguments. Because we are now in a promise, any thrown
621+ // error will cause this method to return a rejected promise.
622+ this . validateRegistrationTokens ( registrationTokenOrTokens , methodName ) ;
623+ this . validateTopic ( topic , methodName ) ;
624+
625+ // Ensure the registration token(s) input argument is an array.
626+ let registrationTokensArray : string [ ] = registrationTokenOrTokens as string [ ] ;
627+ if ( validator . isString ( registrationTokenOrTokens ) ) {
628+ registrationTokensArray = [ registrationTokenOrTokens as string ] ;
629+ }
630+
631+ const request = {
632+ to : topic ,
633+ registration_tokens : registrationTokensArray ,
634+ } ;
635+
636+ return this . messagingRequestHandler . invokeRequestHandler (
637+ FCM_TOPIC_MANAGEMENT_HOST , path , request ,
638+ ) ;
639+ } )
640+ . then ( ( response ) => {
641+ return mapRawResponseToTopicManagementResponse ( response ) ;
642+ } ) ;
643+ }
644+
519645 /**
520646 * Validates the type of the provided registration token(s). If invalid, an error will be thrown.
521647 *
0 commit comments