Skip to content

Commit 60b5cd5

Browse files
authored
fix(auth): fix inconsistence in casing in the native iOS SDK and Web SDK (#18086)
* fix(auth, ios): fix inconsistence in casing in the native iOS SDK with a workaround * format * fix for web
1 parent 476ba53 commit 60b5cd5

File tree

4 files changed

+179
-2
lines changed

4 files changed

+179
-2
lines changed

packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ extension type ConfirmationResultJsImpl._(JSObject _) implements JSObject {
575575
/// See: <https://firebase.google.com/docs/reference/js/firebase.auth.ActionCodeInfo>.
576576
extension type ActionCodeInfo._(JSObject _) implements JSObject {
577577
external ActionCodeData get data;
578+
external JSString get operation;
578579
}
579580

580581
/// Interface representing a user's metadata.

packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,34 @@ ActionCodeInfo? convertWebActionCodeInfo(
172172
}
173173

174174
return ActionCodeInfo(
175-
operation: ActionCodeInfoOperation.passwordReset,
175+
operation:
176+
_convertWebActionCodeOperation(webActionCodeInfo.operation.toDart),
176177
data: ActionCodeInfoData(
177178
email: webActionCodeInfo.data.email?.toDart,
178179
previousEmail: webActionCodeInfo.data.previousEmail?.toDart,
179180
),
180181
);
181182
}
182183

184+
ActionCodeInfoOperation _convertWebActionCodeOperation(String operation) {
185+
switch (operation) {
186+
case 'EMAIL_SIGNIN':
187+
return ActionCodeInfoOperation.emailSignIn;
188+
case 'PASSWORD_RESET':
189+
return ActionCodeInfoOperation.passwordReset;
190+
case 'RECOVER_EMAIL':
191+
return ActionCodeInfoOperation.recoverEmail;
192+
case 'REVERT_SECOND_FACTOR_ADDITION':
193+
return ActionCodeInfoOperation.revertSecondFactorAddition;
194+
case 'VERIFY_AND_CHANGE_EMAIL':
195+
return ActionCodeInfoOperation.verifyAndChangeEmail;
196+
case 'VERIFY_EMAIL':
197+
return ActionCodeInfoOperation.verifyEmail;
198+
default:
199+
return ActionCodeInfoOperation.unknown;
200+
}
201+
}
202+
183203
/// Converts a [auth_interop.AdditionalUserInfo] into a [AdditionalUserInfo].
184204
AdditionalUserInfo? convertWebAdditionalUserInfo(
185205
auth_interop.AdditionalUserInfo? webAdditionalUserInfo,

tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,59 @@ void main() {
271271
fail(e.toString());
272272
}
273273
});
274+
275+
test('returns correct operation for verifyEmail action code',
276+
() async {
277+
final email = generateRandomEmail();
278+
await FirebaseAuth.instance.createUserWithEmailAndPassword(
279+
email: email,
280+
password: testPassword,
281+
);
282+
283+
await FirebaseAuth.instance.currentUser!.sendEmailVerification();
284+
285+
final oobCode = await emulatorOutOfBandCode(
286+
email,
287+
EmulatorOobCodeType.verifyEmail,
288+
);
289+
expect(oobCode, isNotNull);
290+
291+
final actionCodeInfo = await FirebaseAuth.instance.checkActionCode(
292+
oobCode!.oobCode!,
293+
);
294+
295+
expect(
296+
actionCodeInfo.operation,
297+
equals(ActionCodeInfoOperation.verifyEmail),
298+
);
299+
});
300+
301+
test('returns correct operation for passwordReset action code',
302+
() async {
303+
final email = generateRandomEmail();
304+
await FirebaseAuth.instance.createUserWithEmailAndPassword(
305+
email: email,
306+
password: testPassword,
307+
);
308+
await ensureSignedOut();
309+
310+
await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
311+
312+
final oobCode = await emulatorOutOfBandCode(
313+
email,
314+
EmulatorOobCodeType.passwordReset,
315+
);
316+
expect(oobCode, isNotNull);
317+
318+
final actionCodeInfo = await FirebaseAuth.instance.checkActionCode(
319+
oobCode!.oobCode!,
320+
);
321+
322+
expect(
323+
actionCodeInfo.operation,
324+
equals(ActionCodeInfoOperation.passwordReset),
325+
);
326+
});
274327
},
275328
skip: !kIsWeb && Platform.isWindows,
276329
);

0 commit comments

Comments
 (0)