Skip to content

Add support for the new FCM registration ID.#16133

Open
leojaygoogle wants to merge 27 commits into
mainfrom
fid
Open

Add support for the new FCM registration ID.#16133
leojaygoogle wants to merge 27 commits into
mainfrom
fid

Conversation

@leojaygoogle

@leojaygoogle leojaygoogle commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

This is to integrate with the new V1 Registration API go/android-gmi-v1-registration-api

It defaults to NO. When enabled, FCM will not generate an FCM registration token, but an FID instead.
…anagement system.

Currently, the FCM SDK calls the `https://fcmtoken.googleapis.com/register` backend to register an FCM V4 registration token. Going forward, we'd like to deprecate this token and use FID instead.
Once `unregister()` is called successfully and auto-init is disabled, Sending a push notification to the FID will result in an `UNREGISTERED` error.
This method is similar to `messaging(_:didReceiveRegistrationToken:)`, but will be called instead when `isInstallationIdEnabled` is `YES`.
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Comment thread FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h Outdated

@Doris-Ge Doris-Ge left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does our existing implementation listen for a FID change via a listener and re-register on a change? If not, we may want to add that.

I'm still reviewing the PR, but I'd like to give some early feedback in case you want to address or discuss it. Since I'm not familiar with Swift, I need more time for a full review. Sorry about that!

Comment thread FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h Outdated
Comment thread FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h Outdated
Comment thread FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h Outdated
Comment thread FirebaseMessaging/Sources/FIRMessaging.m
Comment thread FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h
Comment thread FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h
Comment thread FirebaseMessaging/Sources/FIRMessaging.m Outdated
Comment thread FirebaseMessaging/Sources/FIRMessaging.m
@"apns_token" : apnsTokenString,
@"app_version" : appVersion,
@"apns_environment" : apnsEnvironment
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've proposed adding the bundle_id field to the backend API in cl/913512427. Can we start passing it here?It should be fine, even in the worst-case scenario where the API Council disapproves the CL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. we will add that later.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@"No Sender ID is available to register");
return;
}
[self setupInstallationIDObserver];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we clear the observer during unregister()? If an app disables the auto init and calls unregister() after calling register(), not clearing the observer may automatically register the app with backend on FID changes, which may be unexpected for developers.

@Doris-Ge Doris-Ge May 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it seems the observer is only set up in the register() method. Calling register() is optional for apps that enable auto-init. We should make sure that those apps can also re-register on FID changes.

Consider setting up this observer regardlessly but only re-register when the app is registered as Eldhose did for Android: https://screenshot.googleplex.com/xmAGrSTKbyRMpSm.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

