@react-native-firebase/auth 26.3.3 → 26.4.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 (26) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/RNFBAuth.podspec +1 -1
  3. package/android/build.gradle +2 -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 +69 -101
  6. package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthCacheRegistry.java +66 -0
  7. package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthListenerRegistry.java +76 -0
  8. package/android/src/test/java/io/invertase/firebase/auth/RNFBAuthCacheRegistryTest.java +172 -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/version.js +1 -1
  13. package/dist/typescript/lib/ActionCodeURL.d.ts.map +1 -1
  14. package/dist/typescript/lib/version.d.ts +1 -1
  15. package/ios/RNFBAuth/RNFBAuthCacheRegistry.h +41 -0
  16. package/ios/RNFBAuth/RNFBAuthCacheRegistry.m +65 -0
  17. package/ios/RNFBAuth/RNFBAuthHelper.m +124 -109
  18. package/ios/RNFBAuth/RNFBAuthListenerRegistry.h +39 -0
  19. package/ios/RNFBAuth/RNFBAuthListenerRegistry.m +83 -0
  20. package/ios/RNFBAuthUnitTests/RNFBAuthCacheRegistryTests.m +131 -0
  21. package/ios/RNFBAuthUnitTests/RNFBAuthListenerRegistryTests.m +152 -0
  22. package/ios/RNFBAuthUnitTests/RNFBAuthUnitTests.xcodeproj/project.pbxproj +261 -0
  23. package/ios/RNFBAuthUnitTests/RNFBAuthUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBAuthUnitTests.xcscheme +69 -0
  24. package/lib/ActionCodeURL.ts +1 -2
  25. package/lib/version.ts +1 -1
  26. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
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
+ ## [26.4.0](https://github.com/invertase/react-native-firebase/compare/v26.3.3...v26.4.0) (2026-09-05)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **auth, ios:** look up customAuthDomains with JS app name ([9873f61](https://github.com/invertase/react-native-firebase/commit/9873f618a972c3324ec05b9e30f5aaefa7ab163b))
11
+ - **auth:** [LOW] handle malformed action links ([0961ccc](https://github.com/invertase/react-native-firebase/commit/0961ccc1108195c40fbb00ba38280f70f1419fbe))
12
+ - **auth:** use RNFBHandleMap for auth state and id-token listeners ([a708e84](https://github.com/invertase/react-native-firebase/commit/a708e847119995b8cfbde07f11ea84d48aede29b))
13
+ - **auth:** use RNFBHandleMap for credential and MFA caches ([bd891f5](https://github.com/invertase/react-native-firebase/commit/bd891f5f8955c8c08d9d9447f2442beda91c9174))
14
+
6
15
  ## [26.3.3](https://github.com/invertase/react-native-firebase/compare/v26.3.2...v26.3.3) (2026-09-01)
7
16
 
8
17
  ### Bug Fixes
package/RNFBAuth.podspec CHANGED
@@ -33,7 +33,7 @@ Pod::Spec.new do |s|
33
33
  'ios/RNFBAuth/RNFBAuthModule.h',
34
34
  'ios/generated/**/*.h',
35
35
  ]
36
- s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
36
+ s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*', 'ios/*UnitTests/**'
37
37
 
38
38
  s.pod_target_xcconfig = {
39
39
  "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBAuthTurboModules\" \"$(PODS_TARGET_SRCROOT)/ios/generated\"",
@@ -91,6 +91,8 @@ dependencies {
91
91
  api appProject
92
92
  implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion("firebase", "bom")}")
93
93
  implementation "com.google.firebase:firebase-auth"
94
+
95
+ testImplementation "junit:junit:4.13.2"
94
96
  }
95
97
 
96
98
  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,39 @@ 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
+ Log.d(TAG, "addAuthStateListener:eventBody " + eventBody.toString());
222
199
 
223
- firebaseAuth.addAuthStateListener(newAuthListener);
224
- mAuthListeners.put(appName, newAuthListener);
225
- }
200
+ ReactNativeFirebaseEvent event =
201
+ new ReactNativeFirebaseEvent("auth_state_changed", eventBody, appName);
202
+ emitter.sendEvent(event);
203
+ };
204
+
205
+ firebaseAuth.addAuthStateListener(newAuthListener);
206
+ authStateListeners.putOrDiscard(
207
+ appName, () -> firebaseAuth.removeAuthStateListener(newAuthListener));
226
208
  }
227
209
 
228
210
  /** Removes the current auth state listener */
229
211
  @Override
230
212
  public void removeAuthStateListener(String appName) {
231
213
  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
- }
214
+ authStateListeners.takeAndRemove(appName);
242
215
  }
243
216
 
244
217
  /** Add a new id token listener - if one doesn't exist already */
@@ -249,46 +222,40 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
249
222
  FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
250
223
  FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
251
224
 
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
- }
225
+ if (idTokenListeners.get(appName) != null) {
226
+ return;
227
+ }
267
228
 
268
- ReactNativeFirebaseEvent event =
269
- new ReactNativeFirebaseEvent("auth_id_token_changed", eventBody, appName);
270
- emitter.sendEvent(event);
271
- };
229
+ FirebaseAuth.IdTokenListener newIdTokenListener =
230
+ firebaseAuth1 -> {
231
+ FirebaseUser user = firebaseAuth1.getCurrentUser();
232
+ ReactNativeFirebaseEventEmitter emitter =
233
+ ReactNativeFirebaseEventEmitter.getSharedInstance();
234
+ WritableMap eventBody = Arguments.createMap();
235
+ if (user != null) {
236
+ eventBody.putBoolean("authenticated", true);
237
+ eventBody.putString("appName", appName);
238
+ eventBody.putMap("user", firebaseUserToMap(user));
239
+ } else {
240
+ eventBody.putString("appName", appName);
241
+ eventBody.putBoolean("authenticated", false);
242
+ }
272
243
 
273
- firebaseAuth.addIdTokenListener(newIdTokenListener);
274
- mIdTokenListeners.put(appName, newIdTokenListener);
275
- }
244
+ ReactNativeFirebaseEvent event =
245
+ new ReactNativeFirebaseEvent("auth_id_token_changed", eventBody, appName);
246
+ emitter.sendEvent(event);
247
+ };
248
+
249
+ firebaseAuth.addIdTokenListener(newIdTokenListener);
250
+ idTokenListeners.putOrDiscard(
251
+ appName, () -> firebaseAuth.removeIdTokenListener(newIdTokenListener));
276
252
  }
