Skip to content

Commit 32c58ba

Browse files
committed
feat(auth)!: migrate auth to TurboModules
1 parent 626dd0c commit 32c58ba

36 files changed

Lines changed: 5052 additions & 404 deletions

jest.setup.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,15 @@ jest.doMock('react-native', () => {
150150
eventsNotifyReady: jest.fn(),
151151
removeListeners: jest.fn(),
152152
},
153-
RNFBAuthModule: {
153+
NativeRNFBTurboAuth: {
154+
getConstants: () => ({
155+
APP_LANGUAGE: {
156+
'[DEFAULT]': 'en-US',
157+
},
158+
APP_USER: {
159+
'[DEFAULT]': null,
160+
},
161+
}),
154162
APP_LANGUAGE: {
155163
'[DEFAULT]': 'en-US',
156164
},
@@ -186,8 +194,7 @@ jest.doMock('react-native', () => {
186194
useEmulator: jest.fn(),
187195
getCustomAuthDomain: jest.fn(() => Promise.resolve(null)),
188196
configureAuthDomain: jest.fn(() => Promise.resolve()),
189-
// User methods
190-
deleteObject: jest.fn(() => Promise.resolve()),
197+
delete: jest.fn(() => Promise.resolve()),
191198
getIdToken: jest.fn(() => Promise.resolve('mock-token')),
192199
getIdTokenResult: jest.fn(() => Promise.resolve({ token: 'mock-token' })),
193200
linkWithCredential: jest.fn(() => Promise.resolve({ user: null })),
@@ -202,6 +209,22 @@ jest.doMock('react-native', () => {
202209
updatePhoneNumber: jest.fn(() => Promise.resolve(null)),
203210
updateProfile: jest.fn(() => Promise.resolve(null)),
204211
verifyBeforeUpdateEmail: jest.fn(() => Promise.resolve(null)),
212+
forceRecaptchaFlowForTesting: jest.fn(() => Promise.resolve()),
213+
setAppVerificationDisabledForTesting: jest.fn(() => Promise.resolve()),
214+
setAutoRetrievedSmsCodeForPhoneNumber: jest.fn(() => Promise.resolve()),
215+
removeAuthStateListener: jest.fn(),
216+
removeIdTokenListener: jest.fn(),
217+
getSession: jest.fn(() => Promise.resolve('session')),
218+
unenrollMultiFactor: jest.fn(() => Promise.resolve()),
219+
finalizeMultiFactorEnrollment: jest.fn(() => Promise.resolve()),
220+
finalizeTotpEnrollment: jest.fn(() => Promise.resolve()),
221+
generateTotpSecret: jest.fn(() => Promise.resolve({ secretKey: 'secret' })),
222+
generateQrCodeUrl: jest.fn(() => Promise.resolve('qr-url')),
223+
openInOtpApp: jest.fn(),
224+
resolveMultiFactorSignIn: jest.fn(() => Promise.resolve({ user: null })),
225+
confirmationResultConfirm: jest.fn(() => Promise.resolve({ user: null })),
226+
verifyPhoneNumber: jest.fn(),
227+
useDeviceLanguage: jest.fn(),
205228
},
206229
NativeRNFBTurboAppCheck: {
207230
activate: jest.fn(),

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ timestamp: 2026-06-26T00:00:00Z
88

99
# TurboModule migration — work queue
1010

11-
> **IN PROGRESS (2026-07-02):** Phase **4** remaining complex — P4d `database` committed; next pickup from phase table (Tier A/B packages not yet queued).
11+
> **IN PROGRESS (2026-07-02):** Phase **4** — P4u `auth` committed; Phase 4 Tier A/B complete except opportunistic docs. Next: Phase **5** `phone-number-verification` or Phase **R** prep.
1212
> **Goal/order:** app foundation → hard probe → easy wins → remaining complex → sync conversion → coordinated break → cleanup (events, shared-state encapsulation). Decisions: [architecture-decisions.md](architecture-decisions.md). Links: [implementation workflow](turbomodule-implementation-workflow.md), [change authoring](../testing/change-authoring-workflow.md), [functions reference](../../../packages/functions/) ([PR #8603](https://github.com/invertase/react-native-firebase/pull/8603)).
1313
1414
Ephemeral tracker; see [OKF policy](../documentation-policy.md).
@@ -322,11 +322,11 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
322322

323323
## Current snapshot
324324

325-
**Label:** `phase-4-database` (2026-07-02); **harness:** area-focused (`database`)
325+
**Label:** `phase-4-auth` (2026-07-02); **harness:** full (implementer applies area narrowing for e2e)
326326

327-
**Next item:** Phase **4** remaining complex (pick next Tier A/B package — no arbiter row yet)
327+
**Next item:** Phase **5** `phone-number-verification` (Android-only) or opportunistic PDoc
328328

329-
**Current gates:** P4d all gates **closed** — committed `feat(database)!: migrate database to TurboModules` 2026-07-02. Delta review: macOS 213 / iOS 219 / Android 220 passing.
329+
**Current gates:** P4u all gates **closed** — committed `feat(auth)!: migrate auth to TurboModules` 2026-07-02. Review: macOS 171 / iOS 185 / Android 192 passing.
330330

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

@@ -358,6 +358,7 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
358358
| Phase 4a messaging event decision | P4a | n/a | n/a | n/a | done | `none` | none | **Defer** events to Phase C — [NewArch-AD-4 § messaging](architecture-decisions.md#messaging--defer-eventemitter-cutover-migrate-shell-only-in-phase-4). |
359359
| Phase 4 `messaging` TurboModules | P4m | **closed** | **closed** | **closed** | done | `area-focused` | `feat(messaging)!: migrate messaging to TurboModules` | Turbo shell only (AD-4 event path preserved). Jest 38/38 + parity 32/32; codegen:verify incl messaging; legacy Java removed. iOS/Android area e2e green on method-call + listener-registration scope. Foreground `onMessage` delivery test **left `xit`** — flaky FCM harness; re-enable in [Phase C](#deferred-cleanup-phase-eventemitter). |
360360
| Phase 4 `database` TurboModules | P4d | **closed** | **closed** | **closed** | done | `area-focused` | `feat(database)!: migrate database to TurboModules` | 5 Turbo specs/shells + committed codegen; Jest 49 + parity 38; area e2e macOS 213 / iOS 219 / Android 220 (delta logs `/tmp/rnfb-e2e-*-database-delta-review.log`). Remediation: invalidate/teardown parity (Reference+Transaction), onComplete guard, contract test 22 methods, jest mock cleanup. Orchestration: macOS/Android `:8090` pre-flight in `firebase.test.js` + OKF. **Deferred minor:** void spec vs Promise JS API; silent Query `RejectedExecutionException` post-invalidate. |
361+
| Phase 4 `auth` TurboModules | P4u | **closed** | **closed** | **closed** | done | `area-focused` | `feat(auth)!: migrate auth to TurboModules` | 60-method spec + shells; Jest 135 + parity 40; review e2e macOS 171 / iOS 185 / Android 192 (logs `/tmp/rnfb-e2e-*-auth-review.log`). AD-4 events preserved; AD-10 authDomains unchanged. **Deferred minor:** iOS no-op stubs for Android-only testing APIs undocumented in spec. **Deferred nit:** phone-auth invalidate gap (legacy parity). |
361362
| Phase Docs — NA reqs + consolidation | PDoc | open | open | open | documentation | `none` | `docs: new-architecture requirements + migration consolidation` | Opportunistic; does not gate 4. |
362363
| Phase PD — platform divergence | PPD | open | open | open | documentation | `none` | `docs(<pkg>): …` per package | Opportunistic; gap-analysis first. |
363364
| Phase R — remove `NativeModules` fallback | PR-fallback | open | open | open | pre-merge-validation | `full` | (Phase R) | Decision B. Jest + `app` e2e "unknown module throws". |

packages/app/__tests__/specNativeParityHelper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const MIGRATED_TURBO_PACKAGES = [
2323
'functions',
2424
'messaging',
2525
'database',
26+
'auth',
2627
] as const;
2728

2829
export type MigratedTurboPackage = (typeof MIGRATED_TURBO_PACKAGES)[number];

packages/auth/RNFBAuth.podspec

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,23 @@ Pod::Spec.new do |s|
2727
s.ios.deployment_target = firebase_ios_target
2828
s.macos.deployment_target = firebase_macos_target
2929
s.tvos.deployment_target = firebase_tvos_target
30-
s.source_files = 'ios/**/*.{h,m}'
30+
s.source_files = 'ios/**/*.{h,m,mm,cpp}'
31+
s.private_header_files = [
32+
'ios/RNFBAuth/RNFBAuthModule.h',
33+
'ios/generated/**/*.h',
34+
]
35+
s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
36+
37+
s.pod_target_xcconfig = {
38+
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBAuthTurboModules\" \"$(PODS_TARGET_SRCROOT)/ios/generated\"",
39+
}
3140

3241
s.dependency 'RNFBApp'
3342

34-
# React Native dependencies
35-
if defined?(install_modules_dependencies()) != nil
36-
install_modules_dependencies(s);
37-
else
38-
s.dependency "React-Core"
43+
install_modules_dependencies(s);
44+
45+
if defined?(ENV["RCT_NEW_ARCH_ENABLED"]) != nil && (ENV["RCT_NEW_ARCH_ENABLED"] == '0')
46+
raise "#{s.name} requires New Architecture. Enable New Architecture to use this module"
3947
end
4048

4149
if defined?($FirebaseSDKVersion)
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { describe, it, jest } from '@jest/globals';
2+
import { assertTurboContract } from '../../app/__tests__/turboModuleContractHelper';
3+
4+
const SPEC_METHODS = [
5+
'configureAuthDomain',
6+
'getCustomAuthDomain',
7+
'addAuthStateListener',
8+
'removeAuthStateListener',
9+
'addIdTokenListener',
10+
'removeIdTokenListener',
11+
'forceRecaptchaFlowForTesting',
12+
'setAutoRetrievedSmsCodeForPhoneNumber',
13+
'setAppVerificationDisabledForTesting',
14+
'useUserAccessGroup',
15+
'signOut',
16+
'signInAnonymously',
17+
'createUserWithEmailAndPassword',
18+
'isSignInWithEmailLink',
19+
'signInWithEmailAndPassword',
20+
'signInWithEmailLink',
21+
'signInWithCustomToken',
22+
'revokeToken',
23+
'sendPasswordResetEmail',
24+
'sendSignInLinkToEmail',
25+
'deleteUser',
26+
'reload',
27+
'sendEmailVerification',
28+
'verifyBeforeUpdateEmail',
29+
'updateEmail',
30+
'updatePassword',
31+
'updatePhoneNumber',
32+
'updateProfile',
33+
'getIdToken',
34+
'getIdTokenResult',
35+
'signInWithCredential',
36+
'signInWithProvider',
37+
'signInWithPhoneNumber',
38+
'verifyPhoneNumberWithMultiFactorInfo',
39+
'verifyPhoneNumberForMultiFactor',
40+
'resolveMultiFactorSignIn',
41+
'resolveTotpSignIn',
42+
'generateTotpSecret',
43+
'generateQrCodeUrl',
44+
'openInOtpApp',
45+
'getSession',
46+
'unenrollMultiFactor',
47+
'finalizeMultiFactorEnrollment',
48+
'finalizeTotpEnrollment',
49+
'confirmationResultConfirm',
50+
'verifyPhoneNumber',
51+
'confirmPasswordReset',
52+
'applyActionCode',
53+
'checkActionCode',
54+
'linkWithCredential',
55+
'linkWithProvider',
56+
'unlink',
57+
'reauthenticateWithCredential',
58+
'reauthenticateWithProvider',
59+
'fetchSignInMethodsForEmail',
60+
'setLanguageCode',
61+
'setTenantId',
62+
'useDeviceLanguage',
63+
'verifyPasswordResetCode',
64+
'useEmulator',
65+
] as const;
66+
67+
describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
68+
it('exposes every spec method callable through the real wrapper', function () {
69+
assertTurboContract(
70+
{
71+
namespace: 'auth',
72+
nativeModuleName: 'NativeRNFBTurboAuth',
73+
nativeEvents: ['auth_state_changed', 'auth_id_token_changed', 'phone_auth_state_changed'],
74+
hasMultiAppSupport: true,
75+
hasCustomUrlOrRegionSupport: false,
76+
turboModule: true,
77+
specMethods: SPEC_METHODS,
78+
constants: {
79+
APP_LANGUAGE: { '[DEFAULT]': 'en-US' },
80+
APP_USER: { '[DEFAULT]': null },
81+
},
82+
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+
),
100+
},
101+
{
102+
createUserWithEmailAndPassword: wrapped => {
103+
void wrapped.createUserWithEmailAndPassword('a@b.com', 'password');
104+
},
105+
signInWithEmailAndPassword: wrapped => {
106+
void wrapped.signInWithEmailAndPassword('a@b.com', 'password');
107+
},
108+
signInWithEmailLink: wrapped => {
109+
void wrapped.signInWithEmailLink('a@b.com', 'https://example.com/link');
110+
},
111+
signInWithCustomToken: wrapped => {
112+
void wrapped.signInWithCustomToken('token');
113+
},
114+
signInWithCredential: wrapped => {
115+
void wrapped.signInWithCredential('google.com', 'token', 'secret');
116+
},
117+
signInWithProvider: wrapped => {
118+
void wrapped.signInWithProvider({ providerId: 'google.com' });
119+
},
120+
signInWithPhoneNumber: wrapped => {
121+
void wrapped.signInWithPhoneNumber('+15555550100', true);
122+
},
123+
sendPasswordResetEmail: wrapped => {
124+
void wrapped.sendPasswordResetEmail('a@b.com', null);
125+
},
126+
sendSignInLinkToEmail: wrapped => {
127+
void wrapped.sendSignInLinkToEmail('a@b.com', { url: 'https://example.com' });
128+
},
129+
sendEmailVerification: wrapped => {
130+
void wrapped.sendEmailVerification(null);
131+
},
132+
verifyBeforeUpdateEmail: wrapped => {
133+
void wrapped.verifyBeforeUpdateEmail('new@b.com', null);
134+
},
135+
updatePhoneNumber: wrapped => {
136+
void wrapped.updatePhoneNumber('phone', 'token', 'secret');
137+
},
138+
updateProfile: wrapped => {
139+
void wrapped.updateProfile({ displayName: 'Test' });
140+
},
141+
getIdToken: wrapped => {
142+
void wrapped.getIdToken(true);
143+
},
144+
getIdTokenResult: wrapped => {
145+
void wrapped.getIdTokenResult(true);
146+
},
147+
verifyPhoneNumberWithMultiFactorInfo: wrapped => {
148+
void wrapped.verifyPhoneNumberWithMultiFactorInfo('uid', 'session');
149+
},
150+
verifyPhoneNumberForMultiFactor: wrapped => {
151+
void wrapped.verifyPhoneNumberForMultiFactor('+15555550100', 'session');
152+
},
153+
resolveMultiFactorSignIn: wrapped => {
154+
void wrapped.resolveMultiFactorSignIn('session', 'verification-id', '123456');
155+
},
156+
resolveTotpSignIn: wrapped => {
157+
void wrapped.resolveTotpSignIn('session', 'uid', '123456');
158+
},
159+
generateTotpSecret: wrapped => {
160+
void wrapped.generateTotpSecret('session');
161+
},
162+
generateQrCodeUrl: wrapped => {
163+
void wrapped.generateQrCodeUrl('secret', 'account', 'issuer');
164+
},
165+
openInOtpApp: wrapped => {
166+
wrapped.openInOtpApp('secret', 'otpauth://totp/example');
167+
},
168+
finalizeMultiFactorEnrollment: wrapped => {
169+
void wrapped.finalizeMultiFactorEnrollment('verification-id', '123456', 'Device');
170+
},
171+
finalizeTotpEnrollment: wrapped => {
172+
void wrapped.finalizeTotpEnrollment('secret', '123456', 'Device');
173+
},
174+
confirmationResultConfirm: wrapped => {
175+
void wrapped.confirmationResultConfirm('123456');
176+
},
177+
verifyPhoneNumber: wrapped => {
178+
wrapped.verifyPhoneNumber('+15555550100', 'request-key', 60, true);
179+
},
180+
confirmPasswordReset: wrapped => {
181+
void wrapped.confirmPasswordReset('code', 'new-password');
182+
},
183+
linkWithCredential: wrapped => {
184+
void wrapped.linkWithCredential('google.com', 'token', 'secret');
185+
},
186+
linkWithProvider: wrapped => {
187+
void wrapped.linkWithProvider({ providerId: 'google.com' });
188+
},
189+
reauthenticateWithCredential: wrapped => {
190+
void wrapped.reauthenticateWithCredential('google.com', 'token', 'secret');
191+
},
192+
reauthenticateWithProvider: wrapped => {
193+
void wrapped.reauthenticateWithProvider({ providerId: 'google.com' });
194+
},
195+
setTenantId: wrapped => {
196+
void wrapped.setTenantId('tenant-id');
197+
},
198+
useEmulator: wrapped => {
199+
wrapped.useEmulator('localhost', 9099);
200+
},
201+
forceRecaptchaFlowForTesting: wrapped => {
202+
void wrapped.forceRecaptchaFlowForTesting(true);
203+
},
204+
setAutoRetrievedSmsCodeForPhoneNumber: wrapped => {
205+
void wrapped.setAutoRetrievedSmsCodeForPhoneNumber('+15555550100', '123456');
206+
},
207+
setAppVerificationDisabledForTesting: wrapped => {
208+
void wrapped.setAppVerificationDisabledForTesting(true);
209+
},
210+
useUserAccessGroup: wrapped => {
211+
void wrapped.useUserAccessGroup('group.example');
212+
},
213+
},
214+
);
215+
});
216+
});

packages/auth/android/build.gradle

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ android {
7575
targetCompatibility = JavaVersion.VERSION_11
7676
}
7777
}
78+
sourceSets {
79+
main {
80+
java.excludes = ['**/generated/jni/**']
81+
}
82+
}
7883
}
7984

8085
repositories {
@@ -92,3 +97,21 @@ ReactNative.shared.applyPackageVersion()
9297
ReactNative.shared.applyDefaultExcludes()
9398
ReactNative.module.applyAndroidVersions()
9499
ReactNative.module.applyReactNativeDependency("api")
100+
101+
def isNewArchitectureDisabled() {
102+
return project.hasProperty("newArchEnabled") && project.newArchEnabled != "true"
103+
}
104+
105+
if (isNewArchitectureDisabled()) {
106+
def ANSI_RED = "\u001B[31m";
107+
def ANSI_RESET = "\u001B[0m";
108+
109+
println("\n\n\n")
110+
println(ANSI_RED + "**************************************************************************************************************")
111+
println("\n\n\n")
112+
println("New Architecture support is required for @react-native-firebase/auth")
113+
println("\n\n\n")
114+
println("**************************************************************************************************************" + ANSI_RESET)
115+
println("\n\n\n")
116+
System.exit(1)
117+
}

0 commit comments

Comments
 (0)