It tests that when `FirebaseMessaging.isInstallationIdEnabled` is `YES`, calling `register()` triggers a V1 registration API HTTP request. After receiving the registered FID, the message delegate's `didReceiveRegistration` should be called.
…register`.

So that it doesn't depend on the `register` being called.
@wiz-9635d3485b

wiz-9635d3485b Bot commented May 13, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 3 Low
Software Management Finding Software Management Findings -
Total 3 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

@leojaygoogle leojaygoogle marked this pull request as ready for review May 13, 2026 17:29
@leojaygoogle leojaygoogle requested a review from a team as a code owner May 13, 2026 17:29
Comment thread FirebaseMessaging/Sources/FIRMessaging.m
}

NSString *urlString = [NSString
stringWithFormat:@"https://fcmregistrations.googleapis.com/v1/projects/%@/registrations",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: after this change is backported to google3, will we point our 1P SDKs to use the staging API or not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, 1p and 3p use the same SDK. They use the staging backend server for some traffic.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! To check my understanding, they all use prod backend for registration traffic but 1P may use the staging backend for some send traffic, is that correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

return;
}
// Registration will only be triggered if FID is changed
if (![identifier isEqualToString:self.lastKnownFID]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we also check if the app instance is registered or not? I think we should avoid creating a registration if the app disables auto-init and never calls register().

// setup FIRMessaging objects
[self setupRmqManager];
[self setupSyncMessageManager];
[self setupInstallationIDObserver];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that we set up this observer no matter whether the installation id flag is enabled? It might be better to only set up this observer when installation id mode is enabled to avoid changing the existing behavior of token rotation.

scope:kFIRMessagingDefaultTokenScope];
NSString *cachedToken = cachedTokenInfo.token;

if (cachedToken) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we should avoid returning a different FID from FIS API.
When the cachedToken is a FID, it would be nice if we can get the FID from FIS and check if cachedToken is equal to the FID. If it's not, then we should use the new FID to register.
Could we use isFreshWithIID to see if the token needs refresh and refresh it if yes?

}];
}

- (void)register {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should always trigger the didReceiveRegistration callback to deliver the FID no matter whether there's a cached FID or not: https://screenshot.googleplex.com/6aheYbeDbYEJnAF. This is designed to let developers retrieve the FID even when the app instance is already registered.

Consider adding a unit test for this scenario.

if (self.isInstallationIdEnabled) {
NSString *normalizeTopic = [[self class] normalizeTopic:topic];
if (normalizeTopic.length) {
[self.pubsub subscribeToTopic:normalizeTopic handler:completion];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also make sure to register the app instance with FCM if it hasn't been registered before calling subscribeToTopic like what we do below at line 822. This is because the subscription will fail on the backend if there's no FCM registration found for this app instance.

Consider adding a unit test for this scenario.

@ncooke3

ncooke3 commented Jun 9, 2026

Copy link
Copy Markdown
Member

@leojaygoogle @Doris-Ge , can messaging CI please be fixed?

@ncooke3

ncooke3 commented Jun 9, 2026

Copy link
Copy Markdown
Member

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the new FCM registration API via Firebase Installation ID (FID), allowing apps to register with FCM using their Installation ID instead of generating standard FCM registration tokens. Key changes include the addition of register and unregister methods, FID rotation handling, and new operation classes FIRMessagingFIDRegisterOperation and FIRMessagingFIDUnregisterOperation. Feedback on these changes highlights several critical issues, including potential crashes from accessing instance variables directly on a nil self after strongifying, resource leaks from creating a new NSURLSession for every request, and a potential crash if the server returns a non-string name in the JSON response. Additionally, a logic error was noted where standard token retrieval is incorrectly triggered during FID rotation when isInstallationIdEnabled is active.

Comment on lines +185 to +187
FIRMessaging_STRONGIFY(self);
// Retry on network error or backend server 5xx error.
if ((error || isServerError(response)) && self->_retryCount < kMaxRetries) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

If self is nil (e.g., if the operation is cancelled or released and weakSelf becomes nil), accessing self->_retryCount directly will cause a segmentation fault and crash the app. Always check if self is non-nil after FIRMessaging_STRONGIFY(self) before accessing instance variables directly.

          FIRMessaging_STRONGIFY(self);
          if (!self) {
            return;
          }
          // Retry on network error or backend server 5xx error.
          if ((error || isServerError(response)) && self->_retryCount < kMaxRetries) {

Comment on lines +193 to +195
FIRMessaging_STRONGIFY(self);
self->_retryCount++;
[self makeRegistrationRequestWithAuthToken:authToken];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

If self is nil when the dispatch_after block executes, accessing self->_retryCount++ directly will cause a segmentation fault and crash the app. Always check if self is non-nil after FIRMessaging_STRONGIFY(self) before accessing instance variables directly.

                  FIRMessaging_STRONGIFY(self);
                  if (!self) {
                    return;
                  }
                  self->_retryCount++;
                  [self makeRegistrationRequestWithAuthToken:authToken];

Comment on lines +140 to +143
FIRMessaging_STRONGIFY(self);

// Retry on network error or backend server 5xx error.
if ((error || isServerError(response)) && self->_retryCount < kMaxRetries) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

If self is nil (e.g., if the operation is cancelled or released and weakSelf becomes nil), accessing self->_retryCount directly will cause a segmentation fault and crash the app. Always check if self is non-nil after FIRMessaging_STRONGIFY(self) before accessing instance variables directly.

          FIRMessaging_STRONGIFY(self);
          if (!self) {
            return;
          }

          // Retry on network error or backend server 5xx error.
          if ((error || isServerError(response)) && self->_retryCount < kMaxRetries) {

Comment on lines +149 to +151
FIRMessaging_STRONGIFY(self);
self->_retryCount++;
[self makeUnregistrationRequestWithAuthToken:authToken];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

If self is nil when the dispatch_after block executes, accessing self->_retryCount++ directly will cause a segmentation fault and crash the app. Always check if self is non-nil after FIRMessaging_STRONGIFY(self) before accessing instance variables directly.

                  FIRMessaging_STRONGIFY(self);
                  if (!self) {
                    return;
                  }
                  self->_retryCount++;
                  [self makeUnregistrationRequestWithAuthToken:authToken];

Comment on lines +703 to +705
[self retrieveFCMTokenForSenderID:self.tokenManager.fcmSenderID
completion:^(NSString *_Nullable FCMToken, NSError *_Nullable error){
}];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If isInstallationIdEnabled is YES, calling retrieveFCMTokenForSenderID will fail or be blocked (as it is a token-related operation). Instead, we should call tokenWithAuthorizedEntity:scope:options:handler: on self.tokenManager directly to perform the internal FID registration when the FID rotates.

        if (self.isInstallationIdEnabled) {
          [self.tokenManager tokenWithAuthorizedEntity:self.tokenManager.fcmSenderID
                                                 scope:kFIRMessagingDefaultTokenScope
                                               options:self.APNSToken ? @{kFIRMessagingTokenOptionsAPNSKey : self.APNSToken} : nil
                                               handler:^(NSString * _Nullable FCMToken, NSError * _Nullable error) {
          }];
        } else {
          [self retrieveFCMTokenForSenderID:self.tokenManager.fcmSenderID
                                 completion:^(NSString *_Nullable FCMToken, NSError *_Nullable error){
                                 }];
        }

Comment on lines +177 to +178
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Creating a new NSURLSession for every single request is highly inefficient and leaks resources (threads/memory) unless explicitly invalidated. Reusing a shared session avoids TLS handshake overhead on every registration request and improves performance.

  static NSURLSession *session;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
    session = [NSURLSession sessionWithConfiguration:config];
  });

Comment on lines +130 to +131
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Creating a new NSURLSession for every single request is highly inefficient and leaks resources (threads/memory) unless explicitly invalidated. Reusing a shared session avoids TLS handshake overhead on every unregistration request and improves performance.

Suggested change
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
static NSURLSession *session;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
session = [NSURLSession sessionWithConfiguration:config];
});

Comment on lines +251 to +253
NSString *name = responseDict[@"name"];
NSString *fid = nil;
if (name.length) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the server returns an unexpected JSON response where name is not a string (e.g., a dictionary or number), calling name.length directly will cause a crash due to unrecognized selector. Always type-check JSON values using isKindOfClass:[NSString class] before calling string methods.

  NSString *name = responseDict[@

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants