Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions docs/firestore/usage/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ If you have started to receive a `app:mergeDexDebug` error after adding Cloud Fi
| **Platforms** | Android, iOS (native Firebase SDK); macOS via firebase-js-sdk web interop |
| **New Architecture** | **Required** from v26. See [Migrating to v26](/migrating-to-v26#new-architecture-requirement). |

**Platform notes:** IndexedDB persistence and `memoryLocalCache` / `persistentLocalCache` are **web only**. Native persistence is controlled by the iOS/Android Firestore SDK. `initializeFirestore` is async on React Native.
**Platform notes:** On iOS and Android, the web-only `localCache` values (`memoryLocalCache`, `persistentLocalCache`, IndexedDB factories) are **not supported**, and persistence is controlled by the native Firestore SDK. On macOS, settings are passed to the firebase-js-sdk Firestore Lite `initializeFirestore`, which has no local cache or persistence. `initializeFirestore` returns `Firestore` synchronously; settings apply in the background after return (on the native bridge, or through the firebase-js-sdk web interop on macOS), and settings errors are reported with `console.warn`.

# What does it do

Expand Down Expand Up @@ -140,6 +140,18 @@ function User({ userId }) {

Realtime changes via `onSnapshot` can be applied to both collections and documents.

Listener errors, such as a permission error, are delivered to `onError` (or `observer.error`). A lone `onNext`
callback is never called with an error. If no error handler is given, the error is logged with `console.error`.
Unlike firebase-js-sdk, `setLogLevel('silent')` does not suppress this log. To handle errors, pass an error callback:

```js
const unsubscribe = onSnapshot(
doc(collection(db, 'Users'), userId),
documentSnapshot => console.log('User data: ', documentSnapshot.data()),
error => console.error('Listener failed: ', error),
);
```

### Snapshots

Once a query has returned a result, Firestore returns either a [`QuerySnapshot`](https://reference.rnfirebase.io/_react-native-firebase/firestore/types/firestore/QuerySnapshot.html) (for
Expand Down Expand Up @@ -686,11 +698,9 @@ sensitive information). `initializeFirestore` must be called before any Firestor
import { getApp } from '@react-native-firebase/app';
import { initializeFirestore } from '@react-native-firebase/firestore';

async function bootstrap() {
await initializeFirestore(getApp(), {
persistence: false, // disable offline persistence
});
}
const db = initializeFirestore(getApp(), {
persistence: false, // disable offline persistence
});
```

## Data bundles
Expand Down
5 changes: 3 additions & 2 deletions docs/migrating-to-v26.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ v26 aligns React Native Firebase modular types and several runtime signatures wi
| ------ | ------ | ----- | ----- |
| `logEvent(analytics, …)` | `Promise<void>` | `void` | Fire-and-forget; remove `await` / `.then()` |
| `initializeAppCheck(app?, options?)` | `Promise<AppCheck>` | `AppCheck` | Returns handle immediately; provider setup continues natively |
| `initializeFirestore(app, settings, databaseId?)` | `Promise<Firestore>` | `Firestore` | Returns instance immediately; `settings()` runs on native bridge |
| `initializeFirestore(app, settings, databaseId?)` | `Promise<Firestore>` | `Firestore` | Returns instance immediately; `settings()` runs in the background (native bridge, or web interop on macOS); errors are reported with `console.warn` |
| `getRemoteConfig(app?, options?)` | `(app?)` only | `(app?, options?: RemoteConfigOptions)` | `options` accepted for parity; no-op on native |
| `aggregateFieldEqual()` | Not exported | Exported | Aggregate query helper |
| `LastFetchStatus` literals | — | — | **Unchanged** — native still uses `no_fetch_yet` / `throttled` (documented drift) |
Expand Down Expand Up @@ -759,7 +759,8 @@ The namespaced API for `@react-native-firebase/firestore` has been **removed**.

## Signature changes

- `initializeFirestore(app, settings, databaseId?)` returns `Firestore` synchronously (previously `Promise<Firestore>`). Remove `await` and `.then()`; settings apply on the native bridge after return.
- `initializeFirestore(app, settings, databaseId?)` returns `Firestore` synchronously (previously `Promise<Firestore>`). Remove `await` and `.then()`; settings apply in the background after return (on the native bridge, or through the firebase-js-sdk web interop on macOS). Invalid settings or a failure while applying them are reported with `console.warn` instead of an unhandled promise rejection.
- `onSnapshot(reference, onNext)` and `onSnapshot(reference, options, onNext)` no longer call a lone `onNext` with a listener error. Errors go to `onError` (or `observer.error`); with no error handler they are logged with `console.error`. Unlike firebase-js-sdk, `setLogLevel('silent')` does not suppress this log.
- `TransactionOptions` is now exported. `runTransaction(firestore, updateFunction, options?)` accepts `{ maxAttempts?: number }` and passes the retry limit to the native iOS and Android Firestore SDKs. When `options` is omitted, the native default applies (**5** attempts, matching [firebase-js-sdk `runTransaction`](https://firebase.google.com/docs/reference/js/firestore_.md#runtransaction)). When `maxAttempts` is provided explicitly, values below **1** throw synchronously with `Max attempts must be at least 1` — the same validation as firebase-js-sdk.
- `aggregateFieldEqual()` and `aggregateQuerySnapshotEqual()` are now exported for aggregate query equality checks (firebase-js-sdk parity).
- FieldValue `maximum(n)` and `minimum(n)` sentinels from firebase-js-sdk 12.15.0 are not exported yet. Android Firebase Firestore SDK 34.15.0 supports them, but iOS Firebase Firestore SDK 12.15.0 does not expose matching `FIRFieldValue` factories, so React Native Firebase waits for cross-platform support.
Expand Down
4 changes: 4 additions & 0 deletions okf-bundle/testing/validation-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ yarn tests:jest --watchman=false packages/firestore/__tests__/pipelines.test.ts

Optional: `yarn tests:jest-coverage`.

<a id="jest-ios-slot-1-host-dependency"></a>

**Host-dependent test (darwin).** `packages/app/__tests__/iosSimulatorHelpers.test.js` › "resolves slot-1 UDID on pinned runtime when present" runs live `simctl` on macOS and fails unless an **`RN E2E iOS slot-1`** simulator exists on `RNFB_IOS_SIM_RUNTIME` (default `iOS 27.0`); on Linux it only static-checks the helper. On a Mac without that device the full suite exits 1 whatever the diff is. To provision it: `yarn tests:e2e:setup-ios-sims 2` (creates slot-0 and slot-1; create/reuse-only — [setup count](running-e2e.md#e2e-slot-index), [iOS slot simulators](running-e2e.md#ios-slot-simulators)). When a reviewer cannot provision it, rerun that file alone (`yarn tests:jest --watchman=false packages/app/__tests__/iosSimulatorHelpers.test.js`) to show only that test fails, and record the `jest` row with the real exit and evidence such as `N-1/N suites; 1 host-dependent failure (no slot-1 sim), unrelated to diff`. This exemption does not apply when the diff touches `scripts/e2e/lib/ios-simulator-helpers.sh`, `scripts/e2e/create-ios-simulators.sh`, or that test file.

## Android JVM unit tests

When `packages/*/android/**` Java bridge/state-machine logic changed (or added under `src/test/java`):
Expand Down
127 changes: 127 additions & 0 deletions packages/firestore/__tests__/initializeFirestore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { TurboModuleRegistry } from 'react-native';

import { getApp } from '@react-native-firebase/app';
import { FirebaseFirestoreModule } from '../lib/FirestoreModule';
import { initializeFirestore } from '../lib/modular';
import type { FirestoreSettings } from '../lib/types/firestore';

const nativeSettings = (
TurboModuleRegistry.getEnforcing('NativeRNFBTurboFirestore') as unknown as {
settings: jest.Mock<() => Promise<void>>;
}
).settings;

const originalSettings = FirebaseFirestoreModule.prototype.settings;

// Instances are cached per database, so each test uses its own.
let databaseIndex = 0;
function nextDatabaseId(): string {
return `initialize-firestore-${databaseIndex++}`;
}

function flushPromises(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 0));
}

describe('initializeFirestore settings errors', function () {
let warn: jest.SpiedFunction<typeof console.warn>;
// `catch` delegates to `then`, so wrapping `then` captures handlers attached either way.
let attached: Promise<unknown>[];

beforeEach(function () {
nativeSettings.mockImplementation(() => Promise.resolve());
warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
attached = [];
jest.spyOn(FirebaseFirestoreModule.prototype, 'settings').mockImplementation(function (
this: FirebaseFirestoreModule,
...args
) {
return wrapSettingsResult(originalSettings.apply(this, args));
});
});

function wrapSettingsResult(result: Promise<void>): Promise<void> {
const then = result.then.bind(result);
result.then = ((onFulfilled, onRejected) => {
const derived = then(onFulfilled, onRejected);
attached.push(derived);
return derived;
}) as typeof result.then;
return result;
}

afterEach(function () {
jest.restoreAllMocks();
nativeSettings.mockReset();
});

async function expectSettingsRejectionHandled(): Promise<void> {
expect(attached).toHaveLength(1);
await expect(attached[0]).resolves.toBeUndefined();
}

it.each([
['an unknown key', { foo: true }, "'settings.foo' is not a valid settings field."],
['a non-boolean ssl', { ssl: 'yes' }, "'settings.ssl' must be a boolean value."],
])('reports %s instead of leaving an unhandled rejection', async function (_, settings, message) {
const databaseId = nextDatabaseId();
const firestore = initializeFirestore(
getApp(),
settings as unknown as FirestoreSettings,
databaseId,
);

expect(firestore).toBeDefined();
await expectSettingsRejectionHandled();
expect(warn).toHaveBeenCalledWith(
`initializeFirestore() could not apply settings for database '${databaseId}': ${message}`,
);
});

it('reports a native settings failure', async function () {
nativeSettings.mockImplementation(() => Promise.reject(new Error('native failure')));
const databaseId = nextDatabaseId();

initializeFirestore(getApp(), { ignoreUndefinedProperties: true }, databaseId);

await expectSettingsRejectionHandled();
expect(warn).toHaveBeenCalledWith(
`initializeFirestore() could not apply settings for database '${databaseId}': [firestore/unknown] native failure`,
);
});

it('reports a non-Error settings rejection', async function () {
// The module wraps native failures in an Error, so reject from `settings()` itself.
jest
.spyOn(FirebaseFirestoreModule.prototype, 'settings')
.mockImplementation(() => wrapSettingsResult(Promise.reject('plain failure')));
const databaseId = nextDatabaseId();

initializeFirestore(getApp(), { ignoreUndefinedProperties: true }, databaseId);

await expectSettingsRejectionHandled();
expect(warn).toHaveBeenCalledWith(
`initializeFirestore() could not apply settings for database '${databaseId}': plain failure`,
);
});

it('names the default database when no database ID is given', async function () {
initializeFirestore(getApp(), { ssl: 'yes' } as unknown as FirestoreSettings);

await expectSettingsRejectionHandled();
expect(warn).toHaveBeenCalledWith(
"initializeFirestore() could not apply settings for database '(default)': 'settings.ssl' must be a boolean value.",
);
});

it('does not warn for valid settings', async function () {
initializeFirestore(getApp(), { ignoreUndefinedProperties: true }, nextDatabaseId());

await flushPromises();
expect(nativeSettings).toHaveBeenCalled();
expect(warn).not.toHaveBeenCalled();
expect(attached).toHaveLength(1);
await attached[0];
});
});
Loading
Loading