Skip to content

Commit eef34e7

Browse files
committed
feat(firestore)!: migrate firestore to TurboModules
BREAKING CHANGE: Firestore native bridge requires New Architecture. Legacy NativeModules bridge removed; four Codegen TurboModule specs (NativeRNFBTurboFirestore{,Collection,Document,Transaction}) with committed generated artifacts, Android/iOS turbo shells, and JS wiring.
1 parent 32918c8 commit eef34e7

42 files changed

Lines changed: 4327 additions & 527 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

jest.setup.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ jest.doMock('react-native', () => {
284284
),
285285
getServerTime: jest.fn((_appName: any, _customUrl: any) => Promise.resolve(Date.now())),
286286
},
287-
RNFBFirestoreModule: {
287+
NativeRNFBTurboFirestore: {
288+
setLogLevel: jest.fn(),
288289
loadBundle: jest.fn(() =>
289290
Promise.resolve({
290291
taskState: 'Success',
@@ -303,9 +304,20 @@ jest.doMock('react-native', () => {
303304
settings: jest.fn(),
304305
addSnapshotsInSync: jest.fn(),
305306
removeSnapshotsInSync: jest.fn(),
307+
persistenceCacheIndexManager: jest.fn(),
308+
},
309+
NativeRNFBTurboFirestoreCollection: {
306310
collectionOffSnapshot: jest.fn(),
307311
namedQueryOnSnapshot: jest.fn(),
308312
collectionOnSnapshot: jest.fn(),
313+
namedQueryGet: jest.fn(() =>
314+
Promise.resolve({
315+
source: 'cache',
316+
changes: [],
317+
documents: [],
318+
metadata: {},
319+
}),
320+
),
309321
collectionGet: jest.fn(() =>
310322
Promise.resolve({
311323
source: 'cache',
@@ -315,6 +327,15 @@ jest.doMock('react-native', () => {
315327
}),
316328
),
317329
collectionCount: jest.fn(() => Promise.resolve({ count: 0 })),
330+
aggregateQuery: jest.fn(() => Promise.resolve({})),
331+
pipelineExecute: jest.fn(() =>
332+
Promise.resolve({
333+
results: [],
334+
executionTime: Date.now(),
335+
}),
336+
),
337+
},
338+
NativeRNFBTurboFirestoreDocument: {
318339
documentDelete: jest.fn(() => Promise.resolve()),
319340
documentOffSnapshot: jest.fn(),
320341
documentOnSnapshot: jest.fn(),
@@ -328,11 +349,20 @@ jest.doMock('react-native', () => {
328349
),
329350
documentSet: jest.fn(() => Promise.resolve()),
330351
documentUpdate: jest.fn(() => Promise.resolve()),
331-
persistenceCacheIndexManager: jest.fn(),
332352
documentBatch: jest.fn(),
353+
},
354+
NativeRNFBTurboFirestoreTransaction: {
333355
transactionApplyBuffer: jest.fn(),
334356
transactionBegin: jest.fn(),
335357
transactionDispose: jest.fn(),
358+
transactionGetDocument: jest.fn(() =>
359+
Promise.resolve({
360+
data: {},
361+
metadata: {},
362+
path: 'firestore/document',
363+
exists: true,
364+
}),
365+
),
336366
},
337367
RNFBFiamModule: {
338368
isMessagesDisplaySuppressed: false,

okf-bundle/new-architecture/architecture-decisions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Multiple specs/modules in one package are **merged flat** into a single resolved
132132

133133
## NewArch-AD-12 — One commit per package — **Accepted**
134134

135-
Convert a whole package (all its legacy modules → all its specs) in one `implementation → independent-review → commit` loop; land it as one `feat(<pkg>): migrate <pkg> to TurboModules`. Multiple specs ≠ multiple commits — they share `codegenConfig`, generated artifacts, podspec/gradle guards, and JS wiring that only build and pass e2e together.
135+
Convert a whole package (all its legacy modules → all its specs) in one `implementation → independent-review → commit` loop; land it as one `feat(<pkg>)!: migrate <pkg> to TurboModules` (breaking: New Architecture required). Multiple specs ≠ multiple commits — they share `codegenConfig`, generated artifacts, podspec/gradle guards, and JS wiring that only build and pass e2e together.
136136

137137
---
138138

@@ -263,7 +263,7 @@ Two resolution surfaces exist and must be used deliberately:
263263
| E5 | [`UtilsStatics.ts`](../../../packages/app/lib/utils/UtilsStatics.ts) `FilePath` getter | `NativeRNFBTurboUtils` | **Phase 0 fix** | Reads path **constants** synchronously from the utils host without a `FirebaseModule` instance (static getter on `Utils.Statics`). Raw was acceptable pre-turbo; under [NewArch-AD-15](#newarch-ad-15--constant-memoization-scope-static-only--accepted) migrate to a dedicated wrapped utils accessor (or memoized static read via resolver) so constants are not rebuilt per access. Turbo name is already correct. | Fix in Phase 0 re-implementation. |
264264
| E6 | [`messaging/lib/index.ts`](../../../packages/messaging/lib/index.ts) `isSupported()` | ~~`RNFBUtilsModule`~~`NativeRNFBTurboUtils` | **Phase 0 fix (bug)** | Cross-package read of Play Services availability. Was using **legacy module name** (returns `undefined` under turbo-only) and a **dynamic constant** (`androidPlayServices`) that can go stale. Must use turbo name + **dynamic method** `androidGetPlayServicesStatus()` per [NewArch-AD-15](#newarch-ad-15--constant-memoization-scope-static-only--accepted). | **Must-fix** in Phase 0. |
265265
| E7 | [`app/lib/modular.ts`](../../../packages/app/lib/modular.ts) `metaGetAll`, `jsonGetAll`, `preferences*` | `NativeRNFBTurboApp` | **Not an exception — migrate** | No policy reason for raw; these are app-module method calls with no arg-prepend skip. Should use **`getAppModule()`** (wrapped) for error mapping consistency. Listed here so gap-analysis catches them. | Migrate to `getAppModule()` in Phase 0. |
266-
| E8 | [`FirestoreStatics.ts`](../../../packages/firestore/lib/FirestoreStatics.ts) `setLogLevel` | `RNFBFirestoreModule` | **Deferred — Phase 1** | Cross-package static helper bypasses `FirebaseModule`/`getNativeModule`. Acceptable until firestore migrates; then switch to turbo name + wrapped surface (or firestore-owned static that uses `getNativeModule`). | Fix when `firestore` migrates. |
266+
| E8 | [`FirestoreStatics.ts`](../../../packages/firestore/lib/FirestoreStatics.ts) `setLogLevel` | `NativeRNFBTurboFirestore` | **Phase 1 fix** | Cross-package static helper bypasses `FirebaseModule`/`getNativeModule`. Uses turbo main host via [`getStaticFirestoreMainModule()`](../../../packages/firestore/lib/internal/staticNativeModule.ts) (NewArch-AD-18 E8). Raw access retained — no wrapped surface for static helpers. | Done — firestore Phase 1. |
267267
| E9 | [`DatabaseSyncTree.ts`](../../../packages/database/lib/DatabaseSyncTree.ts) `native` getter | `RNFBDatabaseQueryModule` | **Deferred — Phase 4** | Internal sync listener tree calls query module directly for low-latency sync ops, bypassing the merged multi-module surface. Acceptable until database migrates; then turbo name + evaluate whether wrapped merge surface suffices. | Fix when `database` migrates. |
268268
| E10 | [`phone-number-verification/lib/index.ts`](../../../packages/phone-number-verification/lib/index.ts) `getNativeModule()` | `RNFBPnvModule` | **Deferred — Phase 5** | Package bypasses `createModuleNamespace` by design ([workflow § gotchas](turbomodule-implementation-workflow.md#gotchas)). Direct resolver is intentional; update to `NativeRNFBTurboPnv` on migration. | Fix when `phone-number-verification` migrates. |
269269

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

Lines changed: 10 additions & 7 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-06-30):** Phase **0** (`app` TurboModules)**done**. Phase **0.1** (`app` compare:types) — **commit** pending. Decisions: [architecture-decisions.md](architecture-decisions.md).
11+
> **IN PROGRESS (2026-06-30):** Phase **2** easy wins**queued**. Phases **0** / **0.1** (`app`) and Phase **1** (`firestore`) — **done**. Decisions: [architecture-decisions.md](architecture-decisions.md).
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).
@@ -161,8 +161,8 @@ Pick **one** of `firestore` or `auth` in Phase 1 (firestore = multi-module + pip
161161
| Phase | Focus | Status | Packages |
162162
|-------|--------|--------|----------|
163163
| **0** | App foundation + unified resolver | **done** | `app` |
164-
| **0.1** | App modular type parity (`compare:types`) | **commit pending** | `app`[§ Phase 0.1](#phase-01-app-comparetypes) |
165-
| **1** | Hard probe | queued | `firestore` **or** `auth` — pick one |
164+
| **0.1** | App modular type parity (`compare:types`) | **done** | `app`[§ Phase 0.1](#phase-01-app-comparetypes) |
165+
| **1** | Hard probe | **done** | `firestore` (multi-module + pipelines; NewArch-AD-14a composite) |
166166
| **2** | Easy wins | queued | `installations`, `perf`, `in-app-messaging`, `app-distribution`, `ml` |
167167
| **3** | Moderate | queued | `app-check`, `remote-config`, `analytics`, `crashlytics`, `storage` |
168168
| **4** | Remaining complex | queued | other Tier A/B + `messaging`, `database` |
@@ -267,11 +267,13 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
267267

268268
## Current snapshot
269269

270-
**Label:** `phase-0.1-compare-types`; **harness:** n/a (types-only)
270+
**Label:** `phase-2-easy-wins`; **harness:** n/a
271271

272-
**Next item:** Phase **0.1** `app` compare:types**commit**
272+
**Next item:** Phase **2** — pick first Tier D package (`installations`, `perf`, `in-app-messaging`, `app-distribution`, or `ml`)
273273

274-
**Current gates:** Phase 0 all **closed** · Phase 0.1 `review_gate` **closed** · `commit_gate` **open**
274+
**Current gates:** Phase 0 / 0.1 / 1 all **closed**
275+
276+
**Package pick (Phase 1):** `firestore` over `auth` — first multi-module package (×4 specs), exercises `pipelineExecute` + NewArch-AD-14a routing composite Proxy; defers largest single-spec (`auth` ×59) to Phase 4.
275277

276278
**Arbiter gate:**
277279

@@ -280,7 +282,8 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
280282
|------|------|----------------------|---------------|---------------|------------------|-------------------|------------------|-------|
281283
| Design review | DR | n/a | n/a | n/a | done | none | none | ✅ Adversarial review complete. |
282284
| Phase 0 `app` TurboModules | P0 | **closed** | **closed** | **closed** | done | `full` | `feat(app): migrate app modules to TurboModules incl general migration infra` | Committed 2026-06-30. |
283-
| Phase 0.1 `app` compare:types | P0.1 | **closed** | **closed** | **open** | `commit` | `none` | `test(app): add app module type comparison config` | 25 deltas documented; `SDK_VERSION` typed `string`; compare:types green for `app`. |
285+
| Phase 0.1 `app` compare:types | P0.1 | **closed** | **closed** | **closed** | done | `none` | `test(app): add app module type comparison config` | Committed 2026-06-30. 25 deltas documented; compare:types green for `app`. |
286+
| Phase 1 `firestore` TurboModules | P1 | **closed** | **closed** | **closed** | done | `area-focused` | `feat(firestore)!: migrate firestore to TurboModules` | Committed 2026-06-30. 4 specs; area e2e 732 pass/7 pending iOS+Android; jest 285/285; compare:types ✓. |
284287

285288
---
286289

okf-bundle/new-architecture/turbomodule-implementation-workflow.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ In addition to [change authoring gates](../testing/change-authoring-workflow.md#
4444

4545
One TurboModule spec **per legacy native module** — do not consolidate the *specs* in the first pass (database ×5, firestore ×4); each legacy module keeps its own `NativeRNFBTurbo*` spec and shell.
4646

47-
**Commit granularity: one commit per package, not per spec.** Splitting a multi-module package across several commits adds little value and is often impractical — the specs share `codegenConfig`, generated artifacts, podspec/`build.gradle` guards, and JS wiring that only compile and pass e2e together. Convert the whole package (all its legacy modules → all its specs) in a single `implementation``independent-review``commit` loop and land it as **one** `feat(<pkg>): migrate <pkg> to TurboModules` commit. Per-spec commits are only warranted if a single legacy module is genuinely independently shippable and reviewable, which is rare. Multiple specs ≠ multiple commits.
47+
**Commit granularity: one commit per package, not per spec.** Splitting a multi-module package across several commits adds little value and is often impractical — the specs share `codegenConfig`, generated artifacts, podspec/`build.gradle` guards, and JS wiring that only compile and pass e2e together. Convert the whole package (all its legacy modules → all its specs) in a single `implementation``independent-review``commit` loop and land it as **one** `feat(<pkg>)!: migrate <pkg> to TurboModules` commit (breaking: New Architecture required). Per-spec commits are only warranted if a single legacy module is genuinely independently shippable and reviewable, which is rare. Multiple specs ≠ multiple commits.
4848

4949
### Multi-spec packages (`app` precedent)
5050

@@ -164,9 +164,11 @@ Per package (or per phase batch), same commit when user-facing:
164164
## TurboModule `commit`
165165

166166
```text
167-
feat(<pkg>): migrate <module-or-package> to TurboModules
167+
feat(<pkg>)!: migrate <module-or-package> to TurboModules
168168
```
169169

170+
Breaking change (`!`): TurboModule migration requires New Architecture; legacy bridge is removed per package.
171+
170172
**Never stage:** `tests/harness.overrides.js`, any `.only`, temporary sub-suite edits in `tests/app.js`.
171173

172174
## Gotchas

packages/firestore/RNFBFirestore.podspec

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,28 @@ Pod::Spec.new do |s|
2828
s.macos.deployment_target = firebase_macos_target
2929
s.tvos.deployment_target = firebase_tvos_target
3030
s.swift_version = '5.10'
31-
s.source_files = 'ios/**/*.{h,m,mm,swift}'
31+
s.source_files = 'ios/**/*.{h,m,mm,cpp,swift}'
32+
s.private_header_files = [
33+
'ios/RNFBFirestore/RNFBFirestoreModule.h',
34+
'ios/RNFBFirestore/RNFBFirestoreCollectionModule.h',
35+
'ios/RNFBFirestore/RNFBFirestoreDocumentModule.h',
36+
'ios/RNFBFirestore/RNFBFirestoreTransactionModule.h',
37+
'ios/generated/**/*.h',
38+
]
39+
s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'
40+
41+
s.pod_target_xcconfig = {
42+
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/generated/RNFBFirestoreTurboModules\" \"$(PODS_TARGET_SRCROOT)/ios/generated\"",
43+
}
3244

3345
s.dependency 'RNFBApp'
3446

35-
# React Native dependencies
36-
if defined?(install_modules_dependencies()) != nil
37-
install_modules_dependencies(s);
38-
else
39-
s.dependency "React-Core"
47+
install_modules_dependencies(s);
48+
49+
# Fail fast for old architecture users, but safely in case the variable goes away
50+
# completely in future react-native versions
51+
if defined?(ENV["RCT_NEW_ARCH_ENABLED"]) != nil && (ENV["RCT_NEW_ARCH_ENABLED"] == '0')
52+
raise "#{s.name} requires New Architecture. Enable New Architecture to use this module"
4053
end
4154

4255
if defined?($FirebaseSDKVersion)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { describe, expect, it, jest } from '@jest/globals';
2+
import { TurboModuleRegistry } from 'react-native';
3+
import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal';
4+
import FirebaseModule from '@react-native-firebase/app/dist/module/internal/FirebaseModule';
5+
import { getNativeModule } from '@react-native-firebase/app/dist/module/internal/registry/nativeModule';
6+
import type { WrappedNativeModule } from '@react-native-firebase/app/dist/module/internal/NativeModules';
7+
8+
const MAIN_METHODS = [
9+
'setLogLevel',
10+
'loadBundle',
11+
'clearPersistence',
12+
'waitForPendingWrites',
13+
'disableNetwork',
14+
'enableNetwork',
15+
'useEmulator',
16+
'settings',
17+
'terminate',
18+
'persistenceCacheIndexManager',
19+
'addSnapshotsInSync',
20+
'removeSnapshotsInSync',
21+
] as const;
22+
23+
const COLLECTION_METHODS = [
24+
'namedQueryOnSnapshot',
25+
'collectionOnSnapshot',
26+
'collectionOffSnapshot',
27+
'namedQueryGet',
28+
'collectionCount',
29+
'aggregateQuery',
30+
'pipelineExecute',
31+
'collectionGet',
32+
] as const;
33+
34+
const DOCUMENT_METHODS = [
35+
'documentOnSnapshot',
36+
'documentOffSnapshot',
37+
'documentGet',
38+
'documentDelete',
39+
'documentSet',
40+
'documentUpdate',
41+
'documentBatch',
42+
] as const;
43+
44+
const TRANSACTION_METHODS = [
45+
'transactionBegin',
46+
'transactionGetDocument',
47+
'transactionDispose',
48+
'transactionApplyBuffer',
49+
] as const;
50+
51+
const ALL_SPEC_METHODS = [
52+
...MAIN_METHODS,
53+
...COLLECTION_METHODS,
54+
...DOCUMENT_METHODS,
55+
...TRANSACTION_METHODS,
56+
];
57+
58+
function createTurboModuleFixture(
59+
methods: Record<string, jest.Mock>,
60+
constants: Record<string, unknown> = {},
61+
): Record<string, unknown> {
62+
const proto = Object.create(Object.prototype, {
63+
getConstants: {
64+
value: () => constants,
65+
enumerable: true,
66+
},
67+
});
68+
69+
for (const [name, fn] of Object.entries(methods)) {
70+
Object.defineProperty(proto, name, {
71+
value: fn,
72+
enumerable: true,
73+
configurable: true,
74+
});
75+
}
76+
77+
return Object.create(proto);
78+
}
79+
80+
describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
81+
it('asserts merged Firestore spec method names are unique (NewArch-AD-11)', function () {
82+
expect(new Set(ALL_SPEC_METHODS).size).toBe(ALL_SPEC_METHODS.length);
83+
expect(ALL_SPEC_METHODS).toHaveLength(31);
84+
});
85+
86+
it('routes methods through a 4-host merge composite Proxy (NewArch-AD-14a)', function () {
87+
const mainMethod = jest.fn(() => 'main');
88+
const collectionMethod = jest.fn(() => 'collection');
89+
const documentMethod = jest.fn(() => 'document');
90+
const transactionMethod = jest.fn(() => 'transaction');
91+
92+
const hostMain = createTurboModuleFixture({ setLogLevel: mainMethod });
93+
const hostCollection = createTurboModuleFixture({ collectionGet: collectionMethod });
94+
const hostDocument = createTurboModuleFixture({ documentGet: documentMethod });
95+
const hostTransaction = createTurboModuleFixture({ transactionBegin: transactionMethod });
96+
97+
jest
98+
.mocked(TurboModuleRegistry.get)
99+
.mockReturnValueOnce(hostMain)
100+
.mockReturnValueOnce(hostCollection)
101+
.mockReturnValueOnce(hostDocument)
102+
.mockReturnValueOnce(hostTransaction);
103+
104+
const config: ModuleConfig = {
105+
namespace: 'firestoreContract',
106+
nativeModuleName: [
107+
'NativeRNFBTurboFirestore',
108+
'NativeRNFBTurboFirestoreCollection',
109+
'NativeRNFBTurboFirestoreDocument',
110+
'NativeRNFBTurboFirestoreTransaction',
111+
],
112+
nativeEvents: false,
113+
hasMultiAppSupport: true,
114+
hasCustomUrlOrRegionSupport: true,
115+
turboModule: true,
116+
};
117+
118+
class MergeModule extends FirebaseModule<any> {
119+
constructor() {
120+
super({ name: '[DEFAULT]' } as any, config);
121+
}
122+
}
123+
124+
const wrapped = getNativeModule(new MergeModule()) as WrappedNativeModule & {
125+
setLogLevel: () => string;
126+
collectionGet: () => string;
127+
documentGet: () => string;
128+
transactionBegin: () => string;
129+
};
130+
131+
expect(wrapped.setLogLevel()).toBe('main');
132+
expect(wrapped.collectionGet()).toBe('collection');
133+
expect(wrapped.documentGet()).toBe('document');
134+
expect(wrapped.transactionBegin()).toBe('transaction');
135+
expect(mainMethod).toHaveBeenCalledTimes(1);
136+
expect(collectionMethod).toHaveBeenCalledTimes(1);
137+
expect(documentMethod).toHaveBeenCalledTimes(1);
138+
expect(transactionMethod).toHaveBeenCalledTimes(1);
139+
expect(Object.keys(wrapped).sort()).toEqual([
140+
'collectionGet',
141+
'documentGet',
142+
'setLogLevel',
143+
'transactionBegin',
144+
]);
145+
});
146+
});

0 commit comments

Comments
 (0)