277
253
 
278
254
  /** Removes the current id token listener */
279
255
  @Override
280
256
  public void removeIdTokenListener(String appName) {
281
257
  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
- }
258
+ idTokenListeners.takeAndRemove(appName);
292
259
  }
293
260
 
294
261
  /**
@@ -1152,7 +1119,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
1152
1119
 
1153
1120
  final MultiFactorSession session = task.getResult();
1154
1121
  final String sessionId = Integer.toString(session.hashCode());
1155
- mMultiFactorSessions.put(sessionId, session);
1122
+ mMultiFactorSessions.putReplacing(sessionId, session);
1156
1123
 
1157
1124
  promise.resolve(sessionId);
1158
1125
  });
@@ -1493,7 +1460,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
1493
1460
  if (task.isSuccessful()) {
1494
1461
  TotpSecret totpSecret = task.getResult();
1495
1462
  String totpSecretKey = totpSecret.getSharedSecretKey();
1496
- mTotpSecrets.put(totpSecretKey, totpSecret);
1463
+ mTotpSecrets.putReplacing(totpSecretKey, totpSecret);
1497
1464
  WritableMap result = Arguments.createMap();
1498
1465
  result.putString("secretKey", totpSecretKey);
1499
1466
  promise.resolve(result);
@@ -2061,8 +2028,9 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
2061
2028
  if (provider.startsWith("oidc.")) {
2062
2029
  return OAuthProvider.newCredentialBuilder(provider).setIdToken(authToken).build();
2063
2030
  }
2064
- if (credentials.containsKey(authToken) && credentials.get(authToken) != null) {
2065
- return credentials.get(authToken);
2031
+ AuthCredential cachedCredential = credentials.get(authToken);
2032
+ if (cachedCredential != null) {
2033
+ return cachedCredential;
2066
2034
  }
2067
2035
 
2068
2036
  switch (provider) {
@@ -2463,7 +2431,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
2463
2431
  authCredentialsMap.putString("secret", null);
2464
2432
 
2465
2433
  // Temporarily store the non-serializable credential for later
2466
- credentials.put(authHashCode, authCredential);
2434
+ credentials.putReplacing(authHashCode, authCredential);
2467
2435
 
2468
2436
  WritableMap userInfoMap = Arguments.createMap();
2469
2437
  userInfoMap.putString("code", error.getString("code"));
@@ -2565,7 +2533,7 @@ public class NativeRNFBTurboAuth extends NativeRNFBTurboAuthSpec {
2565
2533
  code = "MULTI_FACTOR_AUTH_REQUIRED";
2566
2534
  final MultiFactorResolver resolver = multiFactorException.getResolver();
2567
2535
  final String sessionId = Integer.toString(resolver.getSession().hashCode());
2568
- mCachedResolvers.put(sessionId, resolver);
2536
+ mCachedResolvers.putReplacing(sessionId, resolver);
2569
2537
  // Passing around a resolver ReadableMap leads to issues when trying to send the data back by
2570
2538
  // calling Promise#reject. Building the map just before sending solves that issue.
2571
2539
  error.putString("sessionId", sessionId);
@@ -0,0 +1,66 @@
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 {@link #put}; callers that
25
+ * previously HashMap-upserted use {@link #putReplacing} (atomic last wins). Peek with {@link #get};
26
+ * remove with {@link #take}; invalidate with {@link #clear}.
27
+ *
28
+ * <p><strong>Collision vs prior HashMap.put upsert:</strong> unique {@link #put} throws; replace at
29
+ * call sites is {@link #putReplacing} (last wins). {@link #putOrDiscard} keeps the first mapping.
30
+ */
31
+ final class RNFBAuthCacheRegistry<V> {
32
+ private final RNFBHandleMap<String, V> map = new RNFBHandleMap<>();
33
+
34
+ void put(String key, V value) throws RNFBHandleCollisionException {
35
+ map.put(key, value);
36
+ }
37
+
38
+ /**
39
+ * Unique put. On collision, leave the existing mapping and discard the incoming value (data
40
+ * only).
41
+ *
42
+ * @return true if stored
43
+ */
44
+ boolean putOrDiscard(String key, V value) {
45
+ return map.putIfAbsent(key, value);
46
+ }
47
+
48
+ /**
49
+ * Atomic replace (last wins). Preserves prior HashMap {@code put} upsert. Taken value discarded.
50
+ */
51
+ void putReplacing(String key, V value) {
52
+ map.putReplacing(key, value);
53
+ }
54
+
55
+ V get(String key) {
56
+ return map.get(key);
57
+ }
58
+
59
+ V take(String key) {
60
+ return map.take(key);
61
+ }
62
+
63
+ void clear() {
64
+ map.takeAll();
65
+ }
66
+ }
@@ -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
+ import java.util.List;
23
+
24
+ /**
25
+ * Auth-state / id-token listener map. Unique {@link #put} / {@link #putOrDiscard}; callers {@link
26
+ * #take} or {@link #takeAllAndRemove} then {@code remove()} outside the HandleMap lock.
27
+ * Skip-if-registered is {@code get(appName) != null}.
28
+ */
29
+ final class RNFBAuthListenerRegistry {
30
+ private final RNFBHandleMap<String, AuthListenerHandle> map = new RNFBHandleMap<>();
31
+
32
+ void put(String appName, AuthListenerHandle handle) throws RNFBHandleCollisionException {
33
+ map.put(appName, handle);
34
+ }
35
+
36
+ /**
37
+ * Unique put. On collision, {@code remove()} the incoming handle and leave the existing mapping.
38
+ *
39
+ * @return true if stored
40
+ */
41
+ boolean putOrDiscard(String appName, AuthListenerHandle handle) {
42
+ try {
43
+ map.put(appName, handle);
44
+ return true;
45
+ } catch (RNFBHandleCollisionException collision) {
46
+ if (handle != null) {
47
+ handle.remove();
48
+ }
49
+ return false;
50
+ }
51
+ }
52
+
53
+ AuthListenerHandle get(String appName) {
54
+ return map.get(appName);
55
+ }
56
+
57
+ AuthListenerHandle take(String appName) {
58
+ return map.take(appName);
59
+ }
60
+
61
+ void takeAndRemove(String appName) {
62
+ AuthListenerHandle handle = map.take(appName);
63
+ if (handle != null) {
64
+ handle.remove();
65
+ }
66
+ }
67
+
68
+ void takeAllAndRemove() {
69
+ List<AuthListenerHandle> remaining = map.takeAll();
70
+ for (AuthListenerHandle handle : remaining) {
71
+ if (handle != null) {
72
+ handle.remove();
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,172 @@
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 static org.junit.Assert.assertFalse;
21
+ import static org.junit.Assert.assertNull;
22
+ import static org.junit.Assert.assertSame;
23
+ import static org.junit.Assert.assertTrue;
24
+ import static org.junit.Assert.fail;
25
+
26
+ import io.invertase.firebase.common.RNFBHandleCollisionException;
27
+ import java.util.concurrent.CountDownLatch;
28
+ import java.util.concurrent.TimeUnit;
29
+ import java.util.concurrent.atomic.AtomicReference;
30
+ import org.junit.Test;
31
+
32
+ /**
33
+ * JVM coverage for {@link RNFBAuthCacheRegistry}. Does not instantiate {@code NativeRNFBTurboAuth}
34
+ * (React Native / Firebase) — D12.
35
+ */
36
+ public class RNFBAuthCacheRegistryTest {
37
+
38
+ @Test
39
+ public void putGetTake_happyPath() throws Exception {
40
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
41
+ registry.put("k1", "payload");
42
+ assertSame("payload", registry.get("k1"));
43
+ assertSame("payload", registry.take("k1"));
44
+ assertNull(registry.get("k1"));
45
+ }
46
+
47
+ @Test
48
+ public void put_occupiedId_throwsCollision() throws Exception {
49
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
50
+ registry.put("k1", "first");
51
+ try {
52
+ registry.put("k1", "second");
53
+ fail("expected RNFBHandleCollisionException");
54
+ } catch (RNFBHandleCollisionException e) {
55
+ assertTrue(e.getMessage().contains("k1"));
56
+ assertSame("first", registry.get("k1"));
57
+ }
58
+ }
59
+
60
+ @Test
61
+ public void get_whenFree_isNull() {
62
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
63
+ assertNull(registry.get("missing"));
64
+ }
65
+
66
+ @Test
67
+ public void take_whenFree_isNull() {
68
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
69
+ assertNull(registry.take("missing"));
70
+ }
71
+
72
+ @Test
73
+ public void putOrDiscard_storesWhenFree() {
74
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
75
+ assertTrue(registry.putOrDiscard("k1", "payload"));
76
+ assertSame("payload", registry.get("k1"));
77
+ }
78
+
79
+ @Test
80
+ public void putOrDiscard_collision_keepsExisting() throws Exception {
81
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
82
+ registry.put("k1", "first");
83
+ assertFalse(registry.putOrDiscard("k1", "second"));
84
+ assertSame("first", registry.get("k1"));
85
+ }
86
+
87
+ @Test
88
+ public void putReplacing_whenFree_stores() {
89
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
90
+ registry.putReplacing("k1", "payload");
91
+ assertSame("payload", registry.get("k1"));
92
+ }
93
+
94
+ @Test
95
+ public void putReplacing_whenOccupied_replacesLastWins() throws Exception {
96
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
97
+ registry.put("k1", "first");
98
+ registry.putReplacing("k1", "second");
99
+ assertSame("second", registry.get("k1"));
100
+ }
101
+
102
+ @Test
103
+ public void putReplacing_sameIdFromTwoThreads_lastWins() throws Exception {
104
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
105
+ registry.put("k1", "first");
106
+
107
+ CountDownLatch start = new CountDownLatch(1);
108
+ CountDownLatch done = new CountDownLatch(2);
109
+ AtomicReference<String> winner = new AtomicReference<>();
110
+
111
+ Thread t1 =
112
+ new Thread(
113
+ () -> {
114
+ try {
115
+ start.await();
116
+ registry.putReplacing("k1", "second");
117
+ winner.compareAndSet(null, registry.get("k1"));
118
+ } catch (InterruptedException e) {
119
+ Thread.currentThread().interrupt();
120
+ } finally {
121
+ done.countDown();
122
+ }
123
+ });
124
+ Thread t2 =
125
+ new Thread(
126
+ () -> {
127
+ try {
128
+ start.await();
129
+ registry.putReplacing("k1", "third");
130
+ winner.compareAndSet(null, registry.get("k1"));
131
+ } catch (InterruptedException e) {
132
+ Thread.currentThread().interrupt();
133
+ } finally {
134
+ done.countDown();
135
+ }
136
+ });
137
+
138
+ t1.start();
139
+ t2.start();
140
+ start.countDown();
141
+ assertTrue(done.await(5, TimeUnit.SECONDS));
142
+
143
+ String stored = registry.get("k1");
144
+ assertTrue(stored == "second" || stored == "third");
145
+ }
146
+
147
+ @Test
148
+ public void put_afterTake_allowsReuse() throws Exception {
149
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
150
+ registry.put("k1", "first");
151
+ assertSame("first", registry.take("k1"));
152
+ registry.put("k1", "second");
153
+ assertSame("second", registry.get("k1"));
154
+ }
155
+
156
+ @Test
157
+ public void clear_removesAll() throws Exception {
158
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
159
+ registry.put("a", "1");
160
+ registry.put("b", "2");
161
+ registry.clear();
162
+ assertNull(registry.get("a"));
163
+ assertNull(registry.get("b"));
164
+ }
165
+
166
+ @Test
167
+ public void clear_empty_isNoOp() {
168
+ RNFBAuthCacheRegistry<String> registry = new RNFBAuthCacheRegistry<>();
169
+ registry.clear();
170
+ assertNull(registry.get("k1"));
171
+ }
172
+ }