Add support for the new FCM registration ID.#16133
Conversation
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`.
Using Gemini Code AssistThe 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
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 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. |
Doris-Ge
left a comment
There was a problem hiding this comment.
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!
…okenFetchOperation. It's not used by other classes.
…didUnregister is called only when the deletion succeeded.
| @"apns_token" : apnsTokenString, | ||
| @"app_version" : appVersion, | ||
| @"apns_environment" : apnsEnvironment | ||
| }, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Discussed offline. we will add that later.
| @"No Sender ID is available to register"); | ||
| return; | ||
| } | ||
| [self setupInstallationIDObserver]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
| } | ||
|
|
||
| NSString *urlString = [NSString | ||
| stringWithFormat:@"https://fcmregistrations.googleapis.com/v1/projects/%@/registrations", |
There was a problem hiding this comment.
Just curious: after this change is backported to google3, will we point our 1P SDKs to use the staging API or not?
There was a problem hiding this comment.
AFAIK, 1p and 3p use the same SDK. They use the staging backend server for some traffic.
There was a problem hiding this comment.
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?
We decided to make it a required input.
| return; | ||
| } | ||
| // Registration will only be triggered if FID is changed | ||
| if (![identifier isEqualToString:self.lastKnownFID]) { |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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.
|
@leojaygoogle @Doris-Ge , can messaging CI please be fixed? |
|
/gemini review |
There was a problem hiding this comment.
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.
| FIRMessaging_STRONGIFY(self); | ||
| // Retry on network error or backend server 5xx error. | ||
| if ((error || isServerError(response)) && self->_retryCount < kMaxRetries) { |
There was a problem hiding this comment.
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) {| FIRMessaging_STRONGIFY(self); | ||
| self->_retryCount++; | ||
| [self makeRegistrationRequestWithAuthToken:authToken]; |
There was a problem hiding this comment.
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];| FIRMessaging_STRONGIFY(self); | ||
|
|
||
| // Retry on network error or backend server 5xx error. | ||
| if ((error || isServerError(response)) && self->_retryCount < kMaxRetries) { |
There was a problem hiding this comment.
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) {| FIRMessaging_STRONGIFY(self); | ||
| self->_retryCount++; | ||
| [self makeUnregistrationRequestWithAuthToken:authToken]; |
There was a problem hiding this comment.
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];| [self retrieveFCMTokenForSenderID:self.tokenManager.fcmSenderID | ||
| completion:^(NSString *_Nullable FCMToken, NSError *_Nullable error){ | ||
| }]; |
There was a problem hiding this comment.
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){
}];
}| NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration; | ||
| NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; |
There was a problem hiding this comment.
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];
});| NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration; | ||
| NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; |
There was a problem hiding this comment.
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.
| 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]; | |
| }); |
| NSString *name = responseDict[@"name"]; | ||
| NSString *fid = nil; | ||
| if (name.length) { |
There was a problem hiding this comment.
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[@
This is to integrate with the new V1 Registration API go/android-gmi-v1-registration-api