Skip to content

Commit cca33ce

Browse files
committed
feat(phone-number-verification)!: migrate phone-number-verification to TurboModules
1 parent 32c58ba commit cca33ce

29 files changed

Lines changed: 1422 additions & 22 deletions

jest.setup.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,32 @@ jest.doMock('react-native', () => {
457457
startHttpMetric: jest.fn(() => Promise.resolve()),
458458
stopHttpMetric: jest.fn(() => Promise.resolve()),
459459
},
460+
NativeRNFBTurboPnv: {
461+
enableTestSession: jest.fn(() => Promise.resolve()),
462+
getVerificationSupportInfo: jest.fn(() => Promise.resolve([])),
463+
getVerificationSupportInfoForSimSlot: jest.fn(() => Promise.resolve([])),
464+
getVerifiedPhoneNumber: jest.fn(() =>
465+
Promise.resolve({
466+
phoneNumber: '+15555550100',
467+
token: 'jwt',
468+
expirationTimestamp: 0,
469+
issuedAtTimestamp: 0,
470+
nonce: null,
471+
claims: null,
472+
}),
473+
),
474+
getDigitalCredentialPayload: jest.fn(() => Promise.resolve('payload')),
475+
exchangeCredentialResponseForPhoneNumber: jest.fn(() =>
476+
Promise.resolve({
477+
phoneNumber: '+15555550100',
478+
token: 'jwt',
479+
expirationTimestamp: 0,
480+
issuedAtTimestamp: 0,
481+
nonce: null,
482+
claims: null,
483+
}),
484+
),
485+
},
460486
NativeRNFBTurboML: {},
461487
NativeRNFBTurboConfig: {
462488
getConstants: () => ({

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

Lines changed: 6 additions & 5 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** — P4u `auth` committed; Phase 4 Tier A/B complete except opportunistic docs. Next: Phase **5** `phone-number-verification` or Phase **R** prep.
11+
> **IN PROGRESS (2026-07-02):** Phase **5** complete (P5pnv + auth fix committed). All native packages migrated except Phase **R** / **S** / **C**. Next: Phase **R** pre-merge validation or opportunistic docs.
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-auth` (2026-07-02); **harness:** full (implementer applies area narrowing for e2e)
325+
**Label:** `phase-5-pnv` (2026-07-02); **harness:** full (implementer applies area narrowing for e2e)
326326

327-
**Next item:** Phase **5** `phone-number-verification` (Android-only) or opportunistic PDoc
327+
**Next item:** Phase **R** pre-merge validation or opportunistic PDoc
328328

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.
329+
**Current gates:** P5pnv all gates **closed** — committed 2026-07-02. Auth fix `fix(auth): rename delete turbo method for C++ codegen compatibility` committed separately.
330330

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

@@ -358,7 +358,8 @@ 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). |
361+
| Phase 4 `auth` TurboModules | P4u | **closed** | **closed** | **closed** | done | `area-focused` | `feat(auth)!: migrate auth to TurboModules` |**Follow-up (uncommitted):** spec `delete` → C++ keyword collision; rename + regen; `TaskExecutorService(TAG)` fix applied on disk. |
362+
| Phase 5 `phone-number-verification` TurboModules | P5pnv | **closed** | **closed** | **closed** | done | `area-focused` | `feat(phone-number-verification)!: migrate phone-number-verification to TurboModules` | Android-only 6-method spec; review e2e Android 40/2 pending (log `/tmp/rnfb-e2e-android-pnv-review.log`). AD-18 E10 direct resolver. Parity registered. **Bundled auth fix:** separate commit `fix(auth): rename delete turbo method for C++ codegen compatibility`. |
362363
| Phase Docs — NA reqs + consolidation | PDoc | open | open | open | documentation | `none` | `docs: new-architecture requirements + migration consolidation` | Opportunistic; does not gate 4. |
363364
| Phase PD — platform divergence | PPD | open | open | open | documentation | `none` | `docs(<pkg>): …` per package | Opportunistic; gap-analysis first. |
364365
| 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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const MIGRATED_TURBO_PACKAGES = [
2424
'messaging',
2525
'database',
2626
'auth',
27+
'phone-number-verification',
2728
] as const;
2829

2930
export type MigratedTurboPackage = (typeof MIGRATED_TURBO_PACKAGES)[number];
@@ -36,6 +37,40 @@ export type SpecParityContext = {
3637
iosHeaderPath: string;
3738
};
3839

40+
export type SpecParityContextEntry = {
41+
moduleName: string;
42+
specPath: string;
43+
androidSpecPath: string;
44+
iosHeaderPath: string;
45+
};
46+
47+
/** Explicit spec/native paths for packages with non-default codegen layout (see database/messaging guardrail commits). */
48+
export const PACKAGE_SPEC_PARITY_CONTEXTS: Partial<
49+
Record<MigratedTurboPackage, readonly SpecParityContextEntry[]>
50+
> = {
51+
auth: [
52+
{
53+
moduleName: 'NativeRNFBTurboAuth',
54+
specPath: 'packages/auth/specs/NativeRNFBTurboAuth.ts',
55+
androidSpecPath:
56+
'packages/auth/android/src/main/java/io/invertase/firebase/auth/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAuthSpec.java',
57+
iosHeaderPath:
58+
'packages/auth/ios/generated/RNFBAuthTurboModules/RNFBAuthTurboModules.h',
59+
},
60+
],
61+
'phone-number-verification': [
62+
{
63+
moduleName: 'NativeRNFBTurboPnv',
64+
specPath: 'packages/phone-number-verification/specs/NativeRNFBTurboPnv.ts',
65+
androidSpecPath:
66+
'packages/phone-number-verification/android/src/reactnative/java/io/invertase/firebase/pnv/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboPnvSpec.java',
67+
iosHeaderPath:
68+
'packages/phone-number-verification/ios/generated/RNFBPnvTurboModules/RNFBPnvTurboModules.h',
69+
},
70+
],
71+
};
72+
73+
3974
export function extractSpecMethods(specContent: string, fileName = 'spec.ts'): string[] {
4075
const sourceFile = ts.createSourceFile(
4176
fileName,
@@ -166,9 +201,27 @@ function findSingleMatch(rootDir: string, pattern: RegExp, label: string): strin
166201
return matches[0];
167202
}
168203

204+
function resolveParityContext(
205+
packageName: MigratedTurboPackage,
206+
entry: SpecParityContextEntry,
207+
): SpecParityContext {
208+
return {
209+
packageName,
210+
moduleName: entry.moduleName,
211+
specPath: path.join(REPO_ROOT, entry.specPath),
212+
androidSpecPath: path.join(REPO_ROOT, entry.androidSpecPath),
213+
iosHeaderPath: path.join(REPO_ROOT, entry.iosHeaderPath),
214+
};
215+
}
216+
169217
export function discoverPackageSpecContexts(
170218
packageName: MigratedTurboPackage,
171219
): SpecParityContext[] {
220+
const explicitContexts = PACKAGE_SPEC_PARITY_CONTEXTS[packageName];
221+
if (explicitContexts?.length) {
222+
return explicitContexts.map(entry => resolveParityContext(packageName, entry));
223+
}
224+
172225
const packageDir = path.join(REPO_ROOT, 'packages', packageName);
173226
const specsDir = path.join(packageDir, 'specs');
174227
const specFiles = fs
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it, jest } from '@jest/globals';
2+
import { createTurboModuleFixture } from '../../app/__tests__/turboModuleContractHelper';
3+
4+
const SPEC_METHODS = [
5+
'enableTestSession',
6+
'getVerificationSupportInfo',
7+
'getVerificationSupportInfoForSimSlot',
8+
'getVerifiedPhoneNumber',
9+
'getDigitalCredentialPayload',
10+
'exchangeCredentialResponseForPhoneNumber',
11+
] as const;
12+
13+
describe('TurboModule direct resolver contract (NewArch-AD-17.1 / NewArch-AD-18 E10)', function () {
14+
it('resolves every spec method through the direct turbo resolver', async function () {
15+
const mocks = Object.fromEntries(
16+
SPEC_METHODS.map(method => [
17+
method,
18+
jest.fn(() =>
19+
method === 'enableTestSession'
20+
? Promise.resolve()
21+
: method === 'getDigitalCredentialPayload'
22+
? Promise.resolve('payload')
23+
: method === 'getVerificationSupportInfo' ||
24+
method === 'getVerificationSupportInfoForSimSlot'
25+
? Promise.resolve([])
26+
: Promise.resolve({
27+
phoneNumber: '+15555550100',
28+
token: 'jwt',
29+
expirationTimestamp: 0,
30+
issuedAtTimestamp: 0,
31+
nonce: null,
32+
claims: null,
33+
}),
34+
),
35+
]),
36+
) as Record<string, jest.Mock>;
37+
38+
const raw = createTurboModuleFixture(mocks);
39+
40+
jest.resetModules();
41+
const { setReactNativeModule } = await import(
42+
'@react-native-firebase/app/dist/module/internal/nativeModule'
43+
);
44+
setReactNativeModule('NativeRNFBTurboPnv', raw);
45+
const { getVerificationSupportInfo } = await import('../lib/index');
46+
47+
await getVerificationSupportInfo();
48+
49+
expect(mocks.getVerificationSupportInfo).toHaveBeenCalledTimes(1);
50+
for (const method of SPEC_METHODS) {
51+
expect(typeof raw[method]).toBe('function');
52+
}
53+
});
54+
});

packages/phone-number-verification/android/build.gradle

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ android {
8080
sourceSets {
8181
main {
8282
java.srcDirs = ['src/main/java', 'src/reactnative/java']
83+
java.excludes = ['**/generated/jni/**']
8384
}
8485
}
8586
}
@@ -99,3 +100,21 @@ ReactNative.shared.applyPackageVersion()
99100
ReactNative.shared.applyDefaultExcludes()
100101
ReactNative.module.applyAndroidVersions()
101102
ReactNative.module.applyReactNativeDependency('api')
103+
104+
def isNewArchitectureDisabled() {
105+
return project.hasProperty("newArchEnabled") && project.newArchEnabled != "true"
106+
}
107+
108+
if (isNewArchitectureDisabled()) {
109+
def ANSI_RED = "\u001B[31m";
110+
def ANSI_RESET = "\u001B[0m";
111+
112+
println("\n\n\n")
113+
println(ANSI_RED + "**************************************************************************************************************")
114+
println("\n\n\n")
115+
println("New Architecture support is required for @react-native-firebase/phone-number-verification")
116+
println("\n\n\n")
117+
println("**************************************************************************************************************" + ANSI_RESET)
118+
println("\n\n\n")
119+
System.exit(1)
120+
}

packages/phone-number-verification/android/src/main/java/io/invertase/firebase/pnv/ReactNativeFirebasePnvPackage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class ReactNativeFirebasePnvPackage implements ReactPackage {
3030
@Override
3131
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
3232
List<NativeModule> modules = new ArrayList<>();
33-
modules.add(new ReactNativeFirebasePnvModule(reactContext));
33+
modules.add(new NativeRNFBTurboPnv(reactContext));
3434
return modules;
3535
}
3636

packages/phone-number-verification/android/src/main/java/io/invertase/firebase/pnv/ReactNativeFirebasePnvModule.java renamed to packages/phone-number-verification/android/src/reactnative/java/io/invertase/firebase/pnv/NativeRNFBTurboPnv.java

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,44 @@
1717
*
1818
*/
1919

20+
import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithCodeAndMessage;
21+
2022
import android.app.Activity;
23+
import com.facebook.fbreact.specs.NativeRNFBTurboPnvSpec;
2124
import com.facebook.react.bridge.Arguments;
2225
import com.facebook.react.bridge.Promise;
2326
import com.facebook.react.bridge.ReactApplicationContext;
24-
import com.facebook.react.bridge.ReactMethod;
2527
import com.facebook.react.bridge.WritableArray;
2628
import com.facebook.react.bridge.WritableMap;
2729
import com.google.firebase.pnv.FirebasePhoneNumberVerification;
2830
import com.google.firebase.pnv.FirebasePhoneNumberVerificationException;
2931
import com.google.firebase.pnv.VerificationSupportResult;
3032
import com.google.firebase.pnv.VerifiedPhoneNumberTokenResult;
31-
import io.invertase.firebase.common.ReactNativeFirebaseModule;
33+
import io.invertase.firebase.common.TaskExecutorService;
3234
import java.util.List;
3335
import java.util.Map;
36+
import java.util.concurrent.Executor;
3437

35-
public class ReactNativeFirebasePnvModule extends ReactNativeFirebaseModule {
38+
public class NativeRNFBTurboPnv extends NativeRNFBTurboPnvSpec {
3639
private static final String TAG = "Pnv";
3740

3841
private final FirebasePhoneNumberVerification fpnv;
42+
private final TaskExecutorService executorService;
3943

40-
ReactNativeFirebasePnvModule(ReactApplicationContext reactContext) {
41-
super(reactContext, TAG);
44+
public NativeRNFBTurboPnv(ReactApplicationContext reactContext) {
45+
super(reactContext);
4246
fpnv = FirebasePhoneNumberVerification.getInstance();
47+
executorService = new TaskExecutorService(TAG);
48+
}
49+
50+
@Override
51+
public void invalidate() {
52+
super.invalidate();
53+
executorService.shutdown();
54+
}
55+
56+
private Executor getExecutor() {
57+
return executorService.getExecutor();
4358
}
4459

4560
private static String reasonToString(int reason) {
@@ -150,7 +165,7 @@ private static WritableMap claimsToWritableMap(Map<String, Object> claims) {
150165
return map;
151166
}
152167

153-
@ReactMethod
168+
@Override
154169
public void enableTestSession(String token, Promise promise) {
155170
try {
156171
fpnv.enableTestSession(token);
@@ -160,7 +175,7 @@ public void enableTestSession(String token, Promise promise) {
160175
}
161176
}
162177

163-
@ReactMethod
178+
@Override
164179
public void getVerificationSupportInfo(Promise promise) {
165180
fpnv.getVerificationSupportInfo()
166181
.addOnSuccessListener(
@@ -175,9 +190,9 @@ public void getVerificationSupportInfo(Promise promise) {
175190
});
176191
}
177192

178-
@ReactMethod
179-
public void getVerificationSupportInfoForSimSlot(int simSlot, Promise promise) {
180-
fpnv.getVerificationSupportInfo(simSlot)
193+
@Override
194+
public void getVerificationSupportInfoForSimSlot(double simSlot, Promise promise) {
195+
fpnv.getVerificationSupportInfo((int) simSlot)
181196
.addOnSuccessListener(
182197
getExecutor(),
183198
results -> {
@@ -190,7 +205,7 @@ public void getVerificationSupportInfoForSimSlot(int simSlot, Promise promise) {
190205
});
191206
}
192207

193-
@ReactMethod
208+
@Override
194209
public void getVerifiedPhoneNumber(Promise promise) {
195210
Activity activity = getCurrentActivity();
196211
if (activity == null) {
@@ -211,7 +226,7 @@ public void getVerifiedPhoneNumber(Promise promise) {
211226
});
212227
}
213228

214-
@ReactMethod
229+
@Override
215230
public void getDigitalCredentialPayload(String nonce, Promise promise) {
216231
fpnv.getDigitalCredentialPayload(nonce)
217232
.addOnSuccessListener(
@@ -226,7 +241,7 @@ public void getDigitalCredentialPayload(String nonce, Promise promise) {
226241
});
227242
}
228243

229-
@ReactMethod
244+
@Override
230245
public void exchangeCredentialResponseForPhoneNumber(String dcApiResponse, Promise promise) {
231246
fpnv.exchangeCredentialResponseForPhoneNumber(dcApiResponse)
232247
.addOnSuccessListener(

0 commit comments

Comments
 (0)