Skip to content

Commit f03969e

Browse files
committed
refactor(auth): return sync parity for auth parsers
1 parent 4850938 commit f03969e

23 files changed

Lines changed: 340 additions & 127 deletions

File tree

.github/scripts/compare-types/configs/auth.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,6 @@ const config: PackageConfig = {
204204
reason:
205205
'RN Firebase modular UserCredential includes optional enumerable additionalUserInfo (firebase-js-sdk core fields plus preserved native bridge keys). firebase-js-sdk keeps additionalUserInfo off the public UserCredential interface — use getAdditionalUserInfo there.',
206206
},
207-
{
208-
name: 'isSignInWithEmailLink',
209-
reason:
210-
'iOS/Android: Promise<boolean> via native bridge. Other/All: not aligned yet; firebase-js-sdk synchronous boolean is possible on Other/Hermes and Other/Web.',
211-
},
212207
{
213208
name: 'linkWithRedirect',
214209
reason:
@@ -241,8 +236,7 @@ const config: PackageConfig = {
241236
},
242237
{
243238
name: 'TotpSecret',
244-
reason:
245-
'iOS/Android: generateQrCodeUrl is Promise<string> via native bridge; openInOtpApp is an RN-only helper. Other/All: js-sdk synchronous generateQrCodeUrl is possible when MFA is supported on Other.',
239+
reason: 'RN Firebase extension: openInOtpApp is an RN-only helper.',
246240
},
247241
{
248242
name: 'User',

docs/migrating-to-v26.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,11 @@ The namespaced API for `@react-native-firebase/auth` has been **removed**. This
558558
| `auth().useEmulator(url)` | `connectAuthEmulator(getAuth(), url, options?)` |
559559
| `firebase.auth.EmailAuthProvider` | `EmailAuthProvider` (named export) |
560560

561+
## Signature changes
562+
563+
- `isSignInWithEmailLink(auth, emailLink)` now returns `boolean` synchronously, matching firebase-js-sdk.
564+
- `TotpSecret.generateQrCodeUrl(accountName?, issuer?)` now returns `string` synchronously, matching firebase-js-sdk. When `accountName` or `issuer` is omitted, React Native Firebase fills the same defaults from the current user email and Firebase app name.
565+
561566
## Example
562567

563568
```js

jest.setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ jest.doMock('react-native', () => {
204204
finalizeMultiFactorEnrollment: jest.fn(() => Promise.resolve()),
205205
finalizeTotpEnrollment: jest.fn(() => Promise.resolve()),
206206
generateTotpSecret: jest.fn(() => Promise.resolve({ secretKey: 'secret' })),
207-
generateQrCodeUrl: jest.fn(() => Promise.resolve('qr-url')),
207+
generateQrCodeUrl: jest.fn(() => 'qr-url'),
208208
openInOtpApp: jest.fn(),
209209
resolveMultiFactorSignIn: jest.fn(() => Promise.resolve({ user: null })),
210210
confirmationResultConfirm: jest.fn(() => Promise.resolve({ user: null })),

okf-bundle/new-architecture/migration-work-queue.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,9 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
332332

333333
**Label:** `phase-s-sync-conversion` (2026-07-03)
334334

335-
**Next item:** Phase **S** auth parser/TOTP URL sync conversion implementation
335+
**Next item:** Phase **S** perf metric controls sync conversion (`PS-perf-metrics`)
336336

337-
**Current gates:** Phase **S** `app/registerVersion` gates **closed**; next pickup is auth parser/TOTP URL sync conversion.
337+
**Current gates:** PS-auth-parsers all gates **closed** — committing `refactor(auth): return sync parity for auth parsers`.
338338

339339
**Host rule:** one `:test-cover` at a time — never parallel subagents with e2e.
340340

@@ -373,7 +373,7 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
373373
| Phase R — remove `NativeModules` fallback | PR-fallback | **closed** | **closed** | **closed** | done | `full` + RNFBDebug | `refactor: cleanup legacy arch native module fallback` | Proof 683/823/849 (`/tmp/rnfb-e2e-phaseR-proof-*.log`). Review 2026-07-03: no critical/serious. Android cold boot committed separately. |
374374
| Phase S gap-analysis | PS-gap | n/a | n/a | n/a | done | `none` | none | Completed 2026-07-03. First implementation slice: `app/registerVersion`; follow-ups: auth parser/TOTP URL, then perf metric controls. |
375375
| Phase S `app/registerVersion` sync parity | PS-app-registerVersion | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(app): return sync parity for registerVersion` | Implemented 2026-07-03: `registerVersion(): void`, sync throw Jest assertion, removed stale `configs/app.ts` async-vs-sync entry. Green: `lerna:prepare`, `tsc:compile`, `tsc:compile:consumer`, focused app Jest 10/10, `reference:api`, `compare:types`, `lint:js`. Independent review: no findings; no e2e needed for type-only scope. |
376-
| Phase S auth parser/TOTP sync parity | PS-auth-parsers | **open** | **open** | **open** | `implementation` | `unit-focused` | `refactor(auth): return sync parity for auth parsers` | Candidate methods: `isSignInWithEmailLink` and `TotpSecret.generateQrCodeUrl`; gap-analysis classified both as parser/string-only with cached native SDK objects. |
376+
| Phase S auth parser/TOTP sync parity | PS-auth-parsers | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(auth): return sync parity for auth parsers` | Sync `isSignInWithEmailLink` + `TotpSecret.generateQrCodeUrl`; native sync error shape; jest.setup; tests + e2e sync calls; compare-types + v26 doc. Validation: Jest 91/91, compare:types, lint, reference:api, codegen:verify exit 0. Android area e2e 159/15/0 (`/tmp/rnfb-e2e-android-auth-phaseS-final.log`). |
377377

378378
---
379379

packages/auth/__tests__/auth.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ describe('Auth', function () {
141141
expect(isSignInWithEmailLink).toBeDefined();
142142
});
143143

144+
it('`isSignInWithEmailLink` returns synchronously', function () {
145+
const result = isSignInWithEmailLink(getAuth(), 'https://example.com/link');
146+
147+
expect(result).toBe(false);
148+
expect(result).not.toBeInstanceOf(Promise);
149+
});
150+
144151
it('`onAuthStateChanged` function is properly exposed to end user', function () {
145152
expect(onAuthStateChanged).toBeDefined();
146153
});
@@ -242,6 +249,67 @@ describe('Auth', function () {
242249
expect(ActionCodeURL.parseLink('https://example.com/not-an-action-link')).toBeNull();
243250
});
244251

252+
it('`TotpSecret.generateQrCodeUrl` returns synchronously with default account and issuer', function () {
253+
const generateQrCodeUrl = jest.fn(
254+
(_secretKey: string, _account: string, _issuer: string) => 'otpauth://totp/example',
255+
);
256+
const secret = new TotpSecret('secret-key', {
257+
app: { name: '[DEFAULT]' },
258+
currentUser: { email: 'user@example.com' },
259+
native: { generateQrCodeUrl },
260+
} as never);
261+
262+
const result = secret.generateQrCodeUrl();
263+
264+
expect(result).toBe('otpauth://totp/example');
265+
expect(result).not.toBeInstanceOf(Promise);
266+
expect(generateQrCodeUrl).toHaveBeenCalledWith('secret-key', 'user@example.com', '[DEFAULT]');
267+
});
268+
269+
it('`TotpSecret.generateQrCodeUrl` throws auth/invalid-multi-factor-secret for a missing secret', function () {
270+
const generateQrCodeUrl = jest.fn((_secretKey: string, _account: string, _issuer: string) => {
271+
const error = new Error("can't find secret for provided key");
272+
error.name = 'invalid-multi-factor-secret';
273+
throw error;
274+
});
275+
const secret = new TotpSecret('missing-secret-key', {
276+
app: { name: '[DEFAULT]' },
277+
currentUser: { email: 'user@example.com' },
278+
native: { generateQrCodeUrl },
279+
} as never);
280+
281+
expect(() => secret.generateQrCodeUrl()).toThrow(
282+
"[auth/invalid-multi-factor-secret] can't find secret for provided key",
283+
);
284+
expect(generateQrCodeUrl).toHaveBeenCalledWith(
285+
'missing-secret-key',
286+
'user@example.com',
287+
'[DEFAULT]',
288+
);
289+
});
290+
291+
it('`TotpSecret.generateQrCodeUrl` forwards explicit account and issuer synchronously', function () {
292+
const generateQrCodeUrl = jest.fn(
293+
(_secretKey: string, _account: string, _issuer: string) =>
294+
'otpauth://totp/Example%20App:account%40example.com',
295+
);
296+
const secret = new TotpSecret('secret-key', {
297+
app: { name: '[DEFAULT]' },
298+
currentUser: { email: 'user@example.com' },
299+
native: { generateQrCodeUrl },
300+
} as never);
301+
302+
const result = secret.generateQrCodeUrl('account@example.com', 'Example App');
303+
304+
expect(result).toBe('otpauth://totp/Example%20App:account%40example.com');
305+
expect(result).not.toBeInstanceOf(Promise);
306+
expect(generateQrCodeUrl).toHaveBeenCalledWith(
307+
'secret-key',
308+
'account@example.com',
309+
'Example App',
310+
);
311+
});
312+
245313
it('`deleteUser` function is properly exposed to end user', function () {
246314
expect(deleteUser).toBeDefined();
247315
});

packages/auth/__tests__/nativeModuleContract.test.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, jest } from '@jest/globals';
1+
import { describe, expect, it, jest } from '@jest/globals';
22
import { assertTurboContract } from '../../app/__tests__/turboModuleContractHelper';
33

44
const SPEC_METHODS = [
@@ -80,23 +80,32 @@ describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
8080
APP_USER: { '[DEFAULT]': null },
8181
},
8282
createMock: method =>
83-
jest.fn(() =>
84-
method === 'configureAuthDomain' ||
85-
method === 'addAuthStateListener' ||
86-
method === 'removeAuthStateListener' ||
87-
method === 'addIdTokenListener' ||
88-
method === 'removeIdTokenListener' ||
89-
method === 'setLanguageCode' ||
90-
method === 'useDeviceLanguage' ||
91-
method === 'openInOtpApp' ||
92-
method === 'verifyPhoneNumber' ||
93-
method === 'useEmulator' ||
94-
method === 'isSignInWithEmailLink'
95-
? method === 'isSignInWithEmailLink'
96-
? false
97-
: undefined
98-
: Promise.resolve(),
99-
),
83+
jest.fn(() => {
84+
if (method === 'isSignInWithEmailLink') {
85+
return false;
86+
}
87+
88+
if (method === 'generateQrCodeUrl') {
89+
return 'otpauth://totp/example';
90+
}
91+
92+
if (
93+
method === 'configureAuthDomain' ||
94+
method === 'addAuthStateListener' ||
95+
method === 'removeAuthStateListener' ||
96+
method === 'addIdTokenListener' ||
97+
method === 'removeIdTokenListener' ||
98+
method === 'setLanguageCode' ||
99+
method === 'useDeviceLanguage' ||
100+
method === 'openInOtpApp' ||
101+
method === 'verifyPhoneNumber' ||
102+
method === 'useEmulator'
103+
) {
104+
return undefined;
105+
}
106+
107+
return Promise.resolve();
108+
}),
100109
},
101110
{
102111
createUserWithEmailAndPassword: wrapped => {
@@ -160,7 +169,9 @@ describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
160169
void wrapped.generateTotpSecret('session');
161170
},
162171
generateQrCodeUrl: wrapped => {
163-
void wrapped.generateQrCodeUrl('secret', 'account', 'issuer');
172+
const result = wrapped.generateQrCodeUrl('secret', 'account', 'issuer');
173+
expect(result).toBe('otpauth://totp/example');
174+
expect(result).not.toBeInstanceOf(Promise);
164175
},
165176
openInOtpApp: wrapped => {
166177
wrapped.openInOtpApp('secret', 'otpauth://totp/example');
@@ -198,6 +209,11 @@ describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
198209
useEmulator: wrapped => {
199210
wrapped.useEmulator('localhost', 9099);
200211
},
212+
isSignInWithEmailLink: wrapped => {
213+
const result = wrapped.isSignInWithEmailLink('https://example.com/link');
214+
expect(result).toBe(false);
215+
expect(result).not.toBeInstanceOf(Promise);
216+
},
201217
forceRecaptchaFlowForTesting: wrapped => {
202218
void wrapped.forceRecaptchaFlowForTesting(true);
203219
},

packages/auth/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -427,14 +427,13 @@ public void createUserWithEmailAndPassword(
427427
* isSignInWithEmailLink
428428
*
429429
* @param email
430-
* @param promise
431430
*/
432431
@Override
433-
public void isSignInWithEmailLink(String appName, String emailLink, final Promise promise) {
432+
public boolean isSignInWithEmailLink(String appName, String emailLink) {
434433
Log.d(TAG, "isSignInWithEmailLink");
435434
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
436435
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
437-
promise.resolve(firebaseAuth.isSignInWithEmailLink(emailLink));
436+
return firebaseAuth.isSignInWithEmailLink(emailLink);
438437
}
439438

440439
/**
@@ -1322,20 +1321,15 @@ public void finalizeMultiFactorEnrollment(
13221321
}
13231322

13241323
@Override
1325-
public void generateQrCodeUrl(
1326-
final String appName,
1327-
final String secretKey,
1328-
final String account,
1329-
final String issuer,
1330-
final Promise promise) {
1324+
public String generateQrCodeUrl(
1325+
final String appName, final String secretKey, final String account, final String issuer) {
13311326

13321327
TotpSecret secret = mTotpSecrets.get(secretKey);
13331328
if (secret == null) {
1334-
rejectPromiseWithCodeAndMessage(
1335-
promise, "invalid-multi-factor-secret", "can't find secret for provided key");
1336-
return;
1329+
throw new ReactNativeFirebaseAuthSyncException(
1330+
"invalid-multi-factor-secret", "can't find secret for provided key");
13371331
}
1338-
promise.resolve(secret.generateQrCodeUrl(account, issuer));
1332+
return secret.generateQrCodeUrl(account, issuer);
13391333
}
13401334

13411335
@Override
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
* Structured sync TurboModule error carrying the same RNFB {@code code}/{@code message} pair used by
22+
* {@code rejectPromiseWithCodeAndMessage}.
23+
*/
24+
public class ReactNativeFirebaseAuthSyncException extends RuntimeException {
25+
private final String code;
26+
27+
public ReactNativeFirebaseAuthSyncException(String code, String message) {
28+
super(message);
29+
this.code = code;
30+
}
31+
32+
public String getCode() {
33+
return code;
34+
}
35+
}

packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAuthSpec.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ public NativeRNFBTurboAuthSpec(ReactApplicationContext reactContext) {
118118
@DoNotStrip
119119
public abstract void createUserWithEmailAndPassword(String appName, String email, String password, Promise promise);
120120

121-
@ReactMethod
121+
@ReactMethod(isBlockingSynchronousMethod = true)
122122
@DoNotStrip
123-
public abstract void isSignInWithEmailLink(String appName, String emailLink, Promise promise);
123+
public abstract boolean isSignInWithEmailLink(String appName, String emailLink);
124124

125125
@ReactMethod
126126
@DoNotStrip
@@ -218,9 +218,9 @@ public NativeRNFBTurboAuthSpec(ReactApplicationContext reactContext) {
218218
@DoNotStrip
219219
public abstract void generateTotpSecret(String appName, String sessionKey, Promise promise);
220220

221-
@ReactMethod
221+
@ReactMethod(isBlockingSynchronousMethod = true)
222222
@DoNotStrip
223-
public abstract void generateQrCodeUrl(String appName, String secretKey, String account, String issuer, Promise promise);
223+
public abstract String generateQrCodeUrl(String appName, String secretKey, String account, String issuer);
224224

225225
@ReactMethod
226226
@DoNotStrip

packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/jni/RNFBAuthTurboModules-generated.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_createUser
8484

8585
static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_isSignInWithEmailLink(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
8686
static jmethodID cachedMethodId = nullptr;
87-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "isSignInWithEmailLink", "(Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
87+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, BooleanKind, "isSignInWithEmailLink", "(Ljava/lang/String;Ljava/lang/String;)Z", args, count, cachedMethodId);
8888
}
8989

9090
static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_signInWithEmailAndPassword(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
@@ -209,7 +209,7 @@ static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_generateTo
209209

210210
static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_generateQrCodeUrl(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
211211
static jmethodID cachedMethodId = nullptr;
212-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, PromiseKind, "generateQrCodeUrl", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
212+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, StringKind, "generateQrCodeUrl", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", args, count, cachedMethodId);
213213
}
214214

215215
static facebook::jsi::Value __hostFunction_NativeRNFBTurboAuthSpecJSI_openInOtpApp(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {

0 commit comments

Comments
 (0)