@@ -113,6 +113,9 @@ @implementation FLTFirebaseAuthPlugin {
113113 // Map an id to a MultiFactorResolver object.
114114 NSMutableDictionary <NSString *, FIRTOTPSecret *> *_multiFactorTotpSecretMap;
115115
116+ // Emulator host/port per app, used to build REST URLs for workarounds.
117+ NSMutableDictionary <NSString *, NSDictionary *> *_emulatorConfigs;
118+
116119 NSObject <FlutterBinaryMessenger> *_binaryMessenger;
117120 NSMutableDictionary <NSString *, FlutterEventChannel *> *_eventChannels;
118121 NSMutableDictionary <NSString *, NSObject <FlutterStreamHandler> *> *_streamHandlers;
@@ -134,6 +137,7 @@ - (instancetype)init:(NSObject<FlutterBinaryMessenger> *)messenger {
134137 _multiFactorResolverMap = [NSMutableDictionary dictionary ];
135138 _multiFactorAssertionMap = [NSMutableDictionary dictionary ];
136139 _multiFactorTotpSecretMap = [NSMutableDictionary dictionary ];
140+ _emulatorConfigs = [NSMutableDictionary dictionary ];
137141 }
138142 return self;
139143}
@@ -1137,7 +1141,20 @@ - (void)checkActionCodeApp:(nonnull AuthPigeonFirebaseApp *)app
11371141 if (error != nil ) {
11381142 completion (nil , [FLTFirebaseAuthPlugin convertToFlutterError: error]);
11391143 } else {
1140- completion ([self parseActionCode: info], nil );
1144+ PigeonActionCodeInfo *result = [self parseActionCode: info];
1145+ if (result.operation == ActionCodeInfoOperationUnknown) {
1146+ // Workaround: Firebase iOS SDK >=11.12.0 returns .unknown because
1147+ // actionCodeOperation(forRequestType:) only matches camelCase but the
1148+ // REST API returns SCREAMING_SNAKE_CASE (e.g. "VERIFY_EMAIL").
1149+ // Re-fetch the raw requestType via REST to resolve the operation.
1150+ // See: https://github.com/firebase/flutterfire/issues/17452
1151+ [self resolveActionCodeOperationForApp: app
1152+ code: code
1153+ fallbackInfo: result
1154+ completion: completion];
1155+ } else {
1156+ completion (result, nil );
1157+ }
11411158 }
11421159 }];
11431160}
@@ -1167,6 +1184,91 @@ - (PigeonActionCodeInfo *_Nullable)parseActionCode:(nonnull FIRActionCodeInfo *)
11671184 return [PigeonActionCodeInfo makeWithOperation: operation data: data];
11681185}
11691186
1187+ // / Maps a raw requestType string (either camelCase or SCREAMING_SNAKE_CASE) to
1188+ // / the corresponding Pigeon enum value.
1189+ + (ActionCodeInfoOperation)operationFromRequestType : (nullable NSString *)requestType {
1190+ static NSDictionary <NSString *, NSNumber *> *mapping;
1191+ static dispatch_once_t onceToken;
1192+ dispatch_once (&onceToken, ^{
1193+ mapping = @{
1194+ @" PASSWORD_RESET" : @(ActionCodeInfoOperationPasswordReset),
1195+ @" resetPassword" : @(ActionCodeInfoOperationPasswordReset),
1196+ @" VERIFY_EMAIL" : @(ActionCodeInfoOperationVerifyEmail),
1197+ @" verifyEmail" : @(ActionCodeInfoOperationVerifyEmail),
1198+ @" RECOVER_EMAIL" : @(ActionCodeInfoOperationRecoverEmail),
1199+ @" recoverEmail" : @(ActionCodeInfoOperationRecoverEmail),
1200+ @" EMAIL_SIGNIN" : @(ActionCodeInfoOperationEmailSignIn),
1201+ @" signIn" : @(ActionCodeInfoOperationEmailSignIn),
1202+ @" VERIFY_AND_CHANGE_EMAIL" : @(ActionCodeInfoOperationVerifyAndChangeEmail),
1203+ @" verifyAndChangeEmail" : @(ActionCodeInfoOperationVerifyAndChangeEmail),
1204+ @" REVERT_SECOND_FACTOR_ADDITION" : @(ActionCodeInfoOperationRevertSecondFactorAddition),
1205+ @" revertSecondFactorAddition" : @(ActionCodeInfoOperationRevertSecondFactorAddition),
1206+ };
1207+ });
1208+
1209+ NSNumber *value = mapping[requestType];
1210+ return value ? (ActionCodeInfoOperation)value.integerValue : ActionCodeInfoOperationUnknown;
1211+ }
1212+
1213+ // / Calls the Identity Toolkit REST API directly to retrieve the raw requestType
1214+ // / string, which the iOS SDK fails to parse correctly. Falls back to the original
1215+ // / result if the REST call fails for any reason.
1216+ - (void )resolveActionCodeOperationForApp : (nonnull AuthPigeonFirebaseApp *)app
1217+ code : (nonnull NSString *)code
1218+ fallbackInfo : (nonnull PigeonActionCodeInfo *)fallbackInfo
1219+ completion : (nonnull void (^)(PigeonActionCodeInfo *_Nullable,
1220+ FlutterError *_Nullable))completion {
1221+ FIRApp *firebaseApp = [FLTFirebasePlugin firebaseAppNamed: app.appName];
1222+ NSString *apiKey = firebaseApp.options .APIKey ;
1223+
1224+ NSString *baseURL;
1225+ NSDictionary *emulatorConfig = _emulatorConfigs[app.appName];
1226+ if (emulatorConfig) {
1227+ baseURL = [NSString stringWithFormat: @" http://%@ :%@ /identitytoolkit.googleapis.com" ,
1228+ emulatorConfig[@" host" ], emulatorConfig[@" port" ]];
1229+ } else {
1230+ baseURL = @" https://identitytoolkit.googleapis.com" ;
1231+ }
1232+
1233+ NSString *urlString =
1234+ [NSString stringWithFormat: @" %@ /v1/accounts:resetPassword?key=%@ " , baseURL, apiKey];
1235+ NSURL *url = [NSURL URLWithString: urlString];
1236+
1237+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: url];
1238+ request.HTTPMethod = @" POST" ;
1239+ [request setValue: @" application/json" forHTTPHeaderField: @" Content-Type" ];
1240+ request.HTTPBody = [NSJSONSerialization dataWithJSONObject: @{@" oobCode" : code}
1241+ options: 0
1242+ error: nil ];
1243+
1244+ NSURLSessionDataTask *task = [[NSURLSession sharedSession ]
1245+ dataTaskWithRequest: request
1246+ completionHandler: ^(NSData *_Nullable data, NSURLResponse *_Nullable response,
1247+ NSError *_Nullable error) {
1248+ if (error || !data) {
1249+ completion (fallbackInfo, nil );
1250+ return ;
1251+ }
1252+
1253+ NSDictionary *json = [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil ];
1254+ if (!json || json[@" error" ]) {
1255+ completion (fallbackInfo, nil );
1256+ return ;
1257+ }
1258+
1259+ ActionCodeInfoOperation operation =
1260+ [FLTFirebaseAuthPlugin operationFromRequestType: json[@" requestType" ]];
1261+
1262+ if (operation != ActionCodeInfoOperationUnknown) {
1263+ completion ([PigeonActionCodeInfo makeWithOperation: operation data: fallbackInfo.data],
1264+ nil );
1265+ } else {
1266+ completion (fallbackInfo, nil );
1267+ }
1268+ }];
1269+ [task resume ];
1270+ }
1271+
11701272- (void )confirmPasswordResetApp : (nonnull AuthPigeonFirebaseApp *)app
11711273 code : (nonnull NSString *)code
11721274 newPassword : (nonnull NSString *)newPassword
@@ -1600,6 +1702,7 @@ - (void)useEmulatorApp:(nonnull AuthPigeonFirebaseApp *)app
16001702 completion : (nonnull void (^)(FlutterError *_Nullable))completion {
16011703 FIRAuth *auth = [self getFIRAuthFromAppNameFromPigeon: app];
16021704 [auth useEmulatorWithHost: host port: port];
1705+ _emulatorConfigs[app.appName] = @{@" host" : host, @" port" : @(port)};
16031706 completion (nil );
16041707}
16051708
0 commit comments