@react-native-firebase/auth 26.3.3 → 27.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/RNFBAuth.podspec +8 -2
  3. package/android/build.gradle +5 -0
  4. package/android/src/main/java/io/invertase/firebase/auth/AuthListenerHandle.java +26 -0
  5. package/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java +68 -102
  6. package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthCacheRegistry.kt +77 -0
  7. package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthListenerRegistry.kt +76 -0
  8. package/android/src/test/java/io/invertase/firebase/auth/RNFBAuthCacheRegistryTest.java +184 -0
  9. package/android/src/test/java/io/invertase/firebase/auth/RNFBAuthListenerRegistryTest.java +171 -0
  10. package/dist/module/ActionCodeURL.js +1 -1
  11. package/dist/module/ActionCodeURL.js.map +1 -1
  12. package/dist/module/index.js +1 -2
  13. package/dist/module/index.js.map +1 -1
  14. package/dist/module/version.js +1 -1
  15. package/dist/typescript/lib/ActionCodeURL.d.ts.map +1 -1
  16. package/dist/typescript/lib/types/internal.d.ts +2 -3
  17. package/dist/typescript/lib/types/internal.d.ts.map +1 -1
  18. package/dist/typescript/lib/version.d.ts +1 -1
  19. package/ios/RNFBAuth/RNFBAuthCacheRegistry.h +41 -0
  20. package/ios/RNFBAuth/RNFBAuthCacheRegistry.m +84 -0
  21. package/ios/RNFBAuth/RNFBAuthCacheRegistryStorage.swift +53 -0
  22. package/ios/RNFBAuth/RNFBAuthErrorCodeMapper.swift +333 -0
  23. package/ios/RNFBAuth/RNFBAuthHelper.m +147 -262
  24. package/ios/RNFBAuth/RNFBAuthListenerRegistry.h +39 -0
  25. package/ios/RNFBAuth/RNFBAuthListenerRegistry.m +83 -0
  26. package/ios/RNFBAuth/RNFBAuthListenerRegistryStorage.swift +76 -0
  27. package/ios/RNFBAuth/RNFBAuthNullSentinelDecoder.h +40 -0
  28. package/ios/RNFBAuth/RNFBAuthNullSentinelDecoder.m +49 -0
  29. package/ios/RNFBAuth.xcodeproj/project.pbxproj +22 -0
  30. package/ios/RNFBAuthUnitTests/RNFBAuthCacheRegistryStorageTests.swift +72 -0
  31. package/ios/RNFBAuthUnitTests/RNFBAuthCacheRegistryTests.m +131 -0
  32. package/ios/RNFBAuthUnitTests/RNFBAuthErrorCodeMapperTests.swift +250 -0
  33. package/ios/RNFBAuthUnitTests/RNFBAuthListenerRegistryStorageTests.swift +122 -0
  34. package/ios/RNFBAuthUnitTests/RNFBAuthListenerRegistryTests.m +152 -0
  35. package/ios/RNFBAuthUnitTests/RNFBAuthNullSentinelDecodeTests.m +78 -0
  36. package/ios/RNFBAuthUnitTests/RNFBAuthUnitTests.xcodeproj/project.pbxproj +311 -0
  37. package/ios/RNFBAuthUnitTests/RNFBAuthUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBAuthUnitTests.xcscheme +69 -0
  38. package/lib/ActionCodeURL.ts +1 -2
  39. package/lib/index.ts +1 -1
  40. package/lib/types/internal.ts +2 -2
  41. package/lib/version.ts +1 -1
  42. package/package.json +5 -4
  43. package/plugin/build/index.js +4 -0
  44. package/plugin/build/ios/index.d.ts +2 -1
  45. package/plugin/build/ios/index.js +3 -1
  46. package/plugin/build/ios/openUrlFix.js +12 -0
  47. package/plugin/build/ios/sceneDelegate.d.ts +34 -0
  48. package/plugin/build/ios/sceneDelegate.js +127 -0
  49. package/plugin/build/ios/sceneLifecycle.d.ts +32 -0
  50. package/plugin/build/ios/sceneLifecycle.js +115 -0
  51. package/plugin/src/index.ts +9 -1
  52. package/plugin/src/ios/index.ts +2 -1
  53. package/plugin/src/ios/openUrlFix.ts +15 -0
  54. package/plugin/src/ios/sceneDelegate.ts +159 -0
  55. package/plugin/src/ios/sceneLifecycle.ts +134 -0
  56. package/plugin/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [27.0.0](https://github.com/invertase/react-native-firebase/compare/v26.4.0...v27.0.0) (2026-10-19)
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13
11
+
12
+ ### Features
13
+
14
+ - **app, sdk:** bump Firebase Android 35, Apple 13, and JS 13 ([0ca03d1](https://github.com/invertase/react-native-firebase/commit/0ca03d11b0c271fb37cfbee8a0c3074a21969f0b))
15
+
16
+ ### Bug Fixes
17
+
18
+ - **app,database:** resolve platform native module when bundlers apply package exports ([959d91f](https://github.com/invertase/react-native-firebase/commit/959d91fc79d7155d1d9a08e1f836789e6e6d6156))
19
+ - **auth, analytics, remote-config:** unwrap iOS null sentinels on Object args ([53fdb6a](https://github.com/invertase/react-native-firebase/commit/53fdb6a1c273d4125cff69e33b2d0af6bfd7d6dc))
20
+ - **auth, android:** accept null keys in auth cache registry get/take ([211c6cf](https://github.com/invertase/react-native-firebase/commit/211c6cff37c004c06134242cf84e670afd52eff5))
21
+ - **auth:** apply the reCAPTCHA openURL fix on the UIScene life cycle ([327fbe3](https://github.com/invertase/react-native-firebase/commit/327fbe319ba238c4f50aadc85ed687225b0804c8))
22
+ - **auth:** remove sensitive authentication logs ([57c5d1c](https://github.com/invertase/react-native-firebase/commit/57c5d1c993e8497148cd40a706fb40ebe01426ef))
23
+
24
+ ## [26.4.0](https://github.com/invertase/react-native-firebase/compare/v26.3.3...v26.4.0) (2026-09-05)
25
+
26
+ ### Bug Fixes
27
+
28
+ - **auth, ios:** look up customAuthDomains with JS app name ([9873f61](https://github.com/invertase/react-native-firebase/commit/9873f618a972c3324ec05b9e30f5aaefa7ab163b))
29
+ - **auth:** [LOW] handle malformed action links ([0961ccc](https://github.com/invertase/react-native-firebase/commit/0961ccc1108195c40fbb00ba38280f70f1419fbe))
30
+ - **auth:** use RNFBHandleMap for auth state and id-token listeners ([a708e84](https://github.com/invertase/react-native-firebase/commit/a708e847119995b8cfbde07f11ea84d48aede29b))
31
+ - **auth:** use RNFBHandleMap for credential and MFA caches ([bd891f5](https://github.com/invertase/react-native-firebase/commit/bd891f5f8955c8c08d9d9447f2442beda91c9174))
32
+
6
33
  ## [26.3.3](https://github.com/invertase/react-native-firebase/compare/v26.3.2...v26.3.3) (2026-09-01)
7
34
 
8
35
  ### Bug Fixes
package/RNFBAuth.podspec CHANGED
@@ -28,14 +28,20 @@ Pod::Spec.new do |s|
28
28
  s.ios.deployment_target = firebase_ios_target
29
29
  s.macos.deployment_target = firebase_macos_target
30
30
  s.tvos.deployment_target = firebase_tvos_target
31
- s.source_files = 'ios/**/*.{h,m,mm,cpp}'
31
+ s.swift_version = '5.10'
32
+ s.source_files = 'ios/**/*.{h,m,mm,cpp,swift}'
32
33
  s.private_header_files = [
33
34
  'ios/RNFBAuth/RNFBAuthModule.h',
35
+ 'ios/RNFBAuth/RNFBAuthNullSentinelDecoder.h',
34
36
  'ios/generated/**/*.h',
35
37
  ]
36
- s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
38
+ # CocoaPods FileList uses FNM_PATHNAME: `ios/*UnitTests/**` does not match nested
39
+ # paths like `ios/RNFBAuthUnitTests/HostStubs/**` (those would compile into the pod).
40
+ s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*', 'ios/*UnitTests/**/*'
37
41
 
42
+ # DEFINES_MODULE is required for the generated RNFBAuth-Swift.h interface.
38
43
  s.pod_target_xcconfig = {
44
+ "DEFINES_MODULE" => "YES",
39
45
  "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBAuthTurboModules\" \"$(PODS_TARGET_SRCROOT)/ios/generated\"",
40
46
  "CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES" => "YES",
41
47
  }
@@ -13,6 +13,7 @@ buildscript {
13
13
 
14
14
  dependencies {
15
15
  classpath("com.android.tools.build:gradle:8.4.0")
16
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${project.findProperty('kotlinVersion') ?: '2.4.0'}")
16
17
  }
17
18
  }
18
19
  }
@@ -21,6 +22,8 @@ plugins {
21
22
  id "io.invertase.gradle.build" version "1.5"
22
23
  }
23
24
 
25
+ apply plugin: "org.jetbrains.kotlin.android"
26
+
24
27
  def appProject
25
28
  if (findProject(':@react-native-firebase_app')) {
26
29
  appProject = project(':@react-native-firebase_app')
@@ -91,6 +94,8 @@ dependencies {
91
94
  api appProject
92
95
  implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion("firebase", "bom")}")
93
96
  implementation "com.google.firebase:firebase-auth"
97
+
98
+ testImplementation "junit:junit:4.13.2"
94
99
  }
95
100
 
96
101
  ReactNative.shared.applyPackageVersion()
@@ -0,0 +1,26 @@
1
+ package io.invertase.firebase.auth;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ /**
21
+ * Detach surface stored in {@link RNFBAuthListenerRegistry}. Invoked after {@code get}/{@code
22
+ * take}/{@code takeAll}, never under the HandleMap lock.
23
+ */
24
+ interface AuthListenerHandle {
25
+ void remove();
26
+ }
@@ -84,7 +84,6 @@ import java.text.SimpleDateFormat;
84
84
  import java.util.ArrayList;
85
85
  import java.util.Date;
86
86
  import java.util.HashMap;
87
- import java.util.Iterator;
88
87
  import java.util.List;
89
88
  import java.util.Locale;
90
89
  import java.util.Map;
@@ -104,21 +103,23 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
104
103
  new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
105
104
 
106
105
  private static final String TAG = "Auth";
107
- private static HashMap<String, FirebaseAuth.AuthStateListener> mAuthListeners = new HashMap<>();
108
- private static HashMap<String, FirebaseAuth.IdTokenListener> mIdTokenListeners = new HashMap<>();
106
+ private static final RNFBAuthListenerRegistry authStateListeners = new RNFBAuthListenerRegistry();
107
+ private static final RNFBAuthListenerRegistry idTokenListeners = new RNFBAuthListenerRegistry();
109
108
  private static HashMap<String, String> emulatorConfigs = new HashMap<>();
110
109
  private String mVerificationId;
111
110
  private String mLastPhoneNumber;
112
111
  private PhoneAuthProvider.ForceResendingToken mForceResendingToken;
113
112
  private PhoneAuthCredential mCredential;
114
113
 
115
- private final HashMap<String, MultiFactorResolver> mCachedResolvers = new HashMap<>();
116
- private final HashMap<String, MultiFactorSession> mMultiFactorSessions = new HashMap<>();
117
- private final HashMap<String, TotpSecret> mTotpSecrets = new HashMap<>();
114
+ private final RNFBAuthCacheRegistry<MultiFactorResolver> mCachedResolvers =
115
+ new RNFBAuthCacheRegistry<>();
116
+ private final RNFBAuthCacheRegistry<MultiFactorSession> mMultiFactorSessions =
117
+ new RNFBAuthCacheRegistry<>();
118
+ private final RNFBAuthCacheRegistry<TotpSecret> mTotpSecrets = new RNFBAuthCacheRegistry<>();
118
119
 
119
120
  // storage for anonymous phone auth credentials, used for linkWithCredentials
120
121
  // https://github.com/invertase/react-native-firebase/issues/4911
121
- private HashMap<String, AuthCredential> credentials = new HashMap<>();
122
+ private final RNFBAuthCacheRegistry<AuthCredential> credentials = new RNFBAuthCacheRegistry<>();
122
123
 
123
124
  private final TaskExecutorService executorService;
124
125
 
@@ -142,34 +143,13 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
142
143
  super.invalidate();
143
144
  Log.d(TAG, "instance-destroyed");
144
145
 
145
- Iterator authListenerIterator = mAuthListeners.entrySet().iterator();
146
-
147
- while (authListenerIterator.hasNext()) {
148
- Map.Entry pair = (Map.Entry) authListenerIterator.next();
149
- String appName = (String) pair.getKey();
150
- FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
151
- FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
152
- FirebaseAuth.AuthStateListener mAuthListener =
153
- (FirebaseAuth.AuthStateListener) pair.getValue();
154
- firebaseAuth.removeAuthStateListener(mAuthListener);
155
- authListenerIterator.remove();
156
- }
157
-
158
- Iterator idTokenListenerIterator = mIdTokenListeners.entrySet().iterator();
159
-
160
- while (idTokenListenerIterator.hasNext()) {
161
- Map.Entry pair = (Map.Entry) idTokenListenerIterator.next();
162
- String appName = (String) pair.getKey();
163
- FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
164
- FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
165
- FirebaseAuth.IdTokenListener mAuthListener = (FirebaseAuth.IdTokenListener) pair.getValue();
166
- firebaseAuth.removeIdTokenListener(mAuthListener);
167
- idTokenListenerIterator.remove();
168
- }
146
+ authStateListeners.takeAllAndRemove();
147
+ idTokenListeners.takeAllAndRemove();
169
148
 
170
149
  mCachedResolvers.clear();
171
150
  mMultiFactorSessions.clear();
172
151
  mTotpSecrets.clear();
152
+ // credentials intentionally not cleared (prior HashMap behavior)
173
153
  }
174
154
 
175
155
  @Override
@@ -199,46 +179,37 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
199
179
 
200
180
  FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
201
181
  FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
202
- FirebaseAuth.AuthStateListener mAuthListener = mAuthListeners.get(appName);
203
- if (mAuthListener == null) {
204
- FirebaseAuth.AuthStateListener newAuthListener =
205
- firebaseAuth1 -> {
206
- FirebaseUser user = firebaseAuth1.getCurrentUser();
207
- WritableMap eventBody = Arguments.createMap();
208
- ReactNativeFirebaseEventEmitter emitter =
209
- ReactNativeFirebaseEventEmitter.getSharedInstance();
210
- if (user != null) {
211
- eventBody.putString("appName", appName); // for js side distribution
212
- eventBody.putMap("user", firebaseUserToMap(user));
213
- } else {
214
- eventBody.putString("appName", appName); // for js side distribution
215
- }
216
- Log.d(TAG, "addAuthStateListener:eventBody " + eventBody.toString());
182
+ if (authStateListeners.get(appName) != null) {
183
+ return;
184
+ }
217
185
 
218
- ReactNativeFirebaseEvent event =
219
- new ReactNativeFirebaseEvent("auth_state_changed", eventBody, appName);
220
- emitter.sendEvent(event);
221
- };
186
+ FirebaseAuth.AuthStateListener newAuthListener =
187
+ firebaseAuth1 -> {
188
+ FirebaseUser user = firebaseAuth1.getCurrentUser();
189
+ WritableMap eventBody = Arguments.createMap();
190
+ ReactNativeFirebaseEventEmitter emitter =
191
+ ReactNativeFirebaseEventEmitter.getSharedInstance();
192
+ if (user != null) {
193
+ eventBody.putString("appName", appName); // for js side distribution
194
+ eventBody.putMap("user", firebaseUserToMap(user));
195
+ } else {
196
+ eventBody.putString("appName", appName); // for js side distribution
197
+ }
198
+ ReactNativeFirebaseEvent event =
199
+ new ReactNativeFirebaseEvent("auth_state_changed", eventBody, appName);
200
+ emitter.sendEvent(event);
201
+ };
222
202
 
223
- firebaseAuth.addAuthStateListener(newAuthListener);
224
- mAuthListeners.put(appName, newAuthListener);
225
- }
203
+ firebaseAuth.addAuthStateListener(newAuthListener);
204
+ authStateListeners.putOrDiscard(
205
+ appName, () -> firebaseAuth.removeAuthStateListener(newAuthListener));
226
206
  }
227
207
 
228
208
  /** Removes the current auth state listener */
229
209
  @Override
230
210
  public void removeAuthStateListener(String appName) {
231
211
  Log.d(TAG, "removeAuthStateListener");
232
-
233
- FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
234
- FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
235
-
236
- FirebaseAuth.AuthStateListener mAuthListener = mAuthListeners.get(appName);
237
-
238
- if (mAuthListener != null) {
239
- firebaseAuth.removeAuthStateListener(mAuthListener);
240
- mAuthListeners.remove(appName);
241
- }
212
+ authStateListeners.takeAndRemove(appName);
242
213
  }
243
214
 
244
215
  /** Add a new id token listener - if one doesn't exist already */
@@ -249,46 +220,40 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
249
220
  FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
250
221
  FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
251
222
 
252
- if (!mIdTokenListeners.containsKey(appName)) {
253
- FirebaseAuth.IdTokenListener newIdTokenListener =
254
- firebaseAuth1 -> {
255
- FirebaseUser user = firebaseAuth1.getCurrentUser();
256
- ReactNativeFirebaseEventEmitter emitter =
257
- ReactNativeFirebaseEventEmitter.getSharedInstance();
258
- WritableMap eventBody = Arguments.createMap();
259
- if (user != null) {
260
- eventBody.putBoolean("authenticated", true);
261
- eventBody.putString("appName", appName);
262
- eventBody.putMap("user", firebaseUserToMap(user));
263
- } else {
264
- eventBody.putString("appName", appName);
265
- eventBody.putBoolean("authenticated", false);
266
- }
223
+ if (idTokenListeners.get(appName) != null) {
224
+ return;
225
+ }
267
226
 
268
- ReactNativeFirebaseEvent event =
269
- new ReactNativeFirebaseEvent("auth_id_token_changed", eventBody, appName);
270
- emitter.sendEvent(event);
271
- };
227
+ FirebaseAuth.IdTokenListener newIdTokenListener =
228
+ firebaseAuth1 -> {
229
+ FirebaseUser user = firebaseAuth1.getCurrentUser();
230
+ ReactNativeFirebaseEventEmitter emitter =
231
+ ReactNativeFirebaseEventEmitter.getSharedInstance();
232
+ WritableMap eventBody = Arguments.createMap();
233
+ if (user != null) {
234
+ eventBody.putBoolean("authenticated", true);
235
+ eventBody.putString("appName", appName);
236
+ eventBody.putMap("user", firebaseUserToMap(user));
237
+ } else {
238
+ eventBody.putString("appName", appName);
239
+ eventBody.putBoolean("authenticated", false);
240
+ }
272
241
 
273
- firebaseAuth.addIdTokenListener(newIdTokenListener);
274
- mIdTokenListeners.put(appName, newIdTokenListener);
275
- }
242
+ ReactNativeFirebaseEvent event =
243
+ new ReactNativeFirebaseEvent("auth_id_token_changed", eventBody, appName);
244
+ emitter.sendEvent(event);
245
+ };
246
+
247
+ firebaseAuth.addIdTokenListener(newIdTokenListener);
248
+ idTokenListeners.putOrDiscard(
249
+ appName, () -> firebaseAuth.removeIdTokenListener(newIdTokenListener));
276
250
  }
277
251
 
278
252
  /** Removes the current id token listener */
279
253
  @Override
280
254
  public void removeIdTokenListener(String appName) {
281
255
  Log.d(TAG, "removeIdTokenListener");
282
-
283
- FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
284
- FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
285
-
286
- FirebaseAuth.IdTokenListener mIdTokenListener = mIdTokenListeners.get(appName);
287
-
288
- if (mIdTokenListener != null) {
289
- firebaseAuth.removeIdTokenListener(mIdTokenListener);
290
- mIdTokenListeners.remove(appName);
291
- }
256
+ idTokenListeners.takeAndRemove(appName);
292
257
  }
293
258
 
294
259
  /**
@@ -1152,7 +1117,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
1152
1117
 
1153
1118
  final MultiFactorSession session = task.getResult();
1154
1119
  final String sessionId = Integer.toString(session.hashCode());
1155
- mMultiFactorSessions.put(sessionId, session);
1120
+ mMultiFactorSessions.putReplacing(sessionId, session);
1156
1121
 
1157
1122
  promise.resolve(sessionId);
1158
1123
  });
@@ -1493,7 +1458,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
1493
1458
  if (task.isSuccessful()) {
1494
1459
  TotpSecret totpSecret = task.getResult();
1495
1460
  String totpSecretKey = totpSecret.getSharedSecretKey();
1496
- mTotpSecrets.put(totpSecretKey, totpSecret);
1461
+ mTotpSecrets.putReplacing(totpSecretKey, totpSecret);
1497
1462
  WritableMap result = Arguments.createMap();
1498
1463
  result.putString("secretKey", totpSecretKey);
1499
1464
  promise.resolve(result);
@@ -1551,7 +1516,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
1551
1516
  final String requestKey,
1552
1517
  final double timeout,
1553
1518
  final boolean forceResend) {
1554
- Log.d(TAG, "verifyPhoneNumber:" + phoneNumber);
1519
+ Log.d(TAG, "verifyPhoneNumber");
1555
1520
 
1556
1521
  FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
1557
1522
  final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
@@ -2061,8 +2026,9 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
2061
2026
  if (provider.startsWith("oidc.")) {
2062
2027
  return OAuthProvider.newCredentialBuilder(provider).setIdToken(authToken).build();
2063
2028
  }
2064
- if (credentials.containsKey(authToken) && credentials.get(authToken) != null) {
2065
- return credentials.get(authToken);
2029
+ AuthCredential cachedCredential = credentials.get(authToken);
2030
+ if (cachedCredential != null) {
2031
+ return cachedCredential;
2066
2032
  }
2067
2033
 
2068
2034
  switch (provider) {
@@ -2463,7 +2429,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
2463
2429
  authCredentialsMap.putString("secret", null);
2464
2430
 
2465
2431
  // Temporarily store the non-serializable credential for later
2466
- credentials.put(authHashCode, authCredential);
2432
+ credentials.putReplacing(authHashCode, authCredential);
2467
2433
 
2468
2434
  WritableMap userInfoMap = Arguments.createMap();
2469
2435
  userInfoMap.putString("code", error.getString("code"));
@@ -2565,7 +2531,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
2565
2531
  code = "MULTI_FACTOR_AUTH_REQUIRED";
2566
2532
  final MultiFactorResolver resolver = multiFactorException.getResolver();
2567
2533
  final String sessionId = Integer.toString(resolver.getSession().hashCode());
2568
- mCachedResolvers.put(sessionId, resolver);
2534
+ mCachedResolvers.putReplacing(sessionId, resolver);
2569
2535
  // Passing around a resolver ReadableMap leads to issues when trying to send the data back by
2570
2536
  // calling Promise#reject. Building the map just before sending solves that issue.
2571
2537
  error.putString("sessionId", sessionId);
@@ -0,0 +1,77 @@
1
+ package io.invertase.firebase.auth
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import io.invertase.firebase.common.RNFBHandleCollisionException
21
+ import io.invertase.firebase.common.RNFBHandleMap
22
+
23
+ /**
24
+ * Credential / MFA cache map (data only — no SDK cancel). Unique [put]; callers that previously
25
+ * HashMap-upserted use [putReplacing] (atomic last wins). Peek with [get]; remove with [take];
26
+ * invalidate with [clear].
27
+ *
28
+ * Collision vs prior HashMap.put upsert: unique [put] throws; replace at call sites is
29
+ * [putReplacing] (last wins). [putOrDiscard] keeps the first mapping.
30
+ */
31
+ internal class RNFBAuthCacheRegistry<V> {
32
+ private val map = RNFBHandleMap<String, V>()
33
+
34
+ @Throws(RNFBHandleCollisionException::class)
35
+ fun put(
36
+ key: String,
37
+ value: V,
38
+ ) {
39
+ map.put(key, value)
40
+ }
41
+
42
+ /**
43
+ * Unique put. On collision, leave the existing mapping and discard the incoming value (data
44
+ * only).
45
+ *
46
+ * @return true if stored
47
+ */
48
+ fun putOrDiscard(
49
+ key: String,
50
+ value: V,
51
+ ): Boolean = map.putIfAbsent(key, value)
52
+
53
+ /**
54
+ * Atomic replace (last wins). Preserves prior HashMap `put` upsert. Taken value discarded.
55
+ */
56
+ fun putReplacing(
57
+ key: String,
58
+ value: V,
59
+ ) {
60
+ map.putReplacing(key, value)
61
+ }
62
+
63
+ /**
64
+ * Peek. Nullable [key] matches prior Java `HashMap.get(null)` → null (no NPE). Java callers such
65
+ * as `promiseRejectAuthException` may pass a null sessionId when the error map has none.
66
+ */
67
+ fun get(key: String?): V? = if (key == null) null else map.get(key)
68
+
69
+ /**
70
+ * Remove. Nullable [key] matches prior Java `HashMap.remove(null)` → null (no NPE).
71
+ */
72
+ fun take(key: String?): V? = if (key == null) null else map.take(key)
73
+
74
+ fun clear() {
75
+ map.takeAll()
76
+ }
77
+ }
@@ -0,0 +1,76 @@
1
+ package io.invertase.firebase.auth
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import io.invertase.firebase.common.RNFBHandleCollisionException
21
+ import io.invertase.firebase.common.RNFBHandleMap
22
+
23
+ /**
24
+ * Auth-state / id-token listener map. Unique [put] / [putOrDiscard]; callers [take] or
25
+ * [takeAllAndRemove] then `remove()` outside the HandleMap lock. Skip-if-registered is
26
+ * `get(appName) != null`.
27
+ *
28
+ * Null-key note: [get]/[take]/[takeAndRemove] keep non-null [String] keys. Call sites always
29
+ * pass TurboModule `appName` (never null). Unlike [RNFBAuthCacheRegistry], no Java path passes a
30
+ * null lookup key.
31
+ */
32
+ internal class RNFBAuthListenerRegistry {
33
+ private val map = RNFBHandleMap<String, AuthListenerHandle?>()
34
+
35
+ @Throws(RNFBHandleCollisionException::class)
36
+ fun put(
37
+ appName: String,
38
+ handle: AuthListenerHandle?,
39
+ ) {
40
+ map.put(appName, handle)
41
+ }
42
+
43
+ /**
44
+ * Unique put. On collision, `remove()` the incoming handle and leave the existing mapping.
45
+ *
46
+ * @return true if stored
47
+ */
48
+ fun putOrDiscard(
49
+ appName: String,
50
+ handle: AuthListenerHandle?,
51
+ ): Boolean {
52
+ try {
53
+ map.put(appName, handle)
54
+ return true
55
+ } catch (_: RNFBHandleCollisionException) {
56
+ handle?.remove()
57
+ return false
58
+ }
59
+ }
60
+
61
+ fun get(appName: String): AuthListenerHandle? = map.get(appName)
62
+
63
+ fun take(appName: String): AuthListenerHandle? = map.take(appName)
64
+
65
+ fun takeAndRemove(appName: String) {
66
+ val handle = map.take(appName)
67
+ handle?.remove()
68
+ }
69
+
70
+ fun takeAllAndRemove() {
71
+ val remaining = map.takeAll()
72
+ for (handle in remaining) {
73
+ handle?.remove()
74
+ }
75
+ }
76
+ }