diff --git a/.agents/skills/ts-js-validation/SKILL.md b/.agents/skills/ts-js-validation/SKILL.md index 88dbf3ffac..5d027553f4 100644 --- a/.agents/skills/ts-js-validation/SKILL.md +++ b/.agents/skills/ts-js-validation/SKILL.md @@ -48,18 +48,20 @@ Set one clear default path so the agent does not choose randomly between options ## Command sequence -Run these root `package.json` scripts in order. **Canonical checklist with pipeline/e2e context:** `okf-bundle/testing/validation-checklist.md` (OKF bundle wins if this skill disagrees). +Run these root `package.json` scripts in order. **Canonical checklist:** [validation-checklist.md](../../../okf-bundle/testing/validation-checklist.md). **Agent allowlist (no improvisation):** [agent-command-policy.md](../../../okf-bundle/testing/agent-command-policy.md). OKF bundle wins if this skill disagrees. 1. `yarn lerna:prepare` 2. `yarn tsc:compile` 3. `yarn tsc:compile:consumer` 4. `yarn reference:api` -5. `yarn tests:jest` -6. `yarn format:js` -7. `yarn compare:types` +5. `yarn lint:js` (use `yarn lint:js --fix` then re-run until clean) +6. `yarn tests:jest` +7. `yarn format:js` +8. `yarn compare:types` ## Gotchas +- **Forbidden:** `yarn workspace … prepare`, `cd packages/ && yarn prepare/build`, `yarn jet`, `npx jet` — see [agent command policy](../../../okf-bundle/testing/agent-command-policy.md). On failure, fix product code and re-run the **same** canonical command. - `yarn format:js` writes changes across `packages/**/*.{js,ts,tsx}`. Check the diff after formatting and do not revert user changes. - Run commands from the repository root so workspace resolution, root `tsconfig.json`, and Jest configuration are consistent. - `yarn lerna:prepare` may rebuild or refresh package artifacts needed before TypeScript or tests run. @@ -106,6 +108,7 @@ Use this template: - `yarn tsc:compile`: passed | failed | not run - `yarn tsc:compile:consumer`: passed | failed | not run - `yarn reference:api`: passed | failed | not run +- `yarn lint:js`: passed | failed | not run - `yarn tests:jest`: passed | failed | not run - `yarn format:js`: passed | failed | changed files - `yarn compare:types`: passed | failed | not run diff --git a/.github/scripts/compare-types/configs/app-check.ts b/.github/scripts/compare-types/configs/app-check.ts index 37296e77f6..78eabcf863 100644 --- a/.github/scripts/compare-types/configs/app-check.ts +++ b/.github/scripts/compare-types/configs/app-check.ts @@ -39,6 +39,11 @@ const config: PackageConfig = { }, ], extraInRN: [ + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. The firebase-js-sdk does not export SDK_VERSION from @firebase/app-check.', + }, { name: 'AppCheckProvider', reason: diff --git a/.github/scripts/compare-types/configs/auth.ts b/.github/scripts/compare-types/configs/auth.ts index 48b5d9f6b5..34ebdb00f5 100644 --- a/.github/scripts/compare-types/configs/auth.ts +++ b/.github/scripts/compare-types/configs/auth.ts @@ -161,6 +161,31 @@ const config: PackageConfig = { reason: 'RN Firebase extension type: firebase-js-sdk AdditionalUserInfo core fields plus index signature for extra native bridge keys. Use when typing values from getAdditionalUserInfo or UserCredential.additionalUserInfo that may include provider-specific native fields.', }, + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. The firebase-js-sdk does not export SDK_VERSION from @firebase/auth.', + }, + { + name: 'AuthListenerCallback', + reason: + 'RN Firebase exported auth state listener callback type. firebase-js-sdk uses inline callback types on onAuthStateChanged / onIdTokenChanged rather than exporting this alias.', + }, + { + name: 'CallbackOrObserver', + reason: + 'RN Firebase exported union type for auth listener callbacks and observers. firebase-js-sdk uses inline overloads rather than exporting this helper type.', + }, + { + name: 'UpdateProfile', + reason: + 'RN Firebase exported profile update options type for updateProfile(). firebase-js-sdk declares the shape inline on updateProfile rather than exporting this alias.', + }, + { + name: 'MultiFactor', + reason: + 'RN Firebase exported MultiFactor interface type. firebase-js-sdk exposes multi-factor enrollment through the multiFactor() helper without exporting this interface at the package root.', + }, ], differentShape: [ @@ -219,6 +244,11 @@ const config: PackageConfig = { reason: 'iOS/Android: generateQrCodeUrl is Promise via native bridge; openInOtpApp is an RN-only helper. Other/All: js-sdk synchronous generateQrCodeUrl is possible when MFA is supported on Other.', }, + { + name: 'User', + reason: + 'RN Firebase User optionally exposes multiFactor for native MFA bridge convenience. firebase-js-sdk keeps multi-factor access on the multiFactor(user) helper only.', + }, ], }; diff --git a/.github/scripts/compare-types/configs/database.ts b/.github/scripts/compare-types/configs/database.ts index bd01ed8fd5..f8e34b390a 100644 --- a/.github/scripts/compare-types/configs/database.ts +++ b/.github/scripts/compare-types/configs/database.ts @@ -4,6 +4,11 @@ const config: PackageConfig = { nameMapping: {}, missingInRN: [], extraInRN: [ + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. The firebase-js-sdk does not export SDK_VERSION from @firebase/database.', + }, { name: 'setPersistenceEnabled', reason: @@ -39,23 +44,7 @@ const config: PackageConfig = { 'a standalone `keepSynced()` function.', }, ], - differentShape: [ - { - name: 'Database', - reason: - 'RN Firebase narrows the public `type` member to the concrete `database` literal, while the firebase-js-sdk declaration leaves it less specific.', - }, - { - name: 'DataSnapshot', - reason: - 'RN Firebase exposes native snapshot metadata helpers such as `key`, `priority`, and `size` on the public snapshot type.', - }, - { - name: 'OnDisconnect', - reason: - 'The public method signatures are equivalent, but the generated declaration text differs in union ordering for the priority parameter.', - }, - ], + differentShape: [], }; export default config; diff --git a/.github/scripts/compare-types/configs/firestore.ts b/.github/scripts/compare-types/configs/firestore.ts index 459e53109b..d95eb73c1a 100644 --- a/.github/scripts/compare-types/configs/firestore.ts +++ b/.github/scripts/compare-types/configs/firestore.ts @@ -222,6 +222,11 @@ const config: PackageConfig = { // Extra in RN Firebase // --------------------------------------------------------------------------- extraInRN: [ + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. The firebase-js-sdk does not export SDK_VERSION from @firebase/firestore.', + }, { name: 'FirebaseApp', reason: diff --git a/.github/scripts/compare-types/configs/installations.ts b/.github/scripts/compare-types/configs/installations.ts index 0a438d6107..c71f737d9d 100644 --- a/.github/scripts/compare-types/configs/installations.ts +++ b/.github/scripts/compare-types/configs/installations.ts @@ -12,7 +12,13 @@ import type { PackageConfig } from '../src/types'; const config: PackageConfig = { nameMapping: {}, missingInRN: [], - extraInRN: [], + extraInRN: [ + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. The firebase-js-sdk does not export SDK_VERSION from @firebase/installations.', + }, + ], differentShape: [], }; diff --git a/.github/scripts/compare-types/configs/perf-config.ts b/.github/scripts/compare-types/configs/perf-config.ts index 6da32fe60a..f0284e7518 100644 --- a/.github/scripts/compare-types/configs/perf-config.ts +++ b/.github/scripts/compare-types/configs/perf-config.ts @@ -8,6 +8,11 @@ import type { PackageConfig } from '../../src/types'; const config: PackageConfig = { missingInRN: [], extraInRN: [ + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. The firebase-js-sdk does not export SDK_VERSION from @firebase/performance.', + }, { name: 'HttpMethod', reason: diff --git a/.github/scripts/compare-types/configs/remote-config.ts b/.github/scripts/compare-types/configs/remote-config.ts index 83a89ae3ef..f62440bc0b 100644 --- a/.github/scripts/compare-types/configs/remote-config.ts +++ b/.github/scripts/compare-types/configs/remote-config.ts @@ -66,6 +66,19 @@ const config: PackageConfig = { 'platform resource file (iOS .plist / Android XML). No equivalent ' + 'exists in the firebase-js-sdk web API.', }, + { + name: 'LastFetchStatus', + reason: + 'RN Firebase exports fetch-status string literals as a named const object ' + + '(`SUCCESS`, `FAILURE`, etc.) for modular callers. The firebase-js-sdk does ' + + 'not export this helper; web callers compare against string literals directly.', + }, + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. ' + + 'The firebase-js-sdk does not export `SDK_VERSION` from `@firebase/remote-config`.', + }, ], differentShape: [ { @@ -89,6 +102,13 @@ const config: PackageConfig = { 'RN Firebase only accepts the optional app instance and does not expose the ' + 'initialization-options surface.', }, + { + name: 'ValueSource', + reason: + 'RN Firebase exports value-source string literals as a named const object ' + + '(`REMOTE`, `DEFAULT`, `STATIC`). The firebase-js-sdk exposes `ValueSource` as ' + + 'a string-literal type alias instead of a runtime constants object.', + }, ], }; diff --git a/.github/scripts/compare-types/configs/storage.ts b/.github/scripts/compare-types/configs/storage.ts index e764da9256..1b10da1c2a 100644 --- a/.github/scripts/compare-types/configs/storage.ts +++ b/.github/scripts/compare-types/configs/storage.ts @@ -44,6 +44,11 @@ const config: PackageConfig = { // Extra in RN Firebase // --------------------------------------------------------------------------- extraInRN: [ + { + name: 'SDK_VERSION', + reason: + 'RN Firebase package version string exported from the modular entry point. The firebase-js-sdk does not export SDK_VERSION from @firebase/storage.', + }, { name: 'setMaxOperationRetryTime', reason: diff --git a/.github/scripts/compare-types/src/registry.ts b/.github/scripts/compare-types/src/registry.ts index 31d822e19e..be1e1ac19c 100644 --- a/.github/scripts/compare-types/src/registry.ts +++ b/.github/scripts/compare-types/src/registry.ts @@ -107,14 +107,11 @@ export const packages: PackageEntry[] = [ name: 'auth', firebaseSdkTypesPaths: [requiredFirebaseTypes('auth')], rnFirebaseModularFiles: [ + path.join(rnDist('auth'), 'index.d.ts'), path.join(rnDist('auth'), 'types', 'auth.d.ts'), - path.join(rnDist('auth'), 'modular.d.ts'), ], rnFirebaseSupportFiles: [ - path.join(rnDist('auth'), 'index.d.ts'), path.join(rnDist('auth'), 'constants.d.ts'), - path.join(rnDist('auth'), 'namespaced.d.ts'), - path.join(rnDist('auth'), 'types', 'namespaced.d.ts'), path.join(rnDist('auth'), 'types', 'internal.d.ts'), path.join(rnDist('auth'), 'ConfirmationResult.d.ts'), path.join(rnDist('auth'), 'MultiFactorResolver.d.ts'), @@ -142,12 +139,11 @@ export const packages: PackageEntry[] = [ name: 'storage', firebaseSdkTypesPaths: [requiredFirebaseTypes('storage')], rnFirebaseModularFiles: [ + path.join(rnDist('storage'), 'index.d.ts'), path.join(rnDist('storage'), 'types', 'storage.d.ts'), - path.join(rnDist('storage'), 'modular.d.ts'), ], rnFirebaseSupportFiles: [ path.join(rnDist('storage'), 'StorageStatics.d.ts'), - path.join(rnDist('storage'), 'types', 'namespaced.d.ts'), path.join(rnDist('storage'), 'types', 'internal.d.ts'), ], config: storageConfig, @@ -156,12 +152,12 @@ export const packages: PackageEntry[] = [ name: 'remote-config', firebaseSdkTypesPaths: [requiredFirebaseTypes('remote-config')], rnFirebaseModularFiles: [ + path.join(rnDist('remote-config'), 'index.d.ts'), path.join(rnDist('remote-config'), 'types', 'remote-config.d.ts'), - path.join(rnDist('remote-config'), 'modular.d.ts'), ], rnFirebaseSupportFiles: [ path.join(rnDist('remote-config'), 'statics.d.ts'), - path.join(rnDist('remote-config'), 'types', 'namespaced.d.ts'), + path.join(rnDist('remote-config'), 'types', 'internal.d.ts'), ], config: remoteConfigConfig, }, @@ -203,13 +199,12 @@ export const packages: PackageEntry[] = [ name: 'database', firebaseSdkTypesPaths: [requiredFirebaseTypes('database')], rnFirebaseModularFiles: [ + path.join(rnDist('database'), 'index.d.ts'), path.join(rnDist('database'), 'types', 'database.d.ts'), - path.join(rnDist('database'), 'modular.d.ts'), path.join(rnDist('database'), 'modular', 'query.d.ts'), path.join(rnDist('database'), 'modular', 'transaction.d.ts'), ], rnFirebaseSupportFiles: [ - path.join(rnDist('database'), 'types', 'namespaced.d.ts'), path.join(rnDist('database'), 'types', 'internal.d.ts'), path.join(rnDist('database'), 'DatabaseDataSnapshot.d.ts'), path.join(rnDist('database'), 'DatabaseOnDisconnect.d.ts'), @@ -224,8 +219,8 @@ export const packages: PackageEntry[] = [ name: 'app-check', firebaseSdkTypesPaths: [requiredFirebaseTypes('app-check')], rnFirebaseModularFiles: [ + path.join(rnDist('app-check'), 'index.d.ts'), path.join(rnDist('app-check'), 'types', 'appcheck.d.ts'), - path.join(rnDist('app-check'), 'modular.d.ts'), ], rnFirebaseSupportFiles: [ path.join(rnDist('app-check'), 'providers.d.ts'), @@ -239,8 +234,8 @@ export const packages: PackageEntry[] = [ requiredFirebaseTypes('installations'), ], rnFirebaseModularFiles: [ + path.join(rnDist('installations'), 'index.d.ts'), path.join(rnDist('installations'), 'types', 'installations.d.ts'), - path.join(rnDist('installations'), 'modular.d.ts'), ], rnFirebaseSupportFiles: [path.join(rnDist('installations'), 'types', 'internal.d.ts')], config: installationsConfig, @@ -250,19 +245,16 @@ export const packages: PackageEntry[] = [ firebaseSdkTypesPaths: [requiredFirebaseTypes('firestore')], rnFirebaseModularFiles: [ path.join(rnDist('firestore'), 'types', 'firestore.d.ts'), - path.join(rnDist('firestore'), 'modular.d.ts'), + path.join(rnDist('firestore'), 'index.d.ts'), path.join(rnDist('firestore'), 'modular', 'query.d.ts'), path.join(rnDist('firestore'), 'modular', 'snapshot.d.ts'), path.join(rnDist('firestore'), 'modular', 'Bytes.d.ts'), path.join(rnDist('firestore'), 'modular', 'FieldPath.d.ts'), path.join(rnDist('firestore'), 'modular', 'FieldValue.d.ts'), - path.join(rnDist('firestore'), 'modular', 'GeoPoint.d.ts'), - path.join(rnDist('firestore'), 'modular', 'Timestamp.d.ts'), path.join(rnDist('firestore'), 'modular', 'VectorValue.d.ts'), ], rnFirebaseSupportFiles: [ - path.join(rnDist('firestore'), 'types', 'namespaced.d.ts'), - path.join(rnDist('firestore'), 'types', 'internal.d.ts'), + path.join(rnDist('firestore'), 'types', 'internal.d.ts'), path.join(rnDist('firestore'), 'FirestoreAggregate.d.ts'), path.join(rnDist('firestore'), 'FirestoreFilter.d.ts'), path.join(rnDist('firestore'), 'FirestoreBlob.d.ts'), @@ -300,8 +292,8 @@ export const packages: PackageEntry[] = [ name: 'perf', firebaseSdkTypesPaths: [requiredFirebaseTypes('performance')], rnFirebaseModularFiles: [ + path.join(rnDist('perf'), 'index.d.ts'), path.join(rnDist('perf'), 'types', 'perf.d.ts'), - path.join(rnDist('perf'), 'modular.d.ts'), ], rnFirebaseSupportFiles: [], config: perfConfig, diff --git a/.spellcheck.dict.txt b/.spellcheck.dict.txt index 729c8d6cdc..5e4d76e1cb 100644 --- a/.spellcheck.dict.txt +++ b/.spellcheck.dict.txt @@ -8,6 +8,7 @@ Analytics ANR APIs APIs. +accessors APNs AppAttest AppCheck @@ -221,6 +222,7 @@ src SSV stacktrace substring +subpath switchOn SVG TestLab @@ -258,6 +260,7 @@ v22 v23 v24 v25 +v26 Ventura VertexAI VPN diff --git a/AGENTS.md b/AGENTS.md index ae640057db..66ab87c7a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,12 +9,14 @@ ## Working - Run from repo root; install once with `yarn`. +- **Agent shell commands:** [agent-command-policy.md](okf-bundle/testing/agent-command-policy.md) only (allowlist). E2e additionally [running-e2e.md § agent rule](okf-bundle/testing/running-e2e.md#agent-rule-read-first). - Follow local package patterns; check `type-test.ts`, `__tests__/`, and plugin dirs before public API/platform changes. - Start with `okf-bundle/index.md` for repo-specific implementation/testing/maintenance knowledge. +- **Change authoring:** [change-authoring-workflow.md](okf-bundle/testing/change-authoring-workflow.md) — baseline → unit-focused implementation → area-focused review → commit. - Use package indexes under `okf-bundle/packages/` for package-specific workflows and active work queues. - Follow `okf-bundle/documentation-policy.md`: durable knowledge in reference docs; ephemeral state only in explicit work queues; commits are documentation; single-commit PR titles must match the commit subject exactly. - Testing entry points: `okf-bundle/testing/index.md`; validation requirements: `okf-bundle/testing/validation-checklist.md`. -- Match validation to the **work type** and **validation tier** in OKF ([iteration vocabulary](okf-bundle/testing/iteration-vocabulary.md), package workflows, active work queue gates). +- Match validation to the **work type** and **validation tier** in OKF ([change authoring workflow](okf-bundle/testing/change-authoring-workflow.md); term ids in [iteration vocabulary](okf-bundle/testing/iteration-vocabulary.md)). ## PR instructions diff --git a/docs.json b/docs.json index 685c82c8b6..4845bc9ba2 100644 --- a/docs.json +++ b/docs.json @@ -44,6 +44,7 @@ { "title": "Migration Guide to v23", "href": "/migrating-to-v23" }, { "title": "Migration Guide to v24", "href": "/migrating-to-v24" }, { "title": "Migration Guide to v25", "href": "/migrating-to-v25" }, + { "title": "Migration Guide to v26", "href": "/migrating-to-v26" }, { "title": "TypeScript", "href": "/typescript" }, { "title": "Platforms", "href": "/platforms" }, { "title": "FAQs and Tips", "href": "/faqs-and-tips" }, diff --git a/docs/migrating-to-v25.mdx b/docs/migrating-to-v25.mdx index 32af5110b4..12d1b0fe0c 100644 --- a/docs/migrating-to-v25.mdx +++ b/docs/migrating-to-v25.mdx @@ -2,7 +2,7 @@ title: Migrating to v25 description: Migrate to React Native Firebase v25. previous: /migrating-to-v24 -next: /typescript +next: /migrating-to-v26 --- Version 25 completes the TypeScript alignment started in v24. Modular types across multiple packages now match the [firebase-js-sdk](https://firebase.google.com/docs/web/modular-upgrade) modular API as closely as possible. Runtime behavior is largely unchanged; **TypeScript consumers** and apps using the **modular API** are most affected. diff --git a/docs/migrating-to-v26.mdx b/docs/migrating-to-v26.mdx new file mode 100644 index 0000000000..b7430cc8a9 --- /dev/null +++ b/docs/migrating-to-v26.mdx @@ -0,0 +1,603 @@ +--- +title: Migrating to v26 +description: Migrate to React Native Firebase v26 — namespaced API removal. +previous: /migrating-to-v25 +next: /typescript +--- + +Version 26 removes the deprecated **namespaced** JavaScript API from selected packages. Each migrated package is **modular-only**: use `getX(app?)` and root-level helper functions from the package entry point. Namespaced default exports, `firebase.()`, and `Firebase*Types` namespaces are removed from those packages. + +If you are upgrading from v24 or earlier, complete [Migrating to v25](/migrating-to-v25) first — v25 TypeScript alignment changes still apply. + +## Table of contents + +- [General pattern](/migrating-to-v26#general-pattern) +- [App](/migrating-to-v26#app) +- [Machine Learning (ML)](/migrating-to-v26#machine-learning-ml) +- [In-App Messaging](/migrating-to-v26#in-app-messaging) +- [Installations](/migrating-to-v26#installations) +- [Cloud Messaging](/migrating-to-v26#cloud-messaging) +- [App Distribution](/migrating-to-v26#app-distribution) +- [Cloud Functions](/migrating-to-v26#cloud-functions) +- [Performance Monitoring](/migrating-to-v26#performance-monitoring) +- [App Check](/migrating-to-v26#app-check) +- [Analytics](/migrating-to-v26#analytics) +- [Remote Config](/migrating-to-v26#remote-config) +- [Crashlytics](/migrating-to-v26#crashlytics) +- [Realtime Database](/migrating-to-v26#realtime-database) +- [Cloud Storage](/migrating-to-v26#cloud-storage) +- [Authentication](/migrating-to-v26#authentication) +- [Cloud Firestore](/migrating-to-v26#cloud-firestore) +- [Automated migration checklist](/migrating-to-v26#automated-migration-checklist) + +## General pattern + +For each package listed in this guide: + +1. Remove default export / `firebase.()` usage — import modular helpers from the package root. +2. Replace `Firebase*Types` namespace imports with root types (`InAppMessaging`, `Installations`, `FirebaseML`, etc.). +3. Prefer free functions (`getId(installations)`, `setMessagesDisplaySuppressed(inAppMessaging, enabled)`) over instance methods on service objects. + +**Modular-only in v26:** `@react-native-firebase/app`, `@react-native-firebase/ml`, `@react-native-firebase/in-app-messaging`, `@react-native-firebase/installations`, `@react-native-firebase/messaging`, `@react-native-firebase/app-distribution`, `@react-native-firebase/functions`, `@react-native-firebase/perf`, `@react-native-firebase/app-check`, `@react-native-firebase/remote-config`, `@react-native-firebase/crashlytics`, `@react-native-firebase/database`, `@react-native-firebase/storage`, `@react-native-firebase/analytics`, `@react-native-firebase/auth`, and `@react-native-firebase/firestore`. Other packages may still expose the namespaced API with deprecation warnings; modular imports remain the supported path. + +# App + +The namespaced API for `@react-native-firebase/app` has been **removed**. This package is **modular-only** — use named exports such as `getApp()`, `initializeApp()`, and `getUtils()`. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ----------------------------------------------------- | ------------------------------------------------------------------- | +| Default export `firebase` | Named exports from `@react-native-firebase/app` | +| `firebase.app(name?)` | `getApp(name?)` | +| `firebase.apps` | `getApps()` | +| `firebase.initializeApp(options, name?)` | `initializeApp(options, name?)` | +| `firebase.utils(app?)` | `getUtils(app?)` | +| `firebase.utils.FilePath` | `FilePath` (named export) | +| `firebase.SDK_VERSION` | `SDK_VERSION` (named export) | +| `firebase.setLogLevel(...)` | `setLogLevel(...)` | +| `firebase.()` accessors on the default export | Import modular helpers from each `@react-native-firebase/*` package | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; + +firebase.app().name; +firebase.initializeApp(options, 'secondary'); +firebase.utils().FilePath.DOCUMENT_DIRECTORY; +``` + +```js +// Now +import { getApp, initializeApp, getUtils, FilePath } from '@react-native-firebase/app'; + +getApp().name; +initializeApp(options, 'secondary'); +getUtils().FilePath.DOCUMENT_DIRECTORY; +// or +FilePath.DOCUMENT_DIRECTORY; +``` + +# Machine Learning (ML) + +The namespaced API for `@react-native-firebase/ml` has been **removed**. This package is **modular-only** — use `getML(app?)`. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| --------------------------- | ------------------------------------- | +| `firebase.ml()` | `getML()` or `getML(app)` | +| Default export `ml()` | `getML()` or `getML(app)` | +| `FirebaseMLTypes` namespace | Import `FirebaseML` from package root | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import ml from '@react-native-firebase/ml'; + +firebase.ml().app.name; +ml().app.name; +``` + +```js +// Now +import { getApp } from '@react-native-firebase/app'; +import { getML } from '@react-native-firebase/ml'; + +getML().app.name; // default app +getML(getApp('secondaryFromNative')).app.name; +``` + +# In-App Messaging + +The namespaced API for `@react-native-firebase/in-app-messaging` has been **removed**. This package is **modular-only** — use `getInAppMessaging()` and the root-level helper functions. + +There is no `firebase/in-app-messaging` entry in the firebase-js-sdk modular surface; React Native Firebase follows the same modular service-instance pattern used for other native-only modules. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ------------------------------------------- | ------------------------------------------------------------------------------------ | +| `firebase.inAppMessaging()` | `getInAppMessaging()` | +| Default export `inAppMessaging()` | `getInAppMessaging()` | +| `FirebaseInAppMessagingTypes` | Import `InAppMessaging` and helpers (`setMessagesDisplaySuppressed`, etc.) from root | +| Instance-only usage without modular helpers | Prefer free functions: `setMessagesDisplaySuppressed(inAppMessaging, enabled)`, etc. | + +## Example: suppressing messages during setup + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import inAppMessaging from '@react-native-firebase/in-app-messaging'; + +await firebase.inAppMessaging().setMessagesDisplaySuppressed(true); +// or +await inAppMessaging().setMessagesDisplaySuppressed(true); +``` + +```js +// Now +import { + getInAppMessaging, + setMessagesDisplaySuppressed, +} from '@react-native-firebase/in-app-messaging'; + +await setMessagesDisplaySuppressed(getInAppMessaging(), true); +``` + +See [In-App Messaging usage](/in-app-messaging/usage) for additional examples. + +# Installations + +The namespaced API for `@react-native-firebase/installations` has been **removed**. This package is **modular-only** — use `getInstallations()` and the root-level helper functions. + +Modular `getInstallations()` returns a firebase-js-sdk-shaped `Installations` object exposing only `app`. Use modular helper functions instead of instance methods. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ----------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `firebase.installations()` | `getInstallations()` | +| Default export `installations()` | `getInstallations()` | +| `FirebaseInstallationsTypes` | Import `Installations` and helpers (`getId`, `getToken`, `deleteInstallations`) from root | +| `installations.getId()` / `.getToken()` / `.delete()` | `getId(installations)`, `getToken(installations)`, `deleteInstallations(installations)` | + +## Breaking changes (modular call shape) + +| Before | Now | +| -------------------------- | --------------------------------------------------------------- | +| `installations.getId()` | `getId(installations)` | +| `installations.getToken()` | `getToken(installations)` | +| `installations.delete()` | `deleteInstallations(installations)` — argument is **required** | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import installations from '@react-native-firebase/installations'; + +const id = await firebase.installations().getId(); +// or +const id = await installations().getId(); +``` + +```js +// Now +import { getInstallations, getId } from '@react-native-firebase/installations'; + +const installations = getInstallations(); +const id = await getId(installations); +``` + +See [Installations usage](/installations/usage) for additional examples. + +# Cloud Messaging + +The namespaced API for `@react-native-firebase/messaging` has been **removed**. This package is **modular-only** — use `getMessaging()` and the root-level helper functions. + +Modular `getMessaging()` returns a firebase-js-sdk-shaped `Messaging` object. Prefer free functions (`getToken(messaging)`, `onMessage(messaging, listener)`, etc.) over calling methods on the service instance. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `firebase.messaging()` | `getMessaging()` | +| Default export `messaging()` | `getMessaging()` | +| `FirebaseMessagingTypes` | Import `Messaging`, `RemoteMessage`, and helpers from package root | +| `messaging.getToken()` / `.onMessage()` / etc. | `getToken(messaging)`, `onMessage(messaging, listener)`, etc. | +| `messaging.setDeliveryMetricsExportToBigQuery()` | `experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, enabled)` | +| `firebase.messaging.SDK_VERSION` / `messaging.SDK_VERSION` | `SDK_VERSION` top-level export from package root | + +## Example: foreground messages + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import messaging from '@react-native-firebase/messaging'; + +firebase.messaging().onMessage(message => { + console.log(message.data); +}); +// or +messaging().onMessage(message => { + console.log(message.data); +}); +``` + +```js +// Now +import { getMessaging, onMessage } from '@react-native-firebase/messaging'; + +onMessage(getMessaging(), message => { + console.log(message.data); +}); +``` + +## Example: delivery metrics to BigQuery + +```js +// Previously (removed) +await firebase.messaging().setDeliveryMetricsExportToBigQuery(true); +``` + +```js +// Now +import { + getMessaging, + experimentalSetDeliveryMetricsExportedToBigQueryEnabled, +} from '@react-native-firebase/messaging'; + +await experimentalSetDeliveryMetricsExportedToBigQueryEnabled(getMessaging(), true); +``` + +See [Cloud Messaging usage](/messaging/usage) for additional examples. + +# App Distribution + +The namespaced API for `@react-native-firebase/app-distribution` has been **removed**. This package is **modular-only** — use `getAppDistribution()` and the root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `firebase.appDistribution()` | `getAppDistribution()` | +| Default export `appDistribution()` | `getAppDistribution()` | +| `FirebaseAppDistributionTypes` | Import `AppDistribution`, `AppDistributionRelease` from package root | +| `appDistribution.isTesterSignedIn()` / etc. | `isTesterSignedIn(appDistribution)`, `signInTester(appDistribution)`, `checkForUpdate(appDistribution)`, `signOutTester(appDistribution)` | +| `firebase.appDistribution.SDK_VERSION` | `SDK_VERSION` top-level export from package root | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import appDistribution from '@react-native-firebase/app-distribution'; + +await firebase.appDistribution().isTesterSignedIn(); +``` + +```js +// Now +import { getAppDistribution, isTesterSignedIn } from '@react-native-firebase/app-distribution'; + +const appDistribution = getAppDistribution(); +await isTesterSignedIn(appDistribution); +``` + +# Cloud Functions + +The namespaced API for `@react-native-firebase/functions` has been **removed**. This package is **modular-only** — use `getFunctions()` and root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ----------------------------- | ----------------------------------------------------------------------------- | +| `firebase.functions()` | `getFunctions()` or `getFunctions(app, regionOrCustomDomain)` | +| Default export `functions()` | `getFunctions()` | +| `FirebaseFunctionsTypes` | Import `Functions`, `HttpsCallable`, `HttpsErrorCode`, etc. from package root | +| `functions().httpsCallable()` | `httpsCallable(functions, name, options?)` | +| `functions().useEmulator()` | `connectFunctionsEmulator(functions, host, port)` | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import functions from '@react-native-firebase/functions'; + +await firebase.functions().httpsCallable('myFn')({ foo: 'bar' }); +``` + +```js +// Now +import { getFunctions, httpsCallable } from '@react-native-firebase/functions'; + +const functions = getFunctions(); +await httpsCallable(functions, 'myFn')({ foo: 'bar' }); +``` + +# Performance Monitoring + +The namespaced API for `@react-native-firebase/perf` has been **removed**. This package is **modular-only** — use `getPerformance()` and root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| -------------------------- | ------------------------------------------------------------------------ | +| `firebase.perf()` | `getPerformance()` | +| Default export `perf()` | `getPerformance()` | +| `FirebasePerformanceTypes` | Import `FirebasePerformance`, `PerformanceTrace`, etc. from package root | +| `perf().newTrace()` | `trace(performance, name)` | +| `perf().newHttpMetric()` | `httpMetric(performance, url, httpMethod)` | + +# App Check + +The namespaced API for `@react-native-firebase/app-check` has been **removed**. This package is **modular-only** — use `getAppCheck()` / `initializeAppCheck()` and root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| --------------------------- | ---------------------------------------------------------------- | +| `firebase.appCheck()` | `getAppCheck()` or `initializeAppCheck(app, options)` | +| Default export `appCheck()` | `getAppCheck()` | +| `FirebaseAppCheckTypes` | Import `AppCheck`, `AppCheckTokenResult`, etc. from package root | +| `appCheck().getToken()` | `getToken(appCheck, forceRefresh?)` | + +# Analytics + +The namespaced API for `@react-native-firebase/analytics` has been **removed**. This package is **modular-only** — use `getAnalytics()` and root-level helper functions. + +Analytics supports only the **default Firebase app** (same as before). Calling `getAnalytics(secondaryApp)` throws. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| `firebase.analytics()` | `getAnalytics()` | +| Default export `analytics()` | `getAnalytics()` | +| `FirebaseAnalyticsTypes` | Import `Analytics`, event parameter types, etc. from package root | +| `analytics().logEvent(...)` | `logEvent(analytics, ...)` | +| `analytics().setUserId(...)` etc. | `setUserId(analytics, ...)`, `setConsent(analytics, ...)`, etc. | +| Deprecated helper events (`logScreenView`, `logPurchase`, …) | Still exported as modular helpers; prefer `logEvent(analytics, 'screen_view', params)` | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import analytics from '@react-native-firebase/analytics'; + +await firebase.analytics().logEvent('screen_view', { screen_name: 'Home' }); +await analytics().setUserId('user-123'); +``` + +```js +// Now +import { getAnalytics, logEvent, setUserId } from '@react-native-firebase/analytics'; + +const analytics = getAnalytics(); +await logEvent(analytics, 'screen_view', { screen_name: 'Home' }); +await setUserId(analytics, 'user-123'); +``` + +See [Analytics usage](/analytics/usage) for additional examples. + +# Remote Config + +The namespaced API for `@react-native-firebase/remote-config` has been **removed**. This package is **modular-only** — use `getRemoteConfig()` and root-level helper functions. + +Modular `getRemoteConfig()` returns a firebase-js-sdk-shaped `RemoteConfig` object with `app`, `settings`, `defaultConfig`, `fetchTimeMillis`, and `lastFetchStatus`. Use modular helper functions for fetch/activate/getters; assign `remoteConfig.settings` and `remoteConfig.defaultConfig` instead of removed modular `setConfigSettings()` / `setDefaults()` helpers. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | +| `firebase.remoteConfig()` | `getRemoteConfig()` | +| Default export `remoteConfig()` | `getRemoteConfig()` | +| `FirebaseRemoteConfigTypes` | Import `RemoteConfig`, `Value`, etc. from package root | +| `remoteConfig().activate()` etc. | `activate(remoteConfig)`, `fetchConfig(remoteConfig)`, `getValue(remoteConfig, key)`, etc. | +| `remoteConfig.LastFetchStatus` / `.ValueSource` on namespace | `LastFetchStatus`, `ValueSource` named exports | + +## Breaking changes (modular call shape) + +| Before | Now | +| ------------------------------------------- | ---------------------------------------- | +| `remoteConfig().fetch()` | `fetchConfig(remoteConfig)` | +| `remoteConfig().setConfigSettings({ ... })` | `remoteConfig.settings = { ... }` | +| `remoteConfig().setDefaults({ ... })` | `remoteConfig.defaultConfig = { ... }` | +| `remoteConfig().onConfigUpdated(listener)` | `onConfigUpdate(remoteConfig, observer)` | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import remoteConfig from '@react-native-firebase/remote-config'; + +await firebase.remoteConfig().fetchAndActivate(); +const value = firebase.remoteConfig().getValue('key').asString(); +``` + +```js +// Now +import { getRemoteConfig, fetchAndActivate, getValue } from '@react-native-firebase/remote-config'; + +const remoteConfig = getRemoteConfig(); +await fetchAndActivate(remoteConfig); +const value = getValue(remoteConfig, 'key').asString(); +``` + +See [Remote Config usage](/remote-config/usage) for additional examples. + +# Crashlytics + +The namespaced API for `@react-native-firebase/crashlytics` has been **removed**. This package is **modular-only** — use `getCrashlytics()` and root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ------------------------------ | ----------------------------------------------- | +| `firebase.crashlytics()` | `getCrashlytics()` | +| Default export `crashlytics()` | `getCrashlytics()` | +| `FirebaseCrashlyticsTypes` | Import `Crashlytics` from package root | +| `crashlytics().log()` | `log(crashlytics, message)` | +| `crashlytics().recordError()` | `recordError(crashlytics, error, jsErrorName?)` | + +# Realtime Database + +The namespaced API for `@react-native-firebase/database` has been **removed**. This package is **modular-only** — use `getDatabase()` and root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| --------------------------- | -------------------------------------------------------------- | +| `firebase.database()` | `getDatabase()` or `getDatabase(app, url?)` | +| Default export `database()` | `getDatabase()` | +| `FirebaseDatabaseTypes` | Import `Database`, `DatabaseReference`, etc. from package root | +| `database().ref()` | `ref(database, path?)` | +| `database().refFromURL()` | `refFromURL(database, url)` | + +# Cloud Storage + +The namespaced API for `@react-native-firebase/storage` has been **removed**. This package is **modular-only** — use `getStorage()` and root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| -------------------------- | -------------------------------------------------------------------- | +| `firebase.storage()` | `getStorage()` or `getStorage(app, bucketUrl?)` | +| Default export `storage()` | `getStorage()` | +| `FirebaseStorageTypes` | Import `FirebaseStorage`, `StorageReference`, etc. from package root | +| `storage().ref()` | `ref(storage, path?)` | +| `storageRef.put()` | `uploadBytesResumable(storageRef, data, metadata?)` | + +# Authentication + +The namespaced API for `@react-native-firebase/auth` has been **removed**. This package is **modular-only** — use `getAuth()` / `initializeAuth()` and root-level helper functions. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ---------------------------------------- | --------------------------------------------------------------- | +| `firebase.auth()` | `getAuth()` or `getAuth(app)` | +| Default export `auth()` | `getAuth()` | +| `FirebaseAuthTypes` namespace | Import `Auth`, `User`, `UserCredential`, etc. from package root | +| `auth().signInWithEmailAndPassword(...)` | `signInWithEmailAndPassword(getAuth(), ...)` | +| `auth().useEmulator(url)` | `connectAuthEmulator(getAuth(), url, options?)` | +| `firebase.auth.EmailAuthProvider` | `EmailAuthProvider` (named export) | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import auth from '@react-native-firebase/auth'; + +firebase.auth().signOut(); +auth().currentUser; +``` + +```js +// Now +import { getAuth, signOut } from '@react-native-firebase/auth'; + +const auth = getAuth(); +await signOut(auth); +auth.currentUser; +``` + +# Cloud Firestore + +The namespaced API for `@react-native-firebase/firestore` has been **removed**. This package is **modular-only** — use `getFirestore()` / `initializeFirestore()` and root-level helper functions. The `@react-native-firebase/firestore/pipelines` subpath export is unchanged. + +## Removed namespaced API + +| Removed | Replacement (modular) | +| ---------------------------------------------- | ------------------------------------------------------------------------------------- | +| `firebase.firestore()` | `getFirestore()` or `getFirestore(app, databaseId?)` | +| Default export `firestore()` | `getFirestore()` | +| `FirebaseFirestoreTypes` namespace | Import `Firestore`, `DocumentReference`, `Query`, etc. from package root | +| `firestore().collection(...)` | `collection(getFirestore(), ...)` | +| `firestore().doc(...)` | `doc(getFirestore(), ...)` | +| `firestore().batch()` | `writeBatch(getFirestore())` | +| `firestore().runTransaction(...)` | `runTransaction(getFirestore(), ...)` | +| `firestore().useEmulator(...)` | `connectFirestoreEmulator(getFirestore(), ...)` | +| `firestore().settings(...)` | `initializeFirestore(app, settings, databaseId?)` | +| `firestore.FieldValue` / `firestore.Timestamp` | `FieldValue`, `Timestamp`, `FieldPath`, `Bytes`, `GeoPoint`, `Filter` (named exports) | +| `firestore.Blob` | `Bytes` | + +## Example + +```js +// Previously (removed) +import firebase from '@react-native-firebase/app'; +import firestore from '@react-native-firebase/firestore'; + +firebase.firestore().collection('users').doc('alice').set({ name: 'Alice' }); +firestore.FieldValue.serverTimestamp(); +``` + +```js +// Now +import { + getFirestore, + collection, + doc, + setDoc, + serverTimestamp, +} from '@react-native-firebase/firestore'; + +const db = getFirestore(); +await setDoc(doc(collection(db, 'users'), 'alice'), { + name: 'Alice', + createdAt: serverTimestamp(), +}); +``` + +See [Firestore usage](/firestore/usage) for additional examples. + +# Automated migration checklist + +Use this section when running scripted or agent-assisted upgrades to v26 for modular-only packages. + +## 1. Upgrade dependencies + +```bash +# Bump all @react-native-firebase/* packages to v26 together (monorepo versions are aligned) +yarn add @react-native-firebase/app@^26.0.0 … +cd ios && pod install +``` + +## 2. Fix imports by package + +For each modular-only package you use: + +| Package | Search for | Replace with | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `app` | default `firebase` import, `firebase.app()`, `firebase.apps`, `firebase.initializeApp()`, `firebase.utils()`, `firebase.SDK_VERSION` | `getApp()`, `getApps()`, `initializeApp()`, `getUtils()`, `FilePath`, `SDK_VERSION`, `setLogLevel`, etc. | +| `ml` | `firebase.ml()`, default `ml()`, `FirebaseMLTypes` | `getML()`, root `FirebaseML` type | +| `in-app-messaging` | `firebase.inAppMessaging()`, default `inAppMessaging()`, `FirebaseInAppMessagingTypes` | `getInAppMessaging()`, root modular helpers (`setMessagesDisplaySuppressed`, etc.) | +| `installations` | `firebase.installations()`, default `installations()`, `.getId()`, `.getToken()`, `.delete()`, `FirebaseInstallationsTypes` | `getInstallations()`, `getId()`, `getToken()`, `deleteInstallations()` | +| `messaging` | `firebase.messaging()`, default `messaging()`, `FirebaseMessagingTypes`, instance methods | `getMessaging()`, root helpers (`getToken`, `onMessage`, etc.), `SDK_VERSION`, `experimentalSetDeliveryMetricsExportedToBigQueryEnabled` | +| `app-distribution` | `firebase.appDistribution()`, default `appDistribution()`, `FirebaseAppDistributionTypes`, instance methods | `getAppDistribution()`, `isTesterSignedIn`, `signInTester`, `checkForUpdate`, `signOutTester`, `SDK_VERSION` | +| `functions` | `firebase.functions()`, default `functions()`, `FirebaseFunctionsTypes`, instance methods | `getFunctions()`, `httpsCallable`, `httpsCallableFromUrl`, `connectFunctionsEmulator`, `HttpsErrorCode` | +| `perf` | `firebase.perf()`, default `perf()`, `FirebasePerformanceTypes`, instance methods | `getPerformance()`, `trace`, `httpMetric`, `newScreenTrace`, `startScreenTrace`, `initializePerformance` | +| `app-check` | `firebase.appCheck()`, default `appCheck()`, `FirebaseAppCheckTypes`, instance methods | `getAppCheck()`, `initializeAppCheck`, `getToken`, `getLimitedUseToken`, `setTokenAutoRefreshEnabled`, `onTokenChanged`, `CustomProvider` | +| `analytics` | `firebase.analytics()`, default `analytics()`, `FirebaseAnalyticsTypes`, instance methods | `getAnalytics()`, `logEvent`, `setUserId`, `setConsent`, `getAppInstanceId`, `getSessionId`, etc. | +| `remote-config` | `firebase.remoteConfig()`, default `remoteConfig()`, `FirebaseRemoteConfigTypes`, instance methods | `getRemoteConfig()`, `activate`, `fetchConfig`, `fetchAndActivate`, `getValue`, `getAll`, `onConfigUpdate`, `LastFetchStatus`, `ValueSource` | +| `crashlytics` | `firebase.crashlytics()`, default `crashlytics()`, `FirebaseCrashlyticsTypes`, instance methods | `getCrashlytics()`, `log`, `recordError`, `setUserId`, `setAttribute`, `setAttributes`, `setCrashlyticsCollectionEnabled`, etc. | +| `database` | `firebase.database()`, default `database()`, `FirebaseDatabaseTypes`, instance methods | `getDatabase()`, `ref`, `refFromURL`, `onValue`, `runTransaction`, `connectDatabaseEmulator`, etc. | +| `storage` | `firebase.storage()`, default `storage()`, `FirebaseStorageTypes`, instance/reference methods | `getStorage()`, `ref`, `uploadBytesResumable`, `getDownloadURL`, `connectStorageEmulator`, `StringFormat`, `TaskEvent`, `TaskState` | +| `firestore` | `firebase.firestore()`, default `firestore()`, `FirebaseFirestoreTypes`, instance methods | `getFirestore()`, `doc`, `collection`, `setDoc`, `writeBatch`, `runTransaction`, `connectFirestoreEmulator`, `FieldValue`, `Timestamp`, `Bytes`, `SDK_VERSION` | + +## 3. Validate + +```bash +yarn compile +yarn tests:jest packages//__tests__ +``` diff --git a/docs/typescript.mdx b/docs/typescript.mdx index 777d0acaa6..ac08f1bc53 100644 --- a/docs/typescript.mdx +++ b/docs/typescript.mdx @@ -2,7 +2,7 @@ title: TypeScript description: Using TypeScript with React Native Firebase next: /platforms -previous: /migrating-to-v25 +previous: /migrating-to-v26 --- React Native Firebase ships TypeScript declarations for every module. From v25 onward, modular types and helpers are exported from each package root (for example `@react-native-firebase/auth`) alongside the namespaced default export. diff --git a/okf-bundle/ci-workflows/android.md b/okf-bundle/ci-workflows/android.md index e38f038b67..b8342cad99 100644 --- a/okf-bundle/ci-workflows/android.md +++ b/okf-bundle/ci-workflows/android.md @@ -1,9 +1,9 @@ # Android CI workflows -## E2E job shape +## E2E job shape (CI — mirrors workflow YAML; local: [running e2e](../testing/running-e2e.md)) -1. `yarn tests:android:test-cover --headless` — Detox + Jet (pass/fail gate) -2. `yarn tests:android:post-e2e-coverage` — poll/pull `coverage.ec`, Jacoco report (best-effort, never fails the job) +1. `tests:android:test-cover --headless` — pass/fail gate +2. `tests:android:post-e2e-coverage` — poll/pull `coverage.ec`, Jacoco report (best-effort, never fails the job) 3. **Codecov upload** — two flagged uploads (`e2e-ts-android`, `android-native`); `continue-on-error: true` on the action steps. **`codecov/project/android-native`** fails if the native flag upload is missing (see [coverage design](../testing/coverage-design.md#native-gates)). Android native coverage is flushed in app process by `tests/app.js` Jet `after`; post-e2e pull runs after Detox exits. diff --git a/okf-bundle/ci-workflows/index.md b/okf-bundle/ci-workflows/index.md index 338fbef17e..30d360fbc2 100644 --- a/okf-bundle/ci-workflows/index.md +++ b/okf-bundle/ci-workflows/index.md @@ -6,11 +6,13 @@ GitHub Actions job shape, platform reliability, and artifact triage. * [iOS](ios.md) — simulator boot, logging, troubleshooting, [CI baseload policy](ios.md#ci-baseload-policy-instrumentation) * [Android](android.md) — idling, adb teardown, native coverage -* [Other](other.md) — macOS Jet/Metro, Windows/shared +* [Other](other.md) — macOS e2e, Windows/shared ## Shared E2E dependencies -* [Jet host orchestration](../testing/running-e2e.md#jet-host-orchestration-ports-and-launch-gate) — ports 8090/8091, defer-run launch gate (canonical) +* [Agent command policy](../testing/agent-command-policy.md) — allowlisted install/prepare/validation/e2e commands for agents +* [Running e2e — agent rule](../testing/running-e2e.md#agent-rule-read-first) — e2e `yarn tests:*` detail; never invoke Jet/Detox/Metro directly +* [Test-runner orchestration (log triage)](../testing/running-e2e.md#test-runner-host-orchestration-log-triage-only) — ports 8090/8091, defer-run launch gate markers * [Detox patches](detox-patches.md) — inventory, `ECOMPROMISED`, patch workflow * [Cloud API quota triage](../testing/firebase-testing-project.md#ci-triage-cloud-api-quota-pressure) — live FIS/RC pressure (all platforms) * [iOS diagnostics](ios.md#operational-notes) — `resource-monitor.sh`, `flake-summary.sh`, [CI baseload / load settle](ios.md#ci-baseload-policy-instrumentation) diff --git a/okf-bundle/ci-workflows/ios.md b/okf-bundle/ci-workflows/ios.md index 5272ab0a34..4b308a84cd 100644 --- a/okf-bundle/ci-workflows/ios.md +++ b/okf-bundle/ci-workflows/ios.md @@ -16,7 +16,7 @@ On GHA macOS runners, `simctl list` can show `Booted` before the simulator is te **Pre-boot step** (`.github/workflows/scripts/boot-simulator.sh`), run via `nick-fields/retry` before Detox: -> **Not for local operators.** `boot-simulator.sh` is **CI-only** (this workflow) or invoked **internally** by `tests/e2e/firebase.test.js` on iOS Jet-level retry. Local e2e uses only [running-e2e.md](../testing/running-e2e.md) — [host-clear probes](../testing/running-e2e.md#host-clear-probes) require zero booted simulators; Detox boots `iPhone 17` via `yarn tests:ios:test-cover`. Do not run `boot-simulator.sh` as operator prep. +> **Not for local operators.** `boot-simulator.sh` is **CI-only** (this workflow) or invoked **internally** by `tests/e2e/firebase.test.js` on iOS test-runner retry. Local e2e: [running-e2e.md](../testing/running-e2e.md) only — [host-clear probes](../testing/running-e2e.md#host-clear-probes), [agent rule](../testing/running-e2e.md#agent-rule-read-first). Do not run `boot-simulator.sh` as operator prep. | Phase | What happens | |--------|----------------| @@ -509,7 +509,7 @@ Detox steps use `tee detox-step.log` and `exit ${PIPESTATUS[0]}` so the artifact > **CI/manual mirror only.** The steps below reproduce CI deflake semantics on a developer machine. Local e2e runs must use [running-e2e.md](../testing/running-e2e.md) only — do not substitute `boot-simulator.sh`, `resource-monitor.sh`, or `flake-summary.sh` for the canonical `:build && :test-cover` loop. -To deflake without pushing every change, run the same steps as CI on a macOS machine or VM (SSH is fine). Mirror: emulators → build → `boot-simulator.sh` (pre-boot, `RNFB_START_SIM_LOGS=0`) → `RNFB_SIM_BOOT_MODE=logs` + `RNFB_RECORD_SCREENS=0` → `wait-for-load-settle.sh` → `resource-monitor.sh` → `yarn tests:ios:test-cover` (or `:release`) → `flake-summary.sh`. Wrap in a loop over `iterations` and collect `local-e2e-artifacts/iter-N-*` directories. A self-hosted GHA runner on the same VM is optional when you need exact workflow YAML semantics; direct script iteration is faster for day-to-day patch work. +To deflake without pushing every change, mirror CI on a macOS machine or VM (SSH is fine): emulators → build → `boot-simulator.sh` (pre-boot, CI-only) → `wait-for-load-settle.sh` → `resource-monitor.sh` → `:test-cover` per [running e2e](../testing/running-e2e.md) → `flake-summary.sh`. Wrap in a loop over `iterations` and collect `local-e2e-artifacts/iter-N-*` directories. ### Pinned Homebrew utilities diff --git a/okf-bundle/ci-workflows/other.md b/okf-bundle/ci-workflows/other.md index e7619a8325..61fc471f00 100644 --- a/okf-bundle/ci-workflows/other.md +++ b/okf-bundle/ci-workflows/other.md @@ -1,16 +1,16 @@ # Other CI workflows -## macOS Jet e2e (`tests_e2e_other.yml`) +## macOS e2e (`tests_e2e_other.yml`) -macOS e2e runs **Jet directly**: `yarn tests:macos:test-cover`; app launched via `open` in `tests/.jetrc.js`. +Local macOS e2e: [running e2e § Rules](../testing/running-e2e.md#rules) only. CI pipeline below mirrors `tests_e2e_other.yml` (uses `-ci` packager variants). -### Pipeline +### Pipeline (CI — mirrors `tests_e2e_other.yml`; local operators use [running e2e](../testing/running-e2e.md) only) -1. Build macOS app (`yarn tests:macos:build`, `SKIP_BUNDLING=1`) +1. Build macOS app (`tests:macos:build`, `SKIP_BUNDLING=1`) 2. Start Firestore emulator -3. Start Metro (`yarn tests:packager:jet-ci`) +3. Start Metro (`tests:packager:jet-ci` — CI variant; local: [running e2e § Rules #1](../testing/running-e2e.md#rules)) 4. **Pre-fetch JS bundle**; URL must match app request -5. `yarn tests:macos:test-cover` — Jet `before` hook also prefetches, then `open` app +5. `tests:macos:test-cover` — internal `before` hook prefetches, then `open` app ### CI failure: bundle load hang / `Could not connect to development server` diff --git a/okf-bundle/documentation-policy.md b/okf-bundle/documentation-policy.md index 4475b4295e..f8febc438d 100644 --- a/okf-bundle/documentation-policy.md +++ b/okf-bundle/documentation-policy.md @@ -15,11 +15,11 @@ Single source of truth for OKF knowledge and commit wording. Other OKF docs/work | Kind | Where it lives | What it contains | |------|----------------|------------------| | **Durable** | OKF reference docs (design, runbooks, registries, workflows) | Stable API names, registry IDs, SDK versions, classifications, verification **methods**, architecture, canonical commands | -| **Ephemeral** | Explicit **work-queue** docs only | Session phase/probe IDs, SHAs, gate state, `next_work_type`, snapshot labels, dated banners, run counts | +| **Ephemeral** | Explicit **work-queue** docs only | Session phase/probe IDs, **planned commit subjects** (`commit_subject`), gate state, `next_work_type`, snapshot labels, dated banners, run counts | **Rules** -1. General OKF docs get **durable only** updates: no phase IDs, SHAs, session e2e counts, or gate snapshots. +1. General OKF docs get **durable only** updates: no phase IDs, **commit subjects**, session e2e counts, or gate snapshots. 2. Ephemeral state lives **only** in work queues. When an item closes, durable outcomes move to reference docs; queue rows may archive/delete. 3. Durable docs may link to a work queue for current status; do not duplicate ephemeral fields. @@ -44,7 +44,7 @@ Confirm: | Check | Requirement | |-------|-------------| -| **Canonical location** | Each topic has one owning doc; others link to it ([running e2e](testing/running-e2e.md) for e2e commands, this file for doc/commit policy, etc.) | +| **Canonical location** | Each topic has one owning doc; others link to it ([agent command policy](testing/agent-command-policy.md) for **all** agent shell commands; [change authoring](testing/change-authoring-workflow.md) for workflow/gates/frozen tree; [running e2e](testing/running-e2e.md) for e2e `yarn tests:*` detail — [agent rule](testing/running-e2e.md#agent-rule-read-first); [platform coverage gate](testing/running-e2e.md#platform-coverage-gate-blocking); [iteration vocabulary](testing/iteration-vocabulary.md) for term ids only; this file for doc/commit policy, etc.) | | **DRY** | No duplicated procedures, policy paragraphs, or ephemeral snapshots outside work queues | | **Link hygiene** | Cross-links resolve; indexes list canonical entry points | | **Durability** | No ephemeral fields leaked into general reference docs | @@ -53,8 +53,10 @@ Fix violations before handoff/merge. Work-queue edits still follow this split. ## Work-queue documents -Work queues are **intentionally ephemeral**: phases, SHAs, gates, active coordination. They are not policy or finalized registry/design homes. +Work queues are **intentionally ephemeral**: phases, **commit subjects**, gates, active coordination. They are not policy or finalized registry/design homes. -Work queues record **gates**, **`next_work_type`**, and **`validation_tier`** using [iteration vocabulary](testing/iteration-vocabulary.md). They do **not** name agent roles, dispatch instructions, or session choreography — those are out of scope for the public repo. +Work queues record **gates**, **`next_work_type`**, **`validation_tier`**, and **`commit_subject`** using field names and allowed values from [iteration vocabulary](testing/iteration-vocabulary.md). Gate semantics and workflow rules: [change authoring workflow](testing/change-authoring-workflow.md). They do **not** name agent roles, dispatch instructions, or session choreography — those are out of scope for the public repo. + +Record **`commit_subject`** (the planned Conventional Commit subject line) **before** `git commit`, in the same staged changeset as the item being memorialized. Do not record SHAs — they are unstable under history rewrite. After commit, the subject in git and in the queue must match character-for-character ([PR title rule](#pull-requests) for single-commit PRs). New work queues link here in frontmatter/opening section; do not copy policy inline. diff --git a/okf-bundle/index.md b/okf-bundle/index.md index 80ff776fe1..aa84bd6532 100644 --- a/okf-bundle/index.md +++ b/okf-bundle/index.md @@ -12,12 +12,19 @@ okf_version: "0.1" # Testing -* [Iteration vocabulary](/testing/iteration-vocabulary.md) — work types, validation tiers, gates +* [Agent command policy](/testing/agent-command-policy.md) — allowlisted shell commands for agents (install, prepare, validation, e2e) +* [Change authoring workflow](/testing/change-authoring-workflow.md) — verified product change loop (unit-focused → area-focused review → commit) +* [Iteration vocabulary](/testing/iteration-vocabulary.md) — work type, tier, and queue field identifiers * [Running e2e tests](/testing/running-e2e.md) — canonical e2e commands, narrowing, environment, diagnosis * [Validation checklist](/testing/validation-checklist.md) — compile, Jest, lint, `compare:types`, e2e, coverage * [Coverage design](/testing/coverage-design.md) — unit/e2e coverage policy, native gates, Codecov * [Firebase testing project](/testing/firebase-testing-project.md) — cloud vs emulator, live FIS/RC, helper callables, rules/indexes, deploy +# Cross-cutting work + +* [Namespace API removal workflow](/namespace-api-removal-workflow.md) — modular-only migration checklist, factory design, removal greps +* [Namespace API removal work queue](/namespace-api-removal-work-queue.md) — phase tracker and gate snapshots (ephemeral) + # Packages * [Auth](/packages/auth/index.md) — modular API type parity, platform matrix, `compare:types` diff --git a/okf-bundle/namespace-api-removal-work-queue.md b/okf-bundle/namespace-api-removal-work-queue.md new file mode 100644 index 0000000000..986b260f3b --- /dev/null +++ b/okf-bundle/namespace-api-removal-work-queue.md @@ -0,0 +1,105 @@ +--- +type: Reference +title: Namespace API removal work queue +description: Phase tracker for removing the deprecated namespaced JS API across all packages, leaving a modular-only public surface. +tags: [app, modular, namespaced, deprecation, migration, work-queue] +timestamp: 2026-06-26T00:00:00Z +--- + +# Namespace API removal — work queue + +> **COMPLETE (2026-06-27):** Namespace removal **NV** green on all platforms (`RNFBDebug=true`, no retries). App-check quota skip committed. +> **Order:** pilot smallest (`ml`, `in-app-messaging`) → spike hardest (`messaging`) → bulk small→large → **NF** app cleanup → **NV** full validation. **Workflow:** [namespace-api-removal-workflow.md](namespace-api-removal-workflow.md). + +--- + +Ephemeral tracker; see [OKF policy](documentation-policy.md). Work types / tiers / gate field ids: [iteration vocabulary](testing/iteration-vocabulary.md). **Loop, gates, host rule, harness:** [change authoring workflow](testing/change-authoring-workflow.md) — not restated here. **Agent commands:** [agent command policy](testing/agent-command-policy.md) only — no `yarn workspace … prepare`, no Jet probes. + +--- + +## Phase ordering + +| Phase | Module(s) | Why | +|-------|-----------|-----| +| **N0** | app factory | `gap-analysis` — factory shape; blocks N1 | +| **N1** | `ml` (0m) | hollowest pilot — proves factory + index/test rewrite | +| **N2** | `in-app-messaging` (3m) | second pilot — `withModularFlag` getters | +| **N3** | `messaging` (~28m) | **hardest** — headless task, globals, cross-module hops; native ⇒ `:build` before e2e | +| **N4** | `installations`, `app-distribution`, `functions` (~4m) | small bulk | +| **N5** | `perf`, `app-check`, `crashlytics`, `database`, `storage` | medium bulk | +| **N6** | `remote-config`, `analytics`, `auth`, `firestore` | large blast radius + `compare:types` | +| **NC** | compare-types cleanup | once tree is clean, amend prior namespace-removal commits that left registered-package drift | +| **NF** | `app` + shared infra | [final cleanup](namespace-api-removal-workflow.md#nf--final-cleanup-app--shared-infra) | +| **NV** | all | `pre-merge-validation`, **full** tier | + +`ai`, `vertexai`, `phone-number-verification` already modular-only — no phase. + +--- + +## Resume checklist + +Gate prerequisites before any `:test-cover` ([host rule](testing/change-authoring-workflow.md#host-rule)): + +1. [Pre-flight](testing/running-e2e.md#pre-flight-is-the-host-clear-to-start): [host-clear probes](testing/running-e2e.md#host-clear-probes), [services ready](testing/running-e2e.md#2-services-ready), [harness matches validation tier](testing/running-e2e.md#3-harness-matches-validation-tier) — [area harness: both platform blocks](testing/running-e2e.md#tests-app-js-area-harness); [narrowing gate](testing/running-e2e.md#harness-narrowing-gate-blocking) — **unit-focused** and **area-focused** only); [serial `:test-cover`](testing/running-e2e.md#serialized-e2e-dispatch); [frozen tree](testing/change-authoring-workflow.md#frozen-tree) for `independent-review`. +2. Per-module checklist and removal greps: [namespace-api-removal-workflow.md](namespace-api-removal-workflow.md). User-facing namespace removal: [Migrating to v26](/migrating-to-v26). + +--- + +## Per-module iteration protocol + +Each module follows **one** serial loop. Work types: [change authoring workflow § work types](testing/change-authoring-workflow.md#work-types). Module checklist: [namespace-api-removal-workflow.md § implementation](namespace-api-removal-workflow.md#per-module-implementation). + +| Step | Work type | Closes gate | Rules | +|------|-----------|-------------|-------| +| **1** | `gap-analysis` | — | Read-only; [workflow § gap-analysis](namespace-api-removal-workflow.md#per-module-gap-analysis) | +| **2** | `baseline-capture` | — | **area-focused** tier where native | +| **3** | `implementation` | `implementation` | [Workflow checklist](namespace-api-removal-workflow.md#per-module-implementation); **unit-focused** tier; `.only` OK locally; **no commit** | +| **4** | `independent-review` | `review` | **Frozen tree**; **area-focused** tier; removal greps; minor/nit → remediation + delta re-review | +| **5** | `documentation` | — | Migration guide + OKF if durable learnings | +| **6** | `commit` | `commit` | Set `commit_subject`, close gates in queue doc, stage queue **with** product commit; one focused commit | + +--- + +## Module arbiter gates + +Update immediately after each work type closes a gate ([fields](testing/iteration-vocabulary.md#work-queue-fields)). `~m` = instance methods. + +| Phase | Module | `impl_gate` | `review_gate` | `commit_gate` | `commit_subject` | `next_work_type` | `validation_tier` | Notes | +|-------|--------|-------------|---------------|---------------|------------------|------------------|-------------------|-------| +| N0 | app factory | **closed** | **closed** | **closed** | `refactor(ml): remove deprecated namespace APIs` | — | area-focused | factory shipped with N1 commit | +| N1 | `ml` (0m) | **closed** | **closed** | **closed** | `refactor(ml): remove deprecated namespace APIs` | — | area-focused | pilot modular-only | +| N2 | `in-app-messaging` (3m) | **closed** | **closed** | **closed** | `refactor(in-app-messaging): remove deprecated namespaced API` | — | area-focused | web stub macOS; modular e2e 4×3 | +| N3 | `messaging` (~28m) | **closed** | **closed** | **closed** | `refactor(messaging): remove deprecated namespaced API` | — | area-focused | headless atomic swap; utils native hop | +| N4 | `installations` (4m) | **closed** | **closed** | **closed** | `refactor(installations): remove deprecated namespaced API` | — | area-focused | ios/android 4 passing | +| N4 | `app-distribution` (4m) | **closed** | **closed** | **closed** | `refactor(app-distribution): remove deprecated namespaced API` | — | area-focused | review2 iOS: 49 pass / 0 fail | +| N4 | `functions` (4m) | **closed** | **closed** | **closed** | `refactor(functions): remove deprecated namespaced API` | — | area-focused | review3 iOS: 36 pass / 0 fail (amended) | +| N5 | `perf` (6m) | **closed** | **closed** | **closed** | `refactor(perf): remove deprecated namespaced API` | — | area-focused | review2 iOS: 106 pass / 0 fail | +| N5 | `app-check` (8m) | **closed** | **closed** | **closed** | `refactor(app-check): remove deprecated namespaced API` | — | area-focused | review2 iOS: 50 pass / 0 fail | +| N5 | `crashlytics` (11m) | **closed** | **closed** | **closed** | `refactor(crashlytics): remove deprecated namespaced API` | — | area-focused | review2 iOS: 64 pass / 0 fail | +| N5 | `database` (10m) | **closed** | **closed** | **closed** | `refactor(database): remove deprecated namespaced API` | — | area-focused | review3 iOS: 228 pass / 0 fail (amended) | +| N5 | `storage` (6m) | **closed** | **closed** | **closed** | `refactor(storage): remove deprecated namespaced API` | — | area-focused | review3 iOS: 144 pass / 0 fail (amended) | +| N6 | `remote-config` (15m) | **closed** | **closed** | **closed** | `refactor(remote-config): remove deprecated namespaced API` | — | area-focused | review3 macOS 71/4p, iOS 78/4p, Android 78/4p; `npx jet` spawn fix uncommitted in firebase.test.js | +| N6 | `analytics` (~50m) | **closed** | **closed** | **closed** | `refactor(analytics): remove deprecated namespaced API` | — | area-focused | review1 macOS 62/6p, iOS 63/5p, Android 63/5p | +| N6 | `auth` (~43m) | **closed** | **closed** | **closed** | `refactor(auth): remove deprecated namespaced API` | — | area-focused | review: macOS 139/6p, iOS 148/23p, Android 155/15p; compare:types auth ✓. Minors deferred: legacy `firebase.auth()` error strings + stale JSDoc (NF/string cleanup) | +| N6 | `firestore` (~17m) | **closed** | **closed** | **closed** | `refactor(firestore): remove deprecated namespaced API` | — | area-focused | Loop 3–4 green (701/741/741 area harness); compare:types firestore ✓; lint/format clean; harness reverted. | +| NC | `SDK_VERSION` compare-types drift | **closed** | **closed** | **closed** | `docs(ml,installations,in-app-messaging): document SDK_VERSION export` | — | unit-focused | JSDoc on SDK_VERSION (ml/installations/in-app-messaging); ml stray comment removed. compare:types ✓ all registered; reference:api ✓; lint:js ✓. Configs already on HEAD. Review green; minor/nit deferred (two-track JSDoc vs config-only pattern). Forward commit. | +| NC | `database` compare-types drift | **closed** | **closed** | **closed** | `fix(database): align public types with firebase-js-sdk declarations` | — | unit-focused | Aligned Database/DataSnapshot/OnDisconnect; cleared stale differentShape. Review green; minor getter/harness coupling deferred. compare:types ✓; Jest 45 pass. | +| NC | `app-check` compare-types drift | **closed** | **closed** | **closed** | — | — | unit-focused | compare-types app-check ✓ on HEAD (`b350d17b4`); no further product changes needed. Closed with NC SDK_VERSION pass. | +| NF | `app` | **closed** | **closed** | **closed** | — | — | unit-focused | NF-1..6 committed: utils modular, database proxy purge, app root+deprecation removal, pnv inline, e2e/doc sweep. NF-7 error strings deferred. | +| NV | all | **closed** | **closed** | **closed** | `test(app-check): cleanup quota exceeded handling` | `pre-merge-validation` | full | Static ✓ Jest 1056/1056. **RNFBDebug=true** NV (2026-06-28): macOS 682/36p/0f, iOS 822/85p/0f, Android 848/58p/0f. App-check: quota→skip helper + macOS `CustomProvider`; all app-check modular tests pass iOS/Android. Logs: `/tmp/rnfb-e2e-{macos,ios,android}-nv-{appcheck,debug}.log`. | + +--- + +## N0/N1 review findings + +**Fixed (delta re-review closed):** registry re-resolution + deleted-app guard; internal-`getApp` deprecation warning suppressed; fabricated `getX` message removed. + +**Deferred:** atomic swap for side-effect modules (N3); custom-URL validation (N5); factory unit tests (N2). + +**Edge cases for N3+:** headless handler + module globals; `firebase.utils(app)` hop; dual-exposed statics; instance-method deprecation maps on nested classes. + +--- + +## NV — pre-merge + +`pre-merge-validation`, **full** tier: [change authoring § pre-merge](testing/change-authoring-workflow.md#primary-loop); `compare:types` all registered configs; [validation checklist](testing/validation-checklist.md). diff --git a/okf-bundle/namespace-api-removal-workflow.md b/okf-bundle/namespace-api-removal-workflow.md new file mode 100644 index 0000000000..66ec79aadb --- /dev/null +++ b/okf-bundle/namespace-api-removal-workflow.md @@ -0,0 +1,145 @@ +--- +type: Reference +title: Namespace API removal workflow +description: Cross-package requirements for removing the deprecated namespaced JS API and leaving a modular-only public surface — extends the change authoring workflow. +tags: [app, modular, namespaced, deprecation, migration, workflow] +timestamp: 2026-06-26T00:00:00Z +--- + +# Namespace API removal workflow + +Cross-package requirements for one module (or the final **NF** app cleanup). **Shared loop:** [change authoring workflow](testing/change-authoring-workflow.md). + +**Policy:** [OKF documentation and commit policy](documentation-policy.md). **Live gate snapshots:** [namespace API removal work queue](namespace-api-removal-work-queue.md) (ephemeral). + +## Read first + +| Topic | Document | +|-------|----------| +| **Change authoring loop** | [change-authoring-workflow.md](testing/change-authoring-workflow.md) | +| Work type / tier / gate field ids | [iteration-vocabulary.md](testing/iteration-vocabulary.md) | +| E2e commands | [running-e2e.md](testing/running-e2e.md) | +| Validation commands | [validation-checklist.md](testing/validation-checklist.md) | +| Auth compare-types triage (N6) | [packages/auth/compare-types-triage.md](packages/auth/compare-types-triage.md) | + +## Remove vs keep (per module) + +**Remove** (the namespaced surface): + +| Surface | Where | +|---------|-------| +| `firebase.()` root accessor | `app` root proxy + each package's `firebase` named export | +| `firebase.app().()` accessor | `app` `setOnAppCreate` getters + `declare module '@react-native-firebase/app'` augmentation in `lib/types/namespaced.ts` | +| default export `()` | `lib/namespaced.ts` `export default` | +| `FirebaseTypes` namespace | `lib/types/namespaced.ts` (or `FirebaseXTypes` block in an alt type file) | +| per-method deprecation warnings | `createDeprecationProxy` + `mapOfDeprecationReplacements` entry in `packages/app/lib/common/index.ts` | +| `KNOWN_NAMESPACES` entry + `createModuleNamespace()` | `packages/app/lib/internal/constants.ts`, each `lib/namespaced.ts` | + +**Keep** (modular surface): `getX(app?)` / `fn(instance, …)` in `lib/index.ts` (modular-only entry — no separate `lib/modular.ts`), public types in `lib/types/.ts`, statics as top-level named exports, native wiring. + +**Modular-only end-state templates:** [`phone-number-verification`](../packages/phone-number-verification/lib/modular.ts) (stateless — `getReactNativeModule` directly; `modular.ts` is the sole implementation file) and [`ai`](../packages/ai/lib/index.ts) (instance object via `getAI(app)`; single `index.ts` entry). Factory-based modules (`ml`, `in-app-messaging`, …) follow the **`ai`** pattern: merge former `modular.ts` into `index.ts` and delete `modular.ts`. `vertexai` is a deprecated re-export of `ai`. + +## Modular instance factory (N0) + +Today modular getters route through the namespaced accessor + deprecation proxy (`getApp().()` → `createDeprecationProxy(new ModuleClass())`). Modular-only removal must build the instance **without** the namespaced registry/proxy. + +**Decision (proven on `ml`):** `getOrCreateModularInstance(ModuleClass, config, app?, customUrlOrRegion?)` in `packages/app/lib/internal/registry/modular.ts`. Memoises `new ModuleClass(app, config)` per `app.name`+key; resolves the app via the registry by name (with modular sentinel — see factory JSDoc); applies multi-app guard; clears cache via additive `addOnAppDestroy`. Each module's `lib/index.ts` owns its `ModuleClass` + `ModuleConfig` (modular-only — no `lib/modular.ts`). Stateless modules may skip the instance (phone-number-verification template). + +## Namespace hard gates + +In addition to [change authoring gates](testing/change-authoring-workflow.md#gates): + +| Gate | Requirement | +|------|-------------| +| Gap analysis | Every namespaced capability has a modular export; flag cross-module namespaced hops, constructor side-effects, module-level state, multi-app support | +| Baseline | Module Jest + (where native) **area-focused** e2e on [**every required platform**](testing/running-e2e.md#platform-coverage-gate-blocking); `compare:types` baseline if registered | +| Implementation | Atomic swap — never register in **both** namespaced registry and factory at once (duplicate constructor side-effects) | +| Review | Removal greps empty (below); no deprecation-proxy regression for other modules; `compare:types` unchanged-or-improved if registered | +| Documentation | Row in [`docs/migrating-to-v26.mdx`](../../../docs/migrating-to-v26.mdx) → "namespaced removed"; reconcile v22 deprecation messaging | + +### Removal greps (review must be empty) + +```bash +rg "firebase\.\(" packages/ +rg "createModuleNamespace|getFirebaseRoot" packages//lib +rg "FirebaseTypes" packages/ +rg "''" packages/app/lib/internal/constants.ts +test ! -f packages//lib/namespaced.ts && test ! -f packages//lib/types/namespaced.ts +test ! -f packages//lib/modular.ts +``` + +## Per-module `gap-analysis` + +Read-only. Enumerate namespaced surface (instance methods, getters, statics, default + `firebase` exports, `FirebaseTypes`). Confirm modular parity; list gaps to add first. Note whether `getX()` still delegates via `getApp().()`. + +## Per-module `implementation` + +Complete modular-only checklist (one focused commit scope): + +1. Move `FirebaseModule`, constructor side-effects, module state, statics out of `namespaced.ts` into `lib/index.ts`. +2. Rewire `getX()` in `lib/index.ts` via the N0 factory (or native module directly) — not `getApp().()`; preserve `.app`/`.native`/`.emitter`/multi-app guards/side-effects. +3. Drop the module's `MODULAR_DEPRECATION_ARG` / `withModularFlag` plumbing. +4. Delete `lib/namespaced.ts`, `lib/types/namespaced.ts`, and `lib/modular.ts`; keep `lib/index.ts` as the sole public entry (`import './types/internal'`; export public types + statics). +5. Remove from `KNOWN_NAMESPACES`, `mapOfDeprecationReplacements`, and `declare module '@react-native-firebase/app'` augmentation. +6. `__tests__`: delete `describe('namespace')` + deprecation-pair tests; keep/expand modular. +7. `type-test.ts`: drop namespaced + `FirebaseTypes`. +8. `e2e`: remove `firebase v8 compatibility` suite; keep/expand modular API suite. +9. `README.md` + inline doc snippets. + +**Unit-focused** tier: Jest subset + `tsc:compile` + `type-test.ts`; package-scoped e2e when native touched. Per [change authoring § implementation inner loop](testing/change-authoring-workflow.md#implementation-inner-loop) and [module area harness](#module-area-harness) when applicable. + +## Per-module `independent-review` + +On a **frozen tree** — [change authoring § independent-review](testing/change-authoring-workflow.md#independent-review), plus removal greps and module-specific **area-focused** e2e on [**every required platform**](testing/running-e2e.md#platform-coverage-gate-blocking) (no `.only`; **no Android/macOS shortcuts**). Minor/nit findings: fix or defer-with-rationale, then delta re-review before `commit`. + +## Module area harness + +Extends [change authoring § harness narrowing](testing/change-authoring-workflow.md#harness-narrowing). **Mechanics:** [running e2e § area harness — two platform blocks](testing/running-e2e.md#tests-app-js-area-harness). + +When native e2e runs: load **only** the target package's e2e spec(s) in `tests/app.js`. Narrow **`platformSupportedModules` on both** `if (Platform.other)` and `if (!Platform.other)` (recommended: Pattern A — initial array + `if (false && …)` on **both** blocks). Set **`RNFBDebug = true`** locally per [running e2e § fail-fast](testing/running-e2e.md#fail-fast-rnfbdebug-and-sub-suite-narrowing) — **never commit** (`false` is the committed default). Revert **both** blocks before `commit` or **full** tier. Pass counts must match loaded scope — not full-app totals ([running e2e § gate](testing/running-e2e.md#harness-narrowing-gate-blocking), [platform coverage gate](testing/running-e2e.md#platform-coverage-gate-blocking)). + +## Per-module `documentation` + +- User migration guide row under [`docs/migrating-to-v26.mdx`](../../../docs/migrating-to-v26.mdx) (one section per module) +- [Validation checklist § handoff](testing/validation-checklist.md#handoff-checklist) — `yarn reference:api`, static analysis, etc. when applicable +- Durable learnings in this file or package OKF — not only commit messages + +## Per-module `commit` + +```text +refactor(): remove deprecated namespaced API +``` + +Before `git commit`: + +1. Set the queue row's `commit_subject` to that exact line (replace ``). +2. Close `commit_gate` and update the header/next-pickup line in [namespace API removal work queue](namespace-api-removal-work-queue.md). +3. Stage product, user docs, durable OKF learnings, **and** the queue doc together — one commit. + +**Never stage:** area narrowing, any `.only`, ad-hoc harness edits, or **`RNFBDebug = true`** in `tests/globals.js`. + +## NF — final cleanup (app + shared infra) + +Only after N1–N6 modules committed and their greps empty. Same change-authoring loop; **full** tier for pre-merge. + +- **App namespaced surface:** remove `firebase` + default from `packages/app/lib/index.ts`/`namespaced.ts`; remove namespace plumbing from `registry/namespace.ts`; remove `KNOWN_NAMESPACES` from `constants.ts` and `utils`'s `createModuleNamespace`. +- **Deprecation machinery:** remove `createDeprecationProxy`, `mapOfDeprecationReplacements`, `MODULAR_DEPRECATION_ARG`/`withModularFlag`/`warnIfNotModularCall`, and related globals in `global.d.ts` + `tests/globals.js`. Retire single-slot `setOnAppDestroy` in favor of `addOnAppDestroy`. +- **Pre-existing modular-only packages:** `ai`, `vertexai`, and `phone-number-verification` shipped before N1–N6 but must match the same end-state as migrated modules — **sole public entry is `lib/index.ts`** (no `export * from './modular'` shim; delete `lib/modular.ts` after inlining). `typedoc.json` entry points and internal imports must reference `lib/index.ts` only. +- **Repo sweep:** + +```bash +rg "createModuleNamespace|createDeprecationProxy|KNOWN_NAMESPACES" packages/ +rg "MODULAR_DEPRECATION_ARG|withModularFlag|warnIfNotModularCall" packages/ +rg "RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS|RNFB_MODULAR_DEPRECATION_STRICT_MODE" . +rg "firebase\.(app\(\)\.)?\w+\(" packages/*/e2e packages/*/__tests__ tests/ +``` + +## Gotchas + +- **Atomic swap:** side-effect constructors (messaging headless task) — delete namespaced path in the **same** change as factory wiring. +- **Cross-module namespaced hops** (e.g. `firebase.utils(app)` in messaging): re-point before dependency loses accessor. +- **Dual-exposed statics:** keep modular export; remove namespaced/proxy path only. +- **`compare:types` gap:** only 10 packages registered — rely on `tsc` + `type-test.ts` for the rest. +- **Factory parity:** registry re-resolution by name; custom-URL validation deferred to N5 modules. +- **macOS e2e (native-only modules):** register a no-op `lib/web/` stub via `setReactNativeModule` in `lib/index.ts` so macOS Jet can load and exercise modular JS without a native bridge (see `in-app-messaging` `RNFBFiamModule` web fallback). +- **Firestore on Other/macOS:** Firestore **Lite** only on the web bridge — no full `firebase/firestore`. Unsupported APIs reject with `Not supported in the lite SDK.` See [Other platform Firestore Lite](packages/firestore/other-platform-firestore-lite.md). diff --git a/okf-bundle/packages/firestore/index.md b/okf-bundle/packages/firestore/index.md index c79efe1efd..c8525d2e77 100644 --- a/okf-bundle/packages/firestore/index.md +++ b/okf-bundle/packages/firestore/index.md @@ -6,8 +6,9 @@ Firestore Pipelines (`pipelines` entry) and native bridge behavior. ## Documents +* [**Other platform — Firestore Lite**](other-platform-firestore-lite.md) — macOS/Other uses `firebase/firestore/lite` only; unsupported API list; web bridge files * [Coverage work queue](pipeline-coverage-work-queue.md) — ephemeral gates and `next_work_type` -* [Implementation workflow](pipeline-implementation-workflow.md) — work types: gap-analysis → baseline-capture → implementation → independent-review → documentation → commit +* [Pipelines implementation workflow](pipeline-implementation-workflow.md) — compare-types, serialization, coverage snapshots; extends [change authoring](../../testing/change-authoring-workflow.md) * [Serialization testing](serialization-testing.md) — required Jest/e2e checks per expression export * [Platform parity](pipeline-platform-parity.md) — drift registry, remediation, iOS unsupported map * [SDK support audit](pipeline-sdk-support-audit.md) — guard reconciliation, runtime verification diff --git a/okf-bundle/packages/firestore/other-platform-firestore-lite.md b/okf-bundle/packages/firestore/other-platform-firestore-lite.md new file mode 100644 index 0000000000..3e1fba49ba --- /dev/null +++ b/okf-bundle/packages/firestore/other-platform-firestore-lite.md @@ -0,0 +1,67 @@ +--- +type: Reference +title: Other platform Firestore — Lite SDK +description: Platform backend split and lite-unsupported API list for macOS/Other vs native iOS/Android Firestore. +tags: [firestore, macos, other, lite, web] +timestamp: 2026-06-27T00:00:00Z +--- + +# Other platform Firestore — Lite SDK + +Product and web-bridge rules for **macOS / Other** Firestore. User-facing summary: [`docs/platforms.mdx`](../../../docs/platforms.mdx) § Firestore. + +**Policy:** [OKF documentation and commit policy](../../documentation-policy.md). **Current namespace-removal status:** [work queue](../../namespace-api-removal-work-queue.md) (ephemeral gates only there). + +## Platform split + +| Context | Backend | JS entry | +|---------|---------|----------| +| **iOS / Android** | Native Firebase Firestore SDK | `FirebaseFirestoreModule` → native modules | +| **macOS / Other** | **firebase-js-sdk Firestore Lite only** | `packages/app/lib/internal/web/firebaseFirestore.ts` → `firebase/firestore/lite` | + +`isOther` = `Platform.OS !== 'ios' && Platform.OS !== 'android'`. + +Pipelines on Other use **`firebase/firestore/lite/pipelines`** (side-effect import in the same shared module file) — not full `firebase/firestore`. + +**There is no full `firebase/firestore` product surface on Other/macOS.** Do not import or wire full Firestore SDK paths into the web bridge for bundle, named-query, offline, or listener APIs. + +## Unsupported on Other (lite limitation) + +Web bridge methods must **reject** with `Not supported in the lite SDK.` (matching long-standing behavior on `main`): + +- `loadBundle` / modular `loadBundle()` +- `namedQuery` and named-query collection paths +- Offline & persistence helpers: `clearPersistence`, `clearIndexedDbPersistence`, `disableNetwork`, `enableNetwork`, `waitForPendingWrites` (where lite has no equivalent) +- `onSnapshot` on collection/document references +- `GetOptions.source` / cache-only reads where lite cannot serve them +- `addSnapshotsInSync` / `removeSnapshotsInSync` + +If an API is a lite limitation, it is **unsupported on Other** — not implemented by pulling in full `firebase/firestore`. + +## Supported on Other + +Modular public APIs that Firestore Lite provides must work on Other, including: `getFirestore`, `doc`, `collection`, `getDoc`, `getDocs`, `setDoc`, `updateDoc`, `deleteDoc`, `writeBatch`, `runTransaction`, query constraints (`where`, `or`, `and`, …), aggregates (`getCountFromServer`, …), `connectFirestoreEmulator`, `terminate`, statics (`Bytes`, `FieldPath`, `FieldValue`, `Timestamp`, …), `setLogLevel`, and pipelines (`pipeline()` on lite instances). + +## Web bridge files + +| File | Role | +|------|------| +| `packages/app/lib/internal/web/firebaseFirestore.ts` | Lite (+ lite pipelines import) only | +| `packages/firestore/lib/web/RNFBFirestoreModule.ts` | Lite instance cache; stub unsupported methods | +| `packages/firestore/lib/web/pipelines/` | Pipeline execute via JS SDK on Other | + +**Do not add** a parallel full-SDK module (e.g. `firebaseFirestoreBundle.ts`) or import `firebase/firestore` into the Other bridge. + +## E2e (Other vs native) + +| Platform | Expectation | +|----------|-------------| +| **macOS / Other** | Lite-supported specs must pass. Lite-unsupported specs: `Platform.other` skip **or** assert message contains `Not supported in the lite SDK` (see `packages/firestore/e2e/firestore.e2e.js`). Do not expect native iOS/Android error codes on Other. | +| **iOS / Android** | Native SDK — bundle, named-query, persistence, and listener specs apply. | + +Commands, pre-flight, harness narrowing, and validation tiers: [running e2e](../../testing/running-e2e.md) and [namespace API removal workflow](../../namespace-api-removal-workflow.md) § module area harness — not restated here. + +## Related + +- [Pipeline platform parity](pipeline-platform-parity.md) — pipeline-specific macOS-js drift (separate from core Firestore lite policy) +- [Namespace API removal workflow](../../namespace-api-removal-workflow.md) — per-module removal checklist and gotchas diff --git a/okf-bundle/packages/firestore/pipeline-coverage-work-queue.md b/okf-bundle/packages/firestore/pipeline-coverage-work-queue.md index 3034722034..46217b62b2 100644 --- a/okf-bundle/packages/firestore/pipeline-coverage-work-queue.md +++ b/okf-bundle/packages/firestore/pipeline-coverage-work-queue.md @@ -8,7 +8,7 @@ timestamp: 2026-06-25T12:00:00Z # Pipeline coverage and parity — work queue -> **IN PROGRESS (2026-06-25):** **J0-6′–J0-9′** receiver parity — implementation + area review **complete** (uncommitted). **J0b** committed `c27b6f115`. **Next:** commit batch → **J1** bridge remediation. +> **IN PROGRESS (2026-06-25):** **J0-6′–J0-9′** receiver parity — implementation + area-focused review **complete** (uncommitted). **J0b** committed `c27b6f115`. **Next:** commit batch → **J1** bridge remediation. > **Goal/order:** platform parity first; then TS/native coverage toward intractable limits. Links: [parity](pipeline-platform-parity.md), [SDK audit](pipeline-sdk-support-audit.md), [coverage](../../testing/coverage-design.md), [e2e](../../testing/running-e2e.md), [architecture](pipelines.md). --- @@ -36,9 +36,9 @@ Ephemeral tracker; see [OKF policy](../../documentation-policy.md). ## Resume checklist -Gate prerequisites before any `:test-cover` ([host rule](../../testing/iteration-vocabulary.md#host-rule)): +Gate prerequisites before any `:test-cover` ([host rule](../../testing/change-authoring-workflow.md#host-rule)): -1. [Pre-flight](../../testing/running-e2e.md#pre-flight-is-the-host-clear-to-start): [host-clear probes](../../testing/running-e2e.md#host-clear-probes), [services ready](../../testing/running-e2e.md#2-services-ready), [harness matches validation tier](../../testing/running-e2e.md#3-harness-matches-validation-tier) ([narrowing gate](../../testing/running-e2e.md#harness-narrowing-gate-blocking) — required for **focused** and **area**; not [push harness](#harness)); [serial `:test-cover](../../testing/running-e2e.md#serialized-e2e-dispatch)`; [frozen tree](../../testing/iteration-vocabulary.md#frozen-tree) for `independent-review`. +1. [Pre-flight](../../testing/running-e2e.md#pre-flight-is-the-host-clear-to-start): [host-clear probes](../../testing/running-e2e.md#host-clear-probes), [services ready](../../testing/running-e2e.md#2-services-ready), [harness matches validation tier](../../testing/running-e2e.md#3-harness-matches-validation-tier) ([narrowing gate](../../testing/running-e2e.md#harness-narrowing-gate-blocking) — required for **unit-focused** and **area-focused**; not [push harness](#harness)); [serial `:test-cover](../../testing/running-e2e.md#serialized-e2e-dispatch)`; [frozen tree](../../testing/change-authoring-workflow.md#frozen-tree) for `independent-review`. 2. Guard probes: [SDK runtime verification](pipeline-sdk-support-audit.md#6-runtime-verification-authoritative) + [Phase J protocol](#phase-j-iteration-protocol-strict) below. 3. Coverage deltas: full clean cycle; never trust stale `.ec`/profraw ([coverage stale data](../../testing/coverage-design.md#stale-coverage-data)). @@ -67,7 +67,7 @@ Gate prerequisites before any `:test-cover` ([host rule](../../testing/iteration | **O** | Android Executor remainder | queued | sub-60% after E *(was old M)* | | **P** | Jest-only TS paths | queued | validation branches *(was old N)* | | **Q** | Intractability audit | queued | measured caps per file *(was old O)* | -| **R** | Pre-merge harness restore | queued | **Full** unfocused 3-platform snapshot — [full validation tier](../../testing/running-e2e.md#e2e-validation-tiers-focused-area-full) *(was old P)* | +| **R** | Pre-merge harness restore | queued | **Full** unfocused 3-platform snapshot — [full validation tier](../../testing/running-e2e.md#e2e-validation-tiers-unit-focused-area-focused-full) *(was old P)* | **Compare-types exports:** out of scope until **R**. During **J**, no new `Platform.android` / `Platform.ios` branches for coverage; file drift, fix in **J**, or document SDK limitation. @@ -76,11 +76,11 @@ Gate prerequisites before any `:test-cover` ([host rule](../../testing/iteration ## Current snapshot -**Label:** `j0-remainder-review-complete`; **harness:** full test app (area review used local firestore-only narrowing — not committed) +**Label:** `j0-remainder-review-complete`; **harness:** full test app (area-focused review used local firestore-only narrowing — not committed) **E2e counts (Phase H baseline):** macOS **141**, iOS **146**, Android **146** ✅ *(full app load; re-verify before merge)* -**Area review (2026-06-25):** iOS Pipeline-only harness — **100/100** passing (~135s); Jest pipelines **219/219**. +**area-focused review (2026-06-25):** iOS Pipeline-only harness — **100/100** passing (~135s); Jest pipelines **219/219**. **Next item:** **Commit** J0-6′–J0-9′ batch → **J1** P-001 Android operand coercion. @@ -89,20 +89,20 @@ Gate prerequisites before any `:test-cover` ([host rule](../../testing/iteration | Probe | Code | `implementation_gate` | `review_gate` | `next_work_type` | `validation_tier` | `platform` | Notes | | ----------------------------- | ----------- | --------------------- | ------------- | ---------------- | ----------------- | ---------- | ----------------------------------------------------------------------- | -| **J0-1** `stringRepeat` | `f14092909` | closed | **closed** | — | — | — | Area review 2026-06-25: 100/100/100; stringRepeat unified iOS path | -| **J0-2** `switchOn` | `ae795b96c` | closed | **closed** | — | — | — | Committed 2026-06-25; area review 100/100/100 | -| **J0-3** `trunc` | `138e45690` | closed | **closed** | — | — | — | Area review 2026-06-25: 100/100/100; trunc unified iOS path | -| **J0-4** `conditional` | `cde7b812c` | closed | **closed** | — | — | — | Area review 100/100/100; iOS wire `conditional`; unified e2e | -| **J0-5** `round` | `5b4717d0c` | closed | **closed** | — | — | — | Area review 100/100/100; round unified iOS path (TS-only) | +| **J0-1** `stringRepeat` | `f14092909` | closed | **closed** | — | — | — | area-focused review 2026-06-25: 100/100/100; stringRepeat unified iOS path | +| **J0-2** `switchOn` | `ae795b96c` | closed | **closed** | — | — | — | Committed 2026-06-25; area-focused review 100/100/100 | +| **J0-3** `trunc` | `138e45690` | closed | **closed** | — | — | — | area-focused review 2026-06-25: 100/100/100; trunc unified iOS path | +| **J0-4** `conditional` | `cde7b812c` | closed | **closed** | — | — | — | area-focused review 100/100/100; iOS wire `conditional`; unified e2e | +| **J0-5** `round` | `5b4717d0c` | closed | **closed** | — | — | — | area-focused review 100/100/100; round unified iOS path (TS-only) | | **J0-6** `substring` | `8b76d8bc4` | closed | **closed** | — | — | — | **rnfb-bridge-gap** — reclassified; guard retained | | **J0-7** `timestampAdd` | — | closed | **closed** | — | — | — | **rnfb-bridge-gap** — probe + SDK source; guard retained | | **J0-8** `timestampSubtract` | — | closed | **closed** | — | — | — | **rnfb-bridge-gap** — SDK `timestamp_subtract`; fix iOS wire + receiver | | **J0-9** `arrayGet` | — | closed | **closed** | — | — | — | **rnfb-bridge-gap** — SDK `array_get` receiver wire; guard retained | -| **J0b** | `c27b6f115` | closed | **closed** | — | — | — | Area review 2026-06-25: iOS 100/100; Jest switchOn ok | -| **J0-6′** `substring` | — | **closed** | **closed** | — | area | ios | iOS receiver chain; guard removed; unified e2e | -| **J0-7′** `timestampAdd` | — | **closed** | **closed** | — | area | ios | Same batch — `timestampAdd(amount:unit:)` | -| **J0-8′** `timestampSubtract` | — | **closed** | **closed** | — | area | ios | Same batch — wire `timestamp_subtract` | -| **J0-9′** `arrayGet` | — | **closed** | **closed** | — | area | ios | Same batch — `.arrayGet(_:)` | +| **J0b** | `c27b6f115` | closed | **closed** | — | — | — | area-focused review 2026-06-25: iOS 100/100; Jest switchOn ok | +| **J0-6′** `substring` | — | **closed** | **closed** | — | area-focused | ios | iOS receiver chain; guard removed; unified e2e | +| **J0-7′** `timestampAdd` | — | **closed** | **closed** | — | area-focused | ios | Same batch — `timestampAdd(amount:unit:)` | +| **J0-8′** `timestampSubtract` | — | **closed** | **closed** | — | area-focused | ios | Same batch — wire `timestamp_subtract` | +| **J0-9′** `arrayGet` | — | **closed** | **closed** | — | area-focused | ios | Same batch — `.arrayGet(_:)` | @@ -225,13 +225,13 @@ Earlier: A–E baseline, dead code, gap map, lowering/executor e2e. ### Phase J iteration protocol (strict) -Each J0 probe / J1–J6 bridge step follows **one** serial loop. No overlap. Work types: [iteration vocabulary](../../testing/iteration-vocabulary.md). +Each J0 probe / J1–J6 bridge step follows **one** serial loop. No overlap. Work types: [change authoring workflow](../../testing/change-authoring-workflow.md#work-types). | Step | Work type | Closes gate | Rules | | ----- | -------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **1** | `implementation` | `implementation` | Code/e2e changes; Jest + **focused** tier; `.only` / tight area narrowing OK locally; **no commit** | -| **2** | `independent-review` | `review` | **Frozen tree**; **area** tier; no `.only`; area narrowing only in `tests/app.js` + `tests/globals.js`; serial [host rule](../../testing/iteration-vocabulary.md#host-rule) | +| **1** | `implementation` | `implementation` | Code/e2e changes; Jest + **unit-focused** tier; `.only` / tight area narrowing OK locally; **no commit** | +| **2** | `independent-review` | `review` | **Frozen tree**; **area-focused** tier; no `.only`; area narrowing only in `tests/app.js` + `tests/globals.js`; serial [host rule](../../testing/change-authoring-workflow.md#host-rule) | | **3** | `commit` | `commit` | One focused commit only after `review_gate` closed | @@ -261,13 +261,13 @@ Per [SDK audit §6](pipeline-sdk-support-audit.md): one function/commit; remove **Commit:** `c27b6f115` — consolidate switchOn boolean receiver lowering; area iOS 100/100. -**Why (J0-2 independent-review, 2026-06-25):** `switchOn` landed ~**387 lines** in `RNFBFirestorePipelineNodeBuilder.swift` — a parallel coercion layer alongside existing stack-based lowering. Correctness verified (area e2e green); **maintainability / drift risk** if more one-off paths land before consolidation. +**Why (J0-2 independent-review, 2026-06-25):** `switchOn` landed ~**387 lines** in `RNFBFirestorePipelineNodeBuilder.swift` — a parallel coercion layer alongside existing stack-based lowering. Correctness verified (area-focused e2e green); **maintainability / drift risk** if more one-off paths land before consolidation. -**Goal:** Consolidate J0-added boolean/receiver-chain lowering into **shared** NodeBuilder paths (align with Android `scheduleBooleanReceiverChain` / `EnterObjectBooleanFrame` where feasible). Remove fragile KVC probing where `ExprBridge` extraction exists. **No behavior change** — area-tier Pipeline e2e must stay green (especially `switchOn`, `stringRepeat`, and any probe-specific cases). +**Goal:** Consolidate J0-added boolean/receiver-chain lowering into **shared** NodeBuilder paths (align with Android `scheduleBooleanReceiverChain` / `EnterObjectBooleanFrame` where feasible). Remove fragile KVC probing where `ExprBridge` extraction exists. **No behavior change** — area-focused-tier Pipeline e2e must stay green (especially `switchOn`, `stringRepeat`, and any probe-specific cases). **Scope:** `packages/firestore/ios/RNFBFirestore/RNFBFirestorePipelineNodeBuilder.swift` only (unless consolidation requires Parser touch — stop and note). -**Protocol:** Same [Phase J iteration protocol](#phase-j-iteration-protocol-strict) — `implementation` (**focused**, switchOn + affected probe tests) → `independent-review` (**area**) → `commit`. +**Protocol:** Same [Phase J iteration protocol](#phase-j-iteration-protocol-strict) — `implementation` (**unit-focused**, switchOn + affected probe tests) → `independent-review` (**area-focused**) → `commit`. **Gate for J1–J6:** **J0 complete + J0b committed** + parity registry updated. @@ -319,15 +319,15 @@ Per [SDK audit §6](pipeline-sdk-support-audit.md): one function/commit; remove | **Q** | Intractability audit (map execute, debug gates, codegen caps) | -**R:** revert harness narrowing; **full** unfocused 3-platform run ([full tier](../../testing/running-e2e.md#e2e-validation-tiers-focused-area-full)); final gate before compare-types. +**R:** revert harness narrowing; **full** unfocused 3-platform run ([full tier](../../testing/running-e2e.md#e2e-validation-tiers-unit-focused-area-focused-full)); final gate before compare-types. --- ## Harness - **Push state (committed):** full test app — all `platformSupportedModules` + `require.context` in `tests/app.js`. For merge/CI only; **not** the harness for local `:test-cover` during J–Q. -- **Local `:test-cover`:** must match arbiter `**validation_tier`** — [running e2e § harness + narrowing gate](../../testing/running-e2e.md#harness-narrowing-gate-blocking). `**implementation` → focused** and `**independent-review` → area:** both require [area narrowing](pipeline-implementation-workflow.md#narrowing-during-pipeline-iterations) locally **before** first run even when git has full harness. Revert before **R** (full tier). -- `tests/globals.js` — `RNFBDebug = true` optional for fail-fast +- **Local `:test-cover`:** must match arbiter `**validation_tier`** — [running e2e § harness + narrowing gate](../../testing/running-e2e.md#harness-narrowing-gate-blocking). `**implementation` → unit-focused** and `**independent-review` → area-focused:** both require [area narrowing](pipeline-implementation-workflow.md#pipeline-area-harness) locally **before** first run even when git has full harness. Revert before **R** (full tier). +- `tests/globals.js` — `RNFBDebug = true` optional **locally** for fail-fast; committed default must stay `false` ([running e2e § RNFBDebug](../../testing/running-e2e.md#fast-iteration-test-narrowing)) --- @@ -337,7 +337,7 @@ Per [SDK audit §6](pipeline-sdk-support-audit.md): one function/commit; remove 1. Audit or implement bridge fix with **shared** e2e assertions. 2. Update OKF parity registry (open/close rows). -3. **Phase J:** follow [Phase J iteration protocol](#phase-j-iteration-protocol-strict) — `implementation` (Jest + **focused** tier) → `independent-review` (**area** tier, frozen tree) → `commit`; never commit before `review_gate` closed; never overlap `:test-cover` ([host rule](../../testing/iteration-vocabulary.md#host-rule)). +3. **Phase J:** follow [Phase J iteration protocol](#phase-j-iteration-protocol-strict) — `implementation` (Jest + **unit-focused** tier) → `independent-review` (**area-focused** tier, frozen tree) → `commit`; never commit before `review_gate` closed; never overlap `:test-cover` ([host rule](../../testing/change-authoring-workflow.md#host-rule)). 4. 3-platform e2e on canonical commands ([running-e2e rules 6–7](../../testing/running-e2e.md)). **Phases K–Q (coverage):** diff --git a/okf-bundle/packages/firestore/pipeline-implementation-workflow.md b/okf-bundle/packages/firestore/pipeline-implementation-workflow.md index a4cfcf6e40..8b51684a2f 100644 --- a/okf-bundle/packages/firestore/pipeline-implementation-workflow.md +++ b/okf-bundle/packages/firestore/pipeline-implementation-workflow.md @@ -1,52 +1,45 @@ --- type: Reference -title: Pipeline implementation workflow (compare-types iterations) -description: Canonical workflow for closing one Firestore Pipelines compare-types gap — gap analysis, baseline coverage, implementation, independent review, documentation, and commit. +title: Firestore Pipelines implementation workflow +description: Pipeline-specific artifacts for compare-types exports, serialization, coverage snapshots, and area harness setup — extends the cross-package change authoring workflow. tags: [firestore, pipelines, compare-types, workflow, coverage] -timestamp: 2026-06-25T12:00:00Z +timestamp: 2026-06-26T00:00:00Z --- -# Pipeline implementation workflow +# Firestore Pipelines implementation workflow -Single source for one `.github/scripts/compare-types/configs/firestore-pipelines.ts` missing export. Other docs/prompts link here. - -One iteration = one compare-types backlog entry + one focused commit. +Pipeline-specific requirements for one `.github/scripts/compare-types/configs/firestore-pipelines.ts` export (or equivalent parity item). **Shared loop:** [change authoring workflow](../../testing/change-authoring-workflow.md). **Policy:** [OKF documentation and commit policy](../../documentation-policy.md). -## Architecture and semantics (read first) +## Read first | Topic | Document | |-------|----------| -| Bridge design, subqueries, coercion, native coverage strategy | [Pipelines implementation design](pipelines.md) | -| Coverage and parity phase tracker | [Pipeline coverage work queue](pipeline-coverage-work-queue.md) | -| Work types, gates, tiers | [Iteration vocabulary](../../testing/iteration-vocabulary.md) | -| E2e commands, fast iteration hacks, handoff rules | [Running e2e tests](../../testing/running-e2e.md) | -| Coverage policy, per-file reading, refactor-vs-test | [Coverage design](../../testing/coverage-design.md) | -| All validation commands | [Validation checklist](../../testing/validation-checklist.md) | -| Serialization tests per export | [Serialization testing](serialization-testing.md) | +| **Change authoring loop** | [change-authoring-workflow.md](../../testing/change-authoring-workflow.md) | +| Bridge design, subqueries, coercion | [pipelines.md](pipelines.md) | +| Live gate snapshots | [pipeline-coverage-work-queue.md](pipeline-coverage-work-queue.md) | +| Work types, tiers, gates | [change-authoring-workflow.md](../../testing/change-authoring-workflow.md); term ids: [iteration-vocabulary.md](../../testing/iteration-vocabulary.md) | +| E2e commands | [running-e2e.md](../../testing/running-e2e.md) | +| Coverage policy | [coverage-design.md](../../testing/coverage-design.md) | +| Validation commands | [validation-checklist.md](../../testing/validation-checklist.md) | +| Serialization tests per export | [serialization-testing.md](serialization-testing.md) | | Compare-types machinery | `.github/scripts/compare-types/README.md` | -## Hard gates (blocking) +## Pipeline hard gates -Commit/handoff only when **all** pass: +In addition to [change authoring gates](../../testing/change-authoring-workflow.md#gates): | Gate | Requirement | |------|-------------| -| Gap analysis | Selected export confirmed against firebase-js-sdk; platform support verified; semantic dependencies identified | -| Baseline e2e | Full `Pipeline.e2e.js` on macOS/iOS/Android, **area** tier; no `.only` | +| Gap analysis | Export confirmed against firebase-js-sdk; platform support verified; semantic dependencies identified | | Baseline coverage | `snapshot-pipeline-coverage.sh before-` stdout recorded | -| Implementation | Public API, lowering, Jest, serialization matrix, consumer type-test, **focused** e2e ([focused tier](../../testing/running-e2e.md#e2e-validation-tiers-focused-area-full)) | -| Review e2e | Full `Pipeline.e2e.js` on all platforms, **area** tier; no `.only` | -| Review coverage | `after-` snapshot; [coverage policy](../../testing/coverage-design.md#coverage-as-completion-signal) | -| Validation | Full [Validation checklist](../../testing/validation-checklist.md) + OKF review § | -| Documentation | User docs + OKF bundle updates (decisions/learnings consolidated) | -| Commit | One focused commit; area narrowing and `.only` never staged | -| Pre-merge | **Full** unfocused 3-platform e2e; revert all narrowing ([full tier](../../testing/running-e2e.md#e2e-validation-tiers-focused-area-full)) | - -**Coverage acceptance:** [Coverage design § completion signal](../../testing/coverage-design.md#coverage-as-completion-signal) and [expectations (policy)](../../testing/coverage-design.md#coverage-expectations-policy). +| Implementation | Public API, lowering, Jest, [serialization-testing.md](serialization-testing.md), consumer type-test, **unit-focused** e2e | +| Review coverage | `after-` snapshot; [coverage completion signal](../../testing/coverage-design.md#coverage-as-completion-signal) | +| Review e2e | Full `Pipeline.e2e.js` on macOS, iOS, Android — **area-focused** tier; no `.only` | +| Compare-types | Remove `missingInRN` entry when RNFB shape matches firebase-js-sdk | -### Coverage snapshots (pipeline) +### Coverage snapshots After full 3-platform e2e: @@ -55,79 +48,9 @@ bash scripts/snapshot-pipeline-coverage.sh before- bash scripts/snapshot-pipeline-coverage.sh after- ``` -Paste stdout in the iteration report. - -## Implementation model - -Run gap analysis + baseline first. Keep `implementation` and `independent-review` in **separate contexts** ([frozen tree](../../testing/iteration-vocabulary.md#frozen-tree) for review). - -```mermaid -flowchart TD - A["gap-analysis"] --> B["baseline-capture"] - B --> C["implementation"] - C --> D["independent-review"] - D --> E["documentation"] - E --> F["commit"] -``` - -| Step | Work type | Validation tier | OKF docs to load | -|------|-----------|-----------------|------------------| -| 1 | `gap-analysis` | — | `firestore-pipelines.ts`; compare-types README | -| 2 | `baseline-capture` | `area` | [running-e2e](../../testing/running-e2e.md); `before-` snapshot | -| 3 | `implementation` | `focused` | [pipelines.md](pipelines.md); [serialization-testing.md](serialization-testing.md); [narrowing § below](#narrowing-during-pipeline-iterations) | -| 4 | `independent-review` | `area` | [validation-checklist.md](../../testing/validation-checklist.md); [narrowing § below](#narrowing-during-pipeline-iterations) | -| 5 | `documentation` | — | `docs/firestore/pipelines/` conventions below | -| 6 | `commit` | — | Commit scope below | - -### `implementation` work type - -- Selected export name and firebase-js-sdk reference paths (`node_modules/@firebase/firestore/pipelines/pipelines.d.ts`, `dist/pipelines.esm.js`) -- Follow [pipelines.md](pipelines.md) patterns only; no one-export abstraction -- **Fast iteration:** **focused** tier — per [narrowing § below](#narrowing-during-pipeline-iterations) and [running-e2e](../../testing/running-e2e.md#e2e-validation-tiers-focused-area-full) -- **Serialization blocking:** [serialization-testing.md](serialization-testing.md) all four checks -- Deliverable: diff, unit/e2e tests, consumer type-test updates; closes `implementation_gate`; **do not commit** - -### `independent-review` work type - -On a **frozen tree** ([iteration vocabulary](../../testing/iteration-vocabulary.md#frozen-tree)): - -- **Revert** `it.only` / `describe.only`; run **area**-tier e2e — area narrowing may remain per [narrowing § below](#narrowing-during-pipeline-iterations) -- Run the full [Validation checklist](../../testing/validation-checklist.md) (no `:test-cover-reuse`) -- Run full `Pipeline.e2e.js` e2e + coverage on macOS, iOS, Android; record `after-` snapshot -- Compare coverage to baseline; coverage on touched files must rise until intractable limits or plateau → refactor -- Deliverable: pass/fail, commands, coverage delta; closes `review_gate`; **do not commit** - -If review fails, return to `implementation`, then repeat `independent-review`. - -### iOS guard probe iterations - -iOS guard probes/bridge fixes use the same split; stricter serial gate: [work queue runtime guard protocol](pipeline-coverage-work-queue.md#phase-j-iteration-protocol-strict) + [running-e2e serial policy](../../testing/running-e2e.md#serialized-e2e-dispatch). - -Guard probes: one function per commit; no batching. [Pre-flight](../../testing/running-e2e.md#pre-flight-is-the-host-clear-to-start) (by reference). Commands: [running-e2e.md](../../testing/running-e2e.md). - -**Live gate status:** [pipeline-coverage-work-queue](pipeline-coverage-work-queue.md) (ephemeral tracker — see [documentation policy](../../documentation-policy.md)). - -### Narrowing during pipeline iterations - -Generic definitions: [test narrowing](../../testing/running-e2e.md#fast-iteration-test-narrowing), [validation tiers](../../testing/running-e2e.md#e2e-validation-tiers-focused-area-full), [harness vs tier](../../testing/running-e2e.md#3-harness-matches-validation-tier), **[harness narrowing gate (blocking)](../../testing/running-e2e.md#harness-narrowing-gate-blocking)**. Pipeline rules: - -**Before the first `:test-cover` in `implementation` or `independent-review`:** apply area narrowing below even if the branch commit has full harness ([work queue § harness](pipeline-coverage-work-queue.md#harness)). Full app load is **full** tier only (phase **R**). A green run on the committed full harness does **not** close `implementation_gate` or `review_gate`. - -| Kind | `implementation` (**focused**) | `independent-review` (**area**) | `pre-merge-validation` (**full**) | `commit` | -|------|--------------------------------|----------------------------------|-----------------------------------|----------| -| **Area narrowing** (`tests/app.js`, `tests/globals.js`) | **Required** before `:test-cover` | **Required** before `:test-cover` — full `Pipeline.e2e.js`, no `.only` | Revert — all modules | Never | -| **Single-test** (`it.only`) | Allowed | Revert | Revert | Never | -| **Single-suite** (`describe.only`) | Allowed | Revert | Revert | Never | +## Compare-types gap analysis -**Area setup (required for both focused and area tiers):** firestore-only `platformSupportedModules` + `require('../packages/firestore/e2e/Pipeline.e2e.js')`; `RNFBDebug = true`. - -**Sanity check:** ~**100** passing per platform when only `Pipeline.e2e.js` loads. Pass counts in the **hundreds or thousands** mean full app load — stop and re-apply narrowing ([running-e2e § gate](../../testing/running-e2e.md#harness-narrowing-gate-blocking)). - -## Step 1 — Compare-types gap analysis - -### Work queue: `missingInRN` - -Primary queue: **`missingInRN`** in `.github/scripts/compare-types/configs/firestore-pipelines.ts`. +Primary backlog: **`missingInRN`** in `.github/scripts/compare-types/configs/firestore-pipelines.ts`. 1. Read the config in **file order**. 2. Select first still-relevant `missingInRN` entry unless a semantic companion must ship first. @@ -139,11 +62,11 @@ Primary queue: **`missingInRN`** in `.github/scripts/compare-types/configs/fires 5. Run `yarn compare:types` to confirm the entry is still undocumented drift (not stale). 6. Note ordering dependencies; do not skip ahead without user approval. -`compare-types` config is backlog, not permission. Remove `missingInRN` only when RNFB matches firebase-js-sdk shape. +`compare-types` config is backlog, not permission. ### Shape differences: `differentShape` -`differentShape` is not the default queue. Each entry needs a strict, intractable RN technical reason (bridge, platform SDK, Hermes/Metro, etc.). +Each entry needs a strict, intractable RN technical reason (bridge, platform SDK, Hermes/Metro, etc.). - If indefensible, align RNFB to firebase-js-sdk and remove the entry. - Do not use `differentShape` for avoidable drift (formatting, naming, optional params). @@ -151,37 +74,31 @@ Primary queue: **`missingInRN`** in `.github/scripts/compare-types/configs/fires `extraInRN`: justify RN-specific surface or remove. -## Step 2 — Coverage baseline (before coding) +## Pipeline `implementation` -Skip only when continuing immediately after same-worktree `after-` snapshot. +- Follow [pipelines.md](pipelines.md) patterns only; no one-export abstraction. +- **Serialization blocking:** [serialization-testing.md](serialization-testing.md) all four checks. +- Trace patterns in `packages/firestore/lib/pipelines/`, `internal.ts`, native parsers, web bridge. +- Implement public API matching firebase-js-sdk **exactly** (including lowercase string unions — native may normalize internally). +- Add Jest, consumer `type-test.ts`, serialization matrix, e2e cases. +- **Unit-focused** tier per [change authoring § implementation inner loop](../../testing/change-authoring-workflow.md#implementation-inner-loop) and [pipeline area harness](#pipeline-area-harness) below. -**Retroactive baseline:** If implementation was already committed without baseline numbers, check out the parent commit, apply harness only if needed for e2e load, run full baseline, snapshot `before-`, return to HEAD. +## Pipeline `independent-review` -1. Revert leftover `.only`; area narrowing allowed for pipeline baseline. -2. [Pre-flight](../../testing/running-e2e.md#pre-flight-is-the-host-clear-to-start) and prerequisites ([Rules §1–2](../../testing/running-e2e.md#rules)); rebuild native if needed. -3. Full pipeline e2e + coverage on **macOS, iOS, Android**; no reuse variants. -4. `bash scripts/snapshot-pipeline-coverage.sh before-` — paste full stdout into the iteration report. +On a **frozen tree** — full [change authoring § independent-review](../../testing/change-authoring-workflow.md#independent-review), plus: -Start implementation only after green 3-platform baseline. +- Full `Pipeline.e2e.js` e2e + coverage on all platforms; `after-` snapshot. +- Compare coverage to baseline; touched-file coverage must rise until intractable limits or plateau → refactor. -## Step 3 — `implementation` +## Pipeline area harness -1. Trace patterns in `packages/firestore/lib/pipelines/`, `internal.ts`, native parsers, web bridge. -2. Implement public API matching firebase-js-sdk **exactly** (including lowercase string unions — native may normalize internally). -3. Add Jest, consumer `type-test.ts`, serialization matrix, e2e cases. -4. Use focused narrowing during development; area narrowing may persist — [narrowing](#narrowing-during-pipeline-iterations). -5. Re-run focused-tier e2e per fix with clean [pre-flight](../../testing/running-e2e.md#pre-flight-is-the-host-clear-to-start). +Extends [change authoring § harness narrowing](../../testing/change-authoring-workflow.md#harness-narrowing). **Mechanics:** [running e2e § area harness — two platform blocks](../../testing/running-e2e.md#tests-app-js-area-harness). -## Step 4 — `independent-review` +**Area setup (required for `unit-focused` and `area-focused` tiers):** firestore-only `platformSupportedModules` with **both** `if (Platform.other)` and `if (!Platform.other)` disabled (`if (false && …)` on each) or trimmed inside each block; load `require('../packages/firestore/e2e/Pipeline.e2e.js')` or full firestore `require.context` per scope; **`RNFBDebug = true`** locally per [running e2e § fail-fast](../../testing/running-e2e.md#fail-fast-rnfbdebug-and-sub-suite-narrowing) — **never commit**. Revert **both** platform blocks before **full** tier or `commit`. -1. Remove `it.only` / `describe.only`. -2. Keep area narrowing if applied — [narrowing §](#narrowing-during-pipeline-iterations). -3. Full [Validation checklist](../../testing/validation-checklist.md). -4. Full `Pipeline.e2e.js` e2e + coverage on all platforms; `after-` snapshot. -5. Compare before/after; touched-file coverage must rise until intractable limit. Plateau below limit → refactor. -6. Remove `firestore-pipelines.ts` entry when shape matches. +**Sanity check:** ~**100** passing per platform when only `Pipeline.e2e.js` loads. Pass counts in the **hundreds or thousands** mean full app load — stop and re-apply narrowing ([running-e2e § gate](../../testing/running-e2e.md#harness-narrowing-gate-blocking)). -## Step 5 — `documentation` +## Pipeline `documentation` Per export, same commit: @@ -190,36 +107,21 @@ Per export, same commit: - Page or section under `docs/firestore/pipelines/` - Parity table row on pipelines overview - `docs.json` sidebar entry for new pages -- `yarn lint:markdown` and `yarn lint:spellcheck` +- [Validation checklist § lint and formatting](../../testing/validation-checklist.md#lint-and-formatting) — markdown/spellcheck when docs changed **OKF bundle maintenance** -Durable learnings, same commit: - -- Update `pipelines.md`, `serialization-testing.md`, or this workflow for changed bridge behavior/gotchas/rules. +- Update `pipelines.md`, `serialization-testing.md`, or this file for changed bridge behavior/gotchas/rules. - Cross-cutting testing/coverage learnings go in `okf-bundle/testing/`. -- Move ad-hoc notes into OKF; fix conflicting linked docs. - Record non-obvious choices in OKF, not only commit messages. -Skills/docs link to OKF; do not restate. - -## Step 6 — `commit` - -Message: +## Pipeline `commit` ```text feat(firestore): expose pipeline ``` -**Never stage:** area narrowing in `tests/app.js` / `tests/globals.js`, any `.only`, skill snapshot logs. - -Verify before commit: - -```bash -git status -git diff --stat -rg '\.only\(' packages/firestore/e2e/ -``` +**Never stage:** area narrowing in `tests/app.js` / `tests/globals.js`, any `.only`. ## Gotchas @@ -229,32 +131,4 @@ rg '\.only\(' packages/firestore/e2e/ - **Two native expression builders per platform** — edit the live path; use coverage to disambiguate ([pipelines.md § Cross-platform e2e](pipelines.md#two-expression-builders-per-platform--edit-the-live-one)). - iOS profraw pull can flake — retry once before treating as environment failure. -## Iteration report template - -```markdown -# Pipeline iteration: - -## Gap analysis -- why this item was next -- companion exports (if any) - -## Baseline -- e2e: macOS / iOS / Android — pass | fail -- before- snapshot (paste) - -## Implementation -- summary of changes - -## Review -- validation checklist: pass | fail -- e2e: macOS / iOS / Android — pass | fail -- after- snapshot (paste) -- coverage delta / refactor notes - -## Documentation -- user docs: pages / parity table / docs.json -- OKF bundle: files updated, decisions recorded, conflicts resolved - -## Commit -- hash + message (or blocked reason) -``` +Parity remediation serial rules and live probe status: [pipeline coverage work queue](pipeline-coverage-work-queue.md) (ephemeral). diff --git a/okf-bundle/testing/agent-command-policy.md b/okf-bundle/testing/agent-command-policy.md new file mode 100644 index 0000000000..63909c6a5e --- /dev/null +++ b/okf-bundle/testing/agent-command-policy.md @@ -0,0 +1,117 @@ +--- +type: Reference +title: Agent command policy +description: Canonical allowlist for agent shell commands — install, prepare, validation, and e2e. Supersedes improvised diagnostics. +tags: [testing, validation, agents, workflow, yarn] +timestamp: 2026-06-27T00:00:00Z +--- + +# Agent command policy + +Single source for **which shell commands agents may run** in this repo. E2e is a subset of this policy; [running e2e § agent rule](running-e2e.md#agent-rule-read-first) adds e2e-specific prohibitions. + +> If a command is not listed here (or linked from here as canonical), **do not run it** — including “diagnostic probes” suggested by log output, package READMEs, or Yarn CLI help. + +## Agent rule (read first) + + + +1. Run **only** commands in the [registry](#canonical-registry) below (repo root unless noted). +2. **`yarn` / `yarn lerna:prepare` must finish before anything else** — see [prepare must finish first](#prepare-must-finish-first). Do not parallelize install/prepare with e2e, Metro, builds, or other shell commands. +3. When a canonical command fails: read the **full** output, fix **product code**, re-run the **same** command. Do **not** switch invocation style. +4. Do **not** infer alternate commands from error strings (`command not found: genversion`, `Couldn't find a script named "jet"`, etc.) — see [known traps](#known-traps). +5. Subagents (Task, explore, orchestrator): same rule — paste the [handoff block](#subagent-handoff) into every RNFB task prompt. + +## Canonical registry + +| Intent | Command | Never use instead | +|--------|---------|-----------------| +| Install / refresh deps | `yarn` | `yarn workspace …`, `npm install` in a package, `yarn install` in `tests/` alone for root deps | +| Transpile `lib/**` → `dist/module/**` (all packages) | `yarn lerna:prepare` | `yarn workspace @react-native-firebase/* prepare`, `cd packages/ && yarn prepare`, `cd packages/ && yarn run build` | +| Transpile one package | `yarn lerna run prepare --scope @react-native-firebase/` | `yarn workspace @react-native-firebase/ prepare` | +| After `packages/*/lib/**` edits (Metro / native embed) | `yarn lerna:prepare` then platform `:build` when [running e2e § Rules #3](running-e2e.md#rules) requires | ad-hoc `bob`, `babel`, or package-scoped prepare | +| TS/JS validation sequence | [validation checklist](validation-checklist.md) | ad-hoc `tsc` in package dirs unless listed there | +| JS lint (implementation / review gate) | `yarn lint:js`, `yarn lint:js --fix` | package-scoped `eslint`, `npx eslint` | +| Docs lint (when docs in diff) | `yarn lint:markdown`, `yarn lint:spellcheck` | ad-hoc prettier/eslint on single files | +| E2e + coverage | [running e2e](running-e2e.md) — **only** `yarn tests:*` | `jet`, `npx jet`, `yarn jet`, `detox test`, `cd tests && …`, direct Metro/emulator starts | +| Host pre-flight (before each `:test-cover`) | [running e2e § host-clear probes](running-e2e.md#host-clear-probes) | `pgrep`, polling `:8090`, spawn probes of Jet/Detox | + +### Prepare / transpile (detail) + +`yarn lerna:prepare` runs each package's **`prepare`** script (`build` → `compile` via react-native-builder-bob). That is what produces **`dist/module/**`** consumed by Metro and native embed paths. + +- **`yarn compile`** (package script) is **not** a standalone agent entrypoint — it is invoked **inside** `prepare` via lerna. Do not run `cd packages/ && yarn compile` for handoff unless [validation checklist](validation-checklist.md) explicitly adds an exception (none today). +- **`yarn`** at repo root runs `postinstallDev` → `yarn prepare && yarn lerna:prepare`; a fresh install already transpiles. Re-run **`yarn lerna:prepare`** after `lib/**` edits without reinstalling. + + + +### Prepare must finish first (blocking) + +**`yarn`**, **`yarn lerna:prepare`**, and **`yarn lerna run prepare --scope …`** are **blocking foreground** commands. Wait for the shell to return **exit code 0** before starting **any** other command — including in the same agent turn via parallel tool calls. + +| Do not start until prepare exits 0 | Why | +|-----------------------------------|-----| +| `yarn tests:*` (e2e, packager, build) | Metro and native embed read **`dist/module/**`**, not `lib/**` — partial prepare → missing modules, stale bundles | +| `yarn tests:packager:jet-reset-cache` | Reset after prepare, not during it | +| `yarn tsc:compile`, Jest, `compare:types` | May read transpiled output or assume `dist/` is current | +| Another `yarn` / scoped prepare | Overlapping Nx/Lerna runs race on `dist/` | + +**Agent rule:** one prepare invocation per message batch; wait for completion; then run the next step (Metro restart if needed → pre-flight → `:test-cover`). [Running e2e § prepare completion gate](running-e2e.md#prepare-completion-gate-blocking) is the e2e-side mirror of this rule. + +**Symptoms when violated:** `Cannot find module '…/dist/module/…'`, Metro 500 on bundle, e2e failures before tests run, or green Metro `/status` while the app loads a half-written `dist/`. + +## When install or prepare fails + +1. Re-run from repo root: **`yarn`** or **`yarn lerna:prepare`** (full log — do not truncate). +2. Note the **first** Nx/Lerna project that failed (e.g. `@react-native-firebase/functions:prepare`). +3. Fix **product code** in that package (TypeScript errors, missing exports, etc.). +4. Re-run **`yarn lerna:prepare`** — same command, same cwd. +5. Do **not** “verify tooling” with `yarn workspace … prepare`, `yarn bin …`, or package-scoped `yarn run build` — Yarn 4 uses **different PATH** for those invocations ([genversion trap](#genversion--prepare-paths)). + +## Forbidden (always) + +| Command | Why | +|---------|-----| +| `yarn workspace @react-native-firebase/* prepare` (and variants) | Not canonical; breaks root devDependency binary resolution | +| `cd packages/ && yarn prepare` / `yarn run build` | Same trap; not the postinstall / lerna code path | +| `yarn jet`, `npx jet`, `cd tests && yarn jet …` | [E2e agent rule](running-e2e.md#agent-rule-read-first) | +| `detox test`, `cd tests && detox …` | E2e agent rule | +| Ad-hoc Metro / emulator start | Use `yarn tests:packager:jet`, `yarn tests:emulator:start` | +| Spawn / PATH probes to “test” Jet or genversion | Log triage only; fix product code and re-run canonical command | + +## Known traps + + + +### genversion / prepare paths + +- **`genversion` exists** at root `node_modules/.bin` after `yarn`. +- **`yarn lerna:prepare`** (and `yarn install` → `postinstallDev`) runs prepare via Nx with root toolchain on PATH → bare `genversion` in package `"build"` scripts **works**. +- **`yarn workspace … prepare`** or **`cd packages/foo && yarn run build`** does **not** expose root-only devDependencies → `command not found: genversion`. That is **not** corrupt `node_modules`; do **not** patch scripts with `yarn run -T genversion` unless deliberately changing repo policy on `main`. + +### Jet + +- **`yarn jet --help`** working or failing in `tests/` is **not** a valid e2e or install gate. +- Jet is started **internally** by `yarn tests::test-cover`. Stale `:8090` → [pre-flight recovery](running-e2e.md#pre-flight-recovery), then re-run the same `:test-cover` command. + +## Subagent handoff + +Paste into Task / explore / work-queue prompts: + +```text +RNFB agent command policy: okf-bundle/testing/agent-command-policy.md ONLY. +E2e: okf-bundle/testing/running-e2e.md yarn tests:* ONLY. +Never: yarn workspace prepare, yarn jet, npx jet, cd packages/* && yarn prepare/build for diagnostics. +Prepare/install: yarn or yarn lerna:prepare must exit 0 before ANY other command — never parallelize with e2e/Metro/build. +Area harness: okf-bundle/testing/running-e2e.md#tests-app-js-area-harness — edit BOTH Platform.other and !Platform.other blocks; revert both before commit. +On failure: fix product code, re-run the same canonical command. +``` + +## Related docs + +| Topic | Owner | +|-------|--------| +| E2e commands, pre-flight, tiers | [running-e2e.md](running-e2e.md) | +| Handoff validation sequence | [validation-checklist.md](validation-checklist.md) | +| Work types and gates | [change-authoring-workflow.md](change-authoring-workflow.md) | +| Doc / commit policy | [documentation-policy.md](../documentation-policy.md) | diff --git a/okf-bundle/testing/change-authoring-workflow.md b/okf-bundle/testing/change-authoring-workflow.md new file mode 100644 index 0000000000..3314913b42 --- /dev/null +++ b/okf-bundle/testing/change-authoring-workflow.md @@ -0,0 +1,217 @@ +--- +type: Reference +title: Change authoring workflow +description: Canonical cross-package loop for verified product changes — baseline, unit-focused implementation, area-focused review, documentation, commit, and pre-merge validation. +tags: [testing, validation, workflow, implementation, review] +timestamp: 2026-06-26T00:00:00Z +--- + +# Change authoring workflow + +Single source for **how to author and verify a product change** in RNFB (bug fix, feature, parity, coverage). Package workflows add artifacts; work queues add ephemeral gate state — neither restates this loop. + +**Policy:** [OKF documentation and commit policy](../documentation-policy.md). **Terms:** [iteration vocabulary](iteration-vocabulary.md). + +## Primary loop + +```mermaid +flowchart TD + START([Pick change scope]) --> GA{Need feasibility /
semantics check?} + GA -->|yes| GAP["gap-analysis
tier: none"] + GA -->|no| BC{Need before snapshot
or area-focused e2e baseline?} + GAP --> BC + + BC -->|yes| BASE["baseline-capture
tier: area-focused"] + BC -->|no| IMPL + BASE --> IMPL + + IMPL["implementation
tier: unit-focused
Jest + narrow e2e loop"] + IMPL --> IG{implementation gate
green?} + IG -->|no| IMPL + IG -->|yes| REV + + REV["independent-review
tier: area-focused
frozen tree"] + REV --> RG{review gate
green?} + RG -->|blocking findings| IMPL + RG -->|yes| DOC + + DOC{User-facing or
OKF durable updates?} + DOC -->|yes| DOCS["documentation
tier: none"] + DOC -->|no| COMMIT + DOCS --> COMMIT + + COMMIT["commit
tier: none"] + COMMIT --> PM{Branch ready
to merge?} + PM -->|yes| FULL["pre-merge-validation
tier: full"] + PM -->|no| END([Hand off / next item]) + FULL --> END +``` + +## Work types + +| Work type | When | Validation tier | Product edits | Commit | +|-----------|------|-----------------|---------------|--------| +| `gap-analysis` | Unclear feasibility, export shape, platform support | none | read-only | no | +| `baseline-capture` | Need before metrics or area-focused e2e on the item | `area-focused` | harness narrow OK locally | no | +| `implementation` | Author fix/feature + tests | `unit-focused` | yes | no | +| `independent-review` | Verify frozen diff | `area-focused` | no — [frozen tree](#frozen-tree) | no | +| `documentation` | User docs + durable OKF updates | none | docs only | no | +| `commit` | Gates closed for the item | none | staging only | yes | +| `pre-merge-validation` | Branch merge gate | `full` | revert narrowing first | no | + +**Commands per work type:** [validation checklist](validation-checklist.md) — link only; do not duplicate here. + +## Validation tiers + +Tier id strings: [iteration vocabulary § validation tier identifiers](iteration-vocabulary.md#validation-tier-identifiers). + +```mermaid +flowchart LR + subgraph unitFocused ["unit-focused — implementation"] + F1[Area harness narrowing required] + F2[Jest + package-scoped tests] + F3[".only OK locally"] + F4[Fast e2e subset] + F5[Never commit narrowing or .only] + end + + subgraph areaFocused ["area-focused — baseline-capture / independent-review"] + A1[Area harness narrowing required] + A2[Full loaded package spec] + A3[No .only] + A4[Frozen tree for review] + end + + subgraph full ["full — pre-merge-validation"] + P1[Revert all narrowing] + P2[All modules / full app] + P3[Once per branch before merge] + end +``` + +E2e scope, pre-flight, and harness gate: [running e2e § agent rule](running-e2e.md#agent-rule-read-first) (canonical commands only), [validation tiers](running-e2e.md#e2e-validation-tiers-unit-focused-area-focused-full), [harness narrowing gate](running-e2e.md#harness-narrowing-gate-blocking). + +**Command rule:** Agents run **only** [agent command policy](agent-command-policy.md) allowlisted commands for install, prepare, and validation — no improvised `yarn workspace … prepare`, `yarn jet`, or package-scoped build probes. + +## Gates + +| Gate | Closes when | +|------|-------------| +| `implementation` | `implementation` work type complete — code plus **unit-focused**-tier checks green; [static analysis](validation-checklist.md#lint-and-formatting) green on the diff | +| `review` | `independent-review` complete — **area-focused**-tier checks green on frozen tree; applicable [validation checklist](validation-checklist.md) rows green (including static analysis) | +| `commit` | Durable commit exists for the item | + +**Trust rule:** Code on disk or in git with `review` still **open** is unverified until `independent-review` closes the gate. + +If review finds blocking issues, return to **`implementation`** (`unit-focused`), then repeat **`independent-review`** (`area-focused`). + +## Frozen tree + +Required for **`independent-review`** and for any `:test-cover` run that closes the **`review`** gate: + +- No edits to `packages/**`, `tests/**` (except reverting `.only`), or bundle-affecting OKF docs during the run. +- Wait for or cancel in-flight runs before editing again. + +Keep **`implementation`** and **`independent-review`** in separate passes. E2e enforcement during runs: [running e2e § rules](running-e2e.md#rules). + +## Host rule + +On a shared dev host during change authoring: + +- One `:test-cover` at a time — never overlap **unit-focused**-tier and **area-focused**-tier runs. +- Every run starts from [running e2e § pre-flight](running-e2e.md#pre-flight-is-the-host-clear-to-start) (host-clear probes, services, harness tier). +- Use only [canonical e2e commands](running-e2e.md#rules). Stalled runs → [stalled run detection](running-e2e.md#stalled-run-detection). + +## `implementation` inner loop + +```mermaid +flowchart TD + P0[Pre-flight: host clear, services, harness tier] + P1[Edit product code + tests] + P2[Jest — package or scoped paths] + P3{Native touched?} + P3 -->|no| P4[macOS e2e when TS/runtime path] + P3 -->|yes| P5["native rebuild + platform e2e"] + P4 --> P6{Green?} + P5 --> P6 + P6 -->|no| P1 + P6 -->|yes| STATIC[Static analysis — validation checklist § lint] + STATIC --> DONE([Close implementation gate]) + P0 --> P1 +``` + +**Host rule:** one `:test-cover` at a time; never overlap **unit-focused** and **area-focused** tiers on one host ([§ host rule](#host-rule)). + +**Static analysis before handoff:** Before closing the **`implementation`** gate, run the [validation checklist § lint and formatting](validation-checklist.md#lint-and-formatting) rows (`yarn lint:js`; `yarn lint:markdown` / `yarn lint:spellcheck` when docs changed). Fix violations in product code — do not hand off with lint failures. Command list lives only in the checklist; do not duplicate here. + +Step detail: [running e2e § unit-focused iteration loop](running-e2e.md#unit-focused-tier-iteration-loop). + + + +### E2e diagnosis escalation + +When **`unit-focused`** e2e fails and product cause is unclear: + +1. Confirm [pre-flight](running-e2e.md#pre-flight-is-the-host-clear-to-start) was complete ([prepare completion gate](running-e2e.md#prepare-completion-gate-blocking) when `lib/**` changed, host-clear probes, services, area narrowing, **`RNFBDebug = true`**). +2. If the **same failure repeats** on back-to-back runs with no assertion progress and the host is known clean → **sub-suite narrow** ([running e2e § fail-fast](running-e2e.md#fail-fast-rnfbdebug-and-sub-suite-narrowing)): one spec file or `describe.only` on the failing band (e.g. aggregate `count()` / `average()` / `sum()` only). Still **unit-focused**; never commit narrowing. +3. If sub-suite runs still fail without actionable assertion text → add **temporary native instrumentation** (NSLog, `adb logcat` tags, etc.) on the code path under test; use [running e2e § diagnosing hangs](running-e2e.md#diagnosing-hangs) for log commands. **Remove instrumentation before `commit`** and before **`area-focused`** gate closure on a frozen tree. +4. Do not treat Jet WS disconnect / orchestration timeout alone as product failure — [stalled run detection](running-e2e.md#stalled-run-detection) and pre-flight recovery first. + +This escalation applies to **any** change authoring item, not only namespace removal. Work queues record outcomes; they do not restate this loop. + +## `independent-review` + +On a **frozen tree**: + +1. Revert all `.only`. +2. Keep area narrowing; run **area-focused**-tier e2e for loaded package spec(s) on [**every required platform**](running-e2e.md#platform-coverage-gate-blocking) (serial; pre-flight each run). +3. Run applicable [validation checklist](validation-checklist.md) rows — **blocking:** [static analysis § lint and formatting](validation-checklist.md#lint-and-formatting) (`yarn lint:js` on the frozen tree; markdown/spellcheck when docs touched); `yarn reference:api` when public surface changed. For packages registered in `compare:types`, `yarn compare:types` is a **blocking review gate**: the touched package must have zero undocumented or stale differences before `review_gate` closes. If the global command fails on unrelated registered packages, record/fix that drift in the work queue; do not treat an unrelated failure as permission to skip the touched package's type-parity check. +4. If the package workflow requires coverage: [coverage design § completion signal](coverage-design.md#coverage-as-completion-signal). +5. Outcome closes **review gate** or returns to **`implementation`**. + +Keep **`implementation`** and **`independent-review`** in separate passes ([§ frozen tree](#frozen-tree)). + +## Harness narrowing + +**Before the first `:test-cover` at `unit-focused` or `area-focused` tier:** apply package area narrowing in `tests/app.js` / `tests/globals.js` even when the branch commit has full harness. **`tests/app.js` has two platform populate blocks** — [running e2e § area harness (two platform blocks)](running-e2e.md#tests-app-js-area-harness): edit **both** `if (Platform.other)` and `if (!Platform.other)` (or disable both with Pattern A). Set **`RNFBDebug = true`** locally in `tests/globals.js` ([running e2e § fail-fast](running-e2e.md#fail-fast-rnfbdebug-and-sub-suite-narrowing)). Full app load is **`full`** tier only. + +| Kind | `implementation` (**unit-focused**) | `independent-review` (**area-focused**) | `pre-merge-validation` (**full**) | `commit` | +|------|-------------------------------------|------------------------------------------|-----------------------------------|----------| +| **Area narrowing** | Required before `:test-cover` | Required before `:test-cover` | Revert — all modules | Never | +| **`RNFBDebug = true`** | Required locally before `:test-cover` | Required locally before `:test-cover` | Revert — `false` | Never | +| **Single-test** (`.only`) | Allowed (diagnosis) | Revert | Revert | Never | +| **Single-suite** (`describe.only` / one spec file) | Allowed (diagnosis only — [escalation](#e2e-diagnosis-escalation)) | Revert | Revert | Never | + +Package workflows define **which module/spec** to load (e.g. Firestore → [pipeline implementation workflow § narrowing](../packages/firestore/pipeline-implementation-workflow.md#pipeline-area-harness)). + +**Sanity check:** pass counts must match loaded scope — not full-app totals ([running e2e § gate](running-e2e.md#harness-narrowing-gate-blocking)). + +## `commit` + +- One focused commit per item when gates close. +- **Never stage:** area narrowing, any `.only`, ad-hoc harness edits, or **`RNFBDebug = true`** in `tests/globals.js` ([running e2e § before merge](running-e2e.md#before-merge-pr-handoff), [platform coverage gate](running-e2e.md#platform-coverage-gate-blocking)). +- **Work queue:** before `git commit`, set the row's `commit_subject` to the commit's subject line, close `commit_gate`, and stage the queue doc **in the same commit** as the product change ([documentation policy § work queues](../documentation-policy.md#work-queue-documents)). Do not record SHAs in queue docs. + +```bash +git status +git diff --stat +rg '\.only\(' packages/ +``` + +## Package extensions + +| Package / area | Adds to this loop | +|----------------|-------------------| +| Firestore Pipelines | Compare-types gap pick, serialization matrix, `Pipeline.e2e.js` setup, coverage snapshots — [pipeline implementation workflow](../packages/firestore/pipeline-implementation-workflow.md) | +| Other packages | `okf-bundle/packages//` index when a workflow exists | + +Ephemeral coordination (gate rows, `next_work_type`, `commit_subject`): **work queues only** — not part of this workflow. + +## Related docs + +| Topic | Document | +|-------|----------| +| Term ids and queue field schema | [iteration-vocabulary.md](iteration-vocabulary.md) | +| E2e commands | [running-e2e.md](running-e2e.md) | +| Validation commands | [validation-checklist.md](validation-checklist.md) | +| Coverage policy | [coverage-design.md](coverage-design.md) | diff --git a/okf-bundle/testing/coverage-design.md b/okf-bundle/testing/coverage-design.md index c1131588c6..c0c8685553 100644 --- a/okf-bundle/testing/coverage-design.md +++ b/okf-bundle/testing/coverage-design.md @@ -64,17 +64,11 @@ After `tests::test-cover`: Native artifacts (`.profraw`, `.ec`, Jacoco XML) are trustworthy only with the fresh e2e run that produced them. Re-processing leftovers can create valid-looking stale reports. -If numbers look wrong, run the clean cycle before debugging generators: +If numbers look wrong, run the clean cycle before debugging generators — e2e steps: [running e2e § Rules](running-e2e.md#rules) and [typical loop](running-e2e.md#typical-loop); then post-process below (this doc owns post-e2e coverage export only): ```bash -# iOS -yarn tests:ios:build && yarn tests:ios:test-cover && yarn tests:ios:test:process-coverage - -# Android -yarn tests:android:build && yarn tests:android:test-cover && yarn tests:android:post-e2e-coverage - -# macOS (TS only) -yarn tests:macos:test-cover +yarn tests:ios:test:process-coverage +yarn tests:android:post-e2e-coverage ``` Post-process deletes raw iOS `.profraw` / Android `.ec`; missing raw file means "no fresh coverage." Do not use reuse variants for native deltas ([runbook](running-e2e.md)). @@ -125,13 +119,13 @@ yarn tests:jest-coverage # E2e TypeScript coverage (Jet + NYC) -Commands: [e2e runbook](running-e2e.md). +**Run e2e:** [running e2e § Rules](running-e2e.md#rules) — canonical `:test-cover` commands only; do not duplicate them here. -| Platform | Script | Notes | -|----------|--------|-------| -| macOS | `tests:macos:test-cover` | Jet only | -| iOS | `tests:ios:test-cover` | Detox → Jet `--coverage` | -| Android | `tests:android:test-cover` | Detox → Jet `--coverage` | +| Platform | Entry (repo root) | Notes | +|----------|-------------------|-------| +| macOS | `tests:macos:test-cover` | See runbook | +| iOS | `tests:ios:test-cover` | See runbook | +| Android | `tests:android:test-cover` | See runbook | Jet self-wraps under NYC with `--coverage`. @@ -140,7 +134,7 @@ Jet self-wraps under NYC with `--coverage`. - Metro bundles `packages/*/dist/module/**` with inline source maps (`tests/.babelrc`: `useInlineSourceMaps: true`). - NYC (`tests/nyc.config.js`) remaps to `packages/*/lib/**` → **`coverage/lcov.info`** (`cwd: '..'`). - Jet re-invokes under `tests/node_modules/.bin/nyc` (checks `NYC_CONFIG`). Detox/macOS need no extra `nyc` prefix; Jet must run from `tests/`. -- **Transfer:** patched Jet/mocha-remote WS only (`coverage-ready` → `pull-coverage` → `coverage-data` → `coverage-ack`); HTTP POST `/coverage` deleted (`attachHttpServer` removed). Host launch/orchestrate control uses a **separate** HTTP server on **8091** (not the 8090 WS stack) — see [Jet host orchestration](running-e2e.md#jet-host-orchestration-ports-and-launch-gate). Patches: `.yarn/patches/` (`jet`, `mocha-remote-client`, `mocha-remote-server`). See [iOS issues 6–6b](../ci-workflows/ios.md#6-jet-websocket-disconnect-1006--1001), [issue 8](../ci-workflows/ios.md#8-coverage-teardown-handshake-failure-tests-pass-nyc-00), [jet patch workflow](../ci-workflows/detox-patches.md#updating-the-jet-patch-headless). +- **Transfer:** patched test-runner/mocha-remote WS only (`coverage-ready` → `pull-coverage` → `coverage-data` → `coverage-ack`); HTTP POST `/coverage` deleted (`attachHttpServer` removed). Host launch/orchestrate control uses a **separate** HTTP server on **8091** (not the 8090 WS stack) — see [test-runner orchestration (log triage)](running-e2e.md#test-runner-host-orchestration-log-triage-only). Patches: `.yarn/patches/` (`jet`, `mocha-remote-client`, `mocha-remote-server`). See [iOS issues 6–6b](../ci-workflows/ios.md#6-jet-websocket-disconnect-1006--1001), [issue 8](../ci-workflows/ios.md#8-coverage-teardown-handshake-failure-tests-pass-nyc-00), [jet patch workflow](../ci-workflows/detox-patches.md#updating-the-jet-patch-headless). **NYC settings:** @@ -156,9 +150,9 @@ reporter: ['lcov', 'html', 'text-summary'], | Symptom | Cause | Fix | |---------|-------|-----| -| `[jet-coverage] merged 0 file(s)` | Jet client still POSTs to removed `/coverage` HTTP endpoint, or stale Metro bundle | `yarn install` (jet patch wires `client.uploadCoverage()`); macOS: restart packager with `yarn react-native start --reset-cache` in `tests/` | +| `[jet-coverage] merged 0 file(s)` | Stale Metro bundle or missing test-runner patch | `yarn install`; macOS: restart packager per [running e2e § Rules #1](running-e2e.md#rules) | | macOS bundle still has `'/coverage'` fetch | Metro resolves Jet via `"react-native": "src/index"` — patch must touch `jet/src/index.tsx`, not only `lib/` | Re-run after patch; `--reset-cache` | -| iOS/Android merged 0, macOS OK | Prebuilt app bundle predates Jet/Istanbul fix | `yarn tests:ios:build` / `yarn tests:android:build` then `:test-cover` | +| iOS/Android merged 0, macOS OK | Prebuilt app bundle predates Istanbul fix | Re-run [running e2e § Rules](running-e2e.md#rules) (`:build` then `:test-cover`) | | Metro 500 on bundle | Missing babel plugins in `tests/` | `yarn install`; confirm `tests/node_modules/babel-plugin-istanbul` exists | # E2e Android native (Jacoco) @@ -272,14 +266,14 @@ No `:test-cover-reuse` / `:test-reuse` — stale native risk ([runbook](running- | Stale profraw uploaded | Re-process without re-e2e | Process deletes profraw; exit 1 if missing next time | | Stale Android Jacoco / collapsed native % | Re-run `post-e2e-coverage` without fresh e2e | Post-e2e deletes `.ec` after report; run full `:build` → `:test-cover` → `:post-e2e-coverage` | | Coverage numbers suspect (any platform) | Leftover raw artifacts or reuse shortcuts | Full clean cycle per platform; see [Stale coverage data](#stale-coverage-data) | -| No `packages/` hits in iOS export | Wrong binary / not instrumented | `yarn tests:ios:build`; check Podfile | +| No `packages/` hits in iOS export | Wrong binary / not instrumented | Re-run `tests:ios:build` per [running e2e § Rules](running-e2e.md#rules); check Podfile | | Empty Jacoco XML (~235 B) | AGP 8 path, missing `src/reactnative/java`, no ec | Check post-e2e logs | | Android ec missing after pass | SIGINT before flush | `[native-coverage] flushing android coverage` in log; `MainApplication` registration | | Jet after: coverage not enabled | Release / non-instrumented build | Use `:test-cover` debug builds | | `swiftCompatibility56` undefined | Profile link flags on all Pods | App target only for `OTHER_LDFLAGS` | | No `[jet-coverage] WS received` | Patches missing | `yarn install`; `.yarn/patches/` | | WS closed on `reconnect_recovered` | Handshake on dead socket | Client retry + server pull; `JET_COVERAGE_TEARDOWN_RE` — [iOS issue 8](../ci-workflows/ios.md#8-coverage-teardown-handshake-failure-tests-pass-nyc-00) | -| Empty NYC / lcov | Jet not from `tests/` cwd | Detox spawns `yarn jet` in `tests/` | +| Empty NYC / lcov | Environment or patch issue during `:test-cover` | Re-run per [running e2e](running-e2e.md) — do not invoke the test runner directly | | Codecov missing iOS native | Wrong path/name | `coverage/ios-native/lcov.info` | | Upload **Unusable** | Bad `SF:` paths | `process-ios-native-coverage.js` rewrite | | `ios-native` / `android-native` fail | Upload missing → 0% | Uploads tab; process/post-e2e steps | diff --git a/okf-bundle/testing/firebase-testing-project.md b/okf-bundle/testing/firebase-testing-project.md index 4a3e83f884..5b233cd8ad 100644 --- a/okf-bundle/testing/firebase-testing-project.md +++ b/okf-bundle/testing/firebase-testing-project.md @@ -52,7 +52,7 @@ flowchart TB # Where configuration lives -Config root: [`.github/workflows/scripts/`](../../.github/workflows/scripts/) (`firebase.json` cwd for Firebase CLI and `yarn tests:emulator:start`). +Config root: [`.github/workflows/scripts/`](../../.github/workflows/scripts/) (`firebase.json` cwd for Firebase CLI). Emulator start: [running e2e § Rules #2](running-e2e.md#rules). | File | Purpose | |------|---------| @@ -82,10 +82,7 @@ Runtime wiring: ## Started locally -```bash -yarn tests:emulator:start # foreground (dev) -yarn tests:emulator:start-ci # background (CI) -``` +Foreground/background emulator start: [running e2e § Rules #2](running-e2e.md#rules). Runs from `.github/workflows/scripts/`: @@ -211,7 +208,7 @@ Functions **e2e** callables run on the **emulator** (`:5001`). Under CI load, cl | **Client** (`functions.e2e.js`) | `e2eCallableTimeoutOptions()` → 120s | `{ timeout: 1000 }` only | | **Server** (`functions/src/*.ts`) | `E2E_TEST_FUNCTION_TIMEOUT_SECONDS` (120) via `e2eCallOptions.ts` | `sleeperV2` unchanged (intentional hang) | -Restart emulator after rebuilding `functions/` (`yarn tests:emulator:start`). +Restart emulator after rebuilding `functions/` — [running e2e § Rules #2](running-e2e.md#rules). # Cloud project: deploy rules and indexes @@ -275,8 +272,8 @@ After deploy, index creation is async; `findNearest` may wait/stay skipped until GHA e2e: -1. `yarn tests:emulator:start-ci` — background emulator -2. Build + Detox/Jet run (needs network for `pipelines-e2e` cloud) +1. Emulator — [running e2e § Rules #2](running-e2e.md#rules) (`tests:emulator:start-ci` in CI) +2. Build + Detox run — [running e2e](running-e2e.md) (needs network for `pipelines-e2e` cloud) 3. Emulator cache under `~/.cache/firebase/emulators` Pipeline tests share Jet session with Firestore e2e but execute on cloud; `(default)` setup/wipe stays emulator. diff --git a/okf-bundle/testing/index.md b/okf-bundle/testing/index.md index 9b963324c3..cc43c939ad 100644 --- a/okf-bundle/testing/index.md +++ b/okf-bundle/testing/index.md @@ -1,8 +1,10 @@ # Testing +* [Agent command policy](agent-command-policy.md) — **read before any shell command** (install, prepare, validation, e2e); **`yarn` / `yarn lerna:prepare` must exit 0 before any other command** * [Documentation/commit policy](../documentation-policy.md) — durable vs ephemeral, OKF scan -* [Iteration vocabulary](iteration-vocabulary.md) — work types, validation tiers, gates -* [Running e2e tests](running-e2e.md) — canonical e2e commands; start here +* [Change authoring workflow](change-authoring-workflow.md) — verified product change loop (unit-focused → area-focused review → commit) +* [Iteration vocabulary](iteration-vocabulary.md) — work type, tier, and queue field identifiers +* [Running e2e tests](running-e2e.md) — canonical e2e commands; start here for `:test-cover` * [Validation checklist](validation-checklist.md) — handoff command sequence * [Coverage design](coverage-design.md) — coverage policy, Codecov/native gates * [Firebase testing project](firebase-testing-project.md) — cloud vs emulator, live FIS/RC, helper callables, rules/indexes, deploy diff --git a/okf-bundle/testing/iteration-vocabulary.md b/okf-bundle/testing/iteration-vocabulary.md index 8df7e1e605..56cf76a84c 100644 --- a/okf-bundle/testing/iteration-vocabulary.md +++ b/okf-bundle/testing/iteration-vocabulary.md @@ -1,85 +1,79 @@ --- type: Reference title: Iteration vocabulary -description: Workflow-neutral terms for iteration steps, validation tiers, gates, and host rules used across OKF docs and work queues. -tags: [testing, validation, workflow, gates, work-queue] +description: Identifier glossary and work-queue field schema for OKF — not workflow rules or commands. +tags: [testing, validation, workflow, work-queue] timestamp: 2026-06-25T00:00:00Z --- # Iteration vocabulary -Canonical terms for **what kind of work** an iteration requires and **which validation applies**. These terms describe verification work — not agent roles, session types, or dispatch policy. +Glossary of **string identifiers** and **work-queue field names** used across OKF. This doc does not define procedures, gate rules, harness policy, or e2e commands — each topic has one owning doc; others link. **Policy:** [OKF documentation and commit policy](../documentation-policy.md). -## Work types +| Topic | Owner | +|-------|--------| +| Change loop, gates, frozen tree, host rule | [change authoring workflow](change-authoring-workflow.md) | +| **All agent shell commands** (install, prepare, validation, e2e) | [agent command policy](agent-command-policy.md) | +| E2e-only detail, pre-flight, harness gate, tier scope | [running e2e](running-e2e.md) — `yarn tests:*` subset of agent command policy | +| Validation command sequence | [validation checklist](validation-checklist.md) | +| Work-queue gate snapshots | Package work queues (ephemeral) | -| Work type | Purpose | Typical validation tier | Product edits during work | Commit allowed | -|-----------|---------|-------------------------|---------------------------|----------------| -| `gap-analysis` | Confirm export/SDK semantics and feasibility | none | read-only | no | -| `baseline-capture` | Record before snapshots and area-tier e2e baseline | `area` | harness narrowing OK locally; never commit narrowing | no | -| `implementation` | Code, unit tests, fast e2e loop | `focused` | yes | no | -| `independent-review` | Adversarial pass on a **frozen tree** | `area` (+ checklist where workflow requires) | no — [frozen tree](#frozen-tree) | no | -| `documentation` | User docs and durable OKF updates | none | docs only | no | -| `commit` | Single focused commit after gates close | none | staging/commit only | yes | -| `pre-merge-validation` | Branch-wide unfocused gate before merge | `full` | revert all narrowing first | no | +## Work type identifiers -Work types are ordered in package workflows (e.g. [pipeline implementation workflow](../packages/firestore/pipeline-implementation-workflow.md)). A work queue row names the **`next_work_type`** when pickup should continue an in-flight item. +| Work type | Brief meaning | +|-----------|---------------| +| `gap-analysis` | Read-only feasibility / semantics check | +| `baseline-capture` | Record before snapshots or baselines | +| `implementation` | Author product code and tests | +| `independent-review` | Verify a frozen diff | +| `documentation` | User docs and durable OKF updates | +| `commit` | Stage and create one commit | +| `pre-merge-validation` | Branch-wide merge gate | -## Validation tiers +When to use each work type, validation tier, edit policy, and commit rules: [change authoring § work types](change-authoring-workflow.md#work-types). -E2e scope and narrowing rules: [running e2e § validation tiers](running-e2e.md#e2e-validation-tiers-focused-area-full). +## Validation tier identifiers -| Tier | E2e scope | Narrowing | -|------|-----------|-----------| -| `focused` | Fast loop while product code is changing | **Area narrowing required** before `:test-cover`; `.only` OK locally — never commit ([harness gate](running-e2e.md#harness-narrowing-gate-blocking)) | -| `area` | Full loaded spec(s) for the package/area under change | **Area narrowing required** before `:test-cover`; **no** `.only` ([harness gate](running-e2e.md#harness-narrowing-gate-blocking)) | -| `full` | All modules, all platforms | Revert all narrowing | +| Tier id | Brief meaning | +|---------|---------------| +| `unit-focused` | Fast validation while product code is changing | +| `area-focused` | Full loaded package spec(s) for the change area | +| `full` | Unfocused — all modules and platforms | -Jest, prepare, compile, and checklist commands per work type: [validation checklist](validation-checklist.md). +E2e scope, narrowing, and harness rules: [change authoring § validation tiers](change-authoring-workflow.md#validation-tiers), [running e2e § validation tiers](running-e2e.md#e2e-validation-tiers-unit-focused-area-focused-full). -## Gates +## Gate identifiers -Binary checkpoints on a queue item or iteration. Values: `open` | `closed`. Work queues may also mark an item **`blocked`** when a dependency gate is open elsewhere. +Work queues use these **field names** (values: `open` | `closed`): -| Gate | Closed when | -|------|-------------| -| `implementation` | `implementation` work type complete — code plus focused-tier checks reported green | -| `review` | `independent-review` work type complete — area-tier (and checklist where required) green on frozen tree | -| `commit` | Durable commit exists for the item | +| Field | Tracks | +|-------|--------| +| `implementation_gate` | `implementation` work type complete | +| `review_gate` | `independent-review` work type complete | +| `commit_gate` | Durable commit exists for the item | -**Trust rule:** Code may exist on disk or in git while `review` is still `open`. That state is **unverified** until `independent-review` closes the `review` gate. +What closes each gate, trust rules, and loop transitions: [change authoring § gates](change-authoring-workflow.md#gates). -## Frozen tree +`commit_gate` closes when a durable commit exists whose subject matches the row's `commit_subject`. -Required for `independent-review` and for any `:test-cover` run that closes the `review` gate: - -- No edits to `packages/**`, `tests/**` (except reverting `.only`), or bundle-affecting OKF docs during the run. -- Wait for or cancel in-flight runs before editing again. - -See also: [running e2e rule 7](running-e2e.md#rules) and [host rule](running-e2e.md#serialized-e2e-loops-shared-dev-host). - -## Host rule - -On a shared dev host: - -- One `:test-cover` at a time — never overlap focused-tier and area-tier runs. -- [Pre-flight](running-e2e.md#pre-flight-is-the-host-clear-to-start) before every run: [host-clear probes](running-e2e.md#host-clear-probes), [services ready](running-e2e.md#2-services-ready), [harness matches `validation_tier`](running-e2e.md#3-harness-matches-validation-tier) ([narrowing gate](running-e2e.md#harness-narrowing-gate-blocking) for `focused` and `area`). -- Canonical commands only — [running e2e](running-e2e.md). Stalled runs → [stalled run detection](running-e2e.md#stalled-run-detection). +Items may also be marked **`blocked`** when a dependency gate is open elsewhere. ## Work-queue fields Ephemeral work queues may record: -| Field | Meaning | -|-------|---------| -| `next_work_type` | Which work type unblocks the item next | -| `validation_tier` | `focused` \| `area` \| `full` for the next validation pass | -| `platform` | Optional scope (e.g. `ios` for iOS-only guard probes) | +| Field | Allowed values / meaning | +|-------|--------------------------| +| `next_work_type` | A [work type identifier](#work-type-identifiers) | +| `validation_tier` | `unit-focused` \| `area-focused` \| `full` | +| `platform` | Optional scope (e.g. `ios`) | | `implementation_gate` | `open` \| `closed` | | `review_gate` | `open` \| `closed` | | `commit_gate` | `open` \| `closed` | -| `blocked` | Item or dependent blocked until named gate closes | +| `commit_subject` | Planned or landed **first line** of the item's focused commit (Conventional Commits subject). Set **before** `git commit`; must match the commit that closes `commit_gate`. Do not record SHAs. | +| `blocked` | Item or dependency blocked until named gate closes | Queues record **state**, not who executes the work. @@ -87,8 +81,9 @@ Queues record **state**, not who executes the work. | Topic | Document | |-------|----------| -| E2e commands and tiers | [running-e2e.md](running-e2e.md) | +| **Change authoring loop** | [change-authoring-workflow.md](change-authoring-workflow.md) | +| E2e commands | [running-e2e.md](running-e2e.md) | | Validation commands | [validation-checklist.md](validation-checklist.md) | | Doc/commit policy | [documentation-policy.md](../documentation-policy.md) | -| Pipeline iteration steps | [pipeline-implementation-workflow.md](../packages/firestore/pipeline-implementation-workflow.md) | +| Pipeline-specific artifacts | [pipeline-implementation-workflow.md](../packages/firestore/pipeline-implementation-workflow.md) | | Live gate snapshots | [pipeline coverage work queue](../packages/firestore/pipeline-coverage-work-queue.md) | diff --git a/okf-bundle/testing/running-e2e.md b/okf-bundle/testing/running-e2e.md index bcdb105698..7e075c209a 100644 --- a/okf-bundle/testing/running-e2e.md +++ b/okf-bundle/testing/running-e2e.md @@ -10,7 +10,15 @@ timestamp: 2026-06-25T00:00:00Z Canonical local e2e commands. Use **only** these commands. `-ci` variants are CI-only. Avoid `:test-cover-reuse`, `:test-cover-and-process`, `:test-reuse` (stale native risk). If another doc disagrees, this wins. -> All e2e how-to lives here; other docs link here. +> All e2e how-to lives here; other docs link here — they do **not** define alternate entrypoints or commands. + +## Agent rule (read first) + + + +**Never invoke the test runner (Jet), Detox, Metro, or emulators directly.** Use **only** the repo-root `yarn tests:*` commands defined in this document (for example `yarn tests:packager:jet`, `yarn tests:emulator:start`, `yarn tests::test-cover`). Do not run `jet`, `npx jet`, `yarn jet`, `detox test`, `cd tests && …`, or ad-hoc Metro/emulator start commands. When another doc mentions e2e, Jet, Detox, or pre-flight, follow the link to this runbook — do not infer commands from log output or implementation details. + +Install, prepare, and validation commands are **not** in this doc — they live in [agent command policy](agent-command-policy.md) (read before any non-e2e shell command). ## Prerequisites (once per checkout) @@ -34,8 +42,8 @@ yarn tests:emulator:start 3. **Rebuild when needed** - Native changed → `yarn tests:ios:build` / `yarn tests:android:build` before e2e. macOS uses firebase-js-sdk only — no native rebuild. - - `packages/*/lib/**` changed → `yarn lerna:prepare` (Metro serves `dist/module/**`, not `lib/**`; e2e specs under `packages/*/e2e/**` and `tests/**` are served directly). - - TS coverage: iOS/Android embed JS at **build** time; run `:build` before `:test-cover` so Istanbul + patched Jet `uploadCoverage` are in app. macOS loads from Metro live; after Jet patch changes, restart packager with `--reset-cache` in `tests/`. Patch `src/index.tsx`, not only compiled `lib/`. + - `packages/*/lib/**` changed → **`yarn lerna:prepare` must run to completion (exit 0) before anything else** — Metro serves `dist/module/**`, not `lib/**`. See [prepare completion gate](#prepare-completion-gate-blocking) and [agent command policy § prepare must finish first](agent-command-policy.md#prepare-must-finish-first). After prepare finishes, restart the packager with `yarn tests:packager:jet-reset-cache` when Metro was already running ([Rules §1](#rules)). + - TS coverage: iOS/Android embed JS at **build** time; run `:build` before `:test-cover` so Istanbul + patched test-runner coverage upload is in app. macOS loads from Metro live; after test-runner patch changes, restart the packager with `yarn tests:packager:jet-reset-cache` ([Rules §1](#rules)). 4. **Always run with coverage:** @@ -49,54 +57,53 @@ yarn tests:macos:test-cover 5. **Report locations** — [Coverage design](coverage-design.md). -6. **One e2e at a time** — never overlap `:test-cover`/Detox/Jet on one host. Jet uses **8090** (WebSocket) plus **8091** (host control HTTP when defer-run is enabled); all platforms share Jet + Metro `:8081`; parallel runs race on coverage/device/emulator state. Every run starts after [clean pre-flight](#pre-flight-is-the-host-clear-to-start). See [Jet host orchestration](#jet-host-orchestration-ports-and-launch-gate). +6. **One e2e at a time** — never overlap `:test-cover` runs on one host. All platforms share Metro `:8081` and the test-runner WebSocket port (default **8090**); parallel runs race on coverage/device/emulator state. Every run starts after [clean pre-flight](#pre-flight-is-the-host-clear-to-start). Log triage for port/orchestration markers: [test-runner host orchestration](#test-runner-host-orchestration-log-triage-only). 7. **No source edits during e2e** — wait/cancel cleanly before editing `packages/**`, `tests/**`, or bundle-affecting OKF docs. Saves can hot reload/rebundle and invalidate tests/coverage. ## Serialized e2e loops (shared dev host) -Use [validation tiers](#e2e-validation-tiers-focused-area-full): **focused**, **area**, **full**. Match tier to [work type](iteration-vocabulary.md#work-types). Runs are serial from clean [pre-flight](#pre-flight-is-the-host-clear-to-start). Log long output; upstream gets exit code + short summary. +Use [validation tiers](#e2e-validation-tiers-unit-focused-area-focused-full): **unit-focused**, **area-focused**, **full**. Match tier to [work type](change-authoring-workflow.md#work-types). Runs are serial from clean [pre-flight](#pre-flight-is-the-host-clear-to-start). Log long output; upstream gets exit code + short summary. **Policy:** [OKF documentation and commit policy](../documentation-policy.md). **Terms:** [iteration vocabulary](iteration-vocabulary.md). ### How a platform run is structured (Android/iOS) -Wait on Detox/Jest; Jet is its **child**: +**Internal only — do not invoke sub-commands.** Wait on the single repo-root `:test-cover` command; Detox/Jest and the test runner start automatically. ```text -yarn tests:android:test-cover - └─ detox test → jest (e2e/jest.config.js) - └─ firebase.test.js spawns: yarn jet --target=android --coverage (:8090) - └─ app on emulator/simulator +yarn tests:android:test-cover # only command you run + └─ (internal) detox → jest → firebase.test.js → test runner on :8090 → app ``` -macOS: `yarn tests:macos:test-cover` → `jet --target=macos` (same `:8090` WS). +macOS: `yarn tests:macos:test-cover` only — same `:8090` transport, no Detox. + +**Do not poll `pgrep`, `detox`, process names, or `:8090` for completion.** They match stale wrappers, orphans, zombies, and contention. -**Do not poll `pgrep`, `detox`, `jet.js`, or `:8090` for completion.** They match stale wrappers, orphans, zombies, and contention. + + -#### Jet host orchestration (ports and launch gate) +#### Test-runner host orchestration (log triage only) -**Canonical owner** for Detox↔Jet↔app sequencing in `tests/e2e/firebase.test.js` and the patched Jet CLI. CI platform pages link here; do not duplicate the full port/protocol story elsewhere. +**No commands to run from this section** — for interpreting `:test-cover` logs and CI artifacts only. Patch workflow: [detox-patches.md](../ci-workflows/detox-patches.md#updating-the-jet-patch-headless). CI triage: [iOS orchestration](../ci-workflows/ios.md#e2e-test-app-orchestration-detox--jet). | Port | Protocol | Role | |------|----------|------| -| **8090** (default `JET_REMOTE_PORT`) | WebSocket (`mocha-remote-*`) | App ↔ Jet test transport; `server.run()` drives Mocha in the app | -| **8091** (default `JET_REMOTE_PORT + 1`, override `RNFB_JET_CONTROL_PORT`) | HTTP POST only | Host ↔ Jet **control plane** — not used by the app | +| **8090** (default `JET_REMOTE_PORT`) | WebSocket (`mocha-remote-*`) | App ↔ host test transport; drives Mocha in the app | +| **8091** (default `JET_REMOTE_PORT + 1`, override `RNFB_JET_CONTROL_PORT`) | HTTP POST only | Host ↔ test-runner **control plane** — not used by the app | -**Why two ports** — Port 8090 is a WebSocket server (`ws` library). Plain HTTP `POST` to that socket (e.g. `/launch-ready`) gets **426 Upgrade Required** and can crash Jet with `ERR_HTTP_HEADERS_SENT` if a control handler shares the same HTTP stack. Control endpoints therefore live on a **separate** small HTTP server (`startControlHttpServer` in the Jet patch). +**Why two ports** — Port 8090 is a WebSocket server (`ws` library). Plain HTTP `POST` to that socket (e.g. `/launch-ready`) gets **426 Upgrade Required** and can crash the runner with `ERR_HTTP_HEADERS_SENT` if a control handler shares the same HTTP stack. Control endpoints therefore live on a **separate** small HTTP server (`startControlHttpServer` in the test-runner patch). -**Launch gate (orchestration race fix)** — `firebase.test.js` spawns Jet with `RNFB_JET_DEFER_RUN=1`. Jet listens on 8090 and **defers** `server.run()` until the host signals launch success: +**Launch gate (orchestration race fix)** — `firebase.test.js` starts the test runner with `RNFB_JET_DEFER_RUN=1`. It listens on 8090 and **defers** `server.run()` until the host signals launch success: 1. Host waits for TCP **8090**, then Metro (debug) if needed, then `launchAppWithRetry`. 2. Host `POST`s **`/orchestrate-state`** (`{ "phase": "launch-pending" | "launch-ok" | … }`) to the control port (best-effort diagnostics). -3. After `launchApp` succeeds, host `POST`s **`/launch-ready`** → Jet calls `server.run()` and the app may receive the mocha-remote `run` action. -4. Mocha tests must not start during a stuck or retried `launchApp`; on inner launch retry the host may kill and respawn Jet before `terminateApp`/simulator reboot. +3. After `launchApp` succeeds, host `POST`s **`/launch-ready`** → test runner calls `server.run()` and the app may receive the mocha-remote `run` action. +4. Mocha tests must not start during a stuck or retried `launchApp`; on inner launch retry the host may kill and respawn the test runner before `terminateApp`/simulator reboot. -**Log markers** — `[rnfb-e2e] orchestrate-state=…`, `[jet-control] deferring server.run until POST /launch-ready`, `[jet-control] launch-ready received`, `[jet-control] listening on http://…:8091`. +**Log markers** — `[rnfb-e2e] orchestrate-state=…`, `[jet-control] deferring server.run until POST /launch-ready`, `[jet-control] launch-ready received`, `[jet-control] listening on http://…:8091`, `[jet-coverage] …`, `Jet client connected`. -**Pre-flight** — [Host-clear probes](#host-clear-probes) check **8090 only** (stray Jet WS listener). **8091** may be open while Jet is running; do not treat it as a stale-process signal. - -**Patches / code** — Jet patch (`cli.js`: defer run, control HTTP, enriched `disconnect_context`); `tests/e2e/firebase.test.js` (`postJetControl`, `createJetSession`). Patch workflow: [detox-patches.md](../ci-workflows/detox-patches.md#updating-the-jet-patch-headless). CI triage: [iOS orchestration](../ci-workflows/ios.md#e2e-test-app-orchestration-detox--jet). +**Pre-flight** — [Host-clear probes](#host-clear-probes) check **8090 only** (stray test-runner WS listener). **8091** may be open during a run; do not treat it as a stale-process signal by itself. #### CI iOS instrumentation (not local) @@ -116,7 +123,7 @@ yarn tests:ios:test-cover 2>&1 | tee /tmp/rnfb-e2e-ios.log yarn tests:macos:test-cover 2>&1 | tee /tmp/rnfb-e2e-macos.log ``` -Use `/tmp/rnfb-e2e-.log` (overwrite each iteration). Do not substitute `detox test`, `cd tests && yarn detox …`, or other entrypoints. +Use `/tmp/rnfb-e2e-.log` (overwrite each iteration). Do not substitute other entrypoints — see [agent rule](#agent-rule-read-first). 4. Completion = shell exit code. `0` finished; non-zero failed/aborted. Read log for counts. 5. Parse log tail; do not infer from processes: @@ -135,7 +142,21 @@ Markers: `✨ Tests Complete ✨`, Jest `N passing` / `N failing`, `[jet-coverag **Canonical owner** for host-clear probes, recovery after abort, and service checks. Other OKF docs link here by reference — do not duplicate commands or probes elsewhere. -Run **all three** steps before every `:test-cover`. After an [interrupted run](#interrupted-run-abort-killed-terminal-eaddrinuse-on-8090), run [pre-flight recovery](#pre-flight-recovery) and re-run the probes. +Run **all four** steps before every `:test-cover`. After an [interrupted run](#interrupted-run-abort-killed-terminal-eaddrinuse-on-8090), run [pre-flight recovery](#pre-flight-recovery) and re-run the probes. + + + +#### 0. Prepare complete (when `packages/*/lib/**` changed) + +If product code under `packages/*/lib/**` was edited in this session, **`yarn lerna:prepare`** (or scoped `yarn lerna run prepare --scope …`) must have **fully finished with exit code 0** before pre-flight steps 1–3 or any `:test-cover` / `:build`. + +- **Wait** for the prepare shell to return — do not batch prepare in parallel with Metro restart, pre-flight probes, or e2e in the same agent turn. +- **Then** restart Metro when it was already running: `yarn tests:packager:jet-reset-cache` ([Rules §3](#rules)). +- **Then** continue with host-clear probes and service checks below. + +Skipping this gate causes missing or half-written `dist/module/**` while Metro `/status` still returns 200 — a common source of bundle-load and module-not-found failures that look like product bugs. + +Owner for install/prepare serialization: [agent command policy § prepare must finish first](agent-command-policy.md#prepare-must-finish-first). #### 1. Host clear @@ -144,7 +165,7 @@ No in-flight test run on the target platform: | Platform | Clear when | |----------|------------| | **Android** | [Host-clear probes](#host-clear-probes) pass (no instrumentation PID) | -| **iOS** | [Host-clear probes](#host-clear-probes) pass — **zero booted simulators** and no stray Jet on `:8090`. Detox boots `iPhone 17` from `tests/.detoxrc.js`; do not pre-boot or leave simulators running. | +| **iOS** | [Host-clear probes](#host-clear-probes) pass — **zero booted simulators** and no stray listener on `:8090`. Detox boots `iPhone 17` from `tests/.detoxrc.js`; do not pre-boot or leave simulators running. | | **macOS** | [Host-clear probes](#host-clear-probes) pass (no `io.invertase.testing` process) | Also wait for any visible unfinished `yarn tests:*:test-cover`. @@ -192,7 +213,7 @@ curl -sf http://127.0.0.1:8081/status >/dev/null # Metro (127.0.0.1 matches te curl -sf http://127.0.0.1:8080 >/dev/null # Firestore emulator ``` -If either fails: start `yarn tests:packager:jet` and `yarn tests:emulator:start` (background); re-check until both pass. After `yarn lerna:prepare` or Jet patch edits, restart Metro with `--reset-cache` in `tests/` ([Rules §3](#rules)). +If either fails: start `yarn tests:packager:jet` and `yarn tests:emulator:start` (background); re-check until both pass. After **`yarn lerna:prepare` has finished** (step [0](#prepare-completion-gate-blocking)) or test-runner patch edits, restart the packager with `yarn tests:packager:jet-reset-cache` ([Rules §1](#rules)) — never restart Metro while prepare is still running. A listener on `:8081` or `:8080` is **not** sufficient — HTTP checks must succeed. @@ -202,11 +223,11 @@ Confirm `tests/app.js` / `tests/globals.js` match the item's **`validation_tier` | Tier | Harness before `:test-cover` | |------|------------------------------| -| **Focused** (`implementation`) | **Area narrowing required** — trim modules + load only the spec under change (e.g. firestore + `Pipeline.e2e.js`); `.only` OK locally | -| **Area** (`independent-review`, `baseline-capture`) | **Area narrowing required** — same module/spec trim as focused; load **full** spec file(s) for the area; **no** `.only` | -| **Full** (`pre-merge-validation`) | Revert all narrowing — full app (`require.context`, all modules) | +| **Unit-focused** (`implementation`) | **Area narrowing required** — trim modules + load only the spec under change (e.g. firestore + `Pipeline.e2e.js`); `.only` OK locally. Set **`RNFBDebug = true`** locally in `tests/globals.js` ([§ fail-fast](#fail-fast-rnfbdebug-and-sub-suite-narrowing)). | +| **Area-focused** (`independent-review`, `baseline-capture`) | **Area narrowing required** — same module/spec trim as unit-focused; load **full** spec file(s) for the package area; **no** `.only`. Set **`RNFBDebug = true`** locally ([§ fail-fast](#fail-fast-rnfbdebug-and-sub-suite-narrowing)). | +| **Full** (`pre-merge-validation`) | Revert all narrowing — full app (`require.context`, all modules); **`RNFBDebug = false`** (committed default). | -Committed full harness on the branch does **not** override **focused** or **area** tier for local runs. Package workflows define area setup (e.g. [pipelines § narrowing](../packages/firestore/pipeline-implementation-workflow.md#narrowing-during-pipeline-iterations)). Never commit narrowing until **full** tier. +Committed full harness on the branch does **not** override **unit-focused** or **area-focused** tier for local runs. Package workflows define area setup (e.g. [pipelines § area harness](../packages/firestore/pipeline-implementation-workflow.md#pipeline-area-harness)). **How to edit `tests/app.js`:** [two platform blocks](#tests-app-js-area-harness). Never commit narrowing until **full** tier. See [Harness narrowing gate (blocking)](#harness-narrowing-gate-blocking) — a run that skips step 3 does **not** close `implementation_gate` or `review_gate`. @@ -217,61 +238,95 @@ Completion = shell exit code + log markers — not open-ended log tailing. | Platform | Early markers (≈2–3 min) | Done | |----------|--------------------------|------| | **macOS** | `Jet client connected` | `✨ Tests Complete ✨`, Jest `N passing` | -| **iOS/Android** | Detox launch done, Jet connected | Same | +| **iOS/Android** | Detox launch done, `Jet client connected` | Same | **If stalled** — no new markers for **5 minutes**, or past tier budget (~15m macOS, ~45–60m iOS/Android) without `Tests Complete`: treat as [interrupted run](#interrupted-run-abort-killed-terminal-eaddrinuse-on-8090). Run [pre-flight recovery](#pre-flight-recovery), confirm [host-clear probes](#host-clear-probes) and [services ready](#2-services-ready), retry. Do not keep watching flat tee output. - macOS bundle/Metro hangs → [ci-workflows/other.md § bundle load hang](../ci-workflows/other.md#ci-failure-bundle-load-hang--could-not-connect-to-development-server) - iOS Metro at launch → [ci-workflows/ios.md § Metro unresponsive](../ci-workflows/ios.md) -Do not poll `pgrep` / `jet.js` / `:8090` for *completion* ([above](#how-a-platform-run-is-structured-androidios)). Stall detection uses **missing progress markers**, not exit polling. +Do not poll `pgrep`, process names, or `:8090` for *completion* ([above](#how-a-platform-run-is-structured-androidios)). Stall detection uses **missing progress markers**, not exit polling. ### Harness narrowing gate (blocking) -**Both `focused` and `area` tiers require area narrowing in `tests/app.js` / `tests/globals.js` before the first `:test-cover`.** The only difference between those tiers is whether `.only` is allowed and whether the full area spec loads — not whether the harness stays at full app load. +**Both `unit-focused` and `area-focused` tiers require area narrowing in `tests/app.js` / `tests/globals.js` before the first `:test-cover`.** The only difference between those tiers is whether `.only` is allowed and whether the full package-area spec loads — not whether the harness stays at full app load. | Mistake | Symptom | Gate impact | |---------|---------|-------------| | Run `:test-cover` on committed full harness during `implementation` or `independent-review` | macOS/iOS/Android pass counts in the **hundreds or thousands** (all modules via `require.context`) | Run is **invalid** — does not close `implementation_gate` or `review_gate` | -| Correct pipeline area harness | ~**100** passing per platform for `Pipeline.e2e.js` only ([pipeline workflow](../packages/firestore/pipeline-implementation-workflow.md#narrowing-during-pipeline-iterations)) | Expected for J0–Q iterations | +| Narrow **only** `if (Platform.other)` or only set initial `platformSupportedModules` while **`if (!Platform.other)`** still pushes full native list | macOS ~700 firestore tests pass; iOS/Android logs show `database`, `crashlytics`, etc.; thousands of tests / Jet WS 1006 under load | Run is **invalid** on iOS/Android — see [two platform blocks](#tests-app-js-area-harness) | +| Correct pipeline area harness | ~**100** passing per platform for `Pipeline.e2e.js` only ([pipeline workflow](../packages/firestore/pipeline-implementation-workflow.md#pipeline-area-harness)) | Expected for pipeline area runs | + +**Apply locally before every `:test-cover` at unit-focused or area-focused tier** — even when git shows the full push harness. Revert `tests/app.js` / `tests/globals.js` after the run if the branch commit keeps full harness (typical until phase **R**). + +**Validation report must state:** harness narrowed (yes/no), which module/spec loads, whether pass counts match area scope, and **which platforms ran** with exit codes. A green full-app run is not a substitute. -**Apply locally before every `:test-cover` at focused or area tier** — even when git shows the full push harness. Revert `tests/app.js` / `tests/globals.js` after the run if the branch commit keeps full harness (typical until phase **R**). + -**Validation report must state:** harness narrowed (yes/no), which module/spec loads, and whether pass counts match area scope. A green full-app run is not a substitute. +### Platform coverage gate (blocking — no shortcuts) + +**Both `unit-focused` (implementation) and `area-focused` (baseline-capture, independent-review) require e2e on every platform where the changed module loads in the committed harness** — not a subset for convenience. + +Determine required platforms from `tests/app.js` ([two platform blocks](#tests-app-js-area-harness) — use **committed** lists when deciding macOS vs native requirement, not a narrowed local harness): + +| Platform class | When required | +|----------------|---------------| +| **macOS** (`Platform.other`) | Module appears in the committed `if (Platform.other)` list (or your narrowed list includes it on macOS) | +| **iOS** and **Android** | Module appears in the committed `if (!Platform.other)` list (or your narrowed list includes it on native) | + +**Area-focused (`baseline-capture`, `independent-review`) — closes `review_gate` / baseline only when:** + +1. Full loaded package spec(s) with [area narrowing](#harness-narrowing-gate-blocking) (no `.only`). +2. **Serial** `:test-cover` on **each required platform** above — pre-flight before **every** run. +3. Native platforms: `yarn tests::build` before first `:test-cover` when product/native JS changed ([Rules §4](#rules)). +4. Subagent/orchestrator return includes a **platform matrix**: platform, exit code, pass/fail/pending counts, log path. + +**Invalid shortcuts (do not close gates):** + +- “macOS + iOS minimum”; skipping **Android** when the module loads on Android. +- “Skip Android if time tight” or “Android fallback only if iOS failures look env-related” without a fresh Android run. +- Substituting a prior implementer log for `independent-review` on the frozen tree. + +**Module-specific skip:** only when the module is **absent** from that platform’s harness list (e.g. `messaging` is not on macOS). Record in the work-queue **Notes** — not an oral exception. + +**Unit-focused (`implementation`) — native touched:** macOS first when the path is TS/web-only; when the module loads on iOS **and** Android, run **both** before closing `implementation_gate` (same narrowing; `.only` OK locally; never commit). + +See also: [coverage design § platform parity](coverage-design.md#coverage-expectations-policy), [validation checklist § handoff](validation-checklist.md#handoff-checklist). **Checklist (copy before first run):** -1. `platformSupportedModules` lists only the package under change (e.g. `firestore` only). -2. Spec load uses direct `require` of the area spec — not `require.context` for all packages. -3. No `.only` when tier is **area**; `.only` optional when tier is **focused**. -4. Grep log: pass count consistent with area scope (~100 for pipeline-only), not full app (~141+ macOS baseline with full load per [work queue](../packages/firestore/pipeline-coverage-work-queue.md)). +1. [Both platform blocks](#tests-app-js-area-harness) narrowed or disabled — not just macOS / not just initial array. +2. `platformSupportedModules` lists only the package under change (e.g. `firestore` + `app`). +3. Spec load uses direct `require` of the area spec — not `require.context` for all packages — when sub-suite narrowing applies; otherwise full package `require.context` is OK when the module list is narrowed. +4. No `.only` when tier is **area-focused**; `.only` optional when tier is **unit-focused**. +5. Grep log: pass count consistent with area scope (~100 for pipeline-only, ~700 for full firestore package on macOS), not full app (~141+ macOS baseline with full load per [work queue](../packages/firestore/pipeline-coverage-work-queue.md)). -### Focused-tier iteration loop +### Unit-focused-tier iteration loop -For `implementation` work type ([focused tier](#e2e-validation-tiers-focused-area-full)): +For `implementation` work type — validation tier **unit-focused** ([change authoring workflow](change-authoring-workflow.md#implementation-inner-loop)): -1. [Pre-flight](#pre-flight-is-the-host-clear-to-start) — [host-clear probes](#host-clear-probes), services ready, **harness narrowed** (step 3); if probes fail, [pre-flight recovery](#pre-flight-recovery) first. +1. [Pre-flight](#pre-flight-is-the-host-clear-to-start) — [prepare completion gate](#prepare-completion-gate-blocking) when `lib/**` changed, [host-clear probes](#host-clear-probes), services ready, **harness narrowed** (step 3), **`RNFBDebug = true`** locally; if probes fail, [pre-flight recovery](#pre-flight-recovery) first. 2. Edit e2e/spec; add `.only` if needed; never commit narrowing. 3. macOS first when TS-only: `yarn tests:macos:test-cover 2>&1 | tee /tmp/rnfb-e2e-macos.log` — wait for exit code ([stalled run](#stalled-run-detection) if markers stop). 4. If macOS green and native touched: `yarn tests::build && yarn tests::test-cover 2>&1 | tee /tmp/rnfb-e2e-.log`; one platform at a time. 5. Grep log tail → fix → repeat from step 1. -6. When `implementation_gate` closes, next work type is `independent-review` at **area** tier — [frozen tree](iteration-vocabulary.md#frozen-tree); no `.only`; area narrowing per package workflow. +6. When `implementation_gate` closes, next work type is `independent-review` at **area-focused** tier — [frozen tree](change-authoring-workflow.md#frozen-tree); no `.only`; area narrowing per package workflow. ### Serialized e2e dispatch -Never overlap runs that use `:test-cover`. See [host rule](iteration-vocabulary.md#host-rule). +Never overlap runs that use `:test-cover`. See [host rule](change-authoring-workflow.md#host-rule). | Rule | Requirement | |------|-------------| | **One e2e run at a time** | Wait for prior shell exit code + short log summary | -| **No overlapping tiers** | Never run focused-tier and area-tier `:test-cover` concurrently on one host | +| **No overlapping tiers** | Never run unit-focused-tier and area-focused-tier `:test-cover` concurrently on one host | | **Clean pre-flight every run** | [Pre-flight](#pre-flight-is-the-host-clear-to-start) — [host-clear probes](#host-clear-probes), services, harness tier | -| **iOS guard probe loop** | `implementation` (Jest + **focused**) → `independent-review` (**area**, frozen tree) → `commit` — [work queue protocol](../packages/firestore/pipeline-coverage-work-queue.md#phase-j-iteration-protocol-strict) | +| **Phase J loop** | `implementation` (Jest + **unit-focused**) → `independent-review` (**area-focused**, frozen tree) → `commit` — [work queue protocol](../packages/firestore/pipeline-coverage-work-queue.md#phase-j-iteration-protocol-strict) | | Validation tier | E2e scope | Narrowing allowed | Typical work type | |-----------------|-----------|-------------------|-------------------| -| **Focused** | Backpressure while product code is changing | `it.only` / `describe.only` / tight area narrowing in `tests/app.js` — **never commit** | `implementation` | -| **Area** | Full loaded spec(s) for the package/area under change | **Area narrowing required** in `tests/app.js` / `tests/globals.js`; **no** `.only` | `baseline-capture`, `independent-review` | +| **Unit-focused** | Backpressure while product code is changing | `it.only` / `describe.only` / tight area narrowing in `tests/app.js` — **never commit** | `implementation` | +| **Area-focused** | Full loaded spec(s) for the package/area under change | **Area narrowing required** in `tests/app.js` / `tests/globals.js`; **no** `.only` | `baseline-capture`, `independent-review` | | **Full** | All modules, all platforms | None — revert all narrowing | `pre-merge-validation` | Each run owns its blocking `:test-cover` and returns summaries only. @@ -282,15 +337,16 @@ Run [pre-flight recovery](#pre-flight-recovery), confirm [host-clear probes](#ho ### What not to do -- Do not invoke `detox test`, `npx jet`, or `cd tests && …`; use repo-root `yarn tests::test-cover` (+ `:build` when needed). -- Do not background `:test-cover` and poll `pgrep`, `detox`, or `jet.js` for completion. +- Do not invoke the test runner (Jet), Detox, Metro, or emulators except through repo-root `yarn tests:*` commands in this doc — see [agent rule](#agent-rule-read-first). +- Do not run `:test-cover`, `:build`, Metro restart, or pre-flight while **`yarn` / `yarn lerna:prepare` is still in progress** — wait for exit 0 first ([prepare completion gate](#prepare-completion-gate-blocking)). +- Do not background `:test-cover` and poll `pgrep`, `detox`, or process names for completion. - Do not use `:test-cover-reuse`, `:test-cover-and-process`, or `:test-reuse` when measuring coverage or closing review gates. - Do not use `:8090` listening as “e2e still running” without the platform active signal above. - Do not start iOS/Android/macOS `:test-cover` concurrently on one host. - Do not edit source while a tee'd run is still in progress. - Do not passively tail tee output when progress markers stop — follow [stalled run detection](#stalled-run-detection). -- Do not run **full** harness (`require.context`, all modules) for **focused**/**area** tier — match [harness to tier](#3-harness-matches-validation-tier). -- Do not run `.github/workflows/scripts/boot-simulator.sh`, `simctl shutdown all`, or `kill -9` on `:8090` as prep. `boot-simulator.sh` is CI-only or internal to iOS Jet retry. +- Do not run **full** harness (`require.context`, all modules) for **unit-focused**/**area-focused** tier — match [harness to tier](#3-harness-matches-validation-tier). +- Do not run `.github/workflows/scripts/boot-simulator.sh`, `simctl shutdown all`, or `kill -9` on `:8090` as prep. `boot-simulator.sh` is CI-only or internal to iOS test-runner retry. ## Typical loop @@ -315,22 +371,120 @@ Full e2e loads every package. Narrow locally; **never commit** narrowing. | **Single-test narrowing** | `it.only(...)` | One case in a loaded file | | **Single-suite narrowing** | `describe.only(...)` | One block in a loaded file | -**Area narrowing** = `tests/app.js` / `tests/globals.js` only; not Jet `--grep` or packager `--target`. +**Area narrowing** = `tests/app.js` / `tests/globals.js` only; not test-runner `--grep` or packager `--target`. + + + +### Area harness in `tests/app.js` (two platform blocks) + +**Canonical owner** for how to narrow the test app. Package workflows name **which module/spec**; this section defines **how** to edit `tests/app.js` so narrowing works on **every** platform. + +#### Why two blocks + +Committed `tests/app.js` builds `platformSupportedModules` from **two separate blocks** — only one runs per platform at bundle time: + +| Block | Runs on | Committed role | +|-------|---------|----------------| +| `if (Platform.other) { … push … }` | **macOS / Other** | Full macOS module list | +| `if (!Platform.other) { … push … }` | **iOS / Android** | Full native module list | + +Committed shape: `const platformSupportedModules = []`, then **both** blocks push their full lists. + +**Common agent mistake:** set a narrowed initial array (e.g. `['app', 'firestore']`) but leave **`if (!Platform.other)`** pushing every native module → macOS looks narrowed (~700 firestore tests) while iOS/Android still run the **full app** (thousands of tests, unrelated modules like `database` appear in logs). That invalidates [harness narrowing gate](#harness-narrowing-gate-blocking) on native platforms. + +#### Apply narrowing (pick one pattern) + +**Pattern A — initial array + disable both blocks (recommended for one area on all platforms):** + +1. Replace `const platformSupportedModules = []` with the narrowed list (almost always include `'app'`). +2. Change **both** populate blocks to `if (false && Platform.other)` and `if (false && !Platform.other)` so neither block re-expands the list. +3. Add `// TEMP: area harness — never commit` above your edits. + +```javascript +// TEMP: firestore area harness — never commit +const platformSupportedModules = ['app', 'firestore']; + +if (false && Platform.other) { + // committed macOS list — disabled while narrowed + platformSupportedModules.push('app'); + // … +} + +if (false && !Platform.other) { + // committed iOS/Android list — disabled while narrowed + platformSupportedModules.push('app'); + // … +} +``` + +**Pattern B — trim inside each block (when macOS and native lists must differ):** + +1. Keep `const platformSupportedModules = []`. +2. Edit **`if (Platform.other)`** pushes to only the modules needed on macOS. +3. Edit **`if (!Platform.other)`** pushes to only the modules needed on iOS/Android. +4. **Both blocks must be edited** before `:test-cover` when the work item requires native platforms — editing only one block is invalid. + +Do **not** use `if (false && Platform.other)` on one block while leaving the other block active unless that asymmetry is intentional. + +#### Spec loading (optional second narrowing) + +Module specs load later in `loadTests()` via `platformSupportedModules.includes('')` — usually `require.context('../packages//e2e', …)`. + +| Goal | Change | +|------|--------| +| Full package area | Leave `require.context` as-is; narrowing the module list is enough | +| Single spec file | Replace `require.context` for that module with `require('../packages//e2e/.e2e.js')` ([sub-suite](#fail-fast-rnfbdebug-and-sub-suite-narrowing) — unit-focused diagnosis only unless package workflow says otherwise) | + +#### Revert before `full` tier or `commit` + +Restore committed harness on **both** blocks: + +1. `const platformSupportedModules = []` +2. `if (Platform.other) { … }` — full macOS list, **no** `false &&` +3. `if (!Platform.other) { … }` — full native list, **no** `false &&` +4. Restore any `require.context` edits; remove `// TEMP` comments +5. Revert `tests/globals.js` (`RNFBDebug = false`) per [before merge](#before-merge-pr-handoff) + +**Pre-flight check:** grep `tests/app.js` — if either block is `if (Platform.other)` / `if (!Platform.other)` **without** `false &&` and pushes modules outside your area, native or macOS runs are not narrowed. + +#### Sanity check by platform + +| Platform | Narrowed firestore-only (full `packages/firestore/e2e`) | Pipeline-only (`Pipeline.e2e.js`) | +|----------|--------------------------------------------------------|----------------------------------| +| macOS | ~**700** passing | ~**100** passing | +| iOS / Android | Same order of magnitude as macOS for the same spec scope | ~**100** passing | + +Pass counts in the **thousands** or unrelated suites (`database`, `crashlytics`, …) in the log → re-apply [both blocks](#tests-app-js-area-harness) and re-run. + +**Area example (Pattern A):** firestore-only `platformSupportedModules` + both blocks disabled; full firestore specs via existing `require.context` in `loadTests`. + +Package-specific spec names: [Firestore pipeline harness](../packages/firestore/pipeline-implementation-workflow.md#pipeline-area-harness), [namespace removal § module area harness](../namespace-api-removal-workflow.md#module-area-harness). + + + +### Fail-fast (`RNFBDebug`) and sub-suite narrowing + +**`RNFBDebug`** (`tests/globals.js`): for **`unit-focused`** and **`area-focused`** tiers, set `globalThis.RNFBDebug = true` **locally before the first `:test-cover`** — not optional. It prints per-case start/finish and **disables Mocha retry/backoff**, so failures surface immediately instead of burning time on retries. **Committed default must stay `false`.** Revert to `false` with other harness edits before **`full`** tier or commit ([§ before merge](#before-merge-pr-handoff)). + +| Kind | Mechanism | When | +|------|-----------|------| +| **Sub-suite narrowing** | `describe.only` / `require` one e2e file (e.g. `Aggregate/count.e2e.js` only) | **Unit-focused diagnosis only** — after [pre-flight](#pre-flight-is-the-host-clear-to-start) is clean and the **same failure repeats** on back-to-back runs without assertion progress. Never for **`area-focused`** gate closure (no `.only`). Never commit. | +| **Single-test narrowing** | `it.only(...)` | Same as sub-suite — unit-focused diagnosis only | -**Area example:** firestore-only modules + `require('../packages/firestore/e2e/Pipeline.e2e.js')`; mark `// TEMP: …`. +Package workflows may name default area specs (e.g. [Firestore pipeline harness](../packages/firestore/pipeline-implementation-workflow.md#pipeline-area-harness)); sub-suite narrowing is **tighter** than area narrowing for iteration speed. -**`RNFBDebug`** (`tests/globals.js`): `globalThis.RNFBDebug = true`; prints per-case start/finish and disables Mocha retry/backoff for fail-fast. +**E2e diagnosis escalation** (cross-package): [change authoring § implementation inner loop](change-authoring-workflow.md#e2e-diagnosis-escalation). -Package workflows may further restrict narrowing per [validation tier](#e2e-validation-tiers-focused-area-full). +Package workflows may further restrict narrowing per [validation tier](#e2e-validation-tiers-unit-focused-area-focused-full). -## E2e validation tiers (focused / area / full) +## E2e validation tiers (unit-focused / area-focused / full) -All tiers use [canonical commands](#rules), [host rule](iteration-vocabulary.md#host-rule), and clean [pre-flight](#pre-flight-is-the-host-clear-to-start). Tier names describe **scope**, not who runs the commands — see [iteration vocabulary](iteration-vocabulary.md). +All tiers use [canonical commands](#rules), [host rule](change-authoring-workflow.md#host-rule), and clean [pre-flight](#pre-flight-is-the-host-clear-to-start). Tier names describe **scope**, not who runs the commands — see [iteration vocabulary](iteration-vocabulary.md#validation-tier-identifiers). | Validation tier | E2e scope | Narrowing allowed | Typical work type | |-----------------|-----------|-------------------|-------------------| -| **Focused** | Fast loop while product code is changing | `it.only` / `describe.only` / tight area narrowing in `tests/app.js` — **never commit** | `implementation` | -| **Area** | Full loaded spec(s) for the package/area under change | **Area narrowing required** in `tests/app.js` / `tests/globals.js`; **no** `.only` | `baseline-capture`, `independent-review` | +| **Unit-focused** | Fast loop while product code is changing | `it.only` / `describe.only` / tight area narrowing in `tests/app.js` — **never commit** | `implementation` | +| **Area-focused** | Full loaded spec(s) for the package/area under change | **Area narrowing required** in `tests/app.js` / `tests/globals.js`; **no** `.only` | `baseline-capture`, `independent-review` | | **Full** | Unfocused — all modules, all platforms | None — revert all narrowing | `pre-merge-validation` | **Universal rules:** @@ -338,19 +492,19 @@ All tiers use [canonical commands](#rules), [host rule](iteration-vocabulary.md# - E2e is **always serial** — one `:test-cover` at a time on the host. - Every run starts from **verified [pre-flight](#pre-flight-is-the-host-clear-to-start)**; if probes fail, [pre-flight recovery](#pre-flight-recovery) before another run. - Use **only** canonical commands from this doc. -- Never overlap focused-tier and area-tier `:test-cover` on one host. +- Never overlap unit-focused-tier and area-focused-tier `:test-cover` on one host. -See also: [focused-tier loop](#focused-tier-iteration-loop), [dispatch](#serialized-e2e-dispatch), [pre-merge](#before-merge-pr-handoff). +See also: [unit-focused-tier loop](#unit-focused-tier-iteration-loop), [dispatch](#serialized-e2e-dispatch), [pre-merge](#before-merge-pr-handoff). ## Environment - **Devices** — Detox boots simulator/emulator (`iPhone 17` on iOS, `TestingAVD` on Android); [host-clear probes](#host-clear-probes) require zero booted iOS simulators before `:test-cover`. macOS auto-starts app. - **adb empty** — `adb kill-server && adb start-server && adb devices` -- **Stale processes** — one Metro (`:8081`), one emulator set (`:8080`, `:9099`, `:9000`, `:4400`, …). Stray Jet on `:8090` after a run → [pre-flight recovery](#pre-flight-recovery). Restart Metro/emulators: `pkill -f "react-native start"`, `pkill -f "firebase emulators"`, then [Rules §1–2](#rules). +- **Stale processes** — one Metro (`:8081`), one emulator set (`:8080`, `:9099`, `:9000`, `:4400`, …). Stray listener on `:8090` after a run → [pre-flight recovery](#pre-flight-recovery), then restart background services with [Rules §1–2](#rules) (`yarn tests:packager:jet`, `yarn tests:emulator:start`). ## Diagnosing hangs -**Local stalls** — see [stalled run detection](#stalled-run-detection) first (Metro `/status`, Jet connect markers). +**Local stalls** — see [stalled run detection](#stalled-run-detection) first (Metro `/status`, `Jet client connected` markers). **Native / device logs** (remove instrumentation before merge): @@ -366,7 +520,7 @@ See also: [focused-tier loop](#focused-tier-iteration-loop), [dispatch](#seriali Pre-merge applies once to the branch commit stream before merge/push intended for merge, not after every commit. -1. Revert all narrowing ([full tier](#e2e-validation-tiers-focused-area-full)): restore `tests/app.js` (`platformSupportedModules` + `require.context`), default `RNFBDebug` in `tests/globals.js`, remove all `.only`, remove native instrumentation. +1. Revert all narrowing ([full tier](#e2e-validation-tiers-unit-focused-area-focused-full)): restore `tests/app.js` (`platformSupportedModules` + `require.context`), default `RNFBDebug` in `tests/globals.js`, remove all `.only`, remove native instrumentation. 2. [Pre-flight](#pre-flight-is-the-host-clear-to-start) — [host-clear probes](#host-clear-probes) pass before each platform run. 3. Rebuild if needed (`tests::build`; `yarn lerna:prepare` for `lib/**`). 4. Full unfocused suite with coverage on **iOS, Android, macOS** — one platform at a time, all green. diff --git a/okf-bundle/testing/validation-checklist.md b/okf-bundle/testing/validation-checklist.md index 42f26ca355..8a4a45cb5f 100644 --- a/okf-bundle/testing/validation-checklist.md +++ b/okf-bundle/testing/validation-checklist.md @@ -14,27 +14,32 @@ Coverage acceptance: [expectations](coverage-design.md#coverage-expectations-pol ## When to run what -Work types and tiers: [iteration vocabulary](iteration-vocabulary.md). +Work types and tiers: [change authoring workflow](change-authoring-workflow.md). Term ids: [iteration vocabulary](iteration-vocabulary.md). | Work type | Scope | Shortcuts | |-----------|--------|-----------| | `gap-analysis` | `compare:types`, config read, SDK declarations | n/a | -| `baseline-capture` | Full loaded spec(s) + e2e coverage on macOS, iOS, Android | **area** tier; [area narrowing required](running-e2e.md#harness-narrowing-gate-blocking); no `.only`, no `:test-cover-reuse` | -| `implementation` | Focused Jest + e2e | **focused** tier; [area narrowing required before `:test-cover`](running-e2e.md#harness-narrowing-gate-blocking) + optional `.only`; [pipelines workflow](../packages/firestore/pipeline-implementation-workflow.md#narrowing-during-pipeline-iterations) | -| `independent-review` | Full checklist below on all platforms | **area** tier; [area narrowing required](running-e2e.md#harness-narrowing-gate-blocking); [frozen tree](iteration-vocabulary.md#frozen-tree); never commit narrowing | +| `baseline-capture` | Full loaded spec(s) + e2e on [**every required platform**](running-e2e.md#platform-coverage-gate-blocking) | **area-focused** tier; [area narrowing required](running-e2e.md#harness-narrowing-gate-blocking); no `.only`, no `:test-cover-reuse`; **no platform shortcuts** | +| `implementation` | Unit-focused Jest + e2e on required platforms when native/TS path needs it | **unit-focused** tier; [area narrowing + RNFBDebug=true locally](running-e2e.md#fail-fast-rnfbdebug-and-sub-suite-narrowing) before `:test-cover`; optional `.only` / sub-suite for diagnosis; [platform coverage gate](running-e2e.md#platform-coverage-gate-blocking) when module loads on iOS and Android | +| `independent-review` | Full checklist; e2e on **every required platform** (macOS / iOS / Android per harness) | **area-focused** tier; [platform coverage gate](running-e2e.md#platform-coverage-gate-blocking) — **no shortcuts**; [frozen tree](change-authoring-workflow.md#frozen-tree); never commit narrowing, sub-suite `.only`, or `RNFBDebug = true` ([fail-fast §](running-e2e.md#fail-fast-rnfbdebug-and-sub-suite-narrowing)) | | `pre-merge-validation` | Full unfocused suite | **full** tier — [running-e2e § merge](running-e2e.md#before-merge-pr-handoff); entire PR branch, once | ## Prepare and compile -Repo root unless noted: +Repo root. **Agents:** [agent command policy](agent-command-policy.md) — only these invocations; never `yarn workspace … prepare` or package-scoped `yarn run build` for diagnostics. ```bash -yarn lerna:prepare # after packages/*/lib/** edits (Metro serves dist/) -cd packages/ && yarn compile # package under change +yarn # install + postinstallDev (includes lerna:prepare) +yarn lerna:prepare # after packages/*/lib/** edits — transpiles lib → dist/module via each package prepare target +yarn lerna run prepare --scope @react-native-firebase/ # single package only when needed yarn tsc:compile yarn tsc:compile:consumer ``` +`yarn lerna:prepare` runs each package **`prepare`** script (`build` then `compile`/bob). That is the canonical **`lib/**` → `dist/module/**`** path. Do **not** use `cd packages/ && yarn compile` as a substitute — `compile` is a step **inside** `prepare`, not a standalone agent entrypoint. + +**Blocking:** `yarn` and `yarn lerna:prepare` must **exit 0 before any other command** (Jest, tsc, e2e, Metro, builds) — never parallelize. [Agent command policy § prepare must finish first](agent-command-policy.md#prepare-must-finish-first); e2e pre-flight: [running e2e § prepare completion gate](running-e2e.md#prepare-completion-gate-blocking). + ## API reference and type parity ```bash @@ -44,6 +49,14 @@ yarn compare:types # remove stale config entries when fixed Configs: `.github/scripts/compare-types/configs/`. Package workflows define ordering (e.g. [pipelines](../packages/firestore/pipeline-implementation-workflow.md#step-1--compare-types-gap-analysis)). +For any package registered in `compare:types`, type parity is a **review-gate requirement**, not a best-effort signal. Before closing `independent-review`, the touched package must have: + +- no undocumented differences, +- no stale config entries, +- and any intentional RN-only exports documented in that package config. + +If `yarn compare:types` fails because of unrelated packages, keep the touched package's result in the handoff and add/fix a work-queue item for the unrelated drift. Do not close a review gate for a registered package when its own compare-types output is failing. + ## Jest ```bash @@ -58,20 +71,32 @@ yarn tests:jest --watchman=false packages/firestore/__tests__/pipelines.test.ts Optional: `yarn tests:jest-coverage`. + + ## Lint and formatting +**Blocking before `implementation` handoff and on the frozen tree for `independent-review`.** Run from repo root after prepare/compile when TS/JS changed. + +```bash +yarn lint:js # eslint packages/* — must exit 0 +yarn lint:js --fix # auto-fix; re-run yarn lint:js until clean +yarn format:js # inspect diff after; prefer lint:js --fix first +``` + +Docs (when `docs/**` or OKF markdown changed): + ```bash -yarn lint:js -yarn format:js # inspect diff after +yarn lint:markdown +yarn lint:spellcheck ``` -Docs: `yarn lint:markdown`, `yarn lint:spellcheck`. +Native (when platform sources in diff): `yarn lint:android`, `yarn lint:ios:check`. `lint:android` can flake; rerun once/twice if failure is not clearly in diff. -Native: `yarn lint:android`, `yarn lint:ios:check`. `lint:android` can flake; rerun once/twice if failure is not clearly in diff. +Full aggregate (pre-merge optional): `yarn lint` (= js + android + ios check). ## E2e with coverage -[Pre-flight](running-e2e.md#pre-flight-is-the-host-clear-to-start) (host-clear probes + services + harness tier) before every run. Match harness to work type — **focused**/**area** never use full app load ([running e2e § harness](running-e2e.md#3-harness-matches-validation-tier)). +[Pre-flight](running-e2e.md#pre-flight-is-the-host-clear-to-start) (host-clear probes + services + harness tier) before every run — [agent command policy](agent-command-policy.md) and [e2e agent rule](running-e2e.md#agent-rule-read-first): use **only** `yarn tests:*` commands from [running e2e](running-e2e.md). Match harness to work type — **unit-focused**/**area-focused** never use full app load ([running e2e § harness](running-e2e.md#3-harness-matches-validation-tier)). Commands: [Running e2e tests](running-e2e.md). Post-process: [Coverage design](coverage-design.md) (iOS `tests:ios:test:process-coverage`, Android `tests:android:post-e2e-coverage`). @@ -89,14 +114,13 @@ Goal: each iteration improves OKF and removes conflicting guidance. ## Handoff checklist -- [ ] `yarn lerna:prepare` -- [ ] `cd packages/ && yarn compile` +- [ ] `yarn lerna:prepare` (after any `packages/*/lib/**` edits) - [ ] `yarn tsc:compile`, `yarn tsc:compile:consumer` - [ ] `yarn reference:api` - [ ] `yarn tests:jest` - [ ] `yarn compare:types` (stale config entries removed) - [ ] `yarn lint:js` (+ markdown/spellcheck if docs; + platform lint if native) -- [ ] E2e green on macOS, iOS, Android ([running-e2e](running-e2e.md); [harness narrowing gate](running-e2e.md#harness-narrowing-gate-blocking) applied; no `.only`) +- [ ] E2e green on **every required platform** for the changed module ([platform coverage gate](running-e2e.md#platform-coverage-gate-blocking); [harness narrowing gate](running-e2e.md#harness-narrowing-gate-blocking); no `.only`; committed `RNFBDebug` remains `false`) - [ ] Native coverage post-processing per [coverage-design](coverage-design.md) - [ ] [Coverage policy](coverage-design.md#coverage-expectations-policy) satisfied on touched files - [ ] OKF bundle reviewed/updated per § above diff --git a/packages/ai/lib/models/utils.ts b/packages/ai/lib/models/utils.ts index 1ac7cc4a08..368d326111 100644 --- a/packages/ai/lib/models/utils.ts +++ b/packages/ai/lib/models/utils.ts @@ -14,7 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { FirebaseAppCheckTypes } from '@react-native-firebase/app-check'; +import type { AppCheck } from '@react-native-firebase/app-check'; +import { getLimitedUseToken, getToken } from '@react-native-firebase/app-check'; import { AIError } from '../errors'; import { AI, AIErrorCode } from '../public-types'; import { AIService } from '../service'; @@ -52,10 +53,10 @@ export function initApiSettings(ai: AI): ApiSettings { backend: ai.backend, }; - const appCheck = ai.appCheck as FirebaseAppCheckTypes.Module; + const appCheck = ai.appCheck as AppCheck; if (appCheck) { apiSettings.getAppCheckToken = () => - ai.options?.useLimitedUseAppCheckTokens ? appCheck.getLimitedUseToken() : appCheck.getToken(); + ai.options?.useLimitedUseAppCheckTokens ? getLimitedUseToken(appCheck) : getToken(appCheck); } if ((ai as AIService).auth?.currentUser) { diff --git a/packages/ai/lib/public-types.ts b/packages/ai/lib/public-types.ts index e6b0b9c765..c0e8078df2 100644 --- a/packages/ai/lib/public-types.ts +++ b/packages/ai/lib/public-types.ts @@ -16,8 +16,8 @@ */ import { ReactNativeFirebase } from '@react-native-firebase/app'; -import { FirebaseAuthTypes } from '@react-native-firebase/auth'; -import { FirebaseAppCheckTypes } from '@react-native-firebase/app-check'; +import type { Auth } from '@react-native-firebase/auth'; +import type { AppCheck } from '@react-native-firebase/app-check'; import { Backend } from './backend'; export * from './types'; @@ -40,8 +40,8 @@ export interface AIOptions { * Whether to use App Check limited use tokens. Defaults to false. */ useLimitedUseAppCheckTokens?: boolean; - appCheck?: FirebaseAppCheckTypes.Module | null; - auth?: FirebaseAuthTypes.Module | null; + appCheck?: AppCheck | null; + auth?: Auth | null; } /** @@ -90,8 +90,8 @@ export interface AI { * The {@link @firebase/app!FirebaseApp} this {@link AI} instance is associated with. */ app: ReactNativeFirebase.FirebaseApp; - appCheck?: FirebaseAppCheckTypes.Module | null; - auth?: FirebaseAuthTypes.Module | null; + appCheck?: AppCheck | null; + auth?: Auth | null; /** * A {@link Backend} instance that specifies the configuration for the target backend, * either the Gemini Developer API (using {@link GoogleAIBackend}) or the diff --git a/packages/ai/lib/service.ts b/packages/ai/lib/service.ts index 79bf741303..f2570f0724 100644 --- a/packages/ai/lib/service.ts +++ b/packages/ai/lib/service.ts @@ -17,20 +17,20 @@ import { ReactNativeFirebase } from '@react-native-firebase/app'; import { AI, Backend } from './public-types'; -import { FirebaseAuthTypes } from '@react-native-firebase/auth'; -import { FirebaseAppCheckTypes } from '@react-native-firebase/app-check'; +import type { Auth } from '@react-native-firebase/auth'; +import type { AppCheck } from '@react-native-firebase/app-check'; import { VertexAIBackend } from './backend'; export class AIService implements AI { - auth: FirebaseAuthTypes.Module | null; - appCheck: FirebaseAppCheckTypes.Module | null; + auth: Auth | null; + appCheck: AppCheck | null; location: string; constructor( public app: ReactNativeFirebase.FirebaseApp, public backend: Backend, - auth?: FirebaseAuthTypes.Module, - appCheck?: FirebaseAppCheckTypes.Module, + auth?: Auth, + appCheck?: AppCheck, ) { this.auth = auth || null; this.appCheck = appCheck || null; diff --git a/packages/ai/lib/types/internal.ts b/packages/ai/lib/types/internal.ts index 8b51e8c846..5e47e83388 100644 --- a/packages/ai/lib/types/internal.ts +++ b/packages/ai/lib/types/internal.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FirebaseAppCheckTypes } from '@react-native-firebase/app-check'; +import type { AppCheckTokenResult } from '@react-native-firebase/app-check'; import { Backend } from '../public-types'; export interface ApiSettings { @@ -28,5 +28,5 @@ export interface ApiSettings { automaticDataCollectionEnabled?: boolean; backend: Backend; getAuthToken?: () => Promise; - getAppCheckToken?: () => Promise; + getAppCheckToken?: () => Promise; } diff --git a/packages/analytics/__tests__/analytics.test.ts b/packages/analytics/__tests__/analytics.test.ts index ed94b33396..24d683d050 100644 --- a/packages/analytics/__tests__/analytics.test.ts +++ b/packages/analytics/__tests__/analytics.test.ts @@ -1,10 +1,6 @@ -import { jest, afterAll, beforeAll, describe, expect, it, xit, beforeEach } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; -// @ts-ignore test -import FirebaseModule from '@react-native-firebase/app/dist/module/internal/FirebaseModule'; - -import analytics, { - firebase, +import { getAnalytics, initializeAnalytics, getGoogleAnalyticsClientId, @@ -63,708 +59,7 @@ import analytics, { settings, } from '../lib'; -// @ts-ignore test -import { createCheckV9Deprecation } from '@react-native-firebase/app/dist/module/common/unitTestUtils'; -// @ts-ignore test -import type { CheckV9DeprecationFunction } from '@react-native-firebase/app/dist/module/common/unitTestUtils'; - describe('Analytics', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.analytics).toBeDefined(); - expect(app.analytics().app).toEqual(app); - }); - - it('throws if non default app arg provided to firebase.analytics(APP)', function () { - const app = firebase.app('secondaryFromNative'); - - const expectedError = [ - 'You attempted to call "firebase.analytics(app)" but; analytics does not support multiple Firebase Apps.', - '', - 'Ensure the app provided is the default Firebase app only and not the "secondaryFromNative" app.', - ].join('\r\n'); - - // @ts-ignore test - expect(() => firebase.analytics(app)).toThrow(expectedError); - }); - - it('throws if analytics access from a non default app', function () { - const app = firebase.app('secondaryFromNative'); - - const expectedError = [ - 'You attempted to call "firebase.app(\'secondaryFromNative\').analytics" but; analytics does not support multiple Firebase Apps.', - '', - 'Ensure you access analytics from the default application only.', - ].join('\r\n'); - - expect(() => app.analytics()).toThrow(expectedError); - }); - - // TODO in app/registry/namespace.js - if (!hasCustomUrlOrRegionSupport) - xit('throws if args provided to firebase.app().analytics(ARGS)', function () { - try { - // @ts-ignore test - firebase.app().analytics('foo', 'arg2'); - return Promise.reject(new Error('Did not throw')); - } catch (e: any) { - e.message.should.containEql('does not support multiple Firebase Apps'); - return Promise.resolve(); - } - }); - - it('errors if milliseconds not a number', function () { - // @ts-ignore test - expect(() => firebase.analytics().setSessionTimeoutDuration('123')).toThrow( - "'milliseconds' expected a number value", - ); - }); - - it('throws if none string none null values', function () { - // @ts-ignore test - expect(() => firebase.analytics().setUserId(123)).toThrow("'id' expected a string value"); - }); - - it('throws if name is not a string', function () { - // @ts-ignore test - expect(() => firebase.analytics().setUserProperty(1337, 'invertase')).toThrow( - "'name' expected a string value", - ); - }); - - it('throws if value is invalid', function () { - // @ts-ignore test - expect(() => firebase.analytics().setUserProperty('invertase3', 33.3333)).toThrow( - "'value' expected a string value", - ); - }); - - it('throws if properties is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().setUserProperties(1337)).toThrow( - "'properties' expected an object of key/value pairs", - ); - }); - - it('throws if property value is invalid', function () { - const props = { - test: '123', - foo: { - bar: 'baz', - }, - }; - // @ts-ignore test - expect(() => firebase.analytics().setUserProperties(props)).toThrow( - "'properties' value for parameter 'foo' is invalid", - ); - }); - - it('throws if value is a number', function () { - // @ts-ignore test - expect(() => firebase.analytics().setUserProperties({ invertase1: 123 })).toThrow( - "'properties' value for parameter 'invertase1' is invalid, expected a string.", - ); - }); - - it('throws if consentSettings is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().setConsent(1337)).toThrow( - 'The supplied arg must be an object of key/values.', - ); - }); - - it('throws if consentSettings is invalid', function () { - const consentSettings = { - ad_storage: true, - foo: { - bar: 'baz', - }, - }; - // @ts-ignore test - expect(() => firebase.analytics().setConsent(consentSettings)).toThrow( - "'consentSettings' value for parameter 'foo' is invalid, expected a boolean.", - ); - }); - - it('throws if one value of consentSettings is a number', function () { - // @ts-ignore test - expect(() => firebase.analytics().setConsent({ ad_storage: 123 })).toThrow( - "'consentSettings' value for parameter 'ad_storage' is invalid, expected a boolean.", - ); - }); - - it('throws if security_storage of consentSettings is not one of granted or denied', function () { - // @ts-ignore test - expect(() => firebase.analytics().setConsent({ security_storage: 'invalid' })).toThrow( - "'consentSettings' value for parameter 'security_storage' is invalid, expected one of 'granted' or 'denied'.", - ); - }); - - it('accepts security_storage of consentSettings is one of granted or denied', function () { - expect(() => firebase.analytics().setConsent({ security_storage: 'granted' })).not.toThrow(); - expect(() => firebase.analytics().setConsent({ security_storage: 'denied' })).not.toThrow(); - }); - - it('errors when no parameters are set', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSearch()).toThrow( - 'The supplied arg must be an object of key/values', - ); - }); - - describe('logEvent()', function () { - it('errors if name is not a string', function () { - // @ts-ignore test - expect(() => firebase.analytics().logEvent(123)).toThrow( - "firebase.analytics().logEvent(*) 'name' expected a string value.", - ); - }); - - it('errors if params is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logEvent('invertase_event', 'foobar')).toThrow( - "firebase.analytics().logEvent(_, *) 'params' expected an object value.", - ); - }); - - it('errors on using a reserved name', function () { - expect(() => firebase.analytics().logEvent('session_start')).toThrow( - "firebase.analytics().logEvent(*) 'name' the event name 'session_start' is reserved and can not be used.", - ); - }); - - it('errors if name not alphanumeric', function () { - expect(() => firebase.analytics().logEvent('!@£$%^&*')).toThrow( - "firebase.analytics().logEvent(*) 'name' invalid event name '!@£$%^&*'. Names should contain 1 to 40 alphanumeric characters or underscores.", - ); - }); - - describe('logScreenView()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logScreenView(123)).toThrow( - 'firebase.analytics().logScreenView(*):', - ); - }); - - it('accepts arbitrary custom event parameters while rejecting defined parameters with wrong types', function () { - expect(() => firebase.analytics().logScreenView({ foo: 'bar' })).not.toThrow(); - expect(() => - // @ts-ignore test - firebase.analytics().logScreenView({ screen_name: 123, foo: 'bar' }), - ).toThrow('firebase.analytics().logScreenView(*):'); - }); - }); - - describe('logAddPaymentInfo()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logAddPaymentInfo(123)).toThrow( - 'firebase.analytics().logAddPaymentInfo(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logAddPaymentInfo({ - value: 123, - }), - ).toThrow('firebase.analytics().logAddPaymentInfo(*):'); - }); - }); - }); - - describe('setDefaultEventParameters()', function () { - it('errors if params is not a object', function () { - // @ts-ignore test - expect(() => firebase.analytics().setDefaultEventParameters('123')).toThrow( - "firebase.analytics().setDefaultEventParameters(*) 'params' expected an object value when it is defined.", - ); - }); - }); - - describe('logAddToCart()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logAddToCart(123)).toThrow( - 'firebase.analytics().logAddToCart(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logAddToCart({ - value: 123, - }), - ).toThrow('firebase.analytics().logAddToCart(*):'); - }); - }); - - describe('logAddShippingInfo()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logAddShippingInfo(123)).toThrow( - 'firebase.analytics().logAddShippingInfo(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logAddShippingInfo({ - value: 123, - }), - ).toThrow('firebase.analytics().logAddShippingInfo(*):'); - }); - }); - - describe('logAddToWishlist()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logAddToWishlist(123)).toThrow( - 'firebase.analytics().logAddToWishlist(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logAddToWishlist({ - value: 123, - }), - ).toThrow('firebase.analytics().logAddToWishlist(*):'); - }); - - it('items accept arbitrary custom event parameters', function () { - expect(() => - firebase.analytics().logAddToWishlist({ items: [{ foo: 'bar' }] }), - ).not.toThrow(); - }); - }); - - describe('logBeginCheckout()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logBeginCheckout(123)).toThrow( - 'firebase.analytics().logBeginCheckout(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logBeginCheckout({ - value: 123, - }), - ).toThrow('firebase.analytics().logBeginCheckout(*):'); - }); - - it('accepts arbitrary custom event parameters', function () { - expect(() => - firebase.analytics().logBeginCheckout({ - value: 123, - currency: 'EUR', - foo: 'bar', - }), - ).not.toThrow(); - }); - }); - - describe('logGenerateLead()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logGenerateLead(123)).toThrow( - 'firebase.analytics().logGenerateLead(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logGenerateLead({ - value: 123, - }), - ).toThrow('firebase.analytics().logGenerateLead(*):'); - }); - }); - - describe('logCampaignDetails()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logCampaignDetails(123)).toThrow( - 'firebase.analytics().logCampaignDetails(*):', - ); - }); - }); - - describe('logEarnVirtualCurrency()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logEarnVirtualCurrency(123)).toThrow( - 'firebase.analytics().logEarnVirtualCurrency(*):', - ); - }); - }); - - describe('logJoinGroup()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logJoinGroup(123)).toThrow( - 'firebase.analytics().logJoinGroup(*):', - ); - }); - }); - - describe('logLevelEnd()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logLevelEnd(123)).toThrow( - 'firebase.analytics().logLevelEnd(*):', - ); - }); - }); - - describe('logLevelStart()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logLevelStart(123)).toThrow( - 'firebase.analytics().logLevelStart(*):', - ); - }); - }); - - describe('logLevelUp()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logLevelUp(123)).toThrow( - 'firebase.analytics().logLevelUp(*):', - ); - }); - }); - - describe('logLogin()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logLogin(123)).toThrow( - 'firebase.analytics().logLogin(*):', - ); - }); - }); - - describe('logPostScore()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logPostScore(123)).toThrow( - 'firebase.analytics().logPostScore(*):', - ); - }); - }); - - describe('logSelectContent()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSelectContent(123)).toThrow( - 'firebase.analytics().logSelectContent(*):', - ); - }); - }); - - describe('logSearch()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSearch(123)).toThrow( - 'firebase.analytics().logSearch(*):', - ); - }); - }); - - describe('logSelectItem()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSelectItem(123)).toThrow( - 'firebase.analytics().logSelectItem(*):', - ); - }); - }); - - describe('logSetCheckoutOption()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSetCheckoutOption(123)).toThrow( - 'firebase.analytics().logSetCheckoutOption(*):', - ); - }); - }); - - describe('logShare()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logShare(123)).toThrow( - 'firebase.analytics().logShare(*):', - ); - }); - }); - - describe('logSignUp()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSignUp(123)).toThrow( - 'firebase.analytics().logSignUp(*):', - ); - }); - }); - - describe('logSelectPromotion()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSelectPromotion(123)).toThrow( - 'firebase.analytics().logSelectPromotion(*):', - ); - }); - }); - - describe('logSpendVirtualCurrency()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logSpendVirtualCurrency(123)).toThrow( - 'firebase.analytics().logSpendVirtualCurrency(*):', - ); - }); - }); - - describe('logUnlockAchievement()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logUnlockAchievement(123)).toThrow( - 'firebase.analytics().logUnlockAchievement(*):', - ); - }); - }); - - describe('logPurchase()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logPurchase(123)).toThrow( - 'firebase.analytics().logPurchase(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logPurchase({ - value: 123, - }), - ).toThrow('firebase.analytics().logPurchase(*):'); - }); - - it('accepts arbitrary custom event parameters', function () { - expect(() => - firebase.analytics().logPurchase({ - value: 123, - currency: 'EUR', - foo: 'bar', - }), - ).not.toThrow(); - }); - }); - - describe('logRefund()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logRefund(123)).toThrow( - 'firebase.analytics().logRefund(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logRefund({ - value: 123, - }), - ).toThrow('firebase.analytics().logRefund(*):'); - }); - }); - - describe('logViewCart()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logViewCart(123)).toThrow( - 'firebase.analytics().logViewCart(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logViewCart({ - value: 123, - }), - ).toThrow('firebase.analytics().logViewCart(*):'); - }); - }); - - describe('logViewItem()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logViewItem(123)).toThrow( - 'firebase.analytics().logViewItem(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logViewItem({ - value: 123, - }), - ).toThrow('firebase.analytics().logViewItem(*):'); - }); - }); - - describe('logViewItemList()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logViewItemList(123)).toThrow( - 'firebase.analytics().logViewItemList(*):', - ); - }); - }); - - describe('logRemoveFromCart()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logRemoveFromCart(123)).toThrow( - 'firebase.analytics().logRemoveFromCart(*):', - ); - }); - - it('errors when compound values are not set', function () { - expect(() => - firebase.analytics().logRemoveFromCart({ - value: 123, - }), - ).toThrow('firebase.analytics().logRemoveFromCart(*):'); - }); - }); - - describe('logViewPromotion()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logViewPromotion(123)).toThrow( - 'firebase.analytics().logViewPromotion(*):', - ); - }); - }); - - describe('logViewSearchResults()', function () { - it('errors if param is not an object', function () { - // @ts-ignore test - expect(() => firebase.analytics().logViewSearchResults(123)).toThrow( - 'firebase.analytics().logViewSearchResults(*):', - ); - }); - }); - - describe('setAnalyticsCollectionEnabled()', function () { - it('throws if not a boolean', function () { - // @ts-ignore - expect(() => firebase.analytics().setAnalyticsCollectionEnabled('foo')).toThrow( - "firebase.analytics().setAnalyticsCollectionEnabled(*) 'enabled' expected a boolean value.", - ); - }); - }); - - describe('initiateOnDeviceConversionMeasurementWithEmailAddress()', function () { - it('throws if not a string', function () { - expect(() => - // @ts-ignore - firebase.analytics().initiateOnDeviceConversionMeasurementWithEmailAddress(true), - ).toThrow( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithEmailAddress(*) 'emailAddress' expected a string value.", - ); - }); - }); - - describe('initiateOnDeviceConversionMeasurementWithHashedEmailAddress()', function () { - it('throws if not a string', function () { - expect(() => - // @ts-ignore - firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(true), - ).toThrow( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.", - ); - }); - }); - - describe('initiateOnDeviceConversionMeasurementWithHashedPhoneNumber()', function () { - it('throws if not a string', function () { - expect(() => - // @ts-ignore - firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(1234), - ).toThrow( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.", - ); - }); - - it('throws if hashed value is a phone number in E.164 format', function () { - expect(() => - firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithHashedPhoneNumber('+1234567890'), - ).toThrow( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.", - ); - }); - }); - - describe('TypeScript migration maintains existing `analytics()` exports', function () { - it('`analytics()` is properly exposed to end user', function () { - expect(analytics().logAddToCart).toBeDefined(); - expect(analytics().logAddPaymentInfo).toBeDefined(); - expect(analytics().logAddShippingInfo).toBeDefined(); - expect(analytics().logAddToWishlist).toBeDefined(); - expect(analytics().logAppOpen).toBeDefined(); - expect(analytics().logBeginCheckout).toBeDefined(); - expect(analytics().logCampaignDetails).toBeDefined(); - expect(analytics().logEarnVirtualCurrency).toBeDefined(); - expect(analytics().logEvent).toBeDefined(); - expect(analytics().logGenerateLead).toBeDefined(); - expect(analytics().logJoinGroup).toBeDefined(); - expect(analytics().logLevelEnd).toBeDefined(); - expect(analytics().logLevelStart).toBeDefined(); - expect(analytics().logLevelUp).toBeDefined(); - expect(analytics().logLogin).toBeDefined(); - expect(analytics().logPostScore).toBeDefined(); - expect(analytics().logSelectContent).toBeDefined(); - expect(analytics().logSetCheckoutOption).toBeDefined(); - expect(analytics().logShare).toBeDefined(); - expect(analytics().logSignUp).toBeDefined(); - expect(analytics().logSpendVirtualCurrency).toBeDefined(); - expect(analytics().logTutorialBegin).toBeDefined(); - expect(analytics().logTutorialComplete).toBeDefined(); - expect(analytics().logUnlockAchievement).toBeDefined(); - expect(analytics().logViewItem).toBeDefined(); - expect(analytics().logViewItemList).toBeDefined(); - expect(analytics().resetAnalyticsData).toBeDefined(); - expect(analytics().logViewCart).toBeDefined(); - expect(analytics().setAnalyticsCollectionEnabled).toBeDefined(); - expect(analytics().logSelectPromotion).toBeDefined(); - expect(analytics().logScreenView).toBeDefined(); - expect(analytics().logViewPromotion).toBeDefined(); - expect(analytics().setSessionTimeoutDuration).toBeDefined(); - expect(analytics().setUserId).toBeDefined(); - expect(analytics().setUserProperties).toBeDefined(); - expect(analytics().logViewSearchResults).toBeDefined(); - expect(analytics().setUserProperty).toBeDefined(); - expect(analytics().setConsent).toBeDefined(); - }); - }); - }); - describe('modular', function () { it('`getAnalytics` function is properly exposed to end user', function () { expect(getAnalytics).toBeDefined(); @@ -1007,979 +302,4 @@ describe('Analytics', function () { expect(logTransaction).toBeDefined(); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let analyticsRefV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - analyticsRefV9Deprecation = createCheckV9Deprecation(['analytics']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - source: 'cache', - changes: [], - documents: [], - metadata: {}, - path: 'foo', - } as never), - }, - ); - }); - }); - - describe('Analytics', function () { - it('analytics.logEvent()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => logEvent(analytics, 'invertase_event'), - () => analytics.logEvent('invertase_event'), - 'logEvent', - ); - }); - - it('analytics.setAnalyticsCollectionEnabled()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => setAnalyticsCollectionEnabled(analytics, true), - () => analytics.setAnalyticsCollectionEnabled(true), - 'setAnalyticsCollectionEnabled', - ); - }); - - it('analytics.setSessionTimeoutDuration()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => setSessionTimeoutDuration(analytics, 180000), - () => analytics.setSessionTimeoutDuration(180000), - 'setSessionTimeoutDuration', - ); - }); - - it('analytics.getAppInstanceId()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => getAppInstanceId(analytics), - () => analytics.getAppInstanceId(), - 'getAppInstanceId', - ); - }); - - it('analytics.getSessionId()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => getSessionId(analytics), - () => analytics.getSessionId(), - 'getSessionId', - ); - }); - - it('analytics.setUserId()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => setUserId(analytics, 'id'), - () => analytics.setUserId('id'), - 'setUserId', - ); - }); - - it('analytics.setUserProperty()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => setUserProperty(analytics, 'prop', 'value'), - () => analytics.setUserProperty('prop', 'value'), - 'setUserProperty', - ); - }); - - it('analytics.setUserProperties()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => setUserProperties(analytics, { prop: 'value' }), - () => analytics.setUserProperties({ prop: 'value' }), - 'setUserProperties', - ); - }); - - it('analytics.resetAnalyticsData()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => resetAnalyticsData(analytics), - () => analytics.resetAnalyticsData(), - 'resetAnalyticsData', - ); - }); - - it('analytics.setConsent()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => setConsent(analytics, { ad_storage: true }), - () => analytics.setConsent({ ad_storage: true }), - 'setConsent', - ); - }); - - it('analytics.logAddPaymentInfo()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => analytics.logAddPaymentInfo({ value: 1, currency: 'usd' }), - 'logAddPaymentInfo', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => logAddPaymentInfo(analytics, { value: 1, currency: 'usd' }), - 'logAddPaymentInfo', - ); - }); - - it('analytics.logScreenView()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logScreenView({ - screen_class: 'ProductScreen', - screen_name: 'ProductScreen', - }), - 'logScreenView', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logScreenView(analytics, { - screen_class: 'ProductScreen', - screen_name: 'ProductScreen', - }), - 'logScreenView', - ); - }); - - it('analytics.logAddShippingInfo()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logAddShippingInfo({ - value: 100, - currency: 'usd', - }), - 'logAddShippingInfo', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logAddShippingInfo(analytics, { - value: 100, - currency: 'usd', - }), - 'logAddShippingInfo', - ); - }); - - it('analytics.logAddToCart()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logAddToCart({ - value: 100, - currency: 'usd', - }), - 'logAddToCart', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logAddToCart(analytics, { - value: 100, - currency: 'usd', - }), - 'logAddToCart', - ); - }); - - it('analytics.logAddToWishlist()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logAddToWishlist({ - value: 100, - currency: 'usd', - }), - 'logAddToWishlist', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logAddToWishlist(analytics, { - value: 100, - currency: 'usd', - }), - 'logAddToWishlist', - ); - }); - - it('analytics.logAppOpen()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => analytics.logAppOpen(), - 'logAppOpen', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => logAppOpen(analytics), - 'logAppOpen', - ); - }); - - it('analytics.logBeginCheckout()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logBeginCheckout({ - value: 100, - currency: 'usd', - }), - 'logBeginCheckout', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logBeginCheckout(analytics, { - value: 100, - currency: 'usd', - }), - 'logBeginCheckout', - ); - }); - - it('analytics.logCampaignDetails()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logCampaignDetails({ - source: 'email', - medium: 'cta_button', - campaign: 'newsletter', - }), - 'logCampaignDetails', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logCampaignDetails(analytics, { - source: 'email', - medium: 'cta_button', - campaign: 'newsletter', - }), - 'logCampaignDetails', - ); - }); - - it('analytics.logEarnVirtualCurrency()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logEarnVirtualCurrency({ - virtual_currency_name: 'coins', - value: 100, - }), - 'logEarnVirtualCurrency', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logEarnVirtualCurrency(analytics, { - virtual_currency_name: 'coins', - value: 100, - }), - 'logEarnVirtualCurrency', - ); - }); - - it('analytics.logGenerateLead()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logGenerateLead({ - currency: 'USD', - value: 123, - }), - 'logGenerateLead', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logGenerateLead(analytics, { - currency: 'USD', - value: 123, - }), - 'logGenerateLead', - ); - }); - - it('analytics.logJoinGroup()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logJoinGroup({ - group_id: '12345', - }), - 'logJoinGroup', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logJoinGroup(analytics, { - group_id: '12345', - }), - 'logJoinGroup', - ); - }); - - it('analytics.logLevelEnd()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logLevelEnd({ - level: 12, - success: true, - }), - 'logLevelEnd', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logLevelEnd(analytics, { - level: 12, - success: true, - }), - 'logLevelEnd', - ); - }); - - it('analytics.logLevelStart()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logLevelStart({ - level: 12, - }), - 'logLevelStart', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logLevelStart(analytics, { - level: 12, - }), - 'logLevelStart', - ); - }); - - it('analytics.logLevelUp()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logLevelUp({ - level: 12, - }), - 'logLevelUp', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logLevelUp(analytics, { - level: 12, - }), - 'logLevelUp', - ); - }); - - it('analytics.logLogin()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logLogin({ - method: 'facebook.com', - }), - 'logLogin', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logLogin(analytics, { - method: 'facebook.com', - }), - 'logLogin', - ); - }); - - it('analytics.logPostScore()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logPostScore({ - score: 567334, - level: 3, - }), - 'logPostScore', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logPostScore(analytics, { - score: 567334, - level: 3, - }), - 'logPostScore', - ); - }); - - it('analytics.logSelectContent()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logSelectContent({ - content_type: 'clothing', - item_id: 'abcd', - }), - 'logSelectContent', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logSelectContent(analytics, { - content_type: 'clothing', - item_id: 'abcd', - }), - 'logSelectContent', - ); - }); - - it('analytics.logPurchase()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logPurchase({ - value: 100, - currency: 'usd', - }), - 'logPurchase', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logPurchase(analytics, { - value: 100, - currency: 'usd', - }), - 'logPurchase', - ); - }); - - it('analytics.logRefund()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logRefund({ - value: 100, - currency: 'usd', - }), - 'logRefund', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logRefund(analytics, { - value: 100, - currency: 'usd', - }), - 'logRefund', - ); - }); - - it('analytics.logRemoveFromCart()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logRemoveFromCart({ - value: 100, - currency: 'usd', - }), - 'logRemoveFromCart', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logRemoveFromCart(analytics, { - value: 100, - currency: 'usd', - }), - 'logRemoveFromCart', - ); - }); - - it('analytics.logSearch()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logSearch({ - search_term: 't-shirts', - }), - 'logSearch', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logSearch(analytics, { - search_term: 't-shirts', - }), - 'logSearch', - ); - }); - - it('analytics.logSelectItem()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logSelectItem({ - item_list_id: '54690834', - item_list_name: 't-shirts', - content_type: 'clothing', - }), - 'logSelectItem', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logSelectItem(analytics, { - item_list_id: '54690834', - item_list_name: 't-shirts', - content_type: 'clothing', - }), - 'logSelectItem', - ); - }); - - it('analytics.logSetCheckoutOption()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logSetCheckoutOption({ - checkout_step: 2, - checkout_option: 'false', - }), - 'logSetCheckoutOption', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logSetCheckoutOption(analytics, { - checkout_step: 2, - checkout_option: 'false', - }), - 'logSetCheckoutOption', - ); - }); - - it('analytics.logSelectPromotion()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logSelectPromotion({ - creative_name: 'the promotion', - creative_slot: 'evening', - location_id: 'london', - promotion_id: '230593', - promotion_name: 'summer sale', - }), - 'logSelectPromotion', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logSelectPromotion(analytics, { - creative_name: 'the promotion', - creative_slot: 'evening', - location_id: 'london', - promotion_id: '230593', - promotion_name: 'summer sale', - }), - 'logSelectPromotion', - ); - }); - - it('analytics.logShare()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logShare({ - content_type: 't-shirts', - item_id: '12345', - method: 'twitter.com', - }), - 'logShare', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logShare(analytics, { - content_type: 't-shirts', - item_id: '12345', - method: 'twitter.com', - }), - 'logShare', - ); - }); - - it('analytics.logSignUp()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logSignUp({ - method: 'facebook.com', - }), - 'logSignUp', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logSignUp(analytics, { - method: 'facebook.com', - }), - 'logSignUp', - ); - }); - - it('analytics.logSpendVirtualCurrency()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logSpendVirtualCurrency({ - item_name: 'battle_pass', - virtual_currency_name: 'coins', - value: 100, - }), - 'logSpendVirtualCurrency', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logSpendVirtualCurrency(analytics, { - item_name: 'battle_pass', - virtual_currency_name: 'coins', - value: 100, - }), - 'logSpendVirtualCurrency', - ); - }); - - it('analytics.logTutorialBegin()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => analytics.logTutorialBegin(), - 'logTutorialBegin', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => logTutorialBegin(analytics), - 'logTutorialBegin', - ); - }); - - it('analytics.logTutorialComplete()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => analytics.logTutorialComplete(), - 'logTutorialComplete', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => logTutorialComplete(analytics), - 'logTutorialComplete', - ); - }); - - it('analytics.logUnlockAchievement()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => analytics.logUnlockAchievement({ achievement_id: '12345' }), - 'logUnlockAchievement', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => logUnlockAchievement(analytics, { achievement_id: '12345' }), - 'logUnlockAchievement', - ); - }); - - it('analytics.logViewCart()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => analytics.logViewCart({ value: 100, currency: 'usd' }), - 'logViewCart', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => logViewCart(analytics, { value: 100, currency: 'usd' }), - 'logViewCart', - ); - }); - - it('analytics.logViewItem()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => analytics.logViewItem({ value: 100, currency: 'usd' }), - 'logViewItem', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => logViewItem(analytics, { value: 100, currency: 'usd' }), - 'logViewItem', - ); - }); - }); - - it('analytics.logViewPromotion()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logViewPromotion({ - creative_name: 'the promotion', - creative_slot: 'evening', - location_id: 'london', - promotion_id: '230593', - }), - 'logViewPromotion', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logViewPromotion(analytics, { - creative_name: 'the promotion', - creative_slot: 'evening', - location_id: 'london', - promotion_id: '230593', - }), - 'logViewPromotion', - ); - }); - - it('analytics.logViewSearchResults()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - analytics.logViewSearchResults({ - search_term: 'clothing', - }), - 'logViewSearchResults', - ); - - analyticsRefV9Deprecation( - // This is deprecated for both namespaced and modular. - () => {}, - () => - logViewSearchResults(analytics, { - search_term: 'clothing', - }), - 'logViewSearchResults', - ); - }); - - it('analytics.setDefaultEventParameters()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => - setDefaultEventParameters(analytics, { - search_term: 'clothing', - }), - () => - analytics.setDefaultEventParameters({ - search_term: 'clothing', - }), - 'setDefaultEventParameters', - ); - }); - - it('analytics.initiateOnDeviceConversionMeasurementWithEmailAddress()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => initiateOnDeviceConversionMeasurementWithEmailAddress(analytics, 'some@email.com'), - () => analytics.initiateOnDeviceConversionMeasurementWithEmailAddress('some@email.com'), - 'initiateOnDeviceConversionMeasurementWithEmailAddress', - ); - }); - - it('analytics.initiateOnDeviceConversionMeasurementWithHashedEmailAddress()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => - initiateOnDeviceConversionMeasurementWithHashedEmailAddress(analytics, 'some@email.com'), - () => - analytics.initiateOnDeviceConversionMeasurementWithHashedEmailAddress('some@email.com'), - 'initiateOnDeviceConversionMeasurementWithHashedEmailAddress', - ); - }); - - it('analytics.initiateOnDeviceConversionMeasurementWithPhoneNumber()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => initiateOnDeviceConversionMeasurementWithPhoneNumber(analytics, '+1555321'), - () => analytics.initiateOnDeviceConversionMeasurementWithPhoneNumber('+1555321'), - 'initiateOnDeviceConversionMeasurementWithPhoneNumber', - ); - }); - - it('analytics.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber()', function () { - const analytics = getAnalytics(); - analyticsRefV9Deprecation( - () => - initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( - analytics, - 'b1b1b3b0b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1', - ), - () => - analytics.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( - 'b1b1b3b0b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1', - ), - 'initiateOnDeviceConversionMeasurementWithHashedPhoneNumber', - ); - }); - }); }); diff --git a/packages/analytics/e2e/analytics.e2e.js b/packages/analytics/e2e/analytics.e2e.js index 1886aa9ba6..1863678047 100644 --- a/packages/analytics/e2e/analytics.e2e.js +++ b/packages/analytics/e2e/analytics.e2e.js @@ -70,510 +70,6 @@ describe('analytics()', function () { }); }); - describe('firebase v8 compatibility', function () { - beforeEach(async function () { - const { getAnalytics, logEvent } = analyticsModular; - await logEvent(getAnalytics(), 'screen_view'); - }); - - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('logEvent()', function () { - it('log an event without parameters', async function () { - await firebase.analytics().logEvent('invertase_event'); - }); - - it('log an event with parameters', async function () { - await firebase.analytics().logEvent('invertase_event', { - boolean: true, - number: 1, - string: 'string', - }); - }); - }); - - describe('setSessionTimeoutDuration()', function () { - it('default duration', async function () { - await firebase.analytics().setSessionTimeoutDuration(); - }); - - xit('custom duration', async function () { - // TODO: worked on Detox v17, causes crash after transition to v18. Why? - await firebase.analytics().setSessionTimeoutDuration(13371337); - }); - }); - - describe('getAppInstanceId()', function () { - it('calls native fn without error', async function () { - await firebase.analytics().getAppInstanceId(); - }); - }); - - describe('setUserId()', function () { - it('allows a null values to be set', async function () { - await firebase.analytics().setUserId(null); - }); - - it('accepts string values', async function () { - await firebase.analytics().setUserId('rn-firebase'); - }); - }); - - describe('setUserProperty()', function () { - it('allows a null values to be set', async function () { - await firebase.analytics().setUserProperty('invertase', null); - }); - - it('accepts string values', async function () { - await firebase.analytics().setUserProperty('invertase2', 'rn-firebase'); - }); - }); - - describe('setUserProperties()', function () { - it('allows null values to be set', async function () { - await firebase.analytics().setUserProperties({ invertase2: null }); - }); - - it('accepts string values', async function () { - await firebase.analytics().setUserProperties({ invertase3: 'rn-firebase' }); - }); - }); - - describe('logScreenView()', function () { - it('calls logScreenView', async function () { - await firebase - .analytics() - .logScreenView({ screen_name: 'invertase screen', screen_class: 'invertase class' }); - }); - }); - - describe('logAddPaymentInfo()', function () { - it('calls logAddPaymentInfo', async function () { - await firebase.analytics().logAddPaymentInfo({ - value: 123, - currency: 'USD', - items: [], - }); - }); - }); - - describe('logAddToCart()', function () { - it('calls logAddToCart', async function () { - await firebase.analytics().logAddToCart({ - value: 123, - currency: 'GBP', - }); - }); - }); - - describe('logAddShippingInfo()', function () { - it('calls logAddShippingInfo', async function () { - await firebase.analytics().logAddShippingInfo({ - value: 123, - currency: 'GBP', - }); - }); - }); - - describe('logAddToWishlist()', function () { - it('calls logAddToWishlist', async function () { - await firebase.analytics().logAddToWishlist({ - items: [ - { - item_id: 'foo', - item_name: 'foo', - item_category: 'foo', - item_location_id: 'foo', - quantity: 5, - }, - ], - value: 123, - currency: 'GBP', - }); - }); - }); - - describe('logAppOpen()', function () { - it('calls logAppOpen', async function () { - await firebase.analytics().logAppOpen(); - }); - }); - - describe('logBeginCheckout()', function () { - it('calls logBeginCheckout', async function () { - await firebase.analytics().logBeginCheckout(); - }); - }); - - describe('logCampaignDetails()', function () { - it('calls logCampaignDetails', async function () { - await firebase.analytics().logCampaignDetails({ - source: 'foo', - medium: 'bar', - campaign: 'baz', - }); - }); - }); - - describe('logEarnVirtualCurrency()', function () { - it('calls logEarnVirtualCurrency', async function () { - await firebase.analytics().logEarnVirtualCurrency({ - virtual_currency_name: 'foo', - value: 123, - }); - }); - }); - - describe('logPurchase()', function () { - it('calls logPurchase', async function () { - await firebase.analytics().logPurchase({ - currency: 'USD', - value: 123, - affiliation: 'affiliation', - }); - }); - }); - - describe('logViewPromotion()', function () { - it('calls logViewPromotion', async function () { - await firebase.analytics().logViewPromotion({ - creative_name: 'creative_name', - creative_slot: 'creative_slot', - }); - }); - }); - - describe('logGenerateLead()', function () { - it('calls logGenerateLead', async function () { - await firebase.analytics().logGenerateLead({ - currency: 'USD', - value: 123, - }); - }); - }); - - describe('logJoinGroup()', function () { - it('calls logJoinGroup', async function () { - await firebase.analytics().logJoinGroup({ - group_id: '123', - }); - }); - }); - - describe('logLevelEnd()', function () { - it('calls logLevelEnd', async function () { - await firebase.analytics().logLevelEnd({ - level: 123, - success: true, - }); - }); - }); - - describe('logLevelStart()', function () { - it('calls logLevelEnd', async function () { - await firebase.analytics().logLevelStart({ - level: 123, - }); - }); - }); - - describe('logLevelUp()', function () { - it('calls logLevelUp', async function () { - await firebase.analytics().logLevelUp({ - level: 123, - character: 'foo', - }); - }); - }); - - describe('logLogin()', function () { - it('calls logLogin', async function () { - await firebase.analytics().logLogin({ - method: 'facebook.com', - }); - }); - }); - - describe('logPostScore()', function () { - it('calls logPostScore', async function () { - await firebase.analytics().logPostScore({ - score: 123, - }); - }); - }); - - describe('logRemoveFromCart()', function () { - it('calls logRemoveFromCart', async function () { - await firebase.analytics().logRemoveFromCart({ - value: 123, - currency: 'USD', - }); - }); - }); - - describe('logSearch()', function () { - it('calls logSearch', async function () { - await firebase.analytics().logSearch({ - search_term: 'foo', - }); - }); - }); - - describe('logSetCheckoutOption()', function () { - it('calls logSelectContent', async function () { - await firebase.analytics().logSetCheckoutOption({ - checkout_step: 123, - checkout_option: 'foo', - }); - }); - }); - - describe('logSelectItem()', function () { - it('calls logSelectItem', async function () { - await firebase.analytics().logSelectItem({ - item_list_id: 'foo', - item_list_name: 'foo', - content_type: 'foo', - }); - }); - }); - - describe('logShare()', function () { - it('calls logShare', async function () { - await firebase.analytics().logShare({ - content_type: 'foo', - item_id: 'foo', - method: 'foo', - }); - }); - }); - - describe('logSignUp()', function () { - it('calls logSignUp', async function () { - await firebase.analytics().logSignUp({ - method: 'facebook.com', - }); - }); - }); - - describe('logSpendVirtualCurrency()', function () { - it('calls logSpendVirtualCurrency', async function () { - await firebase.analytics().logSpendVirtualCurrency({ - item_name: 'foo', - virtual_currency_name: 'foo', - value: 123, - }); - }); - }); - - describe('logTutorialBegin()', function () { - it('calls logTutorialBegin', async function () { - await firebase.analytics().logTutorialBegin(); - }); - }); - - describe('logTutorialComplete()', function () { - it('calls logTutorialComplete', async function () { - await firebase.analytics().logTutorialComplete(); - }); - }); - - describe('logUnlockAchievement()', function () { - it('calls logUnlockAchievement', async function () { - await firebase.analytics().logUnlockAchievement({ - achievement_id: 'foo', - }); - }); - }); - - describe('logViewCart()', function () { - it('calls logViewCart', async function () { - await firebase.analytics().logViewCart(); - }); - }); - - describe('logViewItem()', function () { - it('calls logViewItem', async function () { - await firebase.analytics().logViewItem({ - items: [ - { - item_id: 'foo', - item_name: 'foo', - item_category: 'foo', - item_location_id: 'foo', - }, - ], - value: 123, - currency: 'GBP', - }); - }); - }); - - describe('logViewItemList()', function () { - it('calls logViewItemList', async function () { - await firebase.analytics().logViewItemList({ - item_list_name: 'foo', - items: [ - { - item_id: 'foo', - item_name: 'foo', - item_category: 'foo', - item_location_id: 'foo', - price: 123, - }, - ], - }); - }); - }); - - describe('logRefund()', function () { - it('calls logRefund', async function () { - await firebase.analytics().logRefund({ - affiliation: 'affiliation', - coupon: 'coupon', - }); - }); - }); - - describe('logSelectContent()', function () { - it('calls logSelectContent', async function () { - await firebase.analytics().logSelectContent({ - content_type: 'clothing', - item_id: 'abcd', - }); - }); - }); - - describe('logSelectPromotion()', function () { - it('calls logSelectPromotion', async function () { - await firebase.analytics().logSelectPromotion({ - creative_name: 'string', - creative_slot: 'string', - location_id: 'string', - promotion_id: 'string', - promotion_name: 'string', - }); - }); - }); - - describe('logViewSearchResults()', function () { - it('calls logViewSearchResults', async function () { - await firebase.analytics().logViewSearchResults({ - search_term: 'promotion', - }); - }); - }); - - describe('setDefaultEventParameters()', function () { - it('set null default parameter', async function () { - await firebase.analytics().setDefaultEventParameters(null); - }); - - it('set undefined default parameter', async function () { - await firebase.analytics().setDefaultEventParameters(undefined); - }); - - it('set default parameters', async function () { - await firebase.analytics().setDefaultEventParameters({ number: 1, stringn: '123' }); - }); - }); - - describe('setConsent()', function () { - it('set ad_storage=true on consentSettings', async function () { - const consentSettings = { - ad_storage: true, - }; - await firebase.analytics().setConsent(consentSettings); - }); - - it('set ad_storage=false and analytics_storage=true on consentSettings', async function () { - const consentSettings = { - ad_storage: false, - analytics_storage: true, - }; - await firebase.analytics().setConsent(consentSettings); - }); - }); - - // Test this one near end so all the previous hits are visible in DebugView is that is enabled - describe('resetAnalyticsData()', function () { - it('calls native fn without error', async function () { - await firebase.analytics().resetAnalyticsData(); - }); - }); - - // Test this last so it does not stop delivery to DebugView - describe('initiateOnDeviceConversionMeasurementWithEmailAddress()', function () { - it('calls native API successfully', async function () { - await firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithEmailAddress('conversionTest@example.com'); - }); - - it('calls native API successfully with hashed email', async function () { - // Normalized email address: 'conversiontest@example.com' - // echo -n 'conversiontest@example.com' | shasum -a 256 - - await firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithHashedEmailAddress( - '73914334417d04bc2922331e5fb3b3572ab88debfa0c63beb0c56f7b31d4aaed', - ); - }); - }); - - // Test this last so it does not stop delivery to DebugView - describe('on-device conversion measurement with phone', function () { - it('calls native API successfully', async function () { - await firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithPhoneNumber('+14155551212'); - }); - - it('calls native API successfully with hashed phone', async function () { - await firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( - '5dce05f429bc23dbd9e2caa03f336b56d4ee2aa374d8708f4f12eb4e10204c2b', - ); - }); - - it('handles mal-formatted phone number', async function () { - try { - await firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithPhoneNumber('+notaphonenumber'); - fail('Should have returned an error for malformatted phone number'); - } catch (e) { - // coerce the error message to a string and verify - if (!(e + '').includes('expected a string value in E.164 format')) { - fail('Should have returned an error for malformatted phone number'); - } - } - }); - }); - - // Test this last so it does not stop delivery to DebugView - describe('setAnalyticsCollectionEnabled()', function () { - it('false', async function () { - await firebase.analytics().setAnalyticsCollectionEnabled(false); - }); - - // Enable as the last action, so the rest of the hits are visible in DebugView if enabled - it('true', async function () { - await firebase.analytics().setAnalyticsCollectionEnabled(true); - }); - }); - }); - describe('modular', function () { beforeEach(async function () { const { getAnalytics, logEvent } = analyticsModular; @@ -615,17 +111,7 @@ describe('analytics()', function () { }); }); - describe('tests that are for deprecated API for modular and namespace', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - + describe('deprecated helper events', function () { describe('logScreenView()', function () { it('calls logScreenView', async function () { const { getAnalytics, logScreenView } = analyticsModular; diff --git a/packages/analytics/lib/index.ts b/packages/analytics/lib/index.ts index 7529efe959..dbb01d7554 100644 --- a/packages/analytics/lib/index.ts +++ b/packages/analytics/lib/index.ts @@ -15,7 +15,78 @@ * */ -// Export types from types/analytics +import type { FirebaseApp } from '@react-native-firebase/app'; +import { Platform } from 'react-native'; +import { + isAlphaNumericUnderscore, + isE164PhoneNumber, + isIOS, + isBoolean, + isNull, + isNumber, + isObject, + isOneOf, + isString, + isUndefined, +} from '@react-native-firebase/app/dist/module/common'; + +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; + +import './types/internal'; + +// Internal types are now available through module declarations in app package +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; + +import { validateStruct, validateCompound } from './struct'; +import { RNFBAnalyticsModule } from './web/RNFBAnalyticsModule'; +import { version } from './version'; +import * as structs from './structs'; +import type { + Analytics, + AnalyticsCallOptions, + ConsentSettings, + AnalyticsSettings, + SettingsOptions, + AddPaymentInfoEventParameters, + ScreenViewParameters, + AddShippingInfoParameters, + AddToCartEventParameters, + AddToWishlistEventParameters, + BeginCheckoutEventParameters, + CampaignDetailsEventParameters, + EarnVirtualCurrencyEventParameters, + GenerateLeadEventParameters, + JoinGroupEventParameters, + LevelEndEventParameters, + LevelStartEventParameters, + LevelUpEventParameters, + LoginEventParameters, + PostScoreEventParameters, + SelectContentEventParameters, + PurchaseEventParameters, + RefundEventParameters, + RemoveFromCartEventParameters, + SearchEventParameters, + SelectItemEventParameters, + SetCheckoutOptionEventParameters, + SelectPromotionEventParameters, + ShareEventParameters, + SignUpEventParameters, + SpendVirtualCurrencyEventParameters, + UnlockAchievementEventParameters, + ViewCartEventParameters, + ViewItemEventParameters, + ViewItemListEventParameters, + ViewPromotionEventParameters, + ViewSearchResultsParameters, + EventParams, + CustomEventName, +} from './types/analytics'; + export type { Analytics, AnalyticsCallOptions, @@ -62,12 +133,1761 @@ export type { GtagConfigParams, EventNameString, CustomEventName, - FirebaseAnalyticsTypes, } from './types/analytics'; -// Export modular API functions -export * from './modular'; +const ReservedEventNames: readonly string[] = [ + 'ad_activeview', + 'ad_click', + 'ad_exposure', + // 'ad_impression', // manual ad_impression logging is allowed, See #6307 + 'ad_query', + 'ad_reward', + 'adunit_exposure', + 'app_background', + 'app_clear_data', + // 'app_exception', + 'app_remove', + 'app_store_refund', + 'app_store_subscription_cancel', + 'app_store_subscription_convert', + 'app_store_subscription_renew', + 'app_update', + 'app_upgrade', + 'dynamic_link_app_open', + 'dynamic_link_app_update', + 'dynamic_link_first_open', + 'error', + 'first_open', + 'first_visit', + 'in_app_purchase', + 'notification_dismiss', + 'notification_foreground', + 'notification_open', + 'notification_receive', + 'os_update', + 'session_start', + 'session_start_with_rollout', + 'user_engagement', +] as const; + +const namespace = 'analytics'; + +const nativeModuleName = 'RNFBAnalyticsModule'; + +class FirebaseAnalyticsModule extends FirebaseModule { + logEvent( + name: string, + params: { [key: string]: any } = {}, + options: AnalyticsCallOptions = { global: false }, + ): Promise { + if (!isString(name)) { + throw new Error("firebase.analytics().logEvent(*) 'name' expected a string value."); + } + + if (!isUndefined(params) && !isObject(params)) { + throw new Error("firebase.analytics().logEvent(_, *) 'params' expected an object value."); + } + + // check name is not a reserved event name + if (isOneOf(name, ReservedEventNames as any[])) { + throw new Error( + `firebase.analytics().logEvent(*) 'name' the event name '${name}' is reserved and can not be used.`, + ); + } + + // name format validation + if (!isAlphaNumericUnderscore(name) || name.length > 40) { + throw new Error( + `firebase.analytics().logEvent(*) 'name' invalid event name '${name}'. Names should contain 1 to 40 alphanumeric characters or underscores.`, + ); + } + + if (!isUndefined(options)) { + if (!isObject(options)) { + throw new Error( + "firebase.analytics().logEvent(_, _, *) 'options' expected an object value.", + ); + } + + if (!isUndefined(options.global) && !isBoolean(options.global)) { + throw new Error("'options.global' property expected a boolean."); + } + } + + return this.native.logEvent(name, params); + } + + setAnalyticsCollectionEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "firebase.analytics().setAnalyticsCollectionEnabled(*) 'enabled' expected a boolean value.", + ); + } + + return this.native.setAnalyticsCollectionEnabled(enabled); + } + + setSessionTimeoutDuration(milliseconds: number = 1800000): Promise { + if (!isNumber(milliseconds)) { + throw new Error( + "firebase.analytics().setSessionTimeoutDuration(*) 'milliseconds' expected a number value.", + ); + } + + if (milliseconds < 0) { + throw new Error( + "firebase.analytics().setSessionTimeoutDuration(*) 'milliseconds' expected a positive number value.", + ); + } + + return this.native.setSessionTimeoutDuration(milliseconds); + } + + getAppInstanceId(): Promise { + return this.native.getAppInstanceId(); + } + + getSessionId(): Promise { + return this.native.getSessionId(); + } + + setUserId(id: string | null): Promise { + if (!isNull(id) && !isString(id)) { + throw new Error("firebase.analytics().setUserId(*) 'id' expected a string value."); + } + + return this.native.setUserId(id); + } + + setUserProperty(name: string, value: string | null): Promise { + if (!isString(name)) { + throw new Error("firebase.analytics().setUserProperty(*) 'name' expected a string value."); + } + + if (value !== null && !isString(value)) { + throw new Error( + "firebase.analytics().setUserProperty(_, *) 'value' expected a string value.", + ); + } + + return this.native.setUserProperty(name, value); + } + + setUserProperties( + properties: { [key: string]: string | null }, + options: AnalyticsCallOptions = { global: false }, + ): Promise { + if (!isObject(properties)) { + throw new Error( + "firebase.analytics().setUserProperties(*) 'properties' expected an object of key/value pairs.", + ); + } + + if (!isUndefined(options)) { + if (!isObject(options)) { + throw new Error( + "firebase.analytics().logEvent(_, _, *) 'options' expected an object value.", + ); + } + + if (!isUndefined(options.global) && !isBoolean(options.global)) { + throw new Error("'options.global' property expected a boolean."); + } + } + + const entries = Object.entries(properties); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (!entry) continue; + const [key, value] = entry; + if (!isNull(value) && !isString(value)) { + throw new Error( + `firebase.analytics().setUserProperties(*) 'properties' value for parameter '${key}' is invalid, expected a string.`, + ); + } + } + + return this.native.setUserProperties(properties); + } + + resetAnalyticsData(): Promise { + return this.native.resetAnalyticsData(); + } + + setConsent(consentSettings: ConsentSettings): Promise { + if (!isObject(consentSettings)) { + throw new Error( + 'firebase.analytics().setConsent(*): The supplied arg must be an object of key/values.', + ); + } + + const entries = Object.entries(consentSettings); + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (!entry) continue; + const [key, value] = entry; + if (key === 'security_storage') { + if (!isOneOf(value, ['granted', 'denied'])) { + throw new Error( + `firebase.analytics().setConsent(*) 'consentSettings' value for parameter '${key}' is invalid, expected one of 'granted' or 'denied'.`, + ); + } + continue; + } + if (!isBoolean(value)) { + throw new Error( + `firebase.analytics().setConsent(*) 'consentSettings' value for parameter '${key}' is invalid, expected a boolean.`, + ); + } + } + + return this.native.setConsent(consentSettings); + } + + /** ------------------- + * EVENTS + * -------------------- */ + logAddPaymentInfo(object: AddPaymentInfoEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logAddPaymentInfo(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddPaymentInfo(*):'); + + return this.logEvent( + 'add_payment_info', + validateStruct(object, structs.AddPaymentInfo, 'firebase.analytics().logAddPaymentInfo(*):'), + ); + } + + logScreenView(object: ScreenViewParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logScreenView(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'screen_view', + validateStruct(object, structs.ScreenView, 'firebase.analytics().logScreenView(*):'), + ); + } + + logAddShippingInfo(object: AddShippingInfoParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logAddShippingInfo(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddShippingInfo(*):'); + + return this.logEvent( + 'add_shipping_info', + validateStruct( + object, + structs.AddShippingInfo, + 'firebase.analytics().logAddShippingInfo(*):', + ), + ); + } + + logAddToCart(object: AddToCartEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logAddToCart(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddToCart(*):'); + + return this.logEvent( + 'add_to_cart', + validateStruct(object, structs.AddToCart, 'firebase.analytics().logAddToCart(*):'), + ); + } + + logAddToWishlist(object: AddToWishlistEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logAddToWishlist(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddToWishlist(*):'); + + return this.logEvent( + 'add_to_wishlist', + validateStruct(object, structs.AddToWishlist, 'firebase.analytics().logAddToWishlist(*):'), + ); + } + + logAppOpen(): Promise { + return this.logEvent('app_open'); + } + + logBeginCheckout(object: BeginCheckoutEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logBeginCheckout(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logBeginCheckout(*):'); + + return this.logEvent( + 'begin_checkout', + validateStruct(object, structs.BeginCheckout, 'firebase.analytics().logBeginCheckout(*):'), + ); + } + + logCampaignDetails(object: CampaignDetailsEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logCampaignDetails(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'campaign_details', + validateStruct( + object, + structs.CampaignDetails, + 'firebase.analytics().logCampaignDetails(*):', + ), + ); + } + + logEarnVirtualCurrency(object: EarnVirtualCurrencyEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logEarnVirtualCurrency(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'earn_virtual_currency', + validateStruct( + object, + structs.EarnVirtualCurrency, + 'firebase.analytics().logEarnVirtualCurrency(*):', + ), + ); + } + + logGenerateLead(object: GenerateLeadEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logGenerateLead(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logGenerateLead(*):'); + + return this.logEvent( + 'generate_lead', + validateStruct(object, structs.GenerateLead, 'firebase.analytics().logGenerateLead(*):'), + ); + } + + logJoinGroup(object: JoinGroupEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logJoinGroup(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'join_group', + validateStruct(object, structs.JoinGroup, 'firebase.analytics().logJoinGroup(*):'), + ); + } + + logLevelEnd(object: LevelEndEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logLevelEnd(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'level_end', + validateStruct(object, structs.LevelEnd, 'firebase.analytics().logLevelEnd(*):'), + ); + } + + logLevelStart(object: LevelStartEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logLevelStart(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'level_start', + validateStruct(object, structs.LevelStart, 'firebase.analytics().logLevelStart(*):'), + ); + } + + logLevelUp(object: LevelUpEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logLevelUp(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'level_up', + validateStruct(object, structs.LevelUp, 'firebase.analytics().logLevelUp(*):'), + ); + } + + logLogin(object: LoginEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logLogin(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'login', + validateStruct(object, structs.Login, 'firebase.analytics().logLogin(*):'), + ); + } + + logPostScore(object: PostScoreEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logPostScore(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'post_score', + validateStruct(object, structs.PostScore, 'firebase.analytics().logPostScore(*):'), + ); + } + + logSelectContent(object: SelectContentEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logSelectContent(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'select_content', + validateStruct(object, structs.SelectContent, 'firebase.analytics().logSelectContent(*):'), + ); + } + + logPurchase(object: PurchaseEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logPurchase(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logPurchase(*):'); + + return this.logEvent( + 'purchase', + validateStruct(object, structs.Purchase, 'firebase.analytics().logPurchaseEvent(*):'), + ); + } + + logRefund(object: RefundEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logRefund(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logRefund(*):'); + + return this.logEvent( + 'refund', + validateStruct(object, structs.Refund, 'firebase.analytics().logRefund(*):'), + ); + } + + logRemoveFromCart(object: RemoveFromCartEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logRemoveFromCart(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logRemoveFromCart(*):'); + + return this.logEvent( + 'remove_from_cart', + validateStruct(object, structs.RemoveFromCart, 'firebase.analytics().logRemoveFromCart(*):'), + ); + } + + logSearch(object: SearchEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logSearch(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'search', + validateStruct(object, structs.Search, 'firebase.analytics().logSearch(*):'), + ); + } + + logSelectItem(object: SelectItemEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logSelectItem(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'select_item', + validateStruct(object, structs.SelectItem, 'firebase.analytics().logSelectItem(*):'), + ); + } + + logSetCheckoutOption(object: SetCheckoutOptionEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logSetCheckoutOption(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'set_checkout_option', + validateStruct( + object, + structs.SetCheckoutOption, + 'firebase.analytics().logSetCheckoutOption(*):', + ), + ); + } + + logSelectPromotion(object: SelectPromotionEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logSelectPromotion(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'select_promotion', + validateStruct( + object, + structs.SelectPromotion, + 'firebase.analytics().logSelectPromotion(*):', + ), + ); + } + + logShare(object: ShareEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logShare(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'share', + validateStruct(object, structs.Share, 'firebase.analytics().logShare(*):'), + ); + } + + logSignUp(object: SignUpEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logSignUp(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'sign_up', + validateStruct(object, structs.SignUp, 'firebase.analytics().logSignUp(*):'), + ); + } + + logSpendVirtualCurrency(object: SpendVirtualCurrencyEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logSpendVirtualCurrency(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'spend_virtual_currency', + validateStruct( + object, + structs.SpendVirtualCurrency, + 'firebase.analytics().logSpendVirtualCurrency(*):', + ), + ); + } + + logTutorialBegin(): Promise { + return this.logEvent('tutorial_begin'); + } + + logTutorialComplete(): Promise { + return this.logEvent('tutorial_complete'); + } + + logUnlockAchievement(object: UnlockAchievementEventParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logUnlockAchievement(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'unlock_achievement', + validateStruct( + object, + structs.UnlockAchievement, + 'firebase.analytics().logUnlockAchievement(*):', + ), + ); + } + + logViewCart(object: ViewCartEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logViewCart(*): The supplied arg must be an object of key/values.', + ); + } + + validateCompound(object, 'value', 'currency', 'firebase.analytics().logViewCart(*):'); + + return this.logEvent( + 'view_cart', + validateStruct(object, structs.ViewCart, 'firebase.analytics().logViewCart(*):'), + ); + } + + logViewItem(object: ViewItemEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logViewItem(*): The supplied arg must be an object of key/values.', + ); + } + validateCompound(object, 'value', 'currency', 'firebase.analytics().logViewItem(*):'); + + return this.logEvent( + 'view_item', + validateStruct(object, structs.ViewItem, 'firebase.analytics().logViewItem(*):'), + ); + } + + logViewItemList(object: ViewItemListEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logViewItemList(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'view_item_list', + validateStruct(object, structs.ViewItemList, 'firebase.analytics().logViewItemList(*):'), + ); + } + + logViewPromotion(object: ViewPromotionEventParameters = {}): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logViewPromotion(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'view_promotion', + validateStruct(object, structs.ViewPromotion, 'firebase.analytics().logViewPromotion(*):'), + ); + } + /** + * Unsupported in "Enhanced Ecommerce reports": + * https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Event#public-static-final-string-view_search_results + */ + logViewSearchResults(object: ViewSearchResultsParameters): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.analytics().logViewSearchResults(*): The supplied arg must be an object of key/values.', + ); + } + + return this.logEvent( + 'view_search_results', + validateStruct( + object, + structs.ViewSearchResults, + 'firebase.analytics().logViewSearchResults(*):', + ), + ); + } + + setDefaultEventParameters(params?: { [key: string]: any }): Promise { + if (!isObject(params) && !isNull(params) && !isUndefined(params)) { + throw new Error( + "firebase.analytics().setDefaultEventParameters(*) 'params' expected an object value when it is defined.", + ); + } + + return this.native.setDefaultEventParameters(params); + } + + initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress: string): Promise { + if (!isString(emailAddress)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithEmailAddress(*) 'emailAddress' expected a string value.", + ); + } + + if (!isIOS) { + return Promise.resolve(); + } + + return ( + this.native.initiateOnDeviceConversionMeasurementWithEmailAddress?.(emailAddress) ?? + Promise.resolve() + ); + } + + initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + hashedEmailAddress: string, + ): Promise { + if (!isString(hashedEmailAddress)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.", + ); + } + + if (!isIOS) { + return Promise.resolve(); + } + + return ( + this.native.initiateOnDeviceConversionMeasurementWithHashedEmailAddress?.( + hashedEmailAddress, + ) ?? Promise.resolve() + ); + } + + initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber: string): Promise { + if (!isE164PhoneNumber(phoneNumber)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithPhoneNumber(*) 'phoneNumber' expected a string value in E.164 format.", + ); + } + + if (!isIOS) { + return Promise.resolve(); + } + + return ( + this.native.initiateOnDeviceConversionMeasurementWithPhoneNumber?.(phoneNumber) ?? + Promise.resolve() + ); + } + + initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + hashedPhoneNumber: string, + ): Promise { + if (isE164PhoneNumber(hashedPhoneNumber)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.", + ); + } + + if (!isString(hashedPhoneNumber)) { + throw new Error( + "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.", + ); + } + + if (!isIOS) { + return Promise.resolve(); + } + + return ( + this.native.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber?.(hashedPhoneNumber) ?? + Promise.resolve() + ); + } +} + +const config: ModuleConfig = { + namespace, + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, +}; + +export const SDK_VERSION: string = version; + +/** + * Returns an Analytics instance for the given app. + */ +export function getAnalytics(app?: FirebaseApp): Analytics { + return getOrCreateModularInstance(FirebaseAnalyticsModule, config, app) as unknown as Analytics; +} + +/** + * Returns an Analytics instance for the given app. + */ +export function initializeAnalytics(app: FirebaseApp, _options?: AnalyticsSettings): Analytics { + return getAnalytics(app); +} + +/** + * Retrieves a unique Google Analytics identifier for the web client. + */ +export async function getGoogleAnalyticsClientId(analytics: Analytics): Promise { + if (Platform.OS === 'android' || Platform.OS === 'ios') { + throw new Error('getGoogleAnalyticsClientId is web-only.'); + } else { + const instanceId = await getAppInstanceId(analytics); + return instanceId || ''; + } +} + +import type { AnalyticsInternal } from './types/internal'; + +/** + * Log a custom event with optional params. + */ +// Overloads for standard event names +export function logEvent( + analytics: Analytics, + name: 'add_payment_info', + params?: { + items?: EventParams['items']; + currency?: EventParams['currency']; + value?: EventParams['value']; + coupon?: EventParams['coupon']; + payment_type?: EventParams['payment_type']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'add_shipping_info', + params?: { + items?: EventParams['items']; + currency?: EventParams['currency']; + value?: EventParams['value']; + coupon?: EventParams['coupon']; + shipping_tier?: EventParams['shipping_tier']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'add_to_cart', + params?: { + items?: EventParams['items']; + currency?: EventParams['currency']; + value?: EventParams['value']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'add_to_wishlist', + params?: { + items?: EventParams['items']; + currency?: EventParams['currency']; + value?: EventParams['value']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'begin_checkout', + params?: { + currency?: EventParams['currency']; + value?: EventParams['value']; + coupon?: EventParams['coupon']; + items?: EventParams['items']; + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'checkout_progress', + params?: { + checkout_step?: EventParams['checkout_step']; + checkout_option?: EventParams['checkout_option']; + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'exception', + params?: { + description?: EventParams['description']; + fatal?: EventParams['fatal']; + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'generate_lead', + params?: { + currency?: EventParams['currency']; + value?: EventParams['value']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'login', + params: { + method: EventParams['method']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'page_view', + params?: { + page_title?: EventParams['page_title']; + page_location?: EventParams['page_location']; + page_path?: EventParams['page_path']; + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'purchase', + params?: { + affiliation?: EventParams['affiliation']; + coupon?: EventParams['coupon']; + currency?: EventParams['currency']; + items?: EventParams['items']; + shipping?: EventParams['shipping']; + tax?: EventParams['tax']; + value?: EventParams['value']; + transaction_id?: EventParams['transaction_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'refund', + params?: { + affiliation?: EventParams['affiliation']; + coupon?: EventParams['coupon']; + currency?: EventParams['currency']; + items?: EventParams['items']; + shipping?: EventParams['shipping']; + tax?: EventParams['tax']; + value?: EventParams['value']; + transaction_id?: EventParams['transaction_id']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'remove_from_cart', + params?: { + items?: EventParams['items']; + value?: EventParams['value']; + currency?: EventParams['currency']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'screen_view', + params?: { + screen_name?: EventParams['screen_name']; + screen_class?: EventParams['screen_class']; + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'search', + params: { + search_term: EventParams['search_term']; + number_of_nights?: number; + number_of_rooms?: number; + number_of_passengers?: number; + origin?: string; + destination?: string; + start_date?: string; + end_date?: string; + travel_class?: string; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'select_content', + params: { + content_type: EventParams['content_type']; + item_id: EventParams['item_id']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'select_item', + params: { + items?: EventParams['items']; + content_type: EventParams['content_type']; + item_list_id: EventParams['item_list_id']; + item_list_name: EventParams['item_list_name']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'select_promotion', + params: { + creative_name: string; + creative_slot: string; + items?: EventParams['items']; + location_id: string; + promotion_id: EventParams['promotion_id']; + promotion_name: EventParams['promotion_name']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'set_checkout_option', + params?: { + checkout_step?: EventParams['checkout_step']; + checkout_option?: EventParams['checkout_option']; + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'share', + params: { + content_type: EventParams['content_type']; + item_id: EventParams['item_id']; + method: EventParams['method']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'sign_up', + params: { + method: EventParams['method']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'timing_complete', + params?: { + [key: string]: any; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'view_cart', + params?: { + items?: EventParams['items']; + currency?: EventParams['currency']; + value?: EventParams['value']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'view_item', + params?: { + items?: EventParams['items']; + currency?: EventParams['currency']; + value?: EventParams['value']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'view_item_list', + params?: { + items?: EventParams['items']; + item_list_id?: EventParams['item_list_id']; + item_list_name?: EventParams['item_list_name']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'view_promotion', + params?: { + items?: EventParams['items']; + location_id?: string; + creative_name?: string; + creative_slot?: string; + promotion_id?: EventParams['promotion_id']; + promotion_name?: EventParams['promotion_name']; + }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: 'view_search_results', + params: { + search_term: EventParams['search_term']; + }, + options?: AnalyticsCallOptions, +): Promise; + +// Generic fallback for custom event names +export function logEvent( + analytics: Analytics, + name: CustomEventName, + params?: { [key: string]: any }, + options?: AnalyticsCallOptions, +): Promise; + +export function logEvent( + analytics: Analytics, + name: string, + params: { [key: string]: any } = {}, + options: AnalyticsCallOptions = { global: false }, +): Promise { + return analytics.logEvent(name, params, options); +} + +/** Logs verified in-app purchase events in Google Analytics for Firebase + * after a purchase is successful. + * Modular API only; iOS only (StoreKit 2). Throws on Android and web before reaching native. + */ +export function logTransaction(analytics: Analytics, transaction_id: string): Promise { + if (Platform.OS !== 'ios') { + return Promise.reject(new Error('logTransaction is only available on iOS')); + } + + return (analytics as AnalyticsInternal).native.logTransaction(transaction_id); +} + +/** + * If true, allows the device to collect analytical data and send it to Firebase. Useful for GDPR. + */ +export function setAnalyticsCollectionEnabled( + analytics: Analytics, + enabled: boolean, +): Promise { + return analytics.setAnalyticsCollectionEnabled(enabled); +} + +/** + * Sets the duration of inactivity that terminates the current session. + */ +export function setSessionTimeoutDuration( + analytics: Analytics, + milliseconds: number = 1800000, +): Promise { + // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS + return analytics.setSessionTimeoutDuration(milliseconds); +} + +/** + * Retrieve the app instance id of the application. + */ +export function getAppInstanceId(analytics: Analytics): Promise { + // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS + return analytics.getAppInstanceId(); +} + +/** + * Retrieves the session id from the client. + * On iOS, Firebase SDK may return an error that is handled internally and may take many minutes to return a valid value. Check native debug logs for more details. + */ +export function getSessionId(analytics: Analytics): Promise { + // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS + return analytics.getSessionId(); +} + +/** + * Gives a user a unique identification. + */ +export function setUserId(analytics: Analytics, id: string | null): Promise { + return analytics.setUserId(id); +} + +/** + * Sets a key/value pair of data on the current user. Each Firebase project can have up to 25 uniquely named (case-sensitive) user properties. + */ +export function setUserProperty( + analytics: Analytics, + name: string, + value: string | null, +): Promise { + // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS + return analytics.setUserProperty(name, value); +} + +/** + * Sets multiple key/value pairs of data on the current user. Each Firebase project can have up to 25 uniquely named (case-sensitive) user properties. + */ +export function setUserProperties( + analytics: Analytics, + properties: { [key: string]: string | null }, + options: AnalyticsCallOptions = { global: false }, +): Promise { + return analytics.setUserProperties(properties, options); +} + +/** + * Clears all analytics data for this instance from the device and resets the app instance ID. + */ +export function resetAnalyticsData(analytics: Analytics): Promise { + // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS + return analytics.resetAnalyticsData(); +} + +/** + * E-Commerce Purchase event. This event signifies that an item(s) was purchased by a user. Note: This is different from the in-app purchase event, which is reported automatically for Google Play-based apps. + */ +export function logAddPaymentInfo( + analytics: Analytics, + params: AddPaymentInfoEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logAddPaymentInfo(params); +} + +/** + * Sets or clears the screen name and class the user is currently viewing. + */ +export function logScreenView(analytics: Analytics, params: ScreenViewParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logScreenView(params); +} + +/** + * Add Payment Info event. This event signifies that a user has submitted their payment information to your app. + */ +export function logAddShippingInfo( + analytics: Analytics, + params: AddShippingInfoParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logAddShippingInfo(params); +} + +/** + * E-Commerce Add To Cart event. + */ +export function logAddToCart( + analytics: Analytics, + params: AddToCartEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logAddToCart(params); +} + +/** + * E-Commerce Add To Wishlist event. This event signifies that an item was added to a wishlist. + */ +export function logAddToWishlist( + analytics: Analytics, + params: AddToWishlistEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logAddToWishlist(params); +} + +/** + * App Open event. By logging this event when an App is moved to the foreground, developers can understand how often users leave and return during the course of a Session. + */ +export function logAppOpen(analytics: Analytics): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logAppOpen(); +} + +/** + * E-Commerce Begin Checkout event. This event signifies that a user has begun the process of checking out. + */ +export function logBeginCheckout( + analytics: Analytics, + params: BeginCheckoutEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logBeginCheckout(params); +} + +/** + * Log this event to supply the referral details of a re-engagement campaign. + */ +export function logCampaignDetails( + analytics: Analytics, + params: CampaignDetailsEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logCampaignDetails(params); +} + +/** + * Earn Virtual Currency event. This event tracks the awarding of virtual currency in your app. + */ +export function logEarnVirtualCurrency( + analytics: Analytics, + params: EarnVirtualCurrencyEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logEarnVirtualCurrency(params); +} + +/** + * Generate Lead event. Log this event when a lead has been generated in the app. + */ +export function logGenerateLead( + analytics: Analytics, + params: GenerateLeadEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logGenerateLead(params); +} + +/** + * Join Group event. Log this event when a user joins a group such as a guild, team or family. + */ +export function logJoinGroup( + analytics: Analytics, + params: JoinGroupEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logJoinGroup(params); +} + +/** + * Level End event. + */ +export function logLevelEnd(analytics: Analytics, params: LevelEndEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logLevelEnd(params); +} + +/** + * Level Start event. + */ +export function logLevelStart( + analytics: Analytics, + params: LevelStartEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logLevelStart(params); +} + +/** + * Level Up event. This event signifies that a player has leveled up in your gaming app. + */ +export function logLevelUp(analytics: Analytics, params: LevelUpEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logLevelUp(params); +} + +/** + * Login event. Apps with a login feature can report this event to signify that a user has logged in. + */ +export function logLogin(analytics: Analytics, params: LoginEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logLogin(params); +} + +/** + * Post Score event. Log this event when the user posts a score in your gaming app. + */ +export function logPostScore( + analytics: Analytics, + params: PostScoreEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logPostScore(params); +} + +/** + * Select Content event. This general purpose event signifies that a user has selected some content of a certain type in an app. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {SelectContentEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logSelectContent( + analytics: Analytics, + params: SelectContentEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logSelectContent(params); +} + +/** + * E-Commerce Purchase event. This event signifies that an item(s) was purchased by a user. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {PurchaseEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logPurchase(analytics: Analytics, params: PurchaseEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logPurchase(params); +} + +/** + * E-Commerce Refund event. This event signifies that a refund was issued. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {RefundEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logRefund(analytics: Analytics, params: RefundEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logRefund(params); +} + +/** + * Remove from cart event. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {RemoveFromCartEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logRemoveFromCart( + analytics: Analytics, + params: RemoveFromCartEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logRemoveFromCart(params); +} + +/** + * Search event. Apps that support search features can use this event to contextualize search operations by supplying the appropriate, corresponding parameters. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {SearchEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logSearch(analytics: Analytics, params: SearchEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logSearch(params); +} + +/** + * Select Item event. This event signifies that an item was selected by a user from a list. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {SelectItemEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logSelectItem( + analytics: Analytics, + params: SelectItemEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logSelectItem(params); +} + +/** + * Set checkout option event. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {SetCheckoutOptionEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logSetCheckoutOption( + analytics: Analytics, + params: SetCheckoutOptionEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logSetCheckoutOption(params); +} + +/** + * Select promotion event. This event signifies that a user has selected a promotion offer. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {SelectPromotionEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logSelectPromotion( + analytics: Analytics, + params: SelectPromotionEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logSelectPromotion(params); +} + +/** + * Share event. Apps with social features can log the Share event to identify the most viral content. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {ShareEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logShare(analytics: Analytics, params: ShareEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logShare(params); +} + +/** + * Sign Up event. This event indicates that a user has signed up for an account in your app. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {SignUpEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logSignUp(analytics: Analytics, params: SignUpEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logSignUp(params); +} + +/** + * Spend Virtual Currency event. This event tracks the sale of virtual goods in your app. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {SpendVirtualCurrencyEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logSpendVirtualCurrency( + analytics: Analytics, + params: SpendVirtualCurrencyEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logSpendVirtualCurrency(params); +} + +/** + * Tutorial Begin event. This event signifies the start of the on-boarding process in your app. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @returns {Promise} + */ +export function logTutorialBegin(analytics: Analytics): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logTutorialBegin(); +} + +/** + * Tutorial End event. Use this event to signify the user's completion of your app's on-boarding process. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @returns {Promise} + */ +export function logTutorialComplete(analytics: Analytics): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logTutorialComplete(); +} + +/** + * Unlock Achievement event. Log this event when the user has unlocked an achievement in your game. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {UnlockAchievementEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logUnlockAchievement( + analytics: Analytics, + params: UnlockAchievementEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logUnlockAchievement(params); +} + +/** + * E-commerce View Cart event. This event signifies that a user has viewed their cart. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {ViewCartEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logViewCart(analytics: Analytics, params: ViewCartEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logViewCart(params); +} + +/** + * View Item event. This event signifies that some content was shown to the user. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {ViewItemEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logViewItem(analytics: Analytics, params: ViewItemEventParameters): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logViewItem(params); +} + +/** + * View Item List event. Log this event when the user has been presented with a list of items of a certain category. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {ViewItemListEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logViewItemList( + analytics: Analytics, + params: ViewItemListEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logViewItemList(params); +} + +/** + * View Promotion event. This event signifies that a promotion was shown to a user. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {ViewPromotionEventParameters} params - Event parameters. + * @returns {Promise} + */ +export function logViewPromotion( + analytics: Analytics, + params: ViewPromotionEventParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logViewPromotion(params); +} + +/** + * View Search Results event. Log this event when the user has been presented with the results of a search. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {ViewSearchResultsParameters} params - Event parameters. + * @returns {Promise} + */ +export function logViewSearchResults( + analytics: Analytics, + params: ViewSearchResultsParameters, +): Promise { + // This is deprecated for both namespaced and modular. + return analytics.logViewSearchResults(params); +} + +/** + * Adds parameters that will be set on every event logged from the SDK, including automatic ones. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {object} [params={}] - Parameters to be added to the map of parameters added to every event. + * @returns {Promise} + */ +export function setDefaultEventParameters( + analytics: Analytics, + params: { [key: string]: any } = {}, +): Promise { + return analytics.setDefaultEventParameters(params); +} + +/** + * start privacy-sensitive on-device conversion management. + * This is iOS-only. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {string} emailAddress - Email address, properly formatted complete with domain name e.g, 'user@example.com'. + * @returns {Promise} + */ +export function initiateOnDeviceConversionMeasurementWithEmailAddress( + analytics: Analytics, + emailAddress: string, +): Promise { + return analytics.initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress); +} + +/** + * start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * {@link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials} + * + * @param analytics Analytics instance. + * @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, 'user@example.com' + */ +export function initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + analytics: Analytics, + hashedEmailAddress: string, +): Promise { + return analytics.initiateOnDeviceConversionMeasurementWithHashedEmailAddress(hashedEmailAddress); +} + +/** + * start privacy-sensitive on-device conversion management. + * This is iOS-only. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {string} phoneNumber - Phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. + * @returns {Promise} + */ +export function initiateOnDeviceConversionMeasurementWithPhoneNumber( + analytics: Analytics, + phoneNumber: string, +): Promise { + return analytics.initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber); +} + +/** + * start privacy-sensitive on-device conversion management. + * This is iOS-only. + * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile + * {@link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials} + * + * @param analytics Analytics instance. + * @param hashedPhoneNumber sha256-hashed of normalized phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. + */ +export function initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + analytics: Analytics, + hashedPhoneNumber: string, +): Promise { + return analytics.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(hashedPhoneNumber); +} + +/** + * Checks four different things. + * 1. Checks if it's not a browser extension environment. + * 2. Checks if cookies are enabled in current browser. + * 3. Checks if IndexedDB is supported by the browser environment. + * 4. Checks if the current browser context is valid for using IndexedDB.open(). + * @returns {Promise} + */ +export function isSupported(): Promise { + return Promise.resolve(true); +} + +/** + * Sets the applicable end user consent state for this app. + * @param {FirebaseAnalytics} analytics - Analytics instance. + * @param {ConsentSettings} consentSettings - See ConsentSettings. + * @returns {Promise} + */ +export function setConsent(analytics: Analytics, consentSettings: ConsentSettings): Promise { + return analytics.setConsent(consentSettings); +} + +/** + * Configures Firebase Analytics to use custom gtag or dataLayer names. + * Intended to be used if gtag.js script has been installed on this page independently of Firebase Analytics, and is using non-default names for either the gtag function or for dataLayer. Must be called before calling `getAnalytics()` or it won't have any effect. Web only. + * @param {SettingsOptions} _options - See SettingsOptions - currently unused. + * @returns {void} + */ + +export function settings(_options: SettingsOptions): void { + // Returns nothing until Web implemented. +} -// Export namespaced API -export * from './namespaced'; -export { default } from './namespaced'; +setReactNativeModule(nativeModuleName, RNFBAnalyticsModule as unknown as Record); diff --git a/packages/analytics/lib/modular.ts b/packages/analytics/lib/modular.ts deleted file mode 100644 index addddaab2e..0000000000 --- a/packages/analytics/lib/modular.ts +++ /dev/null @@ -1,1040 +0,0 @@ -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import { getApp } from '@react-native-firebase/app'; -import type { Analytics } from './types/analytics'; -import type { AnalyticsInternal } from './types/internal'; -import { Platform } from 'react-native'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import type { - AnalyticsSettings, - AnalyticsCallOptions, - ConsentSettings, - AddPaymentInfoEventParameters, - ScreenViewParameters, - AddShippingInfoParameters, - AddToCartEventParameters, - AddToWishlistEventParameters, - BeginCheckoutEventParameters, - CampaignDetailsEventParameters, - EarnVirtualCurrencyEventParameters, - GenerateLeadEventParameters, - JoinGroupEventParameters, - LevelEndEventParameters, - LevelStartEventParameters, - LevelUpEventParameters, - LoginEventParameters, - PostScoreEventParameters, - SelectContentEventParameters, - PurchaseEventParameters, - RefundEventParameters, - RemoveFromCartEventParameters, - SearchEventParameters, - SelectItemEventParameters, - SetCheckoutOptionEventParameters, - SelectPromotionEventParameters, - ShareEventParameters, - SignUpEventParameters, - SpendVirtualCurrencyEventParameters, - UnlockAchievementEventParameters, - ViewCartEventParameters, - ViewItemEventParameters, - ViewItemListEventParameters, - ViewPromotionEventParameters, - ViewSearchResultsParameters, - SettingsOptions, - EventParams, - CustomEventName, -} from './types/analytics'; - -/** - * Returns an Analytics instance for the given app. - */ -export function getAnalytics(app?: ReactNativeFirebase.FirebaseApp): Analytics { - if (app) { - return getApp(app.name).analytics(); - } - return getApp().analytics(); -} - -/** - * Returns an Analytics instance for the given app. - */ -export function initializeAnalytics( - app: ReactNativeFirebase.FirebaseApp, - _options?: AnalyticsSettings, -): Analytics { - return getApp(app.name).analytics(); -} - -/** - * Retrieves a unique Google Analytics identifier for the web client. - */ -export async function getGoogleAnalyticsClientId(analytics: Analytics): Promise { - if (Platform.OS === 'android' || Platform.OS === 'ios') { - throw new Error('getGoogleAnalyticsClientId is web-only.'); - } else { - const instanceId = await getAppInstanceId(analytics); - return instanceId || ''; - } -} - -/** - * Log a custom event with optional params. - */ -// Overloads for standard event names -export function logEvent( - analytics: Analytics, - name: 'add_payment_info', - params?: { - items?: EventParams['items']; - currency?: EventParams['currency']; - value?: EventParams['value']; - coupon?: EventParams['coupon']; - payment_type?: EventParams['payment_type']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'add_shipping_info', - params?: { - items?: EventParams['items']; - currency?: EventParams['currency']; - value?: EventParams['value']; - coupon?: EventParams['coupon']; - shipping_tier?: EventParams['shipping_tier']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'add_to_cart', - params?: { - items?: EventParams['items']; - currency?: EventParams['currency']; - value?: EventParams['value']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'add_to_wishlist', - params?: { - items?: EventParams['items']; - currency?: EventParams['currency']; - value?: EventParams['value']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'begin_checkout', - params?: { - currency?: EventParams['currency']; - value?: EventParams['value']; - coupon?: EventParams['coupon']; - items?: EventParams['items']; - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'checkout_progress', - params?: { - checkout_step?: EventParams['checkout_step']; - checkout_option?: EventParams['checkout_option']; - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'exception', - params?: { - description?: EventParams['description']; - fatal?: EventParams['fatal']; - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'generate_lead', - params?: { - currency?: EventParams['currency']; - value?: EventParams['value']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'login', - params: { - method: EventParams['method']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'page_view', - params?: { - page_title?: EventParams['page_title']; - page_location?: EventParams['page_location']; - page_path?: EventParams['page_path']; - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'purchase', - params?: { - affiliation?: EventParams['affiliation']; - coupon?: EventParams['coupon']; - currency?: EventParams['currency']; - items?: EventParams['items']; - shipping?: EventParams['shipping']; - tax?: EventParams['tax']; - value?: EventParams['value']; - transaction_id?: EventParams['transaction_id']; - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'refund', - params?: { - affiliation?: EventParams['affiliation']; - coupon?: EventParams['coupon']; - currency?: EventParams['currency']; - items?: EventParams['items']; - shipping?: EventParams['shipping']; - tax?: EventParams['tax']; - value?: EventParams['value']; - transaction_id?: EventParams['transaction_id']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'remove_from_cart', - params?: { - items?: EventParams['items']; - value?: EventParams['value']; - currency?: EventParams['currency']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'screen_view', - params?: { - screen_name?: EventParams['screen_name']; - screen_class?: EventParams['screen_class']; - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'search', - params: { - search_term: EventParams['search_term']; - number_of_nights?: number; - number_of_rooms?: number; - number_of_passengers?: number; - origin?: string; - destination?: string; - start_date?: string; - end_date?: string; - travel_class?: string; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'select_content', - params: { - content_type: EventParams['content_type']; - item_id: EventParams['item_id']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'select_item', - params: { - items?: EventParams['items']; - content_type: EventParams['content_type']; - item_list_id: EventParams['item_list_id']; - item_list_name: EventParams['item_list_name']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'select_promotion', - params: { - creative_name: string; - creative_slot: string; - items?: EventParams['items']; - location_id: string; - promotion_id: EventParams['promotion_id']; - promotion_name: EventParams['promotion_name']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'set_checkout_option', - params?: { - checkout_step?: EventParams['checkout_step']; - checkout_option?: EventParams['checkout_option']; - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'share', - params: { - content_type: EventParams['content_type']; - item_id: EventParams['item_id']; - method: EventParams['method']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'sign_up', - params: { - method: EventParams['method']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'timing_complete', - params?: { - [key: string]: any; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'view_cart', - params?: { - items?: EventParams['items']; - currency?: EventParams['currency']; - value?: EventParams['value']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'view_item', - params?: { - items?: EventParams['items']; - currency?: EventParams['currency']; - value?: EventParams['value']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'view_item_list', - params?: { - items?: EventParams['items']; - item_list_id?: EventParams['item_list_id']; - item_list_name?: EventParams['item_list_name']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'view_promotion', - params?: { - items?: EventParams['items']; - location_id?: string; - creative_name?: string; - creative_slot?: string; - promotion_id?: EventParams['promotion_id']; - promotion_name?: EventParams['promotion_name']; - }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: 'view_search_results', - params: { - search_term: EventParams['search_term']; - }, - options?: AnalyticsCallOptions, -): Promise; - -// Generic fallback for custom event names -export function logEvent( - analytics: Analytics, - name: CustomEventName, - params?: { [key: string]: any }, - options?: AnalyticsCallOptions, -): Promise; - -export function logEvent( - analytics: Analytics, - name: string, - params: { [key: string]: any } = {}, - options: AnalyticsCallOptions = { global: false }, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.logEvent.call(analytics, name, params, options, MODULAR_DEPRECATION_ARG); -} - -/** Logs verified in-app purchase events in Google Analytics for Firebase - * after a purchase is successful. - * Modular API only; iOS only (StoreKit 2). Throws on Android and web before reaching native. - */ -export function logTransaction(analytics: Analytics, transaction_id: string): Promise { - if (Platform.OS !== 'ios') { - return Promise.reject(new Error('logTransaction is only available on iOS')); - } - - return (analytics as AnalyticsInternal).native.logTransaction(transaction_id); -} - -/** - * If true, allows the device to collect analytical data and send it to Firebase. Useful for GDPR. - */ -export function setAnalyticsCollectionEnabled( - analytics: Analytics, - enabled: boolean, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.setAnalyticsCollectionEnabled.call(analytics, enabled, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the duration of inactivity that terminates the current session. - */ -export function setSessionTimeoutDuration( - analytics: Analytics, - milliseconds: number = 1800000, -): Promise { - // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.setSessionTimeoutDuration.call(analytics, milliseconds, MODULAR_DEPRECATION_ARG); -} - -/** - * Retrieve the app instance id of the application. - */ -export function getAppInstanceId(analytics: Analytics): Promise { - // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.getAppInstanceId.call(analytics, MODULAR_DEPRECATION_ARG); -} - -/** - * Retrieves the session id from the client. - * On iOS, Firebase SDK may return an error that is handled internally and may take many minutes to return a valid value. Check native debug logs for more details. - */ -export function getSessionId(analytics: Analytics): Promise { - // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.getSessionId.call(analytics, MODULAR_DEPRECATION_ARG); -} - -/** - * Gives a user a unique identification. - */ -export function setUserId(analytics: Analytics, id: string | null): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.setUserId.call(analytics, id, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets a key/value pair of data on the current user. Each Firebase project can have up to 25 uniquely named (case-sensitive) user properties. - */ -export function setUserProperty( - analytics: Analytics, - name: string, - value: string | null, -): Promise { - // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.setUserProperty.call(analytics, name, value, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets multiple key/value pairs of data on the current user. Each Firebase project can have up to 25 uniquely named (case-sensitive) user properties. - */ -export function setUserProperties( - analytics: Analytics, - properties: { [key: string]: string | null }, - options: AnalyticsCallOptions = { global: false }, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.setUserProperties.call(analytics, properties, options, MODULAR_DEPRECATION_ARG); -} - -/** - * Clears all analytics data for this instance from the device and resets the app instance ID. - */ -export function resetAnalyticsData(analytics: Analytics): Promise { - // This doesn't exist on firebase-js-sdk, but probably should keep this for android & iOS - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.resetAnalyticsData.call(analytics, MODULAR_DEPRECATION_ARG); -} - -/** - * E-Commerce Purchase event. This event signifies that an item(s) was purchased by a user. Note: This is different from the in-app purchase event, which is reported automatically for Google Play-based apps. - */ -export function logAddPaymentInfo( - analytics: Analytics, - params: AddPaymentInfoEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logAddPaymentInfo(params); -} - -/** - * Sets or clears the screen name and class the user is currently viewing. - */ -export function logScreenView(analytics: Analytics, params: ScreenViewParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logScreenView(params); -} - -/** - * Add Payment Info event. This event signifies that a user has submitted their payment information to your app. - */ -export function logAddShippingInfo( - analytics: Analytics, - params: AddShippingInfoParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logAddShippingInfo(params); -} - -/** - * E-Commerce Add To Cart event. - */ -export function logAddToCart( - analytics: Analytics, - params: AddToCartEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logAddToCart(params); -} - -/** - * E-Commerce Add To Wishlist event. This event signifies that an item was added to a wishlist. - */ -export function logAddToWishlist( - analytics: Analytics, - params: AddToWishlistEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logAddToWishlist(params); -} - -/** - * App Open event. By logging this event when an App is moved to the foreground, developers can understand how often users leave and return during the course of a Session. - */ -export function logAppOpen(analytics: Analytics): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logAppOpen(); -} - -/** - * E-Commerce Begin Checkout event. This event signifies that a user has begun the process of checking out. - */ -export function logBeginCheckout( - analytics: Analytics, - params: BeginCheckoutEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logBeginCheckout(params); -} - -/** - * Log this event to supply the referral details of a re-engagement campaign. - */ -export function logCampaignDetails( - analytics: Analytics, - params: CampaignDetailsEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logCampaignDetails(params); -} - -/** - * Earn Virtual Currency event. This event tracks the awarding of virtual currency in your app. - */ -export function logEarnVirtualCurrency( - analytics: Analytics, - params: EarnVirtualCurrencyEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logEarnVirtualCurrency(params); -} - -/** - * Generate Lead event. Log this event when a lead has been generated in the app. - */ -export function logGenerateLead( - analytics: Analytics, - params: GenerateLeadEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logGenerateLead(params); -} - -/** - * Join Group event. Log this event when a user joins a group such as a guild, team or family. - */ -export function logJoinGroup( - analytics: Analytics, - params: JoinGroupEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logJoinGroup(params); -} - -/** - * Level End event. - */ -export function logLevelEnd(analytics: Analytics, params: LevelEndEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logLevelEnd(params); -} - -/** - * Level Start event. - */ -export function logLevelStart( - analytics: Analytics, - params: LevelStartEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logLevelStart(params); -} - -/** - * Level Up event. This event signifies that a player has leveled up in your gaming app. - */ -export function logLevelUp(analytics: Analytics, params: LevelUpEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logLevelUp(params); -} - -/** - * Login event. Apps with a login feature can report this event to signify that a user has logged in. - */ -export function logLogin(analytics: Analytics, params: LoginEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logLogin(params); -} - -/** - * Post Score event. Log this event when the user posts a score in your gaming app. - */ -export function logPostScore( - analytics: Analytics, - params: PostScoreEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logPostScore(params); -} - -/** - * Select Content event. This general purpose event signifies that a user has selected some content of a certain type in an app. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {SelectContentEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logSelectContent( - analytics: Analytics, - params: SelectContentEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logSelectContent(params); -} - -/** - * E-Commerce Purchase event. This event signifies that an item(s) was purchased by a user. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {PurchaseEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logPurchase(analytics: Analytics, params: PurchaseEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logPurchase(params); -} - -/** - * E-Commerce Refund event. This event signifies that a refund was issued. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {RefundEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logRefund(analytics: Analytics, params: RefundEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logRefund(params); -} - -/** - * Remove from cart event. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {RemoveFromCartEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logRemoveFromCart( - analytics: Analytics, - params: RemoveFromCartEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logRemoveFromCart(params); -} - -/** - * Search event. Apps that support search features can use this event to contextualize search operations by supplying the appropriate, corresponding parameters. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {SearchEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logSearch(analytics: Analytics, params: SearchEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logSearch(params); -} - -/** - * Select Item event. This event signifies that an item was selected by a user from a list. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {SelectItemEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logSelectItem( - analytics: Analytics, - params: SelectItemEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logSelectItem(params); -} - -/** - * Set checkout option event. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {SetCheckoutOptionEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logSetCheckoutOption( - analytics: Analytics, - params: SetCheckoutOptionEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logSetCheckoutOption(params); -} - -/** - * Select promotion event. This event signifies that a user has selected a promotion offer. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {SelectPromotionEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logSelectPromotion( - analytics: Analytics, - params: SelectPromotionEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logSelectPromotion(params); -} - -/** - * Share event. Apps with social features can log the Share event to identify the most viral content. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {ShareEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logShare(analytics: Analytics, params: ShareEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logShare(params); -} - -/** - * Sign Up event. This event indicates that a user has signed up for an account in your app. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {SignUpEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logSignUp(analytics: Analytics, params: SignUpEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logSignUp(params); -} - -/** - * Spend Virtual Currency event. This event tracks the sale of virtual goods in your app. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {SpendVirtualCurrencyEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logSpendVirtualCurrency( - analytics: Analytics, - params: SpendVirtualCurrencyEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logSpendVirtualCurrency(params); -} - -/** - * Tutorial Begin event. This event signifies the start of the on-boarding process in your app. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @returns {Promise} - */ -export function logTutorialBegin(analytics: Analytics): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logTutorialBegin(); -} - -/** - * Tutorial End event. Use this event to signify the user's completion of your app's on-boarding process. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @returns {Promise} - */ -export function logTutorialComplete(analytics: Analytics): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logTutorialComplete(); -} - -/** - * Unlock Achievement event. Log this event when the user has unlocked an achievement in your game. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {UnlockAchievementEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logUnlockAchievement( - analytics: Analytics, - params: UnlockAchievementEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logUnlockAchievement(params); -} - -/** - * E-commerce View Cart event. This event signifies that a user has viewed their cart. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {ViewCartEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logViewCart(analytics: Analytics, params: ViewCartEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logViewCart(params); -} - -/** - * View Item event. This event signifies that some content was shown to the user. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {ViewItemEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logViewItem(analytics: Analytics, params: ViewItemEventParameters): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logViewItem(params); -} - -/** - * View Item List event. Log this event when the user has been presented with a list of items of a certain category. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {ViewItemListEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logViewItemList( - analytics: Analytics, - params: ViewItemListEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logViewItemList(params); -} - -/** - * View Promotion event. This event signifies that a promotion was shown to a user. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {ViewPromotionEventParameters} params - Event parameters. - * @returns {Promise} - */ -export function logViewPromotion( - analytics: Analytics, - params: ViewPromotionEventParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logViewPromotion(params); -} - -/** - * View Search Results event. Log this event when the user has been presented with the results of a search. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {ViewSearchResultsParameters} params - Event parameters. - * @returns {Promise} - */ -export function logViewSearchResults( - analytics: Analytics, - params: ViewSearchResultsParameters, -): Promise { - // This is deprecated for both namespaced and modular. - return analytics.logViewSearchResults(params); -} - -/** - * Adds parameters that will be set on every event logged from the SDK, including automatic ones. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {object} [params={}] - Parameters to be added to the map of parameters added to every event. - * @returns {Promise} - */ -export function setDefaultEventParameters( - analytics: Analytics, - params: { [key: string]: any } = {}, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.setDefaultEventParameters.call(analytics, params, MODULAR_DEPRECATION_ARG); -} - -/** - * start privacy-sensitive on-device conversion management. - * This is iOS-only. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {string} emailAddress - Email address, properly formatted complete with domain name e.g, 'user@example.com'. - * @returns {Promise} - */ -export function initiateOnDeviceConversionMeasurementWithEmailAddress( - analytics: Analytics, - emailAddress: string, -): Promise { - return analytics.initiateOnDeviceConversionMeasurementWithEmailAddress.call( - analytics, - emailAddress, - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * start privacy-sensitive on-device conversion management. - * This is iOS-only. - * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile - * {@link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials} - * - * @param analytics Analytics instance. - * @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, 'user@example.com' - */ -export function initiateOnDeviceConversionMeasurementWithHashedEmailAddress( - analytics: Analytics, - hashedEmailAddress: string, -): Promise { - return analytics.initiateOnDeviceConversionMeasurementWithHashedEmailAddress.call( - analytics, - hashedEmailAddress, - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * start privacy-sensitive on-device conversion management. - * This is iOS-only. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {string} phoneNumber - Phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. - * @returns {Promise} - */ -export function initiateOnDeviceConversionMeasurementWithPhoneNumber( - analytics: Analytics, - phoneNumber: string, -): Promise { - return analytics.initiateOnDeviceConversionMeasurementWithPhoneNumber.call( - analytics, - phoneNumber, - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * start privacy-sensitive on-device conversion management. - * This is iOS-only. - * This is a no-op if you do not include '$RNFirebaseAnalyticsGoogleAppMeasurementOnDeviceConversion = true' in your Podfile - * {@link https://firebase.google.com/docs/tutorials/ads-ios-on-device-measurement/step-3#use-hashed-credentials} - * - * @param analytics Analytics instance. - * @param hashedPhoneNumber sha256-hashed of normalized phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces. - */ -export function initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( - analytics: Analytics, - hashedPhoneNumber: string, -): Promise { - return analytics.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber.call( - analytics, - hashedPhoneNumber, - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Checks four different things. - * 1. Checks if it's not a browser extension environment. - * 2. Checks if cookies are enabled in current browser. - * 3. Checks if IndexedDB is supported by the browser environment. - * 4. Checks if the current browser context is valid for using IndexedDB.open(). - * @returns {Promise} - */ -export function isSupported(): Promise { - return Promise.resolve(true); -} - -/** - * Sets the applicable end user consent state for this app. - * @param {FirebaseAnalytics} analytics - Analytics instance. - * @param {ConsentSettings} consentSettings - See ConsentSettings. - * @returns {Promise} - */ -export function setConsent(analytics: Analytics, consentSettings: ConsentSettings): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is filtered out internally - return analytics.setConsent.call(analytics, consentSettings, MODULAR_DEPRECATION_ARG); -} - -/** - * Configures Firebase Analytics to use custom gtag or dataLayer names. - * Intended to be used if gtag.js script has been installed on this page independently of Firebase Analytics, and is using non-default names for either the gtag function or for dataLayer. Must be called before calling `getAnalytics()` or it won't have any effect. Web only. - * @param {SettingsOptions} _options - See SettingsOptions - currently unused. - * @returns {void} - */ - -export function settings(_options: SettingsOptions): void { - // Returns nothing until Web implemented. -} diff --git a/packages/analytics/lib/namespaced.ts b/packages/analytics/lib/namespaced.ts deleted file mode 100644 index 65a6f7c96b..0000000000 --- a/packages/analytics/lib/namespaced.ts +++ /dev/null @@ -1,915 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -import { - isAlphaNumericUnderscore, - isE164PhoneNumber, - isIOS, - isNull, - isNumber, - isObject, - isOneOf, - isString, - isUndefined, -} from '@react-native-firebase/app/dist/module/common'; - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; - -import './types/internal'; - -// Internal types are now available through module declarations in app package -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import { isBoolean } from '@react-native-firebase/app/dist/module/common'; - -import { validateStruct, validateCompound } from './struct'; -import { RNFBAnalyticsModule } from './web/RNFBAnalyticsModule'; -import { version } from './version'; -import * as structs from './structs'; -import { - type Statics, - type AnalyticsCallOptions, - type ConsentSettings, - type AddPaymentInfoEventParameters, - type ScreenViewParameters, - type AddShippingInfoParameters, - type AddToCartEventParameters, - type AddToWishlistEventParameters, - type BeginCheckoutEventParameters, - type CampaignDetailsEventParameters, - type EarnVirtualCurrencyEventParameters, - type GenerateLeadEventParameters, - type JoinGroupEventParameters, - type LevelEndEventParameters, - type LevelStartEventParameters, - type LevelUpEventParameters, - type LoginEventParameters, - type PostScoreEventParameters, - type SelectContentEventParameters, - type PurchaseEventParameters, - type RefundEventParameters, - type RemoveFromCartEventParameters, - type SearchEventParameters, - type SelectItemEventParameters, - type SetCheckoutOptionEventParameters, - type SelectPromotionEventParameters, - type ShareEventParameters, - type SignUpEventParameters, - type SpendVirtualCurrencyEventParameters, - type UnlockAchievementEventParameters, - type ViewCartEventParameters, - type ViewItemEventParameters, - type ViewItemListEventParameters, - type ViewPromotionEventParameters, - type ViewSearchResultsParameters, - type Analytics, -} from './types/analytics'; - -const ReservedEventNames: readonly string[] = [ - 'ad_activeview', - 'ad_click', - 'ad_exposure', - // 'ad_impression', // manual ad_impression logging is allowed, See #6307 - 'ad_query', - 'ad_reward', - 'adunit_exposure', - 'app_background', - 'app_clear_data', - // 'app_exception', - 'app_remove', - 'app_store_refund', - 'app_store_subscription_cancel', - 'app_store_subscription_convert', - 'app_store_subscription_renew', - 'app_update', - 'app_upgrade', - 'dynamic_link_app_open', - 'dynamic_link_app_update', - 'dynamic_link_first_open', - 'error', - 'first_open', - 'first_visit', - 'in_app_purchase', - 'notification_dismiss', - 'notification_foreground', - 'notification_open', - 'notification_receive', - 'os_update', - 'session_start', - 'session_start_with_rollout', - 'user_engagement', -] as const; - -const statics: Partial = {}; - -const namespace = 'analytics'; - -const nativeModuleName = 'RNFBAnalyticsModule'; - -class FirebaseAnalyticsModule extends FirebaseModule { - logEvent( - name: string, - params: { [key: string]: any } = {}, - options: AnalyticsCallOptions = { global: false }, - ): Promise { - if (!isString(name)) { - throw new Error("firebase.analytics().logEvent(*) 'name' expected a string value."); - } - - if (!isUndefined(params) && !isObject(params)) { - throw new Error("firebase.analytics().logEvent(_, *) 'params' expected an object value."); - } - - // check name is not a reserved event name - if (isOneOf(name, ReservedEventNames as any[])) { - throw new Error( - `firebase.analytics().logEvent(*) 'name' the event name '${name}' is reserved and can not be used.`, - ); - } - - // name format validation - if (!isAlphaNumericUnderscore(name) || name.length > 40) { - throw new Error( - `firebase.analytics().logEvent(*) 'name' invalid event name '${name}'. Names should contain 1 to 40 alphanumeric characters or underscores.`, - ); - } - - if (!isUndefined(options)) { - if (!isObject(options)) { - throw new Error( - "firebase.analytics().logEvent(_, _, *) 'options' expected an object value.", - ); - } - - if (!isUndefined(options.global) && !isBoolean(options.global)) { - throw new Error("'options.global' property expected a boolean."); - } - } - - return this.native.logEvent(name, params); - } - - setAnalyticsCollectionEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.analytics().setAnalyticsCollectionEnabled(*) 'enabled' expected a boolean value.", - ); - } - - return this.native.setAnalyticsCollectionEnabled(enabled); - } - - setSessionTimeoutDuration(milliseconds: number = 1800000): Promise { - if (!isNumber(milliseconds)) { - throw new Error( - "firebase.analytics().setSessionTimeoutDuration(*) 'milliseconds' expected a number value.", - ); - } - - if (milliseconds < 0) { - throw new Error( - "firebase.analytics().setSessionTimeoutDuration(*) 'milliseconds' expected a positive number value.", - ); - } - - return this.native.setSessionTimeoutDuration(milliseconds); - } - - getAppInstanceId(): Promise { - return this.native.getAppInstanceId(); - } - - getSessionId(): Promise { - return this.native.getSessionId(); - } - - setUserId(id: string | null): Promise { - if (!isNull(id) && !isString(id)) { - throw new Error("firebase.analytics().setUserId(*) 'id' expected a string value."); - } - - return this.native.setUserId(id); - } - - setUserProperty(name: string, value: string | null): Promise { - if (!isString(name)) { - throw new Error("firebase.analytics().setUserProperty(*) 'name' expected a string value."); - } - - if (value !== null && !isString(value)) { - throw new Error( - "firebase.analytics().setUserProperty(_, *) 'value' expected a string value.", - ); - } - - return this.native.setUserProperty(name, value); - } - - setUserProperties( - properties: { [key: string]: string | null }, - options: AnalyticsCallOptions = { global: false }, - ): Promise { - if (!isObject(properties)) { - throw new Error( - "firebase.analytics().setUserProperties(*) 'properties' expected an object of key/value pairs.", - ); - } - - if (!isUndefined(options)) { - if (!isObject(options)) { - throw new Error( - "firebase.analytics().logEvent(_, _, *) 'options' expected an object value.", - ); - } - - if (!isUndefined(options.global) && !isBoolean(options.global)) { - throw new Error("'options.global' property expected a boolean."); - } - } - - const entries = Object.entries(properties); - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (!entry) continue; - const [key, value] = entry; - if (!isNull(value) && !isString(value)) { - throw new Error( - `firebase.analytics().setUserProperties(*) 'properties' value for parameter '${key}' is invalid, expected a string.`, - ); - } - } - - return this.native.setUserProperties(properties); - } - - resetAnalyticsData(): Promise { - return this.native.resetAnalyticsData(); - } - - setConsent(consentSettings: ConsentSettings): Promise { - if (!isObject(consentSettings)) { - throw new Error( - 'firebase.analytics().setConsent(*): The supplied arg must be an object of key/values.', - ); - } - - const entries = Object.entries(consentSettings); - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (!entry) continue; - const [key, value] = entry; - if (key === 'security_storage') { - if (!isOneOf(value, ['granted', 'denied'])) { - throw new Error( - `firebase.analytics().setConsent(*) 'consentSettings' value for parameter '${key}' is invalid, expected one of 'granted' or 'denied'.`, - ); - } - continue; - } - if (!isBoolean(value)) { - throw new Error( - `firebase.analytics().setConsent(*) 'consentSettings' value for parameter '${key}' is invalid, expected a boolean.`, - ); - } - } - - return this.native.setConsent(consentSettings); - } - - /** ------------------- - * EVENTS - * -------------------- */ - logAddPaymentInfo(object: AddPaymentInfoEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logAddPaymentInfo(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddPaymentInfo(*):'); - - return this.logEvent( - 'add_payment_info', - validateStruct(object, structs.AddPaymentInfo, 'firebase.analytics().logAddPaymentInfo(*):'), - ); - } - - logScreenView(object: ScreenViewParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logScreenView(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'screen_view', - validateStruct(object, structs.ScreenView, 'firebase.analytics().logScreenView(*):'), - ); - } - - logAddShippingInfo(object: AddShippingInfoParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logAddShippingInfo(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddShippingInfo(*):'); - - return this.logEvent( - 'add_shipping_info', - validateStruct( - object, - structs.AddShippingInfo, - 'firebase.analytics().logAddShippingInfo(*):', - ), - ); - } - - logAddToCart(object: AddToCartEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logAddToCart(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddToCart(*):'); - - return this.logEvent( - 'add_to_cart', - validateStruct(object, structs.AddToCart, 'firebase.analytics().logAddToCart(*):'), - ); - } - - logAddToWishlist(object: AddToWishlistEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logAddToWishlist(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logAddToWishlist(*):'); - - return this.logEvent( - 'add_to_wishlist', - validateStruct(object, structs.AddToWishlist, 'firebase.analytics().logAddToWishlist(*):'), - ); - } - - logAppOpen(): Promise { - return this.logEvent('app_open'); - } - - logBeginCheckout(object: BeginCheckoutEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logBeginCheckout(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logBeginCheckout(*):'); - - return this.logEvent( - 'begin_checkout', - validateStruct(object, structs.BeginCheckout, 'firebase.analytics().logBeginCheckout(*):'), - ); - } - - logCampaignDetails(object: CampaignDetailsEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logCampaignDetails(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'campaign_details', - validateStruct( - object, - structs.CampaignDetails, - 'firebase.analytics().logCampaignDetails(*):', - ), - ); - } - - logEarnVirtualCurrency(object: EarnVirtualCurrencyEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logEarnVirtualCurrency(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'earn_virtual_currency', - validateStruct( - object, - structs.EarnVirtualCurrency, - 'firebase.analytics().logEarnVirtualCurrency(*):', - ), - ); - } - - logGenerateLead(object: GenerateLeadEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logGenerateLead(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logGenerateLead(*):'); - - return this.logEvent( - 'generate_lead', - validateStruct(object, structs.GenerateLead, 'firebase.analytics().logGenerateLead(*):'), - ); - } - - logJoinGroup(object: JoinGroupEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logJoinGroup(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'join_group', - validateStruct(object, structs.JoinGroup, 'firebase.analytics().logJoinGroup(*):'), - ); - } - - logLevelEnd(object: LevelEndEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logLevelEnd(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'level_end', - validateStruct(object, structs.LevelEnd, 'firebase.analytics().logLevelEnd(*):'), - ); - } - - logLevelStart(object: LevelStartEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logLevelStart(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'level_start', - validateStruct(object, structs.LevelStart, 'firebase.analytics().logLevelStart(*):'), - ); - } - - logLevelUp(object: LevelUpEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logLevelUp(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'level_up', - validateStruct(object, structs.LevelUp, 'firebase.analytics().logLevelUp(*):'), - ); - } - - logLogin(object: LoginEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logLogin(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'login', - validateStruct(object, structs.Login, 'firebase.analytics().logLogin(*):'), - ); - } - - logPostScore(object: PostScoreEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logPostScore(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'post_score', - validateStruct(object, structs.PostScore, 'firebase.analytics().logPostScore(*):'), - ); - } - - logSelectContent(object: SelectContentEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logSelectContent(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'select_content', - validateStruct(object, structs.SelectContent, 'firebase.analytics().logSelectContent(*):'), - ); - } - - logPurchase(object: PurchaseEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logPurchase(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logPurchase(*):'); - - return this.logEvent( - 'purchase', - validateStruct(object, structs.Purchase, 'firebase.analytics().logPurchaseEvent(*):'), - ); - } - - logRefund(object: RefundEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logRefund(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logRefund(*):'); - - return this.logEvent( - 'refund', - validateStruct(object, structs.Refund, 'firebase.analytics().logRefund(*):'), - ); - } - - logRemoveFromCart(object: RemoveFromCartEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logRemoveFromCart(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logRemoveFromCart(*):'); - - return this.logEvent( - 'remove_from_cart', - validateStruct(object, structs.RemoveFromCart, 'firebase.analytics().logRemoveFromCart(*):'), - ); - } - - logSearch(object: SearchEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logSearch(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'search', - validateStruct(object, structs.Search, 'firebase.analytics().logSearch(*):'), - ); - } - - logSelectItem(object: SelectItemEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logSelectItem(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'select_item', - validateStruct(object, structs.SelectItem, 'firebase.analytics().logSelectItem(*):'), - ); - } - - logSetCheckoutOption(object: SetCheckoutOptionEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logSetCheckoutOption(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'set_checkout_option', - validateStruct( - object, - structs.SetCheckoutOption, - 'firebase.analytics().logSetCheckoutOption(*):', - ), - ); - } - - logSelectPromotion(object: SelectPromotionEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logSelectPromotion(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'select_promotion', - validateStruct( - object, - structs.SelectPromotion, - 'firebase.analytics().logSelectPromotion(*):', - ), - ); - } - - logShare(object: ShareEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logShare(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'share', - validateStruct(object, structs.Share, 'firebase.analytics().logShare(*):'), - ); - } - - logSignUp(object: SignUpEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logSignUp(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'sign_up', - validateStruct(object, structs.SignUp, 'firebase.analytics().logSignUp(*):'), - ); - } - - logSpendVirtualCurrency(object: SpendVirtualCurrencyEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logSpendVirtualCurrency(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'spend_virtual_currency', - validateStruct( - object, - structs.SpendVirtualCurrency, - 'firebase.analytics().logSpendVirtualCurrency(*):', - ), - ); - } - - logTutorialBegin(): Promise { - return this.logEvent('tutorial_begin'); - } - - logTutorialComplete(): Promise { - return this.logEvent('tutorial_complete'); - } - - logUnlockAchievement(object: UnlockAchievementEventParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logUnlockAchievement(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'unlock_achievement', - validateStruct( - object, - structs.UnlockAchievement, - 'firebase.analytics().logUnlockAchievement(*):', - ), - ); - } - - logViewCart(object: ViewCartEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logViewCart(*): The supplied arg must be an object of key/values.', - ); - } - - validateCompound(object, 'value', 'currency', 'firebase.analytics().logViewCart(*):'); - - return this.logEvent( - 'view_cart', - validateStruct(object, structs.ViewCart, 'firebase.analytics().logViewCart(*):'), - ); - } - - logViewItem(object: ViewItemEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logViewItem(*): The supplied arg must be an object of key/values.', - ); - } - validateCompound(object, 'value', 'currency', 'firebase.analytics().logViewItem(*):'); - - return this.logEvent( - 'view_item', - validateStruct(object, structs.ViewItem, 'firebase.analytics().logViewItem(*):'), - ); - } - - logViewItemList(object: ViewItemListEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logViewItemList(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'view_item_list', - validateStruct(object, structs.ViewItemList, 'firebase.analytics().logViewItemList(*):'), - ); - } - - logViewPromotion(object: ViewPromotionEventParameters = {}): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logViewPromotion(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'view_promotion', - validateStruct(object, structs.ViewPromotion, 'firebase.analytics().logViewPromotion(*):'), - ); - } - /** - * Unsupported in "Enhanced Ecommerce reports": - * https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Event#public-static-final-string-view_search_results - */ - logViewSearchResults(object: ViewSearchResultsParameters): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.analytics().logViewSearchResults(*): The supplied arg must be an object of key/values.', - ); - } - - return this.logEvent( - 'view_search_results', - validateStruct( - object, - structs.ViewSearchResults, - 'firebase.analytics().logViewSearchResults(*):', - ), - ); - } - - setDefaultEventParameters(params?: { [key: string]: any }): Promise { - if (!isObject(params) && !isNull(params) && !isUndefined(params)) { - throw new Error( - "firebase.analytics().setDefaultEventParameters(*) 'params' expected an object value when it is defined.", - ); - } - - return this.native.setDefaultEventParameters(params); - } - - initiateOnDeviceConversionMeasurementWithEmailAddress(emailAddress: string): Promise { - if (!isString(emailAddress)) { - throw new Error( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithEmailAddress(*) 'emailAddress' expected a string value.", - ); - } - - if (!isIOS) { - return Promise.resolve(); - } - - return ( - this.native.initiateOnDeviceConversionMeasurementWithEmailAddress?.(emailAddress) ?? - Promise.resolve() - ); - } - - initiateOnDeviceConversionMeasurementWithHashedEmailAddress( - hashedEmailAddress: string, - ): Promise { - if (!isString(hashedEmailAddress)) { - throw new Error( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a string value.", - ); - } - - if (!isIOS) { - return Promise.resolve(); - } - - return ( - this.native.initiateOnDeviceConversionMeasurementWithHashedEmailAddress?.( - hashedEmailAddress, - ) ?? Promise.resolve() - ); - } - - initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber: string): Promise { - if (!isE164PhoneNumber(phoneNumber)) { - throw new Error( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithPhoneNumber(*) 'phoneNumber' expected a string value in E.164 format.", - ); - } - - if (!isIOS) { - return Promise.resolve(); - } - - return ( - this.native.initiateOnDeviceConversionMeasurementWithPhoneNumber?.(phoneNumber) ?? - Promise.resolve() - ); - } - - initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( - hashedPhoneNumber: string, - ): Promise { - if (isE164PhoneNumber(hashedPhoneNumber)) { - throw new Error( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.", - ); - } - - if (!isString(hashedPhoneNumber)) { - throw new Error( - "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.", - ); - } - - if (!isIOS) { - return Promise.resolve(); - } - - return ( - this.native.initiateOnDeviceConversionMeasurementWithHashedPhoneNumber?.(hashedPhoneNumber) ?? - Promise.resolve() - ); - } -} - -// import { SDK_VERSION } from '@react-native-firebase/analytics'; -export const SDK_VERSION: string = version; - -// import analytics from '@react-native-firebase/analytics'; -// analytics().logEvent(...); - -// import analytics, { firebase } from '@react-native-firebase/analytics'; -// analytics().logEvent(...); -// firebase.analytics().logEvent(...); - -const analyticsNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: false, - hasMultiAppSupport: false, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseAnalyticsModule, -}); - -type AnalyticsNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - Analytics, - Statics -> & { - analytics: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -export default analyticsNamespace as unknown as AnalyticsNamespace; - -// Register the interop module for non-native platforms. -setReactNativeModule(nativeModuleName, RNFBAnalyticsModule as unknown as Record); - -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'analytics', - Analytics, - Statics, - false - >; diff --git a/packages/analytics/lib/types/analytics.ts b/packages/analytics/lib/types/analytics.ts index 22ade88d20..a2102fb48c 100644 --- a/packages/analytics/lib/types/analytics.ts +++ b/packages/analytics/lib/types/analytics.ts @@ -959,121 +959,3 @@ export interface Analytics extends ReactNativeFirebase.FirebaseModule { hashedPhoneNumber: string, ): Promise; } - -/* eslint-disable @typescript-eslint/no-namespace */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - interface Module { - analytics: FirebaseModuleWithStaticsAndApp; - } - interface FirebaseApp { - analytics(): Analytics; - } - } -} -/* eslint-enable @typescript-eslint/no-namespace */ - -// ============ Backwards Compatibility Namespace - to be removed with namespaced exports ============ - -// Helper types to reference outer scope types within the namespace -// These are needed because TypeScript can't directly alias types with the same name -type _Item = Item; -type _AddPaymentInfoEventParameters = AddPaymentInfoEventParameters; -type _AddShippingInfoEventParameters = AddShippingInfoEventParameters; -type _AddToCartEventParameters = AddToCartEventParameters; -type _AddToWishlistEventParameters = AddToWishlistEventParameters; -type _BeginCheckoutEventParameters = BeginCheckoutEventParameters; -type _CampaignDetailsEventParameters = CampaignDetailsEventParameters; -type _EarnVirtualCurrencyEventParameters = EarnVirtualCurrencyEventParameters; -type _GenerateLeadEventParameters = GenerateLeadEventParameters; -type _JoinGroupEventParameters = JoinGroupEventParameters; -type _LevelEndEventParameters = LevelEndEventParameters; -type _LevelStartEventParameters = LevelStartEventParameters; -type _LevelUpEventParameters = LevelUpEventParameters; -type _LoginEventParameters = LoginEventParameters; -type _PostScoreEventParameters = PostScoreEventParameters; -type _PurchaseEventParameters = PurchaseEventParameters; -type _ScreenViewParameters = ScreenViewParameters; -type _RefundEventParameters = RefundEventParameters; -type _RemoveFromCartEventParameters = RemoveFromCartEventParameters; -type _SearchEventParameters = SearchEventParameters; -type _SelectContentEventParameters = SelectContentEventParameters; -type _SelectItemEventParameters = SelectItemEventParameters; -type _SetCheckoutOptionEventParameters = SetCheckoutOptionEventParameters; -type _SelectPromotionEventParameters = SelectPromotionEventParameters; -type _ShareEventParameters = ShareEventParameters; -type _SignUpEventParameters = SignUpEventParameters; -type _SpendVirtualCurrencyEventParameters = SpendVirtualCurrencyEventParameters; -type _UnlockAchievementEventParameters = UnlockAchievementEventParameters; -type _ViewCartEventParameters = ViewCartEventParameters; -type _ViewItemEventParameters = ViewItemEventParameters; -type _ViewSearchResultsParameters = ViewSearchResultsParameters; -type _ViewItemListEventParameters = ViewItemListEventParameters; -type _ViewPromotionEventParameters = ViewPromotionEventParameters; -type _AddShippingInfoParameters = AddShippingInfoParameters; -type _AnalyticsSettings = AnalyticsSettings; -type _AnalyticsCallOptions = AnalyticsCallOptions; -type _GtagConfigParams = GtagConfigParams; -type _ConsentSettings = ConsentSettings; -type _SettingsOptions = SettingsOptions; -type _EventParams = EventParams; -type _Statics = Statics; - -/** - * @deprecated Use the exported types directly instead. - * FirebaseAnalyticsTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseAnalyticsTypes { - // Short name aliases referencing top-level types - export type Module = Analytics; - export type CallOptions = AnalyticsCallOptions; - export type Consent = ConsentSettings; - export type Settings = AnalyticsSettings; - export type Statics = _Statics; - - // Event parameter interfaces - export type Item = _Item; - export type AddPaymentInfoEventParameters = _AddPaymentInfoEventParameters; - export type AddShippingInfoEventParameters = _AddShippingInfoEventParameters; - export type AddToCartEventParameters = _AddToCartEventParameters; - export type AddToWishlistEventParameters = _AddToWishlistEventParameters; - export type BeginCheckoutEventParameters = _BeginCheckoutEventParameters; - export type CampaignDetailsEventParameters = _CampaignDetailsEventParameters; - export type EarnVirtualCurrencyEventParameters = _EarnVirtualCurrencyEventParameters; - export type GenerateLeadEventParameters = _GenerateLeadEventParameters; - export type JoinGroupEventParameters = _JoinGroupEventParameters; - export type LevelEndEventParameters = _LevelEndEventParameters; - export type LevelStartEventParameters = _LevelStartEventParameters; - export type LevelUpEventParameters = _LevelUpEventParameters; - export type LoginEventParameters = _LoginEventParameters; - export type PostScoreEventParameters = _PostScoreEventParameters; - export type PurchaseEventParameters = _PurchaseEventParameters; - export type ScreenViewParameters = _ScreenViewParameters; - export type RefundEventParameters = _RefundEventParameters; - export type RemoveFromCartEventParameters = _RemoveFromCartEventParameters; - export type SearchEventParameters = _SearchEventParameters; - export type SelectContentEventParameters = _SelectContentEventParameters; - export type SelectItemEventParameters = _SelectItemEventParameters; - export type SetCheckoutOptionEventParameters = _SetCheckoutOptionEventParameters; - export type SelectPromotionEventParameters = _SelectPromotionEventParameters; - export type ShareEventParameters = _ShareEventParameters; - export type SignUpEventParameters = _SignUpEventParameters; - export type SpendVirtualCurrencyEventParameters = _SpendVirtualCurrencyEventParameters; - export type UnlockAchievementEventParameters = _UnlockAchievementEventParameters; - export type ViewCartEventParameters = _ViewCartEventParameters; - export type ViewItemEventParameters = _ViewItemEventParameters; - export type ViewSearchResultsParameters = _ViewSearchResultsParameters; - export type ViewItemListEventParameters = _ViewItemListEventParameters; - export type ViewPromotionEventParameters = _ViewPromotionEventParameters; - export type AddShippingInfoParameters = _AddShippingInfoParameters; - - // Configuration and settings interfaces - export type AnalyticsSettings = _AnalyticsSettings; - export type AnalyticsCallOptions = _AnalyticsCallOptions; - export type GtagConfigParams = _GtagConfigParams; - export type ConsentSettings = _ConsentSettings; - export type SettingsOptions = _SettingsOptions; - export type EventParams = _EventParams; -} -/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/analytics/type-test.ts b/packages/analytics/type-test.ts index 4582265702..9b560d69c0 100644 --- a/packages/analytics/type-test.ts +++ b/packages/analytics/type-test.ts @@ -1,56 +1,10 @@ -import analytics, { - firebase, - // Types - type Analytics, - type AnalyticsCallOptions, - type ConsentSettings, - type AnalyticsSettings, - type SettingsOptions, - type Currency, - type ConsentStatusString, - type Promotion, - type Item, - type AddPaymentInfoEventParameters, - type AddShippingInfoParameters, - type AddToCartEventParameters, - type AddToWishlistEventParameters, - type BeginCheckoutEventParameters, - type CampaignDetailsEventParameters, - type EarnVirtualCurrencyEventParameters, - type GenerateLeadEventParameters, - type JoinGroupEventParameters, - type LevelEndEventParameters, - type LevelStartEventParameters, - type LevelUpEventParameters, - type LoginEventParameters, - type PostScoreEventParameters, - type SelectContentEventParameters, - type PurchaseEventParameters, - type RefundEventParameters, - type RemoveFromCartEventParameters, - type SearchEventParameters, - type SelectItemEventParameters, - type SetCheckoutOptionEventParameters, - type SelectPromotionEventParameters, - type ShareEventParameters, - type SignUpEventParameters, - type SpendVirtualCurrencyEventParameters, - type UnlockAchievementEventParameters, - type ViewCartEventParameters, - type ViewItemEventParameters, - type ViewItemListEventParameters, - type ViewPromotionEventParameters, - type ViewSearchResultsParameters, - type ScreenViewParameters, - type EventParams, - type GtagConfigParams, - type EventNameString, - type CustomEventName, - // Modular API +import { getApp } from '@react-native-firebase/app'; +import { getAnalytics, initializeAnalytics, getGoogleAnalyticsClientId, logEvent, + logTransaction, setAnalyticsCollectionEnabled, setSessionTimeoutDuration, getAppInstanceId, @@ -102,706 +56,111 @@ import analytics, { isSupported, setConsent, settings, + SDK_VERSION, + type Analytics, + type AnalyticsCallOptions, + type ConsentSettings, + type AnalyticsSettings, + type SettingsOptions, + type AddPaymentInfoEventParameters, + type ScreenViewParameters, } from '.'; -console.log(analytics().app); - -// checks module exists at root -console.log(firebase.analytics().app.name); - -// checks module exists at app level -console.log(firebase.app().analytics().app.name); - -// Note: The 'app' property should exist on AnalyticsModule interface -// If TypeScript errors occur, it may be a type inference issue with createModuleNamespace - -// checks statics exist -console.log(firebase.analytics.SDK_VERSION); - -// checks statics exist on defaultExport -console.log(analytics.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// test method calls with proper types -firebase - .analytics() - .logAddPaymentInfo({ value: 123, currency: 'USD' }) - .then(() => { - console.log('logAddPaymentInfo completed'); - }); - -firebase - .analytics() - .logAddToCart({ value: 123, currency: 'USD' }) - .then(() => { - console.log('logAddToCart completed'); - }); - -firebase - .analytics() - .logLogin({ method: 'foo' }) - .then(() => { - console.log('logLogin completed'); - }); - -firebase - .analytics() - .setUserProperties({ foo: 'bar' }) - .then(() => { - console.log('setUserProperties completed'); - }); - -firebase.analytics().setConsent({ ad_storage: true }); +const analytics = getAnalytics(); +console.log(analytics.app.name); -// test type usage -const analyticsInstance: Analytics = firebase.analytics(); -console.log(analyticsInstance.app.name); -const callOptions: AnalyticsCallOptions = { global: true }; -console.log(callOptions.global); -const consentSettings: ConsentSettings = { ad_storage: true, analytics_storage: false }; -const analyticsSettings: AnalyticsSettings = {}; -console.log(analyticsSettings); -const settingsOptions: SettingsOptions = {}; -console.log(settingsOptions); -const currency: Currency = 123; -console.log(currency); -const consentStatus: ConsentStatusString = 'granted'; -console.log(consentStatus); -const promotion: Promotion = { id: 'promo1', name: 'Promotion' }; -console.log(promotion.id); -const item: Item = { item_id: 'item1', item_name: 'Item' }; -console.log(item.item_id); -const addPaymentInfoParams: AddPaymentInfoEventParameters = { value: 123, currency: 'USD' }; -const addShippingInfoParams: AddShippingInfoParameters = { value: 123, currency: 'USD' }; -const addToCartParams: AddToCartEventParameters = { value: 123, currency: 'USD' }; -const addToWishlistParams: AddToWishlistEventParameters = { value: 123, currency: 'USD' }; -const beginCheckoutParams: BeginCheckoutEventParameters = { value: 123, currency: 'USD' }; -const campaignDetailsParams: CampaignDetailsEventParameters = { - source: 'source', - medium: 'medium', - campaign: 'campaign', -}; -const earnVirtualCurrencyParams: EarnVirtualCurrencyEventParameters = { - virtual_currency_name: 'coins', - value: 100, -}; -const generateLeadParams: GenerateLeadEventParameters = { value: 123, currency: 'USD' }; -const joinGroupParams: JoinGroupEventParameters = { group_id: 'group1' }; -const levelEndParams: LevelEndEventParameters = { level: 1, success: true }; -const levelStartParams: LevelStartEventParameters = { level: 1 }; -const levelUpParams: LevelUpEventParameters = { level: 5, character: 'character1' }; -const loginParams: LoginEventParameters = { method: 'email' }; -const postScoreParams: PostScoreEventParameters = { score: 100, level: 5 }; -const selectContentParams: SelectContentEventParameters = { - content_type: 'type', - item_id: 'item1', -}; -const purchaseParams: PurchaseEventParameters = { - value: 123, - currency: 'USD', - transaction_id: 'tx1', -}; -const refundParams: RefundEventParameters = { value: 123, currency: 'USD', transaction_id: 'tx1' }; -const removeFromCartParams: RemoveFromCartEventParameters = { value: 123, currency: 'USD' }; -const searchParams: SearchEventParameters = { search_term: 'term' }; -const selectItemParams: SelectItemEventParameters = { - item_list_id: 'list1', - item_list_name: 'List', - content_type: 'type', -}; -const setCheckoutOptionParams: SetCheckoutOptionEventParameters = { - checkout_step: 1, - checkout_option: 'option', -}; -const selectPromotionParams: SelectPromotionEventParameters = { - promotion_id: 'promo1', - promotion_name: 'Promotion', - creative_name: 'Creative', - creative_slot: 'Slot', - location_id: 'location1', -}; -const shareParams: ShareEventParameters = { - method: 'email', - content_type: 'type', - item_id: 'item1', -}; -const signUpParams: SignUpEventParameters = { method: 'email' }; -const spendVirtualCurrencyParams: SpendVirtualCurrencyEventParameters = { - virtual_currency_name: 'coins', - value: 50, - item_name: 'item', -}; -const unlockAchievementParams: UnlockAchievementEventParameters = { achievement_id: 'ach1' }; -const viewCartParams: ViewCartEventParameters = { value: 123, currency: 'USD' }; -const viewItemParams: ViewItemEventParameters = { value: 123, currency: 'USD' }; -const viewItemListParams: ViewItemListEventParameters = { - item_list_id: 'list1', - item_list_name: 'List', -}; -const viewPromotionParams: ViewPromotionEventParameters = { promotion_id: 'promo1' }; -const viewSearchResultsParams: ViewSearchResultsParameters = { search_term: 'term' }; -const screenViewParams: ScreenViewParameters = { screen_name: 'screen', screen_class: 'Screen' }; -const eventParams: EventParams = { key: 'value' }; -console.log(eventParams.key); -const gtagConfigParams: GtagConfigParams = {}; -console.log(gtagConfigParams); -const eventNameString: EventNameString = 'add_payment_info'; -console.log(eventNameString); -const customEventName: CustomEventName<'my_custom_event'> = 'my_custom_event'; -console.log(customEventName); +const analyticsWithApp = getAnalytics(getApp()); +console.log(analyticsWithApp.app.name); -// checks all methods exist on firebase.analytics() -console.log(firebase.analytics().logAddPaymentInfo); -console.log(firebase.analytics().logAddToCart); -console.log(firebase.analytics().logAddShippingInfo); -console.log(firebase.analytics().logAddToWishlist); -console.log(firebase.analytics().logAppOpen); -console.log(firebase.analytics().logBeginCheckout); -console.log(firebase.analytics().logCampaignDetails); -console.log(firebase.analytics().logEarnVirtualCurrency); -console.log(firebase.analytics().logEvent); -console.log(firebase.analytics().logGenerateLead); -console.log(firebase.analytics().logJoinGroup); -console.log(firebase.analytics().logLevelEnd); -console.log(firebase.analytics().logLevelStart); -console.log(firebase.analytics().logLevelUp); -console.log(firebase.analytics().logLogin); -console.log(firebase.analytics().logPostScore); -console.log(firebase.analytics().logPurchase); -console.log(firebase.analytics().logRemoveFromCart); -console.log(firebase.analytics().logRefund); -console.log(firebase.analytics().logSearch); -console.log(firebase.analytics().logSelectContent); -console.log(firebase.analytics().logSetCheckoutOption); -console.log(firebase.analytics().logShare); -console.log(firebase.analytics().logSignUp); -console.log(firebase.analytics().logSpendVirtualCurrency); -console.log(firebase.analytics().logTutorialBegin); -console.log(firebase.analytics().logTutorialComplete); -console.log(firebase.analytics().logUnlockAchievement); -console.log(firebase.analytics().logViewItem); -console.log(firebase.analytics().logViewItemList); -console.log(firebase.analytics().resetAnalyticsData); -console.log(firebase.analytics().logViewCart); -console.log(firebase.analytics().setAnalyticsCollectionEnabled); -console.log(firebase.analytics().logSelectPromotion); -console.log(firebase.analytics().logScreenView); -console.log(firebase.analytics().logViewPromotion); -console.log(firebase.analytics().setSessionTimeoutDuration); -console.log(firebase.analytics().setUserId); -console.log(firebase.analytics().setUserProperties); -console.log(firebase.analytics().logViewSearchResults); -console.log(firebase.analytics().setUserProperty); -console.log(firebase.analytics().setConsent); - -// checks all methods exist on default export -console.log(analytics().logAddPaymentInfo); -console.log(analytics().logAddToCart); -console.log(analytics().logAddShippingInfo); -console.log(analytics().logAddToWishlist); -console.log(analytics().logAppOpen); -console.log(analytics().logBeginCheckout); -console.log(analytics().logCampaignDetails); -console.log(analytics().logEarnVirtualCurrency); -console.log(analytics().logEvent); -console.log(analytics().logGenerateLead); -console.log(analytics().logJoinGroup); -console.log(analytics().logLevelEnd); -console.log(analytics().logLevelStart); -console.log(analytics().logLevelUp); -console.log(analytics().logLogin); -console.log(analytics().logPostScore); -console.log(analytics().logPurchase); -console.log(analytics().logRemoveFromCart); -console.log(analytics().logRefund); -console.log(analytics().logSearch); -console.log(analytics().logSelectContent); -console.log(analytics().logSetCheckoutOption); -console.log(analytics().logShare); -console.log(analytics().logSignUp); -console.log(analytics().logSpendVirtualCurrency); -console.log(analytics().logTutorialBegin); -console.log(analytics().logTutorialComplete); -console.log(analytics().logUnlockAchievement); -console.log(analytics().logViewItem); -console.log(analytics().logViewItemList); -console.log(analytics().resetAnalyticsData); -console.log(analytics().logViewCart); -console.log(analytics().setAnalyticsCollectionEnabled); -console.log(analytics().logSelectPromotion); -console.log(analytics().logScreenView); -console.log(analytics().logViewPromotion); -console.log(analytics().setSessionTimeoutDuration); -console.log(analytics().setUserId); -console.log(analytics().setUserProperties); -console.log(analytics().logViewSearchResults); -console.log(analytics().setUserProperty); -console.log(analytics().setConsent); - -// checks missing methods exist on firebase.analytics() -console.log(firebase.analytics().getAppInstanceId); -console.log(firebase.analytics().getSessionId); -console.log(firebase.analytics().setDefaultEventParameters); -console.log(firebase.analytics().logSelectItem); -console.log(firebase.analytics().initiateOnDeviceConversionMeasurementWithEmailAddress); -console.log(firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress); -console.log(firebase.analytics().initiateOnDeviceConversionMeasurementWithPhoneNumber); -console.log(firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber); - -// checks missing methods exist on default export -console.log(analytics().getAppInstanceId); -console.log(analytics().getSessionId); -console.log(analytics().setDefaultEventParameters); -console.log(analytics().logSelectItem); -console.log(analytics().initiateOnDeviceConversionMeasurementWithEmailAddress); -console.log(analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress); -console.log(analytics().initiateOnDeviceConversionMeasurementWithPhoneNumber); -console.log(analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber); - -// test method calls with missing methods -firebase - .analytics() - .getAppInstanceId() - .then((id: string | null) => { - console.log('getAppInstanceId:', id); - }); - -firebase - .analytics() - .getSessionId() - .then((id: number | null) => { - console.log('getSessionId:', id); - }); - -firebase - .analytics() - .setDefaultEventParameters({ key: 'value' }) - .then(() => { - console.log('setDefaultEventParameters completed'); - }); - -firebase - .analytics() - .logSelectItem({ item_list_id: 'list1', item_list_name: 'List', content_type: 'type' }) - .then(() => { - console.log('logSelectItem completed'); - }); - -firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithEmailAddress('test@example.com') - .then(() => { - console.log('initiateOnDeviceConversionMeasurementWithEmailAddress completed'); - }); - -firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithHashedEmailAddress('hashed') - .then(() => { - console.log('initiateOnDeviceConversionMeasurementWithHashedEmailAddress completed'); - }); - -firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithPhoneNumber('+1234567890') - .then(() => { - console.log('initiateOnDeviceConversionMeasurementWithPhoneNumber completed'); - }); - -firebase - .analytics() - .initiateOnDeviceConversionMeasurementWithHashedPhoneNumber('hashed') - .then(() => { - console.log('initiateOnDeviceConversionMeasurementWithHashedPhoneNumber completed'); - }); - -// test modular API functions -const analyticsModular = getAnalytics(); -const analyticsModularWithApp = getAnalytics(firebase.app()); -console.log(analyticsModularWithApp.app.name); - -const initializedAnalytics = initializeAnalytics(firebase.app(), {}); +const initializedAnalytics = initializeAnalytics(getApp(), {}); console.log(initializedAnalytics.app.name); -getGoogleAnalyticsClientId(analyticsModular).then((id: string) => { - console.log('getGoogleAnalyticsClientId:', id); -}); - -logEvent(analyticsModular, 'event_name', { key: 'value' }, { global: false }).then(() => { - console.log('logEvent completed'); -}); - -setAnalyticsCollectionEnabled(analyticsModular, true).then(() => { - console.log('setAnalyticsCollectionEnabled completed'); -}); - -setSessionTimeoutDuration(analyticsModular, 1800000).then(() => { - console.log('setSessionTimeoutDuration completed'); -}); - -getAppInstanceId(analyticsModular).then((id: string | null) => { - console.log('getAppInstanceId modular:', id); -}); - -getSessionId(analyticsModular).then((id: number | null) => { - console.log('getSessionId modular:', id); -}); - -setUserId(analyticsModular, 'user123').then(() => { - console.log('setUserId completed'); -}); - -setUserProperty(analyticsModular, 'property', 'value').then(() => { - console.log('setUserProperty completed'); -}); - -setUserProperties(analyticsModular, { prop1: 'value1', prop2: 'value2' }, { global: false }).then( - () => { - console.log('setUserProperties completed'); - }, -); - -resetAnalyticsData(analyticsModular).then(() => { - console.log('resetAnalyticsData completed'); -}); - -logAddPaymentInfo(analyticsModular, addPaymentInfoParams).then(() => { - console.log('logAddPaymentInfo modular completed'); -}); - -logScreenView(analyticsModular, screenViewParams).then(() => { - console.log('logScreenView modular completed'); -}); - -logAddShippingInfo(analyticsModular, addShippingInfoParams).then(() => { - console.log('logAddShippingInfo modular completed'); -}); - -logAddToCart(analyticsModular, addToCartParams).then(() => { - console.log('logAddToCart modular completed'); -}); - -logAddToWishlist(analyticsModular, addToWishlistParams).then(() => { - console.log('logAddToWishlist modular completed'); -}); - -logAppOpen(analyticsModular).then(() => { - console.log('logAppOpen modular completed'); -}); - -logBeginCheckout(analyticsModular, beginCheckoutParams).then(() => { - console.log('logBeginCheckout modular completed'); -}); - -logCampaignDetails(analyticsModular, campaignDetailsParams).then(() => { - console.log('logCampaignDetails modular completed'); -}); - -logEarnVirtualCurrency(analyticsModular, earnVirtualCurrencyParams).then(() => { - console.log('logEarnVirtualCurrency modular completed'); -}); - -logGenerateLead(analyticsModular, generateLeadParams).then(() => { - console.log('logGenerateLead modular completed'); -}); - -logJoinGroup(analyticsModular, joinGroupParams).then(() => { - console.log('logJoinGroup modular completed'); -}); - -logLevelEnd(analyticsModular, levelEndParams).then(() => { - console.log('logLevelEnd modular completed'); -}); - -logLevelStart(analyticsModular, levelStartParams).then(() => { - console.log('logLevelStart modular completed'); -}); - -logLevelUp(analyticsModular, levelUpParams).then(() => { - console.log('logLevelUp modular completed'); -}); - -logLogin(analyticsModular, loginParams).then(() => { - console.log('logLogin modular completed'); -}); - -logPostScore(analyticsModular, postScoreParams).then(() => { - console.log('logPostScore modular completed'); -}); - -logSelectContent(analyticsModular, selectContentParams).then(() => { - console.log('logSelectContent modular completed'); -}); - -logPurchase(analyticsModular, purchaseParams).then(() => { - console.log('logPurchase modular completed'); -}); - -logRefund(analyticsModular, refundParams).then(() => { - console.log('logRefund modular completed'); -}); - -logRemoveFromCart(analyticsModular, removeFromCartParams).then(() => { - console.log('logRemoveFromCart modular completed'); -}); - -logSearch(analyticsModular, searchParams).then(() => { - console.log('logSearch modular completed'); -}); - -logSelectItem(analyticsModular, selectItemParams).then(() => { - console.log('logSelectItem modular completed'); -}); - -logSetCheckoutOption(analyticsModular, setCheckoutOptionParams).then(() => { - console.log('logSetCheckoutOption modular completed'); -}); - -logSelectPromotion(analyticsModular, selectPromotionParams).then(() => { - console.log('logSelectPromotion modular completed'); -}); - -logShare(analyticsModular, shareParams).then(() => { - console.log('logShare modular completed'); -}); - -logSignUp(analyticsModular, signUpParams).then(() => { - console.log('logSignUp modular completed'); -}); - -logSpendVirtualCurrency(analyticsModular, spendVirtualCurrencyParams).then(() => { - console.log('logSpendVirtualCurrency modular completed'); -}); - -logTutorialBegin(analyticsModular).then(() => { - console.log('logTutorialBegin modular completed'); -}); - -logTutorialComplete(analyticsModular).then(() => { - console.log('logTutorialComplete modular completed'); -}); - -logUnlockAchievement(analyticsModular, unlockAchievementParams).then(() => { - console.log('logUnlockAchievement modular completed'); -}); - -logViewCart(analyticsModular, viewCartParams).then(() => { - console.log('logViewCart modular completed'); -}); - -logViewItem(analyticsModular, viewItemParams).then(() => { - console.log('logViewItem modular completed'); -}); - -logViewItemList(analyticsModular, viewItemListParams).then(() => { - console.log('logViewItemList modular completed'); -}); - -logViewPromotion(analyticsModular, viewPromotionParams).then(() => { - console.log('logViewPromotion modular completed'); -}); - -logViewSearchResults(analyticsModular, viewSearchResultsParams).then(() => { - console.log('logViewSearchResults modular completed'); -}); - -setDefaultEventParameters(analyticsModular, { key: 'value' }).then(() => { - console.log('setDefaultEventParameters modular completed'); -}); - -initiateOnDeviceConversionMeasurementWithEmailAddress(analyticsModular, 'test@example.com').then( - () => { - console.log('initiateOnDeviceConversionMeasurementWithEmailAddress modular completed'); - }, +const typedAnalytics: Analytics = analytics; +const typedCallOptions: AnalyticsCallOptions = { global: false }; +const typedConsent: ConsentSettings = { ad_storage: true }; +const typedSettings: AnalyticsSettings = {}; +const typedSettingsOptions: SettingsOptions = {}; +const typedAddPaymentInfo: AddPaymentInfoEventParameters = { currency: 'USD', value: 1 }; +const typedScreenView: ScreenViewParameters = { screen_name: 'Home' }; + +console.log(typedCallOptions); +console.log(typedConsent); +console.log(typedSettings); +console.log(typedSettingsOptions); +console.log(typedAddPaymentInfo); +console.log(typedScreenView); +console.log(SDK_VERSION); + +logEvent(analytics, 'invertase_event'); +logEvent(analytics, 'screen_view', typedScreenView); +setAnalyticsCollectionEnabled(analytics, true); +setSessionTimeoutDuration(analytics, 1800000); +getAppInstanceId(analytics).then(id => console.log(id)); +getSessionId(analytics).then(id => console.log(id)); +setUserId(analytics, 'user'); +setUserProperty(analytics, 'prop', 'value'); +setUserProperties(analytics, { prop: 'value' }); +resetAnalyticsData(analytics); +setDefaultEventParameters(analytics, { foo: 'bar' }); +setConsent(analytics, typedConsent); +settings(typedSettingsOptions); + +logAddPaymentInfo(analytics, typedAddPaymentInfo); +logScreenView(analytics, typedScreenView); +logAddShippingInfo(analytics, { currency: 'USD', value: 1 }); +logAddToCart(analytics, { currency: 'USD', value: 1 }); +logAddToWishlist(analytics, { currency: 'USD', value: 1 }); +logAppOpen(analytics); +logBeginCheckout(analytics, { currency: 'USD', value: 1 }); +logCampaignDetails(analytics, { source: 'src', medium: 'med', campaign: 'camp' }); +logEarnVirtualCurrency(analytics, { virtual_currency_name: 'coins', value: 1 }); +logGenerateLead(analytics, { currency: 'USD', value: 1 }); +logJoinGroup(analytics, { group_id: 'group' }); +logLevelEnd(analytics, { level: 1, success: true }); +logLevelStart(analytics, { level: 1 }); +logLevelUp(analytics, { level: 1, character: 'hero' }); +logLogin(analytics, { method: 'email' }); +logPostScore(analytics, { score: 100, level: 1, character: 'hero' }); +logSelectContent(analytics, { content_type: 'type', item_id: 'id' }); +logPurchase(analytics, { currency: 'USD', value: 1 }); +logRefund(analytics, { currency: 'USD', value: 1 }); +logRemoveFromCart(analytics, { currency: 'USD', value: 1 }); +logSearch(analytics, { search_term: 'term' }); +logSelectItem(analytics, { content_type: 'type', item_list_id: 'list', item_list_name: 'name' }); +logSetCheckoutOption(analytics, { checkout_step: 1, checkout_option: 'option' }); +logSelectPromotion(analytics, { + creative_name: 'creative', + creative_slot: 'slot', + location_id: 'loc', + promotion_id: 'promo', + promotion_name: 'Promo', +}); +logShare(analytics, { content_type: 'type', item_id: 'id', method: 'method' }); +logSignUp(analytics, { method: 'email' }); +logSpendVirtualCurrency(analytics, { virtual_currency_name: 'coins', value: 1, item_name: 'item' }); +logTutorialBegin(analytics); +logTutorialComplete(analytics); +logUnlockAchievement(analytics, { achievement_id: 'ach' }); +logViewCart(analytics, { currency: 'USD', value: 1 }); +logViewItem(analytics, { currency: 'USD', value: 1 }); +logViewItemList(analytics, { items: [] }); +logViewPromotion(analytics, { items: [] }); +logViewSearchResults(analytics, { search_term: 'term' }); + +initiateOnDeviceConversionMeasurementWithEmailAddress(analytics, 'user@example.com'); +initiateOnDeviceConversionMeasurementWithHashedEmailAddress( + analytics, + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ); - -initiateOnDeviceConversionMeasurementWithHashedEmailAddress(analyticsModular, 'hashed').then(() => { - console.log('initiateOnDeviceConversionMeasurementWithHashedEmailAddress modular completed'); -}); - -initiateOnDeviceConversionMeasurementWithPhoneNumber(analyticsModular, '+1234567890').then(() => { - console.log('initiateOnDeviceConversionMeasurementWithPhoneNumber modular completed'); -}); - -initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(analyticsModular, 'hashed').then(() => { - console.log('initiateOnDeviceConversionMeasurementWithHashedPhoneNumber modular completed'); -}); - -isSupported().then((supported: boolean) => { - console.log('isSupported:', supported); -}); - -setConsent(analyticsModular, consentSettings).then(() => { - console.log('setConsent modular completed'); -}); - -settings(settingsOptions); - -// Test logEvent with overloads for standard events -logEvent(analyticsModular, 'add_payment_info', { - currency: 'USD', - value: 100, - payment_type: 'credit', -}).then(() => { - console.log('logEvent add_payment_info completed'); -}); - -logEvent(analyticsModular, 'add_shipping_info', { - currency: 'USD', - value: 50, - shipping_tier: 'ground', -}).then(() => { - console.log('logEvent add_shipping_info completed'); -}); - -logEvent(analyticsModular, 'add_to_cart', { currency: 'USD', value: 25 }).then(() => { - console.log('logEvent add_to_cart completed'); -}); - -logEvent(analyticsModular, 'add_to_wishlist', { currency: 'USD', value: 30 }).then(() => { - console.log('logEvent add_to_wishlist completed'); -}); - -logEvent(analyticsModular, 'begin_checkout', { - currency: 'USD', - value: 100, - coupon: 'SAVE10', -}).then(() => { - console.log('logEvent begin_checkout completed'); -}); - -logEvent(analyticsModular, 'checkout_progress', { - checkout_step: 2, - checkout_option: 'express', -}).then(() => { - console.log('logEvent checkout_progress completed'); -}); - -logEvent(analyticsModular, 'exception', { description: 'Error occurred', fatal: false }).then( - () => { - console.log('logEvent exception completed'); - }, -); - -logEvent(analyticsModular, 'generate_lead', { currency: 'USD', value: 200 }).then(() => { - console.log('logEvent generate_lead completed'); -}); - -logEvent(analyticsModular, 'login', { method: 'email' }).then(() => { - console.log('logEvent login completed'); -}); - -logEvent(analyticsModular, 'page_view', { page_title: 'Home', page_location: '/home' }).then(() => { - console.log('logEvent page_view completed'); -}); - -logEvent(analyticsModular, 'purchase', { - currency: 'USD', - value: 150, - transaction_id: 'tx123', -}).then(() => { - console.log('logEvent purchase completed'); -}); - -logEvent(analyticsModular, 'refund', { currency: 'USD', value: 50, transaction_id: 'tx123' }).then( - () => { - console.log('logEvent refund completed'); - }, -); - -logEvent(analyticsModular, 'remove_from_cart', { currency: 'USD', value: 20 }).then(() => { - console.log('logEvent remove_from_cart completed'); -}); - -logEvent(analyticsModular, 'screen_view', { - screen_name: 'HomeScreen', - screen_class: 'Screen', -}).then(() => { - console.log('logEvent screen_view completed'); -}); - -logEvent(analyticsModular, 'search', { search_term: 'shoes' }).then(() => { - console.log('logEvent search completed'); -}); - -logEvent(analyticsModular, 'select_content', { content_type: 'product', item_id: 'item123' }).then( - () => { - console.log('logEvent select_content completed'); - }, +initiateOnDeviceConversionMeasurementWithPhoneNumber(analytics, '+15551234567'); +initiateOnDeviceConversionMeasurementWithHashedPhoneNumber( + analytics, + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', ); -logEvent(analyticsModular, 'select_item', { - content_type: 'product', - item_list_id: 'list1', - item_list_name: 'Featured', -}).then(() => { - console.log('logEvent select_item completed'); -}); - -logEvent(analyticsModular, 'select_promotion', { - creative_name: 'Summer Sale', - creative_slot: 'banner', - location_id: 'loc1', - promotion_id: 'promo1', - promotion_name: 'Summer Sale', -}).then(() => { - console.log('logEvent select_promotion completed'); -}); - -logEvent(analyticsModular, 'set_checkout_option', { - checkout_step: 1, - checkout_option: 'standard', -}).then(() => { - console.log('logEvent set_checkout_option completed'); -}); - -logEvent(analyticsModular, 'share', { - content_type: 'article', - item_id: 'article123', - method: 'twitter', -}).then(() => { - console.log('logEvent share completed'); -}); - -logEvent(analyticsModular, 'sign_up', { method: 'email' }).then(() => { - console.log('logEvent sign_up completed'); -}); - -logEvent(analyticsModular, 'timing_complete', { duration: 5000 }).then(() => { - console.log('logEvent timing_complete completed'); -}); - -logEvent(analyticsModular, 'view_cart', { currency: 'USD', value: 75 }).then(() => { - console.log('logEvent view_cart completed'); -}); - -logEvent(analyticsModular, 'view_item', { currency: 'USD', value: 40 }).then(() => { - console.log('logEvent view_item completed'); -}); - -logEvent(analyticsModular, 'view_item_list', { - item_list_id: 'list1', - item_list_name: 'Featured Products', -}).then(() => { - console.log('logEvent view_item_list completed'); -}); - -logEvent(analyticsModular, 'view_promotion', { - promotion_id: 'promo1', - promotion_name: 'Summer Sale', -}).then(() => { - console.log('logEvent view_promotion completed'); -}); +isSupported().then(supported => console.log(supported)); +getGoogleAnalyticsClientId(analytics).then(clientId => console.log(clientId)); +logTransaction(analytics, 'transaction-id'); -logEvent(analyticsModular, 'view_search_results', { search_term: 'shoes' }).then(() => { - console.log('logEvent view_search_results completed'); -}); - -// Test custom event name (should use generic overload) -logEvent(analyticsModular, 'custom_event_name' as CustomEventName<'custom_event_name'>, { - key: 'value', -}).then(() => { - console.log('logEvent custom event completed'); -}); +console.log(typedAnalytics.logEvent); +console.log(typedAnalytics.setAnalyticsCollectionEnabled); diff --git a/packages/analytics/typedoc.json b/packages/analytics/typedoc.json index 206c92519a..4ec3b90280 100644 --- a/packages/analytics/typedoc.json +++ b/packages/analytics/typedoc.json @@ -1,48 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/analytics.ts"], - "tsconfig": "tsconfig.json", - "intentionallyNotExported": [ - "_AddPaymentInfoEventParameters", - "_AddShippingInfoEventParameters", - "_AddShippingInfoParameters", - "_AddToCartEventParameters", - "_AddToWishlistEventParameters", - "_AnalyticsCallOptions", - "_AnalyticsSettings", - "_BeginCheckoutEventParameters", - "_CampaignDetailsEventParameters", - "_ConsentSettings", - "_EarnVirtualCurrencyEventParameters", - "_EventParams", - "_GenerateLeadEventParameters", - "_GtagConfigParams", - "_Item", - "_JoinGroupEventParameters", - "_LevelEndEventParameters", - "_LevelStartEventParameters", - "_LevelUpEventParameters", - "_LoginEventParameters", - "_PostScoreEventParameters", - "_PurchaseEventParameters", - "_RefundEventParameters", - "_RemoveFromCartEventParameters", - "_ScreenViewParameters", - "_SearchEventParameters", - "_SelectContentEventParameters", - "_SelectItemEventParameters", - "_SelectPromotionEventParameters", - "_SetCheckoutOptionEventParameters", - "_SettingsOptions", - "_ShareEventParameters", - "_SignUpEventParameters", - "_SpendVirtualCurrencyEventParameters", - "_Statics", - "_UnlockAchievementEventParameters", - "_ViewCartEventParameters", - "_ViewItemEventParameters", - "_ViewItemListEventParameters", - "_ViewPromotionEventParameters", - "_ViewSearchResultsParameters" - ] + "entryPoints": ["lib/index.ts", "lib/types/analytics.ts"], + "tsconfig": "tsconfig.json" } diff --git a/packages/app-check/__tests__/appcheck.test.ts b/packages/app-check/__tests__/appcheck.test.ts index c70bfb3bc2..85136abd4c 100644 --- a/packages/app-check/__tests__/appcheck.test.ts +++ b/packages/app-check/__tests__/appcheck.test.ts @@ -1,71 +1,31 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; +import { describe, expect, it } from '@jest/globals'; import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; - -// @ts-ignore -import { MODULAR_DEPRECATION_ARG } from '../../app/lib/common'; - -import { getApp } from '../../app/lib'; - -import { - firebase, initializeAppCheck, getToken, getLimitedUseToken, setTokenAutoRefreshEnabled, onTokenChanged, CustomProvider, - ReactNativeFirebaseAppCheckProviderOptions, - ReactNativeFirebaseAppCheckProviderAndroidOptions, - ReactNativeFirebaseAppCheckProviderAppleOptions, - ReactNativeFirebaseAppCheckProviderWebOptions, + ReactNativeFirebaseAppCheckProvider, + type ReactNativeFirebaseAppCheckProviderOptions, + type ReactNativeFirebaseAppCheckProviderAndroidOptions, + type ReactNativeFirebaseAppCheckProviderAppleOptions, + type ReactNativeFirebaseAppCheckProviderWebOptions, } from '../lib'; describe('appCheck()', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.appCheck).toBeDefined(); - expect(app.appCheck().app).toEqual(app); - }); - - it('supports multiple apps', async function () { - expect(firebase.appCheck().app.name).toEqual('[DEFAULT]'); - expect(firebase.appCheck(firebase.app('secondaryFromNative')).app.name).toEqual( - 'secondaryFromNative', - ); - expect(firebase.app('secondaryFromNative').appCheck().app.name).toEqual( - 'secondaryFromNative', - ); - }); - - describe('react-native-firebase provider', function () { - it('correctly creates a provider instance', function () { - expect(firebase.appCheck().newReactNativeFirebaseAppCheckProvider()).toBeDefined(); - }); - }); - }); - describe('modular', function () { it('`initializeAppCheck` function is properly exposed to end user', function () { expect(initializeAppCheck).toBeDefined(); }); + it('`initializeAppCheck` rejects missing options at runtime', async function () { + await expect(initializeAppCheck(undefined, undefined)).rejects.toThrow( + 'Invalid configuration: no options defined.', + ); + }); + it('`getToken` function is properly exposed to end user', function () { expect(getToken).toBeDefined(); }); @@ -78,13 +38,16 @@ describe('appCheck()', function () { expect(setTokenAutoRefreshEnabled).toBeDefined(); }); + it('`onTokenChanged` function is properly exposed to end user', function () { + expect(onTokenChanged).toBeDefined(); + }); + it('`CustomProvider` function is properly exposed to end user', function () { expect(CustomProvider).toBeDefined(); }); - it('`ReactNativeAppCheckProvider objects are properly exposed to end user', function () { - expect(firebase.appCheck().newReactNativeFirebaseAppCheckProvider).toBeDefined(); - const provider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider(); + it('ReactNativeAppCheckProvider objects are properly exposed to end user', function () { + const provider = new ReactNativeFirebaseAppCheckProvider(); expect(provider.configure).toBeDefined(); const options = { debugToken: 'foo' } as ReactNativeFirebaseAppCheckProviderOptions; const appleOptions = { @@ -102,119 +65,7 @@ describe('appCheck()', function () { ...options, } as ReactNativeFirebaseAppCheckProviderWebOptions; expect(webOptions).toBeDefined(); - }); - }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let appCheckRefV9Deprecation: CheckV9DeprecationFunction; - let staticsRefV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - appCheckRefV9Deprecation = createCheckV9Deprecation(['appCheck']); - staticsRefV9Deprecation = createCheckV9Deprecation(['appCheck', 'statics']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - source: 'cache', - changes: [], - documents: [], - metadata: {}, - path: 'foo', - } as never), - }, - ); - }); - }); - - describe('AppCheck', function () { - it('appCheck.activate()', function () { - // @ts-ignore - const appCheck = firebase.appCheck.call(null, getApp(), MODULAR_DEPRECATION_ARG); - appCheckRefV9Deprecation( - () => - initializeAppCheck(getApp(), { - provider: { - providerOptions: { - android: { - provider: 'playIntegrity', - }, - }, - }, - isTokenAutoRefreshEnabled: true, - }), - () => appCheck.activate('string'), - 'activate', - ); - }); - - it('appCheck.setTokenAutoRefreshEnabled()', function () { - // @ts-ignore - const appCheck = firebase.appCheck.call(null, getApp(), MODULAR_DEPRECATION_ARG); - appCheckRefV9Deprecation( - () => setTokenAutoRefreshEnabled(appCheck, true), - () => appCheck.setTokenAutoRefreshEnabled(true), - 'setTokenAutoRefreshEnabled', - ); - }); - - it('appCheck.getToken()', function () { - // @ts-ignore - const appCheck = firebase.appCheck.call(null, getApp(), MODULAR_DEPRECATION_ARG); - appCheckRefV9Deprecation( - () => getToken(appCheck, true), - () => appCheck.getToken(true), - 'getToken', - ); - }); - - it('appCheck.getLimitedUseToken()', function () { - // @ts-ignore - const appCheck = firebase.appCheck.call(null, getApp(), MODULAR_DEPRECATION_ARG); - appCheckRefV9Deprecation( - () => getLimitedUseToken(appCheck), - () => appCheck.getLimitedUseToken(), - 'getLimitedUseToken', - ); - }); - - it('appCheck.onTokenChanged()', function () { - // @ts-ignore - const appCheck = firebase.appCheck.call(null, getApp(), MODULAR_DEPRECATION_ARG); - appCheckRefV9Deprecation( - () => - onTokenChanged( - appCheck, - () => {}, - () => {}, - () => {}, - ), - () => - appCheck.onTokenChanged( - () => {}, - () => {}, - () => {}, - ), - 'onTokenChanged', - ); - }); - - it('CustomProvider', function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - const appCheck = firebase.appCheck; - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - staticsRefV9Deprecation( - () => CustomProvider, - () => appCheck.CustomProvider, - 'CustomProvider', - ); - }); + expect(ReactNativeFirebaseAppCheckProvider).toBeDefined(); }); }); }); diff --git a/packages/app-check/e2e/appcheck.e2e.js b/packages/app-check/e2e/appcheck.e2e.js index ce6816b8e7..0da24a314b 100644 --- a/packages/app-check/e2e/appcheck.e2e.js +++ b/packages/app-check/e2e/appcheck.e2e.js @@ -41,6 +41,18 @@ function getRandomToken() { return tokenUUIDs[0]; } +function isAppCheckCloudQuotaError(error) { + const message = error?.message || String(error); + return /Quota exceeded|Too many server requests|RESOURCE_EXHAUSTED/i.test(message); +} + +function skipIfAppCheckCloudQuotaError(error, testContext) { + if (isAppCheckCloudQuotaError(error)) { + testContext.skip(); + } + throw error; +} + function decodeJWT(token) { // Split the token into its parts const parts = token.split('.'); @@ -82,337 +94,12 @@ function decodeJWT(token) { } describe('appCheck()', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('CustomProvider', function () { - if (!Platform.other) { - return; - } - - it('should throw an error if no provider options are defined', function () { - try { - new firebase.appCheck.CustomProvider(); - return Promise.reject(new Error('Did not throw an error.')); - } catch (e) { - e.message.should.containEql('no provider options defined'); - return Promise.resolve(); - } - }); - - it('should throw an error if no getToken function is defined', function () { - try { - new firebase.appCheck.CustomProvider({}); - return Promise.reject(new Error('Did not throw an error.')); - } catch (e) { - e.message.should.containEql('no getToken function defined'); - return Promise.resolve(); - } - }); - - it('should return a token from a custom provider', async function () { - const spy = sinon.spy(); - const provider = new firebase.appCheck.CustomProvider({ - getToken() { - spy(); - return FirebaseHelpers.fetchAppCheckToken(); - }, - }); - - // Call from the provider directly. - const { token, expireTimeMillis } = await provider.getToken(); - spy.should.be.calledOnce(); - token.should.be.a.String(); - expireTimeMillis.should.be.a.Number(); - - // Call from the app check instance. - await firebase - .appCheck() - .initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false }); - const { token: tokenFromAppCheck } = await firebase.appCheck().getToken(true); - tokenFromAppCheck.should.be.a.String(); - - // Confirm that app check used the custom provider getToken function. - spy.should.be.calledTwice(); - }); - - // TODO flakey after many runs, sometimes fails on android & ios, - // possibly a rate limiting issue on the server side - it('should return a limited use token from a custom provider', async function () { - const provider = new firebase.appCheck.CustomProvider({ - getToken() { - return FirebaseHelpers.fetchAppCheckToken(); - }, - }); - - await firebase - .appCheck() - .initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false }); - const { token: tokenFromAppCheck } = await firebase.appCheck().getLimitedUseToken(); - tokenFromAppCheck.should.be.a.String(); - }); - - it('should listen for token changes', async function () { - const provider = new firebase.appCheck.CustomProvider({ - getToken() { - return FirebaseHelpers.fetchAppCheckToken(); - }, - }); - - await firebase - .appCheck() - .initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false }); - const unsubscribe = firebase.appCheck().onTokenChanged(_ => { - // TODO - improve testing cloud function to allow us to return tokens with low expiry - }); - - // TODO - improve testing cloud function to allow us to return tokens with low expiry - // result.should.be.an.Object(); - // const { token, expireTimeMillis } = result; - // token.should.be.a.String(); - // expireTimeMillis.should.be.a.Number(); - unsubscribe(); - }); - }); - - describe('AppCheck API methods', function () { - beforeEach(function () { - const { ReactNativeFirebaseAppCheckProvider } = appCheckModular; - let provider; - - if (!Platform.other) { - provider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider(); - provider.configure({ - android: { - provider: 'debug', - debugToken: getRandomToken(), - }, - apple: { - provider: 'debug', - debugToken: getRandomToken(), - }, - web: { - provider: 'debug', - siteKey: 'none', - }, - }); - } else { - provider = new ReactNativeFirebaseAppCheckProvider({ - getToken() { - return FirebaseHelpers.fetchAppCheckToken(); - }, - }); - } - - // Our tests configure a debug provider with shared secret so we should get a valid token - firebase.appCheck().initializeAppCheck({ provider, isTokenAutoRefreshEnabled: false }); - }); - - describe('config', function () { - // This depends on firebase.json containing a false value for token auto refresh, we - // verify here that it was carried in to the Info.plist correctly - // it relies on token auto refresh being left false for local tests where the app is reused, since it is persistent - // but in CI it's fresh every time and would be true if not overridden in Info.plist - it('should configure token auto refresh in Info.plist on ios', async function () { - if (Platform.ios) { - const tokenRefresh = - await NativeModules.RNFBAppCheckModule.isTokenAutoRefreshEnabled('[DEFAULT]'); - tokenRefresh.should.equal(false); - } else { - this.skip(); - } - }); - }); - - describe('setTokenAutoRefresh())', function () { - it('should set token refresh', async function () { - firebase.appCheck().setTokenAutoRefreshEnabled(false); - - // Only iOS lets us assert on this unfortunately, other platforms have no accessor - if (Platform.ios) { - let tokenRefresh = - await NativeModules.RNFBAppCheckModule.isTokenAutoRefreshEnabled('[DEFAULT]'); - tokenRefresh.should.equal(false); - } - firebase.appCheck().setTokenAutoRefreshEnabled(true); - // Only iOS lets us assert on this unfortunately, other platforms have no accessor - if (Platform.ios) { - tokenRefresh = - await NativeModules.RNFBAppCheckModule.isTokenAutoRefreshEnabled('[DEFAULT]'); - tokenRefresh.should.equal(true); - } - }); - }); - - // TODO flakey after many runs, sometimes fails on android & ios, - // possibly a rate limiting issue on the server side - describe('getToken()', function () { - it('token fetch attempt with configured debug token should work', async function () { - try { - const { token } = await firebase.appCheck().getToken(true); - token.should.not.equal(''); - const decodedToken = decodeJWT(token); - decodedToken.aud[1].should.equal('projects/react-native-firebase-testing'); - if (decodedToken.exp * 1000 < Date.now()) { - return Promise.reject( - `Token expired? now ${Date.now()} token exp: ${decodedToken.exp * 1000}`, - ); - } - - // on android if you move too fast, you may not get a fresh token - await Utils.sleep(2000); - - // Force refresh should get a different token? - // TODO sometimes fails on android https://github.com/firebase/firebase-android-sdk/issues/2954 - const { token: token2 } = await firebase.appCheck().getToken(true); - token2.should.not.equal(''); - const decodedToken2 = decodeJWT(token2); - decodedToken2.aud[1].should.equal('projects/react-native-firebase-testing'); - if (decodedToken2.exp * 1000 < Date.now()) { - return Promise.reject( - `Token expired? now ${Date.now()} token exp: ${decodedToken2.exp * 1000}`, - ); - } - (token === token2).should.be.false(); - } catch (e) { - // we will silently pass rate limiting errors - e.message.should.containEql('Quota exceeded'); - this.skip(); - } - }); - - it('token fetch with config switch to invalid then valid should fail then work', async function () { - if (Platform.other) { - this.skip(); - } - rnfbProvider = firebase.appCheck().newReactNativeFirebaseAppCheckProvider(); - rnfbProvider.configure({ - android: { - provider: 'playIntegrity', - }, - apple: { - provider: 'appAttest', - }, - web: { - provider: 'debug', - siteKey: 'none', - }, - }); - - // Our tests configure a debug provider with shared secret so we should get a valid token - firebase - .appCheck() - .initializeAppCheck({ provider: rnfbProvider, isTokenAutoRefreshEnabled: false }); - try { - await firebase.appCheck().getToken(true); - return Promise.reject(new Error('should have thrown an error')); - } catch (e) { - e.message.should.containEql('appCheck/token-error'); - } - - rnfbProvider.configure({ - android: { - provider: 'debug', - debugToken: getRandomToken(), - }, - apple: { - provider: 'debug', - debugToken: getRandomToken(), - }, - web: { - provider: 'debug', - siteKey: 'none', - }, - }); - firebase - .appCheck() - .initializeAppCheck({ provider: rnfbProvider, isTokenAutoRefreshEnabled: false }); - - try { - const { token } = await firebase.appCheck().getToken(true); - token.should.not.equal(''); - const decodedToken = decodeJWT(token); - decodedToken.aud[1].should.equal('projects/react-native-firebase-testing'); - if (decodedToken.exp * 1000 < Date.now()) { - return Promise.reject( - `Token expired? now ${Date.now()} token exp: ${decodedToken.exp * 1000}`, - ); - } - } catch (e) { - // we will silently pass rate limiting errors - e.message.should.containEql('Quota exceeded'); - this.skip(); - } - }); - }); - - describe('getLimitedUseToken()', function () { - it('limited use token fetch attempt with configured debug token should work', async function () { - try { - const { token } = await firebase.appCheck().getLimitedUseToken(); - token.should.not.equal(''); - const decodedToken = decodeJWT(token); - decodedToken.aud[1].should.equal('projects/react-native-firebase-testing'); - if (decodedToken.exp * 1000 < Date.now()) { - return Promise.reject( - `Token expired? now ${Date.now()} token exp: ${decodedToken.exp * 1000}`, - ); - } - } catch (e) { - // we will silently pass rate limiting errors - e.message.should.containEql('Quota exceeded'); - this.skip(); - } - }); - }); - - describe('activate())', function () { - if (Platform.other) { - return; - } - - it('should activate with default provider and defined token refresh', function () { - firebase - .appCheck() - .activate('ignored', false) - .then(value => expect(value).toBe(undefined)) - .catch(e => new Error('app-check activate failed? ' + e)); - }); - - it('should error if activate gets no parameters', async function () { - try { - firebase.appCheck().activate(); - return Promise.reject(new Error('should have thrown an error')); - } catch (e) { - e.message.should.containEql('siteKeyOrProvider must be a string value'); - } - }); - - it('should not error if activate called with no token refresh value', async function () { - try { - firebase.appCheck().activate('ignored'); - return Promise.resolve(true); - } catch (e) { - return Promise.reject(new Error('should not have thrown an error - ' + e)); - } - }); - }); - }); - }); - describe('modular', function () { let appCheckInstance; beforeEach(async function () { - const { initializeAppCheck, ReactNativeFirebaseAppCheckProvider } = appCheckModular; + const { initializeAppCheck, ReactNativeFirebaseAppCheckProvider, CustomProvider } = + appCheckModular; let provider; @@ -433,7 +120,7 @@ describe('appCheck()', function () { }, }); } else { - provider = new ReactNativeFirebaseAppCheckProvider({ + provider = new CustomProvider({ getToken() { return FirebaseHelpers.fetchAppCheckToken(); }, @@ -499,9 +186,7 @@ describe('appCheck()', function () { } (token === token2).should.be.false(); } catch (e) { - // we will silently pass rate limiting errors - e.message.should.containEql('Quota exceeded'); - this.skip(); + skipIfAppCheckCloudQuotaError(e, this); } }); @@ -567,15 +252,16 @@ describe('appCheck()', function () { ); } } catch (e) { - // we will silently pass rate limiting errors - e.message.should.containEql('Quota exceeded'); - this.skip(); + skipIfAppCheckCloudQuotaError(e, this); } }); }); describe('getLimitedUseToken()', function () { it('limited use token fetch attempt with configured debug token should work', async function () { + if (Platform.other) { + this.skip(); + } const { initializeAppCheck, getLimitedUseToken, ReactNativeFirebaseAppCheckProvider } = appCheckModular; @@ -611,9 +297,7 @@ describe('appCheck()', function () { ); } } catch (e) { - // we will silently pass rate limiting errors - e.message.should.containEql('Quota exceeded'); - this.skip(); + skipIfAppCheckCloudQuotaError(e, this); } }); }); diff --git a/packages/app-check/lib/index.ts b/packages/app-check/lib/index.ts index 4b9fd720da..09d791b24e 100644 --- a/packages/app-check/lib/index.ts +++ b/packages/app-check/lib/index.ts @@ -15,13 +15,302 @@ * */ -// Export types from types/appcheck -export type * from './types/appcheck'; +import { + isBoolean, + isIOS, + isObject, + isString, + isUndefined, + isOther, + parseListenerOrObserver, +} from '@react-native-firebase/app/dist/module/common'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import './types/internal'; +import { Platform } from 'react-native'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import fallBackModule from './web/RNFBAppCheckModule'; +import { version } from './version'; +import type { + AppCheck, + AppCheckOptions, + AppCheckProvider, + AppCheckTokenResult, + PartialObserver, + Unsubscribe, +} from './types/appcheck'; +import type { AppCheckInternal, ProviderWithOptions } from './types/internal'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import { ReactNativeFirebaseAppCheckProvider } from './providers'; + +const nativeModuleName = 'RNFBAppCheckModule'; + +/** + * Type guard to check if a provider has providerOptions. + * This provides proper type narrowing for providers that support platform-specific configuration. + */ +function hasProviderOptions( + provider: AppCheckOptions['provider'], +): provider is ProviderWithOptions { + return ( + provider !== undefined && + 'providerOptions' in provider && + provider.providerOptions !== undefined + ); +} + +class FirebaseAppCheckModule extends FirebaseModule { + _listenerCount: number; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); + + this.emitter.addListener(this.eventNameForApp('appCheck_token_changed'), event => { + this.emitter.emit(this.eventNameForApp('onAppCheckTokenChanged'), event); + }); + + this._listenerCount = 0; + } + + getIsTokenRefreshEnabledDefault(): boolean | undefined { + // no default to start + let isTokenAutoRefreshEnabled: boolean | undefined = undefined; + + return isTokenAutoRefreshEnabled; + } + + newReactNativeFirebaseAppCheckProvider(): ReactNativeFirebaseAppCheckProvider { + return new ReactNativeFirebaseAppCheckProvider(); + } + + initializeAppCheck(options: AppCheckOptions): Promise { + if (isOther) { + if (!isObject(options)) { + throw new Error('Invalid configuration: no options defined.'); + } + if (isUndefined(options.provider)) { + throw new Error('Invalid configuration: no provider defined.'); + } + return this.native.initializeAppCheck(options); + } + // determine token refresh setting, if not specified + if (!isBoolean(options.isTokenAutoRefreshEnabled)) { + const tokenRefresh = this.firebaseJson.app_check_token_auto_refresh; + if (isBoolean(tokenRefresh)) { + options.isTokenAutoRefreshEnabled = tokenRefresh; + } + } + + // If that was not defined, attempt to use app-wide data collection setting per docs: + if (!isBoolean(options.isTokenAutoRefreshEnabled)) { + const dataCollection = this.firebaseJson.app_data_collection_default_enabled; + if (isBoolean(dataCollection)) { + options.isTokenAutoRefreshEnabled = dataCollection; + } + } + + // If that also was not defined, the default is documented as true. + if (!isBoolean(options.isTokenAutoRefreshEnabled)) { + options.isTokenAutoRefreshEnabled = true; + } + this.native.setTokenAutoRefreshEnabled(options.isTokenAutoRefreshEnabled); + + if (!hasProviderOptions(options.provider)) { + throw new Error('Invalid configuration: no provider or no provider options defined.'); + } + const provider = options.provider; + if (Platform.OS === 'android') { + if (!isString(provider.providerOptions?.android?.provider)) { + throw new Error( + 'Invalid configuration: no android provider configured while on android platform.', + ); + } + return this.native.configureProvider( + provider.providerOptions.android.provider, + provider.providerOptions.android.debugToken, + ); + } + if (Platform.OS === 'ios' || Platform.OS === 'macos') { + if (!isString(provider.providerOptions?.apple?.provider)) { + throw new Error( + 'Invalid configuration: no apple provider configured while on apple platform.', + ); + } + return this.native.configureProvider( + provider.providerOptions.apple.provider, + provider.providerOptions.apple.debugToken, + ); + } + throw new Error('Unsupported platform: ' + Platform.OS); + } + + activate( + siteKeyOrProvider: string | AppCheckProvider, + isTokenAutoRefreshEnabled?: boolean, + ): Promise { + if (isOther) { + throw new Error('firebase.appCheck().activate(*) is not supported on other platforms'); + } + if (!isString(siteKeyOrProvider)) { + throw new Error('siteKeyOrProvider must be a string value to match firebase-js-sdk API'); + } + + // We wrap our new flexible interface, with compatible defaults + const rnfbProvider = new ReactNativeFirebaseAppCheckProvider(); + rnfbProvider.configure({ + android: { + provider: 'playIntegrity', + }, + apple: { + provider: 'deviceCheck', + }, + web: { + provider: 'reCaptchaV3', + siteKey: 'none', + }, + }); + + return this.initializeAppCheck({ provider: rnfbProvider, isTokenAutoRefreshEnabled }); + } -// Export modular API functions -export * from './modular'; + // TODO this is an async call + setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled: boolean): void { + this.native.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled); + } + + getToken(forceRefresh?: boolean): Promise { + if (!forceRefresh) { + return this.native.getToken(false); + } else { + return this.native.getToken(true); + } + } + + getLimitedUseToken(): Promise { + return this.native.getLimitedUseToken(); + } + + onTokenChanged( + onNextOrObserver: + | PartialObserver + | ((tokenResult: AppCheckTokenResult) => void), + _onError?: (error: Error) => void, + _onCompletion?: () => void, + ): () => void { + // iOS does not provide any native listening feature + if (isIOS) { + // eslint-disable-next-line no-console + console.warn('onTokenChanged is not implemented on IOS, only for Android'); + return () => {}; + } + const nextFn = parseListenerOrObserver( + onNextOrObserver as + | ((value: AppCheckTokenResult) => void) + | { next: (value: AppCheckTokenResult) => void }, + ); + // let errorFn = function () { }; + // if (onNextOrObserver.error != null) { + // errorFn = onNextOrObserver.error.bind(onNextOrObserver); + // } + // else if (onError) { + // errorFn = onError; + // } + const subscription = this.emitter.addListener( + this.eventNameForApp('onAppCheckTokenChanged'), + nextFn, + ); + if (this._listenerCount === 0) this.native.addAppCheckListener(); + + this._listenerCount++; + return () => { + subscription.remove(); + this._listenerCount--; + if (this._listenerCount === 0) this.native.removeAppCheckListener(); + }; + } +} + +const config: ModuleConfig = { + namespace: 'appCheck', + nativeModuleName, + nativeEvents: ['appCheck_token_changed'], + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: false, +}; + +export const SDK_VERSION = version; + +export { CustomProvider, ReactNativeFirebaseAppCheckProvider } from './providers'; + +function getModularAppCheck(app?: FirebaseApp): AppCheck { + return getOrCreateModularInstance(FirebaseAppCheckModule, config, app) as unknown as AppCheck; +} + +export async function initializeAppCheck( + app?: FirebaseApp, + options?: AppCheckOptions, +): Promise { + if (!isObject(options)) { + throw new Error('Invalid configuration: no options defined.'); + } + const appCheck = getModularAppCheck(app); + await (appCheck as AppCheckInternal).initializeAppCheck(options); + return appCheck; +} + +export function getToken( + appCheckInstance: AppCheck, + forceRefresh?: boolean, +): Promise { + return (appCheckInstance as AppCheckInternal).getToken(forceRefresh); +} + +export function getLimitedUseToken(appCheckInstance: AppCheck): Promise { + return (appCheckInstance as AppCheckInternal).getLimitedUseToken(); +} + +export function setTokenAutoRefreshEnabled( + appCheckInstance: AppCheck, + isAutoRefreshEnabled: boolean, +): void { + (appCheckInstance as AppCheckInternal).setTokenAutoRefreshEnabled(isAutoRefreshEnabled); +} + +export function onTokenChanged( + appCheckInstance: AppCheck, + observer: PartialObserver, +): Unsubscribe; + +export function onTokenChanged( + appCheckInstance: AppCheck, + onNext: (tokenResult: AppCheckTokenResult) => void, + onError?: (error: Error) => void, + onCompletion?: () => void, +): Unsubscribe; + +export function onTokenChanged( + appCheckInstance: AppCheck, + onNextOrObserver: + | PartialObserver + | ((tokenResult: AppCheckTokenResult) => void), + onError?: (error: Error) => void, + onCompletion?: () => void, +): Unsubscribe { + return (appCheckInstance as AppCheckInternal).onTokenChanged( + onNextOrObserver, + onError, + onCompletion, + ); +} + +export type * from './types/appcheck'; -// Export namespaced API -export type { FirebaseAppCheckTypes } from './types/namespaced'; -export * from './namespaced'; -export { default } from './namespaced'; +setReactNativeModule(nativeModuleName, fallBackModule as unknown as Record); diff --git a/packages/app-check/lib/modular.ts b/packages/app-check/lib/modular.ts deleted file mode 100644 index 5e7d8c23d8..0000000000 --- a/packages/app-check/lib/modular.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp, type FirebaseApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; - -import type { - AppCheck, - AppCheckOptions, - AppCheckTokenResult, - PartialObserver, - Unsubscribe, -} from './types/appcheck'; -import type { AppCheckInternal } from './types/internal'; - -export { CustomProvider, ReactNativeFirebaseAppCheckProvider } from './providers'; - -type WithModularDeprecationArg = F extends (...args: infer P) => infer R - ? (...args: [...P, typeof MODULAR_DEPRECATION_ARG]) => R - : never; - -/** - * Activate App Check for the given app. Can be called only once per app. - * @param app - The app to initialize App Check for. Optional. - * @param options - App Check options. - * @returns Promise - */ -export async function initializeAppCheck( - app?: FirebaseApp, - options?: AppCheckOptions, -): Promise { - const appInstance = app ? (getApp(app.name) as FirebaseApp) : (getApp() as FirebaseApp); - const appCheck = appInstance.appCheck(); - await ( - (appCheck as AppCheckInternal).initializeAppCheck as WithModularDeprecationArg< - AppCheckInternal['initializeAppCheck'] - > - ).call(appCheck, options as AppCheckOptions, MODULAR_DEPRECATION_ARG); - return appCheck; -} - -/** - * Get the current App Check token. Attaches to the most recent in-flight request if one is present. - * Returns null if no token is present and no token requests are in-flight. - * @param appCheckInstance - The App Check instance. - * @param forceRefresh - Whether to force refresh the token. Optional - * @returns Promise - */ -export function getToken( - appCheckInstance: AppCheck, - forceRefresh?: boolean, -): Promise { - return ( - (appCheckInstance as AppCheckInternal).getToken as WithModularDeprecationArg< - AppCheckInternal['getToken'] - > - ).call(appCheckInstance, forceRefresh, MODULAR_DEPRECATION_ARG) as Promise; -} - -/** - * Get a limited-use (consumable) App Check token. - * For use with server calls to firebase functions or custom backends using the firebase admin SDK. - * @param appCheckInstance - The App Check instance. - * @returns Promise - */ -export function getLimitedUseToken(appCheckInstance: AppCheck): Promise { - return ( - (appCheckInstance as AppCheckInternal).getLimitedUseToken as WithModularDeprecationArg< - AppCheckInternal['getLimitedUseToken'] - > - ).call(appCheckInstance, MODULAR_DEPRECATION_ARG) as Promise; -} - -/** - * Set whether App Check will automatically refresh tokens as needed. - * @param appCheckInstance - The App Check instance. - * @param isAutoRefreshEnabled - Whether to enable auto-refresh. - */ -export function setTokenAutoRefreshEnabled( - appCheckInstance: AppCheck, - isAutoRefreshEnabled: boolean, -): void { - ( - (appCheckInstance as AppCheckInternal).setTokenAutoRefreshEnabled as WithModularDeprecationArg< - AppCheckInternal['setTokenAutoRefreshEnabled'] - > - ).call(appCheckInstance, isAutoRefreshEnabled, MODULAR_DEPRECATION_ARG); -} - -/** - * Registers a listener to changes in the token state. There can be more - * than one listener registered at the same time for one or more - * App Check instances. The listeners call back on the UI thread whenever - * the current token associated with this App Check instance changes. - * - * @param appCheckInstance - The App Check instance. - * @param observer - The listener to register. - * @returns Unsubscribe - */ -export function onTokenChanged( - appCheckInstance: AppCheck, - observer: PartialObserver, -): Unsubscribe; - -/** - * Registers a listener to changes in the token state. There can be more - * than one listener registered at the same time for one or more - * App Check instances. The listeners call back on the UI thread whenever - * the current token associated with this App Check instance changes. - * - * @param appCheckInstance - The App Check instance. - * @param onNext - The callback function for token changes. - * @param onError - Optional error callback. - * @param onCompletion - Optional completion callback. - * @returns Unsubscribe - */ -export function onTokenChanged( - appCheckInstance: AppCheck, - onNext: (tokenResult: AppCheckTokenResult) => void, - onError?: (error: Error) => void, - onCompletion?: () => void, -): Unsubscribe; - -export function onTokenChanged( - appCheckInstance: AppCheck, - onNextOrObserver: - | PartialObserver - | ((tokenResult: AppCheckTokenResult) => void), - onError?: (error: Error) => void, - onCompletion?: () => void, -): Unsubscribe { - return ( - (appCheckInstance as AppCheckInternal).onTokenChanged as WithModularDeprecationArg< - AppCheckInternal['onTokenChanged'] - > - ).call( - appCheckInstance, - onNextOrObserver, - onError, - onCompletion, - MODULAR_DEPRECATION_ARG, - ) as Unsubscribe; -} diff --git a/packages/app-check/lib/namespaced.ts b/packages/app-check/lib/namespaced.ts deleted file mode 100644 index 678773adcc..0000000000 --- a/packages/app-check/lib/namespaced.ts +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { - isBoolean, - isIOS, - isString, - isObject, - isUndefined, - isOther, - parseListenerOrObserver, -} from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; -import { Platform } from 'react-native'; -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import fallBackModule from './web/RNFBAppCheckModule'; -import { version } from './version'; -import type { - AppCheckOptions, - AppCheckProvider, - AppCheckTokenResult, - PartialObserver, -} from './types/appcheck'; -import type { ProviderWithOptions } from './types/internal'; -import type { FirebaseAppCheckTypes } from './types/namespaced'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import { CustomProvider, ReactNativeFirebaseAppCheckProvider } from './providers'; - -const namespace = 'appCheck'; - -const nativeModuleName = 'RNFBAppCheckModule'; - -const statics = { - CustomProvider, -}; - -/** - * Type guard to check if a provider has providerOptions. - * This provides proper type narrowing for providers that support platform-specific configuration. - */ -function hasProviderOptions( - provider: AppCheckOptions['provider'], -): provider is ProviderWithOptions { - return ( - provider !== undefined && - 'providerOptions' in provider && - provider.providerOptions !== undefined - ); -} - -class FirebaseAppCheckModule extends FirebaseModule { - _listenerCount: number; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - customUrlOrRegion?: string | null, - ) { - super(app, config, customUrlOrRegion); - - this.emitter.addListener(this.eventNameForApp('appCheck_token_changed'), event => { - this.emitter.emit(this.eventNameForApp('onAppCheckTokenChanged'), event); - }); - - this._listenerCount = 0; - } - - getIsTokenRefreshEnabledDefault(): boolean | undefined { - // no default to start - let isTokenAutoRefreshEnabled: boolean | undefined = undefined; - - return isTokenAutoRefreshEnabled; - } - - newReactNativeFirebaseAppCheckProvider(): ReactNativeFirebaseAppCheckProvider { - return new ReactNativeFirebaseAppCheckProvider(); - } - - initializeAppCheck(options: AppCheckOptions): Promise { - if (isOther) { - if (!isObject(options)) { - throw new Error('Invalid configuration: no options defined.'); - } - if (isUndefined(options.provider)) { - throw new Error('Invalid configuration: no provider defined.'); - } - return this.native.initializeAppCheck(options); - } - // determine token refresh setting, if not specified - if (!isBoolean(options.isTokenAutoRefreshEnabled)) { - const tokenRefresh = this.firebaseJson.app_check_token_auto_refresh; - if (isBoolean(tokenRefresh)) { - options.isTokenAutoRefreshEnabled = tokenRefresh; - } - } - - // If that was not defined, attempt to use app-wide data collection setting per docs: - if (!isBoolean(options.isTokenAutoRefreshEnabled)) { - const dataCollection = this.firebaseJson.app_data_collection_default_enabled; - if (isBoolean(dataCollection)) { - options.isTokenAutoRefreshEnabled = dataCollection; - } - } - - // If that also was not defined, the default is documented as true. - if (!isBoolean(options.isTokenAutoRefreshEnabled)) { - options.isTokenAutoRefreshEnabled = true; - } - this.native.setTokenAutoRefreshEnabled(options.isTokenAutoRefreshEnabled); - - if (!hasProviderOptions(options.provider)) { - throw new Error('Invalid configuration: no provider or no provider options defined.'); - } - const provider = options.provider; - if (Platform.OS === 'android') { - if (!isString(provider.providerOptions?.android?.provider)) { - throw new Error( - 'Invalid configuration: no android provider configured while on android platform.', - ); - } - return this.native.configureProvider( - provider.providerOptions.android.provider, - provider.providerOptions.android.debugToken, - ); - } - if (Platform.OS === 'ios' || Platform.OS === 'macos') { - if (!isString(provider.providerOptions?.apple?.provider)) { - throw new Error( - 'Invalid configuration: no apple provider configured while on apple platform.', - ); - } - return this.native.configureProvider( - provider.providerOptions.apple.provider, - provider.providerOptions.apple.debugToken, - ); - } - throw new Error('Unsupported platform: ' + Platform.OS); - } - - activate( - siteKeyOrProvider: string | AppCheckProvider, - isTokenAutoRefreshEnabled?: boolean, - ): Promise { - if (isOther) { - throw new Error('firebase.appCheck().activate(*) is not supported on other platforms'); - } - if (!isString(siteKeyOrProvider)) { - throw new Error('siteKeyOrProvider must be a string value to match firebase-js-sdk API'); - } - - // We wrap our new flexible interface, with compatible defaults - const rnfbProvider = new ReactNativeFirebaseAppCheckProvider(); - rnfbProvider.configure({ - android: { - provider: 'playIntegrity', - }, - apple: { - provider: 'deviceCheck', - }, - web: { - provider: 'reCaptchaV3', - siteKey: 'none', - }, - }); - - return this.initializeAppCheck({ provider: rnfbProvider, isTokenAutoRefreshEnabled }); - } - - // TODO this is an async call - setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled: boolean): void { - this.native.setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled); - } - - getToken(forceRefresh?: boolean): Promise { - if (!forceRefresh) { - return this.native.getToken(false); - } else { - return this.native.getToken(true); - } - } - - getLimitedUseToken(): Promise { - return this.native.getLimitedUseToken(); - } - - onTokenChanged( - onNextOrObserver: - | PartialObserver - | ((tokenResult: FirebaseAppCheckTypes.AppCheckListenerResult) => void), - _onError?: (error: Error) => void, - _onCompletion?: () => void, - ): () => void { - // iOS does not provide any native listening feature - if (isIOS) { - // eslint-disable-next-line no-console - console.warn('onTokenChanged is not implemented on IOS, only for Android'); - return () => {}; - } - const nextFn = parseListenerOrObserver( - onNextOrObserver as - | ((value: FirebaseAppCheckTypes.AppCheckListenerResult) => void) - | { next: (value: FirebaseAppCheckTypes.AppCheckListenerResult) => void }, - ); - // let errorFn = function () { }; - // if (onNextOrObserver.error != null) { - // errorFn = onNextOrObserver.error.bind(onNextOrObserver); - // } - // else if (onError) { - // errorFn = onError; - // } - const subscription = this.emitter.addListener( - this.eventNameForApp('onAppCheckTokenChanged'), - nextFn, - ); - if (this._listenerCount === 0) this.native.addAppCheckListener(); - - this._listenerCount++; - return () => { - subscription.remove(); - this._listenerCount--; - if (this._listenerCount === 0) this.native.removeAppCheckListener(); - }; - } -} - -export const SDK_VERSION = version; - -const appCheckNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: ['appCheck_token_changed'], - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseAppCheckModule, -}); - -type AppCheckNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseAppCheckTypes.Module, - FirebaseAppCheckTypes.Statics -> & { - appCheck: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseAppCheckTypes.Module, - FirebaseAppCheckTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import appCheck from '@react-native-firebase/app-check'; -// appCheck().X(...); -export default appCheckNamespace as unknown as AppCheckNamespace; - -// import appCheck, { firebase } from '@react-native-firebase/app-check'; -// appCheck().X(...); -// firebase.appCheck().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'appCheck', - FirebaseAppCheckTypes.Module, - FirebaseAppCheckTypes.Statics, - false - >; - -// Register the interop module for non-native platforms. -setReactNativeModule(nativeModuleName, fallBackModule as unknown as Record); diff --git a/packages/app-check/lib/types/namespaced.ts b/packages/app-check/lib/types/namespaced.ts deleted file mode 100644 index 71f7d0c442..0000000000 --- a/packages/app-check/lib/types/namespaced.ts +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase App Check package for React Native. - * - * #### Example 1 - * - * Access the firebase export from the `appCheck` package: - * - * ```js - * import { firebase } from '@react-native-firebase/app-check'; - * - * // firebase.appCheck().X - * ``` - * - * #### Example 2 - * - * Using the default export from the `appCheck` package: - * - * ```js - * import appCheck from '@react-native-firebase/app-check'; - * - * // appCheck().X - * ``` - * - * #### Example 3 - * - * Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/app-check'; - * - * // firebase.appCheck().X - * ``` - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - * @firebase app-check - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseAppCheckTypes { - type FirebaseModule = ReactNativeFirebase.FirebaseModule; - - /** - * An App Check provider. This can be either the built-in reCAPTCHA provider - * or a custom provider. For more on custom providers, see - * https://firebase.google.com/docs/app-check/web-custom-provider - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface AppCheckProvider { - /** - * Returns an AppCheck token. - */ - getToken(): Promise; - } - - /** - * Custom provider class. - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - * @public - */ - export declare class CustomProvider implements AppCheckProvider { - constructor(customProviderOptions: CustomProviderOptions); - getToken(): Promise; - } - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface CustomProviderOptions { - /** - * Function to get an App Check token through a custom provider - * service. - */ - getToken: () => Promise; - } - /** - * Options for App Check initialization. - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface AppCheckOptions { - /** - * The App Check provider to use. This can be either the built-in reCAPTCHA provider - * or a custom provider. - */ - provider: CustomProvider | ReactNativeFirebaseAppCheckProvider; - - /** - * If true, enables SDK to automatically - * refresh AppCheck token as needed. If undefined, the value will default - * to the value of `app.automaticDataCollectionEnabled`. That property - * defaults to false and can be set in the app config. - */ - isTokenAutoRefreshEnabled?: boolean; - } - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export type NextFn = (value: T) => void; - export type ErrorFn = (error: Error) => void; - export type CompleteFn = () => void; - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface Observer { - next: NextFn; - error: ErrorFn; - complete: CompleteFn; - } - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export type PartialObserver = Partial>; - - /** - * A function that unsubscribes from token changes. - */ - export type Unsubscribe = () => void; - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface ReactNativeFirebaseAppCheckProviderOptions { - /** - * debug token to use, if any. Defaults to undefined, pre-configure tokens in firebase web console if needed - */ - debugToken?: string; - } - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface ReactNativeFirebaseAppCheckProviderWebOptions extends ReactNativeFirebaseAppCheckProviderOptions { - /** - * The web provider to use, either `reCaptchaV3` or `reCaptchaEnterprise`, defaults to `reCaptchaV3` - */ - provider?: 'debug' | 'reCaptchaV3' | 'reCaptchaEnterprise'; - - /** - * siteKey for use in web queries, defaults to `none` - */ - siteKey?: string; - } - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface ReactNativeFirebaseAppCheckProviderAppleOptions extends ReactNativeFirebaseAppCheckProviderOptions { - /** - * The apple provider to use, either `deviceCheck` or `appAttest`, or `appAttestWithDeviceCheckFallback`, - * defaults to `DeviceCheck`. `appAttest` requires iOS 14+ or will fail, `appAttestWithDeviceCheckFallback` - * will use `appAttest` for iOS14+ and fallback to `deviceCheck` on devices with ios13 and lower - */ - provider?: 'debug' | 'deviceCheck' | 'appAttest' | 'appAttestWithDeviceCheckFallback'; - } - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface ReactNativeFirebaseAppCheckProviderAndroidOptions extends ReactNativeFirebaseAppCheckProviderOptions { - /** - * The android provider to use, either `debug` or `playIntegrity`. default is `playIntegrity`. - */ - provider?: 'debug' | 'playIntegrity'; - } - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface ReactNativeFirebaseAppCheckProvider extends AppCheckProvider { - /** - * Specify how the app check provider should be configured. The new configuration is - * in effect when this call returns. You must call `getToken()` - * after this call to get a token using the new configuration. - * This custom provider allows for delayed configuration and re-configuration on all platforms - * so AppCheck has the same experience across all platforms, with the only difference being the native - * providers you choose to use on each platform. - */ - configure(options: { - web?: ReactNativeFirebaseAppCheckProviderWebOptions; - android?: ReactNativeFirebaseAppCheckProviderAndroidOptions; - apple?: ReactNativeFirebaseAppCheckProviderAppleOptions; - isTokenAutoRefreshEnabled?: boolean; - }): void; - } - - /** - * Result returned by `getToken()`. - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface AppCheckTokenResult { - /** - * The token string in JWT format. - */ - readonly token: string; - } - /** - * The token returned from an `AppCheckProvider`. - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface AppCheckToken { - /** - * The token string in JWT format. - */ - readonly token: string; - /** - * The local timestamp after which the token will expire. - */ - readonly expireTimeMillis: number; - } - /** - * The result return from `onTokenChanged` - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export type AppCheckListenerResult = AppCheckToken & { readonly appName: string }; - - /** - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface Statics { - // firebase.appCheck.* static props go here - CustomProvider: typeof CustomProvider; - SDK_VERSION: string; - } - - /** - * The Firebase App Check service is available for the default app or a given app. - * - * #### Example 1 - * - * Get the appCheck instance for the **default app**: - * - * ```js - * const appCheckForDefaultApp = firebase.appCheck(); - * ``` - * - * #### Example 2 - * - * Get the appCheck instance for a **secondary app**: - *˚ - * ```js - * const otherApp = firebase.app('otherApp'); - * const appCheckForOtherApp = firebase.appCheck(otherApp); - * ``` - * - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - export interface Module extends FirebaseModule { - /** - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * Create a ReactNativeFirebaseAppCheckProvider option for use in react-native-firebase - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - newReactNativeFirebaseAppCheckProvider(): ReactNativeFirebaseAppCheckProvider; - - /** - * Initialize the AppCheck module. Note that in react-native-firebase AppCheckOptions must always - * be an object with a `provider` member containing `ReactNativeFirebaseAppCheckProvider` that has returned successfully - * from a call to the `configure` method, with sub-providers for the various platforms configured to meet your project - * requirements. This must be called prior to interacting with any firebase services protected by AppCheck - * - * @param options an AppCheckOptions with a configured ReactNativeFirebaseAppCheckProvider as the provider - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - // TODO wrong types - initializeAppCheck(options: AppCheckOptions): Promise; - - /** - * Activate App Check - * On iOS App Check is activated with DeviceCheck provider simply by including the module, using the token auto refresh default or - * the specific value (if configured) in firebase.json, but calling this does no harm. - * On Android if you call this it will install the PlayIntegrity provider in release builds, the Debug provider if debuggable. - * On both platforms you may use this method to alter the token refresh setting after startup. - * On iOS if you want to set a specific AppCheckProviderFactory (for instance to FIRAppCheckDebugProviderFactory or - * FIRAppAttestProvider) you must manually do that in your AppDelegate.m prior to calling [FIRApp configure] - * - * @deprecated use initializeAppCheck to gain access to all platform providers and firebase-js-sdk v9 compatibility - * @param siteKeyOrProvider - This is ignored, Android uses DebugProviderFactory if the app is debuggable (https://firebase.google.com/docs/app-check/android/debug-provider) - * Android uses PlayIntegrityProviderFactory for release builds. - * iOS uses DeviceCheckProviderFactory by default unless altered in AppDelegate.m manually - * @param isTokenAutoRefreshEnabled - If true, enables SDK to automatically - * refresh AppCheck token as needed. If undefined, the value will default - * to the value of `app.automaticDataCollectionEnabled`. That property - * defaults to false and can be set in the app config. - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - activate( - siteKeyOrProvider: string | AppCheckProvider, - isTokenAutoRefreshEnabled?: boolean, - ): Promise; - - /** - * Alter the token auto refresh setting. By default it will take the value of automaticDataCollectionEnabled from Info.plist / AndroidManifest.xml - * @param isTokenAutoRefreshEnabled - If true, the SDK automatically - * refreshes App Check tokens as needed. This overrides any value set - * during `activate()` or taken by default from automaticDataCollectionEnabled in plist / android manifest - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - setTokenAutoRefreshEnabled(isTokenAutoRefreshEnabled: boolean): void; - - /** - * Requests Firebase App Check token. - * This method should only be used if you need to authorize requests to a non-Firebase backend. - * Requests to Firebase backend are authorized automatically if configured. - * - * @param forceRefresh - If true, a new Firebase App Check token is requested and the token cache is ignored. - * If false, the cached token is used if it exists and has not expired yet. - * In most cases, false should be used. True should only be used if the server explicitly returns an error, indicating a revoked token. - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - getToken(forceRefresh?: boolean): Promise; - - /** - * Requests a Firebase App Check token. This method should be used only if you need to authorize requests - * to a non-Firebase backend. Returns limited-use tokens that are intended for use with your non-Firebase - * backend endpoints that are protected with Replay Protection (https://firebase.google.com/docs/app-check/custom-resource-backend#replay-protection). - * This method does not affect the token generation behavior of the getAppCheckToken() method. - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - */ - getLimitedUseToken(): Promise; - - /** - * Registers a listener to changes in the token state. There can be more - * than one listener registered at the same time for one or more - * App Check instances. The listeners call back on the UI thread whenever - * the current token associated with this App Check instance changes. - * - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - * @returns A function that unsubscribes this listener. - */ - // TODO wrong types - onTokenChanged(observer: PartialObserver): () => void; - - /** - * Registers a listener to changes in the token state. There can be more - * than one listener registered at the same time for one or more - * App Check instances. The listeners call back on the UI thread whenever - * the current token associated with this App Check instance changes. - * - * Token listeners do not exist in the native SDK for iOS, no token change events will be emitted on that platform. - * This is not yet implemented on Android, no token change events will be emitted until implemented. - * - * NOTE: Although an `onError` callback can be provided, it will - * never be called, Android sdk code doesn't provide handling for onError function - * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the token stream is never-ending. - * - * @deprecated Use the exported types directly instead. FirebaseAppCheckTypes namespace is kept for backwards compatibility. - * @returns A function that unsubscribes this listener. - */ - // TODO wrong types - onTokenChanged( - onNext: (tokenResult: AppCheckListenerResult) => void, - onError?: (error: Error) => void, - onCompletion?: () => void, - ): () => void; - } -} - -type AppCheckNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseAppCheckTypes.Module, - FirebaseAppCheckTypes.Statics -> & { - appCheck: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseAppCheckTypes.Module, - FirebaseAppCheckTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -declare const defaultExport: AppCheckNamespace; - -export declare const firebase: ReactNativeFirebase.Module & { - appCheck: typeof defaultExport; - app( - name?: string, - ): ReactNativeFirebase.FirebaseApp & { appCheck(): FirebaseAppCheckTypes.Module }; -}; - -export default defaultExport; - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - interface Module { - appCheck: FirebaseModuleWithStaticsAndApp< - FirebaseAppCheckTypes.Module, - FirebaseAppCheckTypes.Statics - >; - } - interface FirebaseApp { - appCheck(): FirebaseAppCheckTypes.Module; - } - } -} diff --git a/packages/app-check/type-test.ts b/packages/app-check/type-test.ts index d1d92a051d..d20af39c9c 100644 --- a/packages/app-check/type-test.ts +++ b/packages/app-check/type-test.ts @@ -1,196 +1,40 @@ -import appCheck, { - firebase, - FirebaseAppCheckTypes, +import { getApp } from '@react-native-firebase/app'; +import { initializeAppCheck, getToken, getLimitedUseToken, setTokenAutoRefreshEnabled, onTokenChanged, CustomProvider, - AppCheck, - AppCheckTokenResult, + SDK_VERSION, + type AppCheck, + type AppCheckOptions, + type AppCheckTokenResult, } from '.'; -console.log(appCheck().app); - -// checks module exists at root -console.log(firebase.appCheck().app.name); - -// checks module exists at app level -console.log(firebase.app().appCheck().app.name); - -// checks statics exist -console.log(firebase.appCheck.SDK_VERSION); - -// checks statics exist on defaultExport -console.log(appCheck.firebase.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks multi-app support exists -console.log(firebase.appCheck(firebase.app()).app.name); - -// checks default export supports app arg -console.log(appCheck(firebase.app()).app.name); - -// checks CustomProvider static exists -console.log(firebase.appCheck.CustomProvider); - -// checks Module instance APIs -const appCheckInstance = firebase.appCheck(); -console.log(appCheckInstance.newReactNativeFirebaseAppCheckProvider()); - -const provider = appCheckInstance.newReactNativeFirebaseAppCheckProvider(); -provider.configure({ - android: { provider: 'playIntegrity' }, - apple: { provider: 'deviceCheck' }, - web: { provider: 'reCaptchaV3', siteKey: 'test' }, -}); - -appCheckInstance - .initializeAppCheck({ - provider: provider, - isTokenAutoRefreshEnabled: true, - }) - .then(() => { - console.log('Initialized'); - }); - -appCheckInstance.activate('test', true).then(() => { - console.log('Activated'); -}); - -appCheckInstance.setTokenAutoRefreshEnabled(true); - -appCheckInstance.getToken().then((result: FirebaseAppCheckTypes.AppCheckTokenResult) => { - console.log(result.token); -}); - -appCheckInstance.getToken(true).then((result: FirebaseAppCheckTypes.AppCheckTokenResult) => { - console.log(result.token); -}); - -appCheckInstance.getLimitedUseToken().then((result: FirebaseAppCheckTypes.AppCheckTokenResult) => { - console.log(result.token); -}); - -const unsubscribe1 = appCheckInstance.onTokenChanged({ - next: (tokenResult: FirebaseAppCheckTypes.AppCheckListenerResult) => { - console.log(tokenResult.token); - console.log(tokenResult.appName); - }, - error: (error: Error) => { - console.log(error.message); - }, - complete: () => { - console.log('Complete'); - }, -}); - -const unsubscribe2 = appCheckInstance.onTokenChanged( - (tokenResult: FirebaseAppCheckTypes.AppCheckListenerResult) => { - console.log(tokenResult.token); - console.log(tokenResult.appName); - }, - (error: Error) => { - console.log(error.message); - }, - () => { - console.log('Complete'); +const options: AppCheckOptions = { + provider: { + providerOptions: { + android: { provider: 'debug' }, + }, }, -); - -unsubscribe1(); -unsubscribe2(); - -// checks modular API functions -const modularProvider = appCheckInstance.newReactNativeFirebaseAppCheckProvider(); -modularProvider.configure({ - android: { provider: 'playIntegrity' }, - apple: { provider: 'deviceCheck' }, - web: { provider: 'reCaptchaV3', siteKey: 'test' }, -}); +}; +const appCheck = {} as AppCheck; -initializeAppCheck(firebase.app(), { - provider: modularProvider, - isTokenAutoRefreshEnabled: true, -}).then((appCheckModular: AppCheck) => { - console.log(appCheckModular.app.name); +initializeAppCheck(getApp(), options).then((instance: AppCheck) => { + console.log(instance.app.name); }); -initializeAppCheck(undefined, { - provider: modularProvider, - isTokenAutoRefreshEnabled: true, -}).then((appCheckModular: AppCheck) => { - console.log(appCheckModular.app.name); -}); - -getToken(appCheckInstance).then((result: AppCheckTokenResult) => { +getToken(appCheck).then((result: AppCheckTokenResult) => { console.log(result.token); }); -getToken(appCheckInstance, true).then((result: AppCheckTokenResult) => { +getLimitedUseToken(appCheck).then((result: AppCheckTokenResult) => { console.log(result.token); }); -getLimitedUseToken(appCheckInstance).then((result: AppCheckTokenResult) => { - console.log(result.token); -}); - -setTokenAutoRefreshEnabled(appCheckInstance, true); - -const modularUnsubscribe1 = onTokenChanged(appCheckInstance, { - next: (tokenResult: AppCheckTokenResult) => { - console.log(tokenResult.token); - }, - error: (error: Error) => { - console.log(error.message); - }, - complete: () => { - console.log('Complete'); - }, -}); - -const modularUnsubscribe2 = onTokenChanged( - appCheckInstance, - (tokenResult: AppCheckTokenResult) => { - console.log(tokenResult.token); - }, - (error: Error) => { - console.log(error.message); - }, - () => { - console.log('Complete'); - }, -); - -modularUnsubscribe1(); -modularUnsubscribe2(); - -// checks modular CustomProvider class -const customProvider = new CustomProvider({ - getToken: async () => { - return { - token: 'test-token', - expireTimeMillis: Date.now() + 3600000, - }; - }, -}); - -// CustomProvider can be used in AppCheckOptions -initializeAppCheck(firebase.app(), { - provider: customProvider, - isTokenAutoRefreshEnabled: true, -}).then(() => { - console.log('CustomProvider initialized'); -}); +setTokenAutoRefreshEnabled(appCheck, true); +onTokenChanged(appCheck, () => {}); -// checks modular ReactNativeFirebaseAppCheckProvider class can be instantiated directly from modular import -const rnfbProvider = appCheckInstance.newReactNativeFirebaseAppCheckProvider(); -// Test that configure method exists and can be called (reusing modularProvider for initializeAppCheck above) -rnfbProvider.configure({ - android: { provider: 'playIntegrity' }, - apple: { provider: 'deviceCheck' }, - web: { provider: 'reCaptchaV3', siteKey: 'test' }, -}); +console.log(CustomProvider); +console.log(SDK_VERSION); diff --git a/packages/app-check/typedoc.json b/packages/app-check/typedoc.json index 518999b52b..001375e245 100644 --- a/packages/app-check/typedoc.json +++ b/packages/app-check/typedoc.json @@ -1,5 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/appcheck.ts"], + "entryPoints": ["lib/index.ts", "lib/types/appcheck.ts"], "tsconfig": "tsconfig.json" } diff --git a/packages/app-distribution/__tests__/app-distribution.test.ts b/packages/app-distribution/__tests__/app-distribution.test.ts index 647ce0385a..584307ad3b 100644 --- a/packages/app-distribution/__tests__/app-distribution.test.ts +++ b/packages/app-distribution/__tests__/app-distribution.test.ts @@ -1,7 +1,6 @@ -import { afterAll, beforeAll, describe, expect, it, beforeEach, jest } from '@jest/globals'; +import { describe, expect, it, jest } from '@jest/globals'; import { - firebase, getAppDistribution, isTesterSignedIn, signInTester, @@ -9,15 +8,6 @@ import { signOutTester, } from '../lib'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; - -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; - -// Mock isIOS to be true so the app distribution methods work in tests jest.mock('@react-native-firebase/app/dist/module/common', () => { const actualCommon = jest.requireActual('@react-native-firebase/app/dist/module/common'); return Object.assign({}, actualCommon, { @@ -26,24 +16,6 @@ jest.mock('@react-native-firebase/app/dist/module/common', () => { }); describe('appDistribution()', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.appDistribution).toBeDefined(); - expect(app.appDistribution().app).toEqual(app); - }); - }); - describe('modular', function () { it('`getAppDistribution` function is properly exposed to end user', function () { expect(getAppDistribution).toBeDefined(); @@ -65,63 +37,4 @@ describe('appDistribution()', function () { expect(signOutTester).toBeDefined(); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let appDistributionV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - appDistributionV9Deprecation = createCheckV9Deprecation(['appDistribution']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - constants: { - isTesterSignedIn: true, - }, - } as never), - }, - ); - }); - }); - - it('isTesterSignedIn', function () { - const appDistribution = getAppDistribution(); - appDistributionV9Deprecation( - () => isTesterSignedIn(appDistribution), - () => appDistribution.isTesterSignedIn(), - 'isTesterSignedIn', - ); - }); - - it('signInTester', function () { - const appDistribution = getAppDistribution(); - appDistributionV9Deprecation( - () => signInTester(appDistribution), - () => appDistribution.signInTester(), - 'signInTester', - ); - }); - - it('checkForUpdate', function () { - const appDistribution = getAppDistribution(); - appDistributionV9Deprecation( - () => checkForUpdate(appDistribution), - () => appDistribution.checkForUpdate(), - 'checkForUpdate', - ); - }); - - it('signOutTester', function () { - const appDistribution = getAppDistribution(); - appDistributionV9Deprecation( - () => signOutTester(appDistribution), - () => appDistribution.signOutTester(), - 'signOutTester', - ); - }); - }); }); diff --git a/packages/app-distribution/e2e/app-distribution.e2e.js b/packages/app-distribution/e2e/app-distribution.e2e.js index 5d05e3d7ee..f9a0e30ae1 100644 --- a/packages/app-distribution/e2e/app-distribution.e2e.js +++ b/packages/app-distribution/e2e/app-distribution.e2e.js @@ -16,65 +16,6 @@ */ describe('appDistribution()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('native module is loaded', function () { - it('checks native module load status', function () { - firebase.appDistribution().native; - }); - }); - - describe('isTesterSignedIn()', function () { - it('checks if a tester is signed in', async function () { - if (Platform.ios) { - await firebase.appDistribution().isTesterSignedIn(); - } else { - this.skip(); - } - }); - }); - // Requires a valid google account logged in on device and associated with iOS testing app - xdescribe('signInTester()', function () { - it('signs a tester in', async function () { - if (Platform.ios) { - await firebase.appDistribution().signInTester(); - } else { - this.skip(); - } - }); - }); - - describe('signOutTester()', function () { - it('signs out a tester', async function () { - if (Platform.ios) { - await firebase.appDistribution().signOutTester(); - } else { - this.skip(); - } - }); - }); - // Requires a valid google account logged in on device and associated with iOS testing app - // plus a new IPA file uploaded - xdescribe('checkForUpdate()', function () { - it('checks for an update', async function () { - if (Platform.ios) { - await firebase.appDistribution().checkForUpdate(); - } else { - this.skip(); - } - }); - }); - }); - describe('modular', function () { describe('native module is loaded', function () { it('checks native module load status', function () { diff --git a/packages/app-distribution/lib/index.ts b/packages/app-distribution/lib/index.ts index 1f2e71292f..b29b399dc3 100644 --- a/packages/app-distribution/lib/index.ts +++ b/packages/app-distribution/lib/index.ts @@ -15,9 +15,96 @@ * */ -export type { AppDistribution, AppDistributionRelease } from './types/app-distribution'; -export type { FirebaseAppDistributionTypes } from './types/namespaced'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { isIOS } from '@react-native-firebase/app/dist/module/common'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import { Platform } from 'react-native'; +import './types/internal'; +import type { AppDistribution, AppDistributionRelease } from './types/app-distribution'; +import type { AppDistributionInternal } from './types/internal'; +import { version } from './version'; + +const nativeModuleName = 'RNFBAppDistributionModule'; + +function rejectUnsupportedPlatform(): Promise { + return Promise.reject( + new Error(`App Distribution is not supported on the ${Platform.OS} platform.`), + ); +} + +class FirebaseAppDistributionModule extends FirebaseModule { + isTesterSignedIn(): Promise { + if (isIOS) { + return this.native.isTesterSignedIn(); + } + + return rejectUnsupportedPlatform(); + } + + signInTester(): Promise { + if (isIOS) { + return this.native.signInTester(); + } + + return rejectUnsupportedPlatform(); + } + + checkForUpdate(): Promise { + if (isIOS) { + return this.native.checkForUpdate(); + } + + return rejectUnsupportedPlatform(); + } + + signOutTester(): Promise { + if (isIOS) { + return this.native.signOutTester(); + } -export * from './modular'; -export * from './namespaced'; -export { default } from './namespaced'; + return rejectUnsupportedPlatform(); + } +} + +const config: ModuleConfig = { + namespace: 'appDistribution', + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, +}; + +export const SDK_VERSION = version; + +/** + * Returns the {@link AppDistribution} instance for the default or given {@link FirebaseApp}. + */ +export function getAppDistribution(app?: FirebaseApp): AppDistribution { + return getOrCreateModularInstance( + FirebaseAppDistributionModule, + config, + app, + ) as unknown as AppDistribution; +} + +export function isTesterSignedIn(appDistribution: AppDistribution): Promise { + return (appDistribution as AppDistributionInternal).isTesterSignedIn(); +} + +export function signInTester(appDistribution: AppDistribution): Promise { + return (appDistribution as AppDistributionInternal).signInTester(); +} + +export function checkForUpdate(appDistribution: AppDistribution): Promise { + return (appDistribution as AppDistributionInternal).checkForUpdate(); +} + +export function signOutTester(appDistribution: AppDistribution): Promise { + return (appDistribution as AppDistributionInternal).signOutTester(); +} + +export type { AppDistribution, AppDistributionRelease } from './types/app-distribution'; diff --git a/packages/app-distribution/lib/modular.ts b/packages/app-distribution/lib/modular.ts deleted file mode 100644 index 439eafdd4a..0000000000 --- a/packages/app-distribution/lib/modular.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import type { AppDistribution, AppDistributionRelease } from './types/app-distribution'; -import type { AppDistributionWithDeprecationArg } from './types/internal'; - -export type FirebaseAppDistribution = AppDistribution; - -function ap(appDistribution: FirebaseAppDistribution): AppDistributionWithDeprecationArg { - return appDistribution as AppDistributionWithDeprecationArg; -} - -export function getAppDistribution(app?: ReactNativeFirebase.FirebaseApp): FirebaseAppDistribution { - if (app) { - return getApp(app.name).appDistribution(); - } - - return getApp().appDistribution(); -} - -export function isTesterSignedIn(appDistribution: FirebaseAppDistribution): Promise { - return ap(appDistribution).isTesterSignedIn.call(appDistribution, MODULAR_DEPRECATION_ARG); -} - -export function signInTester(appDistribution: FirebaseAppDistribution): Promise { - return ap(appDistribution).signInTester.call(appDistribution, MODULAR_DEPRECATION_ARG); -} - -export function checkForUpdate( - appDistribution: FirebaseAppDistribution, -): Promise { - return ap(appDistribution).checkForUpdate.call(appDistribution, MODULAR_DEPRECATION_ARG); -} - -export function signOutTester(appDistribution: FirebaseAppDistribution): Promise { - return ap(appDistribution).signOutTester.call(appDistribution, MODULAR_DEPRECATION_ARG); -} diff --git a/packages/app-distribution/lib/namespaced.ts b/packages/app-distribution/lib/namespaced.ts deleted file mode 100644 index 29debe5b51..0000000000 --- a/packages/app-distribution/lib/namespaced.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import { isIOS } from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import { Platform } from 'react-native'; -import type { AppDistributionRelease } from './types/app-distribution'; -import type { FirebaseAppDistributionTypes } from './types/namespaced'; -import { version } from './version'; - -const statics = {}; - -const namespace = 'appDistribution'; - -const nativeModuleName = 'RNFBAppDistributionModule'; - -function rejectUnsupportedPlatform(): Promise { - return Promise.reject( - new Error(`App Distribution is not supported on the ${Platform.OS} platform.`), - ); -} - -class FirebaseAppDistributionModule extends FirebaseModule { - isTesterSignedIn(): Promise { - if (isIOS) { - return this.native.isTesterSignedIn(); - } - - return rejectUnsupportedPlatform(); - } - - signInTester(): Promise { - if (isIOS) { - return this.native.signInTester(); - } - - return rejectUnsupportedPlatform(); - } - - checkForUpdate(): Promise { - if (isIOS) { - return this.native.checkForUpdate(); - } - - return rejectUnsupportedPlatform(); - } - - signOutTester(): Promise { - if (isIOS) { - return this.native.signOutTester(); - } - - return rejectUnsupportedPlatform(); - } -} - -export const SDK_VERSION = version; - -const appDistributionNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: false, - hasMultiAppSupport: false, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseAppDistributionModule, -}) as unknown as ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseAppDistributionTypes.Module, - FirebaseAppDistributionTypes.Statics ->; - -export default appDistributionNamespace; - -// import appDistribution, { firebase } from '@react-native-firebase/app-distribution'; -// appDistribution().X(...); -// firebase.appDistribution().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'appDistribution', - FirebaseAppDistributionTypes.Module, - FirebaseAppDistributionTypes.Statics, - false - >; diff --git a/packages/app-distribution/lib/types/internal.ts b/packages/app-distribution/lib/types/internal.ts index 269fa956d5..35b37ff093 100644 --- a/packages/app-distribution/lib/types/internal.ts +++ b/packages/app-distribution/lib/types/internal.ts @@ -1,7 +1,5 @@ import type { AppDistribution, AppDistributionRelease } from './app-distribution'; -export type AppDistributionModularDeprecationArg = string; - export interface RNFBAppDistributionModule { isTesterSignedIn(): Promise; signInTester(): Promise; @@ -16,12 +14,9 @@ declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { } export interface AppDistributionInternal extends AppDistribution { + isTesterSignedIn(): Promise; + signInTester(): Promise; + checkForUpdate(): Promise; + signOutTester(): Promise; readonly native: RNFBAppDistributionModule; } - -export type AppDistributionWithDeprecationArg = AppDistributionInternal & { - isTesterSignedIn(_depArg: AppDistributionModularDeprecationArg): Promise; - signInTester(_depArg: AppDistributionModularDeprecationArg): Promise; - checkForUpdate(_depArg: AppDistributionModularDeprecationArg): Promise; - signOutTester(_depArg: AppDistributionModularDeprecationArg): Promise; -}; diff --git a/packages/app-distribution/lib/types/namespaced.ts b/packages/app-distribution/lib/types/namespaced.ts deleted file mode 100644 index cac856ed5a..0000000000 --- a/packages/app-distribution/lib/types/namespaced.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * @deprecated Use the exported types directly instead. - * FirebaseAppDistributionTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseAppDistributionTypes { - /** - * @deprecated Use the exported `AppDistributionRelease` type instead. - */ - export interface AppDistributionRelease { - /** @deprecated Use the exported `AppDistributionRelease` type instead. */ - displayVersion: string; - /** @deprecated Use the exported `AppDistributionRelease` type instead. */ - buildVersion: string; - /** @deprecated Use the exported `AppDistributionRelease` type instead. */ - releaseNotes: string | null; - /** @deprecated Use the exported `AppDistributionRelease` type instead. */ - downloadURL: string; - /** @deprecated Use the exported `AppDistributionRelease` type instead. */ - isExpired: boolean; - } - - /** - * @deprecated Use the default export statics instead. - */ - export interface Statics { - /** @deprecated Use the default export statics instead. */ - SDK_VERSION: string; - } - - /** - * @deprecated Use the exported `AppDistribution` type instead. - */ - export interface Module extends ReactNativeFirebase.FirebaseModule { - /** @deprecated Use the exported `AppDistribution` type instead. */ - app: ReactNativeFirebase.FirebaseApp; - /** @deprecated Use the exported `AppDistribution` type instead. */ - isTesterSignedIn(): Promise; - /** @deprecated Use the exported `AppDistribution` type instead. */ - signInTester(): Promise; - /** @deprecated Use the exported `AppDistribution` type instead. */ - checkForUpdate(): Promise; - /** @deprecated Use the exported `AppDistribution` type instead. */ - signOutTester(): Promise; - } -} - -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - interface Module { - appDistribution: FirebaseModuleWithStaticsAndApp< - FirebaseAppDistributionTypes.Module, - FirebaseAppDistributionTypes.Statics - >; - } - - interface FirebaseApp { - appDistribution(): FirebaseAppDistributionTypes.Module; - } - } -} -/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/app-distribution/type-test.ts b/packages/app-distribution/type-test.ts index 84fdf23cf9..7916065335 100644 --- a/packages/app-distribution/type-test.ts +++ b/packages/app-distribution/type-test.ts @@ -1,6 +1,4 @@ -import appDistribution, { - firebase, - FirebaseAppDistributionTypes, +import { type AppDistribution, type AppDistributionRelease, getAppDistribution, @@ -8,37 +6,10 @@ import appDistribution, { signInTester, checkForUpdate, signOutTester, + SDK_VERSION, } from '.'; -console.log(appDistribution().app.name); - -// checks module exists at root -console.log(firebase.appDistribution().app.name); - -// checks module exists at app level -console.log(firebase.app().appDistribution().app.name); - -// checks statics exist -console.log(firebase.appDistribution.SDK_VERSION); - -// checks statics exist on defaultExport -console.log(appDistribution.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks multi-app support exists -console.log(firebase.appDistribution(firebase.app()).app.name); - -// checks default export supports app arg -console.log(appDistribution(firebase.app()).app.name); - -const appDistributionInstance: AppDistribution = firebase.appDistribution(); -console.log(appDistributionInstance.app.name); -console.log(appDistributionInstance.isTesterSignedIn); -console.log(appDistributionInstance.signInTester); -console.log(appDistributionInstance.checkForUpdate); -console.log(appDistributionInstance.signOutTester); +console.log(SDK_VERSION); const sampleRelease: AppDistributionRelease = { displayVersion: '1.0.0', @@ -53,23 +24,8 @@ console.log(sampleRelease.releaseNotes); console.log(sampleRelease.downloadURL); console.log(sampleRelease.isExpired); -appDistributionInstance.isTesterSignedIn().then((signedIn: boolean) => { - console.log(signedIn); -}); -appDistributionInstance.signInTester().then(() => { - console.log('signed in'); -}); -appDistributionInstance.checkForUpdate().then((release: AppDistributionRelease) => { - console.log(release.displayVersion); -}); -appDistributionInstance.signOutTester().then(() => { - console.log('signed out'); -}); - const modularAppDistribution = getAppDistribution(); -const modularAppDistributionWithApp = getAppDistribution(firebase.app()); console.log(modularAppDistribution.app.name); -console.log(modularAppDistributionWithApp.app.name); isTesterSignedIn(modularAppDistribution).then((signedIn: boolean) => { console.log(signedIn); @@ -84,9 +40,5 @@ signOutTester(modularAppDistribution).then(() => { console.log('signed out via modular'); }); -const namespaceInstance: FirebaseAppDistributionTypes.Module = firebase.appDistribution(); -const namespaceRelease: FirebaseAppDistributionTypes.AppDistributionRelease = sampleRelease; -const namespaceStatics: FirebaseAppDistributionTypes.Statics = firebase.appDistribution; -console.log(namespaceInstance.app.name); -console.log(namespaceRelease.downloadURL); -console.log(namespaceStatics.SDK_VERSION); +const appDistributionInstance: AppDistribution = getAppDistribution(); +console.log(appDistributionInstance.app.name); diff --git a/packages/app/__tests__/app.test.ts b/packages/app/__tests__/app.test.ts index f95dcfa709..a21a03584b 100644 --- a/packages/app/__tests__/app.test.ts +++ b/packages/app/__tests__/app.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, jest } from '@jest/globals'; -import { checkV9Deprecation } from '../lib/common/unitTestUtils'; -import firebase, { +import { deleteApp, registerVersion, onLog, @@ -70,59 +69,6 @@ describe('App', function () { }); }); - describe('`console.warn` only called for non-modular API', function () { - it('deleteApp', function () { - // this test has a slightly special setup - // @ts-ignore test - jest.spyOn(getApp(), '_deleteApp').mockImplementation(() => Promise.resolve(null)); - checkV9Deprecation( - () => {}, // no modular replacement - () => getApp().delete(), // modular getApp(), then non-modular to check - ); - }); - - it('getApps', function () { - checkV9Deprecation( - () => getApps(), - () => firebase.apps, - ); - }); - - it('getApp', function () { - checkV9Deprecation( - () => getApp(), - () => firebase.app(), - ); - }); - - it('setLogLevel', function () { - checkV9Deprecation( - () => setLogLevel('debug'), - () => firebase.setLogLevel('debug'), - ); - }); - - it('FirebaseApp.toString()', function () { - checkV9Deprecation( - () => {}, // no modular replacement - () => getApp().toString(), // modular getApp(), then non-modular to check - ); - }); - - it('FirebaseApp.extendApp()', function () { - checkV9Deprecation( - // no modular replacement for this one so no modular func to send in - () => {}, - // modular getApp(), then non-modular to check - () => { - const app = getApp(); - (app as any).extendApp({ some: 'property' }); - return; - }, - ); - }); - }); - describe('`NativeFirebaseError` can cope with missing properties', function () { it('missing `userInfo.code` does not error', function () { const testNativeError = { diff --git a/packages/app/e2e/app.e2e.js b/packages/app/e2e/app.e2e.js index f825250b60..93bf5abc6f 100644 --- a/packages/app/e2e/app.e2e.js +++ b/packages/app/e2e/app.e2e.js @@ -16,180 +16,50 @@ */ describe('modular', function () { - describe('firebase v8 compat', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - + describe('firebase v9 modular', function () { it('it should allow read the default app from native', function () { if (Platform.other) return; // Not supported on non-native platforms. + const { getApp } = modular; + // app is created in tests app before all hook - should.equal(firebase.app()._nativeInitialized, true); - should.equal(firebase.app().name, '[DEFAULT]'); + should.equal(getApp()._nativeInitialized, true); + should.equal(getApp().name, '[DEFAULT]'); }); it('it should create js apps for natively initialized apps', function () { if (Platform.other) return; // Not supported on non-native platforms. - should.equal(firebase.app('secondaryFromNative')._nativeInitialized, true); - should.equal(firebase.app('secondaryFromNative').name, 'secondaryFromNative'); + const { getApp } = modular; + + should.equal(getApp('secondaryFromNative')._nativeInitialized, true); + should.equal(getApp('secondaryFromNative').name, 'secondaryFromNative'); }); it('natively initialized apps should have options available in js', function () { if (Platform.other) return; // Not supported on non-native platforms. + const { getApp } = modular; const platformAppConfig = FirebaseHelpers.app.config(); - should.equal(firebase.app().options.apiKey, platformAppConfig.apiKey); - should.equal(firebase.app().options.appId, platformAppConfig.appId); - should.equal(firebase.app().options.databaseURL, platformAppConfig.databaseURL); - should.equal(firebase.app().options.messagingSenderId, platformAppConfig.messagingSenderId); - should.equal(firebase.app().options.projectId, platformAppConfig.projectId); - should.equal(firebase.app().options.storageBucket, platformAppConfig.storageBucket); + should.equal(getApp().options.apiKey, platformAppConfig.apiKey); + should.equal(getApp().options.appId, platformAppConfig.appId); + should.equal(getApp().options.databaseURL, platformAppConfig.databaseURL); + should.equal(getApp().options.messagingSenderId, platformAppConfig.messagingSenderId); + should.equal(getApp().options.projectId, platformAppConfig.projectId); + should.equal(getApp().options.storageBucket, platformAppConfig.storageBucket); }); it('SDK_VERSION should return a string version', function () { - firebase.SDK_VERSION.should.be.a.String(); + modular.SDK_VERSION.should.be.a.String(); }); it('apps should provide an array of apps', function () { - should.equal(!!firebase.apps.length, true); - should.equal(firebase.apps.includes(firebase.app('[DEFAULT]')), true); + const { getApps, getApp } = modular; + should.equal(!!getApps().length, true); + should.equal(getApps().includes(getApp('[DEFAULT]')), true); }); it('apps can get and set data collection', async function () { - firebase.app().automaticDataCollectionEnabled = false; - should.equal(firebase.app().automaticDataCollectionEnabled, false); - }); - - it('should allow setting of log level', function () { - firebase.setLogLevel('error'); - firebase.setLogLevel('verbose'); - }); - - it('should error if logLevel is invalid', function () { - try { - firebase.setLogLevel('silent'); - throw new Error('did not throw on invalid loglevel'); - } catch (e) { - e.message.should.containEql('LogLevel must be one of'); - } - }); - - it('it should initialize dynamic apps', async function () { - const appCount = firebase.apps.length; - const name = `testscoreapp${FirebaseHelpers.id}`; - const platformAppConfig = FirebaseHelpers.app.config(); - const newApp = await firebase.initializeApp(platformAppConfig, name); - newApp.name.should.equal(name); - newApp.toString().should.equal(name); - newApp.options.apiKey.should.equal(platformAppConfig.apiKey); - should.equal(firebase.apps.includes(firebase.app(name)), true); - should.equal(firebase.apps.length, appCount + 1); - return newApp.delete(); - }); - - it('should error if dynamic app initialization values are incorrect', async function () { - const appCount = firebase.apps.length; - try { - await firebase.initializeApp({ appId: 'myid' }, 'myname'); - throw new Error('Should have rejected incorrect initializeApp input'); - } catch (e) { - e.message.should.equal("Missing or invalid FirebaseOptions property 'apiKey'."); - should.equal(firebase.apps.length, appCount); - should.equal(firebase.apps.includes('myname'), false); - } - }); - - it('should error if dynamic app initialization values are invalid', async function () { - // firebase-android-sdk and js-sdk will not complain on invalid initialization values, iOS throws - if (Platform.android || Platform.other) { - return; - } - - const appCount = firebase.apps.length; - try { - const firebaseConfig = { - apiKey: 'XXXXXXXXXXXXXXXXXXXXXXX', - authDomain: 'test-XXXXX.firebaseapp.com', - databaseURL: 'https://test-XXXXXX.firebaseio.com', - projectId: 'test-XXXXX', - storageBucket: 'tes-XXXXX.appspot.com', - messagingSenderId: 'XXXXXXXXXXXXX', - appId: '1:XXXXXXXXX', - app_name: 'TEST', - }; - await firebase.initializeApp(firebaseConfig, 'myname'); - throw new Error('Should have rejected incorrect initializeApp input'); - } catch (e) { - e.code.should.containEql('app/unknown'); - e.message.should.containEql('Configuration fails'); - should.equal(firebase.apps.length, appCount); - should.equal(firebase.apps.includes('myname'), false); - } - }); - - it('apps can be deleted, but only if it exists', async function () { - const name = `testscoreapp${FirebaseHelpers.id}`; - const platformAppConfig = FirebaseHelpers.app.config(); - const newApp = await firebase.initializeApp(platformAppConfig, name); - - newApp.name.should.equal(name); - newApp.toString().should.equal(name); - newApp.options.apiKey.should.equal(platformAppConfig.apiKey); - - await newApp.delete(); - try { - await newApp.delete(); - } catch (e) { - e.message.should.equal(`Firebase App named '${name}' already deleted`); - } - try { - firebase.app(name); - } catch (e) { - e.message.should.equal( - `No Firebase App '${name}' has been created - call firebase.initializeApp()`, - ); - } - }); - - it('prevents the default app from being deleted', async function () { - if (Platform.other) return; // We can delete the default app on other platforms. - try { - await firebase.app().delete(); - } catch (e) { - e.message.should.equal('Unable to delete the default native firebase app instance.'); - } - }); - - it('extendApp should provide additional functionality', function () { - const extension = {}; - firebase.app().extendApp({ - extension, - }); - firebase.app().extension.should.equal(extension); - }); - }); - - describe('firebase v9 modular', function () { - it('it should allow read the default app from native', function () { - if (Platform.other) return; // Not supported on non-native platforms. - const { getApp } = modular; - - // app is created in tests app before all hook - should.equal(getApp()._nativeInitialized, true); - should.equal(getApp().name, '[DEFAULT]'); - }); - - it('it should create js apps for natively initialized apps', function () { - if (Platform.other) return; // Not supported on non-native platforms. const { getApp } = modular; - - should.equal(getApp('secondaryFromNative')._nativeInitialized, true); - should.equal(getApp('secondaryFromNative').name, 'secondaryFromNative'); + getApp().automaticDataCollectionEnabled = false; + should.equal(getApp().automaticDataCollectionEnabled, false); }); it('should allow setting of log level', function () { @@ -320,5 +190,14 @@ describe('modular', function () { e.message.should.equal('registerVersion is only supported on Web'); } }); + + it('extendApp should provide additional functionality', function () { + const { getApp } = modular; + const extension = {}; + getApp().extendApp({ + extension, + }); + getApp().extension.should.equal(extension); + }); }); }); diff --git a/packages/app/e2e/asyncStorage.e2e.js b/packages/app/e2e/asyncStorage.e2e.js index 0148b2ba99..d62be84d79 100644 --- a/packages/app/e2e/asyncStorage.e2e.js +++ b/packages/app/e2e/asyncStorage.e2e.js @@ -9,7 +9,7 @@ import { prefix, } from '@react-native-firebase/app/dist/module/internal/asyncStorage'; -describe('firebase.setReactNativeAsyncStorage()', function () { +describe('setReactNativeAsyncStorage()', function () { beforeEach(async function () { setReactNativeAsyncStorageInternal(); await asyncStorage.clear(); diff --git a/packages/app/e2e/utils.e2e.js b/packages/app/e2e/utils.e2e.js index ec477fd4b7..9d3bb4fb9f 100644 --- a/packages/app/e2e/utils.e2e.js +++ b/packages/app/e2e/utils.e2e.js @@ -18,33 +18,23 @@ describe('utils()', function () { if (Platform.other) return; // Not supported on non-native platforms. - describe('namespace', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.utils); - app.utils().app.should.equal(app); + describe('modular', function () { + it('getUtils returns instance for default app', function () { + const { getUtils, getApp } = modular; + getUtils().app.should.equal(getApp()); }); describe('isRunningInTestLab', function () { it('returns true or false', function () { - should.equal(firebase.utils().isRunningInTestLab, false); + const { getUtils } = modular; + should.equal(getUtils().isRunningInTestLab, false); }); }); describe('playServicesAvailability', function () { it('returns isAvailable and Play Service status', async function () { - const playService = await firebase.utils().playServicesAvailability; - //iOS always returns { isAvailable: true, status: 0} + const { getUtils } = modular; + const playService = await getUtils().playServicesAvailability; should(playService.isAvailable).equal(true); should(playService.status).equal(0); }); @@ -52,8 +42,8 @@ describe('utils()', function () { describe('getPlayServicesStatus', function () { it('returns isAvailable and Play Service status', async function () { - const status = await firebase.utils().getPlayServicesStatus(); - //iOS always returns { isAvailable: true, status: 0} + const { getUtils } = modular; + const status = await getUtils().getPlayServicesStatus(); should(status.isAvailable).equal(true); should(status.status).equal(0); }); diff --git a/packages/app/e2e/utilsStatics.e2e.js b/packages/app/e2e/utilsStatics.e2e.js index e4365e694c..7e028a91cd 100644 --- a/packages/app/e2e/utilsStatics.e2e.js +++ b/packages/app/e2e/utilsStatics.e2e.js @@ -18,36 +18,35 @@ describe('utils()', function () { if (Platform.other) return; // Not supported on non-native platforms. - describe('statics', function () { - it('provides native path strings', function () { - firebase.utils.FilePath.should.be.an.Object(); + describe('modular', function () { + it('provides native path strings via FilePath export', function () { + const { FilePath } = modular; + FilePath.should.be.an.Object(); if (Platform.ios) { - firebase.utils.FilePath.MAIN_BUNDLE.should.be.a.String(); + FilePath.MAIN_BUNDLE.should.be.a.String(); } else { - should.equal(firebase.utils.FilePath.MAIN_BUNDLE, null); + should.equal(FilePath.MAIN_BUNDLE, null); } - firebase.utils.FilePath.CACHES_DIRECTORY.should.be.a.String(); - firebase.utils.FilePath.DOCUMENT_DIRECTORY.should.be.a.String(); + FilePath.CACHES_DIRECTORY.should.be.a.String(); + FilePath.DOCUMENT_DIRECTORY.should.be.a.String(); - // Even on android it may be null, skip if so - if (Platform.android && firebase.utils.FilePath.EXTERNAL_DIRECTORY !== null) { - firebase.utils.FilePath.EXTERNAL_DIRECTORY.should.be.a.String(); + if (Platform.android && FilePath.EXTERNAL_DIRECTORY !== null) { + FilePath.EXTERNAL_DIRECTORY.should.be.a.String(); } else { - should.equal(firebase.utils.FilePath.EXTERNAL_DIRECTORY, null); + should.equal(FilePath.EXTERNAL_DIRECTORY, null); } - // Even on android it may be null, skip if so - if (Platform.android && firebase.utils.FilePath.EXTERNAL_STORAGE_DIRECTORY !== null) { - firebase.utils.FilePath.EXTERNAL_STORAGE_DIRECTORY.should.be.a.String(); + if (Platform.android && FilePath.EXTERNAL_STORAGE_DIRECTORY !== null) { + FilePath.EXTERNAL_STORAGE_DIRECTORY.should.be.a.String(); } else { - should.equal(firebase.utils.FilePath.EXTERNAL_STORAGE_DIRECTORY, null); + should.equal(FilePath.EXTERNAL_STORAGE_DIRECTORY, null); } - firebase.utils.FilePath.TEMP_DIRECTORY.should.be.a.String(); - firebase.utils.FilePath.LIBRARY_DIRECTORY.should.be.a.String(); - firebase.utils.FilePath.PICTURES_DIRECTORY.should.be.a.String(); - firebase.utils.FilePath.MOVIES_DIRECTORY.should.be.a.String(); + FilePath.TEMP_DIRECTORY.should.be.a.String(); + FilePath.LIBRARY_DIRECTORY.should.be.a.String(); + FilePath.PICTURES_DIRECTORY.should.be.a.String(); + FilePath.MOVIES_DIRECTORY.should.be.a.String(); }); }); }); diff --git a/packages/app/lib/FirebaseApp.ts b/packages/app/lib/FirebaseApp.ts index 247c538081..540d86a00f 100644 --- a/packages/app/lib/FirebaseApp.ts +++ b/packages/app/lib/FirebaseApp.ts @@ -14,9 +14,8 @@ * limitations under the License. * */ -import { warnIfNotModularCall } from './common'; import { getAppModule } from './internal/registry/nativeModule'; -import type { ReactNativeFirebase, Utils } from './types/app'; +import type { ReactNativeFirebase } from './types/app'; export default class FirebaseApp implements ReactNativeFirebase.FirebaseAppBase { private _name: string; @@ -75,24 +74,16 @@ export default class FirebaseApp implements ReactNativeFirebase.FirebaseAppBase } extendApp(extendedProps: Record): void { - warnIfNotModularCall(arguments); this._checkDestroyed(); Object.assign(this, extendedProps); } delete(): Promise { - warnIfNotModularCall(arguments, 'deleteApp()'); this._checkDestroyed(); return this._deleteApp(); } toString(): string { - warnIfNotModularCall(arguments, '.name property'); return this.name; } - - // For backward compatibility - utils method added by registry - utils(): Utils.Module { - throw new Error('utils() should be added by registry'); - } } diff --git a/packages/app/lib/common/index.ts b/packages/app/lib/common/index.ts index bab9882c54..d11578fcbe 100644 --- a/packages/app/lib/common/index.ts +++ b/packages/app/lib/common/index.ts @@ -124,745 +124,3 @@ export function parseListenerOrObserver( throw new Error("'listenerOrObserver' expected a function or an object with 'next' function."); } - -// Used to indicate if there is no corresponding modular function -const NO_REPLACEMENT = true; - -type MethodMap = Record; -type InstanceMap = Record; -type DeprecationMap = Record; - -const mapOfDeprecationReplacements: DeprecationMap = { - analytics: { - default: { - logEvent: 'logEvent()', - setAnalyticsCollectionEnabled: 'setAnalyticsCollectionEnabled()', - setSessionTimeoutDuration: 'setSessionTimeoutDuration()', - getAppInstanceId: 'getAppInstanceId()', - getSessionId: 'getSessionId()', - setUserId: 'setUserId()', - setUserProperty: 'setUserProperty()', - setUserProperties: 'setUserProperties()', - resetAnalyticsData: 'resetAnalyticsData()', - setDefaultEventParameters: 'setDefaultEventParameters()', - initiateOnDeviceConversionMeasurementWithEmailAddress: - 'initiateOnDeviceConversionMeasurementWithEmailAddress()', - initiateOnDeviceConversionMeasurementWithHashedEmailAddress: - 'initiateOnDeviceConversionMeasurementWithHashedEmailAddress()', - initiateOnDeviceConversionMeasurementWithPhoneNumber: - 'initiateOnDeviceConversionMeasurementWithPhoneNumber()', - initiateOnDeviceConversionMeasurementWithHashedPhoneNumber: - 'initiateOnDeviceConversionMeasurementWithHashedPhoneNumber()', - setConsent: 'setConsent()', - // We're deprecating all helper methods for event. e.g. `logAddPaymentInfo()` from namespaced and modular. - logAddPaymentInfo: 'logEvent()', - logScreenView: 'logEvent()', - logAddShippingInfo: 'logEvent()', - logAddToCart: 'logEvent()', - logAddToWishlist: 'logEvent()', - logAppOpen: 'logEvent()', - logBeginCheckout: 'logEvent()', - logCampaignDetails: 'logEvent()', - logEarnVirtualCurrency: 'logEvent()', - logGenerateLead: 'logEvent()', - logJoinGroup: 'logEvent()', - logLevelEnd: 'logEvent()', - logLevelStart: 'logEvent()', - logLevelUp: 'logEvent()', - logLogin: 'logEvent()', - logPostScore: 'logEvent()', - logSelectContent: 'logEvent()', - logPurchase: 'logEvent()', - logRefund: 'logEvent()', - logRemoveFromCart: 'logEvent()', - logSearch: 'logEvent()', - logSelectItem: 'logEvent()', - logSetCheckoutOption: 'logEvent()', - logSelectPromotion: 'logEvent()', - logShare: 'logEvent()', - logSignUp: 'logEvent()', - logSpendVirtualCurrency: 'logEvent()', - logTutorialBegin: 'logEvent()', - logTutorialComplete: 'logEvent()', - logUnlockAchievement: 'logEvent()', - logViewCart: 'logEvent()', - logViewItem: 'logEvent()', - logViewPromotion: 'logEvent()', - logViewSearchResults: 'logEvent()', - }, - }, - appCheck: { - default: { - activate: 'initializeAppCheck()', - setTokenAutoRefreshEnabled: 'setTokenAutoRefreshEnabled()', - getToken: 'getToken()', - getLimitedUseToken: 'getLimitedUseToken()', - onTokenChanged: 'onTokenChanged()', - }, - statics: { - CustomProvider: 'CustomProvider', - }, - }, - appDistribution: { - default: { - isTesterSignedIn: 'isTesterSignedIn()', - signInTester: 'signInTester()', - checkForUpdate: 'checkForUpdate()', - signOutTester: 'signOutTester()', - }, - }, - auth: { - default: { - applyActionCode: 'applyActionCode()', - checkActionCode: 'checkActionCode()', - confirmPasswordReset: 'confirmPasswordReset()', - createUserWithEmailAndPassword: 'createUserWithEmailAndPassword()', - fetchSignInMethodsForEmail: 'fetchSignInMethodsForEmail()', - getMultiFactorResolver: 'getMultiFactorResolver()', - isSignInWithEmailLink: 'isSignInWithEmailLink()', - onAuthStateChanged: 'onAuthStateChanged()', - onIdTokenChanged: 'onIdTokenChanged()', - sendPasswordResetEmail: 'sendPasswordResetEmail()', - sendSignInLinkToEmail: 'sendSignInLinkToEmail()', - signInAnonymously: 'signInAnonymously()', - signInWithCredential: 'signInWithCredential()', - signInWithCustomToken: 'signInWithCustomToken()', - signInWithEmailAndPassword: 'signInWithEmailAndPassword()', - signInWithEmailLink: 'signInWithEmailLink()', - signInWithPhoneNumber: 'signInWithPhoneNumber()', - signInWithRedirect: 'signInWithRedirect()', - signInWithPopup: 'signInWithPopup()', - signOut: 'signOut()', - useUserAccessGroup: 'useUserAccessGroup()', - verifyPasswordResetCode: 'verifyPasswordResetCode()', - getCustomAuthDomain: 'getCustomAuthDomain()', - useEmulator: 'connectAuthEmulator()', - setLanguageCode: 'useDeviceLanguage()', - multiFactor: 'multiFactor()', - useDeviceLanguage: 'useDeviceLanguage()', - updateCurrentUser: 'updateCurrentUser()', - validatePassword: 'validatePassword()', - }, - User: { - delete: 'deleteUser()', - getIdToken: 'getIdToken()', - getIdTokenResult: 'getIdTokenResult()', - linkWithCredential: 'linkWithCredential()', - linkWithPopup: 'linkWithPopup()', - linkWithRedirect: 'linkWithRedirect()', - reauthenticateWithCredential: 'reauthenticateWithCredential()', - reauthenticateWithPopup: 'reauthenticateWithPopup()', - reauthenticateWithRedirect: 'reauthenticateWithRedirect()', - reload: 'reload()', - sendEmailVerification: 'sendEmailVerification()', - toJSON: NO_REPLACEMENT, - unlink: 'unlink()', - updateEmail: 'updateEmail()', - updatePassword: 'updatePassword()', - updatePhoneNumber: 'updatePhoneNumber()', - updateProfile: 'updateProfile()', - verifyBeforeUpdateEmail: 'verifyBeforeUpdateEmail()', - }, - statics: { - AppleAuthProvider: 'AppleAuthProvider', - EmailAuthProvider: 'EmailAuthProvider', - PhoneAuthProvider: 'PhoneAuthProvider', - GoogleAuthProvider: 'GoogleAuthProvider', - GithubAuthProvider: 'GithubAuthProvider', - TwitterAuthProvider: 'TwitterAuthProvider', - FacebookAuthProvider: 'FacebookAuthProvider', - PhoneMultiFactorGenerator: 'PhoneMultiFactorGenerator', - OAuthProvider: 'OAuthProvider', - OIDCAuthProvider: 'OIDCAuthProvider', - PhoneAuthState: 'PhoneAuthState', - getMultiFactorResolver: 'getMultiFactorResolver()', - multiFactor: 'multiFactor()', - }, - }, - crashlytics: { - default: { - checkForUnsentReports: 'checkForUnsentReports()', - crash: 'crash()', - deleteUnsentReports: 'deleteUnsentReports()', - didCrashOnPreviousExecution: 'didCrashOnPreviousExecution()', - log: 'log()', - setAttribute: 'setAttribute()', - setAttributes: 'setAttributes()', - setUserId: 'setUserId()', - recordError: 'recordError()', - sendUnsentReports: 'sendUnsentReports()', - setCrashlyticsCollectionEnabled: 'setCrashlyticsCollectionEnabled()', - }, - }, - - database: { - default: { - useEmulator: 'connectDatabaseEmulator()', - goOffline: 'goOffline()', - goOnline: 'goOnline()', - ref: 'ref()', - refFromURL: 'refFromURL()', - setPersistenceEnabled: 'setPersistenceEnabled()', - setLoggingEnabled: 'setLoggingEnabled()', - setPersistenceCacheSizeBytes: 'setPersistenceCacheSizeBytes()', - getServerTime: 'getServerTime()', - }, - statics: { - ServerValue: 'ServerValue', - }, - DatabaseReference: { - child: 'child()', - set: 'set()', - update: 'update()', - setWithPriority: 'setWithPriority()', - remove: 'remove()', - on: 'onValue()', - once: 'get()', - endAt: 'endAt()', - endBefore: 'endBefore()', - startAt: 'startAt()', - startAfter: 'startAfter()', - limitToFirst: 'limitToFirst()', - limitToLast: 'limitToLast()', - orderByChild: 'orderByChild()', - orderByKey: 'orderByKey()', - orderByValue: 'orderByValue()', - equalTo: 'equalTo()', - setPriority: 'setPriority()', - push: 'push()', - onDisconnect: 'onDisconnect()', - keepSynced: 'keepSynced()', - transaction: 'runTransaction()', - }, - }, - firestore: { - default: { - batch: 'writeBatch()', - loadBundle: 'loadBundle()', - namedQuery: 'namedQuery()', - clearPersistence: 'clearIndexedDbPersistence()', - waitForPendingWrites: 'waitForPendingWrites()', - terminate: 'terminate()', - useEmulator: 'connectFirestoreEmulator()', - collection: 'collection()', - collectionGroup: 'collectionGroup()', - disableNetwork: 'disableNetwork()', - doc: 'doc()', - enableNetwork: 'enableNetwork()', - runTransaction: 'runTransaction()', - settings: 'settings()', - persistentCacheIndexManager: 'getPersistentCacheIndexManager()', - }, - statics: { - setLogLevel: 'setLogLevel()', - Filter: 'where()', - FieldValue: 'FieldValue', - Timestamp: 'Timestamp', - GeoPoint: 'GeoPoint', - Blob: 'Bytes', - FieldPath: 'FieldPath', - }, - CollectionReference: { - count: 'getCountFromServer()', - countFromServer: 'getCountFromServer()', - endAt: 'endAt()', - endBefore: 'endBefore()', - get: 'getDocs()', - isEqual: NO_REPLACEMENT, - limit: 'limit()', - limitToLast: 'limitToLast()', - onSnapshot: 'onSnapshot()', - orderBy: 'orderBy()', - startAfter: 'startAfter()', - startAt: 'startAt()', - where: 'where()', - add: 'addDoc()', - doc: 'doc()', - }, - DocumentReference: { - collection: 'collection()', - delete: 'deleteDoc()', - get: 'getDoc()', - isEqual: NO_REPLACEMENT, - onSnapshot: 'onSnapshot()', - set: 'setDoc()', - update: 'updateDoc()', - }, - DocumentSnapshot: { - isEqual: NO_REPLACEMENT, - }, - FieldValue: { - arrayRemove: 'arrayRemove()', - arrayUnion: 'arrayUnion()', - delete: 'deleteField()', - increment: 'increment()', - serverTimestamp: 'serverTimestamp()', - }, - Filter: { - or: 'or()', - and: 'and()', - }, - PersistentCacheIndexManager: { - enableIndexAutoCreation: 'enablePersistentCacheIndexAutoCreation()', - disableIndexAutoCreation: 'disablePersistentCacheIndexAutoCreation()', - deleteAllIndexes: 'deleteAllPersistentCacheIndexes()', - }, - Timestamp: { - seconds: NO_REPLACEMENT, - nanoseconds: NO_REPLACEMENT, - }, - }, - functions: { - default: { - useEmulator: 'connectFirestoreEmulator()', - httpsCallable: 'httpsCallable()', - httpsCallableFromUrl: 'httpsCallableFromUrl()', - }, - statics: { - HttpsErrorCode: 'HttpsErrorCode', - }, - }, - installations: { - default: { - delete: 'deleteInstallations()', - getId: 'getId()', - getToken: 'getToken()', - }, - }, - inAppMessaging: { - default: { - isMessagesDisplaySuppressed: 'isMessagesDisplaySuppressed()', - setMessagesDisplaySuppressed: 'setMessagesDisplaySuppressed()', - isAutomaticDataCollectionEnabled: 'isAutomaticDataCollectionEnabled()', - setAutomaticDataCollectionEnabled: 'setAutomaticDataCollectionEnabled()', - triggerEvent: 'triggerEvent()', - }, - }, - messaging: { - default: { - isAutoInitEnabled: 'isAutoInitEnabled()', - isDeviceRegisteredForRemoteMessages: 'isDeviceRegisteredForRemoteMessages()', - isNotificationDelegationEnabled: 'isNotificationDelegationEnabled()', - isDeliveryMetricsExportToBigQueryEnabled: 'isDeliveryMetricsExportToBigQueryEnabled()', - setAutoInitEnabled: 'setAutoInitEnabled()', - getInitialNotification: 'getInitialNotification()', - getDidOpenSettingsForNotification: 'getDidOpenSettingsForNotification()', - getIsHeadless: 'getIsHeadless()', - onNotificationOpenedApp: 'onNotificationOpenedApp()', - onTokenRefresh: 'onTokenRefresh()', - requestPermission: 'requestPermission()', - registerDeviceForRemoteMessages: 'registerDeviceForRemoteMessages()', - unregisterDeviceForRemoteMessages: 'unregisterDeviceForRemoteMessages()', - getAPNSToken: 'getAPNSToken()', - setAPNSToken: 'setAPNSToken()', - hasPermission: 'hasPermission()', - onDeletedMessages: 'onDeletedMessages()', - onMessageSent: 'onMessageSent()', - onSendError: 'onSendError()', - setBackgroundMessageHandler: 'setBackgroundMessageHandler()', - setOpenSettingsForNotificationsHandler: 'setOpenSettingsForNotificationsHandler()', - sendMessage: 'sendMessage()', - subscribeToTopic: 'subscribeToTopic()', - unsubscribeFromTopic: 'unsubscribeFromTopic()', - setNotificationDelegationEnabled: 'setNotificationDelegationEnabled()', - // Actual firebase-js-sdk methods - getToken: 'getToken()', - deleteToken: 'deleteToken()', - onMessage: 'onMessage()', - isSupported: 'isSupported()', - setDeliveryMetricsExportToBigQuery: - 'experimentalSetDeliveryMetricsExportedToBigQueryEnabled()', - }, - statics: { - AuthorizationStatus: 'AuthorizationStatus', - NotificationAndroidPriority: 'NotificationAndroidPriority', - NotificationAndroidVisibility: 'NotificationAndroidVisibility', - }, - }, - perf: { - default: { - setPerformanceCollectionEnabled: 'initializePerformance()', - newTrace: 'trace()', - newHttpMetric: 'httpMetric()', - newScreenTrace: 'newScreenTrace()', - startScreenTrace: 'startScreenTrace()', - }, - }, - remoteConfig: { - default: { - activate: 'activate()', - ensureInitialized: 'ensureInitialized()', - fetchAndActivate: 'fetchAndActivate()', - getAll: 'getAll()', - getBoolean: 'getBoolean()', - getNumber: 'getNumber()', - getString: 'getString()', - getValue: 'getValue()', - reset: 'reset()', - setConfigSettings: 'setConfigSettings()', - fetch: 'fetch()', - setDefaults: 'setDefaults()', - setDefaultsFromResource: 'setDefaultsFromResource()', - onConfigUpdated: 'onConfigUpdated()', - }, - statics: { - LastFetchStatus: 'LastFetchStatus', - ValueSource: 'ValueSource', - }, - }, - storage: { - default: { - useEmulator: 'connectStorageEmulator()', - ref: 'ref()', - refFromURL: 'refFromURL()', - setMaxOperationRetryTime: 'setMaxOperationRetryTime()', - setMaxUploadRetryTime: 'setMaxUploadRetryTime()', - setMaxDownloadRetryTime: 'setMaxDownloadRetryTime()', - }, - Reference: { - delete: 'deleteObject()', - getDownloadURL: 'getDownloadURL()', - getMetadata: 'getMetadata()', - list: 'list()', - listAll: 'listAll()', - updateMetadata: 'updateMetadata()', - put: 'uploadBytesResumable()', - putString: 'uploadString()', - putFile: 'putFile()', - writeToFile: 'writeToFile()', - toString: 'toString()', - child: 'child()', - }, - statics: { - StringFormat: 'StringFormat', - TaskEvent: 'TaskEvent', - TaskState: 'TaskState', - }, - }, -}; - -const modularDeprecationMessage = - 'This method is deprecated (as well as all React Native Firebase namespaced API) and will be removed in the next major release ' + - 'as part of move to match Firebase Web modular SDK API. Please see migration guide for more details: https://rnfirebase.io/migrating-to-v22'; - -export function deprecationConsoleWarning( - nameSpace: string, - methodName: string, - instanceName: string, - isModularMethod: boolean, -): void { - if (!isModularMethod) { - const moduleMap = mapOfDeprecationReplacements[nameSpace]; - if (moduleMap) { - const instanceMap = moduleMap[instanceName]; - const deprecatedMethod = instanceMap?.[methodName]; - if (instanceMap && deprecatedMethod) { - if (!globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS) { - // eslint-disable-next-line no-console - console.warn(createMessage(nameSpace, methodName, instanceName)); - - if (globalThis.RNFB_MODULAR_DEPRECATION_STRICT_MODE === true) { - throw new Error('Deprecated API usage detected while in strict mode.'); - } - } - } - } - } -} - -export function createMessage( - nameSpace: string, - methodName: string, - instanceName: string = 'default', - uniqueMessage: string | null = null, -): string | undefined { - if (uniqueMessage) { - // Unique deprecation message used for testing - return uniqueMessage; - } - - const moduleMap = mapOfDeprecationReplacements[nameSpace]; - if (moduleMap) { - const instance = moduleMap[instanceName]; - if (instance) { - const replacementMethodName = instance[methodName]; - - if (replacementMethodName !== NO_REPLACEMENT) { - return ( - modularDeprecationMessage + - `. Method called was \`${methodName}\`. Please use \`${replacementMethodName}\` instead.` - ); - } else { - return modularDeprecationMessage + `. Method called was \`${methodName}\``; - } - } - } - return undefined; -} - -function getNamespace(target: any): string | undefined { - if (target.constructor.name === 'DatabaseReference') { - return 'database'; - } - if (target.GeoPoint || target.CustomProvider) { - // target is statics object. GeoPoint - Firestore, CustomProvider - AppCheck - return 'firestore'; - } - if (target._config && target._config.namespace) { - return target._config.namespace; - } - if (target.constructor.name === 'Reference') { - return 'storage'; - } - const className = target.name ? target.name : target.constructor.name; - return Object.keys(mapOfDeprecationReplacements).find(key => { - if (mapOfDeprecationReplacements[key]?.[className]) { - return key; - } - return false; - }); -} - -function getInstanceName(target: any): string { - if (target.GeoPoint || target.CustomProvider) { - // target is statics object. GeoPoint - Firestore, CustomProvider - AppCheck - return 'statics'; - } - if (target.ServerValue) { - return 'statics'; - } - if (target._config) { - // module class instance, we use default to store map of deprecated methods - return 'default'; - } - - if (target.constructor.name === 'Reference') { - // if path passed into ref(), it will pass in the arg as target.name - return target.constructor.name; - } - if (target.name) { - // It's a function which has a name property unlike classes - return target.name; - } - // It's a class instance - return target.constructor.name; -} - -const MODULAR_INSTANCE_SYMBOL = Symbol('react-native-firebase-modular-instance'); - -function isObjectLike(value: unknown): value is object | ((...args: any[]) => unknown) { - return (typeof value === 'object' && value !== null) || typeof value === 'function'; -} - -function isModularInstance(value: unknown): boolean { - if (!isObjectLike(value)) { - return false; - } - - return Object.getOwnPropertyDescriptor(value, MODULAR_INSTANCE_SYMBOL)?.value === true; -} - -function markModularInstance(value: T): T { - if (!isObjectLike(value) || isModularInstance(value)) { - return value; - } - - Object.defineProperty(value, MODULAR_INSTANCE_SYMBOL, { - value: true, - configurable: true, - enumerable: false, - writable: false, - }); - - return value; -} - -function isModularAccess(target: unknown, receiver?: unknown): boolean { - return _isModularCall || isModularInstance(target) || isModularInstance(receiver); -} - -export function createDeprecationProxy(instance: T): T { - return new Proxy(instance, { - construct(target: any, args: any[]) { - // needed for Timestamp which we pass as static, when we construct new instance, we need to wrap it in proxy again. - return createDeprecationProxy(new target(...args)); - }, - get(target: any, prop: string | symbol, receiver: any) { - const originalMethod = target[prop]; - const modularAccess = isModularAccess(target, receiver); - - if (prop === 'constructor') { - return Reflect.get(target, prop, receiver); - } - - if (target && target.constructor && target.constructor.name === 'Timestamp') { - deprecationConsoleWarning('firestore', prop as string, 'Timestamp', modularAccess); - return Reflect.get(target, prop, receiver); - } - - if (target && target.name === 'firebaseModuleWithApp') { - // statics - if ( - prop === 'Filter' || - prop === 'FieldValue' || - prop === 'Timestamp' || - prop === 'GeoPoint' || - prop === 'Blob' || - prop === 'FieldPath' - ) { - deprecationConsoleWarning('firestore', prop, 'statics', false); - } - if (prop === 'LastFetchStatus' || prop === 'ValueSource') { - deprecationConsoleWarning('remoteConfig', prop, 'statics', false); - } - if (prop === 'CustomProvider') { - deprecationConsoleWarning('appCheck', prop, 'statics', false); - } - if (prop === 'StringFormat' || prop === 'TaskEvent' || prop === 'TaskState') { - deprecationConsoleWarning('storage', prop, 'statics', false); - } - - if ( - prop === 'PhoneAuthState' || - prop === 'AppleAuthProvider' || - prop === 'PhoneAuthProvider' || - prop === 'GoogleAuthProvider' || - prop === 'GithubAuthProvider' || - prop === 'TwitterAuthProvider' || - prop === 'FacebookAuthProvider' || - prop === 'OAuthProvider' || - prop === 'OIDCAuthProvider' || - prop === 'PhoneMultiFactorGenerator' || - prop === 'EmailAuthProvider' || - prop === 'multiFactor' || - prop === 'getMultiFactorResolver' - ) { - deprecationConsoleWarning('auth', prop, 'statics', false); - } - - if ( - prop === 'AuthorizationStatus' || - prop === 'NotificationAndroidPriority' || - prop === 'NotificationAndroidVisibility' - ) { - deprecationConsoleWarning('messaging', prop, 'statics', false); - } - if (prop === 'ServerValue') { - deprecationConsoleWarning('database', prop, 'statics', false); - } - - if (prop !== 'setLogLevel') { - // we want to capture setLogLevel function call which we do below - return Reflect.get(target, prop, receiver); - } - } - - // Check if it's a getter/setter first - const descriptor = - Object.getOwnPropertyDescriptor(target, prop) || - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop); - - if (descriptor && (descriptor.get || descriptor.set)) { - const instanceName = getInstanceName(target); - const nameSpace = getNamespace(target); - - if (descriptor.get && nameSpace) { - // Handle getter - call it and show deprecation warning - deprecationConsoleWarning(nameSpace, prop as string, instanceName, modularAccess); - const result = descriptor.get.call(target); - return modularAccess ? markModularInstance(result) : result; - } - - if (descriptor.set && nameSpace) { - // Handle setter - return a function that calls the setter with deprecation warning - return function (value: any) { - deprecationConsoleWarning(nameSpace, prop as string, instanceName, modularAccess); - descriptor.set!.call(target, value); - }; - } - } - - if (typeof originalMethod === 'function') { - return function (...args: any[]) { - const isModularMethod = modularAccess || args.includes(MODULAR_DEPRECATION_ARG); - const instanceName = getInstanceName(target); - const nameSpace = getNamespace(target); - - if (nameSpace) { - deprecationConsoleWarning(nameSpace, prop as string, instanceName, isModularMethod); - } - - const result = originalMethod.apply(target, filterModularArgument(args)); - return isModularMethod ? markModularInstance(result) : result; - }; - } - return Reflect.get(target, prop, receiver); - }, - set(target: any, prop: string | symbol, value: unknown, receiver: unknown) { - const modularAccess = isModularAccess(target, receiver); - const descriptor = - Object.getOwnPropertyDescriptor(target, prop) || - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop); - - if (descriptor?.set) { - const instanceName = getInstanceName(target); - const nameSpace = getNamespace(target); - - if (nameSpace) { - deprecationConsoleWarning(nameSpace, prop as string, instanceName, modularAccess); - } - - descriptor.set.call(target, value); - return true; - } - - return Reflect.set(target, prop, value, receiver); - }, - }); -} - -export const MODULAR_DEPRECATION_ARG = 'react-native-firebase-modular-method-call'; - -// Flag to track if we're currently in a modular call -let _isModularCall = false; - -export function withModularFlag(fn: () => T): T { - const previousFlag = _isModularCall; - _isModularCall = true; - try { - return fn(); - } finally { - _isModularCall = previousFlag; - } -} - -export function filterModularArgument(list: any[]): any[] { - return list.filter(arg => arg !== MODULAR_DEPRECATION_ARG); -} - -export function warnIfNotModularCall(args: IArguments, replacementMethodName: string = ''): void { - for (let i = 0; i < args.length; i++) { - if (args[i] === MODULAR_DEPRECATION_ARG) { - return; - } - } - - let message = modularDeprecationMessage; - if (replacementMethodName.length > 0) { - message += ` Please use \`${replacementMethodName}\` instead.`; - } - - if (!globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS) { - // eslint-disable-next-line no-console - console.warn(message); - - if (globalThis.RNFB_MODULAR_DEPRECATION_STRICT_MODE === true) { - throw new Error('Deprecated API usage detected while in strict mode.'); - } - } -} - -export function isModularCall(args: IArguments): boolean { - const argsArray = Array.from(args); - return argsArray.includes(MODULAR_DEPRECATION_ARG); -} diff --git a/packages/app/lib/common/unitTestUtils.ts b/packages/app/lib/common/unitTestUtils.ts index 3011293786..ee7aef2bc5 100644 --- a/packages/app/lib/common/unitTestUtils.ts +++ b/packages/app/lib/common/unitTestUtils.ts @@ -1,74 +1,18 @@ -import { expect, jest } from '@jest/globals'; -import { createMessage } from './index'; +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ -export const checkV9Deprecation = (modularFunction: () => void, nonModularFunction: () => void) => { - const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - consoleWarnSpy.mockRestore(); - modularFunction(); - expect(consoleWarnSpy).not.toHaveBeenCalled(); - consoleWarnSpy.mockClear(); - const consoleWarnSpy2 = jest.spyOn(console, 'warn').mockImplementation(() => {}); - nonModularFunction(); - - expect(consoleWarnSpy2).toHaveBeenCalledTimes(1); - consoleWarnSpy2.mockClear(); -}; - -export type CheckV9DeprecationFunction = ( - modularFunction: () => void, - nonModularFunction: () => void, - methodNameKey: string, - uniqueMessage?: string | null, - ignoreFirebaseAppDeprecationWarning?: boolean, -) => void; - -export const withDeprecationWarningsSilenced = (fn: () => T): T => { - const previous = globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS; - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - try { - return fn(); - } finally { - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = previous; - } -}; - -export const createCheckV9Deprecation = (moduleNames: string[]): CheckV9DeprecationFunction => { - return ( - modularFunction: () => void, - nonModularFunction: () => void, - methodNameKey: string, - uniqueMessage?: string | null, - checkFirebaseAppDeprecationWarning: boolean = false, - ) => { - const moduleName = moduleNames[0] as string; // firestore, database, etc - const instanceName = moduleNames[1] || 'default'; // default, FirestoreCollectionReference, etc - const consoleWarnSpy = jest.spyOn(console, 'warn'); - consoleWarnSpy.mockReset(); - - consoleWarnSpy.mockImplementation(warnMessage => { - const firebaseAppDeprecationMessage = warnMessage.includes('Please use `getApp()` instead.'); - if (checkFirebaseAppDeprecationWarning) { - throw new Error(`Console warn was called unexpectedly with: ${warnMessage}`); - } else { - if (!firebaseAppDeprecationMessage) { - // we want to ignore all firebase app deprecation warnings (e.g. "Please use `getApp()` instead.") unless actually testing for it which we do above - throw new Error(`Console warn was called unexpectedly with: ${warnMessage}`); - } - } - }); - // Do not call `mockRestore()` unless removing the spy - modularFunction(); - consoleWarnSpy.mockReset(); - consoleWarnSpy.mockRestore(); - const consoleWarnSpy2 = jest.spyOn(console, 'warn').mockImplementation(warnMessage => { - const message = createMessage(moduleName, methodNameKey, instanceName, uniqueMessage); - if (message) { - expect(warnMessage).toMatch(message); - } - }); - nonModularFunction(); - - expect(consoleWarnSpy2).toHaveBeenCalledTimes(1); - consoleWarnSpy2.mockReset(); - }; -}; +export {}; diff --git a/packages/app/lib/index.ts b/packages/app/lib/index.ts index 131ad4a115..6e7edd25b0 100644 --- a/packages/app/lib/index.ts +++ b/packages/app/lib/index.ts @@ -15,7 +15,6 @@ * */ -export { firebase, utils, default } from './namespaced'; export * from './modular'; export type { ReactNativeFirebase, diff --git a/packages/app/lib/internal/constants.ts b/packages/app/lib/internal/constants.ts index 4741b11345..9e8d687e8d 100644 --- a/packages/app/lib/internal/constants.ts +++ b/packages/app/lib/internal/constants.ts @@ -18,27 +18,3 @@ export const APP_NATIVE_MODULE = 'RNFBAppModule'; export const DEFAULT_APP_NAME = '[DEFAULT]'; - -export const KNOWN_NAMESPACES = [ - 'appCheck', - 'appDistribution', - 'auth', - 'analytics', - 'remoteConfig', - 'crashlytics', - 'database', - 'inAppMessaging', - 'installations', - 'firestore', - 'functions', - 'indexing', - 'storage', - 'messaging', - 'naturalLanguage', - 'ml', - 'notifications', - 'perf', - 'utils', -] as const; - -export type KnownNamespace = (typeof KNOWN_NAMESPACES)[number]; diff --git a/packages/app/lib/internal/global.d.ts b/packages/app/lib/internal/global.d.ts index 7112dd20c0..d660b78c84 100644 --- a/packages/app/lib/internal/global.d.ts +++ b/packages/app/lib/internal/global.d.ts @@ -24,10 +24,6 @@ declare global { var RNFBTest: boolean | undefined; var RNFBDebugInTestLeakDetection: boolean | undefined; var RNFBDebugLastTest: string | undefined; - - // Modular API deprecation flags - var RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS: boolean | undefined; - var RNFB_MODULAR_DEPRECATION_STRICT_MODE: boolean | undefined; } export {}; diff --git a/packages/app/lib/internal/index.ts b/packages/app/lib/internal/index.ts index cc172b959d..878efbc0c2 100644 --- a/packages/app/lib/internal/index.ts +++ b/packages/app/lib/internal/index.ts @@ -21,7 +21,7 @@ export { default as FirebaseModule } from './FirebaseModule'; export { default as NativeFirebaseError } from './NativeFirebaseError'; export * from './NativeModules'; export * from './registry/app'; -export * from './registry/namespace'; +export * from './registry/modular'; export * from './registry/nativeModule'; export { default as SharedEventEmitter } from './SharedEventEmitter'; export { Logger } from './logger'; diff --git a/packages/app/lib/internal/registry/app.ts b/packages/app/lib/internal/registry/app.ts index 98c1916347..a4b028090a 100644 --- a/packages/app/lib/internal/registry/app.ts +++ b/packages/app/lib/internal/registry/app.ts @@ -15,16 +15,7 @@ * */ -import { - isIOS, - isOther, - isNull, - warnIfNotModularCall, - isObject, - isFunction, - isString, - isUndefined, -} from '../../common'; +import { isIOS, isOther, isNull, isObject, isFunction, isString, isUndefined } from '../../common'; import FirebaseApp from '../../FirebaseApp'; import { DEFAULT_APP_NAME } from '../constants'; import { setReactNativeAsyncStorageInternal } from '../asyncStorage'; @@ -38,7 +29,7 @@ type ReactNativeAsyncStorage = ReactNativeFirebase.ReactNativeAsyncStorage; const APP_REGISTRY: Record = {}; let onAppCreateFn: ((app: FirebaseApp) => void) | null = null; -let onAppDestroyFn: ((app: FirebaseApp) => void) | null = null; +const onAppDestroyCallbacks: Array<(app: FirebaseApp) => void> = []; let initializedNativeApps = false; /** @@ -50,11 +41,11 @@ export function setOnAppCreate(fn: (app: FirebaseApp) => void): void { } /** - * This was needed to avoid metro require cycles... + * Registers an app-destroy listener. * @param fn */ -export function setOnAppDestroy(fn: (app: FirebaseApp) => void): void { - onAppDestroyFn = fn; +export function addOnAppDestroy(fn: (app: FirebaseApp) => void): void { + onAppDestroyCallbacks.push(fn); } /** @@ -93,7 +84,6 @@ export function initializeNativeApps(): void { * @param name */ export function getApp(name: string = DEFAULT_APP_NAME): ReactNativeFirebase.FirebaseApp { - warnIfNotModularCall(arguments, 'getApp()'); if (!initializedNativeApps) { initializeNativeApps(); } @@ -110,7 +100,6 @@ export function getApp(name: string = DEFAULT_APP_NAME): ReactNativeFirebase.Fir * Gets all app instances, used for `firebase.apps` */ export function getApps(): ReactNativeFirebase.FirebaseApp[] { - warnIfNotModularCall(arguments, 'getApps()'); if (!initializedNativeApps) { initializeNativeApps(); } @@ -126,7 +115,6 @@ export function initializeApp( options: Partial = {}, configOrName?: string | FirebaseAppConfig, ): Promise { - warnIfNotModularCall(arguments, 'initializeApp()'); let appConfig: FirebaseAppConfig = configOrName as FirebaseAppConfig; if (!isObject(configOrName) || isNull(configOrName)) { @@ -220,7 +208,6 @@ export function initializeApp( } export function setLogLevel(logLevel: ReactNativeFirebase.LogLevelString): void { - warnIfNotModularCall(arguments, 'setLogLevel()'); if (!['error', 'warn', 'info', 'debug', 'verbose'].includes(logLevel)) { throw new Error('LogLevel must be one of "error", "warn", "info", "debug", "verbose"'); } @@ -233,8 +220,6 @@ export function setLogLevel(logLevel: ReactNativeFirebase.LogLevelString): void } export function setReactNativeAsyncStorage(asyncStorage: ReactNativeAsyncStorage): void { - warnIfNotModularCall(arguments, 'setReactNativeAsyncStorage()'); - if (!isObject(asyncStorage)) { throw new Error("setReactNativeAsyncStorage(*) 'asyncStorage' must be an object."); } @@ -272,7 +257,9 @@ export function deleteApp(name: string, nativeInitialized: boolean): Promise { (app as any)._deleted = true; - onAppDestroyFn?.(app); + for (let i = 0; i < onAppDestroyCallbacks.length; i++) { + onAppDestroyCallbacks[i]?.(app); + } delete APP_REGISTRY[name]; }); } diff --git a/packages/app/lib/internal/registry/modular.ts b/packages/app/lib/internal/registry/modular.ts new file mode 100644 index 0000000000..900ef8a9a7 --- /dev/null +++ b/packages/app/lib/internal/registry/modular.ts @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { DEFAULT_APP_NAME } from '../constants'; +import type FirebaseModule from '../FirebaseModule'; +import type { ReactNativeFirebase } from '../../types/app'; +import type { ModuleConfig } from '../../types/internal'; +import { addOnAppDestroy, getApp } from './app'; + +/** + * Constructor signature shared by every {@link FirebaseModule} subclass. + */ +type ModularModuleClass = new ( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, +) => T; + +// app.name -> instanceKey -> module instance +const MODULAR_INSTANCES: Record> = {}; + +let destroyHookRegistered = false; +function ensureDestroyHook(): void { + if (destroyHookRegistered) { + return; + } + destroyHookRegistered = true; + addOnAppDestroy(app => { + delete MODULAR_INSTANCES[app.name]; + }); +} + +/** + * Builds (and memoises per app) a Firebase service instance for the modular API. + * + * Each module's `index.ts` defines its own `ModuleClass` and `ModuleConfig` and calls this from + * `getX(app?)`. + * + * @param ModuleClass - The module's `FirebaseModule` subclass. + * @param config - Static `ModuleConfig` for the module (namespace, native module name, support flags). + * @param app - The `FirebaseApp` to use; defaults to the default app. + * @param customUrlOrRegion - Optional URL/region/databaseId for modules that support it. + * @returns The memoised module instance for that app + key. + */ +export function getOrCreateModularInstance( + ModuleClass: ModularModuleClass, + config: ModuleConfig, + app?: ReactNativeFirebase.FirebaseApp, + customUrlOrRegion?: string | null, +): T { + ensureDestroyHook(); + + const _app = getApp(app?.name); + + const { namespace, hasMultiAppSupport } = config; + + // Modules such as analytics only run on the default app. + if (!hasMultiAppSupport && _app.name !== DEFAULT_APP_NAME) { + throw new Error( + [ + `You attempted to call "${namespace}" with a secondary Firebase App but; ${namespace} does not support multiple Firebase Apps.`, + '', + `Ensure you access ${namespace} from the default application only.`, + ].join('\r\n'), + ); + } + + const key = customUrlOrRegion ? `${customUrlOrRegion}:${namespace}` : namespace; + + if (!MODULAR_INSTANCES[_app.name]) { + MODULAR_INSTANCES[_app.name] = {}; + } + + if (!MODULAR_INSTANCES[_app.name]![key]) { + MODULAR_INSTANCES[_app.name]![key] = new ModuleClass( + _app as unknown as ReactNativeFirebase.FirebaseAppBase, + Object.assign({}, config), + customUrlOrRegion, + ); + } + + return MODULAR_INSTANCES[_app.name]![key] as T; +} diff --git a/packages/app/lib/internal/registry/namespace.ts b/packages/app/lib/internal/registry/namespace.ts deleted file mode 100644 index 1c391ee2fb..0000000000 --- a/packages/app/lib/internal/registry/namespace.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { isString, createDeprecationProxy } from '../../common'; -import FirebaseApp from '../../FirebaseApp'; -import { version as SDK_VERSION } from '../../version'; -import { DEFAULT_APP_NAME, KNOWN_NAMESPACES, type KnownNamespace } from '../constants'; -import FirebaseModule from '../FirebaseModule'; -import type { ModuleGetter, FirebaseRoot, NamespaceConfig } from '../../types/internal'; -import { - getApp, - getApps, - initializeApp, - setLogLevel, - setReactNativeAsyncStorage, - setOnAppCreate, - setOnAppDestroy, -} from './app'; - -// firebase.X -let FIREBASE_ROOT: FirebaseRoot | null = null; - -const NAMESPACE_REGISTRY: Record = {}; -const APP_MODULE_INSTANCE: Record>> = {}; -const MODULE_GETTER_FOR_APP: Record> = {}; -const MODULE_GETTER_FOR_ROOT: Record = {}; - -/** - * Attaches module namespace getters on every newly created app. - * - * Structured like this to avoid metro require cycles. - */ -setOnAppCreate(app => { - for (let i = 0; i < KNOWN_NAMESPACES.length; i++) { - const moduleNamespace = KNOWN_NAMESPACES[i]; - if (moduleNamespace) { - Object.defineProperty(app, moduleNamespace, { - enumerable: false, - get: firebaseAppModuleProxy.bind(null, app, moduleNamespace), - }); - } - } -}); - -/** - * Destroys all APP_MODULE_INSTANCE & MODULE_GETTER_FOR_APP objects relating to the - * recently destroyed app. - * - * Structured like this to avoid metro require cycles. - */ -setOnAppDestroy(app => { - delete APP_MODULE_INSTANCE[app.name]; - delete MODULE_GETTER_FOR_APP[app.name]; -}); - -/** - * - * @param app - * @param moduleNamespace - * @returns {*} - */ -function getOrCreateModuleForApp(app: FirebaseApp, moduleNamespace: KnownNamespace): ModuleGetter { - if (MODULE_GETTER_FOR_APP[app.name] && MODULE_GETTER_FOR_APP[app.name]?.[moduleNamespace]) { - return MODULE_GETTER_FOR_APP[app.name]![moduleNamespace]!; - } - - if (!MODULE_GETTER_FOR_APP[app.name]) { - MODULE_GETTER_FOR_APP[app.name] = {}; - } - - const config = NAMESPACE_REGISTRY[moduleNamespace]; - if (!config) { - throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); - } - const { hasCustomUrlOrRegionSupport, hasMultiAppSupport, ModuleClass } = config; - - // modules such as analytics only run on the default app - if (!hasMultiAppSupport && app.name !== DEFAULT_APP_NAME) { - throw new Error( - [ - `You attempted to call "firebase.app('${app.name}').${moduleNamespace}" but; ${moduleNamespace} does not support multiple Firebase Apps.`, - '', - `Ensure you access ${moduleNamespace} from the default application only.`, - ].join('\r\n'), - ); - } - - // e.g. firebase.storage(customUrlOrRegion), firebase.functions(customUrlOrRegion), firebase.firestore(databaseId), firebase.database(url) - function firebaseModuleWithArgs(customUrlOrRegionOrDatabaseId?: string): FirebaseModule { - if (customUrlOrRegionOrDatabaseId !== undefined) { - if (!hasCustomUrlOrRegionSupport) { - // TODO throw Module does not support arguments error - } - - if (!isString(customUrlOrRegionOrDatabaseId)) { - // TODO throw Module first argument must be a string error - } - } - - const key = customUrlOrRegionOrDatabaseId - ? `${customUrlOrRegionOrDatabaseId}:${moduleNamespace}` - : moduleNamespace; - - if (!APP_MODULE_INSTANCE[app.name]) { - APP_MODULE_INSTANCE[app.name] = {}; - } - - if (!APP_MODULE_INSTANCE[app.name]?.[key]) { - const moduleConfig = NAMESPACE_REGISTRY[moduleNamespace]; - if (!moduleConfig) { - throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); - } - const module = createDeprecationProxy( - new ModuleClass(app, moduleConfig, customUrlOrRegionOrDatabaseId), - ) as unknown as FirebaseModule; - - APP_MODULE_INSTANCE[app.name]![key] = module; - } - - return APP_MODULE_INSTANCE[app.name]![key]!; - } - - MODULE_GETTER_FOR_APP[app.name]![moduleNamespace] = - firebaseModuleWithArgs as unknown as ModuleGetter; - return MODULE_GETTER_FOR_APP[app.name]![moduleNamespace]!; -} - -/** - * - * @param moduleNamespace - * @returns {*} - */ -function getOrCreateModuleForRoot(moduleNamespace: KnownNamespace): ModuleGetter { - if (MODULE_GETTER_FOR_ROOT[moduleNamespace]) { - return MODULE_GETTER_FOR_ROOT[moduleNamespace]; - } - - const config = NAMESPACE_REGISTRY[moduleNamespace]; - if (!config) { - throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); - } - const { statics, hasMultiAppSupport, ModuleClass } = config; - - // e.g. firebase.storage(app) - function firebaseModuleWithApp(app?: FirebaseApp): FirebaseModule { - const _app = app || getApp(); - - // Duck-type check for FirebaseApp (checking for required properties) - if ( - !_app || - typeof _app !== 'object' || - !('name' in _app) || - !('options' in _app) || - typeof _app.name !== 'string' - ) { - throw new Error( - [ - `"firebase.${moduleNamespace}(app)" arg expects a FirebaseApp instance or undefined.`, - '', - 'Ensure the arg provided is a Firebase app instance; or no args to use the default Firebase app.', - ].join('\r\n'), - ); - } - - // modules such as analytics only run on the default app - if (!hasMultiAppSupport && _app.name !== DEFAULT_APP_NAME) { - throw new Error( - [ - `You attempted to call "firebase.${moduleNamespace}(app)" but; ${moduleNamespace} does not support multiple Firebase Apps.`, - '', - `Ensure the app provided is the default Firebase app only and not the "${_app.name}" app.`, - ].join('\r\n'), - ); - } - - if (!APP_MODULE_INSTANCE[_app.name]) { - APP_MODULE_INSTANCE[_app.name] = {}; - } - - if (!APP_MODULE_INSTANCE[_app.name]?.[moduleNamespace]) { - const moduleConfig = NAMESPACE_REGISTRY[moduleNamespace]; - if (!moduleConfig) { - throw new Error(`Module namespace '${moduleNamespace}' is not registered.`); - } - const module = createDeprecationProxy( - new ModuleClass(_app, moduleConfig), - ) as unknown as FirebaseModule; - APP_MODULE_INSTANCE[_app.name]![moduleNamespace] = module; - } - - return APP_MODULE_INSTANCE[_app.name]![moduleNamespace]!; - } - - Object.assign(firebaseModuleWithApp, statics || {}); - // Object.freeze(firebaseModuleWithApp); - // Wrap around statics, e.g. firebase.firestore.FieldValue, removed freeze as it stops proxy working. it is deprecated anyway - MODULE_GETTER_FOR_ROOT[moduleNamespace] = createDeprecationProxy( - firebaseModuleWithApp, - ) as unknown as ModuleGetter; - - return MODULE_GETTER_FOR_ROOT[moduleNamespace]!; -} - -/** - * - * @param firebaseNamespace - * @param moduleNamespace - * @returns {*} - */ -function firebaseRootModuleProxy( - _firebaseNamespace: FirebaseRoot, - moduleNamespace: string, -): ModuleGetter { - if (NAMESPACE_REGISTRY[moduleNamespace]) { - return getOrCreateModuleForRoot(moduleNamespace as KnownNamespace); - } - - const moduleWithDashes = moduleNamespace - .split(/(?=[A-Z])/) - .join('-') - .toLowerCase(); - - throw new Error( - [ - `You attempted to use 'firebase.${moduleNamespace}' but this module could not be found.`, - '', - `Ensure you have installed and imported the '@react-native-firebase/${moduleWithDashes}' package.`, - ].join('\r\n'), - ); -} - -/** - * - * @param app - * @param moduleNamespace - * @returns {*} - */ -export function firebaseAppModuleProxy(app: FirebaseApp, moduleNamespace: string): ModuleGetter { - if (NAMESPACE_REGISTRY[moduleNamespace]) { - // Call private _checkDestroyed method - (app as unknown as { _checkDestroyed: () => void })._checkDestroyed(); - return getOrCreateModuleForApp(app, moduleNamespace as KnownNamespace); - } - - const moduleWithDashes = moduleNamespace - .split(/(?=[A-Z])/) - .join('-') - .toLowerCase(); - - throw new Error( - [ - `You attempted to use "firebase.app('${app.name}').${moduleNamespace}" but this module could not be found.`, - '', - `Ensure you have installed and imported the '@react-native-firebase/${moduleWithDashes}' package.`, - ].join('\r\n'), - ); -} - -/** - * - * @returns {*} - */ -export function createFirebaseRoot(): FirebaseRoot { - // Create partial root object - module namespaces like 'utils' are added dynamically below - const root = { - initializeApp, - setReactNativeAsyncStorage, - get app() { - return getApp; - }, - get apps() { - return getApps(); - }, - SDK_VERSION, - setLogLevel, - } as FirebaseRoot; - - for (let i = 0; i < KNOWN_NAMESPACES.length; i++) { - const namespace = KNOWN_NAMESPACES[i]; - if (namespace) { - Object.defineProperty(root, namespace, { - enumerable: false, - get: firebaseRootModuleProxy.bind(null, root, namespace), - }); - } - } - - FIREBASE_ROOT = root; - return root; -} - -/** - * - * @returns {*} - */ -export function getFirebaseRoot(): FirebaseRoot { - if (FIREBASE_ROOT) { - return FIREBASE_ROOT; - } - return createFirebaseRoot(); -} - -/** - * - * @param options - * @returns {*} - */ -export function createModuleNamespace(options: NamespaceConfig): ModuleGetter { - const { namespace, ModuleClass } = options; - - if (!NAMESPACE_REGISTRY[namespace]) { - // validation only for internal / module dev usage - const firebaseModuleExtended = (FirebaseModule as unknown as { __extended__: object }) - .__extended__; - const moduleClassExtended = (ModuleClass as unknown as { __extended__: object }).__extended__; - if (firebaseModuleExtended !== moduleClassExtended) { - throw new Error('INTERNAL ERROR: ModuleClass must be an instance of FirebaseModule.'); - } - - NAMESPACE_REGISTRY[namespace] = Object.assign({}, options); - } - - return getFirebaseRoot()[namespace] as ModuleGetter; -} diff --git a/packages/app/lib/modular.ts b/packages/app/lib/modular.ts index 71c2a86d54..52068ef85e 100644 --- a/packages/app/lib/modular.ts +++ b/packages/app/lib/modular.ts @@ -15,8 +15,9 @@ * */ -import { MODULAR_DEPRECATION_ARG } from './common'; -import type { ReactNativeFirebase, LogCallback, LogOptions } from './types/app'; +import type { ReactNativeFirebase, LogCallback, LogOptions, Utils } from './types/app'; +import { getUtils as getUtilsImpl } from './utils'; +import UtilsStatics from './utils/UtilsStatics'; import { deleteApp as deleteAppCompat, getApp as getAppCompat, @@ -36,13 +37,7 @@ import type { RNFBAppModuleInterface } from './internal/NativeModules'; * @returns Promise */ export function deleteApp(app: ReactNativeFirebase.FirebaseApp): Promise { - return deleteAppCompat.call( - null, - app.name, - (app as any)._nativeInitialized, - // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls - MODULAR_DEPRECATION_ARG, - ); + return deleteAppCompat(app.name, (app as any)._nativeInitialized); } /** @@ -75,11 +70,7 @@ export function onLog(logCallback: LogCallback | null, options?: LogOptions): vo * @returns An array of all initialized Firebase apps. */ export function getApps(): ReactNativeFirebase.FirebaseApp[] { - return getAppsCompat.call( - null, - // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls - MODULAR_DEPRECATION_ARG, - ); + return getAppsCompat(); } /** @@ -92,13 +83,7 @@ export function initializeApp( options: ReactNativeFirebase.FirebaseAppOptions, configOrName?: string | ReactNativeFirebase.FirebaseAppConfig, ): Promise { - return initializeAppCompat.call( - null, - options, - configOrName, - // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls - MODULAR_DEPRECATION_ARG, - ); + return initializeAppCompat(options, configOrName); } /** @@ -107,12 +92,7 @@ export function initializeApp( * @returns The requested Firebase app instance. */ export function getApp(name?: string): ReactNativeFirebase.FirebaseApp { - return getAppCompat.call( - null, - name, - // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls - MODULAR_DEPRECATION_ARG, - ); + return getAppCompat(name); } /** @@ -121,12 +101,7 @@ export function getApp(name?: string): ReactNativeFirebase.FirebaseApp { * @returns void */ export function setLogLevel(logLevel: ReactNativeFirebase.LogLevelString): void { - return setLogLevelCompat.call( - null, - logLevel, - // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls - MODULAR_DEPRECATION_ARG, - ); + return setLogLevelCompat(logLevel); } /** @@ -138,12 +113,7 @@ export function setLogLevel(logLevel: ReactNativeFirebase.LogLevelString): void export function setReactNativeAsyncStorage( asyncStorage: ReactNativeFirebase.ReactNativeAsyncStorage, ): void { - return setReactNativeAsyncStorageCompat.call( - null, - asyncStorage, - // @ts-expect-error - Extra arg used by deprecation proxy to detect modular calls - MODULAR_DEPRECATION_ARG, - ); + return setReactNativeAsyncStorageCompat(asyncStorage); } /** @@ -217,3 +187,24 @@ export function preferencesSetString(key: string, value: string): Promise } export const SDK_VERSION = sdkVersion; + +/** + * Returns the {@link Utils.Module} instance for the default or given {@link ReactNativeFirebase.FirebaseApp}. + * + * @param app - The Firebase app to use. When omitted, the default app is used. + */ +export function getUtils(app?: ReactNativeFirebase.FirebaseApp): Utils.Module { + return getUtilsImpl(app); +} + +/** + * Native device file paths for use with file-based APIs such as Storage `putFile` or `writeToFile`. + */ +export const FilePath: Utils.FilePath = new Proxy({} as Utils.FilePath, { + get(_target, prop: string | symbol) { + if (typeof prop === 'string') { + return UtilsStatics.FilePath[prop as keyof Utils.FilePath]; + } + return undefined; + }, +}); diff --git a/packages/app/lib/types/app.ts b/packages/app/lib/types/app.ts index d8534591b8..0be0b8cf58 100644 --- a/packages/app/lib/types/app.ts +++ b/packages/app/lib/types/app.ts @@ -130,7 +130,6 @@ export namespace ReactNativeFirebase { /** * Base interface for FirebaseApp containing core properties and methods. * The concrete FirebaseApp class implements this interface. - * Module-specific methods (auth(), analytics(), etc.) are added to FirebaseApp via declaration merging. */ export interface FirebaseAppBase { /** @@ -152,21 +151,12 @@ export namespace ReactNativeFirebase { * Make this app unusable and free up resources. */ delete(): Promise; - - utils(): Utils.Module; } /** * Full FirebaseApp interface that extends the base interface. - * Module-specific methods (auth(), analytics(), etc.) are added here via declaration merging - * from individual package .d.ts files. */ - export interface FirebaseApp extends FirebaseAppBase { - // Module methods are added here via declaration merging, e.g.: - // auth(): FirebaseAuthTypes.Module; - // analytics(): FirebaseAnalyticsTypes.Module; - // etc. - } + export interface FirebaseApp extends FirebaseAppBase {} /** * Interface for a supplied `AsyncStorage`. diff --git a/packages/app/lib/types/internal.ts b/packages/app/lib/types/internal.ts index c26f6ab0ab..52f2873370 100644 --- a/packages/app/lib/types/internal.ts +++ b/packages/app/lib/types/internal.ts @@ -15,8 +15,6 @@ * */ -import type { ReactNativeFirebase, Utils } from './app'; - /** * Internal types for React Native Firebase * These types are used internally across multiple files and should not be exported to consumers @@ -29,7 +27,7 @@ import type { ReactNativeFirebase, Utils } from './app'; export type FirebaseJsonConfig = Record; /** - * Configuration for module namespace registration + * Configuration for a Firebase module instance. */ export interface ModuleConfig { namespace: string; @@ -41,49 +39,6 @@ export interface ModuleConfig { turboModule?: boolean; } -/** - * Extended configuration for namespace registration including native module details - */ -export interface NamespaceConfig extends ModuleConfig { - nativeModuleName: string | string[]; - nativeEvents: boolean | string[]; - // ModuleClass can be FirebaseModule or any subclass of it - // Uses FirebaseAppBase (the concrete class type) rather than FirebaseApp (the augmented interface) - ModuleClass: new ( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - customUrlOrRegion?: string | null, - ) => ReactNativeFirebase.FirebaseModule; - statics?: object; - version?: string; -} - -/** - * Type for a Firebase module getter function that can optionally accept - * a custom URL/region/databaseId parameter - */ -export type ModuleGetter = { - (customUrlOrRegionOrDatabaseId?: string): ReactNativeFirebase.FirebaseModule; - [key: string]: unknown; -}; - -/** - * Type for Firebase root object with module getters - */ -export interface FirebaseRoot { - initializeApp: ( - options: ReactNativeFirebase.FirebaseAppOptions, - configOrName?: string | ReactNativeFirebase.FirebaseAppConfig, - ) => Promise; - setReactNativeAsyncStorage: (asyncStorage: ReactNativeFirebase.ReactNativeAsyncStorage) => void; - app: (name?: string) => ReactNativeFirebase.FirebaseApp; - apps: ReactNativeFirebase.FirebaseApp[]; - SDK_VERSION: string; - setLogLevel: (logLevel: ReactNativeFirebase.LogLevelString) => void; - utils: Utils.Statics & (() => Utils.Module); - [key: string]: unknown; -} - /** * Native error types */ diff --git a/packages/app/lib/utils/index.ts b/packages/app/lib/utils/index.ts index 757f0e290f..31b5f52c81 100644 --- a/packages/app/lib/utils/index.ts +++ b/packages/app/lib/utils/index.ts @@ -16,14 +16,21 @@ */ import { isIOS } from '../common'; -import { createModuleNamespace, FirebaseModule } from '../internal'; -import UtilsStatics from './UtilsStatics'; -import { Utils } from '../types/app'; +import { FirebaseModule, getOrCreateModularInstance } from '../internal'; +import type { ModuleConfig } from '../internal'; +import type { ReactNativeFirebase, Utils } from '../types/app'; const namespace = 'utils'; -const statics = UtilsStatics; const nativeModuleName = 'RNFBUtilsModule'; +const config: ModuleConfig = { + namespace, + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, +}; + class FirebaseUtilsModule extends FirebaseModule<'RNFBUtilsModule'> { get isRunningInTestLab(): boolean { if (isIOS) { @@ -80,15 +87,11 @@ class FirebaseUtilsModule extends FirebaseModule<'RNFBUtilsModule'> { } } -// import { utils } from '@react-native-firebase/app'; -// utils().X(...); -export default createModuleNamespace({ - statics, - version: UtilsStatics.SDK_VERSION, - namespace, - nativeModuleName, - nativeEvents: false, - hasMultiAppSupport: false, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseUtilsModule, -}) as unknown as Utils.Statics & (() => Utils.Module); +/** + * Returns the {@link Utils.Module} instance for the default or given {@link ReactNativeFirebase.FirebaseApp}. + * + * @param app - The Firebase app to use. When omitted, the default app is used. + */ +export function getUtils(app?: ReactNativeFirebase.FirebaseApp): Utils.Module { + return getOrCreateModularInstance(FirebaseUtilsModule, config, app) as unknown as Utils.Module; +} diff --git a/packages/app/type-test.ts b/packages/app/type-test.ts index 37b009fa85..0fb0d3b07d 100644 --- a/packages/app/type-test.ts +++ b/packages/app/type-test.ts @@ -14,38 +14,37 @@ * limitations under the License. */ -import firebase, { utils } from '.'; - -// checks module exists at root -console.log(firebase.utils().app.name); -console.log(utils().app.name); -console.log(firebase.app().name); -console.log(firebase.app('foo').name); - -// checks module exists at app level -console.log(firebase.app().utils().app.name); +import { + getApp, + getApps, + getUtils, + initializeApp, + SDK_VERSION, + FilePath, +} from '.'; + +// modular app accessors +console.log(getUtils().app.name); +console.log(getApp().name); +console.log(getApp('foo').name); // checks statics exist -console.log(firebase.utils.SDK_VERSION); -console.log(utils.SDK_VERSION); -console.log(firebase.utils.FilePath.CACHES_DIRECTORY); -console.log(utils.FilePath.CACHES_DIRECTORY); - -console.log(firebase.utils.FilePath.CACHES_DIRECTORY); -console.log(utils.FilePath.CACHES_DIRECTORY); +console.log(SDK_VERSION); +console.log(FilePath.CACHES_DIRECTORY); // initialize app variants -firebase.initializeApp({ apiKey: 'a', appId: 'b', projectId: 'c' }); -firebase.initializeApp({ apiKey: 'a', appId: 'b', projectId: 'c' }, 'foo'); +initializeApp({ apiKey: 'a', appId: 'b', projectId: 'c' }); +initializeApp({ apiKey: 'a', appId: 'b', projectId: 'c' }, 'foo'); // utils instance API -const u = firebase.utils(); -console.log(u.isRunningInTestLab); -console.log(u.playServicesAvailability); -u.getPlayServicesStatus(); -u.promptForPlayServices(); -u.makePlayServicesAvailable(); -u.resolutionForPlayServices(); +const modularUtils = getUtils(); +console.log(modularUtils.isRunningInTestLab); +console.log(modularUtils.playServicesAvailability); +modularUtils.getPlayServicesStatus(); +modularUtils.promptForPlayServices(); +modularUtils.makePlayServicesAvailable(); +modularUtils.resolutionForPlayServices(); // checks root exists -console.log(firebase.SDK_VERSION); +console.log(SDK_VERSION); +console.log(getApps().length); diff --git a/packages/app/typedoc.json b/packages/app/typedoc.json index 532f326da2..1a8dcae982 100644 --- a/packages/app/typedoc.json +++ b/packages/app/typedoc.json @@ -1,6 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": ["lib/index.ts"], - "tsconfig": "tsconfig.json", - "intentionallyNotExported": ["FirebaseRoot"] + "tsconfig": "tsconfig.json" } diff --git a/packages/auth/__tests__/auth.test.ts b/packages/auth/__tests__/auth.test.ts index 14a3c067e7..4e50e6cd83 100644 --- a/packages/auth/__tests__/auth.test.ts +++ b/packages/auth/__tests__/auth.test.ts @@ -1,13 +1,7 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; -import { FirebaseAuthTypes } from '../lib/index'; -import type { ActionCodeSettings, User as ModularUser } from '../lib'; +import { describe, expect, it, jest } from '@jest/globals'; // @ts-ignore import User from '../lib/User'; -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; - -import auth, { - firebase, +import { getAuth, initializeAuth, applyActionCode, @@ -24,7 +18,6 @@ import auth, { onIdTokenChanged, sendPasswordResetEmail, sendSignInLinkToEmail, - setLanguageCode, setPersistence, signInAnonymously, signInWithCredential, @@ -77,7 +70,6 @@ import auth, { TotpSecret, TotpMultiFactorGenerator, TwitterAuthProvider, - PhoneAuthState, EmailAuthCredential, OAuthCredential, PhoneAuthCredential, @@ -85,279 +77,28 @@ import auth, { const { PasswordPolicyImpl } = require('../lib/password-policy/PasswordPolicyImpl'); -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; -// @ts-ignore - We don't mind missing types here -import { NativeFirebaseError } from '../../app/lib/internal'; - -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; -// @ts-ignore -import { createDeprecationProxy } from '@react-native-firebase/app/dist/module/common'; -import { AuthInternal } from '../lib/types/internal'; - describe('Auth', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.auth).toBeDefined(); - expect(app.auth().useEmulator).toBeDefined(); - }); - - describe('useEmulator()', function () { - it('useEmulator requires a string url', function () { - // @ts-ignore because we pass an invalid argument... - expect(() => auth().useEmulator()).toThrow( - 'firebase.auth().useEmulator() takes a non-empty string', - ); - expect(() => auth().useEmulator('')).toThrow( - 'firebase.auth().useEmulator() takes a non-empty string', - ); - // @ts-ignore because we pass an invalid argument... - expect(() => auth().useEmulator(123)).toThrow( - 'firebase.auth().useEmulator() takes a non-empty string', - ); - }); - - it('useEmulator requires a well-formed url', function () { - // No http:// - expect(() => auth().useEmulator('localhost:9099')).toThrow( - 'firebase.auth().useEmulator() takes a non-empty string URL', - ); - // No port - expect(() => auth().useEmulator('http://localhost')).toThrow( - 'firebase.auth().useEmulator() unable to parse host and port from URL', - ); - }); - - it('useEmulator -> remaps Android loopback to host', function () { - const foo = auth().useEmulator('http://localhost:9099'); - expect(foo).toEqual(['10.0.2.2', 9099]); - - const bar = auth().useEmulator('http://127.0.0.1:9099'); - expect(bar).toEqual(['10.0.2.2', 9099]); - }); - - it('useEmulator allows hyphens in the hostname', function () { - const result = auth().useEmulator('http://my-host:9099'); - expect(result).toEqual(['my-host', 9099]); - }); - - it('useEmulator exposes emulatorConfig', function () { - const authInstance = getAuth(); - connectAuthEmulator(authInstance, 'http://my-host:9099'); - expect(authInstance.emulatorConfig).toEqual({ - protocol: 'http', - host: 'my-host', - port: 9099, - options: { disableWarnings: false }, - }); - }); - - it('authStateReady resolves after auth result is known', async function () { - const authInstance = getAuth(); - (authInstance as unknown as { _authResult: boolean })._authResult = true; - await expect(authInstance.authStateReady()).resolves.toBeUndefined(); - }); - - it('tenantId setter delegates to setTenantId', async function () { - const authInstance = getAuth() as unknown as { - tenantId: string | null; - setTenantId(tenantId: string | null): Promise; - }; - const originalSetTenantId = authInstance.setTenantId.bind(authInstance); - const calls: Array = []; - authInstance.setTenantId = async (tenantId: string | null) => { - calls.push(tenantId); - }; - - authInstance.tenantId = 'tenant-from-setter'; - await Promise.resolve(); - expect(calls).toEqual(['tenant-from-setter']); - - authInstance.setTenantId = originalSetTenantId; - }); - - describe('tenantId', function () { - it('should be able to set tenantId ', function () { - const auth = firebase.app().auth(); - auth.setTenantId('test-id').then(() => { - expect(auth.tenantId).toBe('test-id'); - }); - }); - - it('should clear JS tenantId when tenantId is null', async function () { - const auth = firebase.app().auth(); - await auth.setTenantId('test-id'); - expect(auth.tenantId).toBe('test-id'); - await auth.setTenantId(null); - expect(auth.tenantId).toBeNull(); - }); - }); - - it('should throw error when tenantId is a non string object ', async function () { - try { - await firebase.app().auth().setTenantId(Object()); - return Promise.reject('It should throw an error'); - } catch (e: any) { - expect(e.message).toBe( - "firebase.auth().setTenantId(*) expected 'tenantId' to be a string", - ); - return Promise.resolve('Error catched'); - } - }); - }); - - describe('getMultiFactorResolver', function () { - it('should return null if no resolver object is found', function () { - const unknownError = NativeFirebaseError.fromEvent( - { - code: 'unknown', - }, - 'auth', - ); - const actual = auth.getMultiFactorResolver(auth(), unknownError); - expect(actual).toBe(null); - }); - - it('should return null if resolver object is null', function () { - const unknownError = NativeFirebaseError.fromEvent( - { - code: 'unknown', - resolver: null, - }, - 'auth', - ); - const actual = auth.getMultiFactorResolver(firebase.app().auth(), unknownError); - expect(actual).toBe(null); - }); - - it('should return the resolver object if its found', function () { - const resolver = { session: '', hints: [] }; - const errorWithResolver = NativeFirebaseError.fromEvent( - { - code: 'multi-factor-auth-required', - resolver, - }, - 'auth', - ); - const actual = auth.getMultiFactorResolver(firebase.app().auth(), errorWithResolver); - // Using expect(actual).toEqual(resolver) causes unexpected errors: - // You attempted to use "firebase.app('[DEFAULT]').appCheck" but this module could not be found. - expect(actual).not.toBeNull(); - // @ts-ignore We know actual is not null - expect(actual.session).toEqual(resolver.session); - // @ts-ignore We know actual is not null - expect(actual.hints).toEqual(resolver.hints); - // @ts-ignore We know actual is not null - expect(actual._auth).not.toBeNull(); - }); - }); - - describe('ActionCodeSettings', function () { - beforeAll(function () { - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => jest.fn().mockResolvedValue({} as never), - }, - ); - }); - }); - - it('should allow linkDomain as `ActionCodeSettings.linkDomain`', function () { - const auth = firebase.app().auth() as unknown as FirebaseAuthTypes.Module & AuthInternal; - const actionCodeSettings = { - url: 'https://example.com', - handleCodeInApp: true, - linkDomain: 'example.com', - } satisfies FirebaseAuthTypes.ActionCodeSettings & ActionCodeSettings; - const email = 'fake@example.com'; - auth.sendSignInLinkToEmail(email, actionCodeSettings); - auth.sendPasswordResetEmail(email, actionCodeSettings); - sendPasswordResetEmail(auth, email, actionCodeSettings); - sendSignInLinkToEmail(auth, email, actionCodeSettings); - - const user: FirebaseAuthTypes.User & ModularUser = new User(auth as AuthInternal, { - uid: 'test-user-id', - displayName: 'Test User', - email: 'test@example.com', - emailVerified: true, - isAnonymous: false, - metadata: { - lastSignInTime: '2023-01-01T00:00:00.000Z', - creationTime: '2023-01-01T00:00:00.000Z', - }, - multiFactor: { - enrolledFactors: [], - }, - phoneNumber: '+1234567890', - tenantId: null, - photoURL: 'https://example.com/photo.jpg', - providerData: [ - { - uid: 'test-uid', - displayName: 'Test User', - email: 'test@example.com', - phoneNumber: '+1234567890', - photoURL: 'https://example.com/photo.jpg', - providerId: 'password', - }, - ], - providerId: 'firebase', - }); - - user.sendEmailVerification(actionCodeSettings); - user.verifyBeforeUpdateEmail(email, actionCodeSettings); - sendEmailVerification(user, actionCodeSettings); - verifyBeforeUpdateEmail(user, email, actionCodeSettings); - }); - }); - }); - describe('modular', function () { it('`getAuth` function is properly exposed to end user', function () { expect(getAuth).toBeDefined(); }); - it('getAuth returns the shared namespaced auth instance', function () { - const previousSilenceValue = globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS; - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - - try { - expect(getAuth()).toBe(firebase.auth()); - } finally { - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = previousSilenceValue; - } + it('getAuth returns a modular auth instance', function () { + const auth = getAuth(); + expect(auth).toBeDefined(); + expect(auth.app).toBeDefined(); + expect(getAuth()).toBe(auth); }); it('`initializeAuth` function is properly exposed to end user', function () { expect(initializeAuth).toBeDefined(); }); - it('initializeAuth returns the shared namespaced auth instance', function () { - const previousSilenceValue = globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS; - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - - try { - expect(initializeAuth(firebase.app())).toBe(firebase.auth()); - } finally { - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = previousSilenceValue; - } + it('initializeAuth returns a modular auth instance', function () { + const { getApp } = require('@react-native-firebase/app'); + const auth = initializeAuth(getApp()); + expect(auth).toBeDefined(); + expect(getAuth(getApp())).toBe(auth); }); it('`applyActionCode` function is properly exposed to end user', function () { @@ -641,652 +382,6 @@ describe('Auth', function () { expect(TwitterAuthProvider).toBeDefined(); }); - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let authV9Deprecation: CheckV9DeprecationFunction; - let userV9Deprecation: CheckV9DeprecationFunction; - let staticsV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - authV9Deprecation = createCheckV9Deprecation(['auth']); - userV9Deprecation = createCheckV9Deprecation(['auth', 'User']); - staticsV9Deprecation = createCheckV9Deprecation(['auth', 'statics']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - result: true, - } as never), - }, - ); - }); - }); - - describe('Auth', function () { - it('applyActionCode', function () { - const auth = getAuth(); - authV9Deprecation( - () => applyActionCode(auth, 'code'), - // @ts-expect-error Combines modular and namespace API - () => auth.applyActionCode('code'), - 'applyActionCode', - ); - }); - - it('checkActionCode', function () { - const auth = getAuth(); - authV9Deprecation( - () => checkActionCode(auth, 'code'), - // @ts-expect-error Combines modular and namespace API - () => auth.checkActionCode('code'), - 'checkActionCode', - ); - }); - - it('confirmPasswordReset', function () { - const auth = getAuth(); - authV9Deprecation( - () => confirmPasswordReset(auth, 'code', 'newPassword'), - // @ts-expect-error Combines modular and namespace API - () => auth.confirmPasswordReset('code', 'newPassword'), - 'confirmPasswordReset', - ); - }); - - it('createUserWithEmailAndPassword', function () { - const auth = getAuth(); - authV9Deprecation( - () => createUserWithEmailAndPassword(auth, 'test@example.com', 'password'), - // @ts-expect-error Combines modular and namespace API - () => auth.createUserWithEmailAndPassword('test@example.com', 'password'), - 'createUserWithEmailAndPassword', - ); - }); - - it('fetchSignInMethodsForEmail', function () { - const auth = getAuth(); - authV9Deprecation( - () => fetchSignInMethodsForEmail(auth, 'test@example.com'), - // @ts-expect-error Combines modular and namespace API - () => auth.fetchSignInMethodsForEmail('test@example.com'), - 'fetchSignInMethodsForEmail', - ); - }); - - it('getMultiFactorResolver', function () { - const auth = getAuth(); - const error = { - userInfo: { - resolver: { - hints: [], - session: {}, - }, - }, - } as any; - authV9Deprecation( - () => getMultiFactorResolver(auth, error), - // @ts-expect-error Combines modular and namespace API - () => auth.getMultiFactorResolver(error), - 'getMultiFactorResolver', - ); - }); - - it('isSignInWithEmailLink', function () { - const auth = getAuth(); - authV9Deprecation( - () => isSignInWithEmailLink(auth, 'emailLink'), - // @ts-expect-error Combines modular and namespace API - () => auth.isSignInWithEmailLink('emailLink'), - 'isSignInWithEmailLink', - ); - }); - - it('onAuthStateChanged', function () { - const auth = getAuth(); - const callback = () => {}; - authV9Deprecation( - () => onAuthStateChanged(auth, callback), - () => auth.onAuthStateChanged(callback), - 'onAuthStateChanged', - ); - }); - - it('onIdTokenChanged', function () { - const auth = getAuth(); - const callback = () => {}; - authV9Deprecation( - () => onIdTokenChanged(auth, callback), - () => auth.onIdTokenChanged(callback), - 'onIdTokenChanged', - ); - }); - - it('signInWithPopup', function () { - const auth = getAuth(); - const provider = { toObject: () => ({}) } as any; - authV9Deprecation( - () => signInWithPopup(auth, provider), - // @ts-expect-error Combines modular and namespace API - () => auth.signInWithPopup(provider), - 'signInWithPopup', - ); - }); - - it('signInWithRedirect', function () { - const auth = getAuth(); - const provider = { toObject: () => ({}) } as any; - authV9Deprecation( - () => signInWithRedirect(auth, provider), - // @ts-expect-error Combines modular and namespace API - () => auth.signInWithRedirect(provider), - 'signInWithRedirect', - ); - }); - - it('sendPasswordResetEmail', function () { - const auth = getAuth(); - authV9Deprecation( - () => sendPasswordResetEmail(auth, 'test@example.com'), - // @ts-expect-error Combines modular and namespace API - () => auth.sendPasswordResetEmail('test@example.com'), - 'sendPasswordResetEmail', - ); - }); - - it('sendSignInLinkToEmail', function () { - const auth = getAuth(); - const actionCodeSettings = { url: 'https://example.com' }; - authV9Deprecation( - () => sendSignInLinkToEmail(auth, 'test@example.com', actionCodeSettings), - // @ts-expect-error Combines modular and namespace API - () => auth.sendSignInLinkToEmail('test@example.com', actionCodeSettings), - 'sendSignInLinkToEmail', - ); - }); - - it('signInAnonymously', function () { - const auth = getAuth(); - authV9Deprecation( - () => signInAnonymously(auth), - // @ts-expect-error Combines modular and namespace API - () => auth.signInAnonymously(), - 'signInAnonymously', - ); - }); - - it('signInWithCredential', function () { - const auth = getAuth(); - const credential = {} as any; - authV9Deprecation( - () => signInWithCredential(auth, credential), - // @ts-expect-error Combines modular and namespace API - () => auth.signInWithCredential(credential), - 'signInWithCredential', - ); - }); - - it('signInWithCustomToken', function () { - const auth = getAuth(); - authV9Deprecation( - () => signInWithCustomToken(auth, 'customToken'), - // @ts-expect-error Combines modular and namespace API - () => auth.signInWithCustomToken('customToken'), - 'signInWithCustomToken', - ); - }); - - it('signInWithEmailAndPassword', function () { - const auth = getAuth(); - authV9Deprecation( - () => signInWithEmailAndPassword(auth, 'test@example.com', 'password'), - // @ts-expect-error Combines modular and namespace API - () => auth.signInWithEmailAndPassword('test@example.com', 'password'), - 'signInWithEmailAndPassword', - ); - }); - - it('signInWithEmailLink', function () { - const auth = getAuth(); - authV9Deprecation( - () => signInWithEmailLink(auth, 'test@example.com', 'emailLink'), - // @ts-expect-error Combines modular and namespace API - () => auth.signInWithEmailLink('test@example.com', 'emailLink'), - 'signInWithEmailLink', - ); - }); - - it('signInWithPhoneNumber', function () { - const auth = getAuth(); - authV9Deprecation( - () => signInWithPhoneNumber(auth, '+1234567890', undefined), - // @ts-expect-error Combines modular and namespace API - () => auth.signInWithPhoneNumber('+1234567890', false), - 'signInWithPhoneNumber', - ); - }); - - it('signOut', function () { - const auth = getAuth(); - authV9Deprecation( - () => signOut(auth), - () => auth.signOut(), - 'signOut', - ); - }); - - it('useUserAccessGroup', function () { - const auth = getAuth(); - authV9Deprecation( - () => useUserAccessGroup(auth, 'group'), - // @ts-expect-error Combines modular and namespace API - () => auth.useUserAccessGroup('group'), - 'useUserAccessGroup', - ); - }); - - it('verifyPasswordResetCode', function () { - const auth = getAuth(); - authV9Deprecation( - () => verifyPasswordResetCode(auth, 'code'), - // @ts-expect-error Combines modular and namespace API - () => auth.verifyPasswordResetCode('code'), - 'verifyPasswordResetCode', - ); - }); - - it('getCustomAuthDomain', function () { - const auth = getAuth(); - authV9Deprecation( - () => getCustomAuthDomain(auth), - // @ts-expect-error Combines modular and namespace API - () => auth.getCustomAuthDomain(), - 'getCustomAuthDomain', - ); - }); - - it('useEmulator', function () { - const auth = getAuth(); - authV9Deprecation( - () => connectAuthEmulator(auth, 'http://localhost:9099'), - // @ts-expect-error Combines modular and namespace API - () => auth.useEmulator('http://localhost:9099'), - 'useEmulator', - ); - }); - - it('setLanguageCode', function () { - const auth = getAuth(); - authV9Deprecation( - () => setLanguageCode(auth, 'en'), - // @ts-expect-error Combines modular and namespace API - () => auth.setLanguageCode('en'), - 'setLanguageCode', - ); - }); - - it('multiFactor', function () { - const auth = getAuth(); - const mockUser = { - userId: 'test-user-id', - multiFactor: { - enrolledFactors: [], - }, - } as any; - - // Mock the currentUser getter to have the same userId - Object.defineProperty(auth, 'currentUser', { - get: () => ({ - userId: 'test-user-id', - }), - configurable: true, - }); - - authV9Deprecation( - () => multiFactor(mockUser), - // @ts-expect-error Combines modular and namespace API - () => auth.multiFactor(mockUser), - 'multiFactor', - ); - }); - }); - - describe('User', function () { - let mockUser: User; - - beforeEach(function () { - mockUser = new User(getAuth() as AuthInternal, { - uid: 'test-user-id', - displayName: 'Test User', - email: 'test@example.com', - emailVerified: true, - isAnonymous: false, - metadata: { - lastSignInTime: '2023-01-01T00:00:00.000Z', - creationTime: '2023-01-01T00:00:00.000Z', - }, - multiFactor: { - enrolledFactors: [], - }, - phoneNumber: '+1234567890', - tenantId: null, - photoURL: 'https://example.com/photo.jpg', - providerData: [ - { - uid: 'test-uid', - displayName: 'Test User', - email: 'test@example.com', - phoneNumber: '+1234567890', - photoURL: 'https://example.com/photo.jpg', - providerId: 'password', - }, - ], - providerId: 'firebase', - }); - mockUser = createDeprecationProxy(mockUser); - }); - - it('delete', function () { - userV9Deprecation( - () => deleteUser(mockUser), - () => mockUser.delete(), - 'delete', - ); - }); - - it('getIdToken', function () { - userV9Deprecation( - () => getIdToken(mockUser), - () => mockUser.getIdToken(), - 'getIdToken', - ); - }); - - it('getIdToken with forceRefresh', function () { - userV9Deprecation( - () => getIdToken(mockUser, true), - () => mockUser.getIdToken(true), - 'getIdToken', - ); - }); - - it('getIdTokenResult', function () { - userV9Deprecation( - () => getIdTokenResult(mockUser), - () => mockUser.getIdTokenResult(), - 'getIdTokenResult', - ); - }); - - it('getIdTokenResult with forceRefresh', function () { - userV9Deprecation( - () => getIdTokenResult(mockUser, true), - () => mockUser.getIdTokenResult(true), - 'getIdTokenResult', - ); - }); - - it('linkWithCredential', function () { - const credential = {} as any; - userV9Deprecation( - () => linkWithCredential(mockUser, credential), - () => mockUser.linkWithCredential(credential), - 'linkWithCredential', - ); - }); - - it('linkWithPopup', function () { - const provider = { toObject: () => ({}) } as any; - userV9Deprecation( - () => linkWithPopup(mockUser, provider), - () => mockUser.linkWithPopup(provider), - 'linkWithPopup', - ); - }); - - it('linkWithRedirect', function () { - const provider = { toObject: () => ({}) } as any; - userV9Deprecation( - () => linkWithRedirect(mockUser, provider), - () => mockUser.linkWithRedirect(provider), - 'linkWithRedirect', - ); - }); - - it('reauthenticateWithCredential', function () { - const credential = {} as any; - userV9Deprecation( - () => reauthenticateWithCredential(mockUser, credential), - () => mockUser.reauthenticateWithCredential(credential), - 'reauthenticateWithCredential', - ); - }); - - it('reauthenticateWithPopup', function () { - const provider = { toObject: () => ({}) } as any; - userV9Deprecation( - () => reauthenticateWithPopup(mockUser, provider), - () => mockUser.reauthenticateWithPopup(provider), - 'reauthenticateWithPopup', - ); - }); - - it('reauthenticateWithRedirect', function () { - const provider = { toObject: () => ({}) } as any; - userV9Deprecation( - () => reauthenticateWithRedirect(mockUser, provider), - () => mockUser.reauthenticateWithRedirect(provider), - 'reauthenticateWithRedirect', - ); - }); - - it('reload', function () { - userV9Deprecation( - () => reload(mockUser), - () => mockUser.reload(), - 'reload', - ); - }); - - it('sendEmailVerification', function () { - userV9Deprecation( - () => sendEmailVerification(mockUser), - () => mockUser.sendEmailVerification(), - 'sendEmailVerification', - ); - }); - - it('sendEmailVerification with actionCodeSettings', function () { - const actionCodeSettings = { url: 'https://example.com' }; - userV9Deprecation( - () => sendEmailVerification(mockUser, actionCodeSettings), - () => mockUser.sendEmailVerification(actionCodeSettings), - 'sendEmailVerification', - ); - }); - - it('unlink', function () { - userV9Deprecation( - () => { - void unlink(mockUser, 'google.com').catch(() => {}); - }, - () => { - void mockUser.unlink('google.com').catch(() => {}); - }, - 'unlink', - ); - }); - - it('updateEmail', function () { - userV9Deprecation( - () => updateEmail(mockUser, 'newemail@example.com'), - () => mockUser.updateEmail('newemail@example.com'), - 'updateEmail', - ); - }); - - it('updatePassword', function () { - userV9Deprecation( - () => updatePassword(mockUser, 'newPassword123'), - () => mockUser.updatePassword('newPassword123'), - 'updatePassword', - ); - }); - - it('updatePhoneNumber', function () { - const credential = {} as any; - userV9Deprecation( - () => updatePhoneNumber(mockUser, credential), - () => mockUser.updatePhoneNumber(credential), - 'updatePhoneNumber', - ); - }); - - it('updateProfile', function () { - const profile = { displayName: 'John Doe', photoURL: 'https://example.com/photo.jpg' }; - userV9Deprecation( - () => updateProfile(mockUser, profile), - () => mockUser.updateProfile(profile), - 'updateProfile', - ); - }); - - it('verifyBeforeUpdateEmail', function () { - userV9Deprecation( - () => verifyBeforeUpdateEmail(mockUser, 'newemail@example.com'), - () => mockUser.verifyBeforeUpdateEmail('newemail@example.com'), - 'verifyBeforeUpdateEmail', - ); - }); - - it('verifyBeforeUpdateEmail with actionCodeSettings', function () { - const actionCodeSettings = { url: 'https://example.com' }; - userV9Deprecation( - () => verifyBeforeUpdateEmail(mockUser, 'newemail@example.com', actionCodeSettings), - () => mockUser.verifyBeforeUpdateEmail('newemail@example.com', actionCodeSettings), - 'verifyBeforeUpdateEmail', - ); - }); - - it('toJSON', function () { - userV9Deprecation( - // No modular equivalent - () => {}, - () => mockUser.toJSON(), - 'toJSON', - ); - }); - }); - - describe('statics', function () { - it('AppleAuthProvider', function () { - staticsV9Deprecation( - () => AppleAuthProvider, - () => auth.AppleAuthProvider, - 'AppleAuthProvider', - ); - }); - - it('EmailAuthProvider', function () { - staticsV9Deprecation( - () => EmailAuthProvider, - () => auth.EmailAuthProvider, - 'EmailAuthProvider', - ); - }); - - it('PhoneAuthProvider', function () { - staticsV9Deprecation( - () => PhoneAuthProvider, - () => auth.PhoneAuthProvider, - 'PhoneAuthProvider', - ); - }); - - it('GoogleAuthProvider', function () { - staticsV9Deprecation( - () => GoogleAuthProvider, - () => auth.GoogleAuthProvider, - 'GoogleAuthProvider', - ); - }); - - it('GithubAuthProvider', function () { - staticsV9Deprecation( - () => GithubAuthProvider, - () => auth.GithubAuthProvider, - 'GithubAuthProvider', - ); - }); - - it('TwitterAuthProvider', function () { - staticsV9Deprecation( - () => TwitterAuthProvider, - () => auth.TwitterAuthProvider, - 'TwitterAuthProvider', - ); - }); - - it('FacebookAuthProvider', function () { - staticsV9Deprecation( - () => FacebookAuthProvider, - () => auth.FacebookAuthProvider, - 'FacebookAuthProvider', - ); - }); - - it('PhoneMultiFactorGenerator', function () { - staticsV9Deprecation( - () => PhoneMultiFactorGenerator, - () => auth.PhoneMultiFactorGenerator, - 'PhoneMultiFactorGenerator', - ); - }); - - it('OAuthProvider', function () { - staticsV9Deprecation( - () => OAuthProvider, - () => auth.OAuthProvider, - 'OAuthProvider', - ); - }); - - it('OIDCAuthProvider', function () { - staticsV9Deprecation( - () => OIDCAuthProvider, - () => auth.OIDCAuthProvider, - 'OIDCAuthProvider', - ); - }); - - it('PhoneAuthState', function () { - staticsV9Deprecation( - () => PhoneAuthState, - () => auth.PhoneAuthState, - 'PhoneAuthState', - ); - }); - - it('getMultiFactorResolver', function () { - staticsV9Deprecation( - () => getMultiFactorResolver, - () => auth.getMultiFactorResolver, - 'getMultiFactorResolver', - ); - }); - - it('multiFactor', function () { - staticsV9Deprecation( - () => multiFactor, - () => auth.multiFactor, - 'multiFactor', - ); - }); - }); - }); - describe('credential classes', function () { it('EmailAuthCredential.fromJSON deserializes password credentials', function () { const credential = EmailAuthCredential.fromJSON({ diff --git a/packages/auth/__tests__/validatePassword.test.js b/packages/auth/__tests__/validatePassword.test.js index 5271a6b781..7564f4398d 100644 --- a/packages/auth/__tests__/validatePassword.test.js +++ b/packages/auth/__tests__/validatePassword.test.js @@ -18,7 +18,7 @@ import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { PasswordPolicyImpl } from '../lib/password-policy/PasswordPolicyImpl'; import { PasswordPolicyMixin } from '../lib/password-policy/PasswordPolicyMixin'; -import { validatePassword as validatePasswordModular } from '../lib/modular'; +import { validatePassword as validatePasswordModular } from '../lib'; const mockPasswordPolicy = { schemaVersion: 1, @@ -312,10 +312,7 @@ describe('validatePassword (modular API)', () => { const result = await validatePassword(mockAuth, 'Password123$'); - expect(mockAuth.validatePassword).toHaveBeenCalledWith( - 'Password123$', - 'react-native-firebase-modular-method-call', - ); + expect(mockAuth.validatePassword).toHaveBeenCalledWith('Password123$'); expect(result).toEqual({ isValid: true }); }); }); diff --git a/packages/auth/e2e/auth.e2e.js b/packages/auth/e2e/auth.e2e.js index fcc8c8c1ba..8ea1f966fa 100644 --- a/packages/auth/e2e/auth.e2e.js +++ b/packages/auth/e2e/auth.e2e.js @@ -24,1081 +24,6 @@ const DISABLED_PASS = 'test1234'; const { clearAllUsers, disableUser, getLastOob, resetPassword } = require('./helpers'); describe('auth() modular', function () { - describe('firebase v8 compatibility', function () { - before(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - - await clearAllUsers(); - await firebase.auth().createUserWithEmailAndPassword(TEST_EMAIL, TEST_PASS); - const disabledUserCredential = await firebase - .auth() - .createUserWithEmailAndPassword(DISABLED_EMAIL, DISABLED_PASS); - await disableUser(disabledUserCredential.user.uid); - }); - - beforeEach(async function () { - if (firebase.auth().currentUser) { - await firebase.auth().signOut(); - await Utils.sleep(50); - } - }); - - after(async function afterTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.auth); - app.auth().app.should.equal(app); - }); - - // removing as pending if module.options.hasMultiAppSupport = true - it('supports multiple apps', async function () { - firebase.auth().app.name.should.equal('[DEFAULT]'); - - firebase - .auth(firebase.app('secondaryFromNative')) - .app.name.should.equal('secondaryFromNative'); - - firebase.app('secondaryFromNative').auth().app.name.should.equal('secondaryFromNative'); - }); - }); - - describe('applyActionCode()', function () { - // Needs a different setup to work against the auth emulator - xit('works as expected', async function () { - await firebase - .auth() - .applyActionCode('fooby shooby dooby') - .then($ => $); - }); - - it('errors on invalid code', async function () { - let didError = false; - try { - await firebase - .auth() - .applyActionCode('fooby shooby dooby') - .then($ => $); - } catch (e) { - didError = true; - e.code.should.equal('auth/invalid-action-code'); - } - didError.should.equal(true, 'Did not error'); - }); - }); - - describe('checkActionCode()', function () { - it('errors on invalid code', async function () { - let didError = false; - try { - await firebase.auth().checkActionCode('fooby shooby dooby'); - } catch (e) { - didError = true; - e.code.should.equal('auth/invalid-action-code'); - } - didError.should.equal(true, 'Did not error'); - }); - }); - - describe('reload()', function () { - it('Meta data returns as expected with annonymous sign in', async function () { - if (Platform.other) return; - await firebase.auth().signInAnonymously(); - await Utils.sleep(500); - const firstUser = firebase.auth().currentUser; - await firstUser.reload(); - - await firebase.auth().signOut(); - - await firebase.auth().signInAnonymously(); - await Utils.sleep(500); - const secondUser = firebase.auth().currentUser; - await secondUser.reload(); - - firstUser.metadata.creationTime.should.not.equal(secondUser.metadata.creationTime); - }); - - it('Meta data returns as expected with email and password sign in', async function () { - const random = Utils.randString(12, '#aA'); - const email1 = `${random}@${random}.com`; - const pass = random; - - await firebase.auth().createUserWithEmailAndPassword(email1, pass); - const firstUser = firebase.auth().currentUser; - await firstUser.reload(); - await Utils.sleep(500); - await firebase.auth().signOut(); - - const anotherRandom = Utils.randString(12, '#aA'); - const email2 = `${anotherRandom}@${anotherRandom}.com`; - await Utils.sleep(500); - await firebase.auth().createUserWithEmailAndPassword(email2, pass); - const secondUser = firebase.auth().currentUser; - await secondUser.reload(); - - firstUser.metadata.creationTime.should.not.equal(secondUser.metadata.creationTime); - }); - }); - - describe('confirmPasswordReset()', function () { - it('errors on invalid code via API', async function () { - let didError = false; - try { - await firebase.auth().confirmPasswordReset('fooby shooby dooby', 'passwordthing'); - } catch (e) { - didError = true; - e.code.should.equal('auth/invalid-action-code'); - } - didError.should.equal(true, 'Did not error'); - }); - }); - - describe('signInWithCustomToken()', function () { - // Needs a different setup when running against the emulator - xit('signs in with a admin sdk created custom auth token', async function () { - const successCb = currentUserCredential => { - const currentUser = currentUserCredential.user; - currentUser.should.be.an.Object(); - currentUser.uid.should.be.a.String(); - currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql(TEST_EMAIL); - currentUser.isAnonymous.should.equal(false); - currentUser.providerId.should.equal('firebase'); - currentUser.should.equal(firebase.auth().currentUser); - - const { additionalUserInfo } = currentUserCredential; - additionalUserInfo.should.be.an.Object(); - additionalUserInfo.isNewUser.should.equal(false); - - return currentUser; - }; - - const user = await firebase - .auth() - .signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS) - .then(successCb); - - const IdToken = await firebase.auth().currentUser.getIdToken(); - - firebase.auth().signOut(); - await Utils.sleep(50); - - const token = await new TestAdminApi(IdToken).auth().createCustomToken(user.uid, {}); - - await firebase.auth().signInWithCustomToken(token); - - firebase.auth().currentUser.email.should.equal(TEST_EMAIL); - }); - }); - - describe('onAuthStateChanged()', function () { - it('calls callback with the current user and when auth state changes', async function () { - await firebase.auth().signInAnonymously(); - - await Utils.sleep(50); - - // Test - const callback = sinon.spy(); - - let unsubscribe; - await new Promise(resolve => { - unsubscribe = firebase.auth().onAuthStateChanged(user => { - callback(user); - resolve(); - }); - }); - - callback.should.be.calledWith(firebase.auth().currentUser); - callback.should.be.calledOnce(); - - // Sign out - - await firebase.auth().signOut(); - - // Assertions - - await Utils.sleep(50); - - callback.should.be.calledWith(null); - callback.should.be.calledTwice(); - - // Tear down - - unsubscribe(); - }); - - it('accept observer instead callback as well', async function () { - await firebase.auth().signInAnonymously(); - await Utils.sleep(200); - - // Test - const observer = { - next(user) { - // Test this access - this.onNext(); - this.user = user; - }, - }; - - let unsubscribe; - await new Promise(resolve => { - observer.onNext = resolve; - unsubscribe = firebase.auth().onAuthStateChanged(observer); - }); - should.exist(observer.user); - - // Sign out - - await firebase.auth().signOut(); - - // Assertions - - await Utils.sleep(50); - - should.not.exist(observer.user); - - // Tear down - - unsubscribe(); - }); - - it('stops listening when unsubscribed', async function () { - await firebase.auth().signInAnonymously(); - await Utils.sleep(200); - - // Test - const callback = sinon.spy(); - - let unsubscribe; - await new Promise(resolve => { - unsubscribe = firebase.auth().onAuthStateChanged(user => { - callback(user); - resolve(); - }); - }); - - callback.should.be.calledWith(firebase.auth().currentUser); - callback.should.be.calledOnce(); - - // Sign out - await firebase.auth().signOut(); - await Utils.sleep(50); - - // Assertions - callback.should.be.calledWith(null); - callback.should.be.calledTwice(); - - // Unsubscribe - unsubscribe(); - - // Sign back in - await firebase.auth().signInAnonymously(); - - // Assertions - callback.should.be.calledTwice(); - - // Tear down - await firebase.auth().signOut(); - await Utils.sleep(50); - }); - - it('returns the same user with multiple subscribers #1815', async function () { - const callback = sinon.spy(); - - let unsubscribe1; - let unsubscribe2; - let unsubscribe3; - - await new Promise(resolve => { - unsubscribe1 = firebase.auth().onAuthStateChanged(user => { - callback(user); - resolve(); - }); - }); - - await new Promise(resolve => { - unsubscribe2 = firebase.auth().onAuthStateChanged(user => { - callback(user); - resolve(); - }); - }); - - await new Promise(resolve => { - unsubscribe3 = firebase.auth().onAuthStateChanged(user => { - callback(user); - resolve(); - }); - }); - - callback.should.be.calledThrice(); - callback.should.be.calledWith(null); - - await firebase.auth().signInAnonymously(); - await Utils.sleep(800); - - unsubscribe1(); - unsubscribe2(); - unsubscribe3(); - - callback.should.be.callCount(6); - - const uid = callback.getCall(3).args[0].uid; - - callback.getCall(4).args[0].uid.should.eql(uid); - callback.getCall(5).args[0].uid.should.eql(uid); - - await firebase.auth().signOut(); - await Utils.sleep(50); - }); - }); - - describe('onIdTokenChanged()', function () { - it('calls callback with the current user and when auth state changes', async function () { - await firebase.auth().signInAnonymously(); - await Utils.sleep(200); - - // Test - const callback = sinon.spy(); - - let unsubscribe; - await new Promise(resolve => { - unsubscribe = firebase.auth().onIdTokenChanged(user => { - callback(user); - resolve(); - }); - }); - - callback.should.be.calledWith(firebase.auth().currentUser); - callback.should.be.calledOnce(); - - // Sign out - await firebase.auth().signOut(); - await Utils.sleep(50); - - // Assertions - callback.should.be.calledWith(null); - callback.should.be.calledTwice(); - - // Tear down - unsubscribe(); - }); - - it('stops listening when unsubscribed', async function () { - await firebase.auth().signInAnonymously(); - await Utils.sleep(200); - - // Test - const callback = sinon.spy(); - - let unsubscribe; - await new Promise(resolve => { - unsubscribe = firebase.auth().onIdTokenChanged(user => { - callback(user); - resolve(); - }); - }); - - callback.should.be.calledWith(firebase.auth().currentUser); - callback.should.be.calledOnce(); - - // Sign out - await firebase.auth().signOut(); - await Utils.sleep(50); - - // Assertions - callback.should.be.calledWith(null); - callback.should.be.calledTwice(); - - // Unsubscribe - unsubscribe(); - - // Sign back in - await firebase.auth().signInAnonymously(); - - // Assertions - callback.should.be.calledTwice(); - - // Tear down - await firebase.auth().signOut(); - await Utils.sleep(50); - }); - - it('listens to a null user when auth result is not defined', async function () { - let unsubscribe; - - const callback = sinon.spy(); - - await new Promise(resolve => { - unsubscribe = firebase.auth().onIdTokenChanged(user => { - callback(user); - resolve(); - }); - - unsubscribe(); - }); - }); - }); - - describe('onUserChanged()', function () { - it('calls callback with the current user and when auth state changes', async function () { - await firebase.auth().signInAnonymously(); - await Utils.sleep(200); - - // Test - const callback = sinon.spy(); - - let unsubscribe; - await new Promise(resolve => { - unsubscribe = firebase.auth().onUserChanged(user => { - callback(user); - resolve(); - }); - }); - - callback.should.be.calledWith(firebase.auth().currentUser); - callback.should.be.calledOnce(); - - // Sign out - await firebase.auth().signOut(); - await Utils.sleep(500); - - // Assertions - callback.should.be.calledWith(null); - // Because of the way onUserChanged works, it will be called double - // - once for onAuthStateChanged - // - once for onIdTokenChanged - callback.should.have.callCount(4); - - // Tear down - unsubscribe(); - }); - - it('stops listening when unsubscribed', async function () { - await firebase.auth().signInAnonymously(); - await Utils.sleep(200); - - // Test - const callback = sinon.spy(); - - let unsubscribe; - await new Promise(resolve => { - unsubscribe = firebase.auth().onUserChanged(user => { - callback(user); - resolve(); - }); - }); - - callback.should.be.calledWith(firebase.auth().currentUser); - callback.should.be.calledOnce(); - - // Sign out - await firebase.auth().signOut(); - await Utils.sleep(200); - - // Assertions - callback.should.be.calledWith(null); - // Because of the way onUserChanged works, it will be called double - // - once for onAuthStateChanged - // - once for onIdTokenChanged - callback.should.have.callCount(4); - - // Unsubscribe - unsubscribe(); - - // Sign back in - await firebase.auth().signInAnonymously(); - await Utils.sleep(200); - - // Assertions - callback.should.have.callCount(4); - - // Tear down - await firebase.auth().signOut(); - }); - }); - - describe('signInAnonymously()', function () { - it('it should sign in anonymously', function () { - const successCb = currentUserCredential => { - const currentUser = currentUserCredential.user; - currentUser.should.be.an.Object(); - currentUser.uid.should.be.a.String(); - currentUser.toJSON().should.be.an.Object(); - should.equal(currentUser.toJSON().email, null); - currentUser.isAnonymous.should.equal(true); - currentUser.providerId.should.equal('firebase'); - currentUser.should.equal(firebase.auth().currentUser); - - const { additionalUserInfo } = currentUserCredential; - additionalUserInfo.should.be.an.Object(); - - return firebase.auth().signOut(); - }; - - return firebase.auth().signInAnonymously().then(successCb); - }); - }); - - describe('signInWithEmailAndPassword()', function () { - it('it should login with email and password', function () { - const successCb = currentUserCredential => { - const currentUser = currentUserCredential.user; - currentUser.should.be.an.Object(); - currentUser.uid.should.be.a.String(); - currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql(TEST_EMAIL); - currentUser.isAnonymous.should.equal(false); - currentUser.providerId.should.equal('firebase'); - currentUser.should.equal(firebase.auth().currentUser); - - const { additionalUserInfo } = currentUserCredential; - additionalUserInfo.should.be.an.Object(); - additionalUserInfo.isNewUser.should.equal(false); - - return firebase.auth().signOut(); - }; - - return firebase.auth().signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS).then(successCb); - }); - - it('it should error on login if user is disabled', function () { - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/user-disabled'); - return Promise.resolve(); - }; - - return firebase - .auth() - .signInWithEmailAndPassword(DISABLED_EMAIL, DISABLED_PASS) - .then(successCb) - .catch(failureCb); - }); - - it('it should error on login if password incorrect', function () { - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/wrong-password'); - return Promise.resolve(); - }; - - return firebase - .auth() - .signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS + '666') - .then(successCb) - .catch(failureCb); - }); - - it('it should error on login if user not found', function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/user-not-found'); - return Promise.resolve(); - }; - - return firebase - .auth() - .signInWithEmailAndPassword(email, pass) - .then(successCb) - .catch(failureCb); - }); - }); - - describe('signInWithCredential()', function () { - it('it should login with email and password', function () { - const credential = firebase.auth.EmailAuthProvider.credential(TEST_EMAIL, TEST_PASS); - - const successCb = currentUserCredential => { - const currentUser = currentUserCredential.user; - currentUser.should.be.an.Object(); - currentUser.uid.should.be.a.String(); - currentUser.toJSON().should.be.an.Object(); - currentUser.toJSON().email.should.eql(TEST_EMAIL); - currentUser.isAnonymous.should.equal(false); - currentUser.providerId.should.equal('firebase'); - currentUser.should.equal(firebase.auth().currentUser); - - const { additionalUserInfo } = currentUserCredential; - additionalUserInfo.should.be.an.Object(); - additionalUserInfo.isNewUser.should.equal(false); - - return firebase.auth().signOut(); - }; - - return firebase.auth().signInWithCredential(credential).then(successCb); - }); - - it('it should error on login if user is disabled', function () { - const credential = firebase.auth.EmailAuthProvider.credential( - DISABLED_EMAIL, - DISABLED_PASS, - ); - - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/user-disabled'); - return Promise.resolve(); - }; - - return firebase.auth().signInWithCredential(credential).then(successCb).catch(failureCb); - }); - - it('it should error on login if password incorrect', function () { - const credential = firebase.auth.EmailAuthProvider.credential( - TEST_EMAIL, - TEST_PASS + '666', - ); - - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/wrong-password'); - return Promise.resolve(); - }; - - return firebase.auth().signInWithCredential(credential).then(successCb).catch(failureCb); - }); - - it('it should error on login if user not found', function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - const credential = firebase.auth.EmailAuthProvider.credential(email, pass); - - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/user-not-found'); - return Promise.resolve(); - }; - - return firebase.auth().signInWithCredential(credential).then(successCb).catch(failureCb); - }); - }); - - describe('createUserWithEmailAndPassword()', function () { - it('it should create a user with an email and password', function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - const successCb = newUserCredential => { - const newUser = newUserCredential.user; - newUser.uid.should.be.a.String(); - newUser.email.should.equal(email.toLowerCase()); - newUser.emailVerified.should.equal(false); - newUser.isAnonymous.should.equal(false); - newUser.providerId.should.equal('firebase'); - newUser.should.equal(firebase.auth().currentUser); - const { additionalUserInfo } = newUserCredential; - additionalUserInfo.should.be.an.Object(); - additionalUserInfo.isNewUser.should.equal(true); - - return newUser.delete(); - }; - - return firebase.auth().createUserWithEmailAndPassword(email, pass).then(successCb); - }); - - it('it should error on create with invalid email', function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}${random}.com.boop.shoop`; - const pass = random; - - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/invalid-email'); - return Promise.resolve(); - }; - - return firebase - .auth() - .createUserWithEmailAndPassword(email, pass) - .then(successCb) - .catch(failureCb); - }); - - it('it should error on create if email in use', function () { - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/email-already-in-use'); - return Promise.resolve(); - }; - - return firebase - .auth() - .createUserWithEmailAndPassword(TEST_EMAIL, TEST_PASS) - .then(successCb) - .catch(failureCb); - }); - - it('it should error on create if password weak', function () { - const email = 'testy@testy.com'; - const pass = '123'; - - const successCb = () => Promise.reject(new Error('Did not error.')); - - const failureCb = error => { - error.code.should.equal('auth/weak-password'); - // cannot test this message - it's different on the web client than ios/android return - // error.message.should.containEql('The given password is invalid.'); - return Promise.resolve(); - }; - - return firebase - .auth() - .createUserWithEmailAndPassword(email, pass) - .then(successCb) - .catch(failureCb); - }); - }); - - describe('fetchSignInMethodsForEmail()', function () { - it('it should return password provider for an email address', function () { - return new Promise((resolve, reject) => { - const successCb = providers => { - providers.should.be.a.Array(); - providers.should.containEql('password'); - resolve(); - }; - - const failureCb = () => { - reject(new Error('Should not have an error.')); - }; - - return firebase - .auth() - .fetchSignInMethodsForEmail(TEST_EMAIL) - .then(successCb) - .catch(failureCb); - }); - }); - - it('it should return an empty array for a not found email', function () { - return new Promise((resolve, reject) => { - const successCb = providers => { - providers.should.be.a.Array(); - providers.should.be.empty(); - resolve(); - }; - - const failureCb = () => { - reject(new Error('Should not have an error.')); - }; - - return firebase - .auth() - .fetchSignInMethodsForEmail('test@i-do-not-exist.com') - .then(successCb) - .catch(failureCb); - }); - }); - - it('it should return an error for a bad email address', function () { - return new Promise((resolve, reject) => { - const successCb = () => { - reject(new Error('Should not have successfully resolved.')); - }; - - const failureCb = error => { - error.code.should.equal('auth/invalid-email'); - resolve(); - }; - - return firebase - .auth() - .fetchSignInMethodsForEmail('foobar') - .then(successCb) - .catch(failureCb); - }); - }); - }); - - describe('signOut()', function () { - it('it should reject signOut if no currentUser', function () { - return new Promise((resolve, reject) => { - if (firebase.auth().currentUser) { - return reject( - new Error(`A user is currently signed in. ${firebase.auth().currentUser.uid}`), - ); - } - - const successCb = () => { - reject(new Error('No signOut error returned')); - }; - - const failureCb = error => { - error.code.should.equal('auth/no-current-user'); - resolve(); - }; - - return firebase.auth().signOut().then(successCb).catch(failureCb); - }); - }); - }); - - describe('delete()', function () { - it('should delete a user', function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - const successCb = authResult => { - const newUser = authResult.user; - newUser.uid.should.be.a.String(); - newUser.email.should.equal(email.toLowerCase()); - newUser.emailVerified.should.equal(false); - newUser.isAnonymous.should.equal(false); - newUser.providerId.should.equal('firebase'); - return firebase.auth().currentUser.delete(); - }; - - return firebase.auth().createUserWithEmailAndPassword(email, pass).then(successCb); - }); - }); - - describe('languageCode', function () { - it('it should change the language code', async function () { - await firebase.auth().setLanguageCode('en'); - - if (firebase.auth().languageCode !== 'en') { - throw new Error('Expected language code to be "en".'); - } - await firebase.auth().setLanguageCode('fr'); - - if (firebase.auth().languageCode !== 'fr') { - throw new Error('Expected language code to be "fr".'); - } - // expect no error - await firebase.auth().setLanguageCode(null); - - try { - await firebase.auth().setLanguageCode(123); - return Promise.reject('It did not error'); - } catch (e) { - e.message.should.containEql("expected 'languageCode' to be a string or null value"); - } - - await firebase.auth().setLanguageCode('en'); - }); - }); - - describe('getRedirectResult()', function () { - it('should throw an unsupported error', function () { - (() => { - firebase.auth().getRedirectResult(); - }).should.throw( - 'firebase.auth().getRedirectResult() is unsupported by the native Firebase SDKs.', - ); - }); - }); - - describe('setPersistence()', function () { - it('should throw an unsupported error', function () { - (() => { - firebase.auth().setPersistence(); - }).should.throw( - 'firebase.auth().setPersistence() is unsupported by the native Firebase SDKs.', - ); - }); - }); - - describe('sendPasswordResetEmail()', function () { - it('should not error', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - - try { - await firebase.auth().sendPasswordResetEmail(email); - } catch (error) { - throw new Error('sendPasswordResetEmail() caused an error', error); - } finally { - await firebase.auth().currentUser.delete(); - } - }); - - it('should verify with valid code', async function () { - // FIXME Fails on android against auth emulator with: - // com.google.firebase.FirebaseException: An internal error has occurred. - if (Platform.ios) { - const random = Utils.randString(12, '#a'); - const email = `${random}@${random}.com`; - const userCredential = await firebase - .auth() - .createUserWithEmailAndPassword(email, random); - userCredential.user.emailVerified.should.equal(false); - firebase.auth().currentUser.email.should.equal(email); - firebase.auth().currentUser.emailVerified.should.equal(false); - - try { - await firebase.auth().sendPasswordResetEmail(email); - const { oobCode } = await getLastOob(email); - await firebase.auth().verifyPasswordResetCode(oobCode); - } catch (error) { - throw new Error('sendPasswordResetEmail() caused an error', error); - } finally { - await firebase.auth().currentUser.delete(); - } - } - }); - - it('should fail to verify with invalid code', async function () { - const random = Utils.randString(12, '#a'); - const email = `${random}@${random}.com`; - const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); - userCredential.user.emailVerified.should.equal(false); - firebase.auth().currentUser.email.should.equal(email); - firebase.auth().currentUser.emailVerified.should.equal(false); - - let didError = false; - try { - await firebase.auth().sendPasswordResetEmail(email); - const { oobCode } = await getLastOob(email); - await firebase.auth().verifyPasswordResetCode(oobCode + 'badcode'); - throw new Error('Invalid code should throw an error'); - } catch (error) { - didError = true; - error.code.should.equal('auth/invalid-action-code'); - } finally { - await firebase.auth().currentUser.delete(); - } - didError.should.equal(true); - }); - - it('should change password correctly OOB', async function () { - const random = Utils.randString(12, '#a'); - const email = `${random}@${random}.com`; - const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); - userCredential.user.emailVerified.should.equal(false); - firebase.auth().currentUser.email.should.equal(email); - firebase.auth().currentUser.emailVerified.should.equal(false); - - try { - await firebase.auth().sendPasswordResetEmail(email); - const { oobCode } = await getLastOob(email); - await resetPassword(oobCode, 'testNewPassword'); - await firebase.auth().signOut(); - await Utils.sleep(50); - await firebase.auth().signInWithEmailAndPassword(email, 'testNewPassword'); - } catch (error) { - throw new Error('sendPasswordResetEmail() caused an error', error); - } finally { - await firebase.auth().currentUser.delete(); - } - }); - - it('should change password correctly via API', async function () { - const random = Utils.randString(12, '#a'); - const email = `${random}@${random}.com`; - const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); - userCredential.user.emailVerified.should.equal(false); - firebase.auth().currentUser.email.should.equal(email); - firebase.auth().currentUser.emailVerified.should.equal(false); - - try { - await firebase.auth().sendPasswordResetEmail(email); - const { oobCode } = await getLastOob(email); - await firebase.auth().confirmPasswordReset(oobCode, 'testNewPassword'); - await firebase.auth().signOut(); - await Utils.sleep(50); - await firebase.auth().signInWithEmailAndPassword(email, 'testNewPassword'); - } catch (error) { - throw new Error('sendPasswordResetEmail() caused an error', error); - } finally { - await firebase.auth().currentUser.delete(); - } - }); - }); - - describe('useDeviceLanguage()', function () { - it('should throw an unsupported error', function () { - (() => { - firebase.auth().useDeviceLanguage(); - }).should.throw( - 'firebase.auth().useDeviceLanguage() is unsupported by the native Firebase SDKs.', - ); - }); - }); - - describe('useUserAccessGroup()', function () { - // Android simply does Promise.resolve, that is sufficient for this test multi-platform - it('should return "null" when accessing a group that exists', async function () { - const successfulKeychain = await firebase - .auth() - .useUserAccessGroup('YYX2P3XVJ7.com.invertase.testing'); // iOS signing team is YYX2P3XVJ7 - - should.not.exist(successfulKeychain); - - //clean up - const resetKeychain = await firebase.auth().useUserAccessGroup(null); - - should.not.exist(resetKeychain); - }); - - it('should throw when requesting an inaccessible group', async function () { - // Android will never throw, so this test is iOS only - if (Platform.ios) { - try { - await firebase.auth().useUserAccessGroup('there.is.no.way.this.group.exists'); - throw new Error('Should have thrown an error for inaccessible group'); - } catch (e) { - e.message.should.containEql('auth/keychain-error'); - } - } - }); - }); - - describe('setTenantId()', function () { - it('should return null if tenantId unset', function () { - should.not.exist(firebase.auth().tenantId); - }); - - if (!Platform.android) { - it('should set tenantId and clear back to null', async function () { - await firebase.auth().setTenantId('e2e-tenant-id'); - firebase.auth().tenantId.should.equal('e2e-tenant-id'); - }); - - after(async function () { - await firebase.auth().setTenantId(null); - should.not.exist(firebase.auth().tenantId); - }); - } - - // multi-tenant is not supported by the firebase auth emulator, and requires a valid multi-tenant tenantid - // After setting this, next user creation will result in internal error on emulator, or auth/invalid-tenant-id live - // it('should return tenantId correctly after setting', async function () { - // await firebase.auth().setTenantId('testTenantId'); - // firebase.auth().tenantId.should.equal('testTenantId'); - // }); - // it('user should have tenant after setting tenantId', async function () { - // await firebase.auth().setTenantId('userTestTenantId'); - // firebase.auth().tenantId.should.equal('userTestTenantId'); - // const random = Utils.randString(12, '#a'); - // const email = `${random}@${random}.com`; - // const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random); - // userCredential.user.tenantId.should.equal('userTestTenantId'); - // }); - }); - }); - describe('modular', function () { before(async function () { const { getApp } = modular; @@ -1125,51 +50,6 @@ describe('auth() modular', function () { await Utils.sleep(50); } }); - - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const { getApp } = modular; - const { getAuth } = authModular; - - const auth = getAuth(getApp()); - should.exist(auth); - }); - - // removing as pending if module.options.hasMultiAppSupport = true - it('supports multiple apps', async function () { - const { getApp } = modular; - const { getAuth } = authModular; - - const defaultAuth = getAuth(getApp()); - defaultAuth.app.name.should.equal('[DEFAULT]'); - - const secondaryApp = getApp('secondaryFromNative'); - const secondaryAuth = getAuth(secondaryApp); - secondaryAuth.app.name.should.equal('secondaryFromNative'); - - secondaryApp.auth().app.name.should.equal('secondaryFromNative'); - }); - - it('supports an app initialized with custom authDomain', async function () { - if (Platform.other) return; - - const { getAuth, getCustomAuthDomain } = authModular; - const { getApp, initializeApp, deleteApp } = modular; - - const name = `testscoreapp${FirebaseHelpers.id}`; - const platformAppConfig = FirebaseHelpers.app.config(); - platformAppConfig.authDomain = 'example.com'; - const newApp = await initializeApp(platformAppConfig, name); - const secondaryApp = getApp(name); - const secondaryAuth = getAuth(secondaryApp); - secondaryAuth.app.name.should.equal(name); - getAuth(secondaryApp).app.name.should.equal(name); - const customAuthDomain = await getCustomAuthDomain(secondaryAuth); - customAuthDomain.should.equal(platformAppConfig.authDomain); - return deleteApp(newApp); - }); - }); - describe('applyActionCode()', function () { // Needs a different setup to work against the auth emulator xit('works as expected', async function () { diff --git a/packages/auth/e2e/emailLink.e2e.js b/packages/auth/e2e/emailLink.e2e.js index f57bca34b7..a5df456285 100644 --- a/packages/auth/e2e/emailLink.e2e.js +++ b/packages/auth/e2e/emailLink.e2e.js @@ -65,28 +65,6 @@ describe('auth() -> emailLink Provider', function () { } }); }); - - describe('namespaced', function () { - before(function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - after(function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('sendSignInLinkToEmail', function () { - it('works with default settings', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - - await firebase.auth().sendSignInLinkToEmail(email); - }); - }); - }); - describe('isSignInWithEmailLink', function () { it('should return true/false', async function () { const { getAuth, isSignInWithEmailLink } = authModular; diff --git a/packages/auth/e2e/multiFactor.e2e.js b/packages/auth/e2e/multiFactor.e2e.js index 5da0b9e0a7..9519d9974a 100644 --- a/packages/auth/e2e/multiFactor.e2e.js +++ b/packages/auth/e2e/multiFactor.e2e.js @@ -15,517 +15,6 @@ describe('multi-factor modular', function () { if (Platform.other) { return; } - - describe('firebase v8 compatibility', function () { - beforeEach(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - - await clearAllUsers(); - await firebase.auth().createUserWithEmailAndPassword(TEST_EMAIL, TEST_PASS); - if (firebase.auth().currentUser) { - await firebase.auth().signOut(); - await Utils.sleep(50); - } - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('has no multi-factor information if not enrolled', async function () { - await firebase.auth().signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS); - const { multiFactor } = firebase.auth().currentUser; - multiFactor.should.be.an.Object(); - multiFactor.enrolledFactors.should.be.an.Array(); - multiFactor.enrolledFactors.length.should.equal(0); - return Promise.resolve(); - }); - - describe('sign-in', function () { - it('requires multi-factor auth when enrolled', async function () { - if (Platform.ios) { - this.skip(); - } - const { phoneNumber, email, password } = await createUserWithMultiFactor(); - - try { - await firebase.auth().signInWithEmailAndPassword(email, password); - } catch (e) { - e.message.should.equal( - '[auth/multi-factor-auth-required] Please complete a second factor challenge to finish signing into this account.', - ); - const resolver = firebase.auth().getMultiFactorResolver(e); - resolver.should.be.an.Object(); - resolver.hints.should.be.an.Array(); - resolver.hints.length.should.equal(1); - resolver.session.should.be.a.String(); - - const verificationId = await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(resolver.hints[0], resolver.session); - verificationId.should.be.a.String(); - - let verificationCode = await getLastSmsCode(phoneNumber); - if (verificationCode == null) { - // iOS simulator uses a masked phone number - const maskedNumber = '+********' + phoneNumber.substring(phoneNumber.length - 4); - verificationCode = await getLastSmsCode(maskedNumber); - } - const credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - verificationCode, - ); - const multiFactorAssertion = - firebase.auth.PhoneMultiFactorGenerator.assertion(credential); - return resolver - .resolveSignIn(multiFactorAssertion) - .then(userCreds => { - userCreds.should.be.an.Object(); - userCreds.user.should.be.an.Object(); - userCreds.user.email.should.equal('verified@example.com'); - userCreds.user.multiFactor.should.be.an.Object(); - userCreds.user.multiFactor.enrolledFactors.length.should.equal(1); - return Promise.resolve(); - }) - .catch(e => { - return Promise.reject(e); - }); - } - - return Promise.reject( - new Error('Multi-factor users need to handle an exception on sign-in'), - ); - }); - - it('reports an error when providing an invalid sms code', async function () { - if (Platform.ios) { - this.skip(); - } - - const { phoneNumber, email, password } = await createUserWithMultiFactor(); - - try { - await firebase.auth().signInWithEmailAndPassword(email, password); - } catch (e) { - e.message.should.equal( - '[auth/multi-factor-auth-required] Please complete a second factor challenge to finish signing into this account.', - ); - const resolver = firebase.auth().getMultiFactorResolver(e); - const verificationId = await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(resolver.hints[0], resolver.session); - - const credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - 'incorrect', - ); - const assertion = firebase.auth.PhoneMultiFactorGenerator.assertion(credential); - try { - await resolver.resolveSignIn(assertion); - } catch (e) { - e.message - .toLocaleLowerCase() - .should.containEql('[auth/invalid-verification-code]'.toLocaleLowerCase()); - - const verificationId = await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(resolver.hints[0], resolver.session); - const verificationCode = await getLastSmsCode(phoneNumber); - const credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - verificationCode, - ); - const assertion = firebase.auth.PhoneMultiFactorGenerator.assertion(credential); - await resolver.resolveSignIn(assertion); - firebase.auth().currentUser.email.should.equal(email); - return Promise.resolve(); - } - } - return Promise.reject(); - }); - - it('reports an error when providing an invalid verification code', async function () { - if (Platform.ios) { - this.skip(); - } - const { phoneNumber, email, password } = await createUserWithMultiFactor(); - - try { - await firebase.auth().signInWithEmailAndPassword(email, password); - } catch (e) { - e.message.should.equal( - '[auth/multi-factor-auth-required] Please complete a second factor challenge to finish signing into this account.', - ); - const resolver = firebase.auth().getMultiFactorResolver(e); - await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(resolver.hints[0], resolver.session); - const verificationCode = await getLastSmsCode(phoneNumber); - - const credential = firebase.auth.PhoneAuthProvider.credential( - 'incorrect', - verificationCode, - ); - const assertion = firebase.auth.PhoneMultiFactorGenerator.assertion(credential); - try { - await resolver.resolveSignIn(assertion); - } catch (e) { - e.message.should.equal( - '[auth/invalid-verification-id] The verification ID used to create the phone auth credential is invalid.', - ); - - return Promise.resolve(); - } - } - return Promise.reject(); - }); - - it('reports an error when using an unknown factor', async function () { - const { email, password } = await createUserWithMultiFactor(); - - try { - await firebase.auth().signInWithEmailAndPassword(email, password); - } catch (e) { - e.message.should.equal( - '[auth/multi-factor-auth-required] Please complete a second factor challenge to finish signing into this account.', - ); - const resolver = firebase.auth().getMultiFactorResolver(e); - const unknownFactor = { - uid: 'notknown', - }; - try { - await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(unknownFactor, resolver.session); - } catch (e) { - e.code.should.equal('auth/multi-factor-info-not-found'); - e.message.should.equal( - '[auth/multi-factor-info-not-found] The user does not have a second factor matching the identifier provided.', - ); - - return Promise.resolve(); - } - } - return Promise.reject(); - }); - }); - - describe('enroll', function () { - it("can't enroll an existing user without verified email", async function () { - if (Platform.ios) { - this.skip(); - } - - await firebase.auth().signInWithEmailAndPassword(TEST_EMAIL, TEST_PASS); - - try { - const multiFactorUser = await firebase.auth().multiFactor(firebase.auth().currentUser); - const session = await multiFactorUser.getSession(); - await firebase - .auth() - .verifyPhoneNumberForMultiFactor({ phoneNumber: getRandomPhoneNumber(), session }); - } catch (e) { - e.message.should.equal( - '[auth/unverified-email] This operation requires a verified email.', - ); - e.code.should.equal('auth/unverified-email'); - return Promise.resolve(); - } - - return Promise.reject(new Error('Should throw error for unverified user.')); - }); - - it('can enroll new factor', async function () { - if (Platform.ios) { - this.skip(); - } - - try { - await createVerifiedUser('verified@example.com', 'test123'); - const phoneNumber = getRandomPhoneNumber(); - - should.deepEqual(firebase.auth().currentUser.multiFactor.enrolledFactors, []); - const multiFactorUser = await firebase.auth().multiFactor(firebase.auth().currentUser); - - const session = await multiFactorUser.getSession(); - - const verificationId = await firebase - .auth() - .verifyPhoneNumberForMultiFactor({ phoneNumber, session }); - const verificationCode = await getLastSmsCode(phoneNumber); - const cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode); - const multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred); - await multiFactorUser.enroll(multiFactorAssertion, 'Hint displayName'); - - const enrolledFactors = firebase.auth().currentUser.multiFactor.enrolledFactors; - enrolledFactors.length.should.equal(1); - enrolledFactors[0].displayName.should.equal('Hint displayName'); - enrolledFactors[0].factorId.should.equal('phone'); - enrolledFactors[0].uid.should.be.a.String(); - enrolledFactors[0].enrollmentTime.should.be.a.String(); - } catch (e) { - return Promise.reject(e); - } - return Promise.resolve(); - }); - - it('can enroll new factor without display name', async function () { - if (Platform.ios) { - this.skip(); - } - - try { - await createVerifiedUser('verified@example.com', 'test123'); - const phoneNumber = getRandomPhoneNumber(); - - should.deepEqual(firebase.auth().currentUser.multiFactor.enrolledFactors, []); - const multiFactorUser = await firebase.auth().multiFactor(firebase.auth().currentUser); - - const session = await multiFactorUser.getSession(); - - const verificationId = await firebase - .auth() - .verifyPhoneNumberForMultiFactor({ phoneNumber, session }); - const verificationCode = await getLastSmsCode(phoneNumber); - const cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode); - const multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred); - await multiFactorUser.enroll(multiFactorAssertion); - - const enrolledFactors = firebase.auth().currentUser.multiFactor.enrolledFactors; - enrolledFactors.length.should.equal(1); - should.equal(enrolledFactors[0].displayName, null); - } catch (e) { - return Promise.reject(e); - } - return Promise.resolve(); - }); - - it('can enroll multiple factors', async function () { - if (Platform.ios) { - this.skip(); - } - - const { email, password, phoneNumber } = await createUserWithMultiFactor(); - await signInUserWithMultiFactor(email, password, phoneNumber); - - const anotherNumber = getRandomPhoneNumber(); - const multiFactorUser = await firebase.auth().multiFactor(firebase.auth().currentUser); - - const session = await multiFactorUser.getSession(); - const verificationId = await firebase - .auth() - .verifyPhoneNumberForMultiFactor({ phoneNumber: anotherNumber, session }); - const verificationCode = await getLastSmsCode(anotherNumber); - const cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode); - const multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred); - const displayName = 'Another displayName'; - await multiFactorUser.enroll(multiFactorAssertion, displayName); - - const enrolledFactors = firebase.auth().currentUser.multiFactor.enrolledFactors; - enrolledFactors.length.should.equal(2); - const matchingFactor = enrolledFactors.find(factor => factor.displayName === displayName); - matchingFactor.should.be.an.Object(); - matchingFactor.uid.should.be.a.String(); - matchingFactor.enrollmentTime.should.be.a.String(); - matchingFactor.factorId.should.equal('phone'); - - return Promise.resolve(); - }); - - it('can not enroll the same factor twice', async function () { - this.skip(); - // This test should probably be implemented but doesn't work: - // Every time the same phone number requests a verification code, - // the emulator endpoint does not return a code, even though the emulator log - // prints a code. - // See https://github.com/firebase/firebase-tools/issues/4290#issuecomment-1281260335 - /* - await clearAllUsers(); - const { email, password, phoneNumber } = await createUserWithMultiFactor(); - await signInUserWithMultiFactor(email, password, phoneNumber); - const multiFactorUser = await firebase.auth().multiFactor(firebase.auth().currentUser); - const session = await multiFactorUser.getSession(); - - const verificationId = await firebase - .auth() - .verifyPhoneNumberForMultiFactor({ phoneNumber, session }); - const verificationCode = await getLastSmsCode(phoneNumber); - - const cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode); - const multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred); - const displayName = 'Another displayName'; - try { - await multiFactorUser.enroll(multiFactorAssertion, displayName); - } catch (e) { - console.error(e); - return Promise.resolve(); - } - return Promise.reject(); - */ - }); - - it('throws an error for wrong verification id', async function () { - if (Platform.ios) { - this.skip(); - } - - const { phoneNumber, email, password } = await createUserWithMultiFactor(); - - // GIVEN a MultiFactorResolver - let resolver = null; - try { - await firebase.auth().signInWithEmailAndPassword(email, password); - return Promise.reject(); - } catch (e) { - e.message.should.equal( - '[auth/multi-factor-auth-required] Please complete a second factor challenge to finish signing into this account.', - ); - resolver = firebase.auth().getMultiFactorResolver(e); - } - await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(resolver.hints[0], resolver.session); - - // AND I request a verification code - const verificationCode = await getLastSmsCode(phoneNumber); - // AND I use an incorrect verificationId - const credential = firebase.auth.PhoneAuthProvider.credential( - 'wrongVerificationId', - verificationCode, - ); - const multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(credential); - - try { - // WHEN I try to resolve the sign-in - await resolver.resolveSignIn(multiFactorAssertion); - } catch (e) { - // THEN an error message is thrown - e.message.should.equal( - '[auth/invalid-verification-id] The verification ID used to create the phone auth credential is invalid.', - ); - return Promise.resolve(); - } - return Promise.reject(); - }); - - it('throws an error for unknown sessions', async function () { - const { email, password } = await createUserWithMultiFactor(); - let resolver = null; - try { - await firebase.auth().signInWithEmailAndPassword(email, password); - return Promise.reject(); - } catch (e) { - e.message.should.equal( - '[auth/multi-factor-auth-required] Please complete a second factor challenge to finish signing into this account.', - ); - resolver = firebase.auth().getMultiFactorResolver(e); - } - - try { - await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(resolver.hints[0], 'unknown-session'); - } catch (e) { - // THEN an error message is thrown - e.message.should.equal( - '[auth/invalid-multi-factor-session] No resolver for session found. Is the session id correct?', - ); - return Promise.resolve(); - } - return Promise.reject(); - }); - - it('throws an error for unknown verification code', async function () { - const { email, password } = await createUserWithMultiFactor(); - - // GIVEN a MultiFactorResolver - let resolver = null; - try { - await firebase.auth().signInWithEmailAndPassword(email, password); - return Promise.reject(); - } catch (e) { - e.message.should.equal( - '[auth/multi-factor-auth-required] Please complete a second factor challenge to finish signing into this account.', - ); - resolver = firebase.auth().getMultiFactorResolver(e); - } - const verificationId = await firebase - .auth() - .verifyPhoneNumberWithMultiFactorInfo(resolver.hints[0], resolver.session); - - // AND I use an incorrect verificationId - const credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - 'wrong-verification-code', - ); - const multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(credential); - - try { - // WHEN I try to resolve the sign-in - await resolver.resolveSignIn(multiFactorAssertion); - } catch (e) { - // THEN an error message is thrown - e.message - .toLocaleLowerCase() - .should.containEql('[auth/invalid-verification-code]'.toLocaleLowerCase()); - - return Promise.resolve(); - } - return Promise.reject(); - }); - - it('can not enroll with phone authentication (unsupported primary factor)', async function () { - if (Platform.ios) { - this.skip(); - } - - // GIVEN a user that only signs in with phone - const testPhone = getRandomPhoneNumber(); - const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); - const lastSmsCode = await getLastSmsCode(testPhone); - await confirmResult.confirm(lastSmsCode); - - // WHEN they attempt to enroll a second factor - const multiFactorUser = await firebase.auth().multiFactor(firebase.auth().currentUser); - const session = await multiFactorUser.getSession(); - try { - await firebase - .auth() - .verifyPhoneNumberForMultiFactor({ phoneNumber: '+1123123', session }); - } catch (e) { - e.message.should.equal( - '[auth/unsupported-first-factor] Enrolling a second factor or signing in with a multi-factor account requires sign-in with a supported first factor.', - ); - return Promise.resolve(); - } - return Promise.reject( - new Error('Enrolling a second factor when using phone authentication is not supported.'), - ); - }); - - it('can not enroll when phone number is missing + sign', async function () { - await createVerifiedUser('verified@example.com', 'test123'); - const multiFactorUser = firebase.auth().multiFactor(firebase.auth().currentUser); - const session = await multiFactorUser.getSession(); - try { - await firebase.auth().verifyPhoneNumberForMultiFactor({ phoneNumber: '491575', session }); - } catch (e) { - e.code.should.equal('auth/invalid-phone-number'); - e.message.should.equal( - '[auth/invalid-phone-number] The format of the phone number provided is incorrect. Please enter the ' + - 'phone number in a format that can be parsed into E.164 format. E.164 ' + - 'phone numbers are written in the format [+][country code][subscriber ' + - 'number including area code].', - ); - return Promise.resolve(); - } - return Promise.reject(); - }); - }); - }); - describe('modular', function () { beforeEach(async function () { const { getApp } = modular; diff --git a/packages/auth/e2e/phone.e2e.js b/packages/auth/e2e/phone.e2e.js index f67c68eefa..49683686fc 100644 --- a/packages/auth/e2e/phone.e2e.js +++ b/packages/auth/e2e/phone.e2e.js @@ -56,190 +56,6 @@ describe('auth() => Phone', function () { if (Platform.other) { return; } - - describe('firebase v8 compatibility', function () { - before(async function () { - const { getApp } = modular; - const { getAuth } = authModular; - try { - await clearAllUsers(); - } catch (e) { - throw e; - } - getAuth(getApp()).settings.appVerificationDisabledForTesting = true; - await Utils.sleep(50); - }); - - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - - if (firebase.auth().currentUser) { - await firebase.auth().signOut(); - await Utils.sleep(50); - } - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('signInWithPhoneNumber', function () { - it('signs in with a valid code', async function () { - const testPhone = getRandomPhoneNumber(); - const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); - confirmResult.verificationId.should.be.a.String(); - should.ok(confirmResult.verificationId.length, 'verificationId string should not be empty'); - confirmResult.confirm.should.be.a.Function(); - const lastSmsCode = await getLastSmsCode(testPhone); - const userCredential = await confirmResult.confirm(lastSmsCode); - assertPhoneUserPublicSurface(userCredential.user, testPhone); - }); - - it('errors on invalid code', async function () { - const testPhone = getRandomPhoneNumber(); - const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); - confirmResult.verificationId.should.be.a.String(); - should.ok(confirmResult.verificationId.length, 'verificationId string should not be empty'); - confirmResult.confirm.should.be.a.Function(); - // Get the last SMS code just to make absolutely sure we don't accidentally use it - const lastSmsCode = await getLastSmsCode(testPhone); - await confirmResult - .confirm(lastSmsCode === '000000' ? '111111' : '000000') - .should.be.rejected(); - // TODO test error code and message - - // If you don't consume the valid code, then it sticks around - await confirmResult.confirm(lastSmsCode); - }); - }); - - // TODO these are now heavily rate limited by Firebase so they fail often - // or take minutes to complete each test. - xdescribe('verifyPhoneNumber', function () { - it('successfully verifies', async function () { - const testPhone = getRandomPhoneNumber(); - const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); - const lastSmsCode = await getLastSmsCode(testPhone); - await confirmResult.confirm(lastSmsCode); - await firebase.auth().verifyPhoneNumber(testPhone, false, false); - }); - - it('uses the autoVerifyTimeout when a non boolean autoVerifyTimeoutOrForceResend is provided', async function () { - const testPhone = getRandomPhoneNumber(); - const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); - const lastSmsCode = await getLastSmsCode(testPhone); - await confirmResult.confirm(lastSmsCode); - await firebase.auth().verifyPhoneNumber(testPhone, 0, false); - }); - - it('throws an error with an invalid on event', async function () { - const testPhone = getRandomPhoneNumber(); - try { - await firebase - .auth() - .verifyPhoneNumber(testPhone) - .on('example', () => {}); - - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql( - "firebase.auth.PhoneAuthListener.on(*, _, _, _) 'event' must equal 'state_changed'.", - ); - return Promise.resolve(); - } - }); - - it('throws an error with an invalid observer event', async function () { - const testPhone = getRandomPhoneNumber(); - try { - await firebase - .auth() - .verifyPhoneNumber(testPhone) - .on('state_changed', null, null, () => {}); - - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql( - "firebase.auth.PhoneAuthListener.on(_, *, _, _) 'observer' must be a function.", - ); - return Promise.resolve(); - } - }); - - it('successfully runs verification complete handler', async function () { - const testPhone = getRandomPhoneNumber(); - const thenCb = sinon.spy(); - await firebase.auth().verifyPhoneNumber(testPhone).then(thenCb); - thenCb.should.be.calledOnce(); - const successAuthSnapshot = thenCb.args[0][0]; - if (Platform.ios) { - successAuthSnapshot.state.should.equal('sent'); - } else { - successAuthSnapshot.state.should.equal('timeout'); - } - }); - - it('successfully runs and calls success callback', async function () { - const testPhone = getRandomPhoneNumber(); - const successCb = sinon.spy(); - const observerCb = sinon.spy(); - const errorCb = sinon.spy(); - - await firebase - .auth() - .verifyPhoneNumber(testPhone) - .on('state_changed', observerCb, errorCb, successCb); - - await Utils.spyToBeCalledOnceAsync(successCb); - errorCb.should.be.callCount(0); - successCb.should.be.calledOnce(); - let observerAuthSnapshot = observerCb.args[0][0]; - const successAuthSnapshot = successCb.args[0][0]; - successAuthSnapshot.verificationId.should.be.a.String(); - if (Platform.ios) { - observerCb.should.be.calledOnce(); - successAuthSnapshot.state.should.equal('sent'); - } else { - // android waits for SMS auto retrieval which does not work on an emulator - // it gets a sent and a timeout message on observer, just the timeout on success - observerCb.should.be.calledTwice(); - observerAuthSnapshot = observerCb.args[1][0]; - successAuthSnapshot.state.should.equal('timeout'); - } - JSON.stringify(successAuthSnapshot).should.equal(JSON.stringify(observerAuthSnapshot)); - }); - - // TODO determine why this is not stable on the emulator, is it also not stable on real device? - xit('successfully runs and calls error callback', async function () { - const successCb = sinon.spy(); - const observerCb = sinon.spy(); - const errorCb = sinon.spy(); - - await firebase - .auth() - .verifyPhoneNumber('notaphonenumber') - .on('state_changed', observerCb, errorCb, successCb); - - await Utils.spyToBeCalledOnceAsync(errorCb); - errorCb.should.be.calledOnce(); - observerCb.should.be.calledOnce(); - // const observerEvent = observerCb.args[0][0]; - successCb.should.be.callCount(0); - // const errorEvent = errorCb.args[0][0]; - // errorEvent.error.should.containEql('auth/invalid-phone-number'); - // JSON.stringify(errorEvent).should.equal(JSON.stringify(observerEvent)); - }); - - it('catches an error and emits an error event', async function () { - const catchCb = sinon.spy(); - await firebase.auth().verifyPhoneNumber('badphonenumber').catch(catchCb); - catchCb.should.be.calledOnce(); - }); - }); - }); - describe('modular', function () { before(async function () { const { getApp } = modular; diff --git a/packages/auth/e2e/provider.e2e.js b/packages/auth/e2e/provider.e2e.js index 02d4b42a99..771004fb25 100644 --- a/packages/auth/e2e/provider.e2e.js +++ b/packages/auth/e2e/provider.e2e.js @@ -1,391 +1,4 @@ describe('auth() -> Providers', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - - if (firebase.auth().currentUser) { - await firebase.auth().signOut(); - await Utils.sleep(50); - } - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('EmailAuthProvider', function () { - describe('constructor', function () { - it('should throw an unsupported error', function () { - (() => new firebase.auth.EmailAuthProvider()).should.throw( - '`new EmailAuthProvider()` is not supported on the native Firebase SDKs.', - ); - }); - }); - - describe('credential', function () { - it('should return a credential object', function () { - const email = 'email@email.com'; - const password = 'password'; - const credential = firebase.auth.EmailAuthProvider.credential(email, password); - credential.providerId.should.equal('password'); - credential.token.should.equal(email); - credential.secret.should.equal(password); - }); - }); - - describe('credentialWithLink', function () { - it('should return a credential object', function () { - const email = 'email@email.com'; - const link = 'link'; - const credential = firebase.auth.EmailAuthProvider.credentialWithLink(email, link); - credential.providerId.should.equal('emailLink'); - credential.token.should.equal(email); - credential.secret.should.equal(link); - }); - }); - - describe('EMAIL_PASSWORD_SIGN_IN_METHOD', function () { - it('should return password', function () { - firebase.auth.EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD.should.equal('password'); - }); - }); - - describe('EMAIL_LINK_SIGN_IN_METHOD', function () { - it('should return emailLink', function () { - firebase.auth.EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD.should.equal('emailLink'); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return password', function () { - firebase.auth.EmailAuthProvider.PROVIDER_ID.should.equal('password'); - }); - }); - }); - - describe('FacebookAuthProvider', function () { - describe('constructor', function () { - it('should throw an unsupported error', function () { - (() => new firebase.auth.FacebookAuthProvider()).should.throw( - '`new FacebookAuthProvider()` is not supported on the native Firebase SDKs.', - ); - }); - }); - - describe('credential', function () { - it('should return a credential object', function () { - const token = '123456'; - const credential = firebase.auth.FacebookAuthProvider.credential(token); - credential.providerId.should.equal('facebook.com'); - credential.signInMethod.should.equal('facebook.com'); - credential.token.should.equal(token); - credential.secret.should.equal(''); - credential.accessToken.should.equal(token); - credential.toJSON().accessToken.should.equal(token); - }); - }); - - describe('credentialLimitedLogin', function () { - it('should return a credential object', function () { - const token = '123456'; - const nonce = '654321'; - const credential = firebase.auth.FacebookAuthProvider.credential(token, nonce); - credential.providerId.should.equal('facebook.com'); - credential.token.should.equal(token); - credential.secret.should.equal(nonce); - credential.rawNonce.should.equal(nonce); - credential.toJSON().nonce.should.equal(nonce); - }); - }); - - describe('credential extraction helpers', function () { - it('should return null for native provider results and errors', function () { - should.equal(firebase.auth.FacebookAuthProvider.credentialFromResult({}), null); - should.equal(firebase.auth.FacebookAuthProvider.credentialFromError({}), null); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return facebook.com', function () { - firebase.auth.FacebookAuthProvider.PROVIDER_ID.should.equal('facebook.com'); - }); - }); - - describe('FACEBOOK_SIGN_IN_METHOD', function () { - it('should return facebook.com', function () { - firebase.auth.FacebookAuthProvider.FACEBOOK_SIGN_IN_METHOD.should.equal('facebook.com'); - }); - }); - }); - - describe('GithubAuthProvider', function () { - describe('constructor', function () { - it('should throw an unsupported error', function () { - (() => new firebase.auth.GithubAuthProvider()).should.throw( - '`new GithubAuthProvider()` is not supported on the native Firebase SDKs.', - ); - }); - }); - - describe('credential', function () { - it('should return a credential object', function () { - const token = '123456'; - const credential = firebase.auth.GithubAuthProvider.credential(token); - credential.providerId.should.equal('github.com'); - credential.token.should.equal(token); - credential.secret.should.equal(''); - }); - }); - - describe('credential extraction helpers', function () { - it('should return null for native provider results and errors', function () { - should.equal(firebase.auth.GithubAuthProvider.credentialFromResult({}), null); - should.equal(firebase.auth.GithubAuthProvider.credentialFromError({}), null); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return github.com', function () { - firebase.auth.GithubAuthProvider.PROVIDER_ID.should.equal('github.com'); - }); - }); - }); - - describe('GoogleAuthProvider', function () { - describe('constructor', function () { - it('should throw an unsupported error', function () { - (() => new firebase.auth.GoogleAuthProvider()).should.throw( - '`new GoogleAuthProvider()` is not supported on the native Firebase SDKs.', - ); - }); - }); - - describe('credential', function () { - it('should return a credential object', function () { - const token = '123456'; - const secret = '654321'; - const credential = firebase.auth.GoogleAuthProvider.credential(token, secret); - credential.providerId.should.equal('google.com'); - credential.token.should.equal(token); - credential.secret.should.equal(secret); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return google.com', function () { - firebase.auth.GoogleAuthProvider.PROVIDER_ID.should.equal('google.com'); - }); - }); - - describe('credential extraction helpers', function () { - it('should return null for native provider results and errors', function () { - should.equal(firebase.auth.GoogleAuthProvider.credentialFromResult({}), null); - should.equal(firebase.auth.GoogleAuthProvider.credentialFromError({}), null); - }); - }); - }); - - describe('OAuthProvider', function () { - describe('credential', function () { - it('should return a credential object', function () { - const idToken = '123456'; - const accessToken = '654321'; - const provider = new firebase.auth.OAuthProvider('apple.com'); - const credential = provider.credential({ idToken, accessToken, rawNonce: 'nonce' }); - credential.providerId.should.equal('apple.com'); - credential.token.should.equal(idToken); - credential.secret.should.equal('nonce'); - credential.idToken.should.equal(idToken); - credential.accessToken.should.equal(accessToken); - credential.rawNonce.should.equal('nonce'); - credential.toJSON().rawNonce.should.equal('nonce'); - }); - - it('should return a credential object from json', function () { - const idToken = '123456'; - const accessToken = '654321'; - const credential = firebase.auth.OAuthProvider.credentialFromJSON({ - providerId: 'apple.com', - idToken, - accessToken, - nonce: 'nonce', - }); - credential.providerId.should.equal('apple.com'); - credential.token.should.equal(idToken); - credential.secret.should.equal('nonce'); - credential.idToken.should.equal(idToken); - credential.accessToken.should.equal(accessToken); - credential.rawNonce.should.equal('nonce'); - }); - - it('should return a credential object from a json string', function () { - const idToken = '123456'; - const accessToken = '654321'; - const credential = firebase.auth.OAuthProvider.credentialFromJSON( - JSON.stringify({ - providerId: 'apple.com', - idToken, - accessToken, - rawNonce: 'nonce', - }), - ); - credential.providerId.should.equal('apple.com'); - credential.token.should.equal(idToken); - credential.secret.should.equal('nonce'); - credential.idToken.should.equal(idToken); - credential.accessToken.should.equal(accessToken); - credential.rawNonce.should.equal('nonce'); - }); - - it('should map access-token-only json credentials to the secret bridge field', function () { - const accessToken = '654321'; - const credential = firebase.auth.OAuthProvider.credentialFromJSON({ - providerId: 'oauth', - accessToken, - }); - credential.providerId.should.equal('oauth'); - credential.token.should.equal(''); - credential.secret.should.equal(accessToken); - credential.accessToken.should.equal(accessToken); - }); - - it('should throw if json is missing a provider id', function () { - (() => - firebase.auth.OAuthProvider.credentialFromJSON({ - idToken: '123456', - })).should.throw( - "firebase.auth.OAuthProvider.credentialFromJSON(*) expected 'providerId' to be a string value.", - ); - }); - - it('should throw if string json is malformed', function () { - (() => firebase.auth.OAuthProvider.credentialFromJSON('{')).should.throw( - "firebase.auth.OAuthProvider.credentialFromJSON(*) expected 'json' to be a valid JSON string.", - ); - }); - - it('should map access-token-only credentials to the secret bridge field', function () { - const accessToken = '654321'; - const provider = new firebase.auth.OAuthProvider('oauth'); - const credential = provider.credential({ accessToken }); - credential.providerId.should.equal('oauth'); - credential.token.should.equal(''); - credential.secret.should.equal(accessToken); - credential.accessToken.should.equal(accessToken); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return microsoft', function () { - const provider = new firebase.auth.OAuthProvider('microsoft.com'); - provider.PROVIDER_ID.should.equal('microsoft.com'); - }); - }); - - describe('provider object', function () { - it('should return a credential object with scopes and custom parameters', function () { - const provider = new firebase.auth.OAuthProvider('microsoft.com'); - provider.addScope('profile'); - provider.addScope('email'); - provider.setCustomParameters({ - prompt: 'consent', - }); - - provider.toObject().scopes.should.containEql('profile'); - provider.toObject().scopes.should.containEql('email'); - provider.toObject().customParameters.prompt.should.equal('consent'); - }); - }); - }); - - describe('PhoneAuthProvider', function () { - describe('credential', function () { - it('should return a credential object', function () { - const verificationId = '123456'; - const code = '654321'; - const credential = firebase.auth.PhoneAuthProvider.credential(verificationId, code); - credential.providerId.should.equal('phone'); - credential.token.should.equal(verificationId); - credential.secret.should.equal(code); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return phone', function () { - firebase.auth.PhoneAuthProvider.PROVIDER_ID.should.equal('phone'); - }); - }); - }); - - describe('TwitterAuthProvider', function () { - describe('constructor', function () { - it('should throw an unsupported error', function () { - (() => new firebase.auth.TwitterAuthProvider()).should.throw( - '`new TwitterAuthProvider()` is not supported on the native Firebase SDKs.', - ); - }); - }); - - describe('credential', function () { - it('should return a credential object', function () { - const token = '123456'; - const secret = '654321'; - const credential = firebase.auth.TwitterAuthProvider.credential(token, secret); - credential.providerId.should.equal('twitter.com'); - credential.token.should.equal(token); - credential.secret.should.equal(secret); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return twitter.com', function () { - firebase.auth.TwitterAuthProvider.PROVIDER_ID.should.equal('twitter.com'); - }); - }); - - describe('credential extraction helpers', function () { - it('should return null for native provider results and errors', function () { - should.equal(firebase.auth.TwitterAuthProvider.credentialFromResult({}), null); - should.equal(firebase.auth.TwitterAuthProvider.credentialFromError({}), null); - }); - }); - }); - - describe('OIDCAuthProvider', function () { - describe('constructor', function () { - it('should throw an unsupported error', function () { - (() => new firebase.auth.OIDCAuthProvider()).should.throw( - '`new OIDCAuthProvider()` is not supported on the native Firebase SDKs.', - ); - }); - }); - - describe('credential', function () { - it('should return a credential object', function () { - const token = '123456'; - const secret = '654321'; - const providerSuffix = 'sample-provider'; - const credential = firebase.auth.OIDCAuthProvider.credential( - providerSuffix, - token, - secret, - ); - credential.providerId.should.equal('oidc.' + providerSuffix); - credential.token.should.equal(token); - credential.secret.should.equal(secret); - }); - }); - - describe('PROVIDER_ID', function () { - it('should return oidc.', function () { - firebase.auth.OIDCAuthProvider.PROVIDER_ID.should.equal('oidc.'); - }); - }); - }); - }); - describe('modular', function () { beforeEach(async function () { const { getApp } = modular; diff --git a/packages/auth/e2e/user.e2e.js b/packages/auth/e2e/user.e2e.js index 08e0713e49..099bf99966 100644 --- a/packages/auth/e2e/user.e2e.js +++ b/packages/auth/e2e/user.e2e.js @@ -10,886 +10,6 @@ const { } = require('./helpers'); describe('auth().currentUser', function () { - describe('firebase v8 compatibility', function () { - before(async function () { - const { getAuth, createUserWithEmailAndPassword } = authModular; - await clearAllUsers(); - getAuth().settings.appVerificationDisabledForTesting = true; - await createUserWithEmailAndPassword(getAuth(), TEST_EMAIL, TEST_PASS); - }); - - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - - if (firebase.auth().currentUser) { - await firebase.auth().signOut(); - await Utils.sleep(50); - } - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('getIdToken()', function () { - it('should return a token', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - - const { user } = await firebase.auth().createUserWithEmailAndPassword(email, random); - - // Test - const token = await user.getIdToken(); - - // Assertions - token.should.be.a.String(); - token.length.should.be.greaterThan(24); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - }); - - describe('getIdTokenResult()', function () { - it('should return a valid IdTokenResult Object', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - - const { user } = await firebase.auth().createUserWithEmailAndPassword(email, random); - - // Test - const tokenResult = await user.getIdTokenResult(); - - tokenResult.token.should.be.a.String(); - tokenResult.authTime.should.be.a.String(); - tokenResult.issuedAtTime.should.be.a.String(); - tokenResult.expirationTime.should.be.a.String(); - - new Date(tokenResult.authTime).toString().should.not.equal('Invalid Date'); - new Date(tokenResult.issuedAtTime).toString().should.not.equal('Invalid Date'); - new Date(tokenResult.expirationTime).toString().should.not.equal('Invalid Date'); - - tokenResult.claims.should.be.a.Object(); - tokenResult.claims.iat.should.be.a.Number(); - tokenResult.claims.exp.should.be.a.Number(); - tokenResult.claims.iss.should.be.a.String(); - - new Date(tokenResult.issuedAtTime).getTime().should.equal(tokenResult.claims.iat * 1000); - new Date(tokenResult.expirationTime).getTime().should.equal(tokenResult.claims.exp * 1000); - - tokenResult.signInProvider.should.equal('password'); - tokenResult.token.length.should.be.greaterThan(24); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - }); - - describe('linkWithCredential()', function () { - // hanging against auth emulator? - it('should link anonymous account <-> email account', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - await firebase.auth().signInAnonymously(); - const { currentUser } = firebase.auth(); - - // Test - const credential = firebase.auth.EmailAuthProvider.credential(email, pass); - - const linkedUserCredential = await currentUser.linkWithCredential(credential); - - // Assertions - const linkedUser = linkedUserCredential.user; - linkedUser.should.be.an.Object(); - linkedUser.should.equal(firebase.auth().currentUser); - linkedUser.email.toLowerCase().should.equal(email.toLowerCase()); - linkedUser.isAnonymous.should.equal(false); - linkedUser.providerId.should.equal('firebase'); - linkedUser.providerData.should.be.an.Array(); - linkedUser.providerData.length.should.equal(1); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - - it('should error on link anon <-> email if email already exists', async function () { - await firebase.auth().signInAnonymously(); - const { currentUser } = firebase.auth(); - - // Test - try { - const credential = firebase.auth.EmailAuthProvider.credential(TEST_EMAIL, TEST_PASS); - await currentUser.linkWithCredential(credential); - - // Clean up - await firebase.auth().signOut(); - - // Reject - return Promise.reject(new Error('Did not error on link')); - } catch (error) { - // Assertions - error.code.should.equal('auth/email-already-in-use'); - - // Clean up - await firebase.auth().currentUser.delete(); - } - - return Promise.resolve(); - }); - }); - - describe('reauthenticateWithCredential()', function () { - it('should reauthenticate correctly', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - await firebase.auth().createUserWithEmailAndPassword(email, pass); - - // Test - const credential = firebase.auth.EmailAuthProvider.credential(email, pass); - - await firebase.auth().currentUser.reauthenticateWithCredential(credential); - - // Assertions - const { currentUser } = firebase.auth(); - currentUser.email.should.equal(email.toLowerCase()); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - }); - - describe('reload()', function () { - it('should not error', async function () { - await firebase.auth().signInAnonymously(); - - try { - await firebase.auth().currentUser.reload(); - await firebase.auth().signOut(); - } catch (error) { - // Reject - await firebase.auth().signOut(); - return Promise.reject(new Error('reload() caused an error', error)); - } - - return Promise.resolve(); - }); - }); - - describe('sendEmailVerification()', function () { - it('should error if actionCodeSettings.url is not present', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.sendEmailVerification({}); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.url' expected a string value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.url is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.sendEmailVerification({ url: 123 }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.url' expected a string value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if handleCodeInApp is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase - .auth() - .currentUser.sendEmailVerification({ url: 'string', handleCodeInApp: 123 }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.handleCodeInApp' expected a boolean value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.iOS is not an object', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.sendEmailVerification({ url: 'string', iOS: 123 }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.iOS' expected an object value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.iOS.bundleId is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase - .auth() - .currentUser.sendEmailVerification({ url: 'string', iOS: { bundleId: 123 } }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.iOS.bundleId' expected a string value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android is not an object', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.sendEmailVerification({ url: 'string', android: 123 }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.android' expected an object value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android.packageName is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase - .auth() - .currentUser.sendEmailVerification({ url: 'string', android: { packageName: 123 } }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.android.packageName' expected a string value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android.installApp is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.sendEmailVerification({ - url: 'string', - android: { packageName: 'packageName', installApp: 123 }, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.android.installApp' expected a boolean value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android.minimumVersion is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.sendEmailVerification({ - url: 'string', - android: { packageName: 'packageName', minimumVersion: 123 }, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.android.minimumVersion' expected a string value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should not error', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - - try { - await firebase.auth().currentUser.sendEmailVerification(); - } catch (error) { - return Promise.reject(new Error('sendEmailVerification() caused an error', error)); - } finally { - await firebase.auth().currentUser.delete(); - } - - return Promise.resolve(); - }); - - it('should correctly report emailVerified status', async function () { - const random = Utils.randString(12, '#a'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - firebase.auth().currentUser.email.should.equal(email); - firebase.auth().currentUser.emailVerified.should.equal(false); - - try { - await firebase.auth().currentUser.sendEmailVerification(); - const { oobCode } = await getLastOob(email); - await verifyEmail(oobCode); - firebase.auth().currentUser.emailVerified.should.equal(false); - await firebase.auth().currentUser.reload(); - firebase.auth().currentUser.emailVerified.should.equal(true); - } catch (error) { - return Promise.reject(new Error('sendEmailVerification() caused an error', error)); - } finally { - await firebase.auth().currentUser.delete(); - } - - return Promise.resolve(); - }); - - it('should work with actionCodeSettings', async function () { - const actionCodeSettings = { - handleCodeInApp: true, - url: 'https://react-native-firebase-testing.firebaseapp.com/foo', - }; - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - - try { - await firebase.auth().currentUser.sendEmailVerification(actionCodeSettings); - } catch (error) { - return Promise.reject( - new Error('sendEmailVerification(actionCodeSettings) error' + error.message), - ); - } finally { - await firebase.auth().currentUser.delete(); - } - - return Promise.resolve(); - }); - - it('should throw an error within an invalid action code url', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - await firebase.auth().createUserWithEmailAndPassword(email, random); - - (() => { - firebase.auth().currentUser.sendEmailVerification({ url: [] }); - }).should.throw( - "firebase.auth.User.sendEmailVerification(*) 'actionCodeSettings.url' expected a string value.", - ); - }); - }); - - describe('verifyBeforeUpdateEmail()', function () { - it('should error if newEmail is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(123); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'newEmail' expected a string value"); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.url is not present', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, {}); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.url' expected a string value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.url is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { url: 123 }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.url' expected a string value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if handleCodeInApp is not a boolean', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { - url: 'string', - handleCodeInApp: 123, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.handleCodeInApp' expected a boolean value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.iOS is not an object', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { - url: 'string', - iOS: 123, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.iOS' expected an object value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.iOS.bundleId is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { - url: 'string', - iOS: { bundleId: 123 }, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.iOS.bundleId' expected a string value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android is not an object', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { - url: 'string', - android: 123, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql("'actionCodeSettings.android' expected an object value."); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android.packageName is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { - url: 'string', - android: { packageName: 123 }, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.android.packageName' expected a string value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android.installApp is not a boolean', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { - url: 'string', - android: { packageName: 'string', installApp: 123 }, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.android.installApp' expected a boolean value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - it('should error if actionCodeSettings.android.minimumVersion is not a string', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - await firebase.auth().createUserWithEmailAndPassword(email, random); - try { - firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail, { - url: 'string', - android: { packageName: 'string', minimumVersion: 123 }, - }); - return Promise.reject(new Error('it did not error')); - } catch (error) { - error.message.should.containEql( - "'actionCodeSettings.android.minimumVersion' expected a string value.", - ); - } - await firebase.auth().currentUser.delete(); - }); - - // FIXME ios+android failing with an internal error against auth emulator - // com.google.firebase.FirebaseException: An internal error has occurred. [ VERIFY_AND_CHANGE_EMAIL ] - xit('should not error', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - - try { - await firebase.auth().createUserWithEmailAndPassword(email, random); - await firebase.auth().currentUser.verifyBeforeUpdateEmail(updateEmail); - } catch (_) { - return Promise.reject("'verifyBeforeUpdateEmail()' did not work"); - } - await firebase.auth().currentUser.delete(); - }); - - // FIXME ios+android failing with an internal error against auth emulator - // com.google.firebase.FirebaseException: An internal error has occurred. [ VERIFY_AND_CHANGE_EMAIL ] - xit('should work with actionCodeSettings', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const updateEmail = `${random2}@${random2}.com`; - const actionCodeSettings = { - url: 'https://react-native-firebase-testing.firebaseapp.com/foo', - }; - try { - await firebase.auth().createUserWithEmailAndPassword(email, random); - await firebase - .auth() - .currentUser.verifyBeforeUpdateEmail(updateEmail, actionCodeSettings); - await firebase.auth().currentUser.delete(); - } catch (_) { - try { - await firebase.auth().currentUser.delete(); - } catch (e) { - consle.log(e); - /* do nothing */ - } - - return Promise.reject( - "'verifyBeforeUpdateEmail()' with 'actionCodeSettings' did not work", - ); - } - return Promise.resolve(); - }); - }); - - describe('unlink()', function () { - it('should unlink the email address', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - await firebase.auth().signInAnonymously(); - const { currentUser } = firebase.auth(); - - const credential = firebase.auth.EmailAuthProvider.credential(email, pass); - await currentUser.linkWithCredential(credential); - - // Test - await currentUser.unlink(firebase.auth.EmailAuthProvider.PROVIDER_ID); - - // Assertions - const unlinkedUser = firebase.auth().currentUser; - unlinkedUser.providerData.should.be.an.Array(); - unlinkedUser.providerData.length.should.equal(0); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - }); - - describe('updateEmail()', function () { - it('should update the email address', async function () { - const random = Utils.randString(12, '#a'); - const random2 = Utils.randString(12, '#a'); - const email = `${random}@${random}.com`; - const email2 = `${random2}@${random2}.com`; - // Setup - await firebase.auth().createUserWithEmailAndPassword(email, random); - firebase.auth().currentUser.email.should.equal(email); - - // Update user email - await firebase.auth().currentUser.updateEmail(email2); - - // Assertions - firebase.auth().currentUser.email.should.equal(email2); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - }); - - describe('updatePhoneNumber()', function () { - // Phone number auth is not supported on other platforms - if (Platform.other) { - return; - } - - it('should update the phone number', async function () { - const testPhone = await getRandomPhoneNumber(); - const confirmResult = await firebase.auth().signInWithPhoneNumber(testPhone); - const smsCode = await getLastSmsCode(testPhone); - await confirmResult.confirm(smsCode); - - firebase.auth().currentUser.phoneNumber.should.equal(testPhone); - - const newPhone = await getRandomPhoneNumber(); - const newPhoneVerificationId = await new Promise((resolve, reject) => { - firebase - .auth() - .verifyPhoneNumber(newPhone) - .on('state_changed', phoneAuthSnapshot => { - if (phoneAuthSnapshot.error) { - reject(phoneAuthSnapshot.error); - } else { - resolve(phoneAuthSnapshot.verificationId); - } - }); - }); - - try { - const newSmsCode = await getLastSmsCode(newPhone); - const credential = await firebase.auth.PhoneAuthProvider.credential( - newPhoneVerificationId, - newSmsCode, - ); - - //Update with number? - await firebase - .auth() - .currentUser.updatePhoneNumber(credential) - .then($ => $); - } catch (e) { - throw e; - } - - firebase.auth().currentUser.phoneNumber.should.equal(newPhone); - }); - }); - - describe('updatePassword()', function () { - it('should update the password', async function () { - const random = Utils.randString(12, '#aA'); - const random2 = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - const pass2 = random2; - - // Setup - await firebase.auth().createUserWithEmailAndPassword(email, pass); - - // Update user password - await firebase.auth().currentUser.updatePassword(pass2); - - // Sign out - await firebase.auth().signOut(); - - // Log in with the new password - await firebase.auth().signInWithEmailAndPassword(email, pass2); - - // Assertions - firebase.auth().currentUser.should.be.an.Object(); - firebase.auth().currentUser.email.should.equal(email.toLowerCase()); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - - // Can only be run/reproduced locally with Firebase Auth rate limits lowered on the Firebase console. - xit('should throw too many requests when limit has been reached', async function () { - await Utils.sleep(10000); - try { - // Setup for creating new accounts - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - - const promises = []; - - // Create 10 accounts to force the error - [...Array(10).keys()].map($ => - promises.push( - new Promise(r => r(firebase.auth().createUserWithEmailAndPassword(email + $, pass))), - ), - ); - - await Promise.all(promises); - - return Promise.reject('Should have rejected'); - } catch (ex) { - ex.code.should.equal('auth/too-many-requests'); - return Promise.resolve(); - } - }); - }); - - describe('updateProfile()', function () { - it('should update the profile', async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - const pass = random; - const displayName = random; - const photoURL = `http://${random}.com/${random}.jpg`; - - // Setup - await firebase.auth().createUserWithEmailAndPassword(email, pass); - - // Update user profile - await firebase.auth().currentUser.updateProfile({ - displayName, - photoURL, - }); - - // Assertions - const user = firebase.auth().currentUser; - user.should.be.an.Object(); - user.email.should.equal(email.toLowerCase()); - user.displayName.should.equal(displayName); - user.photoURL.should.equal(photoURL); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - - it('should return a valid profile when signing in anonymously', async function () { - // Setup - await firebase.auth().signInAnonymously(); - const { currentUser } = firebase.auth(); - - // Assertions - currentUser.should.be.an.Object(); - should.equal(currentUser.email, null); - should.equal(currentUser.displayName, null); - should.equal(currentUser.emailVerified, false); - should.equal(currentUser.isAnonymous, true); - should.equal(currentUser.phoneNumber, null); - should.equal(currentUser.photoURL, null); - should.exist(currentUser.metadata.lastSignInTime); - should.exist(currentUser.metadata.creationTime); - should.deepEqual(currentUser.providerData, []); - should.exist(currentUser.providerId); - should.exist(currentUser.uid); - - // Clean up - await firebase.auth().currentUser.delete(); - }); - }); - - describe('linkWithPhoneNumber()', function () { - it('should throw an unsupported error', async function () { - await firebase.auth().signInAnonymously(); - (() => { - firebase.auth().currentUser.linkWithPhoneNumber(); - }).should.throw( - 'firebase.auth.User.linkWithPhoneNumber() is unsupported by the native Firebase SDKs.', - ); - await firebase.auth().signOut(); - }); - }); - - describe('reauthenticateWithPhoneNumber()', function () { - it('should throw an unsupported error', async function () { - await firebase.auth().signInAnonymously(); - (() => { - firebase.auth().currentUser.reauthenticateWithPhoneNumber(); - }).should.throw( - 'firebase.auth.User.reauthenticateWithPhoneNumber() is unsupported by the native Firebase SDKs.', - ); - await firebase.auth().signOut(); - }); - }); - - describe('refreshToken', function () { - it('should throw an unsupported error', async function () { - await firebase.auth().signInAnonymously(); - (() => firebase.auth().currentUser.refreshToken).should.throw( - 'firebase.auth.User.refreshToken is unsupported by the native Firebase SDKs.', - ); - await firebase.auth().signOut(); - }); - }); - - describe('user.metadata', function () { - it("should have the properties 'lastSignInTime' & 'creationTime' which are ISO strings", async function () { - const random = Utils.randString(12, '#aA'); - const email = `${random}@${random}.com`; - - const { user } = await firebase.auth().createUserWithEmailAndPassword(email, random); - - const { metadata } = user; - - should(metadata.lastSignInTime).be.a.String(); - should(metadata.creationTime).be.a.String(); - - new Date(metadata.lastSignInTime).getFullYear().should.equal(new Date().getFullYear()); - new Date(metadata.creationTime).getFullYear().should.equal(new Date().getFullYear()); - - await firebase.auth().currentUser.delete(); - }); - }); - }); - describe('modular', function () { before(async function () { const { getAuth, createUserWithEmailAndPassword } = authModular; diff --git a/packages/auth/lib/ConfirmationResult.ts b/packages/auth/lib/ConfirmationResult.ts index 94faaacf74..5f8dca235b 100644 --- a/packages/auth/lib/ConfirmationResult.ts +++ b/packages/auth/lib/ConfirmationResult.ts @@ -15,7 +15,7 @@ * */ -import type { FirebaseAuthTypes } from './types/namespaced'; +import type { UserCredential } from './types/auth'; import type { AuthInternal } from './types/internal'; export default class ConfirmationResult { @@ -27,13 +27,10 @@ export default class ConfirmationResult { this._verificationId = verificationId; } - confirm(verificationCode: string): Promise { + confirm(verificationCode: string): Promise { return this._auth.native .confirmationResultConfirm(verificationCode) - .then( - userCredential => - this._auth._setUserCredential(userCredential) as FirebaseAuthTypes.UserCredential, - ); + .then(userCredential => this._auth._setUserCredential(userCredential) as UserCredential); } get verificationId(): string { diff --git a/packages/auth/lib/MultiFactorResolver.ts b/packages/auth/lib/MultiFactorResolver.ts index 9232de5d67..68809785fa 100644 --- a/packages/auth/lib/MultiFactorResolver.ts +++ b/packages/auth/lib/MultiFactorResolver.ts @@ -1,12 +1,12 @@ /** * Base class to facilitate multi-factor authentication. */ -import type { FirebaseAuthTypes } from './types/namespaced'; +import type { MultiFactorInfo, MultiFactorSession, UserCredential } from './types/auth'; import type { AuthInternal } from './types/internal'; type ResolverLike = { - hints: FirebaseAuthTypes.MultiFactorInfo[]; - session: FirebaseAuthTypes.MultiFactorSession; + hints: MultiFactorInfo[]; + session: MultiFactorSession; }; type PhoneMultiFactorAssertion = { @@ -17,8 +17,8 @@ type PhoneMultiFactorAssertion = { }; export default class MultiFactorResolver { - readonly hints: FirebaseAuthTypes.MultiFactorInfo[]; - readonly session: FirebaseAuthTypes.MultiFactorSession; + readonly hints: MultiFactorInfo[]; + readonly session: MultiFactorSession; private readonly _auth: AuthInternal; constructor(auth: AuthInternal, resolver: ResolverLike) { @@ -27,7 +27,7 @@ export default class MultiFactorResolver { this.session = resolver.session; } - resolveSignIn(assertion: PhoneMultiFactorAssertion): Promise { + resolveSignIn(assertion: PhoneMultiFactorAssertion): Promise { const { token, secret, uid, verificationCode } = assertion; if (token && secret) { @@ -35,7 +35,7 @@ export default class MultiFactorResolver { this.session, token, secret, - ) as Promise; + ) as Promise; } if (uid && verificationCode) { @@ -43,7 +43,7 @@ export default class MultiFactorResolver { this.session, uid, verificationCode, - ) as Promise; + ) as Promise; } throw new Error('Invalid multi-factor assertion provided for sign-in resolution.'); diff --git a/packages/auth/lib/PhoneAuthListener.ts b/packages/auth/lib/PhoneAuthListener.ts index 1820cc088a..0f1f6cd8d1 100644 --- a/packages/auth/lib/PhoneAuthListener.ts +++ b/packages/auth/lib/PhoneAuthListener.ts @@ -23,7 +23,7 @@ import { } from '@react-native-firebase/app/dist/module/common'; import type { ReactNativeFirebase } from '@react-native-firebase/app'; import NativeFirebaseError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; -import type { FirebaseAuthTypes } from './types/namespaced'; +import type { PhoneAuthSnapshot, PhoneAuthError } from './types/auth'; import type { AuthInternal, NativePhoneAuthCredentialInternal, @@ -38,8 +38,6 @@ type PhoneAuthInternalEventType = type InternalEvents = Record; type PublicEvents = Record<'error' | 'event' | 'success', string>; -type PhoneAuthSnapshot = FirebaseAuthTypes.PhoneAuthSnapshot; -type PhoneAuthError = FirebaseAuthTypes.PhoneAuthError; let REQUEST_ID = 0; @@ -235,16 +233,14 @@ export default class PhoneAuthListener { const snapshot: PhoneAuthSnapshot = { verificationId: state.verificationId, code: null, - error: null, + error: new NativeFirebaseError( + { userInfo: state.error }, + this._jsStack, + 'auth', + ) as ReactNativeFirebase.NativeFirebaseError, state: 'error', }; - snapshot.error = new NativeFirebaseError( - { userInfo: state.error }, - this._jsStack, - 'auth', - ) as ReactNativeFirebase.NativeFirebaseError; - this._emitToObservers(snapshot); this._emitToErrorCb(snapshot); this._removeAllListeners(); diff --git a/packages/auth/lib/TotpMultiFactorGenerator.ts b/packages/auth/lib/TotpMultiFactorGenerator.ts index 87e820ecce..9b90427cee 100644 --- a/packages/auth/lib/TotpMultiFactorGenerator.ts +++ b/packages/auth/lib/TotpMultiFactorGenerator.ts @@ -17,7 +17,7 @@ import { isOther } from '@react-native-firebase/app/dist/module/common'; import { TotpSecret } from './TotpSecret'; -import { getAuth } from './modular'; +import { getAuth } from './index'; import type { Auth, MultiFactorSession, TotpMultiFactorAssertion } from './types/auth'; import type { AuthInternal } from './types/internal'; diff --git a/packages/auth/lib/User.ts b/packages/auth/lib/User.ts index 735a935f7c..469c1bc01b 100644 --- a/packages/auth/lib/User.ts +++ b/packages/auth/lib/User.ts @@ -22,10 +22,17 @@ import { isBoolean, } from '@react-native-firebase/app/dist/module/common'; import type { IdTokenResult, UserInfo, UserMetadata } from './types/auth'; -import type { FirebaseAuthTypes } from './types/namespaced'; +import type { + AuthProvider, + AuthCredential, + ActionCodeSettings, + MultiFactor, + UpdateProfile, + UserCredential, +} from './types/auth'; import type { AuthInternal, NativeUserInternal } from './types/internal'; -type AuthProviderWithObject = FirebaseAuthTypes.AuthProvider & { +type AuthProviderWithObject = AuthProvider & { toObject(): Record; }; @@ -65,7 +72,7 @@ export default class User { }; } - get multiFactor(): FirebaseAuthTypes.MultiFactor | null { + get multiFactor(): MultiFactor | null { return this._user.multiFactor || null; } @@ -114,52 +121,34 @@ export default class User { return this._auth.native.getIdTokenResult(forceRefresh) as Promise; } - linkWithCredential( - credential: FirebaseAuthTypes.AuthCredential, - ): Promise { + linkWithCredential(credential: AuthCredential): Promise { return this._auth.native .linkWithCredential(credential.providerId, credential.token, credential.secret) - .then( - userCredential => - this._auth._setUserCredential(userCredential) as FirebaseAuthTypes.UserCredential, - ); + .then(userCredential => this._auth._setUserCredential(userCredential) as UserCredential); } - linkWithPopup(provider: AuthProviderWithObject): Promise { + linkWithPopup(provider: AuthProviderWithObject): Promise { // call through to linkWithRedirect for shared implementation return this.linkWithRedirect(provider); } - linkWithRedirect(provider: AuthProviderWithObject): Promise { + linkWithRedirect(provider: AuthProviderWithObject): Promise { return this._auth.native .linkWithProvider(provider.toObject()) - .then( - userCredential => - this._auth._setUserCredential(userCredential) as FirebaseAuthTypes.UserCredential, - ); + .then(userCredential => this._auth._setUserCredential(userCredential) as UserCredential); } - reauthenticateWithCredential( - credential: FirebaseAuthTypes.AuthCredential, - ): Promise { + reauthenticateWithCredential(credential: AuthCredential): Promise { return this._auth.native .reauthenticateWithCredential(credential.providerId, credential.token, credential.secret) - .then( - userCredential => - this._auth._setUserCredential(userCredential) as FirebaseAuthTypes.UserCredential, - ); + .then(userCredential => this._auth._setUserCredential(userCredential) as UserCredential); } - reauthenticateWithPopup( - provider: AuthProviderWithObject, - ): Promise { + reauthenticateWithPopup(provider: AuthProviderWithObject): Promise { // call through to reauthenticateWithRedirect for shared implementation return this._auth.native .reauthenticateWithProvider(provider.toObject()) - .then( - userCredential => - this._auth._setUserCredential(userCredential) as FirebaseAuthTypes.UserCredential, - ); + .then(userCredential => this._auth._setUserCredential(userCredential) as UserCredential); } reauthenticateWithRedirect(provider: AuthProviderWithObject): Promise { @@ -176,9 +165,9 @@ export default class User { }); } - sendEmailVerification(actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings): Promise { + sendEmailVerification(actionCodeSettings?: ActionCodeSettings): Promise { if (isObject(actionCodeSettings)) { - const settings = actionCodeSettings as FirebaseAuthTypes.ActionCodeSettings; + const settings = actionCodeSettings as ActionCodeSettings; if (!isString(settings.url)) { throw new Error( @@ -249,7 +238,7 @@ export default class User { return Object.assign({}, this._user); } - unlink(providerId: string): Promise { + unlink(providerId: string): Promise { return this._auth.native.unlink(providerId).then(user => { const updatedUser = this._auth._setUser(user); if (!updatedUser) { @@ -271,7 +260,7 @@ export default class User { }); } - updatePhoneNumber(credential: FirebaseAuthTypes.AuthCredential): Promise { + updatePhoneNumber(credential: AuthCredential): Promise { return this._auth.native .updatePhoneNumber(credential.providerId, credential.token, credential.secret) .then(user => { @@ -279,7 +268,7 @@ export default class User { }); } - updateProfile(updates: FirebaseAuthTypes.UpdateProfile): Promise { + updateProfile(updates: UpdateProfile): Promise { return this._auth.native.updateProfile(updates).then(user => { this._auth._setUser(user); }); @@ -287,7 +276,7 @@ export default class User { verifyBeforeUpdateEmail( newEmail: string, - actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, + actionCodeSettings?: ActionCodeSettings, ): Promise { if (!isString(newEmail)) { throw new Error( @@ -296,7 +285,7 @@ export default class User { } if (isObject(actionCodeSettings)) { - const settings = actionCodeSettings as FirebaseAuthTypes.ActionCodeSettings; + const settings = actionCodeSettings as ActionCodeSettings; if (!isString(settings.url)) { throw new Error( diff --git a/packages/auth/lib/getMultiFactorResolver.ts b/packages/auth/lib/getMultiFactorResolver.ts index eecf39e183..c2b837f87c 100644 --- a/packages/auth/lib/getMultiFactorResolver.ts +++ b/packages/auth/lib/getMultiFactorResolver.ts @@ -1,13 +1,13 @@ import { isOther } from '@react-native-firebase/app/dist/module/common'; -import MultiFactorResolver from './MultiFactorResolver'; -import type { FirebaseAuthTypes } from './types/namespaced'; +import MultiFactorResolverClass from './MultiFactorResolver'; +import type { MultiFactorError, MultiFactorResolver } from './types/auth'; import type { AuthInternal } from './types/internal'; -type ErrorWithResolver = FirebaseAuthTypes.MultiFactorError & { +type ErrorWithResolver = MultiFactorError & { userInfo?: { resolver?: { - hints: FirebaseAuthTypes.MultiFactorResolver['hints']; - session: FirebaseAuthTypes.MultiFactorResolver['session']; + hints: MultiFactorResolver['hints']; + session: MultiFactorResolver['session']; }; }; }; @@ -21,21 +21,19 @@ type ErrorWithResolver = FirebaseAuthTypes.MultiFactorError & { export function getMultiFactorResolver( auth: AuthInternal, error: ErrorWithResolver, -): FirebaseAuthTypes.MultiFactorResolver | null { +): MultiFactorResolver | null { if (isOther) { - return auth.native.getMultiFactorResolver( - error, - ) as FirebaseAuthTypes.MultiFactorResolver | null; + return auth.native.getMultiFactorResolver(error) as MultiFactorResolver | null; } if ( error.hasOwnProperty('userInfo') && error.userInfo?.hasOwnProperty('resolver') && error.userInfo.resolver ) { - return new MultiFactorResolver( + return new MultiFactorResolverClass( auth, error.userInfo.resolver, - ) as unknown as FirebaseAuthTypes.MultiFactorResolver; + ) as unknown as MultiFactorResolver; } return null; diff --git a/packages/auth/lib/index.ts b/packages/auth/lib/index.ts index c0303c0578..3fbbf8afa3 100644 --- a/packages/auth/lib/index.ts +++ b/packages/auth/lib/index.ts @@ -14,7 +14,19 @@ * limitations under the License. * */ -// modular API + +/** + * Modular Auth API for React Native Firebase. + * + * Most exports mirror the [firebase-js-sdk modular Auth API](https://firebase.google.com/docs/reference/js/auth). + * Remarks on individual symbols call out React Native-specific behavior, unsupported + * web-only APIs, and return-type differences. + * + * @packageDocumentation + */ + +import './types/internal'; + export type * from './types/auth'; export { ActionCodeURL } from './ActionCodeURL'; export { @@ -23,9 +35,1838 @@ export { OAuthCredential, PhoneAuthCredential, } from './credentials'; -export * from './modular'; -// namespaced API -export * from './types/namespaced'; -export * from './namespaced'; -export { default } from './namespaced'; +import { + isAndroid, + isBoolean, + isNull, + isOther, + isString, + isValidUrl, + parseListenerOrObserver, +} from '@react-native-firebase/app/dist/module/common'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import { + FirebaseModule, + getOrCreateModularInstance, + type ModuleConfig, +} from '@react-native-firebase/app/dist/module/internal'; +import ConfirmationResultClass from './ConfirmationResult'; +import { ActionCodeURL } from './ActionCodeURL'; +import { PhoneAuthState } from './PhoneAuthState'; +import PhoneAuthListenerClass from './PhoneAuthListener'; +import PhoneMultiFactorGenerator from './PhoneMultiFactorGenerator'; +import TotpMultiFactorGenerator from './TotpMultiFactorGenerator'; +import { TotpSecret } from './TotpSecret'; +import Settings from './Settings'; +import UserClass from './User'; +import { getMultiFactorResolver as createMultiFactorResolver } from './getMultiFactorResolver'; +import { MultiFactorUser as MultiFactorUserModule } from './multiFactor'; +import AppleAuthProvider from './providers/AppleAuthProvider'; +import EmailAuthProvider from './providers/EmailAuthProvider'; +import FacebookAuthProvider from './providers/FacebookAuthProvider'; +import GithubAuthProvider from './providers/GithubAuthProvider'; +import GoogleAuthProvider from './providers/GoogleAuthProvider'; +import OAuthProvider from './providers/OAuthProvider'; +import OIDCAuthProvider from './providers/OIDCAuthProvider'; +import PhoneAuthProvider from './providers/PhoneAuthProvider'; +import TwitterAuthProvider from './providers/TwitterAuthProvider'; +import { version } from './version'; +import { + ActionCodeOperation, + FactorId, + OperationType, + ProviderId, + SignInMethod, +} from './constants'; +import fallBackModule from './web/RNFBAuthModule'; +import { PasswordPolicyMixin } from './password-policy/PasswordPolicyMixin'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import type { + ActionCodeInfo, + ActionCodeSettings, + AdditionalUserInfo, + AdditionalUserInfoNative, + ApplicationVerifier, + Auth, + AuthCredential, + AuthListenerCallback, + AuthProvider, + AuthSettings, + CompleteFn, + ConfirmationResult, + Dependencies, + EmulatorConfig, + ErrorFn, + IdTokenResult, + MultiFactorError, + MultiFactorInfo, + MultiFactorResolver, + MultiFactorSession, + MultiFactorUser, + NextOrObserver, + PasswordValidationStatus, + Persistence, + PhoneAuthCredential, + PhoneAuthListener, + PhoneMultiFactorEnrollInfoOptions, + PhoneMultiFactorInfo, + PopupRedirectResolver, + TotpMultiFactorInfo, + Unsubscribe, + User, + UserCredential, +} from './types/auth'; +import type { CallbackOrObserver } from './types/internal'; +import type { User as ModularUser } from './types/auth'; +import type { + ActionCodeInfoResultInternal, + AuthIdTokenChangedEventInternal, + AuthInternal, + AuthListenerCallbackInternal, + AuthProviderWithObjectInternal, + AuthStateChangedEventInternal, + ConfirmationResultResultInternal, + MultiFactorResolverResultInternal, + MultiFactorUserResultInternal, + MultiFactorUserSourceInternal, + NativePhoneAuthCredentialInternal, + NativeUserCredentialInternal, + NativeUserInternal, + PasswordPolicyInternal, + PasswordValidationStatusInternal, + PhoneAuthStateChangedEventInternal, + UserCredentialResultInternal, + UserInternal, +} from './types/internal'; + +type AuthErrorWithCodeInternal = Error & { + code?: string; +}; + +const nativeEvents = ['auth_state_changed', 'auth_id_token_changed', 'phone_auth_state_changed']; + +const namespace = 'auth'; +const nativeModuleName = 'RNFBAuthModule'; + +const config: ModuleConfig = { + namespace, + nativeModuleName, + nativeEvents, + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: false, +}; + +type BeforeAuthStateChangedEntry = { + callback: (user: ModularUser | null) => void | Promise; + onAbort?: () => void; +}; + +class FirebaseAuthModule extends FirebaseModule { + _user: User | null; + _settings: AuthSettings | null; + _authResult: boolean; + _languageCode: string; + _tenantId: string | null; + _projectPasswordPolicy: PasswordPolicyInternal | null; + _tenantPasswordPolicies: Record; + _emulatorConfig: EmulatorConfig | null; + _authStateReadyPromise: Promise | null; + _beforeAuthStateChangedCallbacks: BeforeAuthStateChangedEntry[]; + _getPasswordPolicyInternal!: () => PasswordPolicyInternal | null; + _updatePasswordPolicy!: () => Promise; + _recachePasswordPolicy!: () => Promise; + validatePassword!: (password: string) => Promise; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); + this._user = null; + this._settings = null; + this._authResult = false; + this._languageCode = this.native.APP_LANGUAGE[this.app.name] ?? ''; + this._tenantId = null; + this._projectPasswordPolicy = null; + this._tenantPasswordPolicies = {}; + this._emulatorConfig = null; + this._authStateReadyPromise = null; + this._beforeAuthStateChangedCallbacks = []; + + if (!this.languageCode) { + this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]'] ?? ''; + } + + const initialUser = this.native.APP_USER[this.app.name]; + if (initialUser) { + this._setUser(initialUser); + } + + this.emitter.addListener(this.eventNameForApp('auth_state_changed'), event => { + const authEvent = event as AuthStateChangedEventInternal; + void this._handleAuthStateChanged(authEvent.user); + }); + + this.emitter.addListener(this.eventNameForApp('phone_auth_state_changed'), event => { + const phoneAuthEvent = event as PhoneAuthStateChangedEventInternal; + const eventKey = `phone:auth:${phoneAuthEvent.requestKey}:${phoneAuthEvent.type}`; + this.emitter.emit(eventKey, phoneAuthEvent.state); + }); + + this.emitter.addListener(this.eventNameForApp('auth_id_token_changed'), event => { + const authEvent = event as AuthIdTokenChangedEventInternal; + this._setUser(authEvent.user); + this.emitter.emit(this.eventNameForApp('onIdTokenChanged'), this._user); + }); + + this.native.addAuthStateListener(); + this.native.addIdTokenListener(); + + // custom authDomain in only available from App's FirebaseOptions, + // but we need it in Auth if it exists. During app configuration we store + // mappings from app name to authDomain, this auth constructor + // is a reasonable time to use the mapping and set it into auth natively + if (!isOther) { + // Only supported on native platforms + this.native.configureAuthDomain(); + } + } + + get languageCode(): string { + return this._languageCode; + } + + set languageCode(code: string | null) { + // For modular API, not recommended to set languageCode directly as it should be set in the native SDKs first + if (!isString(code) && !isNull(code)) { + throw new Error( + "firebase.auth().languageCode = (*) expected 'languageCode' to be a string or null value", + ); + } + // as this is a setter, we can't use async/await. So we set it first so it is available immediately + if (code === null) { + this._languageCode = this.native.APP_LANGUAGE[this.app.name] ?? ''; + + if (!this.languageCode) { + this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]'] ?? ''; + } + } else { + this._languageCode = code; + } + // This sets it natively + void this.setLanguageCode(code); + } + + get config(): Record { + // Native iOS/Android Firebase Auth SDKs do not expose the firebase-js-sdk config object. + return {}; + } + + get tenantId(): string | null { + return this._tenantId; + } + + set tenantId(tenantId: string | null) { + void this.setTenantId(tenantId); + } + + get emulatorConfig(): EmulatorConfig | null { + return this._emulatorConfig; + } + + authStateReady(): Promise { + if (this._authResult) { + return Promise.resolve(); + } + + if (!this._authStateReadyPromise) { + this._authStateReadyPromise = new Promise(resolve => { + const unsubscribe = this.onAuthStateChanged(() => { + if (this._authResult) { + unsubscribe(); + resolve(); + } + }); + + if (this._authResult) { + unsubscribe(); + resolve(); + } + }); + } + + return this._authStateReadyPromise; + } + + beforeAuthStateChanged( + callback: (user: ModularUser | null) => void | Promise, + onAbort?: () => void, + ): Unsubscribe { + const entry: BeforeAuthStateChangedEntry = { callback, onAbort }; + this._beforeAuthStateChangedCallbacks.push(entry); + + return () => { + const index = this._beforeAuthStateChangedCallbacks.indexOf(entry); + if (index >= 0) { + this._beforeAuthStateChangedCallbacks.splice(index, 1); + } + }; + } + + async updateCurrentUser(user: User | null): Promise { + if (user === null) { + await this.signOut(); + return; + } + + const userInternal = user as unknown as { + _auth?: AuthInternal; + _user?: NativeUserInternal; + }; + + if (!userInternal._auth || userInternal._auth !== (this as unknown as AuthInternal)) { + throw new Error( + "firebase.auth().updateCurrentUser() expected 'user' to be an Auth instance from the same Firebase App", + ); + } + + if (!userInternal._user) { + throw new Error("firebase.auth().updateCurrentUser(*) expected 'user' to be a valid User"); + } + + this._setUser(userInternal._user); + } + + async _handleAuthStateChanged(nativeUser?: NativeUserInternal | null): Promise { + const pendingUser = nativeUser + ? (new UserClass(this as unknown as AuthInternal, nativeUser) as User) + : null; + + for (const { callback, onAbort } of [...this._beforeAuthStateChangedCallbacks]) { + try { + await callback(pendingUser as ModularUser | null); + } catch { + onAbort?.(); + return; + } + } + + this._setUser(nativeUser); + this.emitter.emit(this.eventNameForApp('onAuthStateChanged'), this._user); + } + + get settings(): AuthSettings { + if (!this._settings) { + this._settings = new Settings(this as unknown as AuthInternal) as AuthSettings; + } + return this._settings; + } + + get currentUser(): User | null { + return this._user; + } + + _setUser(user?: NativeUserInternal | null): User | null { + this._user = user ? (new UserClass(this as unknown as AuthInternal, user) as User) : null; + this._authResult = true; + this.emitter.emit(this.eventNameForApp('onUserChanged'), this._user); + return this._user; + } + + _setUserCredential(userCredential: NativeUserCredentialInternal): UserCredential { + const user = new UserClass(this as unknown as AuthInternal, userCredential.user) as User; + this._user = user; + this._authResult = true; + this.emitter.emit(this.eventNameForApp('onUserChanged'), this._user); + return { + additionalUserInfo: userCredential.additionalUserInfo, + user, + } as UserCredential; + } + + async setLanguageCode(code: string | null): Promise { + if (!isString(code) && !isNull(code)) { + throw new Error( + "firebase.auth().setLanguageCode(*) expected 'languageCode' to be a string or null value", + ); + } + + await this.native.setLanguageCode(code); + + if (code === null) { + this._languageCode = this.native.APP_LANGUAGE[this.app.name] ?? ''; + + if (!this.languageCode) { + this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]'] ?? ''; + } + } else { + this._languageCode = code; + } + } + + async setTenantId(tenantId: string | null): Promise { + if (!isString(tenantId) && !isNull(tenantId)) { + throw new Error("firebase.auth().setTenantId(*) expected 'tenantId' to be a string"); + } + await this.native.setTenantId(tenantId); + this._tenantId = tenantId; + } + + onAuthStateChanged(listenerOrObserver: CallbackOrObserver): () => void { + const listener = parseListenerOrObserver(listenerOrObserver) as AuthListenerCallback; + const subscription = this.emitter.addListener( + this.eventNameForApp('onAuthStateChanged'), + listener, + ); + + if (this._authResult) { + Promise.resolve().then(() => { + listener(this._user || null); + }); + } + return () => subscription.remove(); + } + + onIdTokenChanged(listenerOrObserver: CallbackOrObserver): () => void { + const listener = parseListenerOrObserver(listenerOrObserver) as AuthListenerCallback; + const subscription = this.emitter.addListener( + this.eventNameForApp('onIdTokenChanged'), + listener, + ); + + if (this._authResult) { + Promise.resolve().then(() => { + listener(this._user || null); + }); + } + return () => subscription.remove(); + } + + onUserChanged(listenerOrObserver: CallbackOrObserver): () => void { + const listener = parseListenerOrObserver(listenerOrObserver) as AuthListenerCallback; + const subscription = this.emitter.addListener(this.eventNameForApp('onUserChanged'), listener); + if (this._authResult) { + Promise.resolve().then(() => { + listener(this._user || null); + }); + } + + return () => { + subscription.remove(); + }; + } + + signOut(): Promise { + return this.native.signOut().then(() => { + this._setUser(null); + }); + } + + signInAnonymously(): Promise { + return this.native + .signInAnonymously() + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ); + } + + signInWithPhoneNumber(phoneNumber: string, forceResend?: boolean): Promise { + if (isAndroid) { + return this.native + .signInWithPhoneNumber(phoneNumber, forceResend || false) + .then( + (result: NativePhoneAuthCredentialInternal) => + new ConfirmationResultClass( + this as unknown as AuthInternal, + result.verificationId, + ) as unknown as ConfirmationResult, + ); + } + + return this.native + .signInWithPhoneNumber(phoneNumber) + .then( + (result: NativePhoneAuthCredentialInternal) => + new ConfirmationResultClass( + this as unknown as AuthInternal, + result.verificationId, + ) as unknown as ConfirmationResult, + ); + } + + verifyPhoneNumber( + phoneNumber: string, + autoVerifyTimeoutOrForceResend?: number | boolean, + forceResend?: boolean, + ): PhoneAuthListener { + let _forceResend = forceResend; + let _autoVerifyTimeout: number | undefined = 60; + + if (isBoolean(autoVerifyTimeoutOrForceResend)) { + _forceResend = autoVerifyTimeoutOrForceResend; + } else { + _autoVerifyTimeout = autoVerifyTimeoutOrForceResend; + } + + return new PhoneAuthListenerClass( + this as unknown as AuthInternal, + phoneNumber, + _autoVerifyTimeout, + _forceResend, + ) as PhoneAuthListener; + } + + verifyPhoneNumberWithMultiFactorInfo( + multiFactorHint: MultiFactorInfo, + session: MultiFactorSession, + ): Promise { + return this.native.verifyPhoneNumberWithMultiFactorInfo(multiFactorHint.uid, session); + } + + verifyPhoneNumberForMultiFactor( + phoneInfoOptions: PhoneMultiFactorEnrollInfoOptions, + ): Promise { + const { phoneNumber, session } = phoneInfoOptions; + return this.native.verifyPhoneNumberForMultiFactor(phoneNumber, session); + } + + resolveMultiFactorSignIn( + session: MultiFactorSession, + verificationId: string, + verificationCode: string, + ): Promise { + return this.native + .resolveMultiFactorSignIn(session, verificationId, verificationCode) + .then((userCredential: NativeUserCredentialInternal) => { + return this._setUserCredential(userCredential); + }); + } + + resolveTotpSignIn( + session: MultiFactorSession, + uid: string, + totpSecret: string, + ): Promise { + return this.native + .resolveTotpSignIn(session, uid, totpSecret) + .then((userCredential: NativeUserCredentialInternal) => { + return this._setUserCredential(userCredential); + }); + } + + createUserWithEmailAndPassword(email: string, password: string): Promise { + return ( + this.native + .createUserWithEmailAndPassword(email, password) + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ) + /* istanbul ignore next - native error handling cannot be unit tested */ + .catch((error: AuthErrorWithCodeInternal) => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }) + ); + } + + signInWithEmailAndPassword(email: string, password: string): Promise { + return ( + this.native + .signInWithEmailAndPassword(email, password) + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ) + /* istanbul ignore next - native error handling cannot be unit tested */ + .catch((error: AuthErrorWithCodeInternal) => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }) + ); + } + + signInWithCustomToken(customToken: string): Promise { + return this.native + .signInWithCustomToken(customToken) + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ); + } + + signInWithCredential(credential: AuthCredential): Promise { + return this.native + .signInWithCredential(credential.providerId, credential.token, credential.secret) + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ); + } + + revokeToken(authorizationCode: string): Promise { + return this.native.revokeToken(authorizationCode); + } + + sendPasswordResetEmail( + email: string, + actionCodeSettings: ActionCodeSettings | null = null, + ): Promise { + return this.native.sendPasswordResetEmail(email, actionCodeSettings); + } + + private _resolveActionCodeSettings(actionCodeSettings?: ActionCodeSettings): ActionCodeSettings { + if (actionCodeSettings && isString(actionCodeSettings.url)) { + return actionCodeSettings; + } + + const authDomain = this.app.options.authDomain; + let url = 'https://localhost'; + if (authDomain && isString(authDomain)) { + url = isValidUrl(authDomain) ? authDomain : `https://${authDomain}`; + } + + return { + ...(actionCodeSettings ?? {}), + url, + handleCodeInApp: actionCodeSettings?.handleCodeInApp ?? true, + }; + } + + sendSignInLinkToEmail(email: string, actionCodeSettings?: ActionCodeSettings): Promise { + return this.native.sendSignInLinkToEmail( + email, + this._resolveActionCodeSettings(actionCodeSettings), + ); + } + + isSignInWithEmailLink(emailLink: string): Promise { + return this.native.isSignInWithEmailLink(emailLink); + } + + signInWithEmailLink(email: string, emailLink?: string): Promise { + return this.native + .signInWithEmailLink(email, emailLink ?? '') + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ); + } + + confirmPasswordReset(code: string, newPassword: string): Promise { + return ( + this.native + .confirmPasswordReset(code, newPassword) + /* istanbul ignore next - native error handling cannot be unit tested */ + .catch((error: AuthErrorWithCodeInternal) => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }) + ); + } + + applyActionCode(code: string): Promise { + return this.native.applyActionCode(code).then(user => { + this._setUser(user); + }); + } + + checkActionCode(code: string): Promise { + return this.native.checkActionCode(code); + } + + fetchSignInMethodsForEmail(email: string): Promise { + return this.native.fetchSignInMethodsForEmail(email); + } + + verifyPasswordResetCode(code: string): Promise { + return this.native.verifyPasswordResetCode(code); + } + + useUserAccessGroup(userAccessGroup: string): Promise { + if (isAndroid) { + return Promise.resolve(); + } + return this.native.useUserAccessGroup(userAccessGroup).then(() => undefined); + } + + getRedirectResult(): Promise { + throw new Error( + 'firebase.auth().getRedirectResult() is unsupported by the native Firebase SDKs.', + ); + } + + setPersistence(): Promise { + throw new Error('firebase.auth().setPersistence() is unsupported by the native Firebase SDKs.'); + } + + signInWithPopup(provider: AuthProvider): Promise { + return this.native + .signInWithProvider((provider as AuthProviderWithObjectInternal).toObject()) + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ); + } + + signInWithRedirect(provider: AuthProvider): Promise { + return this.native + .signInWithProvider((provider as AuthProviderWithObjectInternal).toObject()) + .then((userCredential: NativeUserCredentialInternal) => + this._setUserCredential(userCredential), + ); + } + + // firebase issue - https://github.com/invertase/react-native-firebase/pull/655#issuecomment-349904680 + useDeviceLanguage(): void { + throw new Error( + 'firebase.auth().useDeviceLanguage() is unsupported by the native Firebase SDKs.', + ); + } + + useEmulator(url: string, options?: { disableWarnings?: boolean }): void { + if (!url || !isString(url) || !isValidUrl(url)) { + throw new Error('firebase.auth().useEmulator() takes a non-empty string URL'); + } + + let _url = url; + const androidBypassEmulatorUrlRemap = + typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && + this.firebaseJson.android_bypass_emulator_url_remap; + if (!androidBypassEmulatorUrlRemap && isAndroid && _url) { + if (_url.startsWith('http://localhost')) { + _url = _url.replace('http://localhost', 'http://10.0.2.2'); + // eslint-disable-next-line no-console + console.log( + 'Mapping auth host "localhost" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', + ); + } + if (_url.startsWith('http://127.0.0.1')) { + _url = _url.replace('http://127.0.0.1', 'http://10.0.2.2'); + // eslint-disable-next-line no-console + console.log( + 'Mapping auth host "127.0.0.1" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', + ); + } + } + + // Native calls take the host and port split out + const hostPortRegex = /^http:\/\/([\w\d-.]+):(\d+)$/; + const urlMatches = _url.match(hostPortRegex); + if (!urlMatches) { + throw new Error('firebase.auth().useEmulator() unable to parse host and port from URL'); + } + const host = urlMatches[1]; + const portString = urlMatches[2]; + if (!host) { + throw new Error('firebase.auth().useEmulator() unable to parse host from URL'); + } + const port = portString ? parseInt(portString, 10) : undefined; + this._emulatorConfig = { + protocol: 'http', + host, + port: port ?? null, + options: { + disableWarnings: options?.disableWarnings ?? false, + }, + }; + this.native.useEmulator(host, port); + // @ts-ignore - undocumented return, useful for unit testing + return [host, port]; + } + + getMultiFactorResolver(error: MultiFactorError): MultiFactorResolver { + return createMultiFactorResolver(this as unknown as AuthInternal, error) as MultiFactorResolver; + } + + multiFactor(user: User): MultiFactorUser { + if (!this.currentUser || user.uid !== this.currentUser.uid) { + throw new Error('firebase.auth().multiFactor() only operates on currentUser'); + } + return new MultiFactorUserModule(this as unknown as AuthInternal, user) as MultiFactorUser; + } + + getCustomAuthDomain(): Promise { + return this.native.getCustomAuthDomain(); + } +} + +// Apply password policy mixin to FirebaseAuthModule +Object.assign(FirebaseAuthModule.prototype, PasswordPolicyMixin); + +export { + AppleAuthProvider, + EmailAuthProvider, + FacebookAuthProvider, + GithubAuthProvider, + GoogleAuthProvider, + OAuthProvider, + OIDCAuthProvider, + PhoneAuthProvider, + PhoneAuthState, + PhoneMultiFactorGenerator, + TotpMultiFactorGenerator, + TotpSecret, + TwitterAuthProvider, + ActionCodeOperation, + FactorId, + OperationType, + ProviderId, + SignInMethod, +}; + +type AnyFn = (...args: any[]) => any; + +type UserModuleInternal = UserInternal; +type MultiFactorInfoInternal = MultiFactorInfo | MultiFactorResolverResultInternal['hints'][number]; + +function getAuthInternal(auth: Auth): AuthInternal { + return auth as unknown as AuthInternal; +} + +function getUserInternal(user: User): UserModuleInternal { + return user as unknown as UserModuleInternal; +} + +type AdditionalUserInfoSource = { + isNewUser?: boolean; + profile?: Record | null; + providerId?: string | null; + username?: string | null; +} & Record; + +function normalizeAdditionalUserInfo(info: AdditionalUserInfoSource): AdditionalUserInfoNative { + return { + ...info, + isNewUser: Boolean(info.isNewUser), + profile: info.profile ?? null, + providerId: info.providerId ?? null, + username: info.username ?? null, + }; +} + +function normalizeUserCredential( + userCredential: UserCredentialResultInternal, + overrides: Partial> = {}, +): UserCredential { + const normalizedUserCredential: UserCredential = { + user: userCredential.user as unknown as User, + providerId: + overrides.providerId ?? + userCredential.providerId ?? + userCredential.additionalUserInfo?.providerId ?? + null, + operationType: overrides.operationType ?? userCredential.operationType ?? OperationType.SIGN_IN, + }; + + if (userCredential.additionalUserInfo) { + normalizedUserCredential.additionalUserInfo = normalizeAdditionalUserInfo( + userCredential.additionalUserInfo as AdditionalUserInfoSource, + ); + } + + return normalizedUserCredential; +} + +function normalizeMultiFactorInfo(info: MultiFactorInfoInternal): MultiFactorInfo { + const normalizedInfo = { + uid: info.uid, + displayName: info.displayName ?? null, + enrollmentTime: info.enrollmentTime, + factorId: info.factorId, + }; + + if ('phoneNumber' in info) { + return { + ...normalizedInfo, + phoneNumber: info.phoneNumber, + } as PhoneMultiFactorInfo; + } + + return normalizedInfo as TotpMultiFactorInfo; +} + +function normalizeActionCodeInfo(actionCodeInfo: ActionCodeInfoResultInternal): ActionCodeInfo { + const data = actionCodeInfo.data ?? {}; + + return { + data: { + email: data.email ?? null, + multiFactorInfo: + 'multiFactorInfo' in data && data.multiFactorInfo + ? normalizeMultiFactorInfo(data.multiFactorInfo) + : null, + previousEmail: (('previousEmail' in data ? data.previousEmail : undefined) ?? + ('fromEmail' in data ? data.fromEmail : undefined) ?? + null) as string | null, + }, + operation: actionCodeInfo.operation as ActionCodeInfo['operation'], + }; +} + +function normalizeConfirmationResult( + confirmationResult: ConfirmationResultResultInternal, +): ConfirmationResult { + if (!confirmationResult.verificationId) { + throw new Error('signInWithPhoneNumber() did not return a verificationId.'); + } + + return { + verificationId: confirmationResult.verificationId, + async confirm(verificationCode: string) { + const userCredential = await confirmationResult.confirm(verificationCode); + + if (!userCredential) { + throw new Error('signInWithPhoneNumber().confirm() returned no user credential.'); + } + + return normalizeUserCredential(userCredential, { + providerId: ProviderId.PHONE, + operationType: OperationType.SIGN_IN, + }); + }, + }; +} + +function normalizeMultiFactorResolver( + resolver: MultiFactorResolverResultInternal, +): MultiFactorResolver { + return { + hints: resolver.hints.map(normalizeMultiFactorInfo), + session: resolver.session, + async resolveSignIn(assertion) { + return normalizeUserCredential(await resolver.resolveSignIn(assertion), { + providerId: assertion.factorId === FactorId.PHONE ? ProviderId.PHONE : null, + operationType: OperationType.SIGN_IN, + }); + }, + }; +} + +function normalizeMultiFactorUser(multiFactorUser: MultiFactorUserResultInternal): MultiFactorUser { + return { + enrolledFactors: multiFactorUser.enrolledFactors.map(normalizeMultiFactorInfo), + getSession: () => multiFactorUser.getSession(), + enroll: (assertion, displayName) => multiFactorUser.enroll(assertion, displayName), + unenroll: option => + multiFactorUser.unenroll(option as Parameters[0]), + }; +} + +function normalizeAuthListener( + nextOrObserver: NextOrObserver, +): AuthListenerCallbackInternal | { next: AuthListenerCallbackInternal } { + if (typeof nextOrObserver === 'function') { + return nextOrObserver as AuthListenerCallbackInternal; + } + + if (typeof nextOrObserver.next !== 'function') { + return { next: () => {} }; + } + + return nextOrObserver as { next: AuthListenerCallbackInternal }; +} + +function callAuthMethod( + auth: AuthInternal, + method: F, + ...args: Parameters +): ReturnType { + return method.call(auth, ...args); +} + +function callUserMethod( + user: UserModuleInternal, + method: F, + ...args: Parameters +): ReturnType { + return method.call(user, ...args); +} + +/** + * Returns the Auth instance associated with the provided FirebaseApp. + * + * @param app - The Firebase app instance. Defaults to the default app. + */ +export function getAuth(app?: FirebaseApp): Auth { + return getOrCreateModularInstance(FirebaseAuthModule, config, app) as unknown as Auth; +} + +/** + * This function allows more control over the Auth instance than getAuth(). + * + * @param app - The Firebase app to initialize Auth for. + * @param _deps - Optional firebase-js-sdk dependency bag. + * + * @remarks + * The optional `deps` argument exists for firebase-js-sdk API parity. React Native Firebase + * ignores persistence, popup redirect resolver, and error-map dependencies because native + * iOS/Android SDKs manage auth state and do not support the web-only persistence stack. + * `initializeAuth()` returns the same shared Auth instance as {@link getAuth}. + */ +export function initializeAuth(app: FirebaseApp, _deps?: Dependencies): Auth { + return getAuth(app); +} + +/** + * Applies an out-of-band email action code (for example from a password reset or email verification link). + */ +export function applyActionCode(auth: Auth, oobCode: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.applyActionCode, oobCode); +} + +/** + * Registers a callback to run before an auth state change is committed. + * + * @returns An unsubscribe function. + */ +export function beforeAuthStateChanged( + auth: Auth, + callback: (user: User | null) => void | Promise, + onAbort?: () => void, +): Unsubscribe { + const authInternal = getAuthInternal(auth); + return authInternal.beforeAuthStateChanged(callback, onAbort); +} + +/** + * Checks the validity of an out-of-band email action code and returns metadata about the pending operation. + * + * @remarks React Native Firebase normalizes native results toward firebase-js-sdk shapes, including mapping + * `fromEmail` to `previousEmail` and coercing multi-factor info objects. + */ +export function checkActionCode(auth: Auth, oobCode: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.checkActionCode, oobCode).then( + normalizeActionCodeInfo, + ); +} + +/** + * Confirms a password reset using the out-of-band code from the reset email. + */ +export function confirmPasswordReset( + auth: Auth, + oobCode: string, + newPassword: string, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.confirmPasswordReset, oobCode, newPassword); +} + +/** + * Connects the Auth instance to the Auth emulator. + * + * @remarks Delegates to the native `useEmulator` bridge. Accepts the firebase-js-sdk + * `options.disableWarnings` flag for {@link Auth.emulatorConfig} parity. On web, that flag + * suppresses the emulator DOM warning banner; native iOS/Android SDKs do not surface that banner, + * so the value is recorded on `auth.emulatorConfig.options` only. When `options` is provided, + * `disableWarnings` is required (matching firebase-js-sdk). + */ +export function connectAuthEmulator( + auth: Auth, + url: string, + options?: { disableWarnings: boolean }, +): void { + const authInternal = getAuthInternal(auth); + callAuthMethod(authInternal, authInternal.useEmulator, url, options); +} + +/** + * Creates a new user with an email address and password. + * + * @remarks Returned {@link UserCredential} objects include top-level `providerId` and `operationType` + * fields. `additionalUserInfo`, when present, is attached as a non-enumerable property. + */ +export function createUserWithEmailAndPassword( + auth: Auth, + email: string, + password: string, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.createUserWithEmailAndPassword, + email, + password, + ).then(userCredential => + normalizeUserCredential(userCredential, { + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Fetches the sign-in methods available for the given email address. + */ +export function fetchSignInMethodsForEmail(auth: Auth, email: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.fetchSignInMethodsForEmail, email); +} + +/** + * Extracts a {@link MultiFactorResolver} from a {@link MultiFactorError}. + * + * @throws If the error does not contain resolver hints. + */ +export function getMultiFactorResolver(auth: Auth, error: MultiFactorError): MultiFactorResolver { + const authInternal = getAuthInternal(auth); + const resolver = callAuthMethod( + authInternal, + authInternal.getMultiFactorResolver, + error, + ) as MultiFactorResolverResultInternal | null; + + if (!resolver) { + throw new Error('The provided auth error did not contain a multi-factor resolver.'); + } + + return normalizeMultiFactorResolver(resolver); +} + +/** + * Returns the redirect sign-in result after a browser redirect flow completes. + * + * @remarks + * **Not supported on React Native Firebase.** Always throws synchronously because native provider + * flows do not use the browser redirect contract from the firebase-js-sdk. + */ +export function getRedirectResult( + _auth: Auth, + _resolver?: PopupRedirectResolver, +): Promise { + throw new Error('getRedirectResult is unsupported by the native Firebase SDKs.'); +} + +/** + * Checks whether an email link is a valid sign-in with email link URL. + * + * @remarks React Native Firebase performs this check through the native bridge and returns + * `Promise`. The firebase-js-sdk returns a synchronous `boolean`. + */ +export function isSignInWithEmailLink(auth: Auth, emailLink: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.isSignInWithEmailLink, emailLink); +} + +/** + * Subscribes to auth state changes for the given Auth instance. + * + * @returns An unsubscribe function. + * + * @remarks The legacy `error` and `completed` callback overload exists for firebase-js-sdk API parity. + * Native auth listeners never invoke separate error or completed callbacks. + */ +export function onAuthStateChanged( + auth: Auth, + nextOrObserver: NextOrObserver, + _error?: ErrorFn, + _completed?: CompleteFn, +): Unsubscribe { + // The legacy callback overload exists for JS SDK compatibility, but native auth listeners + // never invoke separate error/completed callbacks. + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.onAuthStateChanged, + normalizeAuthListener(nextOrObserver), + ); +} + +/** + * Subscribes to ID token changes for the given Auth instance. + * + * @returns An unsubscribe function. + * + * @remarks The legacy `error` and `completed` callback overload exists for firebase-js-sdk API parity. + * Native auth listeners never invoke separate error or completed callbacks. + */ +export function onIdTokenChanged( + auth: Auth, + nextOrObserver: NextOrObserver, + _error?: ErrorFn, + _completed?: CompleteFn, +): Unsubscribe { + // The legacy callback overload exists for JS SDK compatibility, but native auth listeners + // never invoke separate error/completed callbacks. + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.onIdTokenChanged, + normalizeAuthListener(nextOrObserver), + ); +} + +/** + * Revokes the given OAuth access token for the current user. + * + * @remarks + * **Web only.** Always throws synchronously on React Native Firebase. + */ +export function revokeAccessToken(_auth: Auth, _token: string): Promise { + throw new Error('revokeAccessToken() is only supported on Web'); +} + +/** + * Revokes a user's Sign in with Apple token. + * + * @remarks + * React Native Firebase-specific API required by Apple's account-deletion guidelines. + * **Supported on iOS** via the native `revokeTokenWithAuthorizationCode` bridge. + * On Android and Web the bridge resolves without performing revocation (no-op). + * Distinct from firebase-js-sdk {@link revokeAccessToken}, which revokes OAuth access tokens on Web only. + */ +export function revokeToken(auth: Auth, authorizationCode: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.revokeToken, authorizationCode); +} + +/** + * Sends a password reset email to the given address. + */ +export function sendPasswordResetEmail( + auth: Auth, + email: string, + actionCodeSettings?: ActionCodeSettings, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.sendPasswordResetEmail, + email, + actionCodeSettings, + ); +} + +/** + * Sends a sign-in with email link to the given address. + * + * @remarks `actionCodeSettings` is required in the modular API (matching firebase-js-sdk). + * The namespaced `firebase.auth().sendSignInLinkToEmail(email, settings?)` API still supplies + * defaults when settings are omitted. + */ +export function sendSignInLinkToEmail( + auth: Auth, + email: string, + actionCodeSettings: ActionCodeSettings, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.sendSignInLinkToEmail, + email, + actionCodeSettings, + ); +} + +/** + * Sets the persistence type for the Auth instance. + * + * @remarks + * **Not supported on React Native Firebase.** Always throws synchronously because auth state is + * managed by the native iOS/Android SDKs rather than the web persistence stack. + */ +export function setPersistence(_auth: Auth, _persistence: Persistence): Promise { + throw new Error('setPersistence is unsupported by the native Firebase SDKs.'); +} + +/** + * Signs in anonymously. + */ +export function signInAnonymously(auth: Auth): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.signInAnonymously).then(userCredential => + normalizeUserCredential(userCredential, { + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Signs in with the given auth credential. + */ +export function signInWithCredential( + auth: Auth, + credential: AuthCredential, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.signInWithCredential, credential).then( + userCredential => + normalizeUserCredential(userCredential, { + providerId: credential.providerId, + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Signs in with a custom authentication token. + */ +export function signInWithCustomToken(auth: Auth, customToken: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.signInWithCustomToken, customToken).then( + userCredential => + normalizeUserCredential(userCredential, { + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Signs in with an email address and password. + */ +export function signInWithEmailAndPassword( + auth: Auth, + email: string, + password: string, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.signInWithEmailAndPassword, + email, + password, + ).then(userCredential => + normalizeUserCredential(userCredential, { + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Signs in using an email and sign-in with email link. + * + * @remarks The `emailLink` argument is optional, matching firebase-js-sdk. + */ +export function signInWithEmailLink( + auth: Auth, + email: string, + emailLink?: string, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.signInWithEmailLink, email, emailLink).then( + userCredential => + normalizeUserCredential(userCredential, { + providerId: ProviderId.PASSWORD, + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Signs in with a phone number and returns a confirmation result for SMS verification. + * + * @remarks + * Native SDKs own the phone verification flow. The optional `appVerifier` argument from the + * firebase-js-sdk is ignored. This modular API also does not accept RNFB's legacy `forceResend` + * argument — use {@link verifyPhoneNumber} for the native listener / force-resend flow instead. + */ +export function signInWithPhoneNumber( + auth: Auth, + phoneNumber: string, + _appVerifier?: ApplicationVerifier, +): Promise { + // Native SDKs own the verification flow, so the modular wrapper intentionally ignores the + // JS SDK's optional ApplicationVerifier and forwards only the phone number. + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.signInWithPhoneNumber, phoneNumber).then( + normalizeConfirmationResult, + ); +} + +/** + * Starts native phone number verification and returns a listener for verification events. + * + * @remarks React Native Firebase-specific API with no firebase-js-sdk equivalent. Use this for + * auto-verification, resend, and multi-step phone auth flows that require native callbacks. + */ +export function verifyPhoneNumber( + auth: Auth, + phoneNumber: string, + autoVerifyTimeoutOrForceResend?: number | boolean, + forceResend?: boolean, +): PhoneAuthListener { + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.verifyPhoneNumber, + phoneNumber, + autoVerifyTimeoutOrForceResend, + forceResend, + ); +} + +/** + * Signs in with the given provider using a native popup-style flow where supported. + */ +export function signInWithPopup( + auth: Auth, + provider: AuthProvider, + _resolver?: PopupRedirectResolver, +): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.signInWithPopup, + provider as unknown as AuthProviderWithObjectInternal, + ).then(userCredential => + normalizeUserCredential(userCredential, { + providerId: provider.providerId, + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Signs in with the given provider using a redirect-style flow. + * + * @remarks On React Native Firebase, native provider flows complete immediately and resolve with a + * {@link UserCredential} instead of following the browser redirect contract used by the + * firebase-js-sdk (which resolves later via {@link getRedirectResult}). + */ +export function signInWithRedirect( + auth: Auth, + provider: AuthProvider, + _resolver?: PopupRedirectResolver, +): Promise { + // Native provider flows complete immediately and return a credential instead of following the + // browser redirect contract from the Firebase JS SDK. + const authInternal = getAuthInternal(auth); + return callAuthMethod( + authInternal, + authInternal.signInWithRedirect, + provider as unknown as AuthProviderWithObjectInternal, + ).then(userCredential => + normalizeUserCredential(userCredential, { + providerId: provider.providerId, + operationType: OperationType.SIGN_IN, + }), + ); +} + +/** + * Signs out the current user for the given Auth instance. + */ +export function signOut(auth: Auth): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.signOut); +} + +/** + * Updates the currently signed-in user on the Auth instance. + */ +export function updateCurrentUser(auth: Auth, user: User | null): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.updateCurrentUser, user); +} + +/** + * Sets the Auth `languageCode` from the device locale. + * + * @remarks + * **Not supported on React Native Firebase.** Always throws synchronously. Set `auth.languageCode` + * directly or use {@link setLanguageCode}. + */ +export function useDeviceLanguage(_auth: Auth): void { + throw new Error('useDeviceLanguage is unsupported by the native Firebase SDKs'); +} + +/** + * Sets the Auth language code. + * + * @remarks React Native Firebase exposes this as a modular helper. The firebase-js-sdk only exposes + * the writable `auth.languageCode` property. + */ +export function setLanguageCode(auth: Auth, languageCode: string | null): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.setLanguageCode, languageCode); +} + +/** + * Configures iOS keychain access group sharing for the Auth instance. + * + * @remarks React Native Firebase-specific iOS helper with no firebase-js-sdk equivalent. + */ +export function useUserAccessGroup(auth: Auth, userAccessGroup: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.useUserAccessGroup, userAccessGroup).then( + () => undefined, + ); +} + +/** + * Verifies a password reset code and returns the associated email address. + */ +export function verifyPasswordResetCode(auth: Auth, code: string): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.verifyPasswordResetCode, code); +} + +/** + * Parses an email action link string. + * + * @param link - The email action link to parse. + * @returns The {@link ActionCodeURL} object, or `null` if the link is invalid. + * + * @remarks Pure URL parsing (ported from firebase-js-sdk). Works on all platforms without a native + * bridge. See also {@link ActionCodeURL.parseLink}. + */ +export function parseActionCodeURL(link: string): ActionCodeURL | null { + return ActionCodeURL.parseLink(link); +} + +/** + * Deletes the given user account. + */ +export function deleteUser(user: User): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.delete); +} + +/** + * Returns the current ID token for the user. + */ +export function getIdToken(user: User, forceRefresh?: boolean): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.getIdToken, forceRefresh); +} + +/** + * Returns the decoded ID token result for the user. + */ +export function getIdTokenResult(user: User, forceRefresh?: boolean): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.getIdTokenResult, forceRefresh); +} + +/** + * Links the user account with the given credential. + */ +export function linkWithCredential( + user: User, + credential: AuthCredential, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.linkWithCredential, credential).then( + userCredential => + normalizeUserCredential(userCredential, { + providerId: credential.providerId, + operationType: OperationType.LINK, + }), + ); +} + +/** + * Links the user account with a phone number using SMS verification. + * + * @remarks + * **Not supported on React Native Firebase.** Always throws synchronously. + */ +export function linkWithPhoneNumber( + _user: User, + _phoneNumber: string, + _appVerifier?: ApplicationVerifier, +): Promise { + throw new Error('linkWithPhoneNumber is unsupported by the native Firebase SDKs'); +} + +/** + * Links the user account with the given provider using a native popup-style flow where supported. + */ +export function linkWithPopup( + user: User, + provider: AuthProvider, + _resolver?: PopupRedirectResolver, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod( + userInternal, + userInternal.linkWithPopup, + provider as unknown as AuthProviderWithObjectInternal, + ).then(userCredential => + normalizeUserCredential(userCredential, { + providerId: provider.providerId, + operationType: OperationType.LINK, + }), + ); +} + +/** + * Links the user account with the given provider using a redirect-style flow. + * + * @remarks On React Native Firebase, native provider flows complete immediately and resolve with a + * {@link UserCredential} instead of following the browser redirect contract used by the + * firebase-js-sdk. + */ +export function linkWithRedirect( + user: User, + provider: AuthProvider, + _resolver?: PopupRedirectResolver, +): Promise { + // Native provider flows complete immediately and return a credential instead of following the + // browser redirect contract from the Firebase JS SDK. + const userInternal = getUserInternal(user); + return callUserMethod( + userInternal, + userInternal.linkWithRedirect, + provider as unknown as AuthProviderWithObjectInternal, + ).then(userCredential => + normalizeUserCredential(userCredential, { + providerId: provider.providerId, + operationType: OperationType.LINK, + }), + ); +} + +/** + * Returns the multi-factor interface for the given user. + * + * @remarks Uses the user's own Auth instance instead of always calling {@link getAuth}, which fixes + * secondary Firebase app usage compared with earlier RNFB behavior. + */ +export function multiFactor(user: User): MultiFactorUser { + return normalizeMultiFactorUser( + new MultiFactorUserModule( + ((user as unknown as UserInternal)._auth || + (getAuth() as unknown as UserInternal['_auth'])) as NonNullable, + user as unknown as MultiFactorUserSourceInternal, + ), + ); +} + +/** + * Reauthenticates the user with the given credential. + */ +export function reauthenticateWithCredential( + user: User, + credential: AuthCredential, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.reauthenticateWithCredential, credential).then( + userCredential => + normalizeUserCredential(userCredential, { + providerId: credential.providerId, + operationType: OperationType.REAUTHENTICATE, + }), + ); +} + +/** + * Reauthenticates the user with a phone number using SMS verification. + * + * @remarks + * **Not supported on React Native Firebase.** Always throws synchronously. + */ +export function reauthenticateWithPhoneNumber( + _user: User, + _phoneNumber: string, + _appVerifier?: ApplicationVerifier, +): Promise { + throw new Error('reauthenticateWithPhoneNumber is unsupported by the native Firebase SDKs'); +} + +/** + * Reauthenticates the user with the given provider using a native popup-style flow where supported. + */ +export function reauthenticateWithPopup( + user: User, + provider: AuthProvider, + _resolver?: PopupRedirectResolver, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod( + userInternal, + userInternal.reauthenticateWithPopup, + provider as unknown as AuthProviderWithObjectInternal, + ).then(userCredential => + normalizeUserCredential(userCredential, { + providerId: provider.providerId, + operationType: OperationType.REAUTHENTICATE, + }), + ); +} + +/** + * Reauthenticates the user with the given provider using a redirect-style flow. + * + * @remarks On React Native Firebase, native provider flows do not follow the browser redirect + * contract. This resolves with `void` after the native flow completes instead of the + * firebase-js-sdk `Promise` signature used for unresolved browser redirects. + */ +export function reauthenticateWithRedirect( + user: User, + provider: AuthProvider, + _resolver?: PopupRedirectResolver, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod( + userInternal, + userInternal.reauthenticateWithRedirect, + provider as unknown as AuthProviderWithObjectInternal, + ); +} + +/** + * Reloads the user's profile data from the server. + */ +export function reload(user: User): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.reload); +} + +/** + * Sends an email verification message to the user. + */ +export function sendEmailVerification( + user: User, + actionCodeSettings?: ActionCodeSettings | null, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod( + userInternal, + userInternal.sendEmailVerification, + actionCodeSettings ?? undefined, + ); +} + +/** + * Unlinks a provider from the user account. + */ +export function unlink(user: User, providerId: string): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.unlink, providerId); +} + +/** + * Updates the user's email address. + */ +export function updateEmail(user: User, newEmail: string): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.updateEmail, newEmail); +} + +/** + * Updates the user's password. + */ +export function updatePassword(user: User, newPassword: string): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.updatePassword, newPassword); +} + +/** + * Updates the user's phone number using a phone auth credential. + */ +export function updatePhoneNumber(user: User, credential: PhoneAuthCredential): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.updatePhoneNumber, credential); +} + +/** + * Updates the user's display name and/or photo URL. + */ +export function updateProfile( + user: User, + profile: { displayName?: string | null; photoURL?: string | null }, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod(userInternal, userInternal.updateProfile, { + displayName: profile.displayName, + photoURL: profile.photoURL, + }); +} + +/** + * Sends a verification email to the new address before updating the user's email. + */ +export function verifyBeforeUpdateEmail( + user: User, + newEmail: string, + actionCodeSettings?: ActionCodeSettings | null, +): Promise { + const userInternal = getUserInternal(user); + return callUserMethod( + userInternal, + userInternal.verifyBeforeUpdateEmail, + newEmail, + actionCodeSettings ?? undefined, + ); +} + +/** + * Returns additional OAuth / federated sign-in information from a {@link UserCredential}. + * + * @remarks Returns firebase-js-sdk core fields plus any extra native keys copied from the bridge. + */ +export function getAdditionalUserInfo(userCredential: UserCredential): AdditionalUserInfo | null { + if (userCredential.additionalUserInfo) { + return userCredential.additionalUserInfo; + } + + const info = (userCredential as unknown as UserCredentialResultInternal).additionalUserInfo; + if (!info) { + return null; + } + + return normalizeAdditionalUserInfo(info as AdditionalUserInfoSource); +} + +/** + * Returns the configured custom auth domain for the Auth instance. + * + * @remarks React Native Firebase-specific helper with no firebase-js-sdk equivalent. + */ +export function getCustomAuthDomain(auth: Auth): Promise { + const authInternal = getAuthInternal(auth); + return callAuthMethod(authInternal, authInternal.getCustomAuthDomain); +} + +/** + * Validates a password against the project's password policy. + */ +export function validatePassword(auth: Auth, password: string): Promise { + if (!auth || !('app' in auth)) { + throw new Error( + `firebase.auth().validatePassword(*) 'auth' must be a valid Auth instance with an 'app' property`, + ); + } + + if (password === null || password === undefined) { + throw new Error( + "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", + ); + } + + const authWithPasswordValidation = getAuthInternal(auth); + return callAuthMethod( + authWithPasswordValidation, + authWithPasswordValidation.validatePassword, + password, + ); +} + +export const SDK_VERSION: string = version; + +// Register the interop module for non-native platforms. +setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/auth/lib/modular.ts b/packages/auth/lib/modular.ts deleted file mode 100644 index 245e1bcb03..0000000000 --- a/packages/auth/lib/modular.ts +++ /dev/null @@ -1,1166 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import { - ActionCodeOperation, - FactorId, - OperationType, - ProviderId, - SignInMethod, -} from './constants'; -import PhoneMultiFactorGenerator from './PhoneMultiFactorGenerator'; -import { PhoneAuthState } from './PhoneAuthState'; -import TotpMultiFactorGenerator from './TotpMultiFactorGenerator'; -import { TotpSecret } from './TotpSecret'; -import { MultiFactorUser as MultiFactorUserModule } from './multiFactor'; -import AppleAuthProvider from './providers/AppleAuthProvider'; -import EmailAuthProvider from './providers/EmailAuthProvider'; -import FacebookAuthProvider from './providers/FacebookAuthProvider'; -import GithubAuthProvider from './providers/GithubAuthProvider'; -import GoogleAuthProvider from './providers/GoogleAuthProvider'; -import OAuthProvider from './providers/OAuthProvider'; -import OIDCAuthProvider from './providers/OIDCAuthProvider'; -import PhoneAuthProvider from './providers/PhoneAuthProvider'; -import TwitterAuthProvider from './providers/TwitterAuthProvider'; -import type { FirebaseApp } from '@react-native-firebase/app'; -import { ActionCodeURL } from './ActionCodeURL'; -import type { - ActionCodeInfo, - ActionCodeSettings, - AdditionalUserInfo, - AdditionalUserInfoNative, - ApplicationVerifier, - Auth, - AuthCredential, - AuthProvider, - CompleteFn, - ConfirmationResult, - Dependencies, - ErrorFn, - IdTokenResult, - MultiFactorError, - MultiFactorInfo, - MultiFactorResolver, - MultiFactorUser, - NextOrObserver, - PasswordValidationStatus, - Persistence, - PhoneAuthCredential, - PhoneAuthListener, - PhoneMultiFactorInfo, - PopupRedirectResolver, - TotpMultiFactorInfo, - Unsubscribe, - User, - UserCredential, -} from './types/auth'; -import type { - ActionCodeInfoResultInternal, - AppWithAuthInternal, - AuthInternal, - AuthListenerCallbackInternal, - AuthProviderWithObjectInternal, - ConfirmationResultResultInternal, - MultiFactorResolverResultInternal, - MultiFactorUserResultInternal, - MultiFactorUserSourceInternal, - UserCredentialResultInternal, - UserInternal, - WithAuthDeprecationArg, -} from './types/internal'; - -/** - * Modular Auth API for React Native Firebase. - * - * Most exports mirror the [firebase-js-sdk modular Auth API](https://firebase.google.com/docs/reference/js/auth). - * Remarks on individual symbols call out React Native-specific behavior, unsupported - * web-only APIs, and return-type differences. - * - * @packageDocumentation - */ - -type AnyFn = (...args: any[]) => any; - -type UserModuleInternal = UserInternal; -type MultiFactorInfoInternal = MultiFactorInfo | MultiFactorResolverResultInternal['hints'][number]; - -export { ActionCodeOperation, FactorId, OperationType, ProviderId, SignInMethod }; - -function appWithAuth(app?: FirebaseApp): AppWithAuthInternal { - return (app ? getApp(app.name) : getApp()) as unknown as AppWithAuthInternal; -} - -function getAuthInternal(auth: Auth): AuthInternal { - return auth as unknown as AuthInternal; -} - -function getUserInternal(user: User): UserModuleInternal { - return user as unknown as UserModuleInternal; -} - -type AdditionalUserInfoSource = { - isNewUser?: boolean; - profile?: Record | null; - providerId?: string | null; - username?: string | null; -} & Record; - -function normalizeAdditionalUserInfo(info: AdditionalUserInfoSource): AdditionalUserInfoNative { - return { - ...info, - isNewUser: Boolean(info.isNewUser), - profile: info.profile ?? null, - providerId: info.providerId ?? null, - username: info.username ?? null, - }; -} - -function normalizeUserCredential( - userCredential: UserCredentialResultInternal, - overrides: Partial> = {}, -): UserCredential { - const normalizedUserCredential: UserCredential = { - user: userCredential.user as unknown as User, - providerId: - overrides.providerId ?? - userCredential.providerId ?? - userCredential.additionalUserInfo?.providerId ?? - null, - operationType: overrides.operationType ?? userCredential.operationType ?? OperationType.SIGN_IN, - }; - - if (userCredential.additionalUserInfo) { - normalizedUserCredential.additionalUserInfo = normalizeAdditionalUserInfo( - userCredential.additionalUserInfo as AdditionalUserInfoSource, - ); - } - - return normalizedUserCredential; -} - -function normalizeMultiFactorInfo(info: MultiFactorInfoInternal): MultiFactorInfo { - const normalizedInfo = { - uid: info.uid, - displayName: info.displayName ?? null, - enrollmentTime: info.enrollmentTime, - factorId: info.factorId, - }; - - if ('phoneNumber' in info) { - return { - ...normalizedInfo, - phoneNumber: info.phoneNumber, - } as PhoneMultiFactorInfo; - } - - return normalizedInfo as TotpMultiFactorInfo; -} - -function normalizeActionCodeInfo(actionCodeInfo: ActionCodeInfoResultInternal): ActionCodeInfo { - const data = actionCodeInfo.data ?? {}; - - return { - data: { - email: data.email ?? null, - multiFactorInfo: - 'multiFactorInfo' in data && data.multiFactorInfo - ? normalizeMultiFactorInfo(data.multiFactorInfo) - : null, - previousEmail: - ('previousEmail' in data ? data.previousEmail : undefined) ?? - ('fromEmail' in data ? data.fromEmail : undefined) ?? - null, - }, - operation: actionCodeInfo.operation as ActionCodeInfo['operation'], - }; -} - -function normalizeConfirmationResult( - confirmationResult: ConfirmationResultResultInternal, -): ConfirmationResult { - if (!confirmationResult.verificationId) { - throw new Error('signInWithPhoneNumber() did not return a verificationId.'); - } - - return { - verificationId: confirmationResult.verificationId, - async confirm(verificationCode: string) { - const userCredential = await confirmationResult.confirm(verificationCode); - - if (!userCredential) { - throw new Error('signInWithPhoneNumber().confirm() returned no user credential.'); - } - - return normalizeUserCredential(userCredential, { - providerId: ProviderId.PHONE, - operationType: OperationType.SIGN_IN, - }); - }, - }; -} - -function normalizeMultiFactorResolver( - resolver: MultiFactorResolverResultInternal, -): MultiFactorResolver { - return { - hints: resolver.hints.map(normalizeMultiFactorInfo), - session: resolver.session, - async resolveSignIn(assertion) { - return normalizeUserCredential(await resolver.resolveSignIn(assertion), { - providerId: assertion.factorId === FactorId.PHONE ? ProviderId.PHONE : null, - operationType: OperationType.SIGN_IN, - }); - }, - }; -} - -function normalizeMultiFactorUser(multiFactorUser: MultiFactorUserResultInternal): MultiFactorUser { - return { - enrolledFactors: multiFactorUser.enrolledFactors.map(normalizeMultiFactorInfo), - getSession: () => multiFactorUser.getSession(), - enroll: (assertion, displayName) => multiFactorUser.enroll(assertion, displayName), - unenroll: option => - multiFactorUser.unenroll(option as Parameters[0]), - }; -} - -export { ActionCodeURL } from './ActionCodeURL'; -export { - AuthCredential, - EmailAuthCredential, - OAuthCredential, - PhoneAuthCredential, -} from './credentials'; - -export { - AppleAuthProvider, - EmailAuthProvider, - FacebookAuthProvider, - GithubAuthProvider, - GoogleAuthProvider, - OAuthProvider, - OIDCAuthProvider, - PhoneAuthProvider, - PhoneAuthState, - PhoneMultiFactorGenerator, - TotpMultiFactorGenerator, - TotpSecret, - TwitterAuthProvider, -}; - -function normalizeAuthListener( - nextOrObserver: NextOrObserver, -): AuthListenerCallbackInternal | { next: AuthListenerCallbackInternal } { - if (typeof nextOrObserver === 'function') { - return nextOrObserver as AuthListenerCallbackInternal; - } - - if (typeof nextOrObserver.next !== 'function') { - return { next: () => {} }; - } - - return nextOrObserver as { next: AuthListenerCallbackInternal }; -} - -function callAuthMethod( - auth: AuthInternal, - method: F, - ...args: Parameters -): ReturnType { - return (method as unknown as WithAuthDeprecationArg).call( - auth, - ...args, - MODULAR_DEPRECATION_ARG, - ); -} - -function callUserMethod( - user: UserModuleInternal, - method: F, - ...args: Parameters -): ReturnType { - return (method as unknown as WithAuthDeprecationArg).call( - user, - ...args, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Returns the Auth instance associated with the provided FirebaseApp. - * - * @param app - The Firebase app instance. Defaults to the default app. - */ -export function getAuth(app?: FirebaseApp): Auth { - // Keep getAuth() on the shared namespaced instance; method wrappers add the modular sentinel. - return appWithAuth(app).auth(); -} - -/** - * This function allows more control over the Auth instance than getAuth(). - * - * @param app - The Firebase app to initialize Auth for. - * @param _deps - Optional firebase-js-sdk dependency bag. - * - * @remarks - * The optional `deps` argument exists for firebase-js-sdk API parity. React Native Firebase - * ignores persistence, popup redirect resolver, and error-map dependencies because native - * iOS/Android SDKs manage auth state and do not support the web-only persistence stack. - * `initializeAuth()` returns the same shared Auth instance as {@link getAuth}. - */ -export function initializeAuth(app: FirebaseApp, _deps?: Dependencies): Auth { - // Keep initializeAuth() aligned with getAuth(); passing the sentinel here creates a second module. - return appWithAuth(app).auth(); -} - -/** - * Applies an out-of-band email action code (for example from a password reset or email verification link). - */ -export function applyActionCode(auth: Auth, oobCode: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.applyActionCode, oobCode); -} - -/** - * Registers a callback to run before an auth state change is committed. - * - * @returns An unsubscribe function. - */ -export function beforeAuthStateChanged( - auth: Auth, - callback: (user: User | null) => void | Promise, - onAbort?: () => void, -): Unsubscribe { - const authInternal = getAuthInternal(auth); - return authInternal.beforeAuthStateChanged(callback, onAbort); -} - -/** - * Checks the validity of an out-of-band email action code and returns metadata about the pending operation. - * - * @remarks React Native Firebase normalizes native results toward firebase-js-sdk shapes, including mapping - * `fromEmail` to `previousEmail` and coercing multi-factor info objects. - */ -export function checkActionCode(auth: Auth, oobCode: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.checkActionCode, oobCode).then( - normalizeActionCodeInfo, - ); -} - -/** - * Confirms a password reset using the out-of-band code from the reset email. - */ -export function confirmPasswordReset( - auth: Auth, - oobCode: string, - newPassword: string, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.confirmPasswordReset, oobCode, newPassword); -} - -/** - * Connects the Auth instance to the Auth emulator. - * - * @remarks Delegates to the native `useEmulator` bridge. Accepts the firebase-js-sdk - * `options.disableWarnings` flag for {@link Auth.emulatorConfig} parity. On web, that flag - * suppresses the emulator DOM warning banner; native iOS/Android SDKs do not surface that banner, - * so the value is recorded on `auth.emulatorConfig.options` only. When `options` is provided, - * `disableWarnings` is required (matching firebase-js-sdk). - */ -export function connectAuthEmulator( - auth: Auth, - url: string, - options?: { disableWarnings: boolean }, -): void { - const authInternal = getAuthInternal(auth); - callAuthMethod(authInternal, authInternal.useEmulator, url, options); -} - -/** - * Creates a new user with an email address and password. - * - * @remarks Returned {@link UserCredential} objects include top-level `providerId` and `operationType` - * fields. `additionalUserInfo`, when present, is attached as a non-enumerable property. - */ -export function createUserWithEmailAndPassword( - auth: Auth, - email: string, - password: string, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.createUserWithEmailAndPassword, - email, - password, - ).then(userCredential => - normalizeUserCredential(userCredential, { - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Fetches the sign-in methods available for the given email address. - */ -export function fetchSignInMethodsForEmail(auth: Auth, email: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.fetchSignInMethodsForEmail, email); -} - -/** - * Extracts a {@link MultiFactorResolver} from a {@link MultiFactorError}. - * - * @throws If the error does not contain resolver hints. - */ -export function getMultiFactorResolver(auth: Auth, error: MultiFactorError): MultiFactorResolver { - const authInternal = getAuthInternal(auth); - const resolver = callAuthMethod( - authInternal, - authInternal.getMultiFactorResolver, - error, - ) as MultiFactorResolverResultInternal | null; - - if (!resolver) { - throw new Error('The provided auth error did not contain a multi-factor resolver.'); - } - - return normalizeMultiFactorResolver(resolver); -} - -/** - * Returns the redirect sign-in result after a browser redirect flow completes. - * - * @remarks - * **Not supported on React Native Firebase.** Always throws synchronously because native provider - * flows do not use the browser redirect contract from the firebase-js-sdk. - */ -export function getRedirectResult( - _auth: Auth, - _resolver?: PopupRedirectResolver, -): Promise { - throw new Error('getRedirectResult is unsupported by the native Firebase SDKs.'); -} - -/** - * Checks whether an email link is a valid sign-in with email link URL. - * - * @remarks React Native Firebase performs this check through the native bridge and returns - * `Promise`. The firebase-js-sdk returns a synchronous `boolean`. - */ -export function isSignInWithEmailLink(auth: Auth, emailLink: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.isSignInWithEmailLink, emailLink); -} - -/** - * Subscribes to auth state changes for the given Auth instance. - * - * @returns An unsubscribe function. - * - * @remarks The legacy `error` and `completed` callback overload exists for firebase-js-sdk API parity. - * Native auth listeners never invoke separate error or completed callbacks. - */ -export function onAuthStateChanged( - auth: Auth, - nextOrObserver: NextOrObserver, - _error?: ErrorFn, - _completed?: CompleteFn, -): Unsubscribe { - // The legacy callback overload exists for JS SDK compatibility, but native auth listeners - // never invoke separate error/completed callbacks. - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.onAuthStateChanged, - normalizeAuthListener(nextOrObserver), - ); -} - -/** - * Subscribes to ID token changes for the given Auth instance. - * - * @returns An unsubscribe function. - * - * @remarks The legacy `error` and `completed` callback overload exists for firebase-js-sdk API parity. - * Native auth listeners never invoke separate error or completed callbacks. - */ -export function onIdTokenChanged( - auth: Auth, - nextOrObserver: NextOrObserver, - _error?: ErrorFn, - _completed?: CompleteFn, -): Unsubscribe { - // The legacy callback overload exists for JS SDK compatibility, but native auth listeners - // never invoke separate error/completed callbacks. - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.onIdTokenChanged, - normalizeAuthListener(nextOrObserver), - ); -} - -/** - * Revokes the given OAuth access token for the current user. - * - * @remarks - * **Web only.** Always throws synchronously on React Native Firebase. - */ -export function revokeAccessToken(_auth: Auth, _token: string): Promise { - throw new Error('revokeAccessToken() is only supported on Web'); -} - -/** - * Revokes a user's Sign in with Apple token. - * - * @remarks - * React Native Firebase-specific API required by Apple's account-deletion guidelines. - * **Supported on iOS** via the native `revokeTokenWithAuthorizationCode` bridge. - * On Android and Web the bridge resolves without performing revocation (no-op). - * Distinct from firebase-js-sdk {@link revokeAccessToken}, which revokes OAuth access tokens on Web only. - */ -export function revokeToken(auth: Auth, authorizationCode: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.revokeToken, authorizationCode); -} - -/** - * Sends a password reset email to the given address. - */ -export function sendPasswordResetEmail( - auth: Auth, - email: string, - actionCodeSettings?: ActionCodeSettings, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.sendPasswordResetEmail, - email, - actionCodeSettings, - ); -} - -/** - * Sends a sign-in with email link to the given address. - * - * @remarks `actionCodeSettings` is required in the modular API (matching firebase-js-sdk). - * The namespaced `firebase.auth().sendSignInLinkToEmail(email, settings?)` API still supplies - * defaults when settings are omitted. - */ -export function sendSignInLinkToEmail( - auth: Auth, - email: string, - actionCodeSettings: ActionCodeSettings, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.sendSignInLinkToEmail, - email, - actionCodeSettings, - ); -} - -/** - * Sets the persistence type for the Auth instance. - * - * @remarks - * **Not supported on React Native Firebase.** Always throws synchronously because auth state is - * managed by the native iOS/Android SDKs rather than the web persistence stack. - */ -export function setPersistence(_auth: Auth, _persistence: Persistence): Promise { - throw new Error('setPersistence is unsupported by the native Firebase SDKs.'); -} - -/** - * Signs in anonymously. - */ -export function signInAnonymously(auth: Auth): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.signInAnonymously).then(userCredential => - normalizeUserCredential(userCredential, { - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Signs in with the given auth credential. - */ -export function signInWithCredential( - auth: Auth, - credential: AuthCredential, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.signInWithCredential, credential).then( - userCredential => - normalizeUserCredential(userCredential, { - providerId: credential.providerId, - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Signs in with a custom authentication token. - */ -export function signInWithCustomToken(auth: Auth, customToken: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.signInWithCustomToken, customToken).then( - userCredential => - normalizeUserCredential(userCredential, { - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Signs in with an email address and password. - */ -export function signInWithEmailAndPassword( - auth: Auth, - email: string, - password: string, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.signInWithEmailAndPassword, - email, - password, - ).then(userCredential => - normalizeUserCredential(userCredential, { - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Signs in using an email and sign-in with email link. - * - * @remarks The `emailLink` argument is optional, matching firebase-js-sdk. - */ -export function signInWithEmailLink( - auth: Auth, - email: string, - emailLink?: string, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.signInWithEmailLink, email, emailLink).then( - userCredential => - normalizeUserCredential(userCredential, { - providerId: ProviderId.PASSWORD, - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Signs in with a phone number and returns a confirmation result for SMS verification. - * - * @remarks - * Native SDKs own the phone verification flow. The optional `appVerifier` argument from the - * firebase-js-sdk is ignored. This modular API also does not accept RNFB's legacy `forceResend` - * argument — use {@link verifyPhoneNumber} for the native listener / force-resend flow instead. - */ -export function signInWithPhoneNumber( - auth: Auth, - phoneNumber: string, - _appVerifier?: ApplicationVerifier, -): Promise { - // Native SDKs own the verification flow, so the modular wrapper intentionally ignores the - // JS SDK's optional ApplicationVerifier and forwards only the phone number. - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.signInWithPhoneNumber, phoneNumber).then( - normalizeConfirmationResult, - ); -} - -/** - * Starts native phone number verification and returns a listener for verification events. - * - * @remarks React Native Firebase-specific API with no firebase-js-sdk equivalent. Use this for - * auto-verification, resend, and multi-step phone auth flows that require native callbacks. - */ -export function verifyPhoneNumber( - auth: Auth, - phoneNumber: string, - autoVerifyTimeoutOrForceResend?: number | boolean, - forceResend?: boolean, -): PhoneAuthListener { - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.verifyPhoneNumber, - phoneNumber, - autoVerifyTimeoutOrForceResend, - forceResend, - ); -} - -/** - * Signs in with the given provider using a native popup-style flow where supported. - */ -export function signInWithPopup( - auth: Auth, - provider: AuthProvider, - _resolver?: PopupRedirectResolver, -): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.signInWithPopup, - provider as unknown as AuthProviderWithObjectInternal, - ).then(userCredential => - normalizeUserCredential(userCredential, { - providerId: provider.providerId, - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Signs in with the given provider using a redirect-style flow. - * - * @remarks On React Native Firebase, native provider flows complete immediately and resolve with a - * {@link UserCredential} instead of following the browser redirect contract used by the - * firebase-js-sdk (which resolves later via {@link getRedirectResult}). - */ -export function signInWithRedirect( - auth: Auth, - provider: AuthProvider, - _resolver?: PopupRedirectResolver, -): Promise { - // Native provider flows complete immediately and return a credential instead of following the - // browser redirect contract from the Firebase JS SDK. - const authInternal = getAuthInternal(auth); - return callAuthMethod( - authInternal, - authInternal.signInWithRedirect, - provider as unknown as AuthProviderWithObjectInternal, - ).then(userCredential => - normalizeUserCredential(userCredential, { - providerId: provider.providerId, - operationType: OperationType.SIGN_IN, - }), - ); -} - -/** - * Signs out the current user for the given Auth instance. - */ -export function signOut(auth: Auth): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.signOut); -} - -/** - * Updates the currently signed-in user on the Auth instance. - */ -export function updateCurrentUser(auth: Auth, user: User | null): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.updateCurrentUser, user); -} - -/** - * Sets the Auth `languageCode` from the device locale. - * - * @remarks - * **Not supported on React Native Firebase.** Always throws synchronously. Set `auth.languageCode` - * directly or use {@link setLanguageCode}. - */ -export function useDeviceLanguage(_auth: Auth): void { - throw new Error('useDeviceLanguage is unsupported by the native Firebase SDKs'); -} - -/** - * Sets the Auth language code. - * - * @remarks React Native Firebase exposes this as a modular helper. The firebase-js-sdk only exposes - * the writable `auth.languageCode` property. - */ -export function setLanguageCode(auth: Auth, languageCode: string | null): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.setLanguageCode, languageCode); -} - -/** - * Configures iOS keychain access group sharing for the Auth instance. - * - * @remarks React Native Firebase-specific iOS helper with no firebase-js-sdk equivalent. - */ -export function useUserAccessGroup(auth: Auth, userAccessGroup: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.useUserAccessGroup, userAccessGroup).then( - () => undefined, - ); -} - -/** - * Verifies a password reset code and returns the associated email address. - */ -export function verifyPasswordResetCode(auth: Auth, code: string): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.verifyPasswordResetCode, code); -} - -/** - * Parses an email action link string. - * - * @param link - The email action link to parse. - * @returns The {@link ActionCodeURL} object, or `null` if the link is invalid. - * - * @remarks Pure URL parsing (ported from firebase-js-sdk). Works on all platforms without a native - * bridge. See also {@link ActionCodeURL.parseLink}. - */ -export function parseActionCodeURL(link: string): ActionCodeURL | null { - return ActionCodeURL.parseLink(link); -} - -/** - * Deletes the given user account. - */ -export function deleteUser(user: User): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.delete); -} - -/** - * Returns the current ID token for the user. - */ -export function getIdToken(user: User, forceRefresh?: boolean): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.getIdToken, forceRefresh); -} - -/** - * Returns the decoded ID token result for the user. - */ -export function getIdTokenResult(user: User, forceRefresh?: boolean): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.getIdTokenResult, forceRefresh); -} - -/** - * Links the user account with the given credential. - */ -export function linkWithCredential( - user: User, - credential: AuthCredential, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.linkWithCredential, credential).then( - userCredential => - normalizeUserCredential(userCredential, { - providerId: credential.providerId, - operationType: OperationType.LINK, - }), - ); -} - -/** - * Links the user account with a phone number using SMS verification. - * - * @remarks - * **Not supported on React Native Firebase.** Always throws synchronously. - */ -export function linkWithPhoneNumber( - _user: User, - _phoneNumber: string, - _appVerifier?: ApplicationVerifier, -): Promise { - throw new Error('linkWithPhoneNumber is unsupported by the native Firebase SDKs'); -} - -/** - * Links the user account with the given provider using a native popup-style flow where supported. - */ -export function linkWithPopup( - user: User, - provider: AuthProvider, - _resolver?: PopupRedirectResolver, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod( - userInternal, - userInternal.linkWithPopup, - provider as unknown as AuthProviderWithObjectInternal, - ).then(userCredential => - normalizeUserCredential(userCredential, { - providerId: provider.providerId, - operationType: OperationType.LINK, - }), - ); -} - -/** - * Links the user account with the given provider using a redirect-style flow. - * - * @remarks On React Native Firebase, native provider flows complete immediately and resolve with a - * {@link UserCredential} instead of following the browser redirect contract used by the - * firebase-js-sdk. - */ -export function linkWithRedirect( - user: User, - provider: AuthProvider, - _resolver?: PopupRedirectResolver, -): Promise { - // Native provider flows complete immediately and return a credential instead of following the - // browser redirect contract from the Firebase JS SDK. - const userInternal = getUserInternal(user); - return callUserMethod( - userInternal, - userInternal.linkWithRedirect, - provider as unknown as AuthProviderWithObjectInternal, - ).then(userCredential => - normalizeUserCredential(userCredential, { - providerId: provider.providerId, - operationType: OperationType.LINK, - }), - ); -} - -/** - * Returns the multi-factor interface for the given user. - * - * @remarks Uses the user's own Auth instance instead of always calling {@link getAuth}, which fixes - * secondary Firebase app usage compared with earlier RNFB behavior. - */ -export function multiFactor(user: User): MultiFactorUser { - return normalizeMultiFactorUser( - new MultiFactorUserModule( - ((user as unknown as UserInternal)._auth || - (getAuth() as unknown as UserInternal['_auth'])) as NonNullable, - user as unknown as MultiFactorUserSourceInternal, - ), - ); -} - -/** - * Reauthenticates the user with the given credential. - */ -export function reauthenticateWithCredential( - user: User, - credential: AuthCredential, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.reauthenticateWithCredential, credential).then( - userCredential => - normalizeUserCredential(userCredential, { - providerId: credential.providerId, - operationType: OperationType.REAUTHENTICATE, - }), - ); -} - -/** - * Reauthenticates the user with a phone number using SMS verification. - * - * @remarks - * **Not supported on React Native Firebase.** Always throws synchronously. - */ -export function reauthenticateWithPhoneNumber( - _user: User, - _phoneNumber: string, - _appVerifier?: ApplicationVerifier, -): Promise { - throw new Error('reauthenticateWithPhoneNumber is unsupported by the native Firebase SDKs'); -} - -/** - * Reauthenticates the user with the given provider using a native popup-style flow where supported. - */ -export function reauthenticateWithPopup( - user: User, - provider: AuthProvider, - _resolver?: PopupRedirectResolver, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod( - userInternal, - userInternal.reauthenticateWithPopup, - provider as unknown as AuthProviderWithObjectInternal, - ).then(userCredential => - normalizeUserCredential(userCredential, { - providerId: provider.providerId, - operationType: OperationType.REAUTHENTICATE, - }), - ); -} - -/** - * Reauthenticates the user with the given provider using a redirect-style flow. - * - * @remarks On React Native Firebase, native provider flows do not follow the browser redirect - * contract. This resolves with `void` after the native flow completes instead of the - * firebase-js-sdk `Promise` signature used for unresolved browser redirects. - */ -export function reauthenticateWithRedirect( - user: User, - provider: AuthProvider, - _resolver?: PopupRedirectResolver, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod( - userInternal, - userInternal.reauthenticateWithRedirect, - provider as unknown as AuthProviderWithObjectInternal, - ); -} - -/** - * Reloads the user's profile data from the server. - */ -export function reload(user: User): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.reload); -} - -/** - * Sends an email verification message to the user. - */ -export function sendEmailVerification( - user: User, - actionCodeSettings?: ActionCodeSettings | null, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod( - userInternal, - userInternal.sendEmailVerification, - actionCodeSettings ?? undefined, - ); -} - -/** - * Unlinks a provider from the user account. - */ -export function unlink(user: User, providerId: string): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.unlink, providerId); -} - -/** - * Updates the user's email address. - */ -export function updateEmail(user: User, newEmail: string): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.updateEmail, newEmail); -} - -/** - * Updates the user's password. - */ -export function updatePassword(user: User, newPassword: string): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.updatePassword, newPassword); -} - -/** - * Updates the user's phone number using a phone auth credential. - */ -export function updatePhoneNumber(user: User, credential: PhoneAuthCredential): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.updatePhoneNumber, credential); -} - -/** - * Updates the user's display name and/or photo URL. - */ -export function updateProfile( - user: User, - profile: { displayName?: string | null; photoURL?: string | null }, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod(userInternal, userInternal.updateProfile, { - displayName: profile.displayName, - photoURL: profile.photoURL, - }); -} - -/** - * Sends a verification email to the new address before updating the user's email. - */ -export function verifyBeforeUpdateEmail( - user: User, - newEmail: string, - actionCodeSettings?: ActionCodeSettings | null, -): Promise { - const userInternal = getUserInternal(user); - return callUserMethod( - userInternal, - userInternal.verifyBeforeUpdateEmail, - newEmail, - actionCodeSettings ?? undefined, - ); -} - -/** - * Returns additional OAuth / federated sign-in information from a {@link UserCredential}. - * - * @remarks Returns firebase-js-sdk core fields plus any extra native keys copied from the bridge. - */ -export function getAdditionalUserInfo(userCredential: UserCredential): AdditionalUserInfo | null { - if (userCredential.additionalUserInfo) { - return userCredential.additionalUserInfo; - } - - const info = (userCredential as unknown as UserCredentialResultInternal).additionalUserInfo; - if (!info) { - return null; - } - - return normalizeAdditionalUserInfo(info as AdditionalUserInfoSource); -} - -/** - * Returns the configured custom auth domain for the Auth instance. - * - * @remarks React Native Firebase-specific helper with no firebase-js-sdk equivalent. - */ -export function getCustomAuthDomain(auth: Auth): Promise { - const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.getCustomAuthDomain); -} - -/** - * Validates a password against the project's password policy. - */ -export function validatePassword(auth: Auth, password: string): Promise { - if (!auth || !('app' in auth)) { - throw new Error( - `firebase.auth().validatePassword(*) 'auth' must be a valid Auth instance with an 'app' property`, - ); - } - - if (password === null || password === undefined) { - throw new Error( - "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", - ); - } - - const authWithPasswordValidation = getAuthInternal(auth); - return callAuthMethod( - authWithPasswordValidation, - authWithPasswordValidation.validatePassword, - password, - ); -} diff --git a/packages/auth/lib/multiFactor.ts b/packages/auth/lib/multiFactor.ts index 846ba65ded..5cc66fdac2 100644 --- a/packages/auth/lib/multiFactor.ts +++ b/packages/auth/lib/multiFactor.ts @@ -1,27 +1,35 @@ -import { reload } from './modular'; import type { MultiFactorAssertion as ModularMultiFactorAssertion, User } from './types/auth'; -import type { FirebaseAuthTypes } from './types/namespaced'; -import type { AuthInternal, MultiFactorEnrollmentAssertionInternal } from './types/internal'; +import type { + MultiFactorInfo, + MultiFactorSession, + MultiFactorUser as MultiFactorUserType, + MultiFactorAssertion, +} from './types/auth'; +import type { + AuthInternal, + MultiFactorEnrollmentAssertionInternal, + UserInternal, +} from './types/internal'; type MultiFactorAuthHost = { - currentUser: FirebaseAuthTypes.User | null; + currentUser: User | null; native: AuthInternal['native']; }; /** * Return a MultiFactorUser instance the gateway to multi-factor operations. */ -export function multiFactor(auth: MultiFactorAuthHost): FirebaseAuthTypes.MultiFactorUser { +export function multiFactor(auth: MultiFactorAuthHost): MultiFactorUserType { return new MultiFactorUser(auth); } export class MultiFactorUser { - readonly enrolledFactors: FirebaseAuthTypes.MultiFactorInfo[]; + readonly enrolledFactors: MultiFactorInfo[]; private readonly _auth: MultiFactorAuthHost; - constructor(auth: MultiFactorAuthHost, user?: FirebaseAuthTypes.User) { + constructor(auth: MultiFactorAuthHost, user?: User) { this._auth = auth; if (user === undefined) { - user = auth.currentUser as FirebaseAuthTypes.User; + user = auth.currentUser as User; } if (!user) { @@ -31,7 +39,7 @@ export class MultiFactorUser { this.enrolledFactors = user.multiFactor?.enrolledFactors ?? []; } - getSession(): Promise { + getSession(): Promise { return this._auth.native.getSession(); } @@ -41,7 +49,7 @@ export class MultiFactorUser { * profile, which is necessary to see the multi-factor changes. */ async enroll( - multiFactorAssertion: FirebaseAuthTypes.MultiFactorAssertion | ModularMultiFactorAssertion, + multiFactorAssertion: MultiFactorAssertion | ModularMultiFactorAssertion, displayName?: string | null, ): Promise { const assertion = multiFactorAssertion as MultiFactorEnrollmentAssertionInternal; @@ -65,16 +73,17 @@ export class MultiFactorUser { // We need to reload the user otherwise the changes are not visible // TODO reload not working on Other platform - await reload(this._auth.currentUser as unknown as User); + const currentUser = this._auth.currentUser as UserInternal | null; + if (currentUser) { + await currentUser.reload(); + } } - async unenroll(enrollmentId: FirebaseAuthTypes.MultiFactorInfo | string): Promise { - await this._auth.native.unenrollMultiFactor( - enrollmentId as string | FirebaseAuthTypes.MultiFactorInfo, - ); + async unenroll(enrollmentId: MultiFactorInfo | string): Promise { + await this._auth.native.unenrollMultiFactor(enrollmentId as string | MultiFactorInfo); if (this._auth.currentUser) { - await reload(this._auth.currentUser as unknown as User); + await (this._auth.currentUser as UserInternal).reload(); } } } diff --git a/packages/auth/lib/namespaced.ts b/packages/auth/lib/namespaced.ts deleted file mode 100644 index 72cfb78195..0000000000 --- a/packages/auth/lib/namespaced.ts +++ /dev/null @@ -1,849 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { - createDeprecationProxy, - isAndroid, - isBoolean, - isNull, - isOther, - isString, - isValidUrl, - parseListenerOrObserver, -} from '@react-native-firebase/app/dist/module/common'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import { - FirebaseModule, - createModuleNamespace, - getFirebaseRoot, - type ModuleConfig, -} from '@react-native-firebase/app/dist/module/internal'; -import ConfirmationResult from './ConfirmationResult'; -import { PhoneAuthState } from './PhoneAuthState'; -import PhoneAuthListener from './PhoneAuthListener'; -import PhoneMultiFactorGenerator from './PhoneMultiFactorGenerator'; -import TotpMultiFactorGenerator from './TotpMultiFactorGenerator'; -import Settings from './Settings'; -import User from './User'; -import { getMultiFactorResolver } from './getMultiFactorResolver'; -import { MultiFactorUser, multiFactor } from './multiFactor'; -import AppleAuthProvider from './providers/AppleAuthProvider'; -import EmailAuthProvider from './providers/EmailAuthProvider'; -import FacebookAuthProvider from './providers/FacebookAuthProvider'; -import GithubAuthProvider from './providers/GithubAuthProvider'; -import GoogleAuthProvider from './providers/GoogleAuthProvider'; -import OAuthProvider from './providers/OAuthProvider'; -import OIDCAuthProvider from './providers/OIDCAuthProvider'; -import PhoneAuthProvider from './providers/PhoneAuthProvider'; -import TwitterAuthProvider from './providers/TwitterAuthProvider'; -import { version } from './version'; -import fallBackModule from './web/RNFBAuthModule'; -import { PasswordPolicyMixin } from './password-policy/PasswordPolicyMixin'; -import type { CallbackOrObserver, FirebaseAuthTypes } from './types/namespaced'; -import type { EmulatorConfig, Unsubscribe, User as ModularUser } from './types/auth'; -import type { - AuthIdTokenChangedEventInternal, - AuthInternal, - AuthStateChangedEventInternal, - NativePhoneAuthCredentialInternal, - NativeUserCredentialInternal, - NativeUserInternal, - PasswordPolicyInternal, - PasswordValidationStatusInternal, - PhoneAuthStateChangedEventInternal, -} from './types/internal'; - -type AuthProviderWithObjectInternal = FirebaseAuthTypes.AuthProvider & { - toObject(): Record; -}; - -type AuthErrorWithCodeInternal = Error & { - code?: string; -}; - -const nativeEvents = ['auth_state_changed', 'auth_id_token_changed', 'phone_auth_state_changed']; - -const statics = { - AppleAuthProvider, - EmailAuthProvider, - PhoneAuthProvider, - GoogleAuthProvider, - GithubAuthProvider, - TwitterAuthProvider, - FacebookAuthProvider, - PhoneMultiFactorGenerator, - TotpMultiFactorGenerator, - OAuthProvider, - OIDCAuthProvider, - PhoneAuthState, - getMultiFactorResolver, - multiFactor, -}; - -const namespace = 'auth'; -const nativeModuleName = 'RNFBAuthModule'; - -type BeforeAuthStateChangedEntry = { - callback: (user: ModularUser | null) => void | Promise; - onAbort?: () => void; -}; - -class FirebaseAuthModule extends FirebaseModule { - _user: FirebaseAuthTypes.User | null; - _settings: FirebaseAuthTypes.AuthSettings | null; - _authResult: boolean; - _languageCode: string; - _tenantId: string | null; - _projectPasswordPolicy: PasswordPolicyInternal | null; - _tenantPasswordPolicies: Record; - _emulatorConfig: EmulatorConfig | null; - _authStateReadyPromise: Promise | null; - _beforeAuthStateChangedCallbacks: BeforeAuthStateChangedEntry[]; - _getPasswordPolicyInternal!: () => PasswordPolicyInternal | null; - _updatePasswordPolicy!: () => Promise; - _recachePasswordPolicy!: () => Promise; - validatePassword!: (password: string) => Promise; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - customUrlOrRegion?: string | null, - ) { - super(app, config, customUrlOrRegion); - this._user = null; - this._settings = null; - this._authResult = false; - this._languageCode = this.native.APP_LANGUAGE[this.app.name] ?? ''; - this._tenantId = null; - this._projectPasswordPolicy = null; - this._tenantPasswordPolicies = {}; - this._emulatorConfig = null; - this._authStateReadyPromise = null; - this._beforeAuthStateChangedCallbacks = []; - - if (!this.languageCode) { - this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]'] ?? ''; - } - - const initialUser = this.native.APP_USER[this.app.name]; - if (initialUser) { - this._setUser(initialUser); - } - - this.emitter.addListener(this.eventNameForApp('auth_state_changed'), event => { - const authEvent = event as AuthStateChangedEventInternal; - void this._handleAuthStateChanged(authEvent.user); - }); - - this.emitter.addListener(this.eventNameForApp('phone_auth_state_changed'), event => { - const phoneAuthEvent = event as PhoneAuthStateChangedEventInternal; - const eventKey = `phone:auth:${phoneAuthEvent.requestKey}:${phoneAuthEvent.type}`; - this.emitter.emit(eventKey, phoneAuthEvent.state); - }); - - this.emitter.addListener(this.eventNameForApp('auth_id_token_changed'), event => { - const authEvent = event as AuthIdTokenChangedEventInternal; - this._setUser(authEvent.user); - this.emitter.emit(this.eventNameForApp('onIdTokenChanged'), this._user); - }); - - this.native.addAuthStateListener(); - this.native.addIdTokenListener(); - - // custom authDomain in only available from App's FirebaseOptions, - // but we need it in Auth if it exists. During app configuration we store - // mappings from app name to authDomain, this auth constructor - // is a reasonable time to use the mapping and set it into auth natively - if (!isOther) { - // Only supported on native platforms - this.native.configureAuthDomain(); - } - } - - get languageCode(): string { - return this._languageCode; - } - - set languageCode(code: string | null) { - // For modular API, not recommended to set languageCode directly as it should be set in the native SDKs first - if (!isString(code) && !isNull(code)) { - throw new Error( - "firebase.auth().languageCode = (*) expected 'languageCode' to be a string or null value", - ); - } - // as this is a setter, we can't use async/await. So we set it first so it is available immediately - if (code === null) { - this._languageCode = this.native.APP_LANGUAGE[this.app.name] ?? ''; - - if (!this.languageCode) { - this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]'] ?? ''; - } - } else { - this._languageCode = code; - } - // This sets it natively - void this.setLanguageCode(code); - } - - get config(): Record { - // Native iOS/Android Firebase Auth SDKs do not expose the firebase-js-sdk config object. - return {}; - } - - get tenantId(): string | null { - return this._tenantId; - } - - set tenantId(tenantId: string | null) { - void this.setTenantId(tenantId); - } - - get emulatorConfig(): EmulatorConfig | null { - return this._emulatorConfig; - } - - authStateReady(): Promise { - if (this._authResult) { - return Promise.resolve(); - } - - if (!this._authStateReadyPromise) { - this._authStateReadyPromise = new Promise(resolve => { - const unsubscribe = this.onAuthStateChanged(() => { - if (this._authResult) { - unsubscribe(); - resolve(); - } - }); - - if (this._authResult) { - unsubscribe(); - resolve(); - } - }); - } - - return this._authStateReadyPromise; - } - - beforeAuthStateChanged( - callback: (user: ModularUser | null) => void | Promise, - onAbort?: () => void, - ): Unsubscribe { - const entry: BeforeAuthStateChangedEntry = { callback, onAbort }; - this._beforeAuthStateChangedCallbacks.push(entry); - - return () => { - const index = this._beforeAuthStateChangedCallbacks.indexOf(entry); - if (index >= 0) { - this._beforeAuthStateChangedCallbacks.splice(index, 1); - } - }; - } - - async updateCurrentUser(user: FirebaseAuthTypes.User | null): Promise { - if (user === null) { - await this.signOut(); - return; - } - - const userInternal = user as unknown as { - _auth?: AuthInternal; - _user?: NativeUserInternal; - }; - - if (!userInternal._auth || userInternal._auth !== (this as unknown as AuthInternal)) { - throw new Error( - "firebase.auth().updateCurrentUser() expected 'user' to be an Auth instance from the same Firebase App", - ); - } - - if (!userInternal._user) { - throw new Error("firebase.auth().updateCurrentUser(*) expected 'user' to be a valid User"); - } - - this._setUser(userInternal._user); - } - - async _handleAuthStateChanged(nativeUser?: NativeUserInternal | null): Promise { - const pendingUser = nativeUser - ? (createDeprecationProxy( - new User(this as unknown as AuthInternal, nativeUser), - ) as FirebaseAuthTypes.User) - : null; - - for (const { callback, onAbort } of [...this._beforeAuthStateChangedCallbacks]) { - try { - await callback(pendingUser as ModularUser | null); - } catch { - onAbort?.(); - return; - } - } - - this._setUser(nativeUser); - this.emitter.emit(this.eventNameForApp('onAuthStateChanged'), this._user); - } - - get settings(): FirebaseAuthTypes.AuthSettings { - if (!this._settings) { - this._settings = new Settings( - this as unknown as AuthInternal, - ) as FirebaseAuthTypes.AuthSettings; - } - return this._settings; - } - - get currentUser(): FirebaseAuthTypes.User | null { - return this._user; - } - - _setUser(user?: NativeUserInternal | null): FirebaseAuthTypes.User | null { - this._user = user - ? (createDeprecationProxy( - new User(this as unknown as AuthInternal, user), - ) as FirebaseAuthTypes.User) - : null; - this._authResult = true; - this.emitter.emit(this.eventNameForApp('onUserChanged'), this._user); - return this._user; - } - - _setUserCredential( - userCredential: NativeUserCredentialInternal, - ): FirebaseAuthTypes.UserCredential { - const user = createDeprecationProxy( - new User(this as unknown as AuthInternal, userCredential.user), - ) as FirebaseAuthTypes.User; - this._user = user; - this._authResult = true; - this.emitter.emit(this.eventNameForApp('onUserChanged'), this._user); - return { - additionalUserInfo: userCredential.additionalUserInfo, - user, - }; - } - - async setLanguageCode(code: string | null): Promise { - if (!isString(code) && !isNull(code)) { - throw new Error( - "firebase.auth().setLanguageCode(*) expected 'languageCode' to be a string or null value", - ); - } - - await this.native.setLanguageCode(code); - - if (code === null) { - this._languageCode = this.native.APP_LANGUAGE[this.app.name] ?? ''; - - if (!this.languageCode) { - this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]'] ?? ''; - } - } else { - this._languageCode = code; - } - } - - async setTenantId(tenantId: string | null): Promise { - if (!isString(tenantId) && !isNull(tenantId)) { - throw new Error("firebase.auth().setTenantId(*) expected 'tenantId' to be a string"); - } - await this.native.setTenantId(tenantId); - this._tenantId = tenantId; - } - - onAuthStateChanged( - listenerOrObserver: CallbackOrObserver, - ): () => void { - const listener = parseListenerOrObserver( - listenerOrObserver, - ) as FirebaseAuthTypes.AuthListenerCallback; - const subscription = this.emitter.addListener( - this.eventNameForApp('onAuthStateChanged'), - listener, - ); - - if (this._authResult) { - Promise.resolve().then(() => { - listener(this._user || null); - }); - } - return () => subscription.remove(); - } - - onIdTokenChanged( - listenerOrObserver: CallbackOrObserver, - ): () => void { - const listener = parseListenerOrObserver( - listenerOrObserver, - ) as FirebaseAuthTypes.AuthListenerCallback; - const subscription = this.emitter.addListener( - this.eventNameForApp('onIdTokenChanged'), - listener, - ); - - if (this._authResult) { - Promise.resolve().then(() => { - listener(this._user || null); - }); - } - return () => subscription.remove(); - } - - onUserChanged( - listenerOrObserver: CallbackOrObserver, - ): () => void { - const listener = parseListenerOrObserver( - listenerOrObserver, - ) as FirebaseAuthTypes.AuthListenerCallback; - const subscription = this.emitter.addListener(this.eventNameForApp('onUserChanged'), listener); - if (this._authResult) { - Promise.resolve().then(() => { - listener(this._user || null); - }); - } - - return () => { - subscription.remove(); - }; - } - - signOut(): Promise { - return this.native.signOut().then(() => { - this._setUser(null); - }); - } - - signInAnonymously(): Promise { - return this.native - .signInAnonymously() - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ); - } - - signInWithPhoneNumber( - phoneNumber: string, - forceResend?: boolean, - ): Promise { - if (isAndroid) { - return this.native - .signInWithPhoneNumber(phoneNumber, forceResend || false) - .then( - (result: NativePhoneAuthCredentialInternal) => - new ConfirmationResult(this as unknown as AuthInternal, result.verificationId), - ); - } - - return this.native - .signInWithPhoneNumber(phoneNumber) - .then( - (result: NativePhoneAuthCredentialInternal) => - new ConfirmationResult(this as unknown as AuthInternal, result.verificationId), - ); - } - - verifyPhoneNumber( - phoneNumber: string, - autoVerifyTimeoutOrForceResend?: number | boolean, - forceResend?: boolean, - ): FirebaseAuthTypes.PhoneAuthListener { - let _forceResend = forceResend; - let _autoVerifyTimeout: number | undefined = 60; - - if (isBoolean(autoVerifyTimeoutOrForceResend)) { - _forceResend = autoVerifyTimeoutOrForceResend; - } else { - _autoVerifyTimeout = autoVerifyTimeoutOrForceResend; - } - - return new PhoneAuthListener( - this as unknown as AuthInternal, - phoneNumber, - _autoVerifyTimeout, - _forceResend, - ) as FirebaseAuthTypes.PhoneAuthListener; - } - - verifyPhoneNumberWithMultiFactorInfo( - multiFactorHint: FirebaseAuthTypes.MultiFactorInfo, - session: FirebaseAuthTypes.MultiFactorSession, - ): Promise { - return this.native.verifyPhoneNumberWithMultiFactorInfo(multiFactorHint.uid, session); - } - - verifyPhoneNumberForMultiFactor( - phoneInfoOptions: FirebaseAuthTypes.PhoneMultiFactorEnrollInfoOptions, - ): Promise { - const { phoneNumber, session } = phoneInfoOptions; - return this.native.verifyPhoneNumberForMultiFactor(phoneNumber, session); - } - - resolveMultiFactorSignIn( - session: FirebaseAuthTypes.MultiFactorSession, - verificationId: string, - verificationCode: string, - ): Promise { - return this.native - .resolveMultiFactorSignIn(session, verificationId, verificationCode) - .then((userCredential: NativeUserCredentialInternal) => { - return this._setUserCredential(userCredential); - }); - } - - resolveTotpSignIn( - session: FirebaseAuthTypes.MultiFactorSession, - uid: string, - totpSecret: string, - ): Promise { - return this.native - .resolveTotpSignIn(session, uid, totpSecret) - .then((userCredential: NativeUserCredentialInternal) => { - return this._setUserCredential(userCredential); - }); - } - - createUserWithEmailAndPassword( - email: string, - password: string, - ): Promise { - return ( - this.native - .createUserWithEmailAndPassword(email, password) - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ) - /* istanbul ignore next - native error handling cannot be unit tested */ - .catch((error: AuthErrorWithCodeInternal) => { - if (error.code === 'auth/password-does-not-meet-requirements') { - return this._recachePasswordPolicy() - .catch(() => { - // Silently ignore recache failures - the original error matters more - }) - .then(() => { - throw error; - }); - } - throw error; - }) - ); - } - - signInWithEmailAndPassword( - email: string, - password: string, - ): Promise { - return ( - this.native - .signInWithEmailAndPassword(email, password) - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ) - /* istanbul ignore next - native error handling cannot be unit tested */ - .catch((error: AuthErrorWithCodeInternal) => { - if (error.code === 'auth/password-does-not-meet-requirements') { - return this._recachePasswordPolicy() - .catch(() => { - // Silently ignore recache failures - the original error matters more - }) - .then(() => { - throw error; - }); - } - throw error; - }) - ); - } - - signInWithCustomToken(customToken: string): Promise { - return this.native - .signInWithCustomToken(customToken) - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ); - } - - signInWithCredential( - credential: FirebaseAuthTypes.AuthCredential, - ): Promise { - return this.native - .signInWithCredential(credential.providerId, credential.token, credential.secret) - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ); - } - - revokeToken(authorizationCode: string): Promise { - return this.native.revokeToken(authorizationCode); - } - - sendPasswordResetEmail( - email: string, - actionCodeSettings: FirebaseAuthTypes.ActionCodeSettings | null = null, - ): Promise { - return this.native.sendPasswordResetEmail(email, actionCodeSettings); - } - - private _resolveActionCodeSettings( - actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, - ): FirebaseAuthTypes.ActionCodeSettings { - if (actionCodeSettings && isString(actionCodeSettings.url)) { - return actionCodeSettings; - } - - const authDomain = this.app.options.authDomain; - let url = 'https://localhost'; - if (authDomain && isString(authDomain)) { - url = isValidUrl(authDomain) ? authDomain : `https://${authDomain}`; - } - - return { - ...(actionCodeSettings ?? {}), - url, - handleCodeInApp: actionCodeSettings?.handleCodeInApp ?? true, - }; - } - - sendSignInLinkToEmail( - email: string, - actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, - ): Promise { - return this.native.sendSignInLinkToEmail( - email, - this._resolveActionCodeSettings(actionCodeSettings), - ); - } - - isSignInWithEmailLink(emailLink: string): Promise { - return this.native.isSignInWithEmailLink(emailLink); - } - - signInWithEmailLink( - email: string, - emailLink?: string, - ): Promise { - return this.native - .signInWithEmailLink(email, emailLink ?? '') - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ); - } - - confirmPasswordReset(code: string, newPassword: string): Promise { - return ( - this.native - .confirmPasswordReset(code, newPassword) - /* istanbul ignore next - native error handling cannot be unit tested */ - .catch((error: AuthErrorWithCodeInternal) => { - if (error.code === 'auth/password-does-not-meet-requirements') { - return this._recachePasswordPolicy() - .catch(() => { - // Silently ignore recache failures - the original error matters more - }) - .then(() => { - throw error; - }); - } - throw error; - }) - ); - } - - applyActionCode(code: string): Promise { - return this.native.applyActionCode(code).then(user => { - this._setUser(user); - }); - } - - checkActionCode(code: string): Promise { - return this.native.checkActionCode(code); - } - - fetchSignInMethodsForEmail(email: string): Promise { - return this.native.fetchSignInMethodsForEmail(email); - } - - verifyPasswordResetCode(code: string): Promise { - return this.native.verifyPasswordResetCode(code); - } - - useUserAccessGroup(userAccessGroup: string): Promise { - if (isAndroid) { - return Promise.resolve(); - } - return this.native.useUserAccessGroup(userAccessGroup).then(() => undefined); - } - - getRedirectResult(): Promise { - throw new Error( - 'firebase.auth().getRedirectResult() is unsupported by the native Firebase SDKs.', - ); - } - - setPersistence(): Promise { - throw new Error('firebase.auth().setPersistence() is unsupported by the native Firebase SDKs.'); - } - - signInWithPopup( - provider: FirebaseAuthTypes.AuthProvider, - ): Promise { - return this.native - .signInWithProvider((provider as AuthProviderWithObjectInternal).toObject()) - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ); - } - - signInWithRedirect( - provider: FirebaseAuthTypes.AuthProvider, - ): Promise { - return this.native - .signInWithProvider((provider as AuthProviderWithObjectInternal).toObject()) - .then((userCredential: NativeUserCredentialInternal) => - this._setUserCredential(userCredential), - ); - } - - // firebase issue - https://github.com/invertase/react-native-firebase/pull/655#issuecomment-349904680 - useDeviceLanguage(): void { - throw new Error( - 'firebase.auth().useDeviceLanguage() is unsupported by the native Firebase SDKs.', - ); - } - - useEmulator(url: string, options?: { disableWarnings?: boolean }): void { - if (!url || !isString(url) || !isValidUrl(url)) { - throw new Error('firebase.auth().useEmulator() takes a non-empty string URL'); - } - - let _url = url; - const androidBypassEmulatorUrlRemap = - typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && - this.firebaseJson.android_bypass_emulator_url_remap; - if (!androidBypassEmulatorUrlRemap && isAndroid && _url) { - if (_url.startsWith('http://localhost')) { - _url = _url.replace('http://localhost', 'http://10.0.2.2'); - // eslint-disable-next-line no-console - console.log( - 'Mapping auth host "localhost" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', - ); - } - if (_url.startsWith('http://127.0.0.1')) { - _url = _url.replace('http://127.0.0.1', 'http://10.0.2.2'); - // eslint-disable-next-line no-console - console.log( - 'Mapping auth host "127.0.0.1" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', - ); - } - } - - // Native calls take the host and port split out - const hostPortRegex = /^http:\/\/([\w\d-.]+):(\d+)$/; - const urlMatches = _url.match(hostPortRegex); - if (!urlMatches) { - throw new Error('firebase.auth().useEmulator() unable to parse host and port from URL'); - } - const host = urlMatches[1]; - const portString = urlMatches[2]; - if (!host) { - throw new Error('firebase.auth().useEmulator() unable to parse host from URL'); - } - const port = portString ? parseInt(portString, 10) : undefined; - this._emulatorConfig = { - protocol: 'http', - host, - port: port ?? null, - options: { - disableWarnings: options?.disableWarnings ?? false, - }, - }; - this.native.useEmulator(host, port); - // @ts-ignore - undocumented return, useful for unit testing - return [host, port]; - } - - getMultiFactorResolver( - error: FirebaseAuthTypes.MultiFactorError, - ): FirebaseAuthTypes.MultiFactorResolver { - return getMultiFactorResolver( - this as unknown as AuthInternal, - error, - ) as FirebaseAuthTypes.MultiFactorResolver; - } - - multiFactor(user: FirebaseAuthTypes.User): FirebaseAuthTypes.MultiFactorUser { - if (!this.currentUser || user.uid !== this.currentUser.uid) { - throw new Error('firebase.auth().multiFactor() only operates on currentUser'); - } - return new MultiFactorUser( - this as unknown as AuthInternal, - user, - ) as FirebaseAuthTypes.MultiFactorUser; - } - - getCustomAuthDomain(): Promise { - return this.native.getCustomAuthDomain(); - } -} - -// Apply password policy mixin to FirebaseAuthModule -Object.assign(FirebaseAuthModule.prototype, PasswordPolicyMixin); - -// import { SDK_VERSION } from '@react-native-firebase/auth'; -export const SDK_VERSION = version; - -const authNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents, - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseAuthModule, -}); - -type AuthNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseAuthTypes.Module, - FirebaseAuthTypes.Statics -> & { - auth: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseAuthTypes.Module, - FirebaseAuthTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import auth from '@react-native-firebase/auth'; -// auth().X(...); -export default authNamespace as unknown as AuthNamespace; - -// import auth, { firebase } from '@react-native-firebase/auth'; -// auth().X(...); -// firebase.auth().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'auth', - FirebaseAuthTypes.Module, - FirebaseAuthTypes.Statics, - false - >; - -// Register the interop module for non-native platforms. -setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/auth/lib/providers/PhoneAuthProvider.ts b/packages/auth/lib/providers/PhoneAuthProvider.ts index 03ac93655d..de1ba74c53 100644 --- a/packages/auth/lib/providers/PhoneAuthProvider.ts +++ b/packages/auth/lib/providers/PhoneAuthProvider.ts @@ -16,6 +16,7 @@ */ import { createPhoneAuthCredential } from '../credentials'; +import type { AuthInternal } from '../types/internal'; import type { ApplicationVerifier, Auth, @@ -35,20 +36,7 @@ type FirebaseError = AuthError; const providerId = 'phone' as const; -type PhoneAuthProviderAuth = { - app: { - auth(): { - verifyPhoneNumber(phoneNumber: string): PhoneAuthListener; - verifyPhoneNumberWithMultiFactorInfo( - hint: Pick, - session: PhoneMultiFactorSignInInfoOptions['session'], - ): Promise; - verifyPhoneNumberForMultiFactor( - phoneInfoOptions: PhoneMultiFactorEnrollInfoOptions, - ): Promise; - }; - }; -}; +type PhoneAuthProviderAuth = AuthInternal; function isPhoneMultiFactorSignInOptions( phoneInfoOptions: PhoneInfoOptions, @@ -131,7 +119,7 @@ export default class PhoneAuthProvider { _appVerifier?: ApplicationVerifier, ): Promise { if (typeof phoneInfoOptions === 'string') { - return verificationIdFromListener(this._auth.app.auth().verifyPhoneNumber(phoneInfoOptions)); + return verificationIdFromListener(this._auth.verifyPhoneNumber(phoneInfoOptions)); } if (isPhoneMultiFactorSignInOptions(phoneInfoOptions)) { @@ -139,19 +127,18 @@ export default class PhoneAuthProvider { uid: phoneInfoOptions.multiFactorUid!, }; - return this._auth.app - .auth() - .verifyPhoneNumberWithMultiFactorInfo(multiFactorHint, phoneInfoOptions.session); + return this._auth.verifyPhoneNumberWithMultiFactorInfo( + multiFactorHint as MultiFactorInfo, + phoneInfoOptions.session, + ); } if (isPhoneMultiFactorEnrollOptions(phoneInfoOptions)) { - return this._auth.app.auth().verifyPhoneNumberForMultiFactor(phoneInfoOptions); + return this._auth.verifyPhoneNumberForMultiFactor(phoneInfoOptions); } if (isPhoneSingleFactorOptions(phoneInfoOptions)) { - return verificationIdFromListener( - this._auth.app.auth().verifyPhoneNumber(phoneInfoOptions.phoneNumber), - ); + return verificationIdFromListener(this._auth.verifyPhoneNumber(phoneInfoOptions.phoneNumber)); } throw new Error( diff --git a/packages/auth/lib/types/auth.ts b/packages/auth/lib/types/auth.ts index 38e3519679..101a49f32c 100644 --- a/packages/auth/lib/types/auth.ts +++ b/packages/auth/lib/types/auth.ts @@ -284,6 +284,8 @@ export interface User extends UserInfo { readonly providerData: UserInfo[]; readonly refreshToken: string; readonly tenantId: string | null; + /** Present on native user instances returned from auth state listeners. */ + readonly multiFactor?: MultiFactor | null; delete(): Promise; getIdToken(forceRefresh?: boolean): Promise; getIdTokenResult(forceRefresh?: boolean): Promise; @@ -391,3 +393,16 @@ export type PhoneInfoOptions = export interface TotpMultiFactorAssertion extends MultiFactorAssertion {} export interface TotpMultiFactorInfo extends MultiFactorInfo {} + +export type AuthListenerCallback = (user: User | null) => void; + +export type CallbackOrObserver any> = T | { next: T }; + +export interface UpdateProfile { + displayName?: string | null; + photoURL?: string | null; +} + +export interface MultiFactor { + enrolledFactors: MultiFactorInfo[]; +} diff --git a/packages/auth/lib/types/internal.ts b/packages/auth/lib/types/internal.ts index 4486db3215..00c58921c9 100644 --- a/packages/auth/lib/types/internal.ts +++ b/packages/auth/lib/types/internal.ts @@ -25,26 +25,23 @@ import type EventEmitter from 'react-native/Libraries/vendor/emitter/EventEmitte import type { ActionCodeInfo, ActionCodeSettings, + AdditionalUserInfo, Auth, AuthCredential, AuthProvider, + ConfirmationResult, IdTokenResult, + MultiFactor, MultiFactorAssertion, + MultiFactorInfo, + MultiFactorResolver, + MultiFactorSession, PhoneAuthListener, User, UserCredential, } from './auth'; -import type { CallbackOrObserver, FirebaseAuthTypes } from './namespaced'; -export type AuthModularDeprecationArg = string; - -export type WithAuthDeprecationArg = F extends (...args: infer P) => infer R - ? (...args: [...P, AuthModularDeprecationArg]) => R - : never; - -export interface AppWithAuthInternal { - auth(deprecationArg?: AuthModularDeprecationArg): Auth; -} +export type { CallbackOrObserver } from './auth'; export type AuthListenerCallbackInternal = (user: User | null) => void; @@ -53,14 +50,14 @@ export type AuthProviderWithObjectInternal = AuthProvider & { }; export type UserCredentialWithAdditionalUserInfoInternal = UserCredential & { - user: FirebaseAuthTypes.User; - additionalUserInfo?: FirebaseAuthTypes.AdditionalUserInfo; + user: User; + additionalUserInfo?: AdditionalUserInfo; }; -export type UserCredentialResultInternal = FirebaseAuthTypes.UserCredential & +export type UserCredentialResultInternal = UserCredential & Partial>; -export type ActionCodeInfoResultInternal = FirebaseAuthTypes.ActionCodeInfo | ActionCodeInfo; +export type ActionCodeInfoResultInternal = ActionCodeInfo; export type ConfirmationResultResultInternal = { verificationId: string | null; @@ -68,23 +65,18 @@ export type ConfirmationResultResultInternal = { }; export type MultiFactorResolverResultInternal = { - hints: FirebaseAuthTypes.MultiFactorInfo[]; - session: FirebaseAuthTypes.MultiFactorSession; - resolveSignIn( - assertion: FirebaseAuthTypes.MultiFactorAssertion | MultiFactorAssertion, - ): Promise; + hints: MultiFactorInfo[]; + session: MultiFactorSession; + resolveSignIn(assertion: MultiFactorAssertion): Promise; }; -export type MultiFactorUserSourceInternal = FirebaseAuthTypes.User; +export type MultiFactorUserSourceInternal = User; export type MultiFactorUserResultInternal = { - enrolledFactors: FirebaseAuthTypes.MultiFactorInfo[]; - getSession(): Promise; - enroll( - assertion: FirebaseAuthTypes.MultiFactorAssertion | MultiFactorAssertion, - displayName?: string | null, - ): Promise; - unenroll(option: FirebaseAuthTypes.MultiFactorInfo | string): Promise; + enrolledFactors: MultiFactorInfo[]; + getSession(): Promise; + enroll(assertion: MultiFactorAssertion, displayName?: string | null): Promise; + unenroll(option: MultiFactorInfo | string): Promise; }; export type MultiFactorEnrollmentAssertionInternal = @@ -123,14 +115,14 @@ export interface NativeUserInternal { emailVerified?: boolean; isAnonymous?: boolean; metadata: NativeUserMetadataInternal; - multiFactor?: FirebaseAuthTypes.MultiFactor | null; + multiFactor?: MultiFactor | null; phoneNumber?: string | null; photoURL?: string | null; tenantId?: string | null; } export interface NativeUserCredentialInternal { - additionalUserInfo?: FirebaseAuthTypes.AdditionalUserInfo; + additionalUserInfo?: AdditionalUserInfo; user: NativeUserInternal; } @@ -247,21 +239,18 @@ export interface RNFBAuthModule { timeout?: number, forceResend?: boolean, ): void | Promise; - verifyPhoneNumberWithMultiFactorInfo( - uid: string, - session: FirebaseAuthTypes.MultiFactorSession, - ): Promise; + verifyPhoneNumberWithMultiFactorInfo(uid: string, session: MultiFactorSession): Promise; verifyPhoneNumberForMultiFactor( phoneNumber: string, - session: FirebaseAuthTypes.MultiFactorSession, + session: MultiFactorSession, ): Promise; resolveMultiFactorSignIn( - session: FirebaseAuthTypes.MultiFactorSession, + session: MultiFactorSession, verificationId: string, verificationCode: string, ): Promise; resolveTotpSignIn( - session: FirebaseAuthTypes.MultiFactorSession, + session: MultiFactorSession, uid: string, totpSecret: string, ): Promise; @@ -282,17 +271,14 @@ export interface RNFBAuthModule { revokeToken(authorizationCode: string): Promise; sendPasswordResetEmail( email: string, - actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings | null, - ): Promise; - sendSignInLinkToEmail( - email: string, - actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, + actionCodeSettings?: ActionCodeSettings | null, ): Promise; + sendSignInLinkToEmail(email: string, actionCodeSettings?: ActionCodeSettings): Promise; isSignInWithEmailLink(emailLink: string): Promise; signInWithEmailLink(email: string, emailLink?: string): Promise; confirmPasswordReset(code: string, newPassword: string): Promise; applyActionCode(code: string): Promise; - checkActionCode(code: string): Promise; + checkActionCode(code: string): Promise; fetchSignInMethodsForEmail(email: string): Promise; verifyPasswordResetCode(code: string): Promise; useUserAccessGroup(userAccessGroup: string): Promise; @@ -318,9 +304,7 @@ export interface RNFBAuthModule { provider: Record, ): Promise; reload(): Promise; - sendEmailVerification( - actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, - ): Promise; + sendEmailVerification(actionCodeSettings?: ActionCodeSettings): Promise; unlink(providerId: string): Promise; updateEmail(email: string): Promise; updatePassword(password: string): Promise; @@ -335,21 +319,21 @@ export interface RNFBAuthModule { }): Promise; verifyBeforeUpdateEmail( newEmail: string, - actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, + actionCodeSettings?: ActionCodeSettings, ): Promise; forceRecaptchaFlowForTesting(forceRecaptchaFlow: boolean): void | Promise; setAppVerificationDisabledForTesting(disabled: boolean): void | Promise; setAutoRetrievedSmsCodeForPhoneNumber(phoneNumber: string, smsCode: string): Promise; - getSession(): Promise; + getSession(): Promise; finalizeMultiFactorEnrollment(token: string, secret: string, displayName?: string): Promise; finalizeTotpEnrollment( totpSecret: string, verificationCode: string, displayName?: string, ): Promise; - unenrollMultiFactor(enrollmentId: string | FirebaseAuthTypes.MultiFactorInfo): Promise; + unenrollMultiFactor(enrollmentId: string | MultiFactorInfo): Promise; getMultiFactorResolver(error: unknown): MultiFactorResolverResultInternal | null; - generateTotpSecret(session: FirebaseAuthTypes.MultiFactorSession): Promise<{ secretKey: string }>; + generateTotpSecret(session: MultiFactorSession): Promise<{ secretKey: string }>; generateQrCodeUrl(secretKey: string, accountName: string, issuer: string): Promise; openInOtpApp(secretKey: string, qrCodeUrl: string): string | void; assertionForSignIn( @@ -360,7 +344,7 @@ export interface RNFBAuthModule { export type AuthInternal = Auth & { app: ReactNativeFirebase.FirebaseApp; - currentUser: FirebaseAuthTypes.User | null; + currentUser: User | null; applyActionCode(code: string): Promise; checkActionCode(code: string): Promise; confirmPasswordReset(code: string, newPassword: string): Promise; @@ -373,10 +357,10 @@ export type AuthInternal = Auth & { getMultiFactorResolver(error: unknown): MultiFactorResolverResultInternal | null; isSignInWithEmailLink(emailLink: string): Promise; onAuthStateChanged( - listenerOrObserver: CallbackOrObserver, + listenerOrObserver: import('./auth').CallbackOrObserver, ): () => void; onIdTokenChanged( - listenerOrObserver: CallbackOrObserver, + listenerOrObserver: import('./auth').CallbackOrObserver, ): () => void; sendPasswordResetEmail( email: string, @@ -415,6 +399,13 @@ export type AuthInternal = Auth & { autoVerifyTimeoutOrForceResend?: number | boolean, forceResend?: boolean, ): PhoneAuthListener; + verifyPhoneNumberWithMultiFactorInfo( + multiFactorHint: MultiFactorInfo, + session: MultiFactorSession, + ): Promise; + verifyPhoneNumberForMultiFactor( + phoneInfoOptions: import('./auth').PhoneMultiFactorEnrollInfoOptions, + ): Promise; verifyPasswordResetCode(code: string): Promise; revokeToken(authorizationCode: string): Promise; native: RNFBAuthModule; @@ -424,23 +415,23 @@ export type AuthInternal = Auth & { _tenantId: string | null; _projectPasswordPolicy: PasswordPolicyInternal | null; _tenantPasswordPolicies: Record; - _setUser(user?: NativeUserInternal | null): FirebaseAuthTypes.User | null; + _setUser(user?: NativeUserInternal | null): User | null; _setUserCredential( userCredential: NativeUserCredentialInternal, ): UserCredentialWithAdditionalUserInfoInternal; resolveMultiFactorSignIn( - session: FirebaseAuthTypes.MultiFactorSession, + session: MultiFactorSession, verificationId: string, verificationCode: string, ): Promise; resolveTotpSignIn( - session: FirebaseAuthTypes.MultiFactorSession, + session: MultiFactorSession, uid: string, totpSecret: string, ): Promise; } & PasswordPolicyMixinInternal; -export type UserInternal = FirebaseAuthTypes.User & { +export type UserInternal = User & { _auth?: AuthInternal; _user?: NativeUserInternal; getIdTokenResult(forceRefresh?: boolean): Promise; @@ -469,8 +460,8 @@ export type UserInternal = FirebaseAuthTypes.User & { verifyBeforeUpdateEmail(newEmail: string, actionCodeSettings?: ActionCodeSettings): Promise; }; -export type ConfirmationResultInternal = FirebaseAuthTypes.ConfirmationResult; -export type MultiFactorResolverInternal = FirebaseAuthTypes.MultiFactorResolver; +export type ConfirmationResultInternal = ConfirmationResult; +export type MultiFactorResolverInternal = MultiFactorResolver; declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { interface ReactNativeFirebaseNativeModules { diff --git a/packages/auth/lib/types/namespaced.ts b/packages/auth/lib/types/namespaced.ts deleted file mode 100644 index 9c0ca50227..0000000000 --- a/packages/auth/lib/types/namespaced.ts +++ /dev/null @@ -1,2350 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ReactNativeFirebase } from '@react-native-firebase/app'; -import type { ActionCodeURL as ActionCodeURLClass } from '../ActionCodeURL'; - -/** - * Firebase Authentication package for React Native. - * - * #### Example: Access the firebase export from the `auth` package: - * - * ```js - * import { firebase } from '@react-native-firebase/auth'; - * - * // firebase.auth().X - * ``` - * - * #### Example: Using the default export from the `auth` package: - * - * ```js - * import auth from '@react-native-firebase/auth'; - * - * // auth().X - * ``` - * - * #### Example: Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/auth'; - * - * // firebase.auth().X - * ``` - * TODO @salakar @ehesp missing auth providers (PhoneAuthProvider, Facebook etc) - * - * @firebase auth - */ -/** - * @deprecated Use the exported auth types directly instead. - * FirebaseAuthTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export declare namespace FirebaseAuthTypes { - import FirebaseModule = ReactNativeFirebase.FirebaseModule; - import NativeFirebaseError = ReactNativeFirebase.NativeFirebaseError; - - export interface AuthError extends NativeFirebaseError { - customData?: Record; - } - - export const OperationType: { - LINK: 'link'; - REAUTHENTICATE: 'reauthenticate'; - SIGN_IN: 'signIn'; - }; - - export type ActionCodeURL = ActionCodeURLClass; - - export interface ApplicationVerifier { - readonly type: string; - verify(): Promise; - } - - export interface PasswordPolicy { - readonly enforcementState?: string; - readonly forceUpgradeOnSignin?: boolean; - readonly schemaVersion: number; - } - - export interface NativeFirebaseAuthError extends NativeFirebaseError { - userInfo: { - /** - * When trying to sign in or link with an AuthCredential which was already associated with an account, - * you might receive an updated credential (depending of provider) which you can use to recover from the error. - */ - authCredential: AuthCredential | null; - /** - * When trying to sign in the user might be prompted for a second factor confirmation. Can - * can use this object to initialize the second factor flow and recover from the error. - */ - resolver: MultiFactorResolver | null; - }; - } - - /** - * Interface that represents the credentials returned by an auth provider. Implementations specify the details - * about each auth provider's credential requirements. - * - * TODO Missing; signInMethod, toJSON, fromJSON - * - * #### Example - * - * ```js - * const provider = firebase.auth.EmailAuthProvider; - * const authCredential = provider.credential('foo@bar.com', '123456'); - * - * await firebase.auth().signInWithCredential(authCredential); - * ``` - */ - export interface AuthCredential { - /** - * The authentication provider ID for the credential. For example, 'facebook.com', or 'google.com'. - */ - providerId: string; - token: string; - secret: string; - } - - /** - * Interface that represents an auth provider. Implemented by other providers. - */ - export interface AuthProvider { - /** - * The provider ID of the provider. - */ - PROVIDER_ID: string; - /** - * Creates a new `AuthCredential`. - * - * @returns {@link AuthCredential}. - * @param token A provider token. - * @param secret A provider secret. - */ - credential: (token: string | null, secret?: string) => AuthCredential; - } - - /** - * Interface that represents an OAuth provider. Implemented by other providers. - */ - export interface OAuthProvider extends AuthProvider { - /** - * The provider ID of the provider. - * @param providerId - */ - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (providerId: string): OAuthProvider; - /** - * Creates a new `AuthCredential`. - * - * @returns {@link AuthCredential}. - * @param token A provider token. - * @param secret A provider secret. - */ - credential: (token: string | null, secret?: string) => AuthCredential; - /** - * Sets the OAuth custom parameters to pass in an OAuth request for sign-in - * operations. - * - * @remarks - * For a detailed list, check the reserved required OAuth 2.0 parameters such as `client_id`, - * `redirect_uri`, `scope`, `response_type`, and `state` are not allowed and will be ignored. - * - * @param customOAuthParameters - The custom OAuth parameters to pass in the OAuth request. - */ - setCustomParameters: (customOAuthParameters: Record) => AuthProvider; - /** - * Retrieve the current list of custom parameters. - * @returns The current map of OAuth custom parameters. - */ - getCustomParameters: () => Record; - /** - * Add an OAuth scope to the credential. - * - * @param scope - Provider OAuth scope to add. - */ - addScope: (scope: string) => AuthProvider; - /** - * Retrieve the current list of OAuth scopes. - */ - getScopes: () => string[]; - } - - /** - * Interface that represents an Open ID Connect auth provider. Implemented by other providers. - */ - export interface OIDCProvider { - /** - * The provider ID of the provider. - */ - PROVIDER_ID: string; - /** - * Creates a new `OIDCProvider`. - * - * @returns {@link AuthCredential}. - * @param oidcSuffix this is the "Provider ID" value from the firebase console fx `azure_test`. - * @param idToken A provider ID token. - */ - credential: (oidcSuffix: string, idToken: string) => AuthCredential; - } - - /** - * Email and password auth provider implementation. - */ - export interface EmailAuthProvider { - /** - * The provider ID. Always returns `password`. - */ - PROVIDER_ID: string; - /** - * This corresponds to the sign-in method identifier as returned in {@link fetchSignInMethodsForEmail}. - * - * #### Example - * - * ```js - * const signInMethods = await firebase.auth().fetchSignInMethodsForEmail('...'); - * if (signInMethods.indexOf(firebase.auth.EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD) != -1) { - * // User can sign in with email/link - * } - * ``` - */ - EMAIL_LINK_SIGN_IN_METHOD: string; - /** - * This corresponds to the sign-in method identifier as returned in {@link fetchSignInMethodsForEmail}. - * - * #### Example - * - * ```js - * const signInMethods = await firebase.auth().fetchSignInMethodsForEmail('...'); - * if (signInMethods.indexOf(firebase.auth.EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD) != -1) { - * // User can sign in with email/password - * } - * ``` - */ - EMAIL_PASSWORD_SIGN_IN_METHOD: string; - /** - * Returns the auth provider credential. - * - * #### Example - * - * ```js - * const authCredential = firebase.auth.EmailAuthProvider.credential('joe.bloggs@example.com', '123456'); - * ``` - * - * @returns {@link AuthCredential} - * @param email Users email address. - * @param password User account password. - */ - credential: (email: string, password: string) => AuthCredential; - /** - * Initialize an `EmailAuthProvider` credential using an email and an email link after a sign in with email link operation. - * - * #### Example - * - * ```js - * const authCredential = firebase.auth.EmailAuthProvider.credentialWithLink('joe.bloggs@example.com', 'https://myexample.com/invite'); - * ``` - * - * @param email Users email address. - * @param emailLink Sign-in email link. - */ - credentialWithLink: (email: string, emailLink: string) => AuthCredential; - } - - /** - * - */ - export interface PhoneAuthState { - /** - * SMS message with verification code sent to phone number. - */ - CODE_SENT: 'sent'; - /** - * The timeout specified in {@link verifyPhoneNumber} has expired. - */ - AUTO_VERIFY_TIMEOUT: 'timeout'; - /** - * Phone number auto-verification succeeded. - */ - AUTO_VERIFIED: 'verified'; - /** - * Phone number verification failed with an error. - */ - ERROR: 'error'; - } - - export interface MultiFactorSession { - // this is has no documented contents, it is simply returned from some APIs and passed to others - } - - export interface PhoneMultiFactorGenerator { - /** - * Identifies second factors of type phone. - */ - FACTOR_ID: FactorId.PHONE; - - /** - * Build a MultiFactorAssertion to resolve the multi-factor sign in process. - */ - assertion(credential: AuthCredential): MultiFactorAssertion; - } - - /** - * Represents a TOTP secret that is used for enrolling a TOTP second factor. - * Contains the shared secret key and other parameters to generate time-based - * one-time passwords. Implements methods to retrieve the shared secret key, - * generate a QR code URL, and open the QR code URL in an OTP authenticator app. - * - * Differs from standard firebase JS implementation in three ways: - * 1- there is no visibility into ony properties other than the secretKey - * 2- there is an added `openInOtpApp` method supported by native SDKs - * 3- the return value of generateQrCodeUrl is a Promise because react-native bridge is async - * @public - */ - export class TotpSecret { - /** used internally to support non-default auth instances */ - private readonly auth; - /** - * Shared secret key/seed used for enrolling in TOTP MFA and generating OTPs. - */ - readonly secretKey: string; - - private constructor(); - - /** - * Returns a QR code URL as described in - * https://github.com/google/google-authenticator/wiki/Key-Uri-Format - * This can be displayed to the user as a QR code to be scanned into a TOTP app like Google Authenticator. - * If the optional parameters are unspecified, an accountName of userEmail and issuer of firebaseAppName are used. - * - * @param accountName the name of the account/app along with a user identifier. - * @param issuer issuer of the TOTP (likely the app name). - * @returns A Promise that resolves to a QR code URL string. - */ - generateQrCodeUrl(accountName?: string, issuer?: string): Promise; - - /** - * Opens the specified QR Code URL in an OTP authenticator app on the device. - * The shared secret key and account name will be populated in the OTP authenticator app. - * The URL uses the otpauth:// scheme and will be opened on an app that handles this scheme, - * if it exists on the device, possibly opening the ecocystem-specific app store with a generic - * query for compatible apps if no app exists on the device. - * - * @param qrCodeUrl the URL to open in the app, from generateQrCodeUrl - */ - openInOtpApp(qrCodeUrl: string): string; - } - - export interface TotpMultiFactorGenerator { - FACTOR_ID: FactorId.TOTP; - - assertionForSignIn(uid: string, totpSecret: string): MultiFactorAssertion; - - assertionForEnrollment(secret: TotpSecret, code: string): MultiFactorAssertion; - - /** - * @param auth - The Auth instance. Only used for native platforms, should be ignored on web. - */ - generateSecret( - session: FirebaseAuthTypes.MultiFactorSession, - auth?: FirebaseAuthTypes.Module, - ): Promise; - } - - export interface MultiFactorError extends AuthError { - /** Details about the MultiFactorError. */ - readonly customData: AuthError['customData'] & { - /** - * The type of operation (sign-in, linking, or re-authentication) that raised the error. - */ - readonly operationType: (typeof OperationType)[keyof typeof OperationType]; - }; - } - - /** - * firebase.auth.X - */ - /** - * @deprecated Use the package default export or modular APIs instead. - */ - export interface Statics { - /** - * Return the #{@link MultiFactorUser} instance for the current user. - */ - multiFactor: multiFactor; - /** - * Try and obtain a #{@link MultiFactorResolver} instance based on an error. - * Returns null if no resolver object could be found. - * - */ - getMultiFactorResolver: getMultiFactorResolver; - /** - * Email and password auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.EmailAuthProvider; - * ``` - */ - EmailAuthProvider: EmailAuthProvider; - /** - * Phone auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.PhoneAuthProvider; - * ``` - */ - PhoneAuthProvider: AuthProvider; - /** - * Google auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.GoogleAuthProvider; - * ``` - */ - GoogleAuthProvider: AuthProvider; - /** - * Apple auth provider implementation. Currently this is iOS only. - * - * For Apple Authentication please see our [`@invertase/react-native-apple-authentication`](https://github.com/invertase/react-native-apple-authentication) library which integrates well with Firebase and provides Firebase + Apple Auth examples. - * - * #### Example - * - * ```js - * firebase.auth.AppleAuthProvider; - * ``` - */ - AppleAuthProvider: AuthProvider; - /** - * Github auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.GithubAuthProvider; - * ``` - */ - GithubAuthProvider: AuthProvider; - /** - * Twitter auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.TwitterAuthProvider; - * ``` - */ - TwitterAuthProvider: AuthProvider; - /** - * Facebook auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.FacebookAuthProvider; - * ``` - */ - FacebookAuthProvider: AuthProvider; - /** - * Custom OAuth auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.OAuthProvider; - * ``` - */ - OAuthProvider: OAuthProvider; - /** - * Custom Open ID connect auth provider implementation. - * - * #### Example - * - * ```js - * firebase.auth.OIDCAuthProvider; - * ``` - */ - OIDCAuthProvider: OIDCProvider; - /** - * A PhoneAuthState interface. - * - * #### Example - * - * ```js - * firebase.auth.PhoneAuthState; - * ``` - */ - PhoneAuthState: PhoneAuthState; - - /** - * A PhoneMultiFactorGenerator interface. - */ - PhoneMultiFactorGenerator: PhoneMultiFactorGenerator; - SDK_VERSION: string; - } - - /** - * A structure containing additional user information from a federated identity provider via {@link UserCredential}. - * - * #### Example - * - * ```js - * const userCredential = await firebase.auth().signInAnonymously(); - * console.log('Additional user info: ', userCredential.additionalUserInfo); - * ``` - * - * @error auth/operation-not-allowed Thrown if anonymous accounts are not enabled. Enable anonymous accounts in the Firebase Console, under the Auth tab. - */ - export interface AdditionalUserInfo { - /** - * Returns whether the user is new or existing. - */ - isNewUser: boolean; - /** - * Returns an Object containing IDP-specific user data if the provider is one of Facebook, - * GitHub, Google, Twitter, Microsoft, or Yahoo. - */ - profile?: Record; - /** - * Returns the provider ID for specifying which provider the information in `profile` is for. - */ - providerId: string; - /** - * Returns the username if the provider is GitHub or Twitter. - */ - username?: string; - } - - /** - * A structure containing a User, an AuthCredential, the operationType, and any additional user - * information that was returned from the identity provider. operationType could be 'signIn' for - * a sign-in operation, 'link' for a linking operation and 'reauthenticate' for a re-authentication operation. - * - * TODO @salakar; missing credential, operationType - */ - export interface UserCredential { - /** - * Any additional user information assigned to the user. - */ - additionalUserInfo?: AdditionalUserInfo; - /** - * Returns the {@link User} interface of this credential. - */ - user: User; - } - - /** - * Holds the user metadata for the current {@link User}. - * - * #### Example - * - * ```js - * const user = firebase.auth().currentUser; - * console.log('User metadata: ', user.metadata); - * ``` - */ - export interface UserMetadata { - /** - * Returns the timestamp at which this account was created as dictated by the server clock - * as an ISO Date string. - */ - creationTime?: string; - /** - * Returns the last signin timestamp as dictated by the server clock as an ISO Date string. - * This is only accurate up to a granularity of 2 minutes for consecutive sign-in attempts. - */ - lastSignInTime?: string; - } - - /** - * Identifies the type of a second factor. - */ - export enum FactorId { - PHONE = 'phone', - TOTP = 'totp', - } - - /** - * Contains information about a second factor. - */ - export type MultiFactorInfo = PhoneMultiFactorInfo | TotpMultiFactorInfo; - - export interface PhoneMultiFactorInfo extends MultiFactorInfoCommon { - factorId: 'phone'; - /** - * The phone number used for this factor. - */ - phoneNumber: string; - } - - export interface TotpMultiFactorInfo extends MultiFactorInfoCommon { - factorId: 'totp'; - } - - export interface MultiFactorInfoCommon { - /** - * User friendly name for this factor. - */ - displayName?: string; - /** - * Time the second factor was enrolled, in UTC. - */ - enrollmentTime: string; - /** - * Unique id for this factor. - */ - uid: string; - } - - export interface MultiFactorAssertion { - token: string; - secret: string; - } - - export interface PhoneMultiFactorEnrollInfoOptions { - phoneNumber: string; - session: MultiFactorSession; - } - - export interface PhoneMultiFactorSignInInfoOptions { - multiFactorHint?: MultiFactorInfo; - - /** - * Unused in react-native-firebase ipmlementation - */ - multiFactorUid?: string; - - session: MultiFactorSession; - } - - /** - * Facilitates the recovery when a user needs to provide a second factor to sign-in. - */ - export interface MultiFactorResolver { - /** - * A list of enrolled factors that can be used to complete the multi-factor challenge. - */ - hints: MultiFactorInfo[]; - /** - * Serialized session this resolver belongs to. - */ - session: MultiFactorSession; - - /** - * For testing purposes only - */ - _auth?: FirebaseAuthTypes.Module; - - /** - * Resolve the multi factor flow. - */ - resolveSignIn(assertion: MultiFactorAssertion): Promise; - } - - /** - * Try and obtain a #{@link MultiFactorResolver} instance based on an error. - * Returns null if no resolver object could be found. - * - * #### Example - * - * ```js - * const auth = firebase.auth(); - * auth.signInWithEmailAndPassword(email, password).then((user) => { - * // signed in - * }).catch((error) => { - * if (error.code === 'auth/multi-factor-auth-required') { - * const resolver = getMultiFactorResolver(auth, error); - * } - * }); - * ``` - */ - export type getMultiFactorResolver = ( - auth: FirebaseAuthTypes.Module, - error: unknown, - ) => MultiFactorResolver | null; - - /** - * The entry point for most multi-factor operations. - */ - export interface MultiFactorUser { - /** - * Returns the user's enrolled factors. - */ - enrolledFactors: MultiFactorInfo[]; - - /** - * Return the session for this user. - */ - getSession(): Promise; - - /** - * Enroll an additional factor. Provide an optional display name that can be shown to the user. - * The method will ensure the user state is reloaded after successfully enrolling a factor. - */ - enroll(assertion: MultiFactorAssertion, displayName?: string): Promise; - - /** - * Unenroll a previously enrolled multi-factor authentication factor. - * @param option The multi-factor option to unenroll. - */ - unenroll(option: MultiFactorInfo | string): Promise; - } - - /** - * Return the #{@link MultiFactorUser} instance for the current user. - */ - export type multiFactor = (auth: FirebaseAuthTypes.Module) => Promise; - - /** - * Holds information about the user's enrolled factors. - * - * #### Example - * - * ```js - * const user = firebase.auth().currentUser; - * console.log('User multi factors: ', user.multiFactor); - * ``` - */ - export interface MultiFactor { - /** - * Returns the enrolled factors - */ - enrolledFactors: MultiFactorInfo[]; - } - - /** - * Represents a collection of standard profile information for a user. Can be used to expose - * profile information returned by an identity provider, such as Google Sign-In or Facebook Login. - * - * TODO @salakar: isEmailVerified - * - * #### Example - * - * ```js - * const user = firebase.auth().currentUser; - * - * user.providerData.forEach((userInfo) => { - * console.log('User info for provider: ', userInfo); - * }); - * ``` - */ - export interface UserInfo { - /** - * Returns the user's display name, if available. - */ - displayName?: string | null; - /** - * Returns the email address corresponding to the user's account in the specified provider, if available. - */ - email?: string | null; - /** - * The phone number normalized based on the E.164 standard (e.g. +16505550101) for the current user. This is null if the user has no phone credential linked to the account. - */ - phoneNumber?: string | null; - /** - * Returns a url to the user's profile picture, if available. - */ - photoURL?: string | null; - /** - * Returns the unique identifier of the provider type that this instance corresponds to. - */ - providerId: string; - /** - * Returns a string representing the multi-tenant tenant id. This is null if the user is not associated with a tenant. - */ - tenantId?: string; - /** - * Returns a user identifier as specified by the authentication provider. - */ - uid: string; - } - - /** - * Interface representing ID token result obtained from {@link User#getIdTokenResult}. - * It contains the ID token JWT string and other helper properties for getting different data - * associated with the token as well as all the decoded payload claims. - * - * TODO @salakar validate timestamp types - * - * #### Example - * - * ```js - * const idTokenResult = await firebase.auth().currentUser.getIdTokenResult(); - * console.log('User JWT: ', idTokenResult.token); - * ``` - */ - export interface IdTokenResult { - /** - * The Firebase Auth ID token JWT string. - */ - token: string; - /** - * The authentication time formatted as a UTC string. This is the time the user authenticated - * (signed in) and not the time the token was refreshed. - */ - authTime: string; - /** - * The ID token issued at time formatted as a UTC string. - */ - issuedAtTime: string; - /** - * The ID token expiration time formatted as a UTC string. - */ - expirationTime: string; - /** - * The sign-in provider through which the ID token was obtained (anonymous, custom, - * phone, password, etc). Note, this does not map to provider IDs. - */ - signInProvider: null | string; - /** - * The entire payload claims of the ID token including the standard reserved claims as well as - * the custom claims. - */ - claims: { - [key: string]: any; - }; - } - - /** - * Request used to update user profile information. - * - * #### Example - * - * ```js - * const update = { - * displayName: 'Alias', - * photoURL: 'https://my-cdn.com/assets/user/123.png', - * }; - * - * await firebase.auth().currentUser.updateProfile(update); - * ``` - */ - export interface UpdateProfile { - /** - * An optional display name for the user. Explicitly pass null to clear the displayName. - */ - displayName?: string | null; - /** - * An optional photo URL for the user. Explicitly pass null to clear the photoURL. - */ - photoURL?: string | null; - } - - /** - * A result from a {@link signInWithPhoneNumber} call. - * - * #### Example - * - * ```js - * // Force a new message to be sent - * const result = await firebase.auth().signInWithPhoneNumber('#4423456789'); - * const user = await result.confirm('12345'); - * ``` - */ - export interface ConfirmationResult { - /** - * The phone number authentication operation's verification ID. This can be used along with - * the verification code to initialize a phone auth credential. - */ - verificationId: string | null; - /** - * Finishes the sign in flow. Validates a code that was sent to the users device. - * - * @param verificationCode The code sent to the users device from Firebase. - */ - confirm(verificationCode: string): Promise; - } - - /** - * Android specific options which can be attached to the {@link ActionCodeSettings} object - * to be sent with requests such as {@link User#sendEmailVerification}. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.sendEmailVerification({ - * android: { - * installApp: true, - * packageName: 'com.awesome.app', - * }, - * }); - * ``` - */ - export interface ActionCodeSettingsAndroid { - /** - * Sets the Android package name. This will try to open the link in an android app if it is installed. - */ - packageName: string; - /** - * If installApp is passed, it specifies whether to install the Android app if the device supports it and the app is not already installed. If this field is provided without a packageName, an error is thrown explaining that the packageName must be provided in conjunction with this field. - */ - installApp?: boolean; - /** - * If minimumVersion is specified, and an older version of the app is installed, the user is taken to the Play Store to upgrade the app. The Android app needs to be registered in the Console. - */ - minimumVersion?: string; - } - - /** - * Additional data returned from a {@link checkActionCode} call. - * For the PASSWORD_RESET, VERIFY_EMAIL, and RECOVER_EMAIL actions, this object contains an email field with the address the email was sent to. - * For the RECOVER_EMAIL action, which allows a user to undo an email address change, this object also contains a fromEmail field with the user account's new email address. After the action completes, the user's email address will revert to the value in the email field from the value in fromEmail field. - * - * #### Example - * - * ```js - * const actionCodeInfo = await firebase.auth().checkActionCode('ABCD'); - *Data - * console.log('Action code email: ', actionCodeInfo.data.email); - * console.log('Action code from email: ', actionCodeInfo.data.fromEmail); - * ``` - */ - export interface ActionCodeInfoData { - /** - * This signifies the email before the call was made. - */ - email?: string; - /** - * This signifies the current email associated with the account, which may have changed as a result of the {@link checkActionCode} call performed. - */ - fromEmail?: string; - } - - /** - * The interface returned from a {@link checkActionCode} call. - * - * #### Example - * - * ```js - * const actionCodeInfo = await firebase.auth().checkActionCode('ABCD'); - * console.log('Action code operation: ', actionCodeInfo.operation); - * ``` - */ - export interface ActionCodeInfo { - /** - * The data associated with the action code. - */ - data: ActionCodeInfoData; - /** - * The operation from where the action originated. - */ - operation: 'PASSWORD_RESET' | 'VERIFY_EMAIL' | 'RECOVER_EMAIL' | 'EMAIL_SIGNIN' | 'ERROR'; - } - - /** - * iOS specific options which can be attached to the {@link ActionCodeSettings} object - * to be sent with requests such as {@link User#sendEmailVerification}. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.sendEmailVerification({ - * iOS: { - * bundleId: '123456', - * }, - * }); - * ``` - */ - export interface ActionCodeSettingsIos { - /** - * Sets the iOS bundle ID. This will try to open the link in an iOS app if it is installed. The iOS app needs to be registered in the Console. - */ - bundleId?: string; - } - - /** - * Options to be sent with requests such as {@link User#sendEmailVerification}. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.sendEmailVerification({ - * handleCodeInApp: true, - * url: 'app/email-verification', - * }); - * ``` - */ - export interface ActionCodeSettings { - /** - * Android specific settings. - */ - android?: ActionCodeSettingsAndroid; - - /** - * Whether the email action link will be opened in a mobile app or a web link first. The default is false. When set to true, the action code link will be be sent as a Universal Link or Android App Link and will be opened by the app if installed. In the false case, the code will be sent to the web widget first and then on continue will redirect to the app if installed. - */ - handleCodeInApp?: boolean; - - /** - * iOS specific settings. - */ - iOS?: ActionCodeSettingsIos; - - /** - * Sets the dynamic link domain (or subdomain) to use for the current link if it is to be opened using Firebase Dynamic Links. As multiple dynamic link domains can be configured per project, this field provides the ability to explicitly choose one. If none is provided, the first domain is used by default. - * Deprecated - use {@link ActionCodeSettings.linkDomain} instead. - */ - dynamicLinkDomain?: string; - - /** - * This URL represents the state/Continue URL in the form of a universal link. This URL can should be constructed as a universal link that would either directly open the app where the action code would be handled or continue to the app after the action code is handled by Firebase. - */ - url: string; - /** - * Firebase Dynamic Links is deprecated and will be shut down as early as August * 2025. - * Instead, use ActionCodeSettings.linkDomain to set a a custom domain. Learn more at: https://firebase.google.com/support/dynamic-links-faq - */ - linkDomain?: string; - } - - /** - * An auth listener callback function for {@link onAuthStateChanged}. - * - * #### Example - * - * ```js - * function listener(user) { - * if (user) { - * // Signed in - * } else { - * // Signed out - * } - * } - * - * firebase.auth().onAuthStateChanged(listener); - * ``` - */ - export type AuthListenerCallback = (user: User | null) => void; - - /** - * A snapshot interface of the current phone auth state. - * - * #### Example - * - * ```js - * firebase.auth().verifyPhoneNumber('+4423456789') - * .on('state_changed', (phoneAuthSnapshot) => { - * console.log('Snapshot state: ', phoneAuthSnapshot.state); - * }); - * ``` - */ - export interface PhoneAuthSnapshot { - /** - * The current phone auth verification state. - * - * - `sent`: On iOS, this is the final event received. Once sent, show a visible input box asking the user to enter the verification code. - * - `timeout`: Auto verification has timed out. Show a visible input box asking the user to enter the verification code. - * - `verified`: The verification code has automatically been verified by the Android device. The snapshot contains the verification ID & code to create a credential. - * - `error`: An error occurred. Handle or allow the promise to reject. - */ - state: 'sent' | 'timeout' | 'verified' | 'error'; - /** - * The verification ID to build a `PhoneAuthProvider` credential. - */ - verificationId: string; - /** - * The verification code. Will only be available if auto verification has taken place. - */ - code: string | null; - /** - * A native JavaScript error if an error occurs. - */ - error: NativeFirebaseError | null; - } - - /** - * A custom error in the event verifying a phone number failed. - * - * #### Example - * - * ```js - * firebase.auth().verifyPhoneNumber('+4423456789') - * .on('state_changed', (phoneAuthSnapshot) => { - * console.log('Snapshot state: ', phoneAuthSnapshot.state); - * }, (phoneAuthError) => { - * console.error('Error: ', phoneAuthError.message); - * }); - * ``` - */ - export interface PhoneAuthError { - /** - * The code the verification failed with. - */ - code: string | null; - /** - * The verification ID which failed. - */ - verificationId: string; - /** - * JavaScript error message. - */ - message: string | null; - /** - * JavaScript error stack trace. - */ - stack: string | null; - } - - /** - * The listener function returned from a {@link verifyPhoneNumber} call. - */ - export interface PhoneAuthListener { - /** - * The phone auth state listener. See {@link PhoneAuthState} for different event state types. - * - * #### Example - * - * ```js - * firebase.auth().verifyPhoneNumber('+4423456789') - * .on('state_changed', (phoneAuthSnapshot) => { - * console.log('State: ', phoneAuthSnapshot.state); - * }, (error) => { - * console.error(error); - * }, (phoneAuthSnapshot) => { - * console.log('Success'); - * }); - * ``` - * - * @param event The event to subscribe to. Currently only `state_changed` is available. - * @param observer The required observer function. Returns a new phone auth snapshot on each event. - * @param errorCb An optional error handler function. This is not required if the `error` snapshot state is being handled in the `observer`. - * @param successCb An optional success handler function. This is not required if the `sent` or `verified` snapshot state is being handled in the `observer`. - */ - on( - event: string, - observer: (snapshot: PhoneAuthSnapshot) => void, - errorCb?: (error: PhoneAuthError) => void, - successCb?: (snapshot: PhoneAuthSnapshot) => void, - ): PhoneAuthListener; - - /** - * A promise handler called once the `on` listener flow has succeeded or rejected. - * - * #### Example - * - * ```js - * firebase.auth().verifyPhoneNumber('+4423456789') - * .on('state_changed', (phoneAuthSnapshot) => { - * if (phoneAuthSnapshot.state === firebase.auth.PhoneAuthState.CODE_SENT) { - * return Promise.resolve(); - * } else { - * return Promise.reject( - * new Error('Code not sent!') - * ); - * } - * }) - * .then((phoneAuthSnapshot) => { - * console.log(phoneAuthSnapshot.state); - * }, (error) => { - * console.error(error.message); - * }); - * ``` - * - * @param onFulfilled Resolved promise handler. - * @param onRejected Rejected promise handler. - */ - then( - onFulfilled?: ((a: PhoneAuthSnapshot) => any) | null, - onRejected?: ((a: NativeFirebaseError) => any) | null, - ): Promise; - - /** - * A promise handler called once the `on` listener flow has rejected. - * - * #### Example - * - * ```js - * firebase.auth().verifyPhoneNumber('+4423456789') - * .on('state_changed', (phoneAuthSnapshot) => { - * return Promise.reject( - * new Error('Code not sent!') - * ); - * }) - * .catch((error) => { - * console.error(error.message); - * }); - * ``` - * - * > Used when no `onRejected` handler is passed to {@link PhoneAuthListener#then}. - * - * @param onRejected Rejected promise handler. - */ - catch(onRejected: (a: NativeFirebaseError) => any): Promise; - } - - /** - * Interface for module auth settings. - * - * #### Example - * - * ```js - * const settings = firebase.auth().settings; - * console.log(settings.appVerificationDisabledForTesting); - * ``` - */ - export interface AuthSettings { - /** - * Forces application verification to use the web reCAPTCHA flow for Phone Authentication. - * - * Once this has been called, every call to PhoneAuthProvider#verifyPhoneNumber() will skip the Play Integrity API verification flow and use the reCAPTCHA flow instead. - * - * > Calling this method a second time will overwrite the previously passed parameter. - * - * @android - * @param appName - * @param forceRecaptchaFlow - * @param promise - */ - forceRecaptchaFlowForTesting: boolean; - - /** - * Flag to disable app verification for the purpose of testing phone authentication. For this property to take effect, it needs to be set before rendering a reCAPTCHA app verifier. When this is disabled, a mock reCAPTCHA is rendered instead. This is useful for manual testing during development or for automated integration tests. - * - * > In order to use this feature, you will need to [whitelist your phone number](https://firebase.google.com/docs/auth/web/phone-auth#test-with-whitelisted-phone-numbers) via the Firebase Console. - * - * @param disabled Boolean value representing whether app verification should be disabled for testing. - */ - appVerificationDisabledForTesting: boolean; - - /** - * Calling this method a second time will overwrite the previously passed parameters. - * Only one number can be configured at a given time. - * - * > The phone number and SMS code here must have been configured in the Firebase Console (Authentication > Sign In Method > Phone). - * - * #### Example - * - * ```js - * await firebase.auth().settings.setAutoRetrievedSmsCodeForPhoneNumber('+4423456789', 'ABCDE'); - * ``` - * - * @android - * @param phoneNumber The users phone number. - * @param smsCode The pre-set SMS code. - */ - setAutoRetrievedSmsCodeForPhoneNumber(phoneNumber: string, smsCode: string): Promise; - } - - /** - * Represents a user's profile information in your Firebase project's user database. It also - * contains helper methods to change or retrieve profile information, as well as to manage that user's authentication state. - * - * #### Example 1 - * - * Subscribing to the users authentication state. - * - * ```js - * firebase.auth().onAuthStateChanged((user) => { - * if (user) { - * console.log('User email: ', user.email); - * } - * }); - * ``` - * - * #### Example 2 - * - * ```js - * const user = firebase.auth().currentUser; - * - * if (user) { - * console.log('User email: ', user.email); - * } - * ``` - */ - export interface User { - /** - * The user's display name (if available). - */ - displayName: string | null; - /** - * - The user's email address (if available). - */ - email: string | null; - /** - * - True if the user's email address has been verified. - */ - emailVerified: boolean; - /** - * Returns true if the user is anonymous; that is, the user account was created with - * {@link signInAnonymously} and has not been linked to another account - * with {@link linkWithCredential}. - */ - isAnonymous: boolean; - - /** - * Returns the {@link UserMetadata} associated with this user. - */ - metadata: UserMetadata; - - /** - * Returns the {@link MultiFactor} associated with this user. - */ - multiFactor: MultiFactor | null; - - /** - * Returns the phone number of the user, as stored in the Firebase project's user database, - * or null if none exists. This can be updated at any time by calling {@link User#updatePhoneNumber}. - */ - phoneNumber: string | null; - - /** - * The URL of the user's profile picture (if available). - */ - photoURL: string | null; - - /** - * Additional provider-specific information about the user. - */ - providerData: UserInfo[]; - - /** - * The authentication provider ID for the current user. - * For example, 'facebook.com', or 'google.com'. - */ - providerId: string; - - /** - * - The user's unique ID. - */ - uid: string; - - /** - * Delete the current user. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.delete(); - * ``` - * - * @error auth/requires-recent-login Thrown if the user's last sign-in time does not meet the security threshold. Use `auth.User#reauthenticateWithCredential` to resolve. This does not apply if the user is anonymous. - */ - delete(): Promise; - - /** - * Returns the users authentication token. - * - * #### Example - * - * ```js - * // Force a token refresh - * const idToken = await firebase.auth().currentUser.getIdToken(true); - * ``` - * - * @param forceRefresh A boolean value which forces Firebase to refresh the token. - */ - getIdToken(forceRefresh?: boolean): Promise; - - /** - * Returns a firebase.auth.IdTokenResult object which contains the ID token JWT string and - * other helper properties for getting different data associated with the token as well as - * all the decoded payload claims. - * - * #### Example - * - * ```js - * // Force a token refresh - * const idTokenResult = await firebase.auth().currentUser.getIdTokenResult(true); - * ``` - * - * @param forceRefresh boolean Force refresh regardless of token expiration. - */ - getIdTokenResult(forceRefresh?: boolean): Promise; - - /** - * Link the user with a 3rd party credential provider. - * - * #### Example - * - * ```js - * const facebookCredential = firebase.auth.FacebookAuthProvider.credential('access token from Facebook'); - * const userCredential = await firebase.auth().currentUser.linkWithCredential(facebookCredential); - * ``` - * - * @error auth/provider-already-linked Thrown if the provider has already been linked to the user. This error is thrown even if this is not the same provider's account that is currently linked to the user. - * @error auth/invalid-credential Thrown if the provider's credential is not valid. This can happen if it has already expired when calling link, or if it used invalid token(s). See the Firebase documentation for your provider, and make sure you pass in the correct parameters to the credential method. - * @error auth/credential-already-in-use Thrown if the account corresponding to the credential already exists among your users, or is already linked to a Firebase User. - * @error auth/email-already-in-use Thrown if the email corresponding to the credential already exists among your users. - * @error auth/operation-not-allowed Thrown if you have not enabled the provider in the Firebase Console. Go to the Firebase Console for your project, in the Auth section and the Sign in Method tab and configure the provider. - * @error auth/invalid-email Thrown if the email used in a auth.EmailAuthProvider.credential is invalid. - * @error auth/wrong-password Thrown if the password used in a auth.EmailAuthProvider.credential is not correct or when the user associated with the email does not have a password. - * @error auth/invalid-verification-code Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @throws on iOS {@link NativeFirebaseAuthError}, on Android {@link NativeFirebaseError} - * @param credential A created {@link AuthCredential}. - */ - linkWithCredential(credential: AuthCredential): Promise; - - /** - * Link the user with a federated 3rd party credential provider (Microsoft, Yahoo). - * The APIs here are the web-compatible linkWithPopup and linkWithRedirect but both - * share the same underlying native SDK behavior and may be used interchangably. - * - * #### Example - * - * ```js - * const provider = new firebase.auth.OAuthProvider('microsoft.com'); - * const userCredential = await firebase.auth().currentUser.linkWithPopup(provider); - * ``` - * - * @error auth/provider-already-linked Thrown if the provider has already been linked to the user. This error is thrown even if this is not the same provider's account that is currently linked to the user. - * @error auth/invalid-credential Thrown if the provider's credential is not valid. This can happen if it has already expired when calling link, or if it used invalid token(s). See the Firebase documentation for your provider, and make sure you pass in the correct parameters to the credential method. - * @error auth/credential-already-in-use Thrown if the account corresponding to the credential already exists among your users, or is already linked to a Firebase User. - * @error auth/email-already-in-use Thrown if the email corresponding to the credential already exists among your users. - * @error auth/operation-not-allowed Thrown if you have not enabled the provider in the Firebase Console. Go to the Firebase Console for your project, in the Auth section and the Sign in Method tab and configure the provider. - * @error auth/invalid-email Thrown if the email used in a auth.EmailAuthProvider.credential is invalid. - * @error auth/wrong-password Thrown if the password used in a auth.EmailAuthProvider.credential is not correct or when the user associated with the email does not have a password. - * @error auth/invalid-verification-code Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @throws on iOS {@link NativeFirebaseAuthError}, on Android {@link NativeFirebaseError} - * @param provider A created {@link AuthProvider}. - */ - linkWithPopup(provider: AuthProvider): Promise; - - /** - * Link the user with a federated 3rd party credential provider (Microsoft, Yahoo). - * The APIs here are the web-compatible linkWithPopup and linkWithRedirect but both - * share the same underlying native SDK behavior and may be used interchangably. - * - * #### Example - * - * ```js - * const provider = new firebase.auth.OAuthProvider('microsoft.com'); - * const userCredential = await firebase.auth().currentUser.linkWithRedirect(provider); - * ``` - * - * @error auth/provider-already-linked Thrown if the provider has already been linked to the user. This error is thrown even if this is not the same provider's account that is currently linked to the user. - * @error auth/invalid-credential Thrown if the provider's credential is not valid. This can happen if it has already expired when calling link, or if it used invalid token(s). See the Firebase documentation for your provider, and make sure you pass in the correct parameters to the credential method. - * @error auth/credential-already-in-use Thrown if the account corresponding to the credential already exists among your users, or is already linked to a Firebase User. - * @error auth/email-already-in-use Thrown if the email corresponding to the credential already exists among your users. - * @error auth/operation-not-allowed Thrown if you have not enabled the provider in the Firebase Console. Go to the Firebase Console for your project, in the Auth section and the Sign in Method tab and configure the provider. - * @error auth/invalid-email Thrown if the email used in a auth.EmailAuthProvider.credential is invalid. - * @error auth/wrong-password Thrown if the password used in a auth.EmailAuthProvider.credential is not correct or when the user associated with the email does not have a password. - * @error auth/invalid-verification-code Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @throws on iOS {@link NativeFirebaseAuthError}, on Android {@link NativeFirebaseError} - * @param provider A created {@link AuthProvider}. - */ - linkWithRedirect(provider: AuthProvider): Promise; - - /** - * Re-authenticate a user with a third-party authentication provider. - * - * #### Example - * - * ```js - * const facebookCredential = firebase.auth.FacebookAuthProvider.credential('access token from Facebook'); - * const userCredential = await firebase.auth().currentUser.reauthenticateWithCredential(facebookCredential); - * ``` - * - * @error auth/user-mismatch Thrown if the credential given does not correspond to the user. - * @error auth/user-not-found Thrown if the credential given does not correspond to any existing user. - * @error auth/invalid-credential Thrown if the provider's credential is not valid. This can happen if it has already expired when calling link, or if it used invalid token(s). See the Firebase documentation for your provider, and make sure you pass in the correct parameters to the credential method. - * @error auth/invalid-email Thrown if the email used in a auth.EmailAuthProvider.credential is invalid. - * @error auth/wrong-password Thrown if the password used in a auth.EmailAuthProvider.credential is not correct or when the user associated with the email does not have a password. - * @error auth/invalid-verification-code Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @param credential A created {@link AuthCredential}. - */ - reauthenticateWithCredential(credential: AuthCredential): Promise; - - /** - * Re-authenticate a user with a federated authentication provider (Microsoft, Yahoo). For native platforms, this will open a browser window. - * #### Example - * - * ```js - * const provider = new firebase.auth.OAuthProvider('microsoft.com'); - * const userCredential = await firebase.auth().currentUser.reauthenticateWithProvider(provider); - * ``` - * - * @error auth/user-mismatch Thrown if the credential given does not correspond to the user. - * @error auth/user-not-found Thrown if the credential given does not correspond to any existing user. - * @error auth/invalid-credential Thrown if the provider's credential is not valid. This can happen if it has already expired when calling link, or if it used invalid token(s). See the Firebase documentation for your provider, and make sure you pass in the correct parameters to the credential method. - * @error auth/invalid-email Thrown if the email used in a auth.EmailAuthProvider.credential is invalid. - * @error auth/wrong-password Thrown if the password used in a auth.EmailAuthProvider.credential is not correct or when the user associated with the email does not have a password. - * @error auth/invalid-verification-code Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @param provider A created {@link AuthProvider}. - * @returns A promise that resolves with no value. - */ - reauthenticateWithRedirect(provider: AuthProvider): Promise; - /** - * Re-authenticate a user with a federated authentication provider (Microsoft, Yahoo). For native platforms, this will open a browser window. - * pop-up equivalent on native platforms. - * - * @param provider - The auth provider. - * @returns A promise that resolves with the user credentials. - */ - reauthenticateWithPopup(provider: AuthProvider): Promise; - /** - * Refreshes the current user. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.reload(); - * ``` - */ - reload(): Promise; - - /** - * Sends a verification email to a user. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.sendEmailVerification({ - * handleCodeInApp: true, - * }); - * ``` - * - * > This will Promise reject if the user is anonymous. - * - * @error auth/missing-android-pkg-name An Android package name must be provided if the Android app is required to be installed. - * @error auth/missing-continue-uri A continue URL must be provided in the request. - * @error auth/missing-ios-bundle-id An iOS bundle ID must be provided if an App Store ID is provided. - * @error auth/invalid-continue-uri The continue URL provided in the request is invalid. - * @error auth/unauthorized-continue-uri The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase console. - * @param actionCodeSettings Any optional additional settings to be set before sending the verification email. - */ - sendEmailVerification(actionCodeSettings?: ActionCodeSettings): Promise; - /** - * Sends a link to the user's email address, when clicked, the user's Authentication email address will be updated to whatever - * was passed as the first argument. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.verifyBeforeUpdateEmail( - * 'foo@emailaddress.com', - * { - * handleCodeInApp: true, - * }); - * ``` - * - * > This will Promise reject if the user is anonymous. - * - * @error auth/missing-android-pkg-name An Android package name must be provided if the Android app is required to be installed. - * @error auth/missing-continue-uri A continue URL must be provided in the request. - * @error auth/missing-ios-bundle-id An iOS bundle ID must be provided if an App Store ID is provided. - * @error auth/invalid-continue-uri The continue URL provided in the request is invalid. - * @error auth/unauthorized-continue-uri The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase console. - * @param actionCodeSettings Any optional additional settings to be set before sending the verification email. - */ - verifyBeforeUpdateEmail(email: string, actionCodeSettings?: ActionCodeSettings): Promise; - - /** - * Returns a JSON-serializable representation of this object. - * - * #### Example - * - * ```js - * const user = firebase.auth().currentUser.toJSON(); - * ``` - */ - toJSON(): object; - - /** - * Unlinks a provider from a user account. - * - * #### Example - * - * ```js - * const user = await firebase.auth().currentUser.unlink('facebook.com'); - * ``` - * - * @error auth/no-such-provider Thrown if the user does not have this provider linked or when the provider ID given does not exist. - * @param providerId - */ - unlink(providerId: string): Promise; - - /** - * Updates the user's email address. - * - * See Firebase docs for more information on security & email validation. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.updateEmail('joe.bloggs@new-email.com'); - * ``` - * - * > This will Promise reject if the user is anonymous. - * - * @error auth/invalid-email Thrown if the email used is invalid. - * @error auth/email-already-in-use Thrown if the email is already used by another user. - * @error auth/requires-recent-login Thrown if the user's last sign-in time does not meet the security threshold. - * @param email The users new email address. - */ - updateEmail(email: string): Promise; - - /** - * Updates the users password. - * - * Important: this is a security sensitive operation that requires the user to have recently signed in. - * If this requirement isn't met, ask the user to authenticate again and then call firebase.User#reauthenticate. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.updatePassword('654321'); - * ``` - * - * > This will Promise reject is the user is anonymous. - * - * @error auth/weak-password Thrown if the password is not strong enough. - * @error auth/requires-recent-login Thrown if the user's last sign-in time does not meet the security threshold. - * @param password The users new password. - */ - updatePassword(password: string): Promise; - - /** - * Updates the user's phone number. - * - * See Firebase docs for more information on security & email validation. - * - * #### Example - * - * ```js - * const snapshot = await firebase.auth().verifyPhoneNumber('+4423456789') - * .on(...); // See PhoneAuthListener - wait for successful verification - * - * const credential = firebase.auth.PhoneAuthProvider.credential(snapshot.verificationId, snapshot.code); - * - * // Update user with new verified phone number - * await firebase.auth().currentUser.updatePhoneNumber(credential); - * ``` - * - * > This will Promise reject is the user is anonymous. - * - * @error auth/invalid-verification-code Thrown if the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the verification ID of the credential is not valid. - * @param credential A created `PhoneAuthCredential`. - */ - updatePhoneNumber(credential: AuthCredential): Promise; - - /** - * Updates a user's profile data. - * - * #### Example - * - * ```js - * await firebase.auth().currentUser.updateProfile({ - * displayName: 'Alias', - * }); - * ``` - */ - updateProfile(updates: UpdateProfile): Promise; - } - - /** - * The Firebase Authentication service is available for the default app or a given app. - * - * #### Example 1 - * - * Get the auth instance for the **default app**: - * - * ```js - * const authForDefaultApp = firebase.auth(); - * ``` - * - * #### Example 2 - * - * Get the auth instance for a **secondary app**: - * - * ```js - * const otherApp = firebase.app('otherApp'); - * const authForOtherApp = firebase.auth(otherApp); - * ``` - * - * TODO @salakar missing updateCurrentUser - */ - /** - * @deprecated Use the package default export or modular APIs instead. - */ - export class Module extends FirebaseModule { - /** - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * Returns the current tenant Id or null if it has never been set - * - * #### Example - * - * ```js - * const tenantId = firebase.auth().tenantId; - * ``` - */ - tenantId: string | null; - /** - * Returns the current language code. - * - * #### Example - * - * ```js - * const language = firebase.auth().languageCode; - * ``` - */ - get languageCode(): string; - /** - * Returns the current `AuthSettings`. - */ - settings: AuthSettings; - - /** - * Returns the currently signed-in user (or null if no user signed in). See the User interface documentation for detailed usage. - * - * #### Example - * - * ```js - * const user = firebase.auth().currentUser; - * ``` - * - * > It is recommended to use {@link onAuthStateChanged} to track whether the user is currently signed in. - */ - currentUser: User | null; - /** - * Sets the tenant id. - * - * #### Example - * - * ```js - * await firebase.auth().setTenantId('tenant-123'); - * ``` - * - * @error auth/invalid-tenant-id if the tenant id is invalid for some reason - * @param tenantId the tenantID current app bind to. Pass `null` to revert to project-level IdPs (firebase-js-sdk parity). On Android, clearing tenant ID may reject because the native Firebase Auth SDK does not support null tenant IDs ([firebase-android-sdk#3398](https://github.com/firebase/firebase-android-sdk/issues/3398)). - */ - setTenantId(tenantId: string | null): Promise; - /** - * Sets the language code. - * - * #### Example - * - * ```js - * // Set language to French - * await firebase.auth().setLanguageCode('fr'); - * ``` - * - * @param languageCode An ISO language code. - * 'null' value will set the language code to the app's current language. - */ - setLanguageCode(languageCode: string | null): Promise; - /** - * Listen for changes in the users auth state (logging in and out). - * This method returns a unsubscribe function to stop listening to events. - * Always ensure you unsubscribe from the listener when no longer needed to prevent updates to components no longer in use. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.auth().onAuthStateChanged((user) => { - * if (user) { - * // Signed in - * } else { - * // Signed out - * } - * }); - * - * // Unsubscribe from further state changes - * unsubscribe(); - * ``` - * - * @param listener A listener function which triggers when auth state changed (for example signing out). - */ - onAuthStateChanged(listener: CallbackOrObserver): () => void; - - /** - * Listen for changes in ID token. - * ID token can be verified (if desired) using the [admin SDK or a 3rd party JWT library](https://firebase.google.com/docs/auth/admin/verify-id-tokens) - * This method returns a unsubscribe function to stop listening to events. - * Always ensure you unsubscribe from the listener when no longer needed to prevent updates to components no longer in use. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.auth().onIdTokenChanged((user) => { - * if (user) { - * // User is signed in or token was refreshed. - * } - * }); - * - * // Unsubscribe from further state changes - * unsubscribe(); - * ``` - * - * @param listener A listener function which triggers when the users ID token changes. - */ - onIdTokenChanged(listener: CallbackOrObserver): () => void; - - /** - * Adds a listener to observe changes to the User object. This is a superset of everything from - * {@link onAuthStateChanged}, {@link onIdTokenChanged} and user changes. The goal of this - * method is to provide easier listening to all user changes, such as when credentials are - * linked and unlinked, without manually having to call {@link User#reload}. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.auth().onUserChanged((user) => { - * if (user) { - * // User is signed in or token was refreshed. - * } - * }); - * - * // Unsubscribe from further state changes - * unsubscribe(); - * ``` - * - * > This is an experimental feature and is only part of React Native Firebase. - * - * @param listener A listener function which triggers when the users data changes. - */ - onUserChanged(listener: CallbackOrObserver): () => void; - - /** - * Signs the user out. - * - * Triggers the {@link onAuthStateChanged} listener. - * - * #### Example - * - * ```js - * await firebase.auth().signOut(); - * ``` - * - */ - signOut(): Promise; - - /** - * Sign in a user anonymously. If the user has already signed in, that user will be returned. - * - * #### Example - * - * ```js - * const userCredential = await firebase.auth().signInAnonymously(); - * ``` - * - * @error auth/operation-not-allowed Thrown if anonymous accounts are not enabled. Enable anonymous accounts in the Firebase Console, under the Auth tab. - */ - signInAnonymously(): Promise; - - /** - * Signs in the user using their phone number. - * - * #### Example - * - * ```js - * // Force a new message to be sent - * const result = await firebase.auth().signInWithPhoneNumber('#4423456789', true); - * ``` - * - * @error auth/invalid-phone-number Thrown if the phone number has an invalid format. - * @error auth/missing-phone-number Thrown if the phone number is missing. - * @error auth/quota-exceeded Thrown if the SMS quota for the Firebase project has been exceeded. - * @error auth/user-disabled Thrown if the user corresponding to the given phone number has been disabled. - * @error auth/operation-not-allowed Thrown if you have not enabled the provider in the Firebase Console. Go to the Firebase Console for your project, in the Auth section and the Sign in Method tab and configure the provider. - * @param phoneNumber The devices phone number. - * @param forceResend Forces a new message to be sent if it was already recently sent. - */ - signInWithPhoneNumber(phoneNumber: string, forceResend?: boolean): Promise; - - /** - * Returns a PhoneAuthListener to listen to phone verification events, - * on the final completion event a PhoneAuthCredential can be generated for - * authentication purposes. - * - * #### Example - * - * ```js - * firebase.auth().verifyPhoneNumber('+4423456789', ) - * .on('state_changed', (phoneAuthSnapshot) => { - * console.log('Snapshot state: ', phoneAuthSnapshot.state); - * }); - * ``` - * - * @param phoneNumber The phone number identifier supplied by the user. Its format is normalized on the server, so it can be in any format here. (e.g. +16505550101). - * @param autoVerifyTimeoutOrForceResend If a number, sets in seconds how to to wait until auto verification times out. If boolean, sets the `forceResend` parameter. - * @param forceResend If true, resend the verification message even if it was recently sent. - */ - verifyPhoneNumber( - phoneNumber: string, - autoVerifyTimeoutOrForceResend?: number | boolean, - forceResend?: boolean, - ): PhoneAuthListener; - - /** - * Obtain a verification id to complete the multi-factor sign-in flow. - */ - verifyPhoneNumberWithMultiFactorInfo( - hint: MultiFactorInfo, - session: MultiFactorSession, - ): Promise; - - /** - * Send an SMS to the user for verification of second factor - * @param phoneInfoOptions the phone number and session to use during enrollment - */ - verifyPhoneNumberForMultiFactor( - phoneInfoOptions: PhoneMultiFactorEnrollInfoOptions, - ): Promise; - - /** - * Creates a new user with an email and password. - * - * This method also signs the user in once the account has been created. - * - * #### Example - * - * ```js - * const userCredential = await firebase.auth().createUserWithEmailAndPassword('joe.bloggs@example.com', '123456'); - * ``` - * - * @error auth/email-already-in-use Thrown if there already exists an account with the given email address. - * @error auth/invalid-email Thrown if the email address is not valid. - * @error auth/operation-not-allowed Thrown if email/password accounts are not enabled. Enable email/password accounts in the Firebase Console, under the Auth tab. - * @error auth/weak-password Thrown if the password is not strong enough. - * @param email The users email address. - * @param password The users password. - */ - createUserWithEmailAndPassword(email: string, password: string): Promise; - - /** - * Signs a user in with an email and password. - * - * ⚠️ Note: - * If "Email Enumeration Protection" is enabled in your Firebase Authentication settings (enabled by default), - * Firebase may return a generic `auth/invalid-login-credentials` error instead of more specific ones like - * `auth/user-not-found` or `auth/wrong-password`. This behavior is intended to prevent leaking information - * about whether an account with the given email exists. - * - * To receive detailed error codes, you must disable "Email Enumeration Protection", which may increase - * security risks if not properly handled on the frontend. - * - * #### Example - * - * ```js - * const userCredential = await firebase.auth().signInWithEmailAndPassword('joe.bloggs@example.com', '123456'); - * ``` - * - * @error auth/invalid-email Thrown if the email address is not valid. - * @error auth/user-disabled Thrown if the user corresponding to the given email has been disabled. - * @error auth/user-not-found Thrown if there is no user corresponding to the given email. (May be suppressed if email enumeration protection is enabled.) - * @error auth/wrong-password Thrown if the password is invalid or missing. (May be suppressed if email enumeration protection is enabled.) - * @param email The user's email address. - * @param password The user's password. - */ - signInWithEmailAndPassword(email: string, password: string): Promise; - - /** - * Signs a user in with a custom token. - * - * #### Example - * - * ```js - * // Create a custom token via the Firebase Admin SDK. - * const token = await firebase.auth().createCustomToken(uid, customClaims); - * ... - * // Use the token on the device to sign in. - * const userCredential = await firebase.auth().signInWithCustomToken(token); - * ``` - * - * @error auth/custom-token-mismatch Thrown if the custom token is for a different Firebase App. - * @error auth/invalid-custom-token Thrown if the custom token format is incorrect. - * @param customToken A custom token generated from the Firebase Admin SDK. - */ - signInWithCustomToken(customToken: string): Promise; - - /** - * Signs the user in with a generated credential. - * - * #### Example - * - * ```js - * // Generate a Firebase credential - * const credential = firebase.auth.FacebookAuthProvider.credential('access token from Facebook'); - * // Sign the user in with the credential - * const userCredential = await firebase.auth().signInWithCredential(credential); - * ``` - * - * @error auth/account-exists-with-different-credential Thrown if there already exists an account with the email address asserted by the credential. - * @error auth/invalid-credential Thrown if the credential is malformed or has expired. - * @error auth/operation-not-allowed Thrown if the type of account corresponding to the credential is not enabled. Enable the account type in the Firebase Console, under the Auth tab. - * @error auth/user-disabled Thrown if the user corresponding to the given credential has been disabled. - * @error auth/user-not-found Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and there is no user corresponding to the given email. - * @error auth/wrong-password Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and the password is invalid for the given email, or if the account corresponding to the email does not have a password set. - * @error auth/invalid-verification-code Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @param credential A generated `AuthCredential`, for example from social auth. - */ - signInWithCredential(credential: AuthCredential): Promise; - - /** - * Signs the user in with a specified provider. This is a web-compatible API along with signInWithRedirect. - * They both share the same call to the underlying native SDK signInWithProvider method. - * - * #### Example - * - * ```js - * // create a new OAuthProvider - * const provider = firebase.auth.OAuthProvider('microsoft.com'); - * // Sign the user in with the provider - * const userCredential = await firebase.auth().signInWithPopup(provider); - * ``` - * - * @error auth/account-exists-with-different-credential Thrown if there already exists an account with the email address asserted by the credential. - * @error auth/invalid-credential Thrown if the credential is malformed or has expired. - * @error auth/operation-not-allowed Thrown if the type of account corresponding to the credential is not enabled. Enable the account type in the Firebase Console, under the Auth tab. - * @error auth/user-disabled Thrown if the user corresponding to the given credential has been disabled. - * @error auth/user-not-found Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and there is no user corresponding to the given email. - * @error auth/wrong-password Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and the password is invalid for the given email, or if the account corresponding to the email does not have a password set. - * @error auth/invalid-verification-code Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @param provider An `AuthProvider` configured for your desired provider, e.g. "microsoft.com" - */ - signInWithPopup(provider: AuthProvider): Promise; - - /** - * Signs the user in with a specified provider. This is a web-compatible API along with signInWithPopup. - * They both share the same call to the underlying native SDK signInWithProvider method. - * - * #### Example - * - * ```js - * // create a new OAuthProvider - * const provider = firebase.auth.OAuthProvider('microsoft.com'); - * // Sign the user in with the provider - * const userCredential = await firebase.auth().signInWithRedirect(provider); - * ``` - * - * @error auth/account-exists-with-different-credential Thrown if there already exists an account with the email address asserted by the credential. - * @error auth/invalid-credential Thrown if the credential is malformed or has expired. - * @error auth/operation-not-allowed Thrown if the type of account corresponding to the credential is not enabled. Enable the account type in the Firebase Console, under the Auth tab. - * @error auth/user-disabled Thrown if the user corresponding to the given credential has been disabled. - * @error auth/user-not-found Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and there is no user corresponding to the given email. - * @error auth/wrong-password Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and the password is invalid for the given email, or if the account corresponding to the email does not have a password set. - * @error auth/invalid-verification-code Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @param provider An `AuthProvider` configured for your desired provider, e.g. "microsoft.com" - */ - signInWithRedirect(provider: AuthProvider): Promise; - - /** - * Revokes a user's Sign in with Apple token. - * - * #### Example - * - * ```js - * // Generate an Apple ID authorizationCode for the currently logged in user (ie, with @invertase/react-native-apple-authentication) - * const { authorizationCode } = await appleAuth.performRequest({ requestedOperation: appleAuth.Operation.REFRESH }); - * // Revoke the token - * await firebase.auth().revokeToken(authorizationCode); - * ``` - * - * @param authorizationCode A generated authorization code from Sign in with Apple. - */ - revokeToken(authorizationCode: string): Promise; - - /** - * Signs the user in with a federated OAuth provider supported by Firebase (Microsoft, Yahoo). - * - * From Firebase Docs: - * Unlike other OAuth providers supported by Firebase such as Google, Facebook, and Twitter, where - * sign-in can directly be achieved with OAuth access token based credentials, Firebase Auth does not - * support the same capability for providers such as Microsoft due to the inability of the Firebase Auth - * server to verify the audience of Microsoft OAuth access tokens. - * - * #### Example - * ```js - * // Generate an OAuth instance - * const provider = new firebase.auth.OAuthProvider('microsoft.com'); - * // Optionally add scopes to the OAuth instance - * provider.addScope('mail.read'); - * // Optionally add custom parameters to the OAuth instance - * provider.setCustomParameters({ - * prompt: 'consent', - * }); - * // Sign in using the OAuth provider - * const userCredential = await firebase.auth().signInWithProvider(provider); - * ``` - * - * @error auth/account-exists-with-different-credential Thrown if there already exists an account with the email address asserted by the credential. - * @error auth/invalid-credential Thrown if the credential is malformed or has expired. - * @error auth/operation-not-allowed Thrown if the type of account corresponding to the credential is not enabled. Enable the account type in the Firebase Console, under the Auth tab. - * @error auth/user-disabled Thrown if the user corresponding to the given credential has been disabled. - * @error auth/user-not-found Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and there is no user corresponding to the given email. - * @error auth/wrong-password Thrown if signing in with a credential from firebase.auth.EmailAuthProvider.credential and the password is invalid for the given email, or if the account corresponding to the email does not have a password set. - * @error auth/invalid-verification-code Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification code of the credential is not valid. - * @error auth/invalid-verification-id Thrown if the credential is a firebase.auth.PhoneAuthProvider.credential and the verification ID of the credential is not valid. - * @param provider A generated `AuthProvider`, for example from social auth. - */ - signInWithProvider(provider: AuthProvider): Promise; - - /** - * Sends a password reset email to the given email address. - * Unlike the web SDK, the email will contain a password reset link rather than a code. - * - * #### Example - * - * ```js - * await firebase.auth().sendPasswordResetEmail('joe.bloggs@example.com'); - * ``` - * - * @error auth/invalid-email Thrown if the email address is not valid. - * @error auth/missing-android-pkg-name An Android package name must be provided if the Android app is required to be installed. - * @error auth/missing-continue-uri A continue URL must be provided in the request. - * @error auth/missing-ios-bundle-id An iOS Bundle ID must be provided if an App Store ID is provided. - * @error auth/invalid-continue-uri The continue URL provided in the request is invalid. - * @error auth/unauthorized-continue-uri The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase console. - * @error auth/user-not-found Thrown if there is no user corresponding to the email address. - * @param email The users email address. - * @param actionCodeSettings Additional settings to be set before sending the reset email. - */ - sendPasswordResetEmail(email: string, actionCodeSettings?: ActionCodeSettings): Promise; - - /** - * Sends a sign in link to the user. - * - * #### Example - * - * ```js - * await firebase.auth().sendSignInLinkToEmail('joe.bloggs@example.com'); - * ``` - * - * @error auth/argument-error Thrown if handleCodeInApp is false. - * @error auth/invalid-email Thrown if the email address is not valid. - * @error auth/missing-android-pkg-name An Android package name must be provided if the Android app is required to be installed. - * @error auth/missing-continue-uri A continue URL must be provided in the request. - * @error auth/missing-ios-bundle-id An iOS Bundle ID must be provided if an App Store ID is provided. - * @error auth/invalid-continue-uri The continue URL provided in the request is invalid. - * @error auth/unauthorized-continue-uri The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase console. - * @param email The users email address. - * @param actionCodeSettings The action code settings. The action code settings which provides Firebase with instructions on how to construct the email link. This includes the sign in completion URL or the deep link for mobile redirects, the mobile apps to use when the sign-in link is opened on an Android or iOS device. Mobile app redirects will only be applicable if the developer configures and accepts the Firebase Dynamic Links terms of condition. The Android package name and iOS bundle ID will be respected only if they are configured in the same Firebase Auth project used. - */ - sendSignInLinkToEmail(email: string, actionCodeSettings?: ActionCodeSettings): Promise; - - /** - * Checks if an incoming link is a sign-in with email link suitable for signInWithEmailLink. - * Note that android and other platforms require `apiKey` link parameter for signInWithEmailLink - * - * #### Example - * - * ```js - * const valid = await firebase.auth().isSignInWithEmailLink(link); - * ``` - * - * @param emailLink The email link to verify prior to using signInWithEmailLink - */ - isSignInWithEmailLink(emailLink: string): Promise; - - /** - * Signs the user in with an email link. - * - * #### Example - * - * ```js - * const userCredential = await firebase.auth().signInWithEmailLink('joe.bloggs@example.com', link); - * ``` - * - * @error auth/expired-action-code Thrown if OTP in email link expires. - * @error auth/invalid-email Thrown if the email address is not valid. - * @error auth/user-disabled Thrown if the user corresponding to the given email has been disabled. - * @param email The users email to sign in with. - * @param emailLink An email link. - */ - signInWithEmailLink(email: string, emailLink: string): Promise; - - /** - * Completes the password reset process with the confirmation code and new password, via - * {@link sendPasswordResetEmail}. - * - * #### Example - * - * ```js - * await firebase.auth().confirmPasswordReset('ABCD', '1234567'); - * ``` - * - * @error auth/expired-action-code Thrown if the password reset code has expired. - * @error auth/invalid-action-code Thrown if the password reset code is invalid. This can happen if the code is malformed or has already been used. - * @error auth/user-disabled Thrown if the user corresponding to the given password reset code has been disabled. - * @error auth/user-not-found Thrown if there is no user corresponding to the password reset code. This may have happened if the user was deleted between when the code was issued and when this method was called. - * @error auth/weak-password Thrown if the new password is not strong enough. - * @param code The code from the password reset email. - * @param newPassword The new password. - */ - confirmPasswordReset(code: string, newPassword: string): Promise; - - /** - * Applies a verification code sent to the user by email or other out-of-band mechanism. - * - * #### Example - * - * ```js - * await firebase.auth().applyActionCode('ABCD'); - * ``` - * - * @error auth/expired-action-code Thrown if the action code has expired. - * @error auth/invalid-action-code Thrown if the action code is invalid. This can happen if the code is malformed or has already been used. - * @error auth/user-disabled Thrown if the user corresponding to the given action code has been disabled. - * @error auth/user-not-found Thrown if there is no user corresponding to the action code. This may have happened if the user was deleted between when the action code was issued and when this method was called. - * @param code A verification code sent to the user. - */ - applyActionCode(code: string): Promise; - - /** - * Checks a verification code sent to the user by email or other out-of-band mechanism. - * - * #### Example - * - * ```js - * const actionCodeInfo = await firebase.auth().checkActionCode('ABCD'); - * console.log('Action code operation: ', actionCodeInfo.operation); - * ``` - * - * @error auth/expired-action-code Thrown if the action code has expired. - * @error auth/invalid-action-code Thrown if the action code is invalid. This can happen if the code is malformed or has already been used. - * @error auth/user-disabled Thrown if the user corresponding to the given action code has been disabled. - * @error auth/user-not-found Thrown if there is no user corresponding to the action code. This may have happened if the user was deleted between when the action code was issued and when this method was called. - * @param code A verification code sent to the user. - */ - checkActionCode(code: string): Promise; - - /** - * Returns a list of authentication methods that can be used to sign in a given user (identified by its main email address). - * - * ⚠️ Note: - * If "Email Enumeration Protection" is enabled in your Firebase Authentication settings (which is the default), - * this method may return an empty array even if the email is registered, especially when called from an unauthenticated context. - * - * This is a security measure to prevent leaking account existence via email enumeration attacks. - * Do not use the result of this method to directly inform the user whether an email is registered. - * - * #### Example - * - * ```js - * const methods = await firebase.auth().fetchSignInMethodsForEmail('joe.bloggs@example.com'); - * - * if (methods.length > 0) { - * // Likely a registered user — offer sign-in - * } else { - * // Could be unregistered OR email enumeration protection is active — offer registration - * } - * ``` - * - * @error auth/invalid-email Thrown if the email address is not valid. - * @param email The user's email address. - */ - fetchSignInMethodsForEmail(email: string): Promise; - - /** - * Checks a password reset code sent to the user by email or other out-of-band mechanism. - * TODO salakar: confirm return behavior (Returns the user's email address if valid.) - * - * #### Example - * - * ```js - * const verifiedEmail = await firebase.auth().verifyPasswordResetCode('ABCD'); - * ``` - * - * @error auth/expired-action-code Thrown if the password reset code has expired. - * @error auth/invalid-action-code Thrown if the password reset code is invalid. This can happen if the code is malformed or has already been used. - * @error auth/user-disabled Thrown if the user corresponding to the given password reset code has been disabled. - * @error auth/user-not-found Thrown if there is no user corresponding to the password reset code. This may have happened if the user was deleted between when the code was issued and when this method was called. - * @param code A password reset code. - * @returns {string} The user's email address if valid - */ - verifyPasswordResetCode(code: string): Promise; - /** - * Switch userAccessGroup and current user to the given accessGroup and the user stored in it. - * Sign in a user with any sign in method, and the same current user is available in all - * apps in the access group. - * - * Set the `useAccessGroup` argument to `null` to stop sharing the auth state (default behaviour), the user state will no longer be - * available to any other apps. - * - * @platform ios - * - * @error auth/keychain-error Thrown if you attempt to access an inaccessible keychain - * @param userAccessGroup A string of the keychain id i.e. "TEAMID.com.example.group1" - */ - useUserAccessGroup(userAccessGroup: string): Promise; - /** - * Modify this Auth instance to communicate with the Firebase Auth emulator. - * This must be called synchronously immediately following the first call to firebase.auth(). - * Do not use with production credentials as emulator traffic is not encrypted. - * - * Note: on android, hosts 'localhost' and '127.0.0.1' are automatically remapped to '10.0.2.2' (the - * "host" computer IP address for android emulators) to make the standard development experience easy. - * If you want to use the emulator on a real android device, you will need to specify the actual host - * computer IP address. - * - * @param url emulator URL, must have host and port (eg, 'http://localhost:9099') - */ - useEmulator(url: string): void; - /** - * Provides a MultiFactorResolver suitable for completion of a multi-factor flow. - * - * @param error The MultiFactorError raised during a sign-in, or reauthentication operation. - */ - getMultiFactorResolver(error: MultiFactorError): MultiFactorResolver; - /** - * The MultiFactorUser corresponding to the user. - * - * This is used to access all multi-factor properties and operations related to the user. - * @param user The user. - */ - multiFactor(user: User): MultiFactorUser; - /** - * Returns the custom auth domain for the auth instance. - */ - getCustomAuthDomain(): Promise; - /** - * Sets the language code on the auth instance. This is to match Firebase JS SDK behavior. - * Please use the `setLanguageCode` method for setting the language code. - */ - set languageCode(code: string | null); - /** - * Gets the config used to initialize this auth instance. This is to match Firebase JS SDK behavior. - * It returns an empty map as the config is not available in the native SDK. - */ - get config(): Record; - } -} - -export type CallbackOrObserver any> = T | { next: T }; - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - interface Module { - auth: FirebaseModuleWithStaticsAndApp; - } - interface FirebaseApp { - auth(): FirebaseAuthTypes.Module; - } - } -} diff --git a/packages/auth/type-test.ts b/packages/auth/type-test.ts index c3512663e1..e51e1ee020 100644 --- a/packages/auth/type-test.ts +++ b/packages/auth/type-test.ts @@ -1,11 +1,9 @@ /* - * Consumer-facing API type tests for @react-native-firebase/auth. - * Part 1: namespaced API (firebase.auth(), default auth()). - * Part 2: modular API (getAuth, signInWithEmailAndPassword, etc. from lib/modular.ts). + * Consumer-facing API type tests for @react-native-firebase/auth (modular API only). */ -import auth, { - firebase, +import { getApp } from '@react-native-firebase/app'; +import { applyActionCode, ActionCodeOperation, beforeAuthStateChanged, @@ -32,7 +30,6 @@ import auth, { onAuthStateChanged, onIdTokenChanged, OperationType, - ProviderId, OAuthProvider, ActionCodeURL, parseActionCodeURL, @@ -57,6 +54,7 @@ import auth, { useUserAccessGroup, validatePassword, verifyPasswordResetCode, + SDK_VERSION, type ActionCodeInfo, type ActionCodeSettings, type ApplicationVerifier, @@ -65,11 +63,8 @@ import auth, { type AuthProvider, type AuthSettings, type Config, - type ConfirmationResult, type Dependencies, - type FirebaseAuthTypes, type IdTokenResult, - type MultiFactorError, type MultiFactorSession, type OAuthCredentialOptions, type PasswordPolicy, @@ -78,7 +73,6 @@ import auth, { type PopupRedirectResolver, type TotpMultiFactorAssertion, type TotpSecret, - type Unsubscribe, type User, type UserCredential, deleteUser, @@ -102,19 +96,7 @@ import auth, { PhoneAuthCredential, } from '.'; -const authModule = auth(); -const namespacedAuth = firebase.auth(); -const authFromApp = firebase.app().auth(); -const authFromAppArg = auth(firebase.app()); - -console.log(authModule.app.name); -console.log(namespacedAuth.currentUser); -console.log(authFromApp.app.name); -console.log(authFromAppArg.app.name); -console.log(firebase.auth.SDK_VERSION); -console.log(auth.firebase.SDK_VERSION); -console.log(firebase.SDK_VERSION); - +console.log(SDK_VERSION); const actionCodeSettings: ActionCodeSettings = { url: 'https://example.com/auth', handleCodeInApp: true, @@ -234,8 +216,8 @@ console.log( ); const modularAuth: Auth = getAuth(); -const modularAuthFromApp: Auth = getAuth(firebase.app()); -const initializedAuth: Auth = initializeAuth(firebase.app(), dependencies); +const modularAuthFromApp: Auth = getAuth(getApp()); +const initializedAuth: Auth = initializeAuth(getApp(), dependencies); const phoneVerificationId: Promise = new PhoneAuthProvider(modularAuth).verifyPhoneNumber( '+16505550101', appVerifier, @@ -252,48 +234,6 @@ console.log(modularAuth.app.name, modularAuthFromApp.app.name, initializedAuth.a console.log(phoneVerificationId); console.log(totpAssertionForSignIn, totpAssertionForEnrollment, generatedTotpSecret); -namespacedAuth.setTenantId('tenant-123'); -namespacedAuth.setLanguageCode('en'); -namespacedAuth.useEmulator('http://localhost:9099'); -namespacedAuth.sendPasswordResetEmail('test@example.com'); -namespacedAuth.sendSignInLinkToEmail('test@example.com', actionCodeSettings); -namespacedAuth.verifyPasswordResetCode('oob-code').then((email: string) => console.log(email)); -namespacedAuth - .checkActionCode('oob-code') - .then((info: FirebaseAuthTypes.ActionCodeInfo) => console.log(info.operation)); -namespacedAuth.getCustomAuthDomain().then((domain: string) => console.log(domain)); - -const namespacedUnsubscribe: Unsubscribe = namespacedAuth.onAuthStateChanged( - (user: FirebaseAuthTypes.User | null) => { - console.log(user?.uid); - }, -); -namespacedUnsubscribe(); - -namespacedAuth.onIdTokenChanged((user: FirebaseAuthTypes.User | null) => { - console.log(user?.email); -}); - -namespacedAuth - .signInAnonymously() - .then((credential: FirebaseAuthTypes.UserCredential) => console.log(credential.user.uid)); -namespacedAuth - .createUserWithEmailAndPassword('new@example.com', 'password123') - .then((credential: FirebaseAuthTypes.UserCredential) => console.log(credential.user.email)); -namespacedAuth - .signInWithEmailAndPassword('test@example.com', 'password123') - .then((credential: FirebaseAuthTypes.UserCredential) => console.log(credential.user.email)); -namespacedAuth - .signInWithCustomToken('custom-token') - .then((credential: FirebaseAuthTypes.UserCredential) => console.log(credential.user.uid)); -namespacedAuth - .signInWithEmailLink('test@example.com', 'email-link') - .then((credential: FirebaseAuthTypes.UserCredential) => console.log(credential.user.email)); -namespacedAuth - .signInWithPhoneNumber('+1234567890') - .then((result: FirebaseAuthTypes.ConfirmationResult) => console.log(result.verificationId)); -namespacedAuth.signOut(); - const emailCredential = EmailAuthProvider.credential('test@example.com', 'password123'); const phoneCredential = PhoneAuthProvider.credential('verification-id', '123456'); console.log(emailCredential.providerId, phoneCredential.providerId); @@ -302,126 +242,66 @@ applyActionCode(modularAuth, 'oob-code'); checkActionCode(modularAuth, 'oob-code').then((info: ActionCodeInfo) => console.log(info.data.email), ); -confirmPasswordReset(modularAuth, 'oob-code', 'new-password'); connectAuthEmulator(modularAuth, 'http://localhost:9099', { disableWarnings: false }); -createUserWithEmailAndPassword(modularAuth, 'new@example.com', 'password123').then( - (credential: UserCredential) => console.log(credential.user.uid), -); -fetchSignInMethodsForEmail(modularAuth, 'test@example.com').then((methods: string[]) => - console.log(methods), -); -getMultiFactorResolver(modularAuth, {} as MultiFactorError); -getRedirectResult(modularAuth, popupRedirectResolver).then(result => console.log(result?.user.uid)); -isSignInWithEmailLink(modularAuth, 'email-link').then((valid: boolean) => console.log(valid)); - -const modularUnsubscribe: Unsubscribe = onAuthStateChanged(modularAuth, (user: User | null) => - console.log(user?.email), -); -modularUnsubscribe(); - -onIdTokenChanged(modularAuth, (user: User | null) => console.log(user?.uid)); -signInAnonymously(modularAuth).then((credential: UserCredential) => - console.log(credential.user.uid), -); -signInWithCredential(modularAuth, emailCredential).then((credential: UserCredential) => - console.log(credential.user.email), -); -signInWithCustomToken(modularAuth, 'custom-token').then((credential: UserCredential) => - console.log(credential.user.uid), -); -signInWithEmailAndPassword(modularAuth, 'test@example.com', 'password123').then( - (credential: UserCredential) => console.log(credential.user.email), -); -signInWithEmailLink(modularAuth, 'test@example.com', 'email-link').then( - (credential: UserCredential) => console.log(credential.user.email), -); -signInWithEmailLink(modularAuth, 'test@example.com').then((credential: UserCredential) => - console.log(credential.user.email), -); -signInWithPhoneNumber(modularAuth, '+1234567890', appVerifier).then((result: ConfirmationResult) => - console.log(result.verificationId), -); -signInWithPopup(modularAuth, redirectProvider, popupRedirectResolver).then(result => - console.log(result.user.uid), -); -signInWithRedirect(modularAuth, redirectProvider, popupRedirectResolver).then(result => - console.log(result.user.uid), -); signOut(modularAuth); -sendPasswordResetEmail(modularAuth, 'test@example.com', actionCodeSettings); sendSignInLinkToEmail(modularAuth, 'test@example.com', actionCodeSettings); setLanguageCode(modularAuth, 'fr'); -useUserAccessGroup(modularAuth, 'group.example'); -verifyPasswordResetCode(modularAuth, 'oob-code').then((email: string) => console.log(email)); -getCustomAuthDomain(modularAuth).then((domain: string) => console.log(domain)); -revokeToken(modularAuth, 'authorization-code'); -validatePassword(modularAuth, 'password123').then((status: PasswordValidationStatus) => - console.log(status.isValid), -); - -beforeAuthStateChanged(modularAuth, async (user: User | null) => { - console.log(user?.uid); -}); -updateCurrentUser(modularAuth, null); -useDeviceLanguage(modularAuth); - -void ActionCodeURL.parseLink('https://example.com/auth?mode=verifyEmail&oobCode=abc'); - -const parsedActionCode = parseActionCodeURL( - 'https://example.com/auth?mode=verifyEmail&oobCode=abc', -); -if (parsedActionCode) { - console.log(parsedActionCode.code); -} - -const additionalUserInfo = getAdditionalUserInfo({ - additionalUserInfo: null, - user: {} as User, -} as unknown as UserCredential); -console.log(additionalUserInfo); - -const maybeUser = namespacedAuth.currentUser; -if (maybeUser) { - maybeUser.reload(); - maybeUser.getIdToken().then((token: string) => console.log(token)); - maybeUser - .getIdTokenResult() - .then((result: FirebaseAuthTypes.IdTokenResult) => console.log(result.claims)); - maybeUser.sendEmailVerification(actionCodeSettings); - maybeUser.updateEmail('new@example.com'); - maybeUser.updatePassword('new-password'); - maybeUser.updatePhoneNumber(phoneCredential); - maybeUser.updateProfile({ displayName: 'New Name', photoURL: 'https://example.com/photo.png' }); - - const namespacedMfaUser = namespacedAuth.multiFactor(maybeUser); - namespacedMfaUser.getSession(); -} - -const modularUser = {} as User; -const modularMfaUser = multiFactor(modularUser); -modularMfaUser.getSession(); -getIdTokenResult(modularUser).then((result: IdTokenResult) => console.log(result.claims)); -modularAuth.authStateReady().then(() => console.log('auth ready')); modularAuth.tenantId = 'tenant-id'; console.log(modularAuth.emulatorConfig?.host); console.log(modularAuth.config); + +const modularUser = {} as User; +multiFactor(modularUser).getSession(); +getIdTokenResult(modularUser).then((result: IdTokenResult) => console.log(result.claims)); deleteUser(modularUser); -getIdToken(modularUser); -linkWithCredential(modularUser, emailCredential); -linkWithPopup(modularUser, redirectProvider, popupRedirectResolver); -linkWithRedirect(modularUser, redirectProvider, popupRedirectResolver); -reauthenticateWithCredential(modularUser, emailCredential); -reauthenticateWithPopup(modularUser, redirectProvider, popupRedirectResolver); -reauthenticateWithRedirect(modularUser, redirectProvider, popupRedirectResolver); -reload(modularUser); sendEmailVerification(modularUser, actionCodeSettings); -unlink(modularUser, ProviderId.GOOGLE); -updateEmail(modularUser, 'new@example.com'); -updatePassword(modularUser, 'new-password'); -updatePhoneNumber(modularUser, phoneCredential); -updateProfile(modularUser, { displayName: 'Name', photoURL: 'https://example.com/photo.png' }); verifyBeforeUpdateEmail(modularUser, 'new@example.com', actionCodeSettings); -namespacedAuth.sendSignInLinkToEmail('test@example.com'); EmailAuthCredential.fromJSON({ email: 'a@b.com', password: 'pw', signInMethod: 'password' }); OAuthCredential.fromJSON({ providerId: 'google.com', idToken: 'token' }); PhoneAuthCredential.fromJSON({ verificationId: 'vid', verificationCode: '123456' }); + +void [ + beforeAuthStateChanged, + confirmPasswordReset, + createUserWithEmailAndPassword, + fetchSignInMethodsForEmail, + getAdditionalUserInfo, + getCustomAuthDomain, + getMultiFactorResolver, + getRedirectResult, + revokeToken, + isSignInWithEmailLink, + onAuthStateChanged, + onIdTokenChanged, + parseActionCodeURL, + sendPasswordResetEmail, + signInAnonymously, + signInWithCredential, + signInWithCustomToken, + signInWithEmailAndPassword, + signInWithEmailLink, + signInWithPhoneNumber, + signInWithPopup, + signInWithRedirect, + updateCurrentUser, + useDeviceLanguage, + useUserAccessGroup, + validatePassword, + verifyPasswordResetCode, + getIdToken, + linkWithCredential, + linkWithPopup, + linkWithRedirect, + reauthenticateWithCredential, + reauthenticateWithPopup, + reauthenticateWithRedirect, + reload, + unlink, + updateEmail, + updatePassword, + updatePhoneNumber, + updateProfile, + ActionCodeURL, + redirectProvider, + popupRedirectResolver, +]; diff --git a/packages/auth/typedoc.json b/packages/auth/typedoc.json index c306f5e13c..760cbe4535 100644 --- a/packages/auth/typedoc.json +++ b/packages/auth/typedoc.json @@ -4,7 +4,6 @@ "tsconfig": "tsconfig.json", "excludeInternal": true, "intentionallyNotExported": [ - "AuthNamespace", "Observer", "AuthInternal", "OAuthCredentialParams" diff --git a/packages/crashlytics/__tests__/crashlytics.test.ts b/packages/crashlytics/__tests__/crashlytics.test.ts index 0f23492890..b86222bac7 100644 --- a/packages/crashlytics/__tests__/crashlytics.test.ts +++ b/packages/crashlytics/__tests__/crashlytics.test.ts @@ -1,12 +1,6 @@ -import { afterAll, beforeAll, describe, expect, it, jest, beforeEach } from '@jest/globals'; -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; +import { describe, expect, it } from '@jest/globals'; + import { - firebase, getCrashlytics, checkForUnsentReports, deleteUnsentReports, @@ -22,24 +16,6 @@ import { } from '../lib'; describe('Crashlytics', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.crashlytics).toBeDefined(); - expect(app.crashlytics().app).toEqual(app); - }); - }); - describe('modular', function () { it('`getCrashlytics` function is properly exposed to end user', function () { expect(getCrashlytics).toBeDefined(); @@ -89,121 +65,4 @@ describe('Crashlytics', function () { expect(setCrashlyticsCollectionEnabled).toBeDefined(); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let checkV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - checkV9Deprecation = createCheckV9Deprecation(['crashlytics']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => jest.fn(), - }, - ); - }); - }); - - it('checkForUnsentReports', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => checkForUnsentReports(crashlytics), - () => crashlytics.checkForUnsentReports(), - 'checkForUnsentReports', - ); - }); - - it('crash', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => crash(crashlytics), - () => crashlytics.crash(), - 'crash', - ); - }); - - it('deleteUnsentReports', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => deleteUnsentReports(crashlytics), - () => crashlytics.deleteUnsentReports(), - 'deleteUnsentReports', - ); - }); - - it('didCrashOnPreviousExecution', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => didCrashOnPreviousExecution(crashlytics), - () => crashlytics.didCrashOnPreviousExecution(), - 'didCrashOnPreviousExecution', - ); - }); - - it('log', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => log(crashlytics, 'message'), - () => crashlytics.log('message'), - 'log', - ); - }); - - it('setAttribute', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => setAttribute(crashlytics, 'name', 'value'), - () => crashlytics.setAttribute('name', 'value'), - 'setAttribute', - ); - }); - - it('setAttributes', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => setAttributes(crashlytics, {}), - () => crashlytics.setAttributes({}), - 'setAttributes', - ); - }); - - it('setUserId', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => setUserId(crashlytics, 'id'), - () => crashlytics.setUserId('id'), - 'setUserId', - ); - }); - - it('recordError', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => recordError(crashlytics, new Error(), 'name'), - () => crashlytics.recordError(new Error(), 'name'), - 'recordError', - ); - }); - - it('sendUnsentReports', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => sendUnsentReports(crashlytics), - () => crashlytics.sendUnsentReports(), - 'sendUnsentReports', - ); - }); - - it('setCrashlyticsCollectionEnabled', function () { - const crashlytics = getCrashlytics(); - checkV9Deprecation( - () => setCrashlyticsCollectionEnabled(crashlytics, true), - () => crashlytics.setCrashlyticsCollectionEnabled(true), - 'setCrashlyticsCollectionEnabled', - ); - }); - }); }); diff --git a/packages/crashlytics/e2e/crashlytics.e2e.js b/packages/crashlytics/e2e/crashlytics.e2e.js index 9160dfc2fc..69369ac3eb 100644 --- a/packages/crashlytics/e2e/crashlytics.e2e.js +++ b/packages/crashlytics/e2e/crashlytics.e2e.js @@ -16,180 +16,6 @@ */ describe('crashlytics()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('log()', function () { - it('accepts any value', async function () { - firebase.crashlytics().log('invertase'); - firebase.crashlytics().log(1337); - firebase.crashlytics().log(null); - firebase.crashlytics().log(true); - firebase.crashlytics().log({}); - firebase.crashlytics().log([]); - firebase.crashlytics().log(() => {}); - }); - }); - - describe('setUserId()', function () { - it('accepts string values', async function () { - await firebase.crashlytics().setUserId('invertase'); - }); - - it('rejects none string values', async function () { - try { - await firebase.crashlytics().setUserId(666.1337); - return Promise.reject(new Error('Did not throw.')); - } catch (e) { - e.message.should.containEql('must be a string'); - } - }); - }); - - describe('setAttribute()', function () { - it('accepts string values', async function () { - await firebase.crashlytics().setAttribute('invertase', '1337'); - }); - - it('rejects none string values', async function () { - try { - await firebase.crashlytics().setAttribute('invertase', 33.3333); - return Promise.reject(new Error('Did not throw.')); - } catch (e) { - e.message.should.containEql('must be a string'); - } - }); - - it('errors if attribute name is not a string', async function () { - try { - await firebase.crashlytics().setAttribute(1337, 'invertase'); - return Promise.reject(new Error('Did not throw.')); - } catch (e) { - e.message.should.containEql('must be a string'); - } - }); - }); - - describe('setAttributes()', function () { - it('errors if arg is not an object', async function () { - try { - await firebase.crashlytics().setAttributes(1337); - return Promise.reject(new Error('Did not throw.')); - } catch (e) { - e.message.should.containEql('must be an object'); - } - }); - - it('accepts string values', async function () { - await firebase.crashlytics().setAttributes({ invertase: '1337' }); - }); - }); - - describe('recordError()', function () { - it('warns if not an error', async function () { - // eslint-disable-next-line no-console - const orig = console.warn; - let logged = false; - // eslint-disable-next-line no-console - console.warn = msg => { - // we console.warn for deprecated API, can be removed when we move to v9 - if (!msg.includes('method is deprecated')) { - msg.should.containEql('expects an instance of Error'); - logged = true; - // eslint-disable-next-line no-console - console.warn = orig; - } - }; - firebase.crashlytics().recordError(1337); - should.equal(logged, true); - }); - - it('accepts Error values', async function () { - firebase.crashlytics().recordError(new Error("I'm a teapot!")); - // TODO verify stack obj - }); - - it('accepts optional jsErrorName', async function () { - firebase - .crashlytics() - .recordError( - new Error("I'm a teapot!"), - 'This message will display in crashlytics dashboard', - ); - // TODO verify stack obj - }); - }); - - describe('sendUnsentReports()', function () { - it("sends unsent reports to the crashlytic's server", function () { - firebase.crashlytics().sendUnsentReports(); - }); - }); - - describe('checkForUnsentReports()', function () { - it('errors if automatic crash report collection is enabled', async function () { - await firebase.crashlytics().setCrashlyticsCollectionEnabled(true); - try { - await firebase.crashlytics().checkForUnsentReports(); - return Promise.reject('Error did not throw'); - } catch (e) { - e.message.should.containEql("has been set to 'true', all reports are automatically sent"); - } - }); - - it("checks device cache for unsent crashlytic's reports", async function () { - await firebase.crashlytics().setCrashlyticsCollectionEnabled(false); - const anyUnsentReports = await firebase.crashlytics().checkForUnsentReports(); - - should(anyUnsentReports).equal(false); - }); - }); - - describe('deleteUnsentReports()', function () { - it('deletes unsent crashlytics reports', async function () { - await firebase.crashlytics().deleteUnsentReports(); - }); - }); - - describe('didCrashOnPreviousExecution()', function () { - // TODO: worked on Detox v17, fails after transition to v18. Why? - xit('checks if app crached on previous execution', async function () { - const didCrash = await firebase.crashlytics().didCrashOnPreviousExecution(); - - should(didCrash).equal(false); - }); - }); - - describe('setCrashlyticsCollectionEnabled()', function () { - it('false', async function () { - await firebase.crashlytics().setCrashlyticsCollectionEnabled(false); - should.equal(firebase.crashlytics().isCrashlyticsCollectionEnabled, false); - }); - - it('true', async function () { - await firebase.crashlytics().setCrashlyticsCollectionEnabled(true); - should.equal(firebase.crashlytics().isCrashlyticsCollectionEnabled, true); - }); - - it('errors if not boolean', async function () { - try { - await firebase.crashlytics().setCrashlyticsCollectionEnabled(1337); - return Promise.reject(new Error('Did not throw.')); - } catch (e) { - e.message.should.containEql('must be a boolean'); - } - }); - }); - }); - describe('modular', function () { describe('log()', function () { it('accepts any value', async function () { diff --git a/packages/crashlytics/lib/handlers.ts b/packages/crashlytics/lib/handlers.ts index 28bd23ca9a..e79e6a521c 100644 --- a/packages/crashlytics/lib/handlers.ts +++ b/packages/crashlytics/lib/handlers.ts @@ -15,7 +15,7 @@ * */ -import { firebase } from '.'; +import { getApp } from '@react-native-firebase/app'; import { isError, once } from '@react-native-firebase/app/dist/module/common'; // @ts-ignore - No declaration file for promise/setimmediate/rejection-tracking import tracking from 'promise/setimmediate/rejection-tracking'; @@ -125,35 +125,16 @@ export const setGlobalErrorHandler = once((nativeModule: NativeModule) => { // Flag the Crashlytics backend that we have a fatal error, they will transform it await nativeModule.setAttribute(FATAL_FLAG, fatalTime); - // remember our current deprecation warning state in case users - // have set it to non-default - const currentDeprecationWarningToggle = - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS; - - // Notify analytics, if it exists - throws error if not try { - // FIXME - disable warnings and use the old namespaced style, - // See https://github.com/invertase/react-native-firebase/issues/8381 - // Unfortunately, this fails completely when using modular! - // Did not matter if I did named imports above or dynamic require here. - // So temporarily reverting and silencing warnings instead - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - // @ts-ignore - analytics types not available in crashlytics - await firebase.app().analytics().logEvent( - 'app_exception', // 'app_exception' is reserved but we make an exception for JS->fatal transforms - { - fatal: 1, // as in firebase-android-sdk - timestamp: fatalTime, - }, - ); + const { getAnalytics, logEvent } = require('@react-native-firebase/analytics'); + await logEvent(getAnalytics(getApp()), 'app_exception', { + fatal: 1, + timestamp: fatalTime, + }); } catch (_) { - // This just means analytics was not present, so we could not log the analytics event - // console.log('error logging analytics app_exception: ' + e); - } finally { - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = currentDeprecationWarningToggle; + // analytics not present } - // If we are chaining to other handlers, just record the error, otherwise we need to crash with it if (nativeModule.isCrashlyticsJavascriptExceptionHandlerChainingEnabled) { await nativeModule.recordErrorPromise(createNativeErrorObj(error, stackFrames, false)); } else { diff --git a/packages/crashlytics/lib/index.ts b/packages/crashlytics/lib/index.ts index 3e07dbac9b..7b1e612499 100644 --- a/packages/crashlytics/lib/index.ts +++ b/packages/crashlytics/lib/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* * Copyright (c) 2016-present Invertase Limited & Contributors * @@ -15,12 +16,208 @@ * */ -// Export types from types/crashlytics -export type { Crashlytics, FirebaseCrashlyticsTypes } from './types/crashlytics'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { + isBoolean, + isError, + isObject, + isString, +} from '@react-native-firebase/app/dist/module/common'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import './types/internal'; +import StackTrace from 'stacktrace-js'; +import { + createNativeErrorObj, + setGlobalErrorHandler, + setOnUnhandledPromiseRejectionHandler, +} from './handlers'; +import { version } from './version'; +import type { Crashlytics } from './types/crashlytics'; +import type { CrashlyticsInternal } from './types/internal'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; -// Export modular API functions -export * from './modular'; +const nativeModuleName = 'RNFBCrashlyticsModule'; -// Export namespaced API -export * from './namespaced'; -export { default } from './namespaced'; +class FirebaseCrashlyticsModule extends FirebaseModule { + _isCrashlyticsCollectionEnabled: boolean; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); + setGlobalErrorHandler(this.native); + setOnUnhandledPromiseRejectionHandler(this.native); + this._isCrashlyticsCollectionEnabled = this.native.isCrashlyticsCollectionEnabled; + } + + get isCrashlyticsCollectionEnabled(): boolean { + // Purposefully did not deprecate this as I think it should remain a property rather than a method. + return this._isCrashlyticsCollectionEnabled; + } + + checkForUnsentReports(): Promise { + if (this.isCrashlyticsCollectionEnabled) { + throw new Error( + "firebase.crashlytics().setCrashlyticsCollectionEnabled(*) has been set to 'true', all reports are automatically sent.", + ); + } + return this.native.checkForUnsentReports(); + } + + crash(): void { + this.native.crash(); + } + + async deleteUnsentReports(): Promise { + await this.native.deleteUnsentReports(); + } + + didCrashOnPreviousExecution(): Promise { + return this.native.didCrashOnPreviousExecution(); + } + + log(message: string): void { + this.native.log(`${message}`); + } + + setAttribute(name: string, value: string): Promise { + if (!isString(name)) { + throw new Error( + 'firebase.crashlytics().setAttribute(*, _): The supplied property name must be a string.', + ); + } + + if (!isString(value)) { + throw new Error( + 'firebase.crashlytics().setAttribute(_, *): The supplied property value must be a string value.', + ); + } + + return this.native.setAttribute(name, value); + } + + setAttributes(object: { [key: string]: string }): Promise { + if (!isObject(object)) { + throw new Error( + 'firebase.crashlytics().setAttributes(*): The supplied arg must be an object of key value strings.', + ); + } + + return this.native.setAttributes(object); + } + + setUserId(userId: string): Promise { + if (!isString(userId)) { + throw new Error( + 'firebase.crashlytics().setUserId(*): The supplied userId must be a string value.', + ); + } + + return this.native.setUserId(userId); + } + + recordError(error: Error, jsErrorName?: string): void { + if (isError(error)) { + StackTrace.fromError(error, { offline: true }).then(stackFrames => { + this.native.recordError(createNativeErrorObj(error, stackFrames, false, jsErrorName)); + }); + } else { + console.warn( + 'firebase.crashlytics().recordError(*) expects an instance of Error. Non Errors will be ignored.', + ); + } + } + + sendUnsentReports(): void { + if (this.isCrashlyticsCollectionEnabled) { + this.native.sendUnsentReports(); + } + } + + setCrashlyticsCollectionEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "firebase.crashlytics().setCrashlyticsCollectionEnabled(*) 'enabled' must be a boolean.", + ); + } + + this._isCrashlyticsCollectionEnabled = enabled; + return this.native.setCrashlyticsCollectionEnabled(enabled); + } +} + +const config: ModuleConfig = { + namespace: 'crashlytics', + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, +}; + +export const SDK_VERSION = version; + +export function getCrashlytics(app?: FirebaseApp): Crashlytics { + return getOrCreateModularInstance( + FirebaseCrashlyticsModule, + config, + app, + ) as unknown as Crashlytics; +} + +export function checkForUnsentReports(crashlytics: Crashlytics): Promise { + return (crashlytics as CrashlyticsInternal).checkForUnsentReports(); +} + +export function deleteUnsentReports(crashlytics: Crashlytics): Promise { + return (crashlytics as CrashlyticsInternal).deleteUnsentReports(); +} + +export function didCrashOnPreviousExecution(crashlytics: Crashlytics): Promise { + return (crashlytics as CrashlyticsInternal).didCrashOnPreviousExecution(); +} + +export function crash(crashlytics: Crashlytics): void { + return (crashlytics as CrashlyticsInternal).crash(); +} + +export function log(crashlytics: Crashlytics, message: string): void { + return (crashlytics as CrashlyticsInternal).log(message); +} + +export function recordError(crashlytics: Crashlytics, error: Error, jsErrorName?: string): void { + return (crashlytics as CrashlyticsInternal).recordError(error, jsErrorName); +} + +export function sendUnsentReports(crashlytics: Crashlytics): void { + return (crashlytics as CrashlyticsInternal).sendUnsentReports(); +} + +export function setUserId(crashlytics: Crashlytics, userId: string): Promise { + return (crashlytics as CrashlyticsInternal).setUserId(userId); +} + +export function setAttribute(crashlytics: Crashlytics, name: string, value: string): Promise { + return (crashlytics as CrashlyticsInternal).setAttribute(name, value); +} + +export function setAttributes( + crashlytics: Crashlytics, + attributes: { [key: string]: string }, +): Promise { + return (crashlytics as CrashlyticsInternal).setAttributes(attributes); +} + +export function setCrashlyticsCollectionEnabled( + crashlytics: Crashlytics, + enabled: boolean, +): Promise { + return (crashlytics as CrashlyticsInternal).setCrashlyticsCollectionEnabled(enabled); +} + +export type { Crashlytics } from './types/crashlytics'; diff --git a/packages/crashlytics/lib/modular.ts b/packages/crashlytics/lib/modular.ts deleted file mode 100644 index 9d6a51c8a1..0000000000 --- a/packages/crashlytics/lib/modular.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { getApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import type { Crashlytics } from './types/crashlytics'; - -/** - * Returns Crashlytics instance. - * #### Example - * ```js - * const crashlytics = getCrashlytics(); - * ``` - */ -export function getCrashlytics(): Crashlytics { - return getApp().crashlytics(); -} - -/** - * Determines whether there are any unsent crash reports cached on the device. The callback only executes - * if automatic data collection is disabled. - * - * #### Example - * - * ```js - * async checkReports() { - * // returns boolean value - * const crashlytics = getCrashlytics(); - * const unsentReports = await checkForUnsentReports(crashlytics); - * } - * - * checkReports(); - * ``` - * @param crashlytics A crashlytics instance. - * @returns Promise that resolves to a boolean indicating if there are unsent reports. - */ -export function checkForUnsentReports(crashlytics: Crashlytics): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.checkForUnsentReports.call(crashlytics, MODULAR_DEPRECATION_ARG); -} - -/** - * Deletes any unsent reports on the device. This method only applies if automatic data collection is - * disabled. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * deleteUnsentReports(crashlytics); - * ``` - * @param crashlytics A crashlytics instance. - */ -export function deleteUnsentReports(crashlytics: Crashlytics): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.deleteUnsentReports.call(crashlytics, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a boolean value indicating whether the app crashed during the previous execution. - * - * #### Example - * - * ```js - * async didCrashPreviously() { - * // returns boolean value - * const crashlytics = getCrashlytics(); - * const didCrash = await didCrashOnPreviousExecution(crashlytics); - * } - * - * didCrashPreviously(); - * ``` - * @param crashlytics A crashlytics instance. - * @returns Promise that resolves to a boolean indicating if the app crashed previously. - */ -export function didCrashOnPreviousExecution(crashlytics: Crashlytics): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.didCrashOnPreviousExecution.call(crashlytics, MODULAR_DEPRECATION_ARG); -} - -/** - * Cause your app to crash for testing purposes. This is a native crash and will not contain a javascript stack trace. - * Note that crashes are intercepted by debuggers on iOS so no report will be seen under those conditions. Additionally - * if it is a debug build you will need to ensure your firebase.json is configured to enable crashlytics even in debug mode. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * crash(crashlytics); - * ``` - * @param crashlytics A crashlytics instance. - */ -export function crash(crashlytics: Crashlytics): void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.crash.call(crashlytics, MODULAR_DEPRECATION_ARG); -} - -/** - * Log a message that will appear in any subsequent Crash or Non-fatal error reports. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * log(crashlytics, 'Testing a crash'); - * crash(crashlytics); - * ``` - * @param crashlytics A crashlytics instance. - * @param message The message to log. - */ -export function log(crashlytics: Crashlytics, message: string): void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.log.call(crashlytics, message, MODULAR_DEPRECATION_ARG); -} - -/** - * Record a JavaScript Error. - * - * The JavaScript stack trace is converted into a mock native iOS or Android exception before submission. - * The line numbers in the stack trace (if available) will be relative to the javascript bundle built by your packager, - * after whatever transpilation or minimization steps happen. You will need to maintain sourcemaps to decode them if desired. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * recordError( - * crashlytics, - * new Error('An error was caught') - * ); - * ``` - * @param crashlytics A crashlytics instance. - * @param error Expects an instance of Error; e.g. classes that extend Error will also be supported. - * @param jsErrorName Optional string containing Javascript error name - */ -export function recordError(crashlytics: Crashlytics, error: Error, jsErrorName?: string): void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.recordError.call(crashlytics, error, jsErrorName, MODULAR_DEPRECATION_ARG); -} - -/** - * Enqueues any unsent reports on the device to upload to Crashlytics. This method only applies if - * automatic data collection is disabled. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * sendUnsentReports(crashlytics); - * ``` - * @param crashlytics A crashlytics instance. - */ -export function sendUnsentReports(crashlytics: Crashlytics): void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.sendUnsentReports.call(crashlytics, MODULAR_DEPRECATION_ARG); -} - -/** - * Specify a user identifier which will be visible in the Firebase Crashlytics console. - * - * It is recommended for privacy purposes that this value be a value that's meaningless to a third-party - * observer; such as an arbitrary string that ties an end-user to a record in your system e.g. a database record id. - * - * #### Example - * - * ```js - * const auth = getAuth(); - * const crashlytics = getCrashlytics(); - * // Custom user id - * await setUserId(crashlytics, '123456789'); - * // Firebase auth uid - * await setUserId( - * crashlytics, - * auth.currentUser.uid - * ); - * ``` - * @param crashlytics A crashlytics instance. - * @param userId An arbitrary string that ties an end-user to a record in your system e.g. a database record id. - */ -export function setUserId(crashlytics: Crashlytics, userId: string): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.setUserId.call(crashlytics, userId, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets a string value to be associated with the given attribute name which will be visible in the Firebase Crashlytics console. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * await setAttribute(crashlytics, 'role', 'admin'); - * ``` - * @param crashlytics A crashlytics instance. - * @param name The name of the attribute to set. - * @param value A string value for the given attribute. - */ -export function setAttribute(crashlytics: Crashlytics, name: string, value: string): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.setAttribute.call(crashlytics, name, value, MODULAR_DEPRECATION_ARG); -} - -/** - * Like `setAttribute` but for multiple attributes. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * await setAttributes(crashlytics, { - * role: 'admin', - * followers: '13', - * }); - * ``` - * @param crashlytics A crashlytics instance. - * @param attributes An object of key/value attribute name and values. - */ -export function setAttributes( - crashlytics: Crashlytics, - attributes: { [key: string]: string }, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - return crashlytics.setAttributes.call(crashlytics, attributes, MODULAR_DEPRECATION_ARG); -} - -/** - * Enable/disable Crashlytics reporting. - * - * Use this for opt-in first user data collection flows combined with `firebase.json` settings to disable auto collection. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * // Disable crash reporting - * await setCrashlyticsCollectionEnabled(crashlytics, false); - * ``` - * @param crashlytics A crashlytics instance. - * @param enabled A boolean value representing whether to enable Crashlytics error collection. - */ -export function setCrashlyticsCollectionEnabled( - crashlytics: Crashlytics, - enabled: boolean, -): Promise { - return crashlytics.setCrashlyticsCollectionEnabled.call( - crashlytics, - enabled, - // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope - MODULAR_DEPRECATION_ARG, - ); -} diff --git a/packages/crashlytics/lib/namespaced.ts b/packages/crashlytics/lib/namespaced.ts deleted file mode 100644 index b5a2fcd206..0000000000 --- a/packages/crashlytics/lib/namespaced.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* eslint-disable no-console */ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import { - isBoolean, - isError, - isObject, - isString, - isOther, - MODULAR_DEPRECATION_ARG, -} from '@react-native-firebase/app/dist/module/common'; -import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import StackTrace from 'stacktrace-js'; -import { - createNativeErrorObj, - setGlobalErrorHandler, - setOnUnhandledPromiseRejectionHandler, -} from './handlers'; -import { version } from './version'; -import type { Crashlytics, Statics } from './types/crashlytics'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -const statics: Partial = {}; - -const namespace = 'crashlytics'; - -const nativeModuleName = 'RNFBCrashlyticsModule'; - -class FirebaseCrashlyticsModule extends FirebaseModule { - _isCrashlyticsCollectionEnabled: boolean; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - customUrlOrRegion?: string | null, - ) { - super(app, config, customUrlOrRegion); - setGlobalErrorHandler(this.native); - setOnUnhandledPromiseRejectionHandler(this.native); - this._isCrashlyticsCollectionEnabled = this.native.isCrashlyticsCollectionEnabled; - } - - get isCrashlyticsCollectionEnabled(): boolean { - // Purposefully did not deprecate this as I think it should remain a property rather than a method. - return this._isCrashlyticsCollectionEnabled; - } - - checkForUnsentReports(): Promise { - if (this.isCrashlyticsCollectionEnabled) { - throw new Error( - "firebase.crashlytics().setCrashlyticsCollectionEnabled(*) has been set to 'true', all reports are automatically sent.", - ); - } - return this.native.checkForUnsentReports(); - } - - crash(): void { - this.native.crash(); - } - - async deleteUnsentReports(): Promise { - await this.native.deleteUnsentReports(); - } - - didCrashOnPreviousExecution(): Promise { - return this.native.didCrashOnPreviousExecution(); - } - - log(message: string): void { - this.native.log(`${message}`); - } - - setAttribute(name: string, value: string): Promise { - if (!isString(name)) { - throw new Error( - 'firebase.crashlytics().setAttribute(*, _): The supplied property name must be a string.', - ); - } - - if (!isString(value)) { - throw new Error( - 'firebase.crashlytics().setAttribute(_, *): The supplied property value must be a string value.', - ); - } - - return this.native.setAttribute(name, value); - } - - setAttributes(object: { [key: string]: string }): Promise { - if (!isObject(object)) { - throw new Error( - 'firebase.crashlytics().setAttributes(*): The supplied arg must be an object of key value strings.', - ); - } - - return this.native.setAttributes(object); - } - - setUserId(userId: string): Promise { - if (!isString(userId)) { - throw new Error( - 'firebase.crashlytics().setUserId(*): The supplied userId must be a string value.', - ); - } - - return this.native.setUserId(userId); - } - - recordError(error: Error, jsErrorName?: string): void { - if (isError(error)) { - StackTrace.fromError(error, { offline: true }).then(stackFrames => { - this.native.recordError(createNativeErrorObj(error, stackFrames, false, jsErrorName)); - }); - } else { - console.warn( - 'firebase.crashlytics().recordError(*) expects an instance of Error. Non Errors will be ignored.', - ); - } - } - - sendUnsentReports(): void { - if (this.isCrashlyticsCollectionEnabled) { - this.native.sendUnsentReports(); - } - } - - setCrashlyticsCollectionEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.crashlytics().setCrashlyticsCollectionEnabled(*) 'enabled' must be a boolean.", - ); - } - - this._isCrashlyticsCollectionEnabled = enabled; - return this.native.setCrashlyticsCollectionEnabled(enabled); - } -} - -// import { SDK_VERSION } from '@react-native-firebase/crashlytics'; -export const SDK_VERSION = version; - -const crashlyticsNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: false, - hasMultiAppSupport: false, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseCrashlyticsModule, -}); - -type CrashlyticsNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - Crashlytics, - Statics -> & { - crashlytics: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import crashlytics from '@react-native-firebase/crashlytics'; -// crashlytics().X(...); -export default crashlyticsNamespace as unknown as CrashlyticsNamespace; - -// import crashlytics, { firebase } from '@react-native-firebase/crashlytics'; -// crashlytics().X(...); -// firebase.crashlytics().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'crashlytics', - Crashlytics, - Statics, - false - >; - -// Register the interop module for non-native platforms. -// Note: This package doesn't have a web fallback module like functions does -// setReactNativeModule(nativeModuleName, fallBackModule); - -// This will throw with 'Default App Not initialized' if the default app is not configured. -if (!isOther) { - // @ts-ignore - Extra arg used by deprecation proxy to detect namespaced calls - firebase.crashlytics.call(null, getApp(), MODULAR_DEPRECATION_ARG); -} diff --git a/packages/crashlytics/lib/types/crashlytics.ts b/packages/crashlytics/lib/types/crashlytics.ts index 50d27ffcfb..e7c18ff0c8 100644 --- a/packages/crashlytics/lib/types/crashlytics.ts +++ b/packages/crashlytics/lib/types/crashlytics.ts @@ -125,35 +125,5 @@ export interface Statics { /** * FirebaseApp type with crashlytics() method. * @deprecated Import FirebaseApp from '@react-native-firebase/app' instead. - * The crashlytics() method is added via module augmentation. */ export type FirebaseApp = ReactNativeFirebase.FirebaseApp; - -// ============ Module Augmentation ============ - -/* eslint-disable @typescript-eslint/no-namespace */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - interface Module { - crashlytics: FirebaseModuleWithStaticsAndApp; - } - interface FirebaseApp { - crashlytics(): Crashlytics; - } - } -} -/* eslint-enable @typescript-eslint/no-namespace */ -// ============ Backwards Compatibility Namespace - to be removed with namespaced exports ============ -type _Statics = Statics; - -/** - * @deprecated Use the exported types directly instead. - * FirebaseCrashlyticsTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseCrashlyticsTypes { - // Short name aliases referencing top-level types - export type Module = Crashlytics; - export type Statics = _Statics; -} -/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/crashlytics/lib/types/internal.ts b/packages/crashlytics/lib/types/internal.ts new file mode 100644 index 0000000000..bbffb2201a --- /dev/null +++ b/packages/crashlytics/lib/types/internal.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import type { Crashlytics } from './crashlytics'; + +export interface RNFBCrashlyticsModule { + isCrashlyticsCollectionEnabled: boolean; + isErrorGenerationOnJSCrashEnabled: boolean; + isCrashlyticsJavascriptExceptionHandlerChainingEnabled: boolean; + checkForUnsentReports(): Promise; + crash(): void; + deleteUnsentReports(): Promise; + didCrashOnPreviousExecution(): Promise; + log(message: string): void; + setAttribute(name: string, value: string): Promise; + setAttributes(object: { [key: string]: string }): Promise; + setUserId(userId: string): Promise; + recordError(errorObj: unknown): void; + sendUnsentReports(): void; + setCrashlyticsCollectionEnabled(enabled: boolean): Promise; + logPromise(message: string): Promise; + recordErrorPromise(errorObj: unknown): Promise; + crashWithStackPromise(errorObj: unknown): Promise; +} + +declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { + interface ReactNativeFirebaseNativeModules { + RNFBCrashlyticsModule: RNFBCrashlyticsModule; + } +} + +export interface CrashlyticsInternal extends Crashlytics { + checkForUnsentReports(): Promise; + crash(): void; + deleteUnsentReports(): Promise; + didCrashOnPreviousExecution(): Promise; + log(message: string): void; + recordError(error: Error, jsErrorName?: string): void; + sendUnsentReports(): void; + setUserId(userId: string): Promise; + setAttribute(name: string, value: string): Promise; + setAttributes(attributes: { [key: string]: string }): Promise; + setCrashlyticsCollectionEnabled(enabled: boolean): Promise; + readonly native: RNFBCrashlyticsModule; +} diff --git a/packages/crashlytics/type-test.ts b/packages/crashlytics/type-test.ts index 09d5b342a9..a4d494242b 100644 --- a/packages/crashlytics/type-test.ts +++ b/packages/crashlytics/type-test.ts @@ -1,9 +1,5 @@ -import crashlytics, { - firebase, - FirebaseCrashlyticsTypes, - // Types - type Crashlytics, - // Modular API +import { getApp } from '@react-native-firebase/app'; +import { getCrashlytics, checkForUnsentReports, deleteUnsentReports, @@ -16,141 +12,29 @@ import crashlytics, { setAttribute, setAttributes, setCrashlyticsCollectionEnabled, + SDK_VERSION, + type Crashlytics, } from '.'; -console.log(crashlytics().app); - -// checks module exists at root -console.log(firebase.crashlytics().app.name); -console.log(firebase.crashlytics().isCrashlyticsCollectionEnabled); - -// checks module exists at app level -console.log(firebase.app().crashlytics().app.name); -console.log(firebase.app().crashlytics().isCrashlyticsCollectionEnabled); - -// checks statics exist -console.log(firebase.crashlytics.SDK_VERSION); - -// checks statics exist on defaultExport -console.log(crashlytics.firebase.SDK_VERSION); - -console.log(crashlytics.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// test type usage -const crashlyticsInstance: Crashlytics = firebase.crashlytics(); -console.log(crashlyticsInstance.app.name); -console.log(crashlyticsInstance.isCrashlyticsCollectionEnabled); - -// checks Module instance APIs -crashlyticsInstance.checkForUnsentReports().then((hasUnsent: boolean) => { - console.log(hasUnsent); -}); - -crashlyticsInstance.deleteUnsentReports().then(() => { - console.log('Unsent reports deleted'); -}); - -crashlyticsInstance.didCrashOnPreviousExecution().then((didCrash: boolean) => { - console.log(didCrash); -}); - -crashlyticsInstance.crash(); - -crashlyticsInstance.log('Test log message'); - -crashlyticsInstance.recordError(new Error('Test error')); -crashlyticsInstance.recordError(new Error('Test error'), 'CustomErrorName'); - -crashlyticsInstance.sendUnsentReports(); - -crashlyticsInstance.setUserId('user123').then(() => { - console.log('User ID set'); -}); - -crashlyticsInstance.setAttribute('key', 'value').then(() => { - console.log('Attribute set'); -}); - -crashlyticsInstance.setAttributes({ key1: 'value1', key2: 'value2' }).then(() => { - console.log('Attributes set'); -}); - -crashlyticsInstance.setCrashlyticsCollectionEnabled(true).then(() => { - console.log('Collection enabled'); -}); - -// checks all methods exist on firebase.crashlytics() -console.log(firebase.crashlytics().checkForUnsentReports); -console.log(firebase.crashlytics().deleteUnsentReports); -console.log(firebase.crashlytics().didCrashOnPreviousExecution); -console.log(firebase.crashlytics().crash); -console.log(firebase.crashlytics().log); -console.log(firebase.crashlytics().recordError); -console.log(firebase.crashlytics().sendUnsentReports); -console.log(firebase.crashlytics().setUserId); -console.log(firebase.crashlytics().setAttribute); -console.log(firebase.crashlytics().setAttributes); -console.log(firebase.crashlytics().setCrashlyticsCollectionEnabled); - -// checks all methods exist on default export -console.log(crashlytics().checkForUnsentReports); -console.log(crashlytics().deleteUnsentReports); -console.log(crashlytics().didCrashOnPreviousExecution); -console.log(crashlytics().crash); -console.log(crashlytics().log); -console.log(crashlytics().recordError); -console.log(crashlytics().sendUnsentReports); -console.log(crashlytics().setUserId); -console.log(crashlytics().setAttribute); -console.log(crashlytics().setAttributes); -console.log(crashlytics().setCrashlyticsCollectionEnabled); - -// test modular API functions -const crashlyticsModular = getCrashlytics(); -console.log(crashlyticsModular.app.name); - -checkForUnsentReports(crashlyticsInstance).then((hasUnsent: boolean) => { - console.log(hasUnsent); -}); - -deleteUnsentReports(crashlyticsInstance).then(() => { - console.log('Unsent reports deleted'); -}); - -didCrashOnPreviousExecution(crashlyticsInstance).then((didCrash: boolean) => { - console.log(didCrash); -}); - -crash(crashlyticsInstance); - -log(crashlyticsInstance, 'Modular log message'); - -recordError(crashlyticsInstance, new Error('Modular error')); -recordError(crashlyticsInstance, new Error('Modular error'), 'CustomErrorName'); - -sendUnsentReports(crashlyticsInstance); - -setUserId(crashlyticsInstance, 'user123').then(() => { - console.log('User ID set'); -}); +const crashlytics = getCrashlytics(); +console.log(crashlytics.app.name); -setAttribute(crashlyticsInstance, 'key', 'value').then(() => { - console.log('Attribute set'); -}); +const crashlyticsWithApp = getCrashlytics(getApp()); +console.log(crashlyticsWithApp.app.name); -setAttributes(crashlyticsInstance, { key1: 'value1', key2: 'value2' }).then(() => { - console.log('Attributes set'); -}); +const typed: Crashlytics = crashlytics; +console.log(typed.isCrashlyticsCollectionEnabled); -setCrashlyticsCollectionEnabled(crashlyticsInstance, true).then(() => { - console.log('Collection enabled'); -}); +checkForUnsentReports(crashlytics).then((value: boolean) => console.log(value)); +deleteUnsentReports(crashlytics).then(() => console.log('deleted')); +didCrashOnPreviousExecution(crashlytics).then((value: boolean) => console.log(value)); +log(crashlytics, 'message'); +recordError(crashlytics, new Error('test')); +sendUnsentReports(crashlytics); +setUserId(crashlytics, 'user'); +setAttribute(crashlytics, 'role', 'admin'); +setAttributes(crashlytics, { role: 'admin' }); +setCrashlyticsCollectionEnabled(crashlytics, true); +crash(crashlytics); -// test FirebaseCrashlyticsTypes namespace -const namespaceInstance: FirebaseCrashlyticsTypes.Module = firebase.crashlytics(); -console.log(namespaceInstance.app.name); -const namespaceStatics: FirebaseCrashlyticsTypes.Statics = firebase.crashlytics; -console.log(namespaceStatics); +console.log(SDK_VERSION); diff --git a/packages/crashlytics/typedoc.json b/packages/crashlytics/typedoc.json index 6e70d1d02d..18e36e7105 100644 --- a/packages/crashlytics/typedoc.json +++ b/packages/crashlytics/typedoc.json @@ -1,6 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/crashlytics.ts"], - "tsconfig": "tsconfig.json", - "intentionallyNotExported": ["_Statics"] + "entryPoints": ["lib/index.ts", "lib/types/crashlytics.ts"], + "tsconfig": "tsconfig.json" } diff --git a/packages/database/__tests__/database.test.ts b/packages/database/__tests__/database.test.ts index 636fdda717..fc1d58fb11 100644 --- a/packages/database/__tests__/database.test.ts +++ b/packages/database/__tests__/database.test.ts @@ -1,7 +1,6 @@ -import { afterAll, beforeAll, describe, expect, it, beforeEach, jest } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; -import database, { - firebase, +import { runTransaction, getDatabase, connectDatabaseEmulator, @@ -48,67 +47,7 @@ import database, { update, } from '../lib'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, - withDeprecationWarningsSilenced, -} from '../../app/lib/common/unitTestUtils'; -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; - describe('Database', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.database).toBeDefined(); - expect(app.database().useEmulator).toBeDefined(); - }); - - describe('useEmulator()', function () { - it('useEmulator requires a string host', function () { - // @ts-ignore because we pass an invalid argument... - expect(() => database().useEmulator()).toThrow( - 'firebase.database().useEmulator() takes a non-empty host', - ); - expect(() => database().useEmulator('', -1)).toThrow( - 'firebase.database().useEmulator() takes a non-empty host', - ); - // @ts-ignore because we pass an invalid argument... - expect(() => database().useEmulator(123)).toThrow( - 'firebase.database().useEmulator() takes a non-empty host', - ); - }); - - it('useEmulator requires a host and port', function () { - expect(() => database().useEmulator('', 9000)).toThrow( - 'firebase.database().useEmulator() takes a non-empty host and port', - ); - // No port - // @ts-ignore because we pass an invalid argument... - expect(() => database().useEmulator('localhost')).toThrow( - 'firebase.database().useEmulator() takes a non-empty host and port', - ); - }); - - it('useEmulator -> remaps Android loopback to host', function () { - const foo = database().useEmulator('localhost', 9000); - expect(foo).toEqual(['10.0.2.2', 9000]); - - const bar = database().useEmulator('127.0.0.1', 9000); - expect(bar).toEqual(['10.0.2.2', 9000]); - }); - }); - }); - describe('modular', function () { it('`onValue` query method is properly exposed to end user', function () { expect(onValue).toBeDefined(); @@ -290,381 +229,4 @@ describe('Database', function () { expect(update).toBeDefined(); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let databaseV9Deprecation: CheckV9DeprecationFunction; - let referenceV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - databaseV9Deprecation = createCheckV9Deprecation(['database']); - referenceV9Deprecation = createCheckV9Deprecation(['database', 'DatabaseReference']); - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockReturnValue({ - constants: { - isDatabaseCollectionEnabled: true, - url: 'https://test.firebaseio.com', - ref: 'ref()', - }, - on: jest.fn(), - off: jest.fn(), - once: jest.fn( - (_appName: any, _customUrl: any, path: any, _modifiers: any, eventType: any) => { - // Database native methods receive (appName, customUrlOrRegion, ...actualArgs) - let key = 'test'; - if (path && typeof path === 'string') { - const parts = path.split('/').filter(p => p); - key = parts[parts.length - 1] || 'test'; - } - - const snapshotData = { - key, - value: 'mock_value', - exists: true, - childKeys: [], - priority: null, - }; - - if (eventType === 'value') { - return Promise.resolve(snapshotData); - } - - return Promise.resolve({ - snapshot: snapshotData, - previousChildName: null, - }); - }, - ), - set: jest.fn(() => Promise.resolve()), - update: jest.fn(() => Promise.resolve()), - setWithPriority: jest.fn(() => Promise.resolve()), - remove: jest.fn(() => Promise.resolve()), - setPriority: jest.fn(() => Promise.resolve()), - keepSynced: jest.fn(() => Promise.resolve()), - transactionStart: jest.fn(() => Promise.resolve()), - transactionTryCommit: jest.fn(() => Promise.resolve()), - goOnline: jest.fn(() => Promise.resolve()), - goOffline: jest.fn(() => Promise.resolve()), - setPersistenceEnabled: jest.fn(() => Promise.resolve()), - setLoggingEnabled: jest.fn(() => Promise.resolve()), - setPersistenceCacheSizeBytes: jest.fn(() => Promise.resolve()), - getServerTime: jest.fn(() => Promise.resolve(Date.now())), - } as any); - }); - - it('useEmulator', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => connectDatabaseEmulator(db, 'localhost', 9000), - // @ts-expect-error Combines modular and namespace API - () => db.useEmulator('localhost', 9000), - 'useEmulator', - ); - }); - - it('goOffline', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => goOffline(db), - // @ts-expect-error Combines modular and namespace API - () => db.goOffline(), - 'goOffline', - ); - }); - - it('goOnline', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => goOnline(db), - // @ts-expect-error Combines modular and namespace API - () => db.goOnline(), - 'goOnline', - ); - }); - - it('ref', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => ref(db, 'test'), - // @ts-expect-error Combines modular and namespace API - () => db.ref('test'), - 'ref', - ); - }); - - it('refFromURL', function () { - const db = getDatabase(); - // Mock the _customUrlOrRegion property directly on the database instance - (db as any)._customUrlOrRegion = 'https://test.firebaseio.com'; - databaseV9Deprecation( - () => refFromURL(db, 'https://test.firebaseio.com'), - // @ts-expect-error Combines modular and namespace API - () => db.refFromURL('https://test.firebaseio.com'), - 'refFromURL', - ); - }); - - it('setPersistenceEnabled', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => setPersistenceEnabled(db, true), - // @ts-expect-error Combines modular and namespace API - () => db.setPersistenceEnabled(true), - 'setPersistenceEnabled', - ); - }); - - it('setLoggingEnabled', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => setLoggingEnabled(db, true), - // @ts-expect-error Combines modular and namespace API - () => db.setLoggingEnabled(true), - 'setLoggingEnabled', - ); - }); - - it('setPersistenceCacheSizeBytes', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => setPersistenceCacheSizeBytes(db, 10000000), - // @ts-expect-error Combines modular and namespace API - () => db.setPersistenceCacheSizeBytes(10000000), - 'setPersistenceCacheSizeBytes', - ); - }); - - it('getServerTime', function () { - const db = getDatabase(); - databaseV9Deprecation( - () => getServerTime(db), - // @ts-expect-error Combines modular and namespace API - () => db.getServerTime(), - 'getServerTime', - ); - }); - - describe('DatabaseReference', function () { - it('child', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => child(testRef, 'child'), - () => testRef2.child('child'), - 'child', - ); - }); - - it('set', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => set(testRef, 'value'), - () => testRef2.set('value'), - 'set', - ); - }); - - it('update', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => update(testRef, { value: 'value' }), - () => testRef2.update({ value: 'value' }), - 'update', - ); - }); - - it('setWithPriority', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => setWithPriority(testRef, 'value', 1), - () => testRef2.setWithPriority('value', 1), - 'setWithPriority', - ); - }); - - it('remove', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => remove(testRef), - () => testRef2.remove(), - 'remove', - ); - }); - - it('onValue', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => onValue(testRef, () => {}), - () => testRef2.on('value', () => {}), - 'on', - ); - }); - - it('get', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => get(testRef), - () => testRef2.once('value'), - 'once', - ); - }); - - it('endAt', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, endAt('value')), - () => testRef2.endAt('value'), - 'endAt', - ); - }); - - it('startAt', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, startAt('value')), - () => testRef2.startAt('value'), - 'startAt', - ); - }); - - it('limitToFirst', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, limitToFirst(10)), - () => testRef2.limitToFirst(10), - 'limitToFirst', - ); - }); - - it('limitToLast', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, limitToLast(10)), - () => testRef2.limitToLast(10), - 'limitToLast', - ); - }); - - it('orderByChild', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, orderByChild('name')), - () => testRef2.orderByChild('name'), - 'orderByChild', - ); - }); - - it('orderByKey', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, orderByKey()), - () => testRef2.orderByKey(), - 'orderByKey', - ); - }); - - it('orderByValue', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, orderByValue()), - () => testRef2.orderByValue(), - 'orderByValue', - ); - }); - - it('equalTo', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => query(testRef, equalTo('value')), - () => testRef2.equalTo('value'), - 'equalTo', - ); - }); - - it('setPriority', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => setPriority(testRef, 'value'), - () => testRef2.setPriority('value'), - 'setPriority', - ); - }); - - it('push', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => push(testRef, 'value'), - () => testRef2.push('value'), - 'push', - ); - }); - - it('onDisconnect', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => onDisconnect(testRef), - () => testRef2.onDisconnect(), - 'onDisconnect', - ); - }); - - it('keepSynced', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => keepSynced(testRef, true), - () => testRef2.keepSynced(true), - 'keepSynced', - ); - }); - }); - - describe('DatabaseTransaction', function () { - it('runTransaction', function () { - const db = getDatabase(); - const testRef = ref(db, 'test'); - const testRef2 = withDeprecationWarningsSilenced(() => firebase.database().ref('test')); - referenceV9Deprecation( - () => runTransaction(testRef, currentData => currentData, { applyLocally: true }), - () => testRef2.transaction((currentData: any) => currentData, undefined, true), - 'transaction', - ); - }); - }); - }); }); diff --git a/packages/database/e2e/DatabaseStatics.e2e.js b/packages/database/e2e/DatabaseStatics.e2e.js index 28712c57ee..5bff6d796f 100644 --- a/packages/database/e2e/DatabaseStatics.e2e.js +++ b/packages/database/e2e/DatabaseStatics.e2e.js @@ -20,70 +20,6 @@ const { PATH, wipe } = require('./helpers'); const TEST_PATH = `${PATH}/statics`; describe('database.X', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - after(function () { - return wipe(TEST_PATH); - }); - - describe('ServerValue.TIMESTAMP', function () { - it('returns a valid object', function () { - const { TIMESTAMP } = firebase.database.ServerValue; - should.equal(Object.keys(TIMESTAMP).length, 1); - TIMESTAMP.should.have.property('.sv'); - TIMESTAMP['.sv'].should.eql('timestamp'); - }); - - it('populates the property with a Unix timestamp', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/timestamp`); - await ref.set(firebase.database.ServerValue.TIMESTAMP); - const snapshot = await ref.once('value'); - snapshot.val().should.be.a.Number(); - }); - }); - - describe('ServerValue.increment', function () { - it('returns a valid object', function () { - const incrementObject = firebase.database.ServerValue.increment(1); - should.equal(Object.keys(incrementObject).length, 1); - incrementObject.should.have.property('.sv'); - incrementObject['.sv'].should.have.property('increment'); - }); - - it('increments on the server', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/increment`); - - await ref.set({ increment: 0 }); - - const res1 = await ref.once('value'); - res1.val().increment.should.equal(0); - - await ref.set({ increment: firebase.database.ServerValue.increment(1) }); - - const res2 = await ref.once('value'); - res2.val().increment.should.equal(1); - }); - - it('increments on the server when no value is present', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/increment-empty`); - - await ref.set({ increment: firebase.database.ServerValue.increment(2) }); - - const res = await ref.once('value'); - res.val().increment.should.equal(2); - }); - }); - }); - describe('modular', function () { after(function () { return wipe(TEST_PATH); diff --git a/packages/database/e2e/database.e2e.js b/packages/database/e2e/database.e2e.js index 9209410449..36d777d32e 100644 --- a/packages/database/e2e/database.e2e.js +++ b/packages/database/e2e/database.e2e.js @@ -16,201 +16,6 @@ */ describe('database()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.database); - app.database().app.should.eql(app); - }); - - it('supports multiple apps', async function () { - firebase.database().app.name.should.eql('[DEFAULT]'); - - firebase - .database(firebase.app('secondaryFromNative')) - .app.name.should.eql('secondaryFromNative'); - - firebase.app('secondaryFromNative').database().app.name.should.eql('secondaryFromNative'); - }); - }); - - describe('ref()', function () { - it('throws if path is not a string', async function () { - try { - firebase.database().ref({ foo: 'bar' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'path' must be a string value"); - return Promise.resolve(); - } - }); - - it('throws if path is not a valid string', async function () { - try { - firebase.database().ref('$$$$$'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "Paths must be non-empty strings and can't contain #, $, [, ], ' or ?", - ); - return Promise.resolve(); - } - }); - }); - - describe('refFromURL()', function () { - it('throws if url is not a url', async function () { - try { - firebase.database().refFromURL('foobar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'url' must be a valid database URL"); - return Promise.resolve(); - } - }); - - it('throws if url from a different domain', async function () { - try { - firebase.database().refFromURL('https://foobar.firebaseio.com'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'url' must be the same domain as the current instance"); - return Promise.resolve(); - } - }); - - it('returns a reference', async function () { - const ref1 = firebase.database().refFromURL(firebase.database()._customUrlOrRegion); - const ref2 = firebase - .database() - .refFromURL(`${firebase.database()._customUrlOrRegion}/foo/bar`); - const ref3 = firebase - .database() - .refFromURL(`${firebase.database()._customUrlOrRegion}/foo/bar?baz=foo`); - should.equal(ref1.path, '/'); - should.equal(ref2.path, 'foo/bar'); - should.equal(ref3.path, 'foo/bar'); - }); - }); - - describe('goOnline()', function () { - it('calls goOnline successfully', async function () { - await firebase.database().goOnline(); - }); - }); - - describe('goOffline()', function () { - it('calls goOffline successfully', async function () { - // await Utils.sleep(5000); - await firebase.database().goOffline(); - - await firebase.database().goOnline(); - }); - }); - - describe('setPersistenceEnabled()', function () { - it('throws if enabled is not a boolean', async function () { - try { - firebase.database().setPersistenceEnabled('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'enabled' must be a boolean value"); - return Promise.resolve(); - } - }); - - it('calls setPersistenceEnabled successfully', async function () { - firebase.database().setPersistenceEnabled(true); - firebase.database().setPersistenceEnabled(false); - }); - }); - - describe('setLoggingEnabled()', function () { - it('throws if enabled is not a boolean', async function () { - try { - firebase.database().setLoggingEnabled('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'enabled' must be a boolean value"); - return Promise.resolve(); - } - }); - - it('calls setLoggingEnabled successfully', async function () { - firebase.database().setLoggingEnabled(true); - firebase.database().setLoggingEnabled(false); - }); - }); - - describe('setPersistenceCacheSizeBytes()', function () { - it('throws if bytes is not a number', async function () { - try { - firebase.database().setPersistenceCacheSizeBytes('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'bytes' must be a number value"); - return Promise.resolve(); - } - }); - - it('throws if bytes is less than 1MB', async function () { - try { - firebase.database().setPersistenceCacheSizeBytes(1234); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'bytes' must be greater than 1048576 bytes (1MB)"); - return Promise.resolve(); - } - }); - - it('throws if bytes is greater than 10MB', async function () { - try { - firebase.database().setPersistenceCacheSizeBytes(100000000000000); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'bytes' must be less than 104857600 bytes (100MB)"); - return Promise.resolve(); - } - }); - - it('calls setPersistenceCacheSizeBytes successfully', async function () { - firebase.database().setPersistenceCacheSizeBytes(1048576); // 1mb - }); - }); - - describe('getServerTime()', function () { - it('returns a valid date', async function () { - const date = firebase.database().getServerTime(); - date.getDate.should.be.Function(); - }); - }); - - describe('handles invalid references()', function () { - it('returns a valid date', async function () { - try { - firebase.database().ref('$'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "Paths must be non-empty strings and can't contain #, $, [, ], ' or ?", - ); - return Promise.resolve(); - } - }); - }); - }); - describe('modular', function () { describe('namespace', function () { it('accessible from getDatabase', function () { diff --git a/packages/database/e2e/internal/connected.e2e.js b/packages/database/e2e/internal/connected.e2e.js index e8e6d75950..c78fb5a890 100644 --- a/packages/database/e2e/internal/connected.e2e.js +++ b/packages/database/e2e/internal/connected.e2e.js @@ -19,53 +19,6 @@ const { PATH } = require('../helpers'); const TEST_PATH = `${PATH}/connected`; describe("database().ref('.info/connected')", function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - await firebase.database().goOnline(); - }); - - afterEach(async function afterEachTest() { - // Ensures the db is online before running each test - await firebase.database().goOnline(); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - xit('returns true when used with once', async function () { - const snapshot = await firebase.database().ref('.info/connected').once('value'); - snapshot.val().should.equal(true); - }); - - xit('returns true when used with once with a previous call', async function () { - await firebase.database().ref(`${TEST_PATH}/foo`).once('value'); - const snapshot = await firebase.database().ref('.info/connected').once('value'); - snapshot.val().should.equal(true); - }); - - // FIXME on android this can work against the emulator - // on iOS it doesn't work at all ? - xit('subscribes to online state', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref('.info/connected'); - const handler = $ => { - callback($.val()); - }; - - ref.on('value', handler); - await firebase.database().goOffline(); - await Utils.sleep(1000); // FIXME why is this sleep needed here? callback is called immediately - await firebase.database().goOnline(); - ref.off('value', handler); - - await Utils.spyToBeCalledTimesAsync(callback, 2); - callback.getCall(0).args[0].should.equal(false); - callback.getCall(1).args[0].should.equal(true); - }); - }); - describe('modular', function () { before(async function () { const { getDatabase, goOnline } = databaseModular; @@ -82,7 +35,7 @@ describe("database().ref('.info/connected')", function () { xit('returns true when used with once', async function () { const { getDatabase, ref, get } = databaseModular; - const snapshot = await get(ref(getDatabase(), '.info/connected'), dbRef); + const snapshot = await get(ref(getDatabase(), '.info/connected')); snapshot.val().should.equal(true); }); @@ -90,7 +43,7 @@ describe("database().ref('.info/connected')", function () { const { getDatabase, ref, get } = databaseModular; await get(ref(getDatabase(), `${TEST_PATH}/foo`)); - const snapshot = await firebase.database().ref('.info/connected').once('value'); + const snapshot = await get(ref(getDatabase(), '.info/connected')); snapshot.val().should.equal(true); }); diff --git a/packages/database/e2e/internal/serverTimeOffset.e2e.js b/packages/database/e2e/internal/serverTimeOffset.e2e.js index 3df68341a6..46ecf864dc 100644 --- a/packages/database/e2e/internal/serverTimeOffset.e2e.js +++ b/packages/database/e2e/internal/serverTimeOffset.e2e.js @@ -16,24 +16,6 @@ */ describe("database().ref('.info/serverTimeOffset')", function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns a valid number value', async function () { - const snapshot = await firebase.database().ref('.info/serverTimeOffset').once('value'); - - should.equal(typeof snapshot.val(), 'number'); - }); - }); - describe('modular', function () { it('returns a valid number value', async function () { const { getDatabase, ref, get } = databaseModular; diff --git a/packages/database/e2e/issues.e2e.js b/packages/database/e2e/issues.e2e.js index 2142dbe751..97e765419d 100644 --- a/packages/database/e2e/issues.e2e.js +++ b/packages/database/e2e/issues.e2e.js @@ -28,152 +28,12 @@ describe('database issues', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - // FIXME requires a second database set up locally, full app initialization etc - xit('#2813 should return a null snapshot key if path is root', async function () { - firebase.database('https://react-native-firebase-testing-db2.firebaseio.com'); - const ref = firebase - .app() - .database('https://react-native-firebase-testing-db2.firebaseio.com') - .ref(); - const snapshot = await ref.once('value'); - should.equal(snapshot.key, null); - }); - - it('#2833 should not mutate modifiers ordering', async function () { - const callback = sinon.spy(); - const testRef = firebase - .database() - .ref() - .child(TEST_PATH) - .orderByChild('disabled') - .equalTo(false); - - testRef._modifiers.toString().should.be.a.String(); - testRef._modifiers.toArray()[0].name.should.equal('orderByChild'); - - testRef.on('value', snapshot => { - callback(snapshot.val()); - }); - - await Utils.spyToBeCalledOnceAsync(callback, 3000); - - testRef.off('value'); - }); - - it('#100 array should return null where key is missing', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/issue_100`); - - const data = { - 1: { - someKey: 'someValue', - someOtherKey: 'someOtherValue', - }, - 2: { - someKey: 'someValue', - someOtherKey: 'someOtherValue', - }, - 3: { - someKey: 'someValue', - someOtherKey: 'someOtherValue', - }, - }; - - await ref.set(data); - const snapshot = await ref.once('value'); - - snapshot.val().should.eql([null, data[1], data[2], data[3]]); - }); - - describe('#108 filters correctly by float values', function () { - it('returns filtered results', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/issue_108/filter`); - - const data = { - foobar: { - name: 'Foobar Pizzas', - latitude: 34.1013717, - }, - notTheFoobar: { - name: "Not the pizza you're looking for", - latitude: 34.456787, - }, - notAFloat: { - name: 'Not a float', - latitude: 37, - }, - }; - - await ref.set(data); - const snapshot = await ref - .orderByChild('latitude') - .startAt(34.00867000999119) - .endAt(34.17462960866099) - .once('value'); - - const val = snapshot.val(); - val.foobar.should.eql(data.foobar); - - should.equal(Object.keys(val).length, 1); - }); - - it('returns correct results when not using float values', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/issue_108/integer`); - - const data = { - foobar: { - name: 'Foobar Pizzas', - latitude: 34.1013717, - }, - notTheFoobar: { - name: "Not the pizza you're looking for", - latitude: 34.456787, - }, - notAFloat: { - name: 'Not a float', - latitude: 37, - }, - }; - - await ref.set(data); - const snapshot = await ref.orderByChild('latitude').equalTo(37).once('value'); - - const val = snapshot.val(); - - val.notAFloat.should.eql(data.notAFloat); - - should.equal(Object.keys(val).length, 1); - }); - }); - - it('#489 reutrns long numbers correctly', async function () { - const LONG = 1508777379000; - const ref = firebase.database().ref(`${TEST_PATH}/issue_489`); - await ref.set(LONG); - const snapshot = await ref.once('value'); - snapshot.val().should.eql(LONG); - }); - }); - describe('modular', function () { // FIXME requires a second database set up locally, full app initialization etc xit('#2813 should return a null snapshot key if path is root', async function () { const { getDatabase, ref, get } = databaseModular; - const db = getDatabase( - /* takes default firebase.app() */ null, - 'https://react-native-firebase-testing-db2.firebaseio.com', - ); + const db = getDatabase(undefined, 'https://react-native-firebase-testing-db2.firebaseio.com'); const dbRef = ref(db); const snapshot = await get(dbRef); should.equal(snapshot.key, null); diff --git a/packages/database/e2e/onDisconnect/cancel.e2e.js b/packages/database/e2e/onDisconnect/cancel.e2e.js index 357cd4e20c..5d49eabe87 100644 --- a/packages/database/e2e/onDisconnect/cancel.e2e.js +++ b/packages/database/e2e/onDisconnect/cancel.e2e.js @@ -24,62 +24,6 @@ describe('database().ref().onDisconnect().cancel()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // Ensures the db is online before running each test - await firebase.database().goOnline(); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if onComplete is not a function', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.cancel('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('cancels all previously queued events', async function () { - const ref = firebase.database().ref(TEST_PATH); - - await ref.set('foobar'); - const value = Date.now(); - - await ref.onDisconnect().set(value); - await ref.onDisconnect().cancel(); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - const snapshot = await ref.once('value'); - snapshot.val().should.eql('foobar'); - }); - - it('calls back to the onComplete function', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH); - - // Set an initial value - await ref.set('foo'); - - await ref.onDisconnect().set('bar'); - await ref.onDisconnect().cancel(callback); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - callback.should.be.calledOnce(); - }); - }); - describe('modular', function () { afterEach(async function () { const { getDatabase, goOnline } = databaseModular; diff --git a/packages/database/e2e/onDisconnect/remove.e2e.js b/packages/database/e2e/onDisconnect/remove.e2e.js index 4189b41daf..ce505e8eaf 100644 --- a/packages/database/e2e/onDisconnect/remove.e2e.js +++ b/packages/database/e2e/onDisconnect/remove.e2e.js @@ -24,60 +24,6 @@ describe('database().ref().onDisconnect().remove()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // Ensures the db is online before running each test - await firebase.database().goOnline(); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if onComplete is not a function', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.remove('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('removes a node whilst offline', async function () { - if (Platform.android) { - // offline / online behavior does not work in android + firebase emulator - this.skip(); - } - const ref = firebase.database().ref(TEST_PATH).child('removeMe'); - await ref.set('foobar'); - await ref.onDisconnect().remove(); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - const snapshot = await ref.once('value'); - snapshot.exists().should.eql(false); - }); - - it('calls back to the onComplete function', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH).child('removeMe'); - - // Set an initial value - await ref.set('foo'); - - await ref.onDisconnect().remove(callback); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - callback.should.be.calledOnce(); - }); - }); - describe('modular', function () { afterEach(async function () { const { getDatabase, goOnline } = databaseModular; diff --git a/packages/database/e2e/onDisconnect/set.e2e.js b/packages/database/e2e/onDisconnect/set.e2e.js index ec53b33845..9d0c3c6de9 100644 --- a/packages/database/e2e/onDisconnect/set.e2e.js +++ b/packages/database/e2e/onDisconnect/set.e2e.js @@ -24,74 +24,6 @@ describe('database().ref().onDisconnect().set()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // Ensures the db is online before running each test - await firebase.database().goOnline(); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if value is not a defined', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.set(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' must be defined"); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.set(null, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('sets value when disconnected', async function () { - if (Platform.android) { - // offline / online behavior does not work in android + firebase emulator - this.skip(); - } - const ref = firebase.database().ref(TEST_PATH); - - const value = Date.now(); - - await ref.onDisconnect().set(value); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - const snapshot = await ref.once('value'); - snapshot.val().should.eql(value); - }); - - it('calls back to the onComplete function', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH); - - // Set an initial value - await ref.set('foo'); - - await ref.onDisconnect().set('bar', callback); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - callback.should.be.calledOnce(); - }); - }); - describe('modular', function () { afterEach(async function () { const { getDatabase, goOnline } = databaseModular; diff --git a/packages/database/e2e/onDisconnect/setWithPriority.e2e.js b/packages/database/e2e/onDisconnect/setWithPriority.e2e.js index 915259106f..1975f8d23b 100644 --- a/packages/database/e2e/onDisconnect/setWithPriority.e2e.js +++ b/packages/database/e2e/onDisconnect/setWithPriority.e2e.js @@ -24,86 +24,6 @@ describe('database().ref().onDisconnect().setWithPriority()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // Ensures the db is online before running each test - await firebase.database().goOnline(); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if value is not a defined', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.setWithPriority(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' must be defined"); - return Promise.resolve(); - } - }); - - it('throws if priority is not a valid type', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.setWithPriority(null, { foo: 'bar' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'priority' must be a number, string or null value"); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.setWithPriority(null, 1, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('sets value with priority when disconnected', async function () { - if (Platform.android) { - // offline / online behavior does not work in android + firebase emulator - this.skip(); - } - const ref = firebase.database().ref(TEST_PATH); - - const value = Date.now(); - - await ref.onDisconnect().setWithPriority(value, 3); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - const snapshot = await ref.once('value'); - snapshot.exportVal()['.value'].should.eql(value); - snapshot.exportVal()['.priority'].should.eql(3); - }); - - it('calls back to the onComplete function', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH); - - // Set an initial value - await ref.set('foo'); - - await ref.onDisconnect().setWithPriority('bar', 2, callback); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - callback.should.be.calledOnce(); - }); - }); - describe('modular', function () { afterEach(async function () { const { getDatabase, goOnline } = databaseModular; diff --git a/packages/database/e2e/onDisconnect/update.e2e.js b/packages/database/e2e/onDisconnect/update.e2e.js index 5abb3018c7..91186d1756 100644 --- a/packages/database/e2e/onDisconnect/update.e2e.js +++ b/packages/database/e2e/onDisconnect/update.e2e.js @@ -24,105 +24,6 @@ describe('database().ref().onDisconnect().update()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // Ensures the db is online before running each test - await firebase.database().goOnline(); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if values is not an object', async function () { - try { - await firebase.database().ref(TEST_PATH).onDisconnect().update('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'values' must be an object"); - return Promise.resolve(); - } - }); - - it('throws if values does not contain any values', async function () { - try { - await firebase.database().ref(TEST_PATH).onDisconnect().update({}); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'values' must be an object containing multiple values"); - return Promise.resolve(); - } - }); - - it('throws if update paths are not valid', async function () { - try { - await firebase.database().ref(TEST_PATH).onDisconnect().update({ - $$$$: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'values' contains an invalid path."); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', function () { - const ref = firebase.database().ref(TEST_PATH).onDisconnect(); - try { - ref.update({ foo: 'bar' }, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('updates value when disconnected', async function () { - if (Platform.android) { - // offline / online behavior does not work in android + firebase emulator - this.skip(); - } - const ref = firebase.database().ref(TEST_PATH); - - const value = Date.now(); - await ref.set({ - foo: { - bar: 'baz', - }, - }); - - await ref.child('foo').onDisconnect().update({ - bar: value, - }); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - const snapshot = await ref.child('foo').once('value'); - snapshot.val().should.eql( - jet.contextify({ - bar: value, - }), - ); - }); - - it('calls back to the onComplete function', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH); - - // Set an initial value - await ref.set('foo'); - await ref.onDisconnect().update({ foo: 'bar' }, callback); - await firebase.database().goOffline(); - await firebase.database().goOnline(); - - callback.should.be.calledOnce(); - }); - }); - describe('modular', function () { afterEach(async function () { const { getDatabase, goOnline } = databaseModular; diff --git a/packages/database/e2e/query/endAt.e2e.js b/packages/database/e2e/query/endAt.e2e.js index 9dac730148..bd609a3c2e 100644 --- a/packages/database/e2e/query/endAt.e2e.js +++ b/packages/database/e2e/query/endAt.e2e.js @@ -28,124 +28,6 @@ describe('database().ref().endAt()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if an value is undefined', async function () { - try { - await firebase.database().ref().endAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' must be a number, string, boolean or null value"); - return Promise.resolve(); - } - }); - - it('throws if an key is not a string', async function () { - try { - await firebase.database().ref().endAt('foo', 1234); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'key' must be a string value if defined"); - return Promise.resolve(); - } - }); - - it('throws if a ending point has already been set', async function () { - try { - await firebase.database().ref().equalTo('foo').endAt('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Ending point was already set (by another call to endAt or equalTo)', - ); - return Promise.resolve(); - } - }); - - it('throws if ordering by key and the key param is set', async function () { - try { - await firebase.database().ref().orderByKey('foo').endAt('foo', 'bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'When ordering by key, you may only pass a value argument to startAt(), endAt(), or equalTo()', - ); - return Promise.resolve(); - } - }); - - it('throws if ordering by key and the value param is not a string', async function () { - try { - await firebase.database().ref().orderByKey('foo').endAt(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'When ordering by key, the value of startAt(), endAt(), or equalTo() must be a string', - ); - return Promise.resolve(); - } - }); - - it('throws if ordering by priority and the value param is not priority type', async function () { - try { - await firebase.database().ref().orderByPriority().endAt(true); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'When ordering by priority, the first value of startAt(), endAt(), or equalTo() must be a valid priority value (null, a number, or a string)', - ); - return Promise.resolve(); - } - }); - - it('snapshot value returns all when no ordering modifier is applied', async function () { - const ref = firebase.database().ref(TEST_PATH); - - await ref.set({ - a: 1, - b: 2, - c: 3, - d: 4, - }); - - const expected = ['a', 'b', 'c', 'd']; - - const snapshot = await ref.endAt(2).once('value'); - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - }); - - it('ends at the correct value', async function () { - const ref = firebase.database().ref(TEST_PATH); - - await ref.set({ - a: 1, - b: 2, - c: 3, - d: 4, - }); - - const snapshot = await ref.orderByValue().endAt(2).once('value'); - - const expected = ['a', 'b']; - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - }); - }); - describe('modular', function () { it('throws if an value is undefined', async function () { const { getDatabase, ref, query, endAt } = databaseModular; diff --git a/packages/database/e2e/query/equalTo.e2e.js b/packages/database/e2e/query/equalTo.e2e.js index da25cfb037..1096c72c5a 100644 --- a/packages/database/e2e/query/equalTo.e2e.js +++ b/packages/database/e2e/query/equalTo.e2e.js @@ -28,96 +28,6 @@ describe('database().ref().equalTo()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if value is not a valid type', async function () { - try { - await firebase.database().ref().equalTo({ foo: 'bar' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' must be a number, string, boolean or null value"); - return Promise.resolve(); - } - }); - - it('throws if key is not a string', async function () { - try { - await firebase.database().ref().equalTo('bar', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'key' must be a string value if defined"); - return Promise.resolve(); - } - }); - - it('throws if a starting point has already been set', async function () { - try { - await firebase.database().ref().startAt('foo').equalTo('bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Starting point was already set (by another call to startAt or equalTo)', - ); - return Promise.resolve(); - } - }); - - it('throws if a ending point has already been set', async function () { - try { - await firebase.database().ref().endAt('foo').equalTo('bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Ending point was already set (by another call to endAt or equalTo)', - ); - return Promise.resolve(); - } - }); - - it('snapshot value is null when no ordering modifier is applied', async function () { - const ref = firebase.database().ref(TEST_PATH); - - await ref.set({ - a: 1, - b: 2, - c: 3, - d: 4, - }); - - const snapshot = await ref.equalTo(2).once('value'); - should.equal(snapshot.val(), null); - }); - - it('returns the correct equal to values', async function () { - const ref = firebase.database().ref(TEST_PATH); - - await ref.set({ - a: 1, - b: 2, - c: 3, - d: 4, - e: 2, - }); - - const snapshot = await ref.orderByValue().equalTo(2).once('value'); - - const expected = ['b', 'e']; - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - }); - }); - describe('modular', function () { it('throws if value is not a valid type', async function () { const { getDatabase, ref, equalTo, query } = databaseModular; diff --git a/packages/database/e2e/query/isEqual.e2e.js b/packages/database/e2e/query/isEqual.e2e.js index 932ff5997a..eca317b915 100644 --- a/packages/database/e2e/query/isEqual.e2e.js +++ b/packages/database/e2e/query/isEqual.e2e.js @@ -16,43 +16,44 @@ */ describe('database().ref().isEqual()', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); + describe('modular', function () { + it('throws if limit other param is not a query instance', async function () { + const { getDatabase, ref } = databaseModular; - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); + try { + ref(getDatabase()).isEqual('foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'other' must be an instance of Query."); + return Promise.resolve(); + } + }); - it('throws if limit other param is not a query instance', async function () { - try { - await firebase.database().ref().isEqual('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' must be an instance of Query."); - return Promise.resolve(); - } - }); + it('returns true if the query is the same instance', async function () { + const { getDatabase, ref } = databaseModular; - it('returns true if the query is the same instance', async function () { - const query = await firebase.database().ref(); - const same = query.isEqual(query); - same.should.eql(true); - }); + const dbRef = ref(getDatabase()); + const same = dbRef.isEqual(dbRef); + same.should.eql(true); + }); - it('returns false if the query is different', async function () { - const query = await firebase.database().ref(); - const other = await firebase.database().ref().limitToLast(2); - const same = query.isEqual(other); - same.should.eql(false); - }); + it('returns false if the query is different', async function () { + const { getDatabase, ref, limitToLast, query } = databaseModular; + + const dbRef = ref(getDatabase()); + const other = query(dbRef, limitToLast(2)); + const same = dbRef.isEqual(other); + same.should.eql(false); + }); + + it('returns true if the query is created differently', async function () { + const { getDatabase, ref, limitToFirst, orderByChild, query } = databaseModular; - it('returns true if the query is created differently', async function () { - const query = await firebase.database().ref().limitToFirst(1).orderByChild('foo'); - const other = await firebase.database().ref().orderByChild('foo').limitToFirst(1); - const same = query.isEqual(other); - same.should.eql(true); + const dbRef = ref(getDatabase()); + const dbQuery = query(dbRef, limitToFirst(1), orderByChild('foo')); + const other = query(dbRef, orderByChild('foo'), limitToFirst(1)); + const same = dbQuery.isEqual(other); + same.should.eql(true); + }); }); }); diff --git a/packages/database/e2e/query/keepSynced.e2e.js b/packages/database/e2e/query/keepSynced.e2e.js index d48b817bb6..4f793ab1fe 100644 --- a/packages/database/e2e/query/keepSynced.e2e.js +++ b/packages/database/e2e/query/keepSynced.e2e.js @@ -16,46 +16,6 @@ */ describe('database().ref().keepSynced()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if bool is not a valid type', async function () { - try { - await firebase.database().ref().keepSynced('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'bool' value must be a boolean value."); - return Promise.resolve(); - } - }); - - it('toggles keepSynced on and off without throwing on supported platforms', async function () { - if (Platform.other) return; - const ref = firebase.database().ref('noop').orderByValue(); - await ref.keepSynced(true); - await ref.keepSynced(false); - }); - - it('keepSynced throws on unsupported platforms', async function () { - if (!Platform.other) return; - try { - await firebase.database().ref('noop').orderByValue().keepSynced(true); - throw new Error('did not throw'); - } catch (error) { - error.code.should.containEql('unsupported'); - error.message.should.containEql('This operation is not supported on this environment.'); - } - }); - }); - describe('modular', function () { it('throws if bool is not a valid type', async function () { const { getDatabase, ref, keepSynced } = databaseModular; diff --git a/packages/database/e2e/query/limitToFirst.e2e.js b/packages/database/e2e/query/limitToFirst.e2e.js index c308c0c957..a970ed2ff4 100644 --- a/packages/database/e2e/query/limitToFirst.e2e.js +++ b/packages/database/e2e/query/limitToFirst.e2e.js @@ -28,101 +28,6 @@ describe('database().ref().limitToFirst()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if limit is invalid', async function () { - try { - await firebase.database().ref().limitToFirst('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'limit' must be a positive integer value"); - return Promise.resolve(); - } - }); - - it('throws if limit has already been set', async function () { - try { - await firebase.database().ref().limitToLast(2).limitToFirst(3); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Limit was already set (by another call to limitToFirst, or limitToLast)', - ); - return Promise.resolve(); - } - }); - - it('returns a limited array data set', async function () { - const ref = firebase.database().ref(`${TEST_PATH}`); - - const initial = { - 0: 'foo', - 1: 'bar', - 2: 'baz', - }; - - await ref.set(initial); - - return ref - .limitToFirst(2) - .once('value') - .then(snapshot => { - snapshot.val().should.eql(jet.contextify(['foo', 'bar'])); - return Promise.resolve(); - }); - }); - - it('returns a limited object data set', async function () { - const ref = firebase.database().ref(`${TEST_PATH}`); - - const initial = { - a: 'foo', - b: 'bar', - c: 'baz', - }; - - await ref.set(initial); - - return ref - .limitToFirst(2) - .once('value') - .then(snapshot => { - snapshot.val().should.eql( - jet.contextify({ - a: 'foo', - b: 'bar', - }), - ); - return Promise.resolve(); - }); - }); - - it('returns a null value when not possible to limit', async function () { - const ref = firebase.database().ref(`${TEST_PATH}`); - - const initial = 'foo'; - - await ref.set(initial); - - return ref - .limitToFirst(2) - .once('value') - .then(snapshot => { - should.equal(snapshot.val(), null); - return Promise.resolve(); - }); - }); - }); - describe('modular', function () { it('throws if limit is invalid', async function () { const { getDatabase, ref, limitToFirst, query } = databaseModular; diff --git a/packages/database/e2e/query/limitToLast.e2e.js b/packages/database/e2e/query/limitToLast.e2e.js index 72550f4e75..96215ea183 100644 --- a/packages/database/e2e/query/limitToLast.e2e.js +++ b/packages/database/e2e/query/limitToLast.e2e.js @@ -28,101 +28,6 @@ describe('database().ref().limitToLast()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if limit is invalid', async function () { - try { - await firebase.database().ref().limitToLast('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'limit' must be a positive integer value"); - return Promise.resolve(); - } - }); - - it('throws if limit has already been set', async function () { - try { - await firebase.database().ref().limitToFirst(3).limitToLast(2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Limit was already set (by another call to limitToFirst, or limitToLast)', - ); - return Promise.resolve(); - } - }); - - it('returns a limited array data set', async function () { - const ref = firebase.database().ref(`${TEST_PATH}`); - - const initial = { - 0: 'foo', - 1: 'bar', - 2: 'baz', - }; - - await ref.set(initial); - - return ref - .limitToLast(2) - .once('value') - .then(snapshot => { - snapshot.val().should.eql(jet.contextify([null, 'bar', 'baz'])); - return Promise.resolve(); - }); - }); - - it('returns a limited object data set', async function () { - const ref = firebase.database().ref(`${TEST_PATH}`); - - const initial = { - a: 'foo', - b: 'bar', - c: 'baz', - }; - - await ref.set(initial); - - return ref - .limitToLast(2) - .once('value') - .then(snapshot => { - snapshot.val().should.eql( - jet.contextify({ - b: 'bar', - c: 'baz', - }), - ); - return Promise.resolve(); - }); - }); - - it('returns a null value when not possible to limit', async function () { - const ref = firebase.database().ref(`${TEST_PATH}`); - - const initial = 'foo'; - - await ref.set(initial); - - return ref - .limitToFirst(2) - .once('value') - .then(snapshot => { - should.equal(snapshot.val(), null); - return Promise.resolve(); - }); - }); - }); - describe('modular', function () { it('throws if limit is invalid', async function () { const { getDatabase, ref, limitToLast, query } = databaseModular; diff --git a/packages/database/e2e/query/on.e2e.js b/packages/database/e2e/query/on.e2e.js index 40a38fd475..0ef6781af5 100644 --- a/packages/database/e2e/query/on.e2e.js +++ b/packages/database/e2e/query/on.e2e.js @@ -26,244 +26,216 @@ const { const TEST_PATH = `${PATH}/on`; describe('database().ref().on()', function () { - before(async function () { - await seed(TEST_PATH); - }); - - after(async function () { - await wipe(TEST_PATH); - }); - - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if event type is invalid', async function () { - try { - await firebase.database().ref().on('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'eventType' must be one of"); - return Promise.resolve(); - } - }); - - it('throws if callback is not a function', async function () { - try { - await firebase.database().ref().on('value', 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'callback' must be a function"); - return Promise.resolve(); - } - }); - - it('throws if cancel callback is not a function', async function () { - try { - await firebase - .database() - .ref() - .on('value', () => {}, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'cancelCallbackOrContext' must be a function or object"); - return Promise.resolve(); - } - }); - - it('throws if context is not an object', async function () { - try { - await firebase - .database() - .ref() - .on( - 'value', - () => {}, - () => {}, - 'foo', - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'context' must be an object."); - return Promise.resolve(); - } - }); - - it('should callback with an initial value', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/init`); - ref.on('value', $ => { - callback($.val()); + describe('modular', function () { + before(async function () { + await seed(TEST_PATH); }); - const value = Date.now(); - await ref.set(value); - await Utils.spyToBeCalledOnceAsync(callback, 5000); - callback.should.be.calledWith(value); + after(async function () { + await wipe(TEST_PATH); + }); - ref.off('value'); - }); + it('throws if callback is not a function', async function () { + const { getDatabase, ref, onValue } = databaseModular; - it('should callback multiple times when the value changes', async function () { - const callback = sinon.spy(); - const date = Date.now(); - const ref = firebase.database().ref(`${TEST_PATH}/multi-change/${date}`); - ref.on('value', $ => { - callback($.val()); + try { + onValue(ref(getDatabase()), 'foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'callback' must be a function"); + return Promise.resolve(); + } }); - // onValue *should* be async, it uses bridge to add native listener - // That would be a breaking API change so wait for initial callback - await Utils.spyToBeCalledOnceAsync(callback, 1000); - callback.should.be.calledWith(null); // initial callback pre-set - await ref.set('foo'); - await ref.set('bar'); - await Utils.spyToBeCalledTimesAsync(callback, 3); - await ref.off('value'); - callback.getCall(1).args[0].should.equal('foo'); - callback.getCall(2).args[0].should.equal('bar'); - }); - - // the cancelCallback is never called for ref.on but ref.once works? - it('should cancel when something goes wrong', async function () { - const successCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - const ref = firebase.database().ref('nope'); - ref.on( - 'value', - $ => { - successCallback($.val()); - }, - error => { - error.message.should.containEql( - "Client doesn't have permission to access the desired data", - ); - cancelCallback(); - }, - ); - await Utils.spyToBeCalledOnceAsync(cancelCallback); - ref.off('value'); - successCallback.should.be.callCount(0); - }); + it('throws if cancel callback is not a function', async function () { + const { getDatabase, ref, onValue } = databaseModular; - it('subscribe to child added events', async function () { - const successCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - const date = Date.now(); - const ref = firebase.database().ref(`${TEST_PATH}/childAdded/${date}`); + try { + onValue(ref(getDatabase()), () => {}, 'foo'); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'cancelCallbackOrContext' must be a function or object"); + return Promise.resolve(); + } + }); - ref.on( - 'child_added', - $ => { - successCallback($.val()); - }, - () => { - cancelCallback(); - }, - ); + it('should callback with an initial value', async function () { + const { getDatabase, ref, set, onValue } = databaseModular; + const dbRef = ref(getDatabase(), `${TEST_PATH}/init`); - await waitForNativeDbListenerRegistration(`${TEST_PATH}/childAdded/${date}`); - await ref.child('child1').set('foo'); - await ref.child('child2').set('bar'); - await Utils.spyToBeCalledTimesAsync(successCallback, 2); - ref.off('child_added'); - successCallback.getCall(0).args[0].should.equal('foo'); - successCallback.getCall(1).args[0].should.equal('bar'); - cancelCallback.should.be.callCount(0); - }); + const callback = sinon.spy(); + const unsubscribe = onValue(dbRef, $ => { + callback($.val()); + }); - it('subscribe to child changed events', async function () { - if (Platform.other) { - this.skip('Errors on JS SDK about a missing index.'); - return; - } - const successCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - const date = Date.now(); - const ref = firebase.database().ref(`${TEST_PATH}/childChanged/${date}`); - const child = ref.child('changeme'); - await child.set('foo'); + const value = Date.now(); + await set(dbRef, value); + await Utils.spyToBeCalledOnceAsync(callback, 5000); + callback.should.be.calledWith(value); - ref.on( - 'child_changed', - $ => { - successCallback($.val()); - }, - () => { - cancelCallback(); - }, - ); - await waitForNativeDbListenerReady(`${TEST_PATH}/childChanged/${date}/changeme`, 'foo'); + unsubscribe(); + }); - const value1 = Date.now(); - const value2 = Date.now() + 123; + it('should callback multiple times when the value changes', async function () { + const { getDatabase, ref, set, onValue } = databaseModular; + const callback = sinon.spy(); + const date = Date.now(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/multi-change/${date}`); + const unsubscribe = onValue(dbRef, $ => { + callback($.val()); + }); + // onValue *should* be async, it uses bridge to add native listener + // That would be a breaking API change so wait for initial callback + await Utils.spyToBeCalledOnceAsync(callback, 1000); + callback.should.be.calledWith(null); // initial callback pre-set + await set(dbRef, 'foo'); + await set(dbRef, 'bar'); + await Utils.spyToBeCalledTimesAsync(callback, 3); + unsubscribe(); + callback.getCall(1).args[0].should.equal('foo'); + callback.getCall(2).args[0].should.equal('bar'); + }); - await child.set(value1); - await child.set(value2); - await Utils.spyToBeCalledTimesAsync(successCallback, 2); - ref.off('child_changed'); - successCallback.getCall(0).args[0].should.equal(value1); - successCallback.getCall(1).args[0].should.equal(value2); - cancelCallback.should.be.callCount(0); - }); + // the cancelCallback is never called for ref.on but ref.once works? + it('should cancel when something goes wrong', async function () { + const { getDatabase, ref, onValue } = databaseModular; + const successCallback = sinon.spy(); + const cancelCallback = sinon.spy(); + const dbRef = ref(getDatabase(), 'nope'); + + const unsubscribe = onValue( + dbRef, + $ => { + successCallback($.val()); + }, + error => { + error.message.should.containEql( + "Client doesn't have permission to access the desired data", + ); + cancelCallback(); + }, + ); + await Utils.spyToBeCalledOnceAsync(cancelCallback); + unsubscribe(); + successCallback.should.be.callCount(0); + }); - it('subscribe to child removed events', async function () { - const successCallback = sinon.spy(); - const cancelCallback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/childRemoved`); - const child = ref.child('removeme'); - await child.set('foo'); - ref.on( - 'child_removed', - $ => { - successCallback($.val()); - }, - () => { - cancelCallback(); - }, - ); - await waitForNativeDbListenerReady(`${TEST_PATH}/childRemoved/removeme`, 'foo'); - await child.remove(); - await Utils.spyToBeCalledOnceAsync(successCallback, 5000); - ref.off('child_removed'); - successCallback.getCall(0).args[0].should.equal('foo'); - cancelCallback.should.be.callCount(0); - }); + it('subscribe to child added events', async function () { + const { getDatabase, ref, set, child, onChildAdded } = databaseModular; + const successCallback = sinon.spy(); + const cancelCallback = sinon.spy(); + const date = Date.now(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childAdded/${date}`); + + const unsubscribe = onChildAdded( + dbRef, + $ => { + successCallback($.val()); + }, + () => { + cancelCallback(); + }, + ); + + await waitForNativeDbListenerRegistration(`${TEST_PATH}/childAdded/${date}`); + await set(child(dbRef, 'child1'), 'foo'); + await set(child(dbRef, 'child2'), 'bar'); + await Utils.spyToBeCalledTimesAsync(successCallback, 2); + unsubscribe(); + successCallback.getCall(0).args[0].should.equal('foo'); + successCallback.getCall(1).args[0].should.equal('bar'); + cancelCallback.should.be.callCount(0); + }); - it('subscribe to child moved events', async function () { - if (Platform.other) { - this.skip('Errors on JS SDK about a missing index.'); - return; - } - const callback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/childMoved`); - const orderedRef = ref.orderByChild('nuggets'); + it('subscribe to child changed events', async function () { + if (Platform.other) { + this.skip('Errors on JS SDK about a missing index.'); + return; + } + const { getDatabase, ref, set, child, onChildChanged } = databaseModular; + const successCallback = sinon.spy(); + const cancelCallback = sinon.spy(); + const date = Date.now(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childChanged/${date}`); + const childRef = child(dbRef, 'changeme'); + await set(childRef, 'foo'); + + const unsubscribe = onChildChanged( + dbRef, + $ => { + successCallback($.val()); + }, + () => { + cancelCallback(); + }, + ); + await waitForNativeDbListenerReady(`${TEST_PATH}/childChanged/${date}/changeme`, 'foo'); + + const value1 = Date.now(); + const value2 = Date.now() + 123; + + await set(childRef, value1); + await set(childRef, value2); + await Utils.spyToBeCalledTimesAsync(successCallback, 2); + unsubscribe(); + successCallback.getCall(0).args[0].should.equal(value1); + successCallback.getCall(1).args[0].should.equal(value2); + cancelCallback.should.be.callCount(0); + }); - const initial = { - alex: { nuggets: 60 }, - rob: { nuggets: 56 }, - vassili: { nuggets: 55.5 }, - tony: { nuggets: 52 }, - greg: { nuggets: 52 }, - }; + it('subscribe to child removed events', async function () { + const { getDatabase, ref, set, child, remove, onChildRemoved } = databaseModular; + const successCallback = sinon.spy(); + const cancelCallback = sinon.spy(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childRemoved`); + const childRef = child(dbRef, 'removeme'); + await set(childRef, 'foo'); + const unsubscribe = onChildRemoved( + dbRef, + $ => { + successCallback($.val()); + }, + () => { + cancelCallback(); + }, + ); + await waitForNativeDbListenerReady(`${TEST_PATH}/childRemoved/removeme`, 'foo'); + await remove(childRef); + await Utils.spyToBeCalledOnceAsync(successCallback, 5000); + unsubscribe(); + successCallback.getCall(0).args[0].should.equal('foo'); + cancelCallback.should.be.callCount(0); + }); - orderedRef.on('child_moved', $ => { - // console.log($); - callback($.val()); + it('subscribe to child moved events', async function () { + if (Platform.other) { + this.skip('Errors on JS SDK about a missing index.'); + return; + } + const { getDatabase, ref, set, child, onChildMoved, query, orderByChild } = databaseModular; + const callback = sinon.spy(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childMoved`); + const orderedRef = query(dbRef, orderByChild('nuggets')); + + const initial = { + alex: { nuggets: 60 }, + rob: { nuggets: 56 }, + vassili: { nuggets: 55.5 }, + tony: { nuggets: 52 }, + greg: { nuggets: 52 }, + }; + + const unsubscribe = onChildMoved(orderedRef, $ => { + callback($.val()); + }); + await waitForNativeDbListenerRegistration(`${TEST_PATH}/childMoved`); + await set(dbRef, initial); + await set(child(dbRef, 'greg/nuggets'), 57); + await set(child(dbRef, 'rob/nuggets'), 61); + await Utils.spyToBeCalledTimesAsync(callback, 2); + unsubscribe(); + callback.getCall(0).args[0].should.be.eql(jet.contextify({ nuggets: 57 })); + callback.getCall(1).args[0].should.be.eql(jet.contextify({ nuggets: 61 })); }); - await waitForNativeDbListenerRegistration(`${TEST_PATH}/childMoved`); - await ref.set(initial); - await ref.child('greg/nuggets').set(57); - await ref.child('rob/nuggets').set(61); - await Utils.spyToBeCalledTimesAsync(callback, 2); - ref.off('child_moved'); - callback.getCall(0).args[0].should.be.eql(jet.contextify({ nuggets: 57 })); - callback.getCall(1).args[0].should.be.eql(jet.contextify({ nuggets: 61 })); }); }); diff --git a/packages/database/e2e/query/once.e2e.js b/packages/database/e2e/query/once.e2e.js index b9c34e8bf8..fa25e900e6 100644 --- a/packages/database/e2e/query/once.e2e.js +++ b/packages/database/e2e/query/once.e2e.js @@ -27,188 +27,153 @@ const { const TEST_PATH = `${PATH}/once`; describe('database().ref().once()', function () { - before(function () { - return seed(TEST_PATH); - }); - - after(function () { - return wipe(TEST_PATH); - }); - - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if event type is invalid', async function () { - try { - await firebase.database().ref().once('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'eventType' must be one of"); - return Promise.resolve(); - } - }); - - it('throws if success callback is not a function', async function () { - try { - await firebase.database().ref().once('value', 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'successCallBack' must be a function"); - return Promise.resolve(); - } - }); - - it('throws if failure callback is not a function', async function () { - try { - await firebase - .database() - .ref() - .once('value', () => {}, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'failureCallbackOrContext' must be a function or context"); - return Promise.resolve(); - } - }); - - it('throws if context is not an object', async function () { - try { - await firebase - .database() - .ref() - .once( - 'value', - () => {}, - () => {}, - 'foo', - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'context' must be a context object."); - return Promise.resolve(); - } - }); - - it('returns a promise', async function () { - const ref = firebase.database().ref('tests/types/number'); - const returnValue = ref.once('value'); - returnValue.should.be.Promise(); - }); - - it('resolves with the correct values', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/types`); - - await Promise.all( - Object.keys(CONTENT.TYPES).map(async key => { - const value = CONTENT.TYPES[key]; - const snapsnot = await ref.child(key).once('value'); - snapsnot.val().should.eql(jet.contextify(value)); - }), - ); - }); - - it('is is called when the value is changed', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/types/number`); - ref.once('value').then(callback); - await ref.set(1337); - await Utils.spyToBeCalledOnceAsync(callback); - }); - - it('errors if permission denied', async function () { - const ref = firebase.database().ref('nope'); - try { - await ref.once('value'); - return Promise.reject(new Error('No permission denied error')); - } catch (error) { - error.code.includes('database/permission-denied').should.be.true(); - return Promise.resolve(); - } - }); - - it('it calls when a child is added', async function () { - const value = Date.now(); - const callback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/childAdded`); - ref - .once('child_added') - .then($ => callback($.val())) - .catch(e => callback(e)); - await waitForNativeDbListenerRegistration(`${TEST_PATH}/childAdded`); - await ref.child('foo').set(value); - await Utils.spyToBeCalledOnceAsync(callback, 5000); - callback.should.be.calledWith(value); - }); - - it('resolves when a child is changed', async function () { - const callbackAdd = sinon.spy(); - const callbackChange = sinon.spy(); - const date = Date.now(); - const ref = firebase.database().ref(`${TEST_PATH}/childChanged/${date}`); - - ref.once('child_added').then($ => callbackAdd($.val())); - await waitForNativeDbListenerRegistration(`${TEST_PATH}/childChanged/${date}`); - await ref.child('foo').set(1); - await Utils.spyToBeCalledOnceAsync(callbackAdd, 10000); - ref.once('child_changed').then($ => callbackChange($.val())); - await waitForNativeDbListenerReady(`${TEST_PATH}/childChanged/${date}/foo`, 1); - await ref.child('foo').set(2); - await Utils.spyToBeCalledOnceAsync(callbackChange, 10000); - callbackChange.should.be.calledWith(2); - }); - - it('resolves when a child is removed', async function () { - const callbackAdd = sinon.spy(); - const callbackRemove = sinon.spy(); - const date = Date.now(); - const ref = firebase.database().ref(`${TEST_PATH}/childRemoved/${date}`); - ref.once('child_added').then($ => callbackAdd($.val())); - await waitForNativeDbListenerRegistration(`${TEST_PATH}/childRemoved/${date}`); - const child = ref.child('removeme'); - await child.set('foo'); - await Utils.spyToBeCalledOnceAsync(callbackAdd, 10000); - ref - .once('child_removed') - .then($ => callbackRemove($.val())) - .catch(e => callback(e)); - await waitForNativeDbListenerReady(`${TEST_PATH}/childRemoved/${date}/removeme`, 'foo'); - await child.remove(); - await Utils.spyToBeCalledOnceAsync(callbackRemove, 10000); - callbackRemove.should.be.calledWith('foo'); - }); - - // https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/database/test/order_by.test.ts#L104 - it('resolves when a child is moved', async function () { - if (Platform.other) { - this.skip('Errors on JS SDK about a missing index.'); - return; - } - const callback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/childMoved`); - const orderedRef = ref.orderByChild('nuggets'); - - const initial = { - alex: { nuggets: 60 }, - rob: { nuggets: 56 }, - vassili: { nuggets: 55.5 }, - tony: { nuggets: 52 }, - greg: { nuggets: 52 }, - }; - orderedRef - .once('child_moved') - .then($ => callback($.val())) - .catch(e => callback(e)); - await waitForNativeDbListenerRegistration(`${TEST_PATH}/childMoved`); - await ref.set(initial); - await ref.child('greg/nuggets').set(57); - await Utils.spyToBeCalledOnceAsync(callback, 5000); - callback.should.be.calledWith({ nuggets: 57 }); + describe('modular', function () { + before(function () { + return seed(TEST_PATH); + }); + + after(function () { + return wipe(TEST_PATH); + }); + + it('returns a promise', async function () { + const { getDatabase, ref, get } = databaseModular; + + const dbRef = ref(getDatabase(), 'tests/types/number'); + const returnValue = get(dbRef); + returnValue.should.be.Promise(); + }); + + it('resolves with the correct values', async function () { + const { getDatabase, ref, child, get } = databaseModular; + + const dbRef = ref(getDatabase(), `${TEST_PATH}/types`); + + await Promise.all( + Object.keys(CONTENT.TYPES).map(async key => { + const value = CONTENT.TYPES[key]; + const snapsnot = await get(child(dbRef, key)); + snapsnot.val().should.eql(jet.contextify(value)); + }), + ); + }); + + it('is is called when the value is changed', async function () { + const { getDatabase, ref, set, onValue } = databaseModular; + const callback = sinon.spy(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/types/number`); + onValue( + dbRef, + $ => { + callback($.val()); + }, + { onlyOnce: true }, + ); + await set(dbRef, 1337); + await Utils.spyToBeCalledOnceAsync(callback); + }); + + it('errors if permission denied', async function () { + const { getDatabase, ref, get } = databaseModular; + + const dbRef = ref(getDatabase(), 'nope'); + try { + await get(dbRef); + return Promise.reject(new Error('No permission denied error')); + } catch (error) { + error.code.includes('database/permission-denied').should.be.true(); + return Promise.resolve(); + } + }); + + it('it calls when a child is added', async function () { + const { getDatabase, ref, set, child, onChildAdded } = databaseModular; + const value = Date.now(); + const callback = sinon.spy(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childAdded`); + onChildAdded( + dbRef, + $ => callback($.val()), + error => callback(error), + { onlyOnce: true }, + ); + await waitForNativeDbListenerRegistration(`${TEST_PATH}/childAdded`); + await set(child(dbRef, 'foo'), value); + await Utils.spyToBeCalledOnceAsync(callback, 5000); + callback.should.be.calledWith(value); + }); + + it('resolves when a child is changed', async function () { + const { getDatabase, ref, set, child, onChildAdded, onChildChanged } = databaseModular; + const callbackAdd = sinon.spy(); + const callbackChange = sinon.spy(); + const date = Date.now(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childChanged/${date}`); + + onChildAdded(dbRef, $ => callbackAdd($.val()), { onlyOnce: true }); + await waitForNativeDbListenerRegistration(`${TEST_PATH}/childChanged/${date}`); + await set(child(dbRef, 'foo'), 1); + await Utils.spyToBeCalledOnceAsync(callbackAdd, 10000); + onChildChanged(dbRef, $ => callbackChange($.val()), { onlyOnce: true }); + await waitForNativeDbListenerReady(`${TEST_PATH}/childChanged/${date}/foo`, 1); + await set(child(dbRef, 'foo'), 2); + await Utils.spyToBeCalledOnceAsync(callbackChange, 10000); + callbackChange.should.be.calledWith(2); + }); + + it('resolves when a child is removed', async function () { + const { getDatabase, ref, set, child, remove, onChildAdded, onChildRemoved } = + databaseModular; + const callbackAdd = sinon.spy(); + const callbackRemove = sinon.spy(); + const date = Date.now(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childRemoved/${date}`); + onChildAdded(dbRef, $ => callbackAdd($.val()), { onlyOnce: true }); + await waitForNativeDbListenerRegistration(`${TEST_PATH}/childRemoved/${date}`); + const childRef = child(dbRef, 'removeme'); + await set(childRef, 'foo'); + await Utils.spyToBeCalledOnceAsync(callbackAdd, 10000); + onChildRemoved( + dbRef, + $ => callbackRemove($.val()), + error => callbackRemove(error), + { onlyOnce: true }, + ); + await waitForNativeDbListenerReady(`${TEST_PATH}/childRemoved/${date}/removeme`, 'foo'); + await remove(childRef); + await Utils.spyToBeCalledOnceAsync(callbackRemove, 10000); + callbackRemove.should.be.calledWith('foo'); + }); + + // https://github.com/firebase/firebase-js-sdk/blob/6b53e0058483c9002d2fe56119f86fc9fb96b56c/packages/database/test/order_by.test.ts#L104 + it('resolves when a child is moved', async function () { + if (Platform.other) { + this.skip('Errors on JS SDK about a missing index.'); + return; + } + const { getDatabase, ref, set, child, onChildMoved, query, orderByChild } = databaseModular; + const callback = sinon.spy(); + const dbRef = ref(getDatabase(), `${TEST_PATH}/childMoved`); + const orderedRef = query(dbRef, orderByChild('nuggets')); + + const initial = { + alex: { nuggets: 60 }, + rob: { nuggets: 56 }, + vassili: { nuggets: 55.5 }, + tony: { nuggets: 52 }, + greg: { nuggets: 52 }, + }; + onChildMoved( + orderedRef, + $ => callback($.val()), + error => callback(error), + { onlyOnce: true }, + ); + await waitForNativeDbListenerRegistration(`${TEST_PATH}/childMoved`); + await set(dbRef, initial); + await set(child(dbRef, 'greg/nuggets'), 57); + await Utils.spyToBeCalledOnceAsync(callback, 5000); + callback.should.be.calledWith({ nuggets: 57 }); + }); }); }); diff --git a/packages/database/e2e/query/orderByChild.e2e.js b/packages/database/e2e/query/orderByChild.e2e.js index b7114c53f3..a1a6837de9 100644 --- a/packages/database/e2e/query/orderByChild.e2e.js +++ b/packages/database/e2e/query/orderByChild.e2e.js @@ -28,66 +28,6 @@ describe('database().ref().orderByChild()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if path is not a string value', async function () { - try { - await firebase.database().ref().orderByChild({ foo: 'bar' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'path' must be a string value"); - return Promise.resolve(); - } - }); - - it('throws if path is an empty path', async function () { - try { - await firebase.database().ref().orderByChild('/'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'path' cannot be empty. Use orderByValue instead"); - return Promise.resolve(); - } - }); - - it('throws if an orderBy call has already been set', async function () { - try { - await firebase.database().ref().orderByKey().orderByChild('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You can't combine multiple orderBy calls"); - return Promise.resolve(); - } - }); - - it('order by a child value', async function () { - const ref = firebase.database().ref(TEST_PATH); - - try { - const snapshot = await ref.child('query').orderByChild('number').once('value'); - - const expected = ['b', 'c', 'a']; - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - - return Promise.resolve(); - } catch (error) { - throw error; - } - }); - }); - describe('modular', function () { it('throws if path is not a string value', async function () { const { getDatabase, ref, orderByChild, query } = databaseModular; diff --git a/packages/database/e2e/query/orderByKey.e2e.js b/packages/database/e2e/query/orderByKey.e2e.js index 91e361f5f3..1a9d35ce9b 100644 --- a/packages/database/e2e/query/orderByKey.e2e.js +++ b/packages/database/e2e/query/orderByKey.e2e.js @@ -28,46 +28,6 @@ describe('database().ref().orderByKey()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if an orderBy call has already been set', async function () { - try { - await firebase.database().ref().orderByChild('foo').orderByKey(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You can't combine multiple orderBy calls"); - return Promise.resolve(); - } - }); - - it('order by a key', async function () { - const ref = firebase.database().ref(TEST_PATH); - - try { - const snapshot = await ref.child('query').orderByKey().once('value'); - - const expected = ['a', 'b', 'c']; - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - - return Promise.resolve(); - } catch (error) { - throw error; - } - }); - }); - describe('modular', function () { it('throws if an orderBy call has already been set', async function () { const { getDatabase, ref, orderByChild, orderByKey, query } = databaseModular; diff --git a/packages/database/e2e/query/orderByPriority.e2e.js b/packages/database/e2e/query/orderByPriority.e2e.js index 4a02479b66..2732475b0b 100644 --- a/packages/database/e2e/query/orderByPriority.e2e.js +++ b/packages/database/e2e/query/orderByPriority.e2e.js @@ -28,52 +28,6 @@ describe('database().ref().orderByPriority()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if an orderBy call has already been set', async function () { - try { - await firebase.database().ref().orderByChild('foo').orderByPriority(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You can't combine multiple orderBy calls"); - return Promise.resolve(); - } - }); - - it('order by priority', async function () { - const ref = firebase.database().ref(TEST_PATH).child('query'); - - await Promise.all([ - ref.child('a').setPriority(2), - ref.child('b').setPriority(3), - ref.child('c').setPriority(1), - ]); - - try { - const snapshot = await ref.orderByPriority().once('value'); - - const expected = ['c', 'a', 'b']; - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - - return Promise.resolve(); - } catch (error) { - throw error; - } - }); - }); - describe('modular', function () { it('throws if an orderBy call has already been set', async function () { const { getDatabase, ref, orderByChild, orderByPriority, query } = databaseModular; diff --git a/packages/database/e2e/query/orderByValue.e2e.js b/packages/database/e2e/query/orderByValue.e2e.js index c4708c4b84..0c207ba3bd 100644 --- a/packages/database/e2e/query/orderByValue.e2e.js +++ b/packages/database/e2e/query/orderByValue.e2e.js @@ -28,52 +28,6 @@ describe('database().ref().orderByValue()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if an orderBy call has already been set', async function () { - try { - await firebase.database().ref().orderByChild('foo').orderByValue(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You can't combine multiple orderBy calls"); - return Promise.resolve(); - } - }); - - it('order by value', async function () { - const ref = firebase.database().ref(TEST_PATH).child('query'); - - await ref.set({ - a: 2, - b: 3, - c: 1, - }); - - try { - const snapshot = await ref.orderByValue().once('value'); - - const expected = ['c', 'a', 'b']; - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - - return Promise.resolve(); - } catch (error) { - throw error; - } - }); - }); - describe('modular', function () { it('throws if an orderBy call has already been set', async function () { const { getDatabase, ref, orderByChild, orderByValue, query } = databaseModular; diff --git a/packages/database/e2e/query/query.e2e.js b/packages/database/e2e/query/query.e2e.js index 03272ff36d..839805f3d2 100644 --- a/packages/database/e2e/query/query.e2e.js +++ b/packages/database/e2e/query/query.e2e.js @@ -15,31 +15,6 @@ */ describe('DatabaseQuery/DatabaseQueryModifiers', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('should not mutate previous queries (#2691)', async function () { - const queryBefore = firebase.database().ref(); - queryBefore._modifiers._modifiers.length.should.equal(0); - - const queryAfter = queryBefore.orderByChild('age'); - queryBefore._modifiers._modifiers.length.should.equal(0); - queryAfter._modifiers._modifiers.length.should.equal(1); - - const queryAfterAfter = queryAfter.equalTo(30); - queryAfter._modifiers._modifiers.length.should.equal(1); - queryAfterAfter._modifiers._modifiers.length.should.equal(3); // adds startAt endAt internally - }); - }); - describe('modular', function () { it('should not mutate previous queries (#2691)', async function () { const { getDatabase, ref, query, orderByChild, equalTo } = databaseModular; diff --git a/packages/database/e2e/query/startAt.e2e.js b/packages/database/e2e/query/startAt.e2e.js index bce383bb14..c198c08517 100644 --- a/packages/database/e2e/query/startAt.e2e.js +++ b/packages/database/e2e/query/startAt.e2e.js @@ -28,119 +28,6 @@ describe('database().ref().startAt()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if an value is undefined', async function () { - try { - await firebase.database().ref().startAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' must be a number, string, boolean or null value"); - return Promise.resolve(); - } - }); - - it('throws if an key is not a string', async function () { - try { - await firebase.database().ref().startAt('foo', 1234); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'key' must be a string value if defined"); - return Promise.resolve(); - } - }); - - it('throws if a starting point has already been set', async function () { - try { - await firebase.database().ref().equalTo('foo').startAt('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Starting point was already set (by another call to startAt or equalTo)', - ); - return Promise.resolve(); - } - }); - - it('throws if ordering by key and the key param is set', async function () { - try { - await firebase.database().ref().orderByKey('foo').startAt('foo', 'bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'When ordering by key, you may only pass a value argument to startAt(), endAt(), or equalTo()', - ); - return Promise.resolve(); - } - }); - - it('throws if ordering by key and the value param is not a string', async function () { - try { - await firebase.database().ref().orderByKey('foo').startAt(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'When ordering by key, the value of startAt(), endAt(), or equalTo() must be a string', - ); - return Promise.resolve(); - } - }); - - it('throws if ordering by priority and the value param is not priority type', async function () { - try { - await firebase.database().ref().orderByPriority().startAt(true); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'When ordering by priority, the first value of startAt(), endAt(), or equalTo() must be a valid priority value (null, a number, or a string)', - ); - return Promise.resolve(); - } - }); - - it('snapshot value is null when no ordering modifier is applied', async function () { - const ref = firebase.database().ref(TEST_PATH); - - await ref.set({ - a: 1, - b: 2, - c: 3, - d: 4, - }); - - const snapshot = await ref.startAt(2).once('value'); - should.equal(snapshot.val(), null); - }); - - it('starts at the correct value', async function () { - const ref = firebase.database().ref(TEST_PATH); - - await ref.set({ - a: 1, - b: 2, - c: 3, - d: 4, - }); - - const snapshot = await ref.orderByValue().startAt(2).once('value'); - - const expected = ['b', 'c', 'd']; - - snapshot.forEach((childSnapshot, i) => { - childSnapshot.key.should.eql(expected[i]); - }); - }); - }); - describe('modular', function () { it('throws if an value is undefined', async function () { const { getDatabase, ref, startAt, query } = databaseModular; diff --git a/packages/database/e2e/query/toJSON.e2e.js b/packages/database/e2e/query/toJSON.e2e.js index d7ccc9e511..d6fdc65fb5 100644 --- a/packages/database/e2e/query/toJSON.e2e.js +++ b/packages/database/e2e/query/toJSON.e2e.js @@ -16,24 +16,6 @@ */ describe('database().ref().toJSON()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns a string version of the current query path', async function () { - const res = firebase.database().ref('foo/bar/baz').toJSON(); - const expected = `${firebase.database()._customUrlOrRegion}/foo/bar/baz`; - should.equal(res, expected); - }); - }); - describe('modular', function () { it('returns a string version of the current query path', async function () { const { getDatabase, ref } = databaseModular; diff --git a/packages/database/e2e/reference/child.e2e.js b/packages/database/e2e/reference/child.e2e.js index 1c6bcf546f..6453f1d602 100644 --- a/packages/database/e2e/reference/child.e2e.js +++ b/packages/database/e2e/reference/child.e2e.js @@ -16,40 +16,6 @@ */ describe('database().ref().child()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if path is not a string', async function () { - try { - firebase.database().ref().child({ foo: 'bar' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'path' must be a string value"); - return Promise.resolve(); - } - }); - - it('throws if path is not a valid string', async function () { - try { - firebase.database().ref().child('$$$$$'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'firebase.database() Paths must be non-empty strings and can\'t contain ".", "#", "$", "[", or "]"', - ); - return Promise.resolve(); - } - }); - }); - describe('modular', function () { it('throws if path is not a string', async function () { const { getDatabase, ref, child } = databaseModular; diff --git a/packages/database/e2e/reference/key.e2e.js b/packages/database/e2e/reference/key.e2e.js index 33846bc67b..d1927f2283 100644 --- a/packages/database/e2e/reference/key.e2e.js +++ b/packages/database/e2e/reference/key.e2e.js @@ -16,30 +16,6 @@ */ describe('database().ref().key', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns null when no reference path is provides', function () { - const ref = firebase.database().ref(); - should.equal(ref.key, null); - }); - - it('return last token in reference path', function () { - const ref1 = firebase.database().ref('foo'); - const ref2 = firebase.database().ref('foo/bar/baz'); - ref1.key.should.equal('foo'); - ref2.key.should.equal('baz'); - }); - }); - describe('modular', function () { it('returns null when no reference path is provides', function () { const { getDatabase, ref } = databaseModular; diff --git a/packages/database/e2e/reference/onDisconnect.e2e.js b/packages/database/e2e/reference/onDisconnect.e2e.js index 5263212090..344c76d201 100644 --- a/packages/database/e2e/reference/onDisconnect.e2e.js +++ b/packages/database/e2e/reference/onDisconnect.e2e.js @@ -18,23 +18,6 @@ // See onDisconnect directory for specific tests describe('database().ref().onDisconnect()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns a new DatabaseOnDisconnect instance', function () { - const instance = firebase.database().ref().onDisconnect(); - instance.constructor.name.should.eql('DatabaseOnDisconnect'); - }); - }); - describe('modular', function () { it('returns a new DatabaseOnDisconnect instance', function () { const { getDatabase, ref, onDisconnect } = databaseModular; diff --git a/packages/database/e2e/reference/parent.e2e.js b/packages/database/e2e/reference/parent.e2e.js index a0f6570aa1..dbd38a347f 100644 --- a/packages/database/e2e/reference/parent.e2e.js +++ b/packages/database/e2e/reference/parent.e2e.js @@ -16,30 +16,6 @@ */ describe('database().ref().parent', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns null when no reference path is provides', function () { - const ref = firebase.database().ref(); - should.equal(ref.parent, null); - }); - - it('return last token in reference path', function () { - const ref1 = firebase.database().ref('/foo').parent; - const ref2 = firebase.database().ref('/foo/bar/baz').parent; - should.equal(ref1, null); - ref2.key.should.equal('bar'); - }); - }); - describe('modular', function () { it('returns null when no reference path is provides', function () { const { getDatabase, ref } = databaseModular; diff --git a/packages/database/e2e/reference/push.e2e.js b/packages/database/e2e/reference/push.e2e.js index 69f48d4f1c..62d3f62b19 100644 --- a/packages/database/e2e/reference/push.e2e.js +++ b/packages/database/e2e/reference/push.e2e.js @@ -20,92 +20,6 @@ const { PATH } = require('../helpers'); const TEST_PATH = `${PATH}/push`; describe('database().ref().push()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if on complete callback is not a function', function () { - try { - firebase.database().ref(TEST_PATH).push('foo', 'bar'); - return Promise.reject(new Error('Did not throw Error')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('returns a promise when no value is passed', function () { - const ref = firebase.database().ref(`${TEST_PATH}/boop`); - const pushed = ref.push(); - return pushed - .then(childRef => { - pushed.ref.parent.toString().should.eql(ref.toString()); - pushed.toString().should.eql(childRef.toString()); - return pushed.once('value'); - }) - .then(snap => { - should.equal(snap.val(), null); - snap.ref.toString().should.eql(pushed.toString()); - }); - }); - - it('returns a promise and sets the provided value', function () { - const ref = firebase.database().ref(`${TEST_PATH}/value`); - const pushed = ref.push(6); - return pushed - .then(childRef => { - pushed.ref.parent.toString().should.eql(ref.toString()); - pushed.toString().should.eql(childRef.toString()); - return pushed.once('value'); - }) - .then(snap => { - snap.val().should.equal(6); - snap.ref.toString().should.eql(pushed.toString()); - }); - }); - - it('returns a to the callback if provided once set', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(`${TEST_PATH}/callback`); - const value = Date.now(); - ref.push(value, () => { - callback(); - }); - await Utils.spyToBeCalledOnceAsync(callback); - }); - - it('throws if push errors', async function () { - const ref = firebase.database().ref('nope'); - return ref - .push('foo') - .then(() => { - throw new Error('Did not error'); - }) - .catch(error => { - error.code.should.equal('database/permission-denied'); - return Promise.resolve(); - }); - }); - - it('returns an error to the callback', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref('nope'); - ref.push('foo', error => { - error.code.should.equal('database/permission-denied'); - callback(); - }); - await Utils.spyToBeCalledOnceAsync(callback); - callback.should.be.calledOnce(); - }); - }); - describe('modular', function () { it('returns a Promise when a value is passed', function () { const { getDatabase, ref, push, get } = databaseModular; diff --git a/packages/database/e2e/reference/remove.e2e.js b/packages/database/e2e/reference/remove.e2e.js index a0d0919651..012bf0e857 100644 --- a/packages/database/e2e/reference/remove.e2e.js +++ b/packages/database/e2e/reference/remove.e2e.js @@ -20,36 +20,6 @@ const { PATH } = require('../helpers'); const TEST_PATH = `${PATH}/remove`; describe('database().ref().remove()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if onComplete is not a function', async function () { - try { - await firebase.database().ref(TEST_PATH).remove('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('removes a value at the path', async function () { - const ref = firebase.database().ref(TEST_PATH); - await ref.set('foo'); - await ref.remove(); - const snapshot = await ref.once('value'); - snapshot.exists().should.equal(false); - }); - }); - describe('modular', function () { it('removes a value at the path', async function () { const { getDatabase, ref, set, remove, get } = databaseModular; diff --git a/packages/database/e2e/reference/root.e2e.js b/packages/database/e2e/reference/root.e2e.js index 43c1d5f93b..b18d12a4ae 100644 --- a/packages/database/e2e/reference/root.e2e.js +++ b/packages/database/e2e/reference/root.e2e.js @@ -16,23 +16,6 @@ */ describe('database().ref().root', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns a root reference', function () { - const ref = firebase.database().ref('foo/bar/baz'); - should.equal(ref.root.key, null); - }); - }); - describe('modular', function () { it('returns a root reference', function () { const { getDatabase, ref } = databaseModular; diff --git a/packages/database/e2e/reference/set.e2e.js b/packages/database/e2e/reference/set.e2e.js index 06da9cea96..8bbfa588a8 100644 --- a/packages/database/e2e/reference/set.e2e.js +++ b/packages/database/e2e/reference/set.e2e.js @@ -28,64 +28,6 @@ describe('set', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no value is provided', async function () { - try { - await firebase.database().ref(TEST_PATH).set(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' must be defined"); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', async function () { - try { - await firebase.database().ref(TEST_PATH).set(null, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('sets a new value', async function () { - const value = Date.now(); - const ref = firebase.database().ref(TEST_PATH); - await ref.set(value); - const snapshot = await ref.once('value'); - snapshot.val().should.eql(value); - }); - - it('callback if function is passed', async function () { - const value = Date.now(); - return new Promise(async resolve => { - await firebase.database().ref(TEST_PATH).set(value, resolve); - }); - }); - - it('throws if permission defined', async function () { - const value = Date.now(); - try { - await firebase.database().ref('nope/foo').set(value); - return Promise.reject(new Error('Did not throw error.')); - } catch (error) { - error.code.includes('database/permission-denied').should.be.true(); - return Promise.resolve(); - } - }); - }); - describe('modular', function () { it('throws if no value is provided', async function () { const { getDatabase, ref, set } = databaseModular; diff --git a/packages/database/e2e/reference/setPriority.e2e.js b/packages/database/e2e/reference/setPriority.e2e.js index 8f92153a23..c3b3199b1f 100644 --- a/packages/database/e2e/reference/setPriority.e2e.js +++ b/packages/database/e2e/reference/setPriority.e2e.js @@ -28,79 +28,6 @@ describe('database().ref().setPriority()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if priority is not a valid type', async function () { - try { - await firebase.database().ref().setPriority({}); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'priority' must be a number, string or null value"); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', async function () { - try { - await firebase.database().ref().setPriority(null, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('should correctly set a priority for all non-null values', async function () { - await Promise.all( - Object.keys(CONTENT.TYPES).map(async dataRef => { - const ref = firebase.database().ref(`${TEST_PATH}/types/${dataRef}`); - await ref.setPriority(1); - const snapshot = await ref.once('value'); - if (snapshot.val() !== null) { - snapshot.getPriority().should.eql(1); - } - }), - ); - }); - - it('callback if function is passed', async function () { - const value = Date.now(); - return new Promise(async resolve => { - await firebase.database().ref(`${TEST_PATH}/types/string`).set(value, resolve); - }); - }); - - it('throws if setting priority on non-existent node', async function () { - try { - await firebase.database().ref('tests/siudfhsuidfj').setPriority(1); - return Promise.reject(new Error('Did not throw error.')); - } catch (_) { - // WEB SDK: INVALID_PARAMETERS: could not set priority on non-existent node - // TODO Get this error? Native code = -999 Unknown - return Promise.resolve(); - } - }); - - it('throws if permission defined', async function () { - try { - await firebase.database().ref('nope/foo').setPriority(1); - return Promise.reject(new Error('Did not throw error.')); - } catch (error) { - error.code.includes('database/permission-denied').should.be.true(); - return Promise.resolve(); - } - }); - }); - describe('modular', function () { it('throws if priority is not a valid type', async function () { const { getDatabase, ref, setPriority } = databaseModular; diff --git a/packages/database/e2e/reference/setWithPriority.e2e.js b/packages/database/e2e/reference/setWithPriority.e2e.js index bc70af2de8..7110bd38f0 100644 --- a/packages/database/e2e/reference/setWithPriority.e2e.js +++ b/packages/database/e2e/reference/setWithPriority.e2e.js @@ -28,67 +28,6 @@ describe('database().ref().setWithPriority()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if newVal is not defined', async function () { - try { - await firebase.database().ref().setWithPriority(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'newVal' must be defined"); - return Promise.resolve(); - } - }); - - it('throws if newPriority is incorrect type', async function () { - try { - await firebase.database().ref().setWithPriority(null, { foo: 'bar' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'newPriority' must be a number, string or null value"); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', async function () { - try { - await firebase.database().ref().setWithPriority(null, null, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('callback if function is passed', async function () { - const value = Date.now(); - return new Promise(async resolve => { - await firebase - .database() - .ref(`${TEST_PATH}/setValueWithCallback`) - .setWithPriority(value, 2, resolve); - }); - }); - - it('sets with a new value and priority', async function () { - const value = Date.now(); - const ref = firebase.database().ref(`${TEST_PATH}/setValue`); - await ref.setWithPriority(value, 2); - const snapshot = await ref.once('value'); - snapshot.val().should.eql(value); - snapshot.getPriority().should.eql(2); - }); - }); - describe('modular', function () { it('throws if newVal is not defined', async function () { const { getDatabase, ref, setWithPriority } = databaseModular; diff --git a/packages/database/e2e/reference/transaction.e2e.js b/packages/database/e2e/reference/transaction.e2e.js index 6e325ba31a..fde20df54d 100644 --- a/packages/database/e2e/reference/transaction.e2e.js +++ b/packages/database/e2e/reference/transaction.e2e.js @@ -29,193 +29,6 @@ describe('database().ref().transaction()', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no transactionUpdate is provided', async function () { - try { - await firebase.database().ref(TEST_PATH).transaction(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'transactionUpdate' must be a function"); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', async function () { - try { - await firebase.database().ref().transaction(NOOP, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('throws if applyLocally is not a boolean', async function () { - try { - await firebase.database().ref().transaction(NOOP, NOOP, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'applyLocally' must be a boolean value if provided"); - return Promise.resolve(); - } - }); - - it('updates the value via a transaction', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/transactionUpdate`); - const beforeValue = (await ref.once('value')).val() || 0; - const { committed, snapshot } = await ref.transaction(value => { - if (!value) { - return 1; - } - return value + 1; - }); - - should.equal(committed, true, 'Transaction did not commit.'); - snapshot.val().should.equal(beforeValue + 1); - }); - - it('aborts transaction if undefined returned', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/transactionAbort`); - await ref.set(1); - - return new Promise((resolve, reject) => { - ref.transaction( - () => { - return undefined; - }, - (error, committed) => { - if (error) { - return reject(error); - } - - if (!committed) { - return resolve(); - } - - return reject(new Error('Transaction did not abort commit.')); - }, - ); - }); - }); - - it('passes valid data through the callback', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/transactionCallback/${Date.now()}`); - await ref.set(1); - - return new Promise((resolve, reject) => { - ref.transaction( - $ => { - return $ + 1; - }, - (error, committed, snapshot) => { - if (error) { - return reject(error); - } - - if (!committed) { - return reject(new Error('Transaction aborted when it should not have done')); - } - - should.equal(snapshot.val(), 2); - return resolve(); - }, - ); - }); - }); - - // FIXME flaky on android local against emulator? - it('throws when an error occurs', async function () { - const ref = firebase.database().ref('nope'); - - try { - await ref.transaction($ => { - return $ + 1; - }); - return Promise.reject(new Error('Did not throw error.')); - } catch (error) { - error.message.should.containEql('permission'); - return Promise.resolve(); - } - }); - - // FIXME flaky on android in CI? works most of the time... - it('passes error back to the callback', async function () { - if (Platform.ios || !global.isCI || Platform.other) { - const ref = firebase.database().ref('nope'); - - return new Promise((resolve, reject) => { - ref - .transaction( - $ => { - return $ + 1; - }, - (error, committed, snapshot) => { - if (snapshot !== null) { - return reject(new Error('Snapshot should not be available')); - } - - if (committed === true) { - return reject(new Error('Transaction should not have committed')); - } - - error.message.should.containEql('permission'); - return resolve(); - }, - ) - .catch(e => { - return reject(e); - }); - }); - } else { - this.skip(); - } - }); - - it('sets a value if one does not exist', async function () { - const ref = firebase.database().ref(`${TEST_PATH}/transactionCreate`); - await ref.remove(); - const value = Date.now(); - - return new Promise((resolve, reject) => { - ref.transaction( - $ => { - if ($ === null) { - return { foo: value }; - } - - throw new Error('Value should not exist'); - }, - (error, committed, snapshot) => { - if (error) { - return reject(error); - } - - if (!committed) { - return reject(new Error('Transaction should have committed')); - } - - snapshot.val().should.eql( - jet.contextify({ - foo: value, - }), - ); - return resolve(); - }, - ); - }); - }); - }); - describe('modular', function () { it('throws if no transactionUpdate is provided', async function () { const { getDatabase, ref, runTransaction } = databaseModular; diff --git a/packages/database/e2e/reference/update.e2e.js b/packages/database/e2e/reference/update.e2e.js index e0a201a168..0ac4e71f45 100644 --- a/packages/database/e2e/reference/update.e2e.js +++ b/packages/database/e2e/reference/update.e2e.js @@ -25,109 +25,6 @@ describe('database().ref().update()', function () { await remove(ref(getDatabase(), TEST_PATH)); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if values is not an object', async function () { - try { - await firebase.database().ref(TEST_PATH).update('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'values' must be an object"); - return Promise.resolve(); - } - }); - - it('throws if update paths are not valid', async function () { - try { - await firebase.database().ref(TEST_PATH).update({ - $$$$: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'values' contains an invalid path."); - return Promise.resolve(); - } - }); - - it('throws if onComplete is not a function', async function () { - try { - await firebase.database().ref(TEST_PATH).update( - { - foo: 'bar', - }, - 'foo', - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'onComplete' must be a function if provided"); - return Promise.resolve(); - } - }); - - it('updates values', async function () { - const value = Date.now(); - const ref = firebase.database().ref(TEST_PATH); - await ref.update({ - foo: value, - }); - const snapshot = await ref.once('value'); - snapshot.val().should.eql( - jet.contextify({ - foo: value, - }), - ); - - await ref.update({}); // empty update should pass, but no side effects - const snapshot2 = await ref.once('value'); - snapshot2.val().should.eql( - jet.contextify({ - foo: value, - }), - ); - }); - - it('removes property if set to `null`', async function () { - const value = Date.now(); - const ref = firebase.database().ref(TEST_PATH); - await ref.update({ - foo: value, - }); - const snapshot = await ref.once('value'); - snapshot.val().should.eql( - jet.contextify({ - foo: value, - }), - ); - await ref.update({ - foo: null, - }); - const snapshot2 = await ref.once('value'); - // Removes key/value - should(snapshot2.val()).be.null(); - }); - - it('callback if function is passed', async function () { - const value = Date.now(); - return new Promise(async resolve => { - await firebase.database().ref(TEST_PATH).update( - { - foo: value, - }, - resolve, - ); - }); - }); - }); - describe('modular', function () { it('throws if values is not an object', async function () { const { getDatabase, ref, update } = databaseModular; diff --git a/packages/database/e2e/snapshot/snapshot.e2e.js b/packages/database/e2e/snapshot/snapshot.e2e.js index c35220be3f..f94faad83f 100644 --- a/packages/database/e2e/snapshot/snapshot.e2e.js +++ b/packages/database/e2e/snapshot/snapshot.e2e.js @@ -28,281 +28,6 @@ describe('database()...snapshot', function () { await wipe(TEST_PATH); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns the snapshot key', async function () { - const snapshot = await firebase - .database() - .ref(TEST_PATH) - .child('types/boolean') - .once('value'); - - snapshot.key.should.equal('boolean'); - }); - - it('returns the snapshot reference', async function () { - const snapshot = await firebase - .database() - .ref(TEST_PATH) - .child('types/boolean') - .once('value'); - - snapshot.ref.key.should.equal('boolean'); - }); - - it('returns the correct boolean for exists', async function () { - const snapshot1 = await firebase - .database() - .ref(TEST_PATH) - .child('types/boolean') - .once('value'); - - const snapshot2 = await firebase.database().ref(TEST_PATH).child('types/nope').once('value'); - - snapshot1.exists().should.equal(true); - snapshot2.exists().should.equal(false); - }); - - it('exports a valid object', async function () { - const snapshot = await firebase.database().ref(TEST_PATH).child('types/string').once('value'); - - const exported = snapshot.exportVal(); - - exported.should.have.property('.value'); - exported.should.have.property('.priority'); - exported['.value'].should.equal('foobar'); - should.equal(exported['.priority'], null); - }); - - it('exports a valid object with a object value', async function () { - const snapshot = await firebase.database().ref(TEST_PATH).child('types/object').once('value'); - - const exported = snapshot.exportVal(); - - exported.should.have.property('.value'); - exported.should.have.property('.priority'); - exported['.value'].should.eql(jet.contextify(CONTENT.TYPES.object)); - should.equal(exported['.priority'], null); - }); - - it('forEach throws if action param is not a function', async function () { - const ref = firebase.database().ref(TEST_PATH).child('unorderedList'); - - await ref.set({ - a: 3, - b: 1, - c: 2, - }); - - const snapshot = await ref.orderByValue().once('value'); - - try { - snapshot.forEach('foo'); - } catch (error) { - error.message.should.containEql("'action' must be a function"); - } - }); - - it('forEach returns an ordered list of snapshots', async function () { - const ref = firebase.database().ref(TEST_PATH).child('unorderedList'); - - await ref.set({ - a: 3, - b: 1, - c: 2, - }); - - const snapshot = await ref.orderByValue().once('value'); - const expected = ['b', 'c', 'a']; - - snapshot.forEach((childSnap, i) => { - childSnap.val().should.equal(i + 1); - childSnap.key.should.equal(expected[i]); - }); - }); - - it('forEach works with arrays', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH).child('types'); - - const snapshot = await ref.once('value'); - - snapshot.child('array').forEach((childSnap, i) => { - callback(); - childSnap.val().should.equal(i); - childSnap.key.should.equal(i.toString()); - }); - - callback.should.be.callCount(snapshot.child('array').numChildren()); - }); - - it('forEach works with objects and cancels when returning true', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH).child('types/object').orderByKey(); - - const snapshot = await ref.once('value'); - - snapshot.forEach(childSnap => { - callback(); - childSnap.key.should.equal('bar'); - childSnap.val().should.equal('baz'); - return true; - }); - - callback.should.be.calledOnce(); - }); - - it('forEach works with arrays and cancels when returning true', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH).child('types'); - - const snapshot = await ref.once('value'); - - snapshot.child('array').forEach(childSnap => { - callback(); - childSnap.val().should.equal(0); - childSnap.key.should.equal('0'); - return true; - }); - - callback.should.be.calledOnce(); - }); - - it('forEach returns false when no child keys', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH).child('types/boolean'); - - const snapshot = await ref.once('value'); - - const bool = snapshot.forEach(() => { - callback(); - }); - - bool.should.equal(false); - callback.should.be.callCount(0); - }); - - it('forEach cancels iteration when returning true', async function () { - const callback = sinon.spy(); - const ref = firebase.database().ref(TEST_PATH).child('types/array'); - - const snapshot = await ref.orderByValue().once('value'); - - const cancelled = snapshot.forEach(childSnap => { - callback(childSnap.val()); - return true; - }); - - cancelled.should.equal(true); - callback.should.be.callCount(1); - callback.getCall(0).args[0].should.equal(0); - }); - - it('getPriority returns the correct value', async function () { - const ref = firebase.database().ref(TEST_PATH).child('getPriority'); - - await ref.setWithPriority('foo', 'bar'); - const snapshot = await ref.once('value'); - - snapshot.getPriority().should.equal('bar'); - }); - - it('hasChild throws if path is not a string value', async function () { - const ref = firebase.database().ref(TEST_PATH).child('types/boolean'); - - const snapshot = await ref.once('value'); - - try { - snapshot.hasChild({ foo: 'bar' }); - } catch (error) { - error.message.should.containEql("'path' must be a string value"); - } - }); - - it('hasChild returns the correct boolean value', async function () { - const ref = firebase.database().ref(TEST_PATH); - - const snapshot1 = await ref.child('types/boolean').once('value'); - const snapshot2 = await ref.child('types').once('value'); - - snapshot1.hasChild('foo').should.equal(false); - snapshot2.hasChild('boolean').should.equal(true); - }); - - it('hasChildren returns the correct boolean value', async function () { - const ref = firebase.database().ref(TEST_PATH); - const snapshot = await ref.child('types/object').once('value'); - snapshot.hasChildren().should.equal(true); - }); - - it('numChildren returns the correct number value', async function () { - const ref = firebase.database().ref(TEST_PATH); - - const snapshot1 = await ref.child('types/boolean').once('value'); - const snapshot2 = await ref.child('types/array').once('value'); - const snapshot3 = await ref.child('types/object').once('value'); - - snapshot1.numChildren().should.equal(0); - snapshot2.numChildren().should.equal(CONTENT.TYPES.array.length); - snapshot3.numChildren().should.equal(Object.keys(CONTENT.TYPES.object).length); - }); - - it('toJSON returns the value of the snapshot', async function () { - const ref = firebase.database().ref(TEST_PATH); - - const snapshot1 = await ref.child('types/string').once('value'); - const snapshot2 = await ref.child('types/object').once('value'); - - snapshot1.toJSON().should.equal('foobar'); - snapshot2.toJSON().should.eql(jet.contextify(CONTENT.TYPES.object)); - }); - - it('val returns the value of the snapshot', async function () { - const ref = firebase.database().ref(TEST_PATH); - - const snapshot1 = await ref.child('types/string').once('value'); - const snapshot2 = await ref.child('types/object').once('value'); - - snapshot1.val().should.equal('foobar'); - snapshot2.val().should.eql(jet.contextify(CONTENT.TYPES.object)); - }); - - it('should return the correct priority for the child snapshots', async function () { - if (Platform.other) { - // TODO - remove once "other" is fully integrated - this.skip(); - } - const ref = firebase.database().ref(TEST_PATH).child('get-priority-children'); - const child1 = ref.child('child1'); - const child2 = ref.child('child2'); - const child3 = ref.child('child3'); - - await Promise.all([ - child1.set({ foo: 'bar' }), - child2.set({ foo: 'bar' }), - child3.set({ foo: 'bar' }), - ]); - - await child1.setPriority(1); - await child2.setPriority(2); - await child3.setPriority(3); - - const snapshot = await ref.once('value'); - snapshot.child('child1').getPriority().should.equal(1); - snapshot.child('child2').getPriority().should.equal(2); - snapshot.child('child3').getPriority().should.equal(3); - }); - }); - describe('modular', function () { it('returns the snapshot key', async function () { const { getDatabase, ref, child, get } = databaseModular; diff --git a/packages/database/lib/DatabaseDataSnapshot.ts b/packages/database/lib/DatabaseDataSnapshot.ts index 88236cf97c..e31f4cc612 100644 --- a/packages/database/lib/DatabaseDataSnapshot.ts +++ b/packages/database/lib/DatabaseDataSnapshot.ts @@ -16,40 +16,33 @@ */ import { - createDeprecationProxy, isArray, isFunction, isNumber, isObject, isString, - MODULAR_DEPRECATION_ARG, } from '@react-native-firebase/app/dist/module/common'; import { deepGet } from '@react-native-firebase/app/dist/module/common/deeps'; -import type { DatabaseSnapshotInternal } from './types/internal'; -import type { FirebaseDatabaseTypes } from './types/namespaced'; +import type { + DatabaseReferenceWithMethodsInternal, + DatabaseSnapshotInternal, +} from './types/internal'; +import type { DatabaseReference, DataSnapshot, IteratedDataSnapshot } from './types/database'; -type ReferenceWithDeprecationArg = FirebaseDatabaseTypes.Reference & { - child(path: string, deprecationArg?: string): FirebaseDatabaseTypes.Reference; -}; - -function ap(reference: FirebaseDatabaseTypes.Reference): ReferenceWithDeprecationArg { - return reference as ReferenceWithDeprecationArg; +function ap(reference: DatabaseReference): DatabaseReferenceWithMethodsInternal { + return reference as DatabaseReferenceWithMethodsInternal; } -export default class DatabaseDataSnapshot implements FirebaseDatabaseTypes.DataSnapshot { +export default class DatabaseDataSnapshot implements DataSnapshot { _snapshot: DatabaseSnapshotInternal; - _ref: FirebaseDatabaseTypes.Reference; + _ref: DatabaseReference; - constructor(reference: FirebaseDatabaseTypes.Reference, snapshot: DatabaseSnapshotInternal) { + constructor(reference: DatabaseReference, snapshot: DatabaseSnapshotInternal) { this._snapshot = snapshot; if (reference.key !== snapshot.key && isString(snapshot.key)) { - this._ref = ap(reference.ref).child.call( - reference.ref, - snapshot.key, - MODULAR_DEPRECATION_ARG, - ); + this._ref = ap(reference.ref).child(snapshot.key); } else { this._ref = reference; } @@ -59,11 +52,11 @@ export default class DatabaseDataSnapshot implements FirebaseDatabaseTypes.DataS return this._snapshot.key; } - get ref(): FirebaseDatabaseTypes.Reference { + get ref(): DatabaseReference { return this._ref; } - child(path: string): FirebaseDatabaseTypes.DataSnapshot { + child(path: string): DataSnapshot { if (!isString(path)) { throw new Error("snapshot().child(*) 'path' must be a string value"); } @@ -75,7 +68,7 @@ export default class DatabaseDataSnapshot implements FirebaseDatabaseTypes.DataS value = null; } - const childRef = ap(this._ref).child.call(this._ref, path, MODULAR_DEPRECATION_ARG); + const childRef = ap(this._ref).child(path); let childPriority: string | number | null = null; if (this._snapshot.childPriorities) { @@ -85,15 +78,13 @@ export default class DatabaseDataSnapshot implements FirebaseDatabaseTypes.DataS } } - return createDeprecationProxy( - new DatabaseDataSnapshot(childRef, { - value, - key: childRef.key, - exists: value !== null, - childKeys: isObject(value) ? Object.keys(value as Record) : [], - priority: childPriority, - }), - ) as FirebaseDatabaseTypes.DataSnapshot; + return new DatabaseDataSnapshot(childRef, { + value, + key: childRef.key, + exists: value !== null, + childKeys: isObject(value) ? Object.keys(value as Record) : [], + priority: childPriority, + }); } exists(): boolean { @@ -113,19 +104,16 @@ export default class DatabaseDataSnapshot implements FirebaseDatabaseTypes.DataS }; } - forEach(action: (child: FirebaseDatabaseTypes.DataSnapshot) => boolean | void): boolean { + forEach(action: (child: IteratedDataSnapshot) => boolean | void): boolean { if (!isFunction(action)) { throw new Error("snapshot.forEach(*) 'action' must be a function."); } - const iterate = action as ( - child: FirebaseDatabaseTypes.DataSnapshot, - index?: number, - ) => boolean | void; + const iterate = action as (child: IteratedDataSnapshot, index?: number) => boolean | void; if (isArray(this._snapshot.value)) { return this._snapshot.value.some( - (_value, i) => iterate(this.child(i.toString()), i) === true, + (_value, i) => iterate(this.child(i.toString()) as IteratedDataSnapshot, i) === true, ); } @@ -141,7 +129,7 @@ export default class DatabaseDataSnapshot implements FirebaseDatabaseTypes.DataS continue; } const snapshot = this.child(key); - const actionReturn = iterate(snapshot, i); + const actionReturn = iterate(snapshot as IteratedDataSnapshot, i); if (actionReturn === true) { cancelled = true; @@ -152,10 +140,27 @@ export default class DatabaseDataSnapshot implements FirebaseDatabaseTypes.DataS return cancelled; } - getPriority(): string | number | null { + get priority(): string | number | null { return this._snapshot.priority ?? null; } + get size(): number { + const value = this.val(); + if (value === null) { + return 4; + } + + try { + return JSON.stringify(value).length; + } catch { + return 0; + } + } + + getPriority(): string | number | null { + return this.priority; + } + hasChild(path: string): boolean { if (!isString(path)) { throw new Error("snapshot.hasChild(*) 'path' must be a string value."); diff --git a/packages/database/lib/DatabaseOnDisconnect.ts b/packages/database/lib/DatabaseOnDisconnect.ts index a92628c78d..6247a3170e 100644 --- a/packages/database/lib/DatabaseOnDisconnect.ts +++ b/packages/database/lib/DatabaseOnDisconnect.ts @@ -27,14 +27,14 @@ import { } from '@react-native-firebase/app/dist/module/common'; import type { DatabaseModuleInternal } from './types/internal'; -import type { FirebaseDatabaseTypes } from './types/namespaced'; +import type { OnDisconnect } from './types/database'; interface DatabaseOnDisconnectReferenceInternal { readonly path: string; readonly _database: DatabaseModuleInternal; } -export default class DatabaseOnDisconnect implements FirebaseDatabaseTypes.OnDisconnect { +export default class DatabaseOnDisconnect implements OnDisconnect { _ref: DatabaseOnDisconnectReferenceInternal; constructor(reference: DatabaseOnDisconnectReferenceInternal) { @@ -86,7 +86,7 @@ export default class DatabaseOnDisconnect implements FirebaseDatabaseTypes.OnDis setWithPriority( value: any, - priority: string | number | null, + priority: number | string | null, onComplete?: (error: Error | null) => void, ): Promise { if (isUndefined(value)) { diff --git a/packages/database/lib/DatabaseQuery.ts b/packages/database/lib/DatabaseQuery.ts index e87ff32349..c0c8e7bc62 100644 --- a/packages/database/lib/DatabaseQuery.ts +++ b/packages/database/lib/DatabaseQuery.ts @@ -16,7 +16,6 @@ */ import { - createDeprecationProxy, isBoolean, isFunction, isNull, @@ -24,7 +23,6 @@ import { isObject, isString, isUndefined, - MODULAR_DEPRECATION_ARG, pathIsEmpty, pathToUrlEncodedString, ReferenceBase, @@ -37,7 +35,7 @@ import type { DatabaseInternal, DatabaseListenPropsInternal, } from './types/internal'; -import type { FirebaseDatabaseTypes } from './types/namespaced'; +import type { DatabaseReference, DataSnapshot, EventType, Query } from './types/database'; const eventTypes = [ 'value', @@ -50,20 +48,7 @@ const eventTypes = [ type DatabaseReferenceConstructor = new ( database: DatabaseInternal, path: string, -) => FirebaseDatabaseTypes.Reference; - -type QueryWithDeprecationArgInternal = FirebaseDatabaseTypes.Query & { - startAt( - value: number | string | boolean | null, - key?: string, - deprecationArg?: string, - ): FirebaseDatabaseTypes.Query; - endAt( - value: number | string | boolean | null, - key?: string, - deprecationArg?: string, - ): FirebaseDatabaseTypes.Query; -}; +) => DatabaseReference; let DatabaseReferenceClass: DatabaseReferenceConstructor | null = null; @@ -71,26 +56,17 @@ export function provideReferenceClass(databaseReference: DatabaseReferenceConstr DatabaseReferenceClass = databaseReference; } -function ap(query: FirebaseDatabaseTypes.Query): QueryWithDeprecationArgInternal { - return query as QueryWithDeprecationArgInternal; -} - -function createReference( - database: DatabaseInternal, - path: string, -): FirebaseDatabaseTypes.Reference { +function createReference(database: DatabaseInternal, path: string): DatabaseReference { if (!DatabaseReferenceClass) { throw new Error('DatabaseReference class has not been provided.'); } - return createDeprecationProxy( - new DatabaseReferenceClass(database, path), - ) as FirebaseDatabaseTypes.Reference; + return new DatabaseReferenceClass(database, path); } let listeners = 0; -export default class DatabaseQuery extends ReferenceBase implements FirebaseDatabaseTypes.Query { +export default class DatabaseQuery extends ReferenceBase implements Query { _database: DatabaseInternal; _modifiers: DatabaseQueryModifiers; @@ -100,11 +76,11 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData this._modifiers = modifiers; } - get ref(): FirebaseDatabaseTypes.Reference { + get ref(): DatabaseReference { return createReference(this._database, this.path); } - endAt(value: number | string | boolean | null, key?: string): FirebaseDatabaseTypes.Query { + endAt(value: number | string | boolean | null, key?: string): Query { if (!isNumber(value) && !isString(value) && !isBoolean(value) && !isNull(value)) { throw new Error( "firebase.database().ref().endAt(*) 'value' must be a number, string, boolean or null value.", @@ -126,10 +102,10 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData const modifiers = this._modifiers._copy().endAt(value, key); modifiers.validateModifiers('firebase.database().ref().endAt()'); - return createDeprecationProxy(new DatabaseQuery(this._database, this.path, modifiers)); + return new DatabaseQuery(this._database, this.path, modifiers); } - equalTo(value: number | string | boolean | null, key?: string): FirebaseDatabaseTypes.Query { + equalTo(value: number | string | boolean | null, key?: string): Query { if (!isNumber(value) && !isString(value) && !isBoolean(value) && !isNull(value)) { throw new Error( "firebase.database().ref().equalTo(*) 'value' must be a number, string, boolean or null value.", @@ -154,14 +130,10 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData ); } - return ap(ap(this).startAt.call(this, value, key, MODULAR_DEPRECATION_ARG)).endAt.call( - this, - value, - MODULAR_DEPRECATION_ARG, - ); + return (this.startAt(value, key) as DatabaseQuery).endAt(value, key); } - isEqual(other: FirebaseDatabaseTypes.Query): boolean { + isEqual(other: Query): boolean { if (!(other instanceof DatabaseQuery)) { throw new Error("firebase.database().ref().isEqual(*) 'other' must be an instance of Query."); } @@ -173,7 +145,7 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData return sameApp && sameDatabasePath && sameModifiers; } - limitToFirst(limit: number): FirebaseDatabaseTypes.Query { + limitToFirst(limit: number): Query { if (this._modifiers.isValidLimit(limit)) { throw new Error( "firebase.database().ref().limitToFirst(*) 'limit' must be a positive integer value.", @@ -186,12 +158,14 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData ); } - return createDeprecationProxy( - new DatabaseQuery(this._database, this.path, this._modifiers._copy().limitToFirst(limit)), - ) as FirebaseDatabaseTypes.Query; + return new DatabaseQuery( + this._database, + this.path, + this._modifiers._copy().limitToFirst(limit), + ); } - limitToLast(limit: number): FirebaseDatabaseTypes.Query { + limitToLast(limit: number): Query { if (this._modifiers.isValidLimit(limit)) { throw new Error( "firebase.database().ref().limitToLast(*) 'limit' must be a positive integer value.", @@ -204,14 +178,12 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData ); } - return createDeprecationProxy( - new DatabaseQuery(this._database, this.path, this._modifiers._copy().limitToLast(limit)), - ) as FirebaseDatabaseTypes.Query; + return new DatabaseQuery(this._database, this.path, this._modifiers._copy().limitToLast(limit)); } off( - eventType?: FirebaseDatabaseTypes.EventType, - callback?: (a: FirebaseDatabaseTypes.DataSnapshot, b?: string | null) => void, + eventType?: EventType, + callback?: (a: DataSnapshot, b?: string | null) => void, context?: Record, ): void { if (arguments.length === 0) { @@ -261,11 +233,11 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData } on( - eventType: FirebaseDatabaseTypes.EventType, - callback: (data: FirebaseDatabaseTypes.DataSnapshot, previousChildKey?: string | null) => void, + eventType: EventType, + callback: (data: DataSnapshot, previousChildKey?: string | null) => void, cancelCallbackOrContext?: ((a: Error) => void) | Record | null, context?: Record | null, - ): (a: FirebaseDatabaseTypes.DataSnapshot | null, b?: string | null) => void { + ): (a: DataSnapshot | null, b?: string | null) => void { if (!eventTypes.includes(eventType)) { throw new Error( `firebase.database().ref().on(*) 'eventType' must be one of ${eventTypes.join(', ')}.`, @@ -341,15 +313,15 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData listeners += 1; - return callback as (a: FirebaseDatabaseTypes.DataSnapshot | null, b?: string | null) => void; + return callback as (a: DataSnapshot | null, b?: string | null) => void; } once( - eventType: FirebaseDatabaseTypes.EventType, - successCallBack?: (a: FirebaseDatabaseTypes.DataSnapshot, b?: string | null) => any, + eventType: EventType, + successCallBack?: (a: DataSnapshot, b?: string | null) => any, failureCallbackOrContext?: ((a: Error) => void) | Record | null, context?: Record, - ): Promise { + ): Promise { if (!eventTypes.includes(eventType)) { throw new Error( `firebase.database().ref().once(*) 'eventType' must be one of ${eventTypes.join(', ')}.`, @@ -381,21 +353,17 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData return this._database.native .once(this.path, modifiers, eventType) .then(result => { - let dataSnapshot: FirebaseDatabaseTypes.DataSnapshot; + let dataSnapshot: DataSnapshot; let previousChildName: string | null | undefined; if (eventType === 'value') { - dataSnapshot = createDeprecationProxy( - new DatabaseDataSnapshot( - this.ref, - result as ConstructorParameters[1], - ), - ) as FirebaseDatabaseTypes.DataSnapshot; + dataSnapshot = new DatabaseDataSnapshot( + this.ref, + result as ConstructorParameters[1], + ); } else { const childResult = result as DatabaseChildSnapshotResultInternal; - dataSnapshot = createDeprecationProxy( - new DatabaseDataSnapshot(this.ref, childResult.snapshot), - ) as FirebaseDatabaseTypes.DataSnapshot; + dataSnapshot = new DatabaseDataSnapshot(this.ref, childResult.snapshot); previousChildName = childResult.previousChildName; } @@ -419,7 +387,7 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData }); } - orderByChild(path: string): FirebaseDatabaseTypes.Query { + orderByChild(path: string): Query { if (!isString(path)) { throw new Error("firebase.database().ref().orderByChild(*) 'path' must be a string value."); } @@ -439,10 +407,10 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData const modifiers = this._modifiers._copy().orderByChild(path); modifiers.validateModifiers('firebase.database().ref().orderByChild()'); - return createDeprecationProxy(new DatabaseQuery(this._database, this.path, modifiers)); + return new DatabaseQuery(this._database, this.path, modifiers); } - orderByKey(): FirebaseDatabaseTypes.Query { + orderByKey(): Query { if (this._modifiers.hasOrderBy()) { throw new Error( "firebase.database().ref().orderByKey() You can't combine multiple orderBy calls.", @@ -452,10 +420,10 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData const modifiers = this._modifiers._copy().orderByKey(); modifiers.validateModifiers('firebase.database().ref().orderByKey()'); - return createDeprecationProxy(new DatabaseQuery(this._database, this.path, modifiers)); + return new DatabaseQuery(this._database, this.path, modifiers); } - orderByPriority(): FirebaseDatabaseTypes.Query { + orderByPriority(): Query { if (this._modifiers.hasOrderBy()) { throw new Error( "firebase.database().ref().orderByPriority() You can't combine multiple orderBy calls.", @@ -465,10 +433,10 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData const modifiers = this._modifiers._copy().orderByPriority(); modifiers.validateModifiers('firebase.database().ref().orderByPriority()'); - return createDeprecationProxy(new DatabaseQuery(this._database, this.path, modifiers)); + return new DatabaseQuery(this._database, this.path, modifiers); } - orderByValue(): FirebaseDatabaseTypes.Query { + orderByValue(): Query { if (this._modifiers.hasOrderBy()) { throw new Error( "firebase.database().ref().orderByValue() You can't combine multiple orderBy calls.", @@ -478,10 +446,10 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData const modifiers = this._modifiers._copy().orderByValue(); modifiers.validateModifiers('firebase.database().ref().orderByValue()'); - return createDeprecationProxy(new DatabaseQuery(this._database, this.path, modifiers)); + return new DatabaseQuery(this._database, this.path, modifiers); } - startAt(value: number | string | boolean | null, key?: string): FirebaseDatabaseTypes.Query { + startAt(value: number | string | boolean | null, key?: string): Query { if (!isNumber(value) && !isString(value) && !isBoolean(value) && !isNull(value)) { throw new Error( "firebase.database().ref().startAt(*) 'value' must be a number, string, boolean or null value.", @@ -503,7 +471,7 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData const modifiers = this._modifiers._copy().startAt(value, key); modifiers.validateModifiers('firebase.database().ref().startAt()'); - return createDeprecationProxy(new DatabaseQuery(this._database, this.path, modifiers)); + return new DatabaseQuery(this._database, this.path, modifiers); } toJSON(): string { @@ -535,7 +503,7 @@ export default class DatabaseQuery extends ReferenceBase implements FirebaseData }$${this._modifiers.toString()}`; } - _generateQueryEventKey(eventType: FirebaseDatabaseTypes.EventType): string { + _generateQueryEventKey(eventType: EventType): string { return `${this._generateQueryKey()}$${listeners}$${eventType}`; } } diff --git a/packages/database/lib/DatabaseReference.ts b/packages/database/lib/DatabaseReference.ts index b3d3d7af11..69ef10b5ac 100644 --- a/packages/database/lib/DatabaseReference.ts +++ b/packages/database/lib/DatabaseReference.ts @@ -16,7 +16,6 @@ */ import { - createDeprecationProxy, generateDatabaseId, isBoolean, isFunction, @@ -26,7 +25,6 @@ import { isString, isUndefined, isValidPath, - MODULAR_DEPRECATION_ARG, pathChild, pathParent, promiseWithOptionalCallback, @@ -40,35 +38,18 @@ import DatabaseQueryModifiers from './DatabaseQueryModifiers'; import DatabaseThenableReference, { provideReferenceClass as provideReferenceClassForThenable, } from './DatabaseThenableReference'; -import type { DatabaseInternal } from './types/internal'; -import type { FirebaseDatabaseTypes } from './types/namespaced'; +import type { DatabaseInternal, DatabaseReferenceWithMethodsInternal } from './types/internal'; +import type { + DatabaseReference as DatabaseReferenceType, + DataSnapshot, + OnDisconnect, + ThenableReference, + TransactionResult, +} from './types/database'; const internalRefs = ['.info/connected', '.info/serverTimeOffset'] as const; -type ReferenceWithChildInternal = FirebaseDatabaseTypes.Reference & { - child(path: string, deprecationArg?: string): FirebaseDatabaseTypes.Reference; -}; - -type ReferenceWithSetInternal = FirebaseDatabaseTypes.Reference & { - set( - value: unknown, - onComplete?: (error: Error | null) => void, - deprecationArg?: string, - ): Promise; -}; - -function apChild(reference: FirebaseDatabaseTypes.Reference): ReferenceWithChildInternal { - return reference as ReferenceWithChildInternal; -} - -function apSet(reference: FirebaseDatabaseTypes.Reference): ReferenceWithSetInternal { - return reference as ReferenceWithSetInternal; -} - -export default class DatabaseReference - extends DatabaseQuery - implements FirebaseDatabaseTypes.Reference -{ +export default class DatabaseReference extends DatabaseQuery implements DatabaseReferenceType { readonly _database: DatabaseInternal; constructor(database: DatabaseInternal, path: string) { @@ -82,29 +63,23 @@ export default class DatabaseReference this._database = database; } - get parent(): FirebaseDatabaseTypes.Reference | null { + get parent(): DatabaseReferenceType | null { const parentPath = pathParent(this.path); if (parentPath === null) { return null; } - return createDeprecationProxy( - new DatabaseReference(this._database, parentPath), - ) as FirebaseDatabaseTypes.Reference; + return new DatabaseReference(this._database, parentPath); } - get root(): FirebaseDatabaseTypes.Reference { - return createDeprecationProxy( - new DatabaseReference(this._database, '/'), - ) as FirebaseDatabaseTypes.Reference; + get root(): DatabaseReferenceType { + return new DatabaseReference(this._database, '/'); } - child(path: string): FirebaseDatabaseTypes.Reference { + child(path: string): DatabaseReferenceType { if (!isString(path)) { throw new Error("firebase.database().ref().child(*) 'path' must be a string value."); } - return createDeprecationProxy( - new DatabaseReference(this._database, pathChild(this.path, path)), - ) as FirebaseDatabaseTypes.Reference; + return new DatabaseReference(this._database, pathChild(this.path, path)); } set(value: any, onComplete?: (error: Error | null) => void): Promise { @@ -195,10 +170,10 @@ export default class DatabaseReference onComplete?: ( error: Error | null, committed: boolean, - finalResult: FirebaseDatabaseTypes.DataSnapshot | null, + finalResult: DataSnapshot | null, ) => void, applyLocally?: boolean, - ): Promise { + ): Promise { if (!isFunction(transactionUpdate)) { throw new Error( "firebase.database().ref().transaction(*) 'transactionUpdate' must be a function.", @@ -230,12 +205,10 @@ export default class DatabaseReference onComplete( null, committed, - createDeprecationProxy( - new DatabaseDataSnapshot( - this, - snapshotData as ConstructorParameters[1], - ), - ) as FirebaseDatabaseTypes.DataSnapshot, + new DatabaseDataSnapshot( + this, + snapshotData as ConstructorParameters[1], + ), ); } } @@ -245,14 +218,17 @@ export default class DatabaseReference return; } + const snapshot = new DatabaseDataSnapshot( + this, + snapshotData as ConstructorParameters[1], + ); + resolve({ committed, - snapshot: createDeprecationProxy( - new DatabaseDataSnapshot( - this, - snapshotData as ConstructorParameters[1], - ), - ) as FirebaseDatabaseTypes.DataSnapshot, + snapshot, + toJSON() { + return { committed, snapshot: snapshot.toJSON() }; + }, }); }; @@ -282,10 +258,7 @@ export default class DatabaseReference ); } - push( - value?: any, - onComplete?: (error: Error | null) => void, - ): FirebaseDatabaseTypes.ThenableReference { + push(value?: any, onComplete?: (error: Error | null) => void): ThenableReference { if (!isUndefined(onComplete) && !isFunction(onComplete)) { throw new Error( "firebase.database().ref().push(_, *) 'onComplete' must be a function if provided.", @@ -298,15 +271,13 @@ export default class DatabaseReference return new DatabaseThenableReference( this._database, pathChild(this.path, id), - Promise.resolve(apChild(this).child.call(this, id, MODULAR_DEPRECATION_ARG)), - ) as unknown as FirebaseDatabaseTypes.ThenableReference; + Promise.resolve(this.child(id)), + ) as unknown as ThenableReference; } - const pushRef = apChild(this).child.call(this, id, MODULAR_DEPRECATION_ARG); + const pushRef = this.child(id) as DatabaseReferenceWithMethodsInternal; - const promise = apSet(pushRef) - .set.call(pushRef, value, onComplete, MODULAR_DEPRECATION_ARG) - .then(() => pushRef); + const promise = pushRef.set(value, onComplete).then(() => pushRef); if (onComplete) { promise.catch(() => {}); @@ -316,10 +287,10 @@ export default class DatabaseReference this._database, pathChild(this.path, id), promise, - ) as unknown as FirebaseDatabaseTypes.ThenableReference; + ) as unknown as ThenableReference; } - onDisconnect(): FirebaseDatabaseTypes.OnDisconnect { + onDisconnect(): OnDisconnect { return new DatabaseOnDisconnect(this); } } diff --git a/packages/database/lib/DatabaseSyncTree.ts b/packages/database/lib/DatabaseSyncTree.ts index 0a711c600b..1cffb83270 100644 --- a/packages/database/lib/DatabaseSyncTree.ts +++ b/packages/database/lib/DatabaseSyncTree.ts @@ -15,7 +15,7 @@ * */ -import { createDeprecationProxy, isString } from '@react-native-firebase/app/dist/module/common'; +import { isString } from '@react-native-firebase/app/dist/module/common'; import NativeError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; import SharedEventEmitter from '@react-native-firebase/app/dist/module/internal/SharedEventEmitter'; import { getReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; @@ -26,11 +26,11 @@ import type { DatabaseSnapshotInternal, RNFBDatabaseModule, } from './types/internal'; -import type { FirebaseDatabaseTypes } from './types/namespaced'; +import type { DatabaseReference, DataSnapshot } from './types/database'; interface DatabaseSyncTreeRegistration { eventType: string; - ref: FirebaseDatabaseTypes.Reference; + ref: DatabaseReference; path: string; key: string; appName: string; @@ -145,7 +145,7 @@ class DatabaseSyncTree { return; } - let snapshot: FirebaseDatabaseTypes.DataSnapshot; + let snapshot: DataSnapshot; let previousChildName: string | null | undefined; if (!event.data) { @@ -153,14 +153,10 @@ class DatabaseSyncTree { } if (event.eventType === 'value') { - snapshot = createDeprecationProxy( - new DatabaseDataSnapshot(registration.ref, event.data as DatabaseSnapshotInternal), - ) as FirebaseDatabaseTypes.DataSnapshot; + snapshot = new DatabaseDataSnapshot(registration.ref, event.data as DatabaseSnapshotInternal); } else { const childData = event.data as DatabaseChildSnapshotResultInternal; - snapshot = createDeprecationProxy( - new DatabaseDataSnapshot(registration.ref, childData.snapshot), - ) as FirebaseDatabaseTypes.DataSnapshot; + snapshot = new DatabaseDataSnapshot(registration.ref, childData.snapshot); previousChildName = childData.previousChildName; } diff --git a/packages/database/lib/DatabaseThenableReference.ts b/packages/database/lib/DatabaseThenableReference.ts index 708eb7b7d6..45708ecb57 100644 --- a/packages/database/lib/DatabaseThenableReference.ts +++ b/packages/database/lib/DatabaseThenableReference.ts @@ -15,15 +15,13 @@ * */ -import { createDeprecationProxy } from '@react-native-firebase/app/dist/module/common'; - import type { DatabaseInternal } from './types/internal'; -import type { FirebaseDatabaseTypes } from './types/namespaced'; +import type { DatabaseReference } from './types/database'; type DatabaseReferenceConstructor = new ( database: DatabaseInternal, path: string, -) => FirebaseDatabaseTypes.Reference; +) => DatabaseReference; let DatabaseReferenceClass: DatabaseReferenceConstructor | null = null; @@ -32,24 +30,18 @@ export function provideReferenceClass(databaseReference: DatabaseReferenceConstr } export default class DatabaseThenableReference implements Pick< - Promise, + Promise, 'then' | 'catch' > { - _ref: FirebaseDatabaseTypes.Reference; - _promise: Promise; + _ref: DatabaseReference; + _promise: Promise; - constructor( - database: DatabaseInternal, - path: string, - promise: Promise, - ) { + constructor(database: DatabaseInternal, path: string, promise: Promise) { if (!DatabaseReferenceClass) { throw new Error('DatabaseReference class has not been provided.'); } - this._ref = createDeprecationProxy( - new DatabaseReferenceClass(database, path), - ) as FirebaseDatabaseTypes.Reference; + this._ref = new DatabaseReferenceClass(database, path); this._promise = promise; return new Proxy(this, { @@ -63,11 +55,11 @@ export default class DatabaseThenableReference implements Pick< }) as unknown as DatabaseThenableReference; } - get then(): Promise['then'] { + get then(): Promise['then'] { return this._promise.then.bind(this._promise); } - get catch(): Promise['catch'] { + get catch(): Promise['catch'] { return this._promise.catch.bind(this._promise); } } diff --git a/packages/database/lib/index.ts b/packages/database/lib/index.ts index 120cf4c6bd..fc82496925 100644 --- a/packages/database/lib/index.ts +++ b/packages/database/lib/index.ts @@ -15,11 +15,296 @@ * */ -// Export modular API -export * from './modular'; +import { + isAndroid, + isBoolean, + isNumber, + isString, +} from '@react-native-firebase/app/dist/module/common'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { + DatabaseInternal, + DatabaseReferenceInternal, + DatabaseTransactionInternal, + DatabaseWithMethodsInternal, + RNFBDatabaseModule, + ServerValueStaticInternal, +} from './types/internal'; +import './types/internal'; +import DatabaseReferenceImpl from './DatabaseReference'; +import DatabaseStatics from './DatabaseStatics'; +import DatabaseTransaction from './DatabaseTransaction'; +import type { + Database, + DatabaseReference, + DataSnapshot, + EmulatorMockTokenOptions, +} from './types/database'; +import { version } from './version'; +import fallBackModule from './web/RNFBDatabaseModule'; + +const nativeModuleName = 'RNFBDatabaseModule'; + +const nativeModuleNames = [ + nativeModuleName, + 'RNFBDatabaseReferenceModule', + 'RNFBDatabaseQueryModule', + 'RNFBDatabaseOnDisconnectModule', + 'RNFBDatabaseTransactionModule', +] as const; + +function ap(reference: DatabaseReference): DatabaseReferenceInternal { + return reference as unknown as DatabaseReferenceInternal; +} + +class FirebaseDatabaseModule extends FirebaseModule { + readonly type = 'database' as const; + _serverTimeOffset: number; + _customUrlOrRegion: string | null; + _transaction: DatabaseTransactionInternal; + + private get nativeModule(): RNFBDatabaseModule { + return this.native as RNFBDatabaseModule; + } + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + databaseUrl?: string | null, + ) { + super(app, config, databaseUrl); + this._serverTimeOffset = 0; + this._customUrlOrRegion = databaseUrl || this.app.options.databaseURL || null; + this._transaction = new DatabaseTransaction(this as unknown as DatabaseInternal); + setTimeout(() => { + this._syncServerTimeOffset(); + }, 100); + } + + _syncServerTimeOffset(): void { + ap(this.ref('.info/serverTimeOffset')).on('value', (snapshot: DataSnapshot) => { + this._serverTimeOffset = snapshot.val(); + }); + } + + getServerTime(): Date { + return new Date(Date.now() + this._serverTimeOffset); + } + + ref(path = '/'): DatabaseReference { + if (!isString(path)) { + throw new Error("firebase.app().database().ref(*) 'path' must be a string value."); + } + + if (/[#$\[\]'?]/g.test(path)) { + throw new Error( + `Paths must be non-empty strings and can't contain #, $, [, ], ' or ? | path: ${path}`, + ); + } + + return new DatabaseReferenceImpl(this, path); + } + + refFromURL(url: string): DatabaseReference { + if (!isString(url) || !url.startsWith('https://')) { + throw new Error( + "firebase.app().database().refFromURL(*) 'url' must be a valid database URL.", + ); + } + + if (!url.includes(this._customUrlOrRegion as string)) { + throw new Error( + `firebase.app().database().refFromURL(*) 'url' must be the same domain as the current instance (${this._customUrlOrRegion}). To use a different database domain, create a new Firebase instance.`, + ); + } + + let path = url.replace(this._customUrlOrRegion as string, ''); + if (path.includes('?')) { + path = path.slice(0, path.indexOf('?')); + } + + return new DatabaseReferenceImpl(this, path || '/'); + } + + goOnline(): Promise { + return this.nativeModule.goOnline(); + } + + goOffline(): Promise { + return this.nativeModule.goOffline(); + } + + setPersistenceEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "firebase.app().database().setPersistenceEnabled(*) 'enabled' must be a boolean value.", + ); + } + + return this.nativeModule.setPersistenceEnabled(enabled); + } + + setLoggingEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "firebase.app().database().setLoggingEnabled(*) 'enabled' must be a boolean value.", + ); + } + + return this.nativeModule.setLoggingEnabled(enabled); + } + + setPersistenceCacheSizeBytes(bytes: number): Promise { + if (!isNumber(bytes)) { + throw new Error( + "firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be a number value.", + ); + } + + if (bytes < 1048576) { + throw new Error( + "firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be greater than 1048576 bytes (1MB).", + ); + } + + if (bytes > 104857600) { + throw new Error( + "firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be less than 104857600 bytes (100MB).", + ); + } + + return this.nativeModule.setPersistenceCacheSizeBytes(bytes); + } + + useEmulator(host: string, port: number): [string, number] { + if (!host || !isString(host) || !port || !isNumber(port)) { + throw new Error('firebase.database().useEmulator() takes a non-empty host and port'); + } + let remappedHost = host; + const androidBypassEmulatorUrlRemap = + typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && + this.firebaseJson.android_bypass_emulator_url_remap; + if (!androidBypassEmulatorUrlRemap && isAndroid && remappedHost) { + if (remappedHost.startsWith('localhost')) { + remappedHost = remappedHost.replace('localhost', '10.0.2.2'); + // eslint-disable-next-line no-console + console.log( + 'Mapping database host "localhost" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', + ); + } + if (remappedHost.startsWith('127.0.0.1')) { + remappedHost = remappedHost.replace('127.0.0.1', '10.0.2.2'); + // eslint-disable-next-line no-console + console.log( + 'Mapping database host "127.0.0.1" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', + ); + } + } + this.nativeModule.useEmulator(remappedHost, port); + return [remappedHost, port]; + } +} + +const config: ModuleConfig = { + namespace: 'database', + nativeModuleName: [...nativeModuleNames], + nativeEvents: ['database_transaction_event', 'database_sync_event'], + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: true, +}; + +export const SDK_VERSION = version; + +const { ServerValue } = DatabaseStatics; + +export function getDatabase(app?: FirebaseApp, url?: string): Database { + return getOrCreateModularInstance( + FirebaseDatabaseModule, + config, + app, + url, + ) as unknown as Database; +} + +export function connectDatabaseEmulator( + db: Database, + host: string, + port: number, + options?: { + mockUserToken?: EmulatorMockTokenOptions | string; + }, +): void { + (db as DatabaseWithMethodsInternal).useEmulator(host, port, options); +} + +export function goOffline(db: Database): void { + (db as DatabaseWithMethodsInternal).goOffline(); +} + +export function goOnline(db: Database): void { + (db as DatabaseWithMethodsInternal).goOnline(); +} + +export function ref(db: Database, path?: string): DatabaseReference { + return (db as DatabaseWithMethodsInternal).ref(path); +} + +export function refFromURL(db: Database, url: string): DatabaseReference { + return (db as DatabaseWithMethodsInternal).refFromURL(url); +} + +export function setPersistenceEnabled(db: Database, enabled: boolean): Promise { + return (db as DatabaseWithMethodsInternal).setPersistenceEnabled(enabled) as Promise; +} + +export function setLoggingEnabled(db: Database, enabled: boolean): Promise { + return (db as DatabaseWithMethodsInternal).setLoggingEnabled(enabled) as Promise; +} + +export function setPersistenceCacheSizeBytes(db: Database, bytes: number): Promise { + return (db as DatabaseWithMethodsInternal).setPersistenceCacheSizeBytes(bytes) as Promise; +} + +export function forceLongPolling(): void { + throw new Error('forceLongPolling() is not implemented'); +} + +export function forceWebSockets(): void { + throw new Error('forceWebSockets() is not implemented'); +} + +export function serverTimestamp(): object { + return ServerValue.TIMESTAMP; +} + +export function getServerTime(db: Database): Date { + return (db as DatabaseWithMethodsInternal).getServerTime(); +} + +export function increment(delta: number): object { + return (ServerValue as ServerValueStaticInternal).increment(delta); +} + +export function enableLogging(enabled: boolean, persistent?: boolean): any; +export function enableLogging(logger: (message: string) => unknown): any; +export function enableLogging( + _enabledOrLogger: boolean | ((message: string) => unknown), + _persistent?: boolean, +): any { + throw new Error('enableLogging() is not implemented'); +} + export type * from './types/database'; +export * from './modular/query'; +export * from './modular/transaction'; -// Export namespaced API -export type { FirebaseDatabaseTypes } from './types/namespaced'; -export * from './namespaced'; -export { default } from './namespaced'; +for (const moduleName of nativeModuleNames) { + setReactNativeModule(moduleName, fallBackModule as unknown as Record); +} diff --git a/packages/database/lib/modular.ts b/packages/database/lib/modular.ts deleted file mode 100644 index c6524573c6..0000000000 --- a/packages/database/lib/modular.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import DatabaseStatics from './DatabaseStatics'; -import type { Database, DatabaseReference, EmulatorMockTokenOptions } from './types/database'; -import type { - AppWithDatabaseInternal, - DatabaseWithMethodsInternal, - ServerValueStaticInternal, -} from './types/internal'; - -const { ServerValue } = DatabaseStatics; - -type FirebaseApp = ReactNativeFirebase.FirebaseApp; - -export function getDatabase(app?: FirebaseApp, url?: string): Database { - if (app) { - return (getApp(app.name) as unknown as AppWithDatabaseInternal).database( - url, - ) as unknown as Database; - } - - return (getApp() as unknown as AppWithDatabaseInternal).database(url) as unknown as Database; -} - -export function connectDatabaseEmulator( - db: Database, - host: string, - port: number, - options?: { - mockUserToken?: EmulatorMockTokenOptions | string; - }, -): void { - (db as DatabaseWithMethodsInternal).useEmulator.call( - db, - host, - port, - options, - MODULAR_DEPRECATION_ARG, - ); -} - -export function goOffline(db: Database): void { - (db as DatabaseWithMethodsInternal).goOffline.call(db, MODULAR_DEPRECATION_ARG); -} - -export function goOnline(db: Database): void { - (db as DatabaseWithMethodsInternal).goOnline.call(db, MODULAR_DEPRECATION_ARG); -} - -export function ref(db: Database, path?: string): DatabaseReference { - return (db as DatabaseWithMethodsInternal).ref.call(db, path, MODULAR_DEPRECATION_ARG); -} - -export function refFromURL(db: Database, url: string): DatabaseReference { - return (db as DatabaseWithMethodsInternal).refFromURL.call(db, url, MODULAR_DEPRECATION_ARG); -} - -export function setPersistenceEnabled(db: Database, enabled: boolean): Promise { - return (db as DatabaseWithMethodsInternal).setPersistenceEnabled.call( - db, - enabled, - MODULAR_DEPRECATION_ARG, - ) as Promise; -} - -export function setLoggingEnabled(db: Database, enabled: boolean): Promise { - return (db as DatabaseWithMethodsInternal).setLoggingEnabled.call( - db, - enabled, - MODULAR_DEPRECATION_ARG, - ) as Promise; -} - -export function setPersistenceCacheSizeBytes(db: Database, bytes: number): Promise { - return (db as DatabaseWithMethodsInternal).setPersistenceCacheSizeBytes.call( - db, - bytes, - MODULAR_DEPRECATION_ARG, - ) as Promise; -} - -export function forceLongPolling(): void { - throw new Error('forceLongPolling() is not implemented'); -} - -export function forceWebSockets(): void { - throw new Error('forceWebSockets() is not implemented'); -} - -export function serverTimestamp(): object { - return ServerValue.TIMESTAMP; -} - -export function getServerTime(db: Database): Date { - return (db as DatabaseWithMethodsInternal).getServerTime.call(db, MODULAR_DEPRECATION_ARG); -} - -export function increment(delta: number): object { - return (ServerValue as ServerValueStaticInternal).increment.call( - ServerValue, - delta, - MODULAR_DEPRECATION_ARG, - ); -} - -export function enableLogging(enabled: boolean, persistent?: boolean): any; -export function enableLogging(logger: (message: string) => unknown): any; -export function enableLogging( - _enabledOrLogger: boolean | ((message: string) => unknown), - _persistent?: boolean, -): any { - throw new Error('enableLogging() is not implemented'); -} - -export * from './modular/query'; -export * from './modular/transaction'; diff --git a/packages/database/lib/modular/query.ts b/packages/database/lib/modular/query.ts index 23b3e00265..9773f2b71c 100644 --- a/packages/database/lib/modular/query.ts +++ b/packages/database/lib/modular/query.ts @@ -15,7 +15,6 @@ * */ -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; import type { DataSnapshot, DatabaseReference, @@ -46,10 +45,7 @@ class DatabaseQueryConstraint extends QueryConstraint { } _apply(query: Query): Query { - return (query as QueryWithModifiersInternal)[this.type].apply(query, [ - ...this.args, - MODULAR_DEPRECATION_ARG, - ]); + return (query as QueryWithModifiersInternal)[this.type](...this.args); } } @@ -129,12 +125,9 @@ function addEventListener( if (options?.onlyOnce) { const userCallback = callback; listenerCallback = ((snapshot: DataSnapshot, previousChildName?: string | null) => { - queryWithSubscriptions.off.call( - queryRef, + queryWithSubscriptions.off( eventType, listenerCallback as (snapshot: DataSnapshot, previousChildName?: string | null) => unknown, - null, - MODULAR_DEPRECATION_ARG, ); return ( @@ -143,22 +136,16 @@ function addEventListener( }) as SnapshotCallbackInternal; } - queryWithSubscriptions.on.call( - queryRef, + queryWithSubscriptions.on( eventType, listenerCallback as (snapshot: DataSnapshot, previousChildName?: string | null) => unknown, cancelCallback as ((error: Error) => unknown) | undefined, - null, - MODULAR_DEPRECATION_ARG, ); return () => - queryWithSubscriptions.off.call( - queryRef, + queryWithSubscriptions.off( eventType, listenerCallback as (snapshot: DataSnapshot, previousChildName?: string | null) => unknown, - null, - MODULAR_DEPRECATION_ARG, ); } @@ -312,24 +299,14 @@ export function onChildRemoved( } export function set(ref: DatabaseReference, value: unknown): Promise { - return (ref as DatabaseReferenceWithMethodsInternal).set.call( - ref, - value, - () => {}, - MODULAR_DEPRECATION_ARG, - ); + return (ref as DatabaseReferenceWithMethodsInternal).set(value); } export function setPriority( ref: DatabaseReference, priority: string | number | null, ): Promise { - return (ref as DatabaseReferenceWithMethodsInternal).setPriority.call( - ref, - priority, - () => {}, - MODULAR_DEPRECATION_ARG, - ); + return (ref as DatabaseReferenceWithMethodsInternal).setPriority(priority); } export function setWithPriority( @@ -337,24 +314,11 @@ export function setWithPriority( value: unknown, priority: string | number | null, ): Promise { - return (ref as DatabaseReferenceWithMethodsInternal).setWithPriority.call( - ref, - value, - priority, - () => {}, - MODULAR_DEPRECATION_ARG, - ); + return (ref as DatabaseReferenceWithMethodsInternal).setWithPriority(value, priority); } export function get(queryRef: Query): Promise { - return (queryRef as QueryWithSubscriptionMethodsInternal).once.call( - queryRef, - 'value', - () => {}, - () => {}, - {}, - MODULAR_DEPRECATION_ARG, - ); + return (queryRef as QueryWithSubscriptionMethodsInternal).once('value'); } export function off( @@ -366,45 +330,25 @@ export function off( } export function child(parent: DatabaseReference, path: string): DatabaseReference { - return (parent as DatabaseReferenceWithMethodsInternal).child.call( - parent, - path, - MODULAR_DEPRECATION_ARG, - ); + return (parent as DatabaseReferenceWithMethodsInternal).child(path); } export function onDisconnect(ref: DatabaseReference): OnDisconnect { - return (ref as DatabaseReferenceWithMethodsInternal).onDisconnect.call( - ref, - MODULAR_DEPRECATION_ARG, - ); + return (ref as DatabaseReferenceWithMethodsInternal).onDisconnect(); } export function keepSynced(ref: DatabaseReference, bool: boolean): Promise { - return (ref as DatabaseReferenceWithMethodsInternal).keepSynced.call( - ref, - bool, - MODULAR_DEPRECATION_ARG, - ); + return (ref as DatabaseReferenceWithMethodsInternal).keepSynced(bool); } export function push(parent: DatabaseReference, value?: unknown): ThenableReference { - return (parent as DatabaseReferenceWithMethodsInternal).push.call( - parent, - value, - undefined, - MODULAR_DEPRECATION_ARG, - ); + return (parent as DatabaseReferenceWithMethodsInternal).push(value); } export function remove(ref: DatabaseReference): Promise { - return (ref as DatabaseReferenceWithMethodsInternal).remove.call(ref, MODULAR_DEPRECATION_ARG); + return (ref as DatabaseReferenceWithMethodsInternal).remove(); } export function update(ref: DatabaseReference, values: object): Promise { - return (ref as DatabaseReferenceWithMethodsInternal).update.call( - ref, - values, - MODULAR_DEPRECATION_ARG, - ); + return (ref as DatabaseReferenceWithMethodsInternal).update(values); } diff --git a/packages/database/lib/modular/transaction.ts b/packages/database/lib/modular/transaction.ts index ed18782a9e..f60a5e2e3c 100644 --- a/packages/database/lib/modular/transaction.ts +++ b/packages/database/lib/modular/transaction.ts @@ -15,7 +15,6 @@ * */ -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; import type { DatabaseReference, TransactionOptions, TransactionResult } from '../types/database'; import type { DatabaseReferenceWithTransactionInternal } from '../types/internal'; @@ -24,11 +23,9 @@ export function runTransaction( transactionUpdate: (currentData: any) => unknown, options?: TransactionOptions, ): Promise { - return (ref as DatabaseReferenceWithTransactionInternal).transaction.call( - ref, + return (ref as DatabaseReferenceWithTransactionInternal).transaction( transactionUpdate, undefined, options?.applyLocally, - MODULAR_DEPRECATION_ARG, ); } diff --git a/packages/database/lib/namespaced.ts b/packages/database/lib/namespaced.ts deleted file mode 100644 index a7f6ab4b59..0000000000 --- a/packages/database/lib/namespaced.ts +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { - isAndroid, - isBoolean, - isNumber, - isString, - MODULAR_DEPRECATION_ARG, - createDeprecationProxy, -} from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, - type ModuleConfig, -} from '@react-native-firebase/app/dist/module/internal'; -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import type { - DatabaseInternal, - DatabaseReferenceInternal, - DatabaseTransactionInternal, - RNFBDatabaseModule, -} from './types/internal'; -import type { FirebaseDatabaseTypes } from './types/namespaced'; -import './types/internal'; -import DatabaseReference from './DatabaseReference'; -import DatabaseStatics from './DatabaseStatics'; -import DatabaseTransaction from './DatabaseTransaction'; -import { version } from './version'; -import fallBackModule from './web/RNFBDatabaseModule'; - -const namespace = 'database'; - -const nativeModuleName = 'RNFBDatabaseModule'; - -const nativeModuleNames = [ - nativeModuleName, - 'RNFBDatabaseReferenceModule', - 'RNFBDatabaseQueryModule', - 'RNFBDatabaseOnDisconnectModule', - 'RNFBDatabaseTransactionModule', -] as const; - -function ap(reference: DatabaseReference): DatabaseReferenceInternal { - return reference as DatabaseReferenceInternal; -} - -class FirebaseDatabaseModule extends FirebaseModule { - readonly type = 'database' as const; - _serverTimeOffset: number; - _customUrlOrRegion: string | null; - _transaction: DatabaseTransactionInternal; - - private get nativeModule(): RNFBDatabaseModule { - return this.native as RNFBDatabaseModule; - } - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - databaseUrl?: string | null, - ) { - super(app, config, databaseUrl); - this._serverTimeOffset = 0; - this._customUrlOrRegion = databaseUrl || this.app.options.databaseURL || null; - this._transaction = new DatabaseTransaction(this as unknown as DatabaseInternal); - setTimeout(() => { - this._syncServerTimeOffset(); - }, 100); - } - - _syncServerTimeOffset(): void { - ap(this.ref('.info/serverTimeOffset')).on( - 'value', - (snapshot: { val(): number }) => { - this._serverTimeOffset = snapshot.val(); - }, - MODULAR_DEPRECATION_ARG, - ); - } - - getServerTime(): Date { - return new Date(Date.now() + this._serverTimeOffset); - } - - ref(path = '/'): DatabaseReference { - if (!isString(path)) { - throw new Error("firebase.app().database().ref(*) 'path' must be a string value."); - } - - if (/[#$\[\]'?]/g.test(path)) { - throw new Error( - `Paths must be non-empty strings and can't contain #, $, [, ], ' or ? | path: ${path}`, - ); - } - - return createDeprecationProxy(new DatabaseReference(this, path)); - } - - refFromURL(url: string): DatabaseReference { - if (!isString(url) || !url.startsWith('https://')) { - throw new Error( - "firebase.app().database().refFromURL(*) 'url' must be a valid database URL.", - ); - } - - if (!url.includes(this._customUrlOrRegion as string)) { - throw new Error( - `firebase.app().database().refFromURL(*) 'url' must be the same domain as the current instance (${this._customUrlOrRegion}). To use a different database domain, create a new Firebase instance.`, - ); - } - - let path = url.replace(this._customUrlOrRegion as string, ''); - if (path.includes('?')) { - path = path.slice(0, path.indexOf('?')); - } - - return createDeprecationProxy(new DatabaseReference(this, path || '/')); - } - - goOnline(): Promise { - return this.nativeModule.goOnline(); - } - - goOffline(): Promise { - return this.nativeModule.goOffline(); - } - - setPersistenceEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.app().database().setPersistenceEnabled(*) 'enabled' must be a boolean value.", - ); - } - - return this.nativeModule.setPersistenceEnabled(enabled); - } - - setLoggingEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.app().database().setLoggingEnabled(*) 'enabled' must be a boolean value.", - ); - } - - return this.nativeModule.setLoggingEnabled(enabled); - } - - setPersistenceCacheSizeBytes(bytes: number): Promise { - if (!isNumber(bytes)) { - throw new Error( - "firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be a number value.", - ); - } - - if (bytes < 1048576) { - throw new Error( - "firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be greater than 1048576 bytes (1MB).", - ); - } - - if (bytes > 104857600) { - throw new Error( - "firebase.app().database().setPersistenceCacheSizeBytes(*) 'bytes' must be less than 104857600 bytes (100MB).", - ); - } - - return this.nativeModule.setPersistenceCacheSizeBytes(bytes); - } - - useEmulator(host: string, port: number): [string, number] { - if (!host || !isString(host) || !port || !isNumber(port)) { - throw new Error('firebase.database().useEmulator() takes a non-empty host and port'); - } - let remappedHost = host; - const androidBypassEmulatorUrlRemap = - typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && - this.firebaseJson.android_bypass_emulator_url_remap; - if (!androidBypassEmulatorUrlRemap && isAndroid && remappedHost) { - if (remappedHost.startsWith('localhost')) { - remappedHost = remappedHost.replace('localhost', '10.0.2.2'); - // eslint-disable-next-line no-console - console.log( - 'Mapping database host "localhost" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', - ); - } - if (remappedHost.startsWith('127.0.0.1')) { - remappedHost = remappedHost.replace('127.0.0.1', '10.0.2.2'); - // eslint-disable-next-line no-console - console.log( - 'Mapping database host "127.0.0.1" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', - ); - } - } - this.nativeModule.useEmulator(remappedHost, port); - return [remappedHost, port]; - } -} - -export const SDK_VERSION = version; - -const databaseNamespace = createModuleNamespace({ - statics: DatabaseStatics, - version, - namespace, - nativeModuleName: [...nativeModuleNames], - nativeEvents: ['database_transaction_event', 'database_sync_event'], - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: true, - ModuleClass: FirebaseDatabaseModule, -}); - -type DatabaseNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseDatabaseTypes.Module, - FirebaseDatabaseTypes.Statics -> & { - database: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseDatabaseTypes.Module, - FirebaseDatabaseTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -export default databaseNamespace as unknown as DatabaseNamespace; - -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'database', - FirebaseDatabaseTypes.Module, - FirebaseDatabaseTypes.Statics, - true - >; - -for (const moduleName of nativeModuleNames) { - setReactNativeModule(moduleName, fallBackModule as unknown as Record); -} diff --git a/packages/database/lib/types/database.ts b/packages/database/lib/types/database.ts index 0052ee6051..b6c0540630 100644 --- a/packages/database/lib/types/database.ts +++ b/packages/database/lib/types/database.ts @@ -59,7 +59,7 @@ export type EmulatorMockTokenOptions = ({ user_id: string } | { sub: string }) & export declare class Database { readonly app: FirebaseApp; - readonly type: 'database'; + readonly 'type' = 'database'; } export declare class TransactionResult { @@ -94,10 +94,10 @@ export interface IteratedDataSnapshot extends DataSnapshot { } export declare class DataSnapshot { - readonly key: string | null; - readonly priority: string | number | null; readonly ref: DatabaseReference; - readonly size: number; + get key(): string | null; + get priority(): string | number | null; + get size(): number; child(path: string): DataSnapshot; exists(): boolean; exportVal(): any; @@ -112,7 +112,7 @@ export declare class OnDisconnect { cancel(): Promise; remove(): Promise; set(value: unknown): Promise; - setWithPriority(value: unknown, priority: string | number | null): Promise; + setWithPriority(value: unknown, priority: number | string | null): Promise; update(values: object): Promise; } diff --git a/packages/database/lib/types/internal.ts b/packages/database/lib/types/internal.ts index db338592df..0b45cf3ea9 100644 --- a/packages/database/lib/types/internal.ts +++ b/packages/database/lib/types/internal.ts @@ -30,12 +30,8 @@ import type { ThenableReference, TransactionResult, } from './database'; -import type { FirebaseDatabaseTypes } from './namespaced'; import type { DatabaseQueryModifier } from '../DatabaseQueryModifiers'; -/** Optional final argument passed by modular API wrappers (MODULAR_DEPRECATION_ARG). */ -export type DatabaseModularDeprecationArg = string; - /** App instance with database() method (e.g. from getApp() when used for getDatabase()). */ export interface AppWithDatabaseInternal { database(url?: string): Database; @@ -47,19 +43,15 @@ export interface DatabaseWithMethodsInternal extends Database { host: string, port: number, options?: { mockUserToken?: EmulatorMockTokenOptions | string }, - deprecationArg?: DatabaseModularDeprecationArg, - ): unknown; - goOffline(deprecationArg?: DatabaseModularDeprecationArg): unknown; - goOnline(deprecationArg?: DatabaseModularDeprecationArg): unknown; - ref(path?: string, deprecationArg?: DatabaseModularDeprecationArg): DatabaseReference; - refFromURL(url: string, deprecationArg?: DatabaseModularDeprecationArg): DatabaseReference; - setPersistenceEnabled(enabled: boolean, deprecationArg?: DatabaseModularDeprecationArg): unknown; - setLoggingEnabled(enabled: boolean, deprecationArg?: DatabaseModularDeprecationArg): unknown; - setPersistenceCacheSizeBytes( - bytes: number, - deprecationArg?: DatabaseModularDeprecationArg, ): unknown; - getServerTime(deprecationArg?: DatabaseModularDeprecationArg): Date; + goOffline(): unknown; + goOnline(): unknown; + ref(path?: string): DatabaseReference; + refFromURL(url: string): DatabaseReference; + setPersistenceEnabled(enabled: boolean): unknown; + setLoggingEnabled(enabled: boolean): unknown; + setPersistenceCacheSizeBytes(bytes: number): unknown; + getServerTime(): Date; } /** Native emitter subscription returned by addListener. */ @@ -157,7 +149,7 @@ export interface RNFBDatabaseModule { once( path: string, modifiers: DatabaseQueryModifier[], - eventType: FirebaseDatabaseTypes.EventType, + eventType: EventType, ): Promise; on(props: DatabaseListenPropsInternal): void; off(queryKey: string, eventRegistrationKey: string): void | Promise; @@ -197,15 +189,11 @@ declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { export interface DatabaseReferenceInternal { readonly path: string; on( - eventType: FirebaseDatabaseTypes.EventType, - callback: (data: FirebaseDatabaseTypes.DataSnapshot, previousChildKey?: string | null) => void, - cancelCallbackOrContext?: - | ((a: Error) => void) - | Record - | DatabaseModularDeprecationArg - | null, + eventType: EventType, + callback: (data: DataSnapshot, previousChildKey?: string | null) => void, + cancelCallbackOrContext?: ((a: Error) => void) | Record | null, context?: Record | null, - ): (a: FirebaseDatabaseTypes.DataSnapshot | null, b?: string | null) => void; + ): (a: DataSnapshot | null, b?: string | null) => void; } /** Query instance viewed through the chainable query modifier methods. */ @@ -217,25 +205,19 @@ export interface QueryWithSubscriptionMethodsInternal extends Query { on( eventType: EventType, callback: (snapshot: DataSnapshot, previousChildName?: string | null) => unknown, - cancelCallbackOrContext?: - | ((error: Error) => unknown) - | ListenOptions - | DatabaseModularDeprecationArg, + cancelCallbackOrContext?: ((error: Error) => unknown) | ListenOptions, context?: ListenOptions | null, - deprecationArg?: DatabaseModularDeprecationArg, ): unknown; off( eventType?: EventType, callback?: (snapshot: DataSnapshot, previousChildName?: string | null) => unknown, context?: null, - deprecationArg?: DatabaseModularDeprecationArg, ): void; once( eventType: EventType, successCallback?: (snapshot: DataSnapshot, previousChildName?: string | null) => unknown, failureCallbackContext?: ((error: Error) => void) | null, context?: ListenOptions, - deprecationArg?: DatabaseModularDeprecationArg, ): Promise; } @@ -246,32 +228,22 @@ export interface QueryConstraintWithApplyInternal extends QueryConstraint { /** Database reference viewed through the mutating modular helpers. */ export interface DatabaseReferenceWithMethodsInternal extends DatabaseReference { - set( - value: unknown, - onComplete?: () => void, - deprecationArg?: DatabaseModularDeprecationArg, - ): Promise; + set(value: unknown, onComplete?: (error: Error | null) => void): Promise; setPriority( priority: string | number | null, - onComplete?: () => void, - deprecationArg?: DatabaseModularDeprecationArg, + onComplete?: (error: Error | null) => void, ): Promise; setWithPriority( value: unknown, priority: string | number | null, - onComplete?: () => void, - deprecationArg?: DatabaseModularDeprecationArg, + onComplete?: (error: Error | null) => void, ): Promise; - child(path: string, deprecationArg?: DatabaseModularDeprecationArg): DatabaseReference; - onDisconnect(deprecationArg?: DatabaseModularDeprecationArg): OnDisconnect; - keepSynced(value: boolean, deprecationArg?: DatabaseModularDeprecationArg): Promise; - push( - value?: unknown, - onComplete?: undefined, - deprecationArg?: DatabaseModularDeprecationArg, - ): ThenableReference; - remove(deprecationArg?: DatabaseModularDeprecationArg): Promise; - update(values: object, deprecationArg?: DatabaseModularDeprecationArg): Promise; + child(path: string): DatabaseReference; + onDisconnect(): OnDisconnect; + keepSynced(value: boolean): Promise; + push(value?: unknown, onComplete?: undefined): ThenableReference; + remove(): Promise; + update(values: object): Promise; } /** Minimal transaction handler contract used by DatabaseReference and namespaced module. */ @@ -306,12 +278,11 @@ export interface DatabaseReferenceWithTransactionInternal extends DatabaseRefere transactionUpdate: (currentData: any) => unknown, onComplete?: undefined, applyLocally?: boolean, - deprecationArg?: DatabaseModularDeprecationArg, ): Promise; } /** Runtime ServerValue object shape used by modular wrappers. */ export interface ServerValueStaticInternal { TIMESTAMP: object; - increment(delta: number, deprecationArg?: DatabaseModularDeprecationArg): object; + increment(delta: number): object; } diff --git a/packages/database/lib/types/namespaced.ts b/packages/database/lib/types/namespaced.ts deleted file mode 100644 index 921d06ab54..0000000000 --- a/packages/database/lib/types/namespaced.ts +++ /dev/null @@ -1,1366 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase Database package for React Native. - * - * #### Example 1 - * - * Access the firebase export from the `database` package: - * - * ```js - * import { firebase } from '@react-native-firebase/database'; - * - * // firebase.database().X - * ``` - * - * #### Example 2 - * - * Using the default export from the `database` package: - * - * ```js - * import database from '@react-native-firebase/database'; - * - * // database().X - * ``` - * - * #### Example 3 - * - * Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/database'; - * - * // firebase.database().X - * ``` - * - * @firebase database - */ -/** - * @deprecated Use the exported types directly instead. - * FirebaseDatabaseTypes namespace is kept for backwards compatibility. - */ -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace FirebaseDatabaseTypes { - import FirebaseModule = ReactNativeFirebase.FirebaseModule; - - /** - * The ServerValue interface provides access to Firebase server values. - */ - /** - * @deprecated Use the exported `ServerValue` type instead. - */ - export interface ServerValue { - /** - * A placeholder value for auto-populating the current timestamp (time since the Unix epoch, - * in milliseconds) as determined by the Firebase servers. - * - * #### Example - * - * ```js - * firebase.database().ref('sessions').push({ - * startedAt: firebase.database.ServerValue.TIMESTAMP, - * }); - * ``` - */ - TIMESTAMP: object; - - /** - * Returns a placeholder value that can be used to atomically increment the current database value by the provided delta. - * - * #### Example - * - * ```js - * firebase.database().ref('posts/123').update({ - * likes: firebase.database.ServerValue.increment(1), - * }); - * ``` - * - * @param delta The amount to modify the current value atomically. - */ - increment(delta: number): object; - } - - /** - * Realtime Database statics. - * - * #### Example - * - * ```js - * firebase.database; - * ``` - */ - /** - * @deprecated Use the exported runtime/statics directly instead. - */ - export interface Statics { - /** - * Returns server specific values, such as the server timestamp. - * - * #### Example - * - * ```js - * firebase.database.ServerValue; - * ``` - */ - ServerValue: ServerValue; - SDK_VERSION: string; - } - - /** - * @deprecated Use the exported `TransactionResult` type instead. - */ - export interface TransactionResult { - committed: boolean; - snapshot: DataSnapshot; - } - - /** - * A Reference represents a specific location in your Database and can be used for reading or - * writing data to that Database location. - * - * You can reference the root or child location in your Database by calling `firebase.database().ref()` - * or `firebase.database().ref("child/path")`. - */ - /** - * @deprecated Use the exported `DatabaseReference` type instead. - */ - export interface Reference extends Query { - /** - * The parent location of a Reference. The parent of a root Reference is `null`. - * - * #### Example - * - * ```js - * firebase.database().ref().parent; // null - * firebase.database().ref('users/dave').parent; // 'users' reference - * ``` - */ - parent: Reference | null; - - /** - * The root Reference of the Database. - * - * #### Example - * - * ```js - * firebase.database().ref().root; // '/' reference path - * firebase.database().ref('users/ada').root; // '/' reference - * ``` - */ - root: Reference; - - /** - * Gets a Reference for the location at the specified relative path. - * - * The relative path can either be a simple child name (for example, "ada") or a deeper - * slash-separated path (for example, "ada/name/first"). - * - * #### Example - * - * ```js - * const usersRef = firebase.database().ref('users'); - * const adaRef = usersRef.child('ada/name/first'); // childRef path is 'users/ada/name/first' - * ``` - * - * @param path A relative path from this location to the desired child location. - */ - child(path: string): Reference; - - /** - * The last part of the Reference's path. - * For example, "ada" is the key for https://.firebaseio.com/users/ada. - * The key of a root Reference is null. - */ - key: string | null; - - /** - * Writes data to this Database location. - * - * This will overwrite any data at this location and all child locations. - * - * The effect of the write will be visible immediately, and the corresponding events - * ("value", "child_added", etc.) will be triggered. Synchronization of the data to the - * Firebase servers will also be started, and the returned Promise will resolve when - * complete. If provided, the `onComplete` callback will be called asynchronously after - * synchronization has finished. - * - * Passing `null` for the new value is equivalent to calling `remove();` namely, all data at - * this location and all child locations will be deleted. - * - * `set()` will remove any priority stored at this location, so if priority is meant to be - * preserved, you need to use `setWithPriority()` instead. - * - * Note that modifying data with set() will cancel any pending transactions at that location, - * so extreme care should be taken if mixing set() and transaction() to modify the same data. - * - * A single set() will generate a single "value" event at the location where the set() was performed. - * - * #### Example - Setting values - * - * ```js - * const ref = firebase.database().ref('users'); - * - * // Set a single node value - * await ref.child('ada/name/first').set('Ada'); - * await ref.child('ada/name/last').set('Lovelace'); - * - * // Set an object value in a single call - * await ref.child('ada/name').set({ - * first: 'Ada', - * last: 'Lovelace', - * }); - * ``` - * - * #### Example - On complete listener - * - * ```js - * const ref = firebase.database().ref('users'); - * - * await ref.child('ada/first/name').set('Ada', (error) => { - * if (error) console.error(error); - * }); - * ``` - * - * @param value The value to be written (string, number, boolean, object, array, or null). - * @param onComplete Callback called when write to server is complete. Contains the parameters (Error | null). - */ - set(value: any, onComplete?: (error: Error | null) => void): Promise; - - /** - * Writes multiple values to the Database at once. - * - * The `values` argument contains multiple property-value pairs that will be written to the Database - * together. Each child property can either be a simple property (for example, "name") or a - * relative path (for example, "name/first") from the current location to the data to update. - * - * As opposed to the `set()` method, `update()` can be use to selectively update only the referenced - * properties at the current location (instead of replacing all the child properties at the - * current location). - * - * The effect of the write will be visible immediately, and the corresponding events ('value', - * 'child_added', etc.) will be triggered. Synchronization of the data to the Firebase servers - * will also be started, and the returned Promise will resolve when complete. If provided, the - * `onComplete` callback will be called asynchronously after synchronization has finished. - * - * A single update() will generate a single "value" event at the location where the update() - * was performed, regardless of how many children were modified. - * - * Note that modifying data with update() will cancel any pending transactions at that location, - * so extreme care should be taken if mixing update() and transaction() to modify the same data. - * - * Passing `null` to `update()` will remove the data at this location. - * - * #### Example - * - * Modify the 'first' and 'last' properties, but leave other values unchanged at this location. - * - * ```js - * await firebase.database().ref('users/ada/name').update({ - * first: 'Ada', - * last: 'Lovelace', - * }) - * ``` - * - * @param values Object containing multiple values. - * @param onComplete Callback called when write to server is complete. Contains the parameters (Error | null). - */ - update( - values: { [key: string]: any }, - onComplete?: (error: Error | null) => void, - ): Promise; - - /** - * Sets a priority for the data at this Database location. Setting null removes any priority at this location. - * - * See {@link Query#orderByPriority} to learn how to use priority values in your query. - * - * #### Example - * - * ```js - * await firebase.database().ref('users/ada').setPriority(1, (error) => { - * if (error) console.error(error); - * }); - * ``` - * - * @param priority The priority value. - * @param onComplete Callback called when write to server is complete. Contains the parameters (Error | null). - */ - setPriority( - priority: string | number | null, - onComplete?: (error: Error | null) => void, - ): Promise; - - /** - * Writes data the Database location. Like `set()` but also specifies the priority for that data. - * - * #### Example - * - * ```js - * await firebase.database().ref('users/ada/name') - * .setWithPriority({ - * first: 'Ada', - * last: 'Lovelace', - * }, 1, (error) => { - * if (error) console.error(error); - * }); - * ``` - * - * @param newVal The new value to set. - * @param newPriority The new priority to set. - * @param onComplete Callback called when write to server is complete. Contains the parameters (Error | null). - */ - setWithPriority( - newVal: any, - newPriority: string | number | null, - onComplete?: (error: Error | null) => void, - ): Promise; - - /** - * Removes the data at this Database location. - * - * Any data at child locations will also be deleted. - * - * The effect of the remove will be visible immediately and the corresponding event 'value' will be triggered. - * Synchronization of the remove to the Firebase servers will also be started, and the returned Promise will - * resolve when complete. If provided, the onComplete callback will be called asynchronously after synchronization - * has finished. - * - * #### Example - * - * ```js - * await firebase.database().ref('users/ada/name') - * .remove(() => { - * console.log('Operation Complete'); - * }); - * ``` - * - * @param onComplete Callback called when write to server is complete. Contains the parameters (Error | null). - */ - remove(onComplete?: (error: Error | null) => void): Promise; - - /** - * Atomically modifies the data at this location. - * - * Atomically modify the data at this location. Unlike a normal `set()`, which just overwrites - * the data regardless of its previous value, `transaction()` is used to modify the existing - * value to a new value, ensuring there are no conflicts with other clients writing to the same - * location at the same time. - * - * To accomplish this, you pass `transaction()` an update function which is used to transform the - * current value into a new value. If another client writes to the location before your new - * value is successfully written, your update function will be called again with the new - * current value, and the write will be retried. This will happen repeatedly until your write - * succeeds without conflict or you abort the transaction by not returning a value from your - * update function. - * - * Note: Modifying data with `set()` will cancel any pending transactions at that location, so - * extreme care should be taken if mixing `set()` and `transaction()` to update the same data. - * - * Note: When using transactions with Security and Firebase Rules in place, be aware that a - * client needs `.read` access in addition to `.write` access in order to perform a transaction. - * This is because the client-side nature of transactions requires the client to read the data - * in order to transactionally update it. - * - * #### Example - * - * ```js - * const userRef = firebase.database().ref('users/ada/profileViews); - * - * userRef.transaction((currentViews) => { - * if (currentViews === null) return 1; - * return currentViews + 1; - * }); - * ``` - * - * @param transactionUpdate A developer-supplied function which will be passed the current data stored at this location (as a JavaScript object). The function should return the new value it would like written (as a JavaScript object). If undefined is returned (i.e. you return with no result) the transaction will be aborted and the data at this location will not be modified. - * @param onComplete A callback function that will be called when the transaction completes. The callback is passed three arguments: a possibly-null Error, a boolean indicating whether the transaction was committed, and a DataSnapshot indicating the final result. If the transaction failed abnormally, the first argument will be an Error object indicating the failure cause. If the transaction finished normally, but no data was committed because no data was returned from transactionUpdate, then second argument will be false. If the transaction completed and committed data to Firebase, the second argument will be true. Regardless, the third argument will be a DataSnapshot containing the resulting data in this location. - * @param applyLocally By default, events are raised each time the transaction update function runs. So if it is run multiple times, you may see intermediate states. You can set this to false to suppress these intermediate states and instead wait until the transaction has completed before events are raised. - */ - transaction( - transactionUpdate: (currentData: any) => any | undefined, - onComplete?: ( - error: Error | null, - committed: boolean, - finalResult: DataSnapshot | null, - ) => void, - applyLocally?: boolean, - ): Promise; - - /** - * Generates a new child location using a unique key and returns its `Reference`. - * - * This is the most common pattern for adding data to a collection of items. - * - * If you provide a value to `push()`, the value will be written to the generated location. - * If you don't pass a value, nothing will be written to the Database and the child will - * remain empty (but you can use the `Reference` elsewhere). - * - * The unique key generated by push() are ordered by the current time, so the resulting list - * of items will be chronologically sorted. The keys are also designed to be unguessable - * (they contain 72 random bits of entropy). - * - * #### Example - * - * ```js - * const newUserRef = firebase.database().ref('users').push(); - * console.log('New record key:', newUserRef.key); - * await newUserRef.set({ - * first: 'Ada', - * last: 'Lovelace', - * }); - * ``` - * - * @param value Optional value to be written at the generated location. - * @param onComplete Callback called when write to server is complete. - */ - push(value?: any, onComplete?: (error: Error | null) => void): ThenableReference; - - /** - * Returns an {@link OnDisconnect} instance. - * - * #### Example - * - * ```js - * const userDisconnectRef = firebase.database().ref('users/ada/isOnline').onDisconnect(); - * // When going offline - * await userDisconnectRef.update(false); - * ``` - */ - onDisconnect(): OnDisconnect; - } - - /** - * @deprecated Use the exported `ThenableReference` type instead. - */ - export interface ThenableReference - extends Reference, Pick, 'then' | 'catch'> {} - - /** - * A Query sorts and filters the data at a Database location so only a subset of the child data - * is included. This can be used to order a collection of data by some attribute (for example, - * height of dinosaurs) as well as to restrict a large list of items (for example, chat messages) - * down to a number suitable for synchronizing to the client. Queries are created by chaining - * together one or more of the filter methods defined here. - * - * Just as with a `Reference`, you can receive data from a Query by using the on() method. You will - * only receive events and `DataSnapshot`s for the subset of the data that matches your query. - */ - /** - * @deprecated Use the exported `Query` type instead. - */ - export interface Query { - /** - * Returns a Reference to the Query's location. - */ - ref: Reference; - - /** - * Creates a Query with the specified ending point. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows you to choose arbitrary starting and - * ending points for your queries. - * - * The ending point is inclusive, so children with exactly the specified value will be included - * in the query. The optional key argument can be used to further limit the range of the query. - * If it is specified, then children that have exactly the specified value must also have a key - * name less than or equal to the specified key. - * - * You can read more about endAt() in [Filtering data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#filtering_data). - * - * #### Example - * - * ```js - * const ref = firebase.database().ref('users'); - * const snapshot = await ref.orderByKey().endAt('Ada Lovelace').once('value'); - * ``` - * - * @param value The value to end at. The argument type depends on which `orderBy*()` function was used in this query. Specify a value that matches the `orderBy*()` type. When used in combination with `orderByKey()`, the value must be a string. - * @param key The child key to end at, among the children with the previously specified priority. This argument is only allowed if ordering by child, value, or priority. - */ - endAt(value: number | string | boolean | null, key?: string): Query; - - /** - * Creates a Query with the specified ending point. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows you to choose arbitrary starting and - * ending points for your queries. - * - * The optional key argument can be used to further limit the range of the query. If it is - * specified, then children that have exactly the specified value must also have exactly the - * specified key as their key name. This can be used to filter result sets with many matches for the same value. - * - * You can read more about equalTo() in [Filtering data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#filtering_data). - * - * #### Example - * - * ```js - * const ref = firebase.database().ref('users'); - * const snapshot = await ref.orderByChild('age').equalTo(30).once('value'); - * ``` - * - * @param value The value to match for. The argument type depends on which `orderBy*()` function was used in this query. Specify a value that matches the `orderBy*()` type. When used in combination with `orderByKey()`, the value must be a string. - * @param key The child key to start at, among the children with the previously specified priority. This argument is only allowed if ordering by child, value, or priority. - */ - equalTo(value: number | string | boolean | null, key?: string): Query; - - /** - * Returns whether or not the current and provided queries represent the same location, have the same query parameters. - * - * Two Reference objects are equivalent if they represent the same location and are from the same instance of - * {@link @firebase/app!FirebaseApp}. Equivalent queries share the same sort order, limits, and starting and ending points. - * - * #### Example - * - * ```js - * const ref1 = firebase.database().ref('users').orderByKey().endAt('Ada Lovelace'); - * const ref2 = firebase.database().ref('users').orderByKey(); - * - * console.log(ref1.isEqual(ref2)); // false - * ``` - * - * #### Example - * - * ```js - * const ref1 = firebase.database().ref('users').orderByKey().endAt('Ada Lovelace'); - * const ref2 = firebase.database().ref('users').endAt('Ada Lovelace').orderByKey(); - * - * console.log(ref1.isEqual(ref2)); // true - * ``` - * - * @param other The query to compare against. - */ - isEqual(other: Query): boolean; - - /** - * Generates a new `Query` limited to the first specific number of children. - * - * The `limitToFirst()` method is used to set a maximum number of children to be synced for a - * given callback. If we set a limit of 100, we will initially only receive up to 100 `child_added` - * events. If we have fewer than 100 messages stored in our Database, a child_added event will - * fire for each message. However, if we have over 100 messages, we will only receive a `child_added` - * event for the first 100 ordered messages. As items change, we will receive `child_removed` events - * for each item that drops out of the active list so that the total number stays at 100. - * - * You can read more about `limitToFirst()` in [Filtering data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#filtering_data). - * - * #### Example - * - * ```js - * const snapshot = firebase.database().ref('users').orderByKey().limitToFirst(2).once('value'); - * console.log(snapshot.numChildren()); // 2 - * ``` - * - * @param limit The maximum number of nodes to include in this query. - */ - limitToFirst(limit: number): Query; - - /** - * Generates a new `Query` object limited to the last specific number of children. - * - * The `limitToLast()` method is used to set a maximum number of children to be synced for a given - * callback. If we set a limit of 100, we will initially only receive up to 100 `child_added` events. - * If we have fewer than 100 messages stored in our Database, a `child_added` event will fire for - * each message. However, if we have over 100 messages, we will only receive a `child_added` event - * for the last 100 ordered messages. As items change, we will receive `child_removed` events for - * each item that drops out of the active list so that the total number stays at 100. - * - * You can read more about `limitToLast()` in [Filtering data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#filtering_data). - * - * #### Example - * - * ```js - * const snapshot = firebase.database().ref('users').orderByKey().limitToLast(2).once('value'); - * console.log(snapshot.numChildren()); // 2 - * ``` - * - * @param limit The maximum number of nodes to include in this query. - */ - limitToLast(limit: number): Query; - - /** - * Detaches a callback previously attached with `on()`. - * - * Detach a callback previously attached with `on()`. Note that if `on()` was called multiple times - * with the same eventType and callback, the callback will be called multiple times for each - * event, and `off()` must be called multiple times to remove the callback. Calling `off()` on a parent - * listener will not automatically remove listeners registered on child nodes, `off()` must also be - * called on any child listeners to remove the callback. - * - * If a callback is not specified, all callbacks for the specified eventType will be removed. - * Similarly, if no eventType is specified, all callbacks for the `Reference` will be removed. - * - * #### Example - * - * ```js - * const ref = firebase.database().ref('settings'); - * const onValueChange = function(snapshot) { ... }; - * const onChildAdded = function(snapshot) { ... }; - * - * ref.on('value', onValueChange); - * ref.child('meta-data').on('child_added', onChildAdded); - * // Sometime later... - * ref.off('value', onValueChange); - * ref.child('meta-data').off('child_added', onChildAdded); - * ``` - * - * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." If omitted, all callbacks for the Reference will be removed. - * @param callback The callback function that was passed to `on()` or `undefined` to remove all callbacks. - * @param context The context that was passed to `on()`. - */ - off( - eventType?: EventType, - callback?: (a: DataSnapshot, b?: string | null) => void, - context?: Record, - ): void; - - /** - * Listens for data changes at a particular location. - * - * This is the primary way to read data from a Database. Your callback will be triggered for the - * initial data and again whenever the data changes. Use `off()` to stop receiving updates.. - * - * **value** event - * - * This event will trigger once with the initial data stored at this location, and then trigger - * again each time the data changes. The `DataSnapshot` passed to the callback will be for the location - * at which on() was called. It won't trigger until the entire contents has been synchronized. - * If the location has no data, it will be triggered with an empty `DataSnapshot` - * (`val()` will return `null`). - * - * **child_added** event - * - * This event will be triggered once for each initial child at this location, and it will be - * triggered again every time a new child is added. The `DataSnapshot` passed into the callback - * will reflect the data for the relevant child. For ordering purposes, it is passed a second argument - * which is a string containing the key of the previous sibling child by sort order, or `null` if - * it is the first child. - * - * **child_removed** event - * - * This event will be triggered once every time a child is removed. The `DataSnapshot` passed into - * the callback will be the old data for the child that was removed. A child will get removed when either: - * - a client explicitly calls `remove()` on that child or one of its ancestors - * - a client calls `set(null)` on that child or one of its ancestors - * - that child has all of its children removed - * - there is a query in effect which now filters out the child (because it's sort order changed or the max limit was hit) - * - * **child_changed** event - * - * This event will be triggered when the data stored in a child (or any of its descendants) changes. - * Note that a single `child_changed` event may represent multiple changes to the child. The - * `DataSnapshot` passed to the callback will contain the new child contents. For ordering purposes, - * the callback is also passed a second argument which is a string containing the key of the previous - * sibling child by sort order, or `null` if it is the first child. - * - * **child_moved** event - * - * This event will be triggered when a child's sort order changes such that its position relative - * to its siblings changes. The `DataSnapshot` passed to the callback will be for the data of the child - * that has moved. It is also passed a second argument which is a string containing the key of the - * previous sibling child by sort order, or `null` if it is the first child. - * - * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." - * @param callback A callback that fires when the specified event occurs. The callback will be passed a DataSnapshot. For ordering purposes, "child_added", "child_changed", and "child_moved" will also be passed a string containing the key of the previous child, by sort order, or `null` if it is the first child. - * @param cancelCallbackOrContext An optional callback that will be notified if your event subscription is ever canceled because your client does not have permission to read this data (or it had permission but has now lost it). This callback will be passed an `Error` object indicating why the failure occurred. - * @param context If provided, this object will be used as `this` when calling your callback(s). - * - */ - on( - eventType: EventType, - callback: (data: DataSnapshot, previousChildKey?: string | null) => void, - cancelCallbackOrContext?: ((a: Error) => void) | Record | null, - context?: Record | null, - ): (a: DataSnapshot | null, b?: string | null) => void; - - /** - * Listens for exactly one event of the specified event type, and then stops listening. - * - * This is equivalent to calling `on()`, and then calling `off()` inside the callback function. See `on()` for details on the event types. - * - * #### Example - * - * ```js - * // Promise - * const snapshot = await firebase.database().ref('users').once('value'); - * // Callback - * firebase.database().ref('users).once('value', (snapshot) => { - * console.log(snapshot.val()); - * }); - * ``` - * - * @param eventType One of the following strings: "value", "child_added", "child_changed", "child_removed", or "child_moved." - * @param successCallback A callback that fires when the specified event occurs. The callback will be passed a DataSnapshot. For ordering purposes, "child_added", "child_changed", and "child_moved" will also be passed a string containing the key of the previous child by sort order, or `null` if it is the first child. - @param failureCallbackContext An optional callback that will be notified if your client does not have permission to read the data. This callback will be passed an Error object indicating why the failure occurred. - */ - - once( - eventType: EventType, - successCallback?: (a: DataSnapshot, b?: string | null) => any, - failureCallbackContext?: ((a: Error) => void) | Record | null, - ): Promise; - - /** - * Generates a new `Query` object ordered by the specified child key. - * - * Queries can only order by one key at a time. Calling `orderByChild()` multiple times on the same query is an error. - * - * Firebase queries allow you to order your data by any child key on the fly. However, if you know in advance what - * your indexes will be, you can define them via the [.indexOn](https://firebase.google.com/docs/database/security/indexing-data?authuser=0) - * rule in your Security Rules for better performance. - * - * You can read more about orderByChild() in [Sort data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#sort_data). - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users').orderByChild('age').once('value'); - * snapshot.forEach((snapshot) => { - * console.log('Users age:', snapshot.val().age); - * }); - * ``` - * - * @param path The child path node to order by. - */ - orderByChild(path: string): Query; - - /** - * Generates a new `Query` object ordered by key. - * - * Sorts the results of a query by their (ascending) key values. - * - * You can read more about `orderByKey()` in [Sort data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#sort_data). - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users').orderByKey().once('value'); - * snapshot.forEach((snapshot) => { - * console.log('User:', snapshot.val()); - * }); - * ``` - */ - orderByKey(): Query; - - /** - * Generates a new Query object ordered by priority. - * - * Applications need not use priority but can order collections by ordinary properties - * (see [Sort data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#sort_data) - * for alternatives to priority). - */ - orderByPriority(): Query; - - /** - * Generates a new `Query` object ordered by value. - * - * If the children of a query are all scalar values (string, number, or boolean), you can order - * the results by their (ascending) values. - * - * You can read more about `orderByValue()` in [Sort data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#sort_data). - * - * #### Example - * - * ```js - * await firebase.database().ref('scores').orderByValue().once('value'); - * ``` - */ - orderByValue(): Query; - - /** - * Creates a `Query` with the specified starting point. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows you to choose arbitrary starting and - * ending points for your queries. - * - * The starting point is inclusive, so children with exactly the specified value will be included - * in the query. The optional key argument can be used to further limit the range of the query. - * If it is specified, then children that have exactly the specified value must also have a key - * name greater than or equal to the specified key. - * - * You can read more about `startAt()` in [Filtering data](https://firebase.google.com/docs/database/web/lists-of-data?authuser=0#filtering_data). - * - * #### Example - * - * ```js - * await firebase.database().ref('users').orderByChild('age').startAt(21).once('value'); - * ``` - * - * @param value The value to start at. The argument type depends on which `orderBy*()` function was used in this query. Specify a value that matches the `orderBy*()` type. When used in combination with `orderByKey()`, the value must be a string. - * @param key The child key to start at. This argument is only allowed if ordering by child, value, or priority. - */ - startAt(value: number | string | boolean | null, key?: string): Query; - - /** - * Returns a JSON-serializable representation of this object. - */ - toJSON(): string; - - /** - * Gets the absolute URL for this location. - * - * The `toString()` method returns a URL that is ready to be put into a browser, curl command, or - * a `firebase.database().refFromURL()` call. Since all of those expect the URL to be url-encoded, - * `toString()` returns an encoded URL. - * - * Append '.json' to the returned URL when typed into a browser to download JSON-formatted data. - * If the location is secured (that is, not publicly readable), you will get a permission-denied error. - * - * #### Example - * - * ```js - * const ref1 = firebase.database().ref(); - * const ref2 = firebase.database().ref('users').orderByValue(); - * - * ref1.toString(); // https://sample-app.firebaseio.com/ - * ref2.toString(); // https://sample-app.firebaseio.com/users - * ``` - */ - toString(): string; - - /** - * By calling `keepSynced(true)` on a location, the data for that location will automatically - * be downloaded and kept in sync, even when no listeners are attached for that location. - * - * #### Example - * - * ```js - * const ref = firebase.database().ref('users'); - * await ref.keepSynced(true); - * ``` - * - * @param bool Pass `true` to keep this location synchronized, pass `false` to stop synchronization. - */ - keepSynced(bool: boolean): Promise; - } - - /** - * The `onDisconnect` class allows you to write or clear data when your client disconnects from the Database server. - * These updates occur whether your client disconnects cleanly or not, so you can rely on them to clean up data - * even if a connection is dropped or a client crashes. - * - * The onDisconnect class is most commonly used to manage presence in applications where it is - * useful to detect how many clients are connected and when other clients disconnect. - * - * To avoid problems when a connection is dropped before the requests can be transferred to the Database - * server, these functions should be called before writing any data. - * - * Note that `onDisconnect` operations are only triggered once. If you want an operation to occur each time a - * disconnect occurs, you'll need to re-establish the `onDisconnect` operations each time you reconnect. - */ - /** - * @deprecated Use the exported `OnDisconnect` type instead. - */ - export interface OnDisconnect { - /** - * Cancels all previously queued `onDisconnect()` set or update events for this location and all children. - * - * If a write has been queued for this location via a `set()` or `update()` at a parent location, - * the write at this location will be canceled, though writes to sibling locations will still occur. - * - * #### Example - * - * ```js - * const ref = firebase.database().ref('onlineState'); - * await ref.onDisconnect().set(false); - * // Sometime later... - * await ref.onDisconnect().cancel(); - * ``` - * - * @param onComplete An optional callback function that will be called when synchronization to the server has completed. The callback will be passed a single parameter: null for success, or an Error object indicating a failure. - */ - cancel(onComplete?: (error: Error | null) => void): Promise; - - /** - * Ensures the data at this location is deleted when the client is disconnected (due to closing the browser, navigating to a new page, or network issues). - * - * @param onComplete An optional callback function that will be called when synchronization to the server has completed. The callback will be passed a single parameter: null for success, or an Error object indicating a failure. - */ - remove(onComplete?: (error: Error | null) => void): Promise; - - /** - * Ensures the data at this location is set to the specified value when the client is disconnected - * (due to closing the app, navigating to a new view, or network issues). - * - * `set()` is especially useful for implementing "presence" systems, where a value should be changed - * or cleared when a user disconnects so that they appear "offline" to other users. - * - * Note that `onDisconnect` operations are only triggered once. If you want an operation to occur each time a - * disconnect occurs, you'll need to re-establish the `onDisconnect` operations each time. - * - * #### Example - * - * ```js - * var ref = firebase.database().ref('users/ada/status'); - * await ref.onDisconnect().set('I disconnected!'); - * ``` - * - * @param value The value to be written to this location on disconnect (can be an object, array, string, number, boolean, or null). - * @param onComplete An optional callback function that will be called when synchronization to the Database server has completed. The callback will be passed a single parameter: null for success, or an Error object indicating a failure. - */ - set(value: any, onComplete?: (error: Error | null) => void): Promise; - - /** - * Ensures the data at this location is set to the specified value and priority when the client is disconnected (due to closing the browser, navigating to a new page, or network issues). - * - * @param value The value to set. - * @param priority The priority to set - * @param onComplete An optional callback function that will be called when synchronization to the Database server has completed. The callback will be passed a single parameter: null for success, or an Error object indicating a failure. - */ - setWithPriority( - value: any, - priority: string | number | null, - onComplete?: (error: Error | null) => void, - ): Promise; - - /** - * Writes multiple values at this location when the client is disconnected (due to closing the browser, navigating to a new page, or network issues). - * - * The `values` argument contains multiple property-value pairs that will be written to the Database together. - * Each child property can either be a simple property (for example, "name") or a relative path (for example, - * "name/first") from the current location to the data to update. - * - * As opposed to the `set()` method, `update()` can be use to selectively update only the referenced - * properties at the current location (instead of replacing all the child properties at the current location). - * - * #### Example - * - * ```js - * var ref = firebase.database().ref("users/ada"); - * ref.update({ - * onlineState: true, - * status: "I'm online." - * }); - * ref.onDisconnect().update({ - * onlineState: false, - * status: "I'm offline." - * }); - * ``` - * - * @param values Object containing multiple values. - * @param onComplete An optional callback function that will be called when synchronization to the server has completed. The callback will be passed a single parameter: null for success, or an Error object indicating a failure. - */ - update( - values: { [key: string]: any }, - onComplete?: (error: Error | null) => void, - ): Promise; - } - - /** - * @deprecated Use the exported `EventType` type instead. - */ - export type EventType = - | 'value' - | 'child_added' - | 'child_changed' - | 'child_moved' - | 'child_removed'; - - /** - * A `DataSnapshot` contains data from a Database location. - * - * Any time you read data from the Database, you receive the data as a `DataSnapshot`. A `DataSnapshot` - * is passed to the event callbacks you attach with `on()` or `once()`. You can extract the contents - * of the snapshot as a JavaScript object by calling the val() method. Alternatively, you can traverse - * into the snapshot by calling `child()` to return child snapshots (which you could then call `val()` on). - */ - /** - * @deprecated Use the exported `DataSnapshot` type instead. - */ - export interface DataSnapshot { - /** - * The key (last part of the path) of the location of this `DataSnapshot`. - * - * The last token in a Database location is considered its key. For example, "ada" is the key - * for the /users/ada/ node. Accessing the key on any `DataSnapshot` will return the key for the - * location that generated it. However, accessing the key on the root URL of a Database will return `null`. - */ - key: string | null; - - /** - * The Reference for the location that generated this `DataSnapshot`. - */ - ref: Reference; - - /** - * Gets another `DataSnapshot` for the location at the specified relative path. - * - * Passing a relative path to the `child()` method of a DataSnapshot returns another `DataSnapshot` - * for the location at the specified relative path. The relative path can either be a simple child - * name (for example, "ada") or a deeper, slash-separated path (for example, "ada/name/first"). - * If the child location has no data, an empty `DataSnapshot` (that is, a `DataSnapshot` whose value - * is `null`) is returned. - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users/ada').once('value'); - * snapshot.child('name').val(); // {first:"Ada",last:"Lovelace"} - * snapshot.child('name/first').val(); // "Ada" - * snapshot.child('name/foo').val(); // null - * ``` - * - * @param path A relative path to the location of child data. - */ - child(path: string): DataSnapshot; - - /** - * Returns true if this `DataSnapshot` contains any data. It is slightly more efficient than using snapshot.val() !== null. - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users/ada').once('value'); - * snapshot.exists(); // true - * snapshot.child('name/foo').exists(); // false - * ``` - */ - exists(): boolean; - - /** - * Exports the entire contents of the DataSnapshot as a JavaScript object. - * - * The `exportVal()` method is similar to val(), except priority information is included (if available), - * making it suitable for backing up your data. - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users/ada').once('value'); - * const data = snapshot.exportVal(); - * console.log(data['.value']); // { ... } - * console.log(data['.priority']); // null - * ``` - */ - exportVal(): any; - - /** - * Enumerates the top-level children in the `DataSnapshot`. - * - * Because of the way JavaScript objects work, the ordering of data in the JavaScript object - * returned by `val()` is not guaranteed to match the ordering on the server nor the ordering - * of `child_added` events. That is where `forEach()` comes in handy. It guarantees the children of - * a DataSnapshot will be iterated in their query order. - * - * If no explicit `orderBy*()` method is used, results are returned ordered by key (unless priorities are used, in which case, results are returned by priority). - * - * @param action A function that will be called for each child DataSnapshot. The callback can return true to cancel further enumeration. - */ - forEach(action: (child: DataSnapshot) => boolean | void): boolean; - - /** - * Gets the priority value of the data in this DataSnapshot. - */ - getPriority(): string | number | null; - - /** - * Returns true if the specified child path has (non-null) data. - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users/ada').once('value'); - * console.log(snapshot.hasChild('name')); // true - * console.log(snapshot.hasChild('foo')); // false - * ``` - * - * @param path A relative path to the location of a potential child. - */ - hasChild(path: string): boolean; - - /** - * Returns whether or not the `DataSnapshot` has any non-null child properties. - * - * You can use `hasChildren()` to determine if a `DataSnapshot` has any children. If it does, you - * can enumerate them using `forEach()`. If it doesn't, then either this snapshot contains a primitive - * value (which can be retrieved with `val()`) or it is empty (in which case, `val()` will return null). - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users').once('value'); - * console.log(snapshot.hasChildren()); // true - * ``` - */ - hasChildren(): boolean; - - /** - * Returns the number of child properties of this DataSnapshot. - */ - numChildren(): number; - - /** - * Returns a JSON-serializable representation of this object. - */ - toJSON(): object | null; - - /** - * Extracts a JavaScript value from a `DataSnapshot`. - * - * Depending on the data in a DataSnapshot, the `val()` method may return a scalar type (string, - * number, or boolean), an array, or an object. It may also return null, indicating that the - * `DataSnapshot` is empty (contains no data). - * - * #### Example - * - * ```js - * const snapshot = await firebase.database().ref('users/ada/last').once('value'); - * snapshot.val(); // "Lovelace" - * ``` - */ - val(): any; - } - - /** - * - * The Firebase Database service is available for the default app or a given app. - * - * #### Example 1 - * - * Get the database instance for the **default app**: - * - * ```js - * const databaseForDefaultApp = firebase.database(); - * ``` - * - * #### Example 2 - * - * Get the database instance for a **secondary app**: - * - * ```js - * const otherApp = firebase.app('otherApp'); - * const databaseForOtherApp = firebase.database(otherApp); - * ``` - * - */ - /** - * @deprecated Use the exported `Database` type and modular API instead. - */ - export declare class Module extends FirebaseModule { - /** - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * Returns the current Firebase Database server time as a JavaScript Date object. - */ - getServerTime(): Date; - - /** - * Returns a `Reference` representing the location in the Database corresponding to the provided path. - * If no path is provided, the Reference will point to the root of the Database. - * - * #### Example - * - * ```js - * // Get a reference to the root of the Database - * const rootRef = firebase.database().ref(); - * - * // Get a reference to the /users/ada node - * const adaRef = firebase.database().ref("users/ada"); - * ``` - * - * @param path Optional path representing the location the returned `Reference` will point. If not provided, the returned `Reference` will point to the root of the Database. - */ - ref(path?: string): Reference; - - /** - * Returns a `Reference` representing the location in the Database corresponding to the provided Firebase URL. - * - * An exception is thrown if the URL is not a valid Firebase Database URL or it has a different domain than the current Database instance. - * - * Note that all query parameters (orderBy, limitToLast, etc.) are ignored and are not applied to the returned Reference. - * - * #### Example - * - * ```js - * // Get a reference to the root of the Database - * const rootRef = firebase.database().ref("https://.firebaseio.com"); - * ``` - * - * @param url The Firebase URL at which the returned Reference will point. - */ - refFromURL(url: string): Reference; - - /** - * Reconnects to the server and synchronizes the offline Database state with the server state. - * - * This method should be used after disabling the active connection with `goOffline()`. Once - * reconnected, the client will transmit the proper data and fire the appropriate events so that - * your client "catches up" automatically. - * - * #### Example - * - * ```js - * await firebase.database().goOnline(); - * ``` - */ - goOnline(): Promise; - - /** - * Disconnects from the server (all Database operations will be completed offline). - * - * The client automatically maintains a persistent connection to the Database server, which - * will remain active indefinitely and reconnect when disconnected. However, the `goOffline()` and - * `goOnline()` methods may be used to control the client connection in cases where a persistent - * connection is undesirable. - * - * While offline, the client will no longer receive data updates from the Database. However, - * all Database operations performed locally will continue to immediately fire events, allowing - * your application to continue behaving normally. Additionally, each operation performed locally - * will automatically be queued and retried upon reconnection to the Database server. - * - * To reconnect to the Database and begin receiving remote events, see `goOnline()`. - * - * #### Example - * - * ```js - * await firebase.database().goOnline(); - * ``` - */ - goOffline(): Promise; - - /** - * Sets whether persistence is enabled for all database calls for the current app - * instance. - * - * > Ensure this is called before any database calls are performed, otherwise - * persistence will only come into effect when the app is next started. - * - * #### Example - * - * ```js - * firebase.database().setPersistenceEnabled(true); - * - * async function bootstrap() { - * // Bootstrapping application - * const snapshot = await firebase.database().ref('settings').once('value'); - * } - * ``` - * - * @param enabled Whether persistence is enabled for the Database service. - */ - setPersistenceEnabled(enabled: boolean): void; - - /** - * Sets the native logging level for the database module. By default, - * only warnings and errors are logged natively. Setting this to true will log all - * database events. - * - * > Ensure logging is disabled for production apps, as excessive logging can cause performance issues. - * - * #### Example - * - * ```js - * // Set debug logging if developing - * if (__DEV__) { - * firebase.database().setLoggingEnabled(true); - * } - * ``` - * - * @param enabled Whether debug logging is enabled. - */ - setLoggingEnabled(enabled: boolean): void; - - /** - * By default Firebase Database will use up to 10MB of disk space to cache data. If the cache grows beyond this size, - * Firebase Database will start removing data that hasn't been recently used. If you find that your application - * caches too little or too much data, call this method to change the cache size. This method must be called before - * creating your first Database reference and only needs to be called once per application. - * - * Note that the specified cache size is only an approximation and the size on disk may temporarily exceed it at times. - * Cache sizes smaller than 1 MB or greater than 100 MB are not supported. - * - * #### Example - * - * ```js - * firebase.database().setPersistenceEnabled(true); - * firebase.database().setPersistenceCacheSizeBytes(2000000); // 2MB - * - * async function bootstrap() { - * // Bootstrapping application - * const snapshot = await firebase.database().ref('settings').once('value'); - * } - * ``` - * - * @param bytes The new size of the cache in bytes. - */ - setPersistenceCacheSizeBytes(bytes: number): void; - - /** - * Modify this Database instance to communicate with the Firebase Database emulator. - * This must be called synchronously immediately following the first call to firebase.database(). - * Do not use with production credentials as emulator traffic is not encrypted. - * - * Note: on android, hosts 'localhost' and '127.0.0.1' are automatically remapped to '10.0.2.2' (the - * "host" computer IP address for android emulators) to make the standard development experience easy. - * If you want to use the emulator on a real android device, you will need to specify the actual host - * computer IP address. - * - * @param host: emulator host (eg, 'localhost') - * @param port: emulator port (eg, 9000) - */ - useEmulator(host: string, port: number): void; - } -} - -type DatabaseNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseDatabaseTypes.Module, - FirebaseDatabaseTypes.Statics -> & { - database: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseDatabaseTypes.Module, - FirebaseDatabaseTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -declare const defaultExport: DatabaseNamespace; - -export declare const firebase: ReactNativeFirebase.Module & { - database: typeof defaultExport; - app( - name?: string, - ): ReactNativeFirebase.FirebaseApp & { database(): FirebaseDatabaseTypes.Module }; -}; - -export default defaultExport; - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ -declare module '@react-native-firebase/app' { - // eslint-disable-next-line @typescript-eslint/no-namespace -- module augmentation uses namespace - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - interface Module { - database: FirebaseModuleWithStaticsAndApp< - FirebaseDatabaseTypes.Module, - FirebaseDatabaseTypes.Statics - >; - } - - interface FirebaseApp { - database(databaseUrl?: string): FirebaseDatabaseTypes.Module; - } - } -} diff --git a/packages/database/type-test.ts b/packages/database/type-test.ts index fcef2d103a..1c0af9b55e 100644 --- a/packages/database/type-test.ts +++ b/packages/database/type-test.ts @@ -1,6 +1,5 @@ -import database, { - firebase, - FirebaseDatabaseTypes, +import { getApp } from '@react-native-firebase/app'; +import { getDatabase, connectDatabaseEmulator, goOffline, @@ -10,318 +9,45 @@ import database, { setPersistenceEnabled, setLoggingEnabled, setPersistenceCacheSizeBytes, - serverTimestamp, getServerTime, + serverTimestamp, increment, - type Database as ModularDatabase, - type DatabaseReference as ModularDatabaseReference, - type DataSnapshot as ModularDataSnapshot, - type EmulatorMockTokenOptions, - type IteratedDataSnapshot, - type Query as ModularQuery, - type QueryConstraint, - type QueryConstraintType, - type ThenableReference as ModularThenableReference, - type TransactionResult as ModularTransactionResult, - endAt, - endBefore, - startAt, - startAfter, - limitToFirst, - limitToLast, - orderByChild, - orderByKey, - orderByPriority, - orderByValue, - equalTo, - query, onValue, - onChildAdded, - onChildChanged, - onChildMoved, - onChildRemoved, - set, - setPriority, - setWithPriority, - get, - child, - onDisconnect, - keepSynced, - push, - remove, - update, runTransaction, + SDK_VERSION, + type Database, + type DatabaseReference, } from '.'; -console.log(database().app); - -// checks module exists at root -console.log(firebase.database().app.name); -console.log(firebase.database().ref('foo/bar')); - -// checks module exists at app level -console.log(firebase.app().database().app.name); - -// app level module accepts string arg -console.log(firebase.app().database('some-string').app.name); -console.log(firebase.app().database('some-string').ref('foo')); +const db = getDatabase(); +console.log(db.app.name); -// checks statics exist -console.log(firebase.database.SDK_VERSION); +const dbWithApp = getDatabase(getApp()); +console.log(dbWithApp.app.name); -// checks statics exist on defaultExport -console.log(database.firebase.SDK_VERSION); +connectDatabaseEmulator(db, 'localhost', 9000); +goOffline(db); +goOnline(db); -// checks root exists -console.log(firebase.SDK_VERSION); +const dbRef: DatabaseReference = ref(db, 'path'); +console.log(dbRef.key); -// checks multi-app support exists -console.log(firebase.database(firebase.app()).app.name); - -// checks default export supports app arg -console.log(database(firebase.app()).app.name); - -// checks statics - ServerValue -console.log(firebase.database.ServerValue.TIMESTAMP); -console.log(firebase.database.ServerValue.increment(1)); - -// checks Module instance APIs -const dbInstance = firebase.database(); -console.log(dbInstance.ref('foo/bar')); -console.log(dbInstance.refFromURL('https://example.firebaseio.com')); - -dbInstance.goOnline().then(() => { - console.log('Online'); -}); +refFromURL(db, 'https://example.firebaseio.com/path'); +setPersistenceEnabled(db, true); +setLoggingEnabled(db, true); +setPersistenceCacheSizeBytes(db, 1048576); +console.log(getServerTime(db)); +console.log(serverTimestamp()); +console.log(increment(1)); -dbInstance.goOffline().then(() => { - console.log('Offline'); -}); - -dbInstance.setPersistenceEnabled(true); -dbInstance.setLoggingEnabled(true); -dbInstance.setPersistenceCacheSizeBytes(2000000); -dbInstance.useEmulator('localhost', 9000); - -const serverTime = dbInstance.getServerTime(); -console.log(serverTime); - -const rootRef = dbInstance.ref(); -const rootChildRef = rootRef.child('users'); -rootChildRef.push({ name: 'test' }); - -rootRef.set({ foo: 'bar' }).then(() => { - console.log('Set complete'); -}); - -rootRef - .update({ foo: 'bar' }, () => {}) - .then(() => { - console.log('Update complete'); - }); - -rootRef.once('value').then((snapshot: FirebaseDatabaseTypes.DataSnapshot) => { - console.log(snapshot.exists()); +onValue(dbRef, snapshot => { console.log(snapshot.val()); - console.log(snapshot.key); - console.log(snapshot.ref); -}); - -rootRef.on('value', (snapshot: FirebaseDatabaseTypes.DataSnapshot) => { - console.log(snapshot.val()); -}); - -rootRef.off('value'); - -// checks modular API functions -const dbModular1: ModularDatabase = getDatabase(); -console.log(dbModular1.app.name); -const dbType: 'database' = dbModular1.type; -void dbType; - -const dbModular2: ModularDatabase = getDatabase(firebase.app()); -console.log(dbModular2.app.name); - -const dbModular3: ModularDatabase = getDatabase(firebase.app(), 'https://example.firebaseio.com'); -console.log(dbModular3.app.name); - -const mockUserToken: EmulatorMockTokenOptions = { user_id: 'test-user' }; -connectDatabaseEmulator(dbModular1, 'localhost', 9000, { mockUserToken }); - -goOffline(dbModular1); - -goOnline(dbModular1); - -const modularRef: ModularDatabaseReference = ref(dbModular1, 'users'); -const modularRef2: ModularDatabaseReference = ref(dbModular1); -console.log(modularRef.key); -console.log(modularRef2.key); - -const modularRefFromURL: ModularDatabaseReference = refFromURL( - dbModular1, - 'https://example.firebaseio.com/users', -); -console.log(modularRefFromURL.key); - -setPersistenceEnabled(dbModular1, true); -setLoggingEnabled(dbModular1, true); -setPersistenceCacheSizeBytes(dbModular1, 2000000); - -const timestamp = serverTimestamp(); -console.log(timestamp); - -const modularServerTime: Date = getServerTime(dbModular1); -console.log(modularServerTime); - -const incrementValue = increment(1); -console.log(incrementValue); - -// checks modular query functions -const testRef: ModularDatabaseReference = ref(dbModular1, 'users'); -const testQuery: ModularQuery = query(testRef, orderByChild('name'), limitToFirst(10)); -console.log(testQuery); - -const queryConstraintType: QueryConstraintType = 'orderByKey'; -void queryConstraintType; -const queryConstraint: QueryConstraint = orderByKey(); -console.log(queryConstraint.type); -void queryConstraint; - -// Test all query constraint functions -console.log(query(testRef, endAt('value'))); -console.log(query(testRef, endBefore('value'))); -console.log(query(testRef, startAt('value'))); -console.log(query(testRef, startAfter('value'))); -console.log(query(testRef, limitToFirst(5))); -console.log(query(testRef, limitToLast(5))); -console.log(query(testRef, orderByChild('age'))); -console.log(query(testRef, orderByKey())); -console.log(query(testRef, orderByPriority())); -console.log(query(testRef, orderByValue())); -console.log(query(testRef, equalTo('value'))); - -const modularUnsubscribe1 = onValue(testRef, (snapshot: ModularDataSnapshot) => { - console.log(snapshot.val()); -}); - -const modularUnsubscribe2 = onValue( - testRef, - (snapshot: ModularDataSnapshot) => { - console.log(snapshot.val()); - }, - (error: Error) => { - console.log(error.message); - }, -); - -const modularUnsubscribe3 = onValue( - testRef, - (snapshot: ModularDataSnapshot) => { - console.log(snapshot.val()); - }, - { onlyOnce: true }, -); - -modularUnsubscribe1(); -modularUnsubscribe2(); -modularUnsubscribe3(); - -const unsubscribeChildAdded = onChildAdded( - testRef, - (snapshot: ModularDataSnapshot, previousChildName?: string | null) => { - console.log(snapshot.val()); - console.log(previousChildName); - }, -); -const unsubscribeChildAddedOptionalPrev = onChildAdded(testRef, (snapshot: ModularDataSnapshot) => { - console.log(snapshot.val()); -}); - -const unsubscribeChildChanged = onChildChanged( - testRef, - (snapshot: ModularDataSnapshot, previousChildName: string | null) => { - console.log(snapshot.val()); - console.log(previousChildName); - }, -); - -const unsubscribeChildMoved = onChildMoved( - testRef, - (snapshot: ModularDataSnapshot, previousChildName: string | null) => { - console.log(snapshot.val()); - console.log(previousChildName); - }, -); - -const unsubscribeChildRemoved = onChildRemoved(testRef, (snapshot: ModularDataSnapshot) => { - console.log(snapshot.val()); -}); - -unsubscribeChildAdded(); -unsubscribeChildAddedOptionalPrev(); -unsubscribeChildChanged(); -unsubscribeChildMoved(); -unsubscribeChildRemoved(); - -set(testRef, { foo: 'bar' }).then(() => { - console.log('Set complete'); -}); - -setPriority(testRef, 'high').then(() => { - console.log('Priority set'); -}); - -setWithPriority(testRef, { foo: 'bar' }, 'high').then(() => { - console.log('Set with priority complete'); -}); - -get(testQuery).then((snapshot: ModularDataSnapshot) => { - console.log(snapshot.val()); - console.log(snapshot.priority); - console.log(snapshot.size); - snapshot.forEach((child: IteratedDataSnapshot) => { - console.log(child.key); - }); -}); - -const modularChildRef = child(testRef, 'child'); -console.log(modularChildRef.key); - -const onDisconnectRef = onDisconnect(testRef); -console.log(onDisconnectRef); -onDisconnectRef.set('offline').then(() => {}); - -keepSynced(testRef, true).then(() => { - console.log('Keep synced set'); -}); - -const modularPushRef: ModularThenableReference = push(testRef, { name: 'test' }); -console.log(modularPushRef.key); - -remove(testRef).then(() => { - console.log('Remove complete'); -}); - -update(testRef, { foo: 'bar' }).then(() => { - console.log('Update complete'); -}); - -runTransaction(testRef, (currentData: any) => { - return { ...currentData, updated: true }; -}).then((result: ModularTransactionResult) => { - console.log(result.committed); - console.log(result.snapshot.val()); - console.log(result.toJSON()); }); -runTransaction( - testRef, - (currentData: any) => { - return { ...currentData, updated: true }; - }, - { applyLocally: true }, -).then((result: ModularTransactionResult) => { +runTransaction(dbRef, current => current).then(result => { console.log(result.committed); }); -console.log(testQuery.isEqual(null)); +const typedDb: Database = db; +console.log(typedDb.app.name); +console.log(SDK_VERSION); diff --git a/packages/database/typedoc.json b/packages/database/typedoc.json index df4361d280..8444dd5393 100644 --- a/packages/database/typedoc.json +++ b/packages/database/typedoc.json @@ -1,6 +1,6 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/database.ts"], + "entryPoints": ["lib/index.ts", "lib/types/database.ts"], "tsconfig": "tsconfig.json", "intentionallyNotExported": ["FirebaseIdToken"] } diff --git a/packages/firestore/__tests__/firestore.test.ts b/packages/firestore/__tests__/firestore.test.ts index ae4a74731a..11553bc437 100644 --- a/packages/firestore/__tests__/firestore.test.ts +++ b/packages/firestore/__tests__/firestore.test.ts @@ -1,26 +1,7 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; -// @ts-ignore test -import { createDeprecationProxy } from '../../app/lib/common'; -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; -// @ts-ignore test -import Query from '../lib/FirestoreQuery'; -// @ts-ignore test -import FirestoreDocumentSnapshot from '../lib/FirestoreDocumentSnapshot'; +import { describe, expect, it } from '@jest/globals'; import { parseSnapshotArgs } from '../lib/utils'; -// @ts-ignore test -import * as nativeModule from '@react-native-firebase/app/dist/module/internal/nativeModuleAndroidIos'; import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, - withDeprecationWarningsSilenced, -} from '../../app/lib/common/unitTestUtils'; - -import { getApp } from '../../app/lib/modular'; -import firestore, { - firebase, - connectFirestoreEmulator, Filter, getFirestore, getAggregateFromServer, @@ -36,7 +17,6 @@ import firestore, { enableNetwork, disableNetwork, clearPersistence, - clearIndexedDbPersistence, terminate, waitForPendingWrites, initializeFirestore, @@ -83,455 +63,44 @@ import firestore, { documentId, vector, VectorValue, + SDK_VERSION, } from '../lib'; -const COLLECTION = 'firestore'; - describe('Firestore', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.firestore).toBeDefined(); - expect(app.firestore().settings).toBeDefined(); - }); - - describe('batch()', function () { - it('returns a new WriteBatch instance', function () { - const instance = firebase.firestore().batch(); - return expect(instance.constructor.name).toEqual('WriteBatch'); - }); - }); - - describe('settings', function () { - it('throws if settings is not an object', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firebase.firestore().settings('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings' must be an object"); - } - }); - - it('throws if passing an incorrect setting key', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firebase.firestore().settings({ foo: 'bar' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings.foo' is not a valid settings field"); - } - }); - - it('throws if cacheSizeBytes is not a number', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firebase.firestore().settings({ cacheSizeBytes: 'foo' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings.cacheSizeBytes' must be a number value"); - } - }); - - it('throws if cacheSizeBytes is less than 1MB', async function () { - try { - await firebase.firestore().settings({ cacheSizeBytes: 123 }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings.cacheSizeBytes' the minimum cache size"); - } - }); - - it('accepts an unlimited cache size', async function () { - await firebase - .firestore() - .settings({ cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED }); - }); - - it('throws if host is not a string', async function () { - // eslint-disable-next-line no-console - const warnOrig = console.warn; - // eslint-disable-next-line no-console - console.warn = (_: string) => {}; - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firebase.firestore().settings({ host: 123 }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings.host' must be a string value"); - } finally { - // eslint-disable-next-line no-console - console.warn = warnOrig; - } - }); - - it('throws if host is an empty string', async function () { - // eslint-disable-next-line no-console - const warnOrig = console.warn; - // eslint-disable-next-line no-console - console.warn = (_: string) => {}; - try { - await firebase.firestore().settings({ host: '' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings.host' must not be an empty string"); - } finally { - // eslint-disable-next-line no-console - console.warn = warnOrig; - } - }); - - it('throws if persistence is not a boolean', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firebase.firestore().settings({ persistence: 'true' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings.persistence' must be a boolean value"); - } - }); - - it('throws if ssl is not a boolean', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firebase.firestore().settings({ ssl: 'true' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'settings.ssl' must be a boolean value"); - } - }); - - it('throws if ignoreUndefinedProperties is not a boolean', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firestore().settings({ ignoreUndefinedProperties: 'bogus' }); - return Promise.reject(new Error('Should throw')); - } catch (e: any) { - return expect(e.message).toContain("ignoreUndefinedProperties' must be a boolean value."); - } - }); - - it("throws if serverTimestampBehavior is not one of 'estimate', 'previous', 'none'", async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firestore().settings({ serverTimestampBehavior: 'bogus' }); - return Promise.reject(new Error('Should throw')); - } catch (e: any) { - return expect(e.message).toContain( - "serverTimestampBehavior' must be one of 'estimate', 'previous', 'none'", - ); - } - }); - }); - - describe('runTransaction()', function () { - it('throws if updateFunction is not a function', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - await firebase.firestore().runTransaction('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'updateFunction' must be a function"); - } - }); - }); - - describe('collectionGroup()', function () { - it('returns a new query instance', function () { - const query = firebase.firestore().collectionGroup(COLLECTION); - expect(query.constructor.name).toEqual('Query'); - }); - - it('throws if id is not a string', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - firebase.firestore().collectionGroup(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'collectionId' must be a string value"); - } - }); - - it('throws if id is empty', async function () { - try { - firebase.firestore().collectionGroup(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'collectionId' must be a non-empty string"); - } - }); - - it('throws if id contains forward-slash', async function () { - try { - firebase.firestore().collectionGroup(`someCollection/bar`); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'collectionId' must not contain '/'"); - } - }); - }); + describe('onSnapshot()', function () { + it("accepts { source: 'cache' } listener options", function () { + const parsed = parseSnapshotArgs([{ source: 'cache' }, () => {}]); - describe('collection()', function () { - it('throws if path is not a string', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - firebase.firestore().collection(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'collectionPath' must be a string value"); - } - }); - - it('throws if path is empty string', async function () { - try { - firebase.firestore().collection(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'collectionPath' must be a non-empty string"); - } - }); - - it('throws if path does not point to a collection', async function () { - try { - firebase.firestore().collection(`firestore/bar`); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'collectionPath' must point to a collection"); - } - }); - - it('returns a new CollectionReference', async function () { - const collectionReference = firebase.firestore().collection('firestore'); - expect(collectionReference.constructor.name).toEqual('CollectionReference'); - expect(collectionReference.path).toEqual('firestore'); + expect(parsed.snapshotListenOptions).toEqual({ + includeMetadataChanges: false, + source: 'cache', }); }); - describe('doc()', function () { - it('throws if path is not a string', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - firebase.firestore().doc(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'documentPath' must be a string value"); - } - }); - - it('throws if path is empty string', async function () { - try { - firebase.firestore().doc(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'documentPath' must be a non-empty string"); - } - }); - - it('throws if path does not point to a document', async function () { - try { - firebase.firestore().doc(`${COLLECTION}/bar/baz`); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'documentPath' must point to a document"); - } - }); - - it('returns a new DocumentReference', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - expect(docRef.constructor.name).toEqual('DocumentReference'); - expect(docRef.path).toEqual(`${COLLECTION}/bar`); - }); - - it('throws when undefined value provided and ignored undefined is false', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: false }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - try { - await docRef.set({ - field1: 1, - field2: undefined, - }); - - return Promise.reject(new Error('Expected set() to throw')); - } catch (e: any) { - return expect(e.message).toEqual('Unsupported field value: undefined'); - } - }); - - it('throws when nested undefined object value provided and ignored undefined is false', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: false }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - try { - await docRef.set({ - field1: 1, - field2: { - shouldNotWork: undefined, - }, - }); - return Promise.reject(new Error('Expected set() to throw')); - } catch (e: any) { - return expect(e.message).toEqual('Unsupported field value: undefined'); - } - }); - - it('throws when nested undefined array value provided and ignored undefined is false', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: false }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - try { - await docRef.set({ - myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], - }); - return Promise.reject(new Error('Expected set() to throw')); - } catch (e: any) { - return expect(e.message).toEqual('Unsupported field value: undefined'); - } - }); - - it('does not throw when nested undefined array value provided and ignore undefined is true', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], - }); - }); - - it('does not throw when nested undefined object value provided and ignore undefined is true', async function () { - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - field1: 1, - field2: { - shouldNotWork: undefined, - }, - }); - }); - - it('does not throw when Date is provided instead of Timestamp', async function () { - // type BarType = { - // myDate: FirebaseFirestoreTypes.Timestamp; - // }; - - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - myDate: new Date(), - }); - }); - - it('does not throw when serverTimestamp is provided instead of Timestamp', async function () { - // type BarType = { - // myDate: FirebaseFirestoreTypes.Timestamp; - // }; - - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - myDate: firestore.FieldValue.serverTimestamp(), - }); - }); - }); - - describe('loadBundle()', function () { - it('throws if bundle is not a string', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - firebase.firestore().loadBundle(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'bundle' must be a string value"); - } - }); - - it('throws if bundle is empty string', async function () { - try { - firebase.firestore().loadBundle(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'bundle' must be a non-empty string"); - } - }); - }); - - describe('namedQuery()', function () { - it('throws if queryName is not a string', async function () { - try { - // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript - firebase.firestore().namedQuery(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'queryName' must be a string value"); - } - }); - - it('throws if queryName is empty string', async function () { - try { - firebase.firestore().namedQuery(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (e: any) { - return expect(e.message).toContain("'queryName' must be a non-empty string"); - } - }); - - describe('FirestorePersistentCacheIndexManager', function () { - it('is exposed to end user', function () { - const firestore1 = firebase.firestore(); - firestore1.settings({ persistence: true }); - const indexManager = firestore1.persistentCacheIndexManager(); - expect(indexManager).toBeDefined(); - expect(indexManager!.constructor.name).toEqual('PersistentCacheIndexManager'); - - expect(indexManager!.enableIndexAutoCreation).toBeInstanceOf(Function); - expect(indexManager!.disableIndexAutoCreation).toBeInstanceOf(Function); - expect(indexManager!.deleteAllIndexes).toBeInstanceOf(Function); - - const firestore2 = firebase.firestore(); - firestore2.settings({ persistence: false }); - - const nullIndexManager = firestore2.persistentCacheIndexManager(); + it("accepts { source: 'default', includeMetadataChanges: true } listener options", function () { + const parsed = parseSnapshotArgs([ + { source: 'default', includeMetadataChanges: true }, + () => {}, + ]); - expect(nullIndexManager).toBeNull(); - }); + expect(parsed.snapshotListenOptions).toEqual({ + includeMetadataChanges: true, + source: 'default', }); }); - describe('onSnapshot()', function () { - it("accepts { source: 'cache' } listener options", function () { - const parsed = parseSnapshotArgs([{ source: 'cache' }, () => {}]); - - expect(parsed.snapshotListenOptions).toEqual({ - includeMetadataChanges: false, - source: 'cache', - }); - }); - - it("accepts { source: 'default', includeMetadataChanges: true } listener options", function () { - const parsed = parseSnapshotArgs([ - { source: 'default', includeMetadataChanges: true }, - () => {}, - ]); - - expect(parsed.snapshotListenOptions).toEqual({ - includeMetadataChanges: true, - source: 'default', - }); - }); - - it("throws for unsupported listener source value 'server'", function () { - expect(() => - parseSnapshotArgs([{ source: 'server' as 'default' | 'cache' }, () => {}]), - ).toThrow("'options' SnapshotOptions.source must be one of 'default' or 'cache'."); - }); + it("throws for unsupported listener source value 'server'", function () { + expect(() => + parseSnapshotArgs([{ source: 'server' as 'default' | 'cache' }, () => {}]), + ).toThrow("'options' SnapshotOptions.source must be one of 'default' or 'cache'."); }); }); describe('modular', function () { + it('`SDK_VERSION` is exported', function () { + expect(SDK_VERSION).toBeDefined(); + }); + it('`getFirestore` function is properly exposed to end user', function () { expect(getFirestore).toBeDefined(); }); @@ -738,10 +307,9 @@ describe('Firestore', function () { it('`getPersistentCacheIndexManager` is properly exposed to end user', function () { expect(getPersistentCacheIndexManager).toBeDefined(); - // FIXME there is currently no way to programmatically alter - // persistence settings via modular API (FirestoreSettings.localCache ...) - const nullIndexManagerModular = getPersistentCacheIndexManager(getFirestore()); - expect(nullIndexManagerModular).toBeNull(); + const indexManager = getPersistentCacheIndexManager(getFirestore()); + expect(indexManager).toBeDefined(); + expect(indexManager!.constructor.name).toEqual('PersistentCacheIndexManager'); }); it('`deleteAllPersistentCacheIndexes` is properly exposed to end user', function () { @@ -789,754 +357,6 @@ describe('Firestore', function () { }); }); - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let collectionRefV9Deprecation: CheckV9DeprecationFunction; - let docRefV9Deprecation: CheckV9DeprecationFunction; - let fieldValueV9Deprecation: CheckV9DeprecationFunction; - let filterV9Deprecation: CheckV9DeprecationFunction; - let persistentCacheIndexManagerV9Deprecation: CheckV9DeprecationFunction; - let firestoreRefV9Deprecation: CheckV9DeprecationFunction; - let staticsV9Deprecation: CheckV9DeprecationFunction; - let timestampV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - firestoreRefV9Deprecation = createCheckV9Deprecation(['firestore']); - collectionRefV9Deprecation = createCheckV9Deprecation([ - 'firestore', - 'FirestoreCollectionReference', - ]); - - docRefV9Deprecation = createCheckV9Deprecation(['firestore', 'DocumentReference']); - - fieldValueV9Deprecation = createCheckV9Deprecation(['firestore', 'FieldValue']); - filterV9Deprecation = createCheckV9Deprecation(['firestore', 'Filter']); - persistentCacheIndexManagerV9Deprecation = createCheckV9Deprecation([ - 'firestore', - 'PersistentCacheIndexManager', - ]); - - staticsV9Deprecation = createCheckV9Deprecation(['firestore', 'statics']); - - timestampV9Deprecation = createCheckV9Deprecation(['firestore', 'Timestamp']); - - // Mock the native module directly to avoid getter caching issues - const mockNative = new Proxy( - {}, - { - get: () => - (jest.fn() as any).mockResolvedValue({ - source: 'cache', - changes: [], - documents: [], - metadata: {}, - path: 'foo', - }), - }, - ); - - // Override the native getter on FirebaseModule prototype - Object.defineProperty(FirebaseModule.prototype, 'native', { - get: function () { - return mockNative; - }, - configurable: true, - }); - - jest - .spyOn(Query.prototype, '_handleQueryCursor') - // @ts-ignore test - .mockImplementation(() => { - return []; - }); - }); - - describe('Firestore', function () { - it('firestore.batch()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => writeBatch(firestore), - // @ts-expect-error Combines modular and namespace API - () => firestore.batch(), - 'batch', - ); - }); - - it('firestore.loadBundle()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => loadBundle(firestore, 'some bundle'), - // @ts-expect-error Combines modular and namespace API - () => firestore.loadBundle('some bundle'), - 'loadBundle', - ); - }); - - it('firestore.namedQuery()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => namedQuery(firestore, 'some name'), - // @ts-expect-error Combines modular and namespace API - () => firestore.namedQuery('some name'), - 'namedQuery', - ); - }); - - it('firestore.clearPersistence()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => clearIndexedDbPersistence(firestore), - // @ts-expect-error Combines modular and namespace API - () => firestore.clearPersistence(), - 'clearPersistence', - ); - // Deprecating the modular method clearPersistence() as it doesn't exist on firebase-js-sdk - firestoreRefV9Deprecation( - () => clearIndexedDbPersistence(firestore), - () => clearPersistence(firestore), - 'clearPersistence', - ); - }); - - it('firestore.waitForPendingWrites()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => waitForPendingWrites(firestore), - // @ts-expect-error Combines modular and namespace API - () => firestore.waitForPendingWrites(), - 'waitForPendingWrites', - ); - }); - - it('firestore.terminate()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => terminate(firestore), - // @ts-expect-error Combines modular and namespace API - () => firestore.terminate(), - 'terminate', - ); - }); - - it('firestore.useEmulator()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => connectFirestoreEmulator(firestore, 'localhost', 8080), - // @ts-expect-error Combines modular and namespace API - () => firestore.useEmulator('localhost', 8080), - 'useEmulator', - ); - }); - - it('firestore.collection()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => collection(firestore, 'collection'), - // @ts-expect-error Combines modular and namespace API - () => firestore.collection('collection'), - 'collection', - ); - }); - - it('firestore.collectionGroup()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => collectionGroup(firestore, 'collection'), - // @ts-expect-error Combines modular and namespace API - () => firestore.collectionGroup('collection'), - 'collectionGroup', - ); - }); - - it('firestore.disableNetwork()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => disableNetwork(firestore), - // @ts-expect-error Combines modular and namespace API - () => firestore.disableNetwork(), - 'disableNetwork', - ); - }); - - it('firestore.doc()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => doc(firestore, 'collection/path'), - // @ts-expect-error Combines modular and namespace API - () => firestore.doc('collection/path'), - 'doc', - ); - }); - - it('firestore.enableNetwork()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => enableNetwork(firestore), - // @ts-expect-error Combines modular and namespace API - () => firestore.enableNetwork(), - 'enableNetwork', - ); - }); - - it('firestore.runTransaction()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - () => runTransaction(firestore, async () => {}), - // @ts-expect-error Combines modular and namespace API - () => firestore.runTransaction(async () => {}), - 'runTransaction', - ); - }); - - it('firestore.settings()', function () { - const firestore = getFirestore(); - firestoreRefV9Deprecation( - // no equivalent settings method for firebase-js-sdk - () => initializeFirestore(getApp(), {}), - // @ts-expect-error Combines modular and namespace API - () => firestore.settings({}), - 'settings', - ); - }); - }); - - describe('CollectionReference', function () { - it('CollectionReference.count()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => getCountFromServer(query), - () => query2.count(), - 'count', - ); - }); - - it('CollectionReference.countFromServer()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => getCountFromServer(query), - () => query2.countFromServer(), - 'countFromServer', - ); - }); - - it('CollectionReference.endAt()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => endAt('foo'), - () => query2.endAt('foo'), - 'endAt', - ); - }); - - it('CollectionReference.endBefore()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => endBefore('foo'), - () => query2.endBefore('foo'), - 'endBefore', - ); - }); - - it('CollectionReference.get()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => getDocs(query), - () => query2.get(), - 'get', - ); - }); - - it('CollectionReference.isEqual()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - // no equivalent method - () => {}, - () => query2.isEqual(query2), - 'isEqual', - ); - }); - - it('CollectionReference.limit()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => limit(9), - () => query2.limit(9), - 'limit', - ); - }); - - it('CollectionReference.limitToLast()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => limitToLast(9), - () => query2.limitToLast(9), - 'limitToLast', - ); - }); - - it('CollectionReference.onSnapshot()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => onSnapshot(query, () => {}), - () => query2.onSnapshot(() => {}), - 'onSnapshot', - ); - }); - - it('CollectionReference.orderBy()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => orderBy('foo', 'asc'), - () => query2.orderBy('foo', 'asc'), - 'orderBy', - ); - }); - - it('CollectionReference.startAfter()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => startAfter('foo'), - () => query2.startAfter('foo'), - 'startAfter', - ); - }); - - it('CollectionReference.startAt()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => startAt('foo'), - () => query2.startAt('foo'), - 'startAt', - ); - }); - - it('CollectionReference.where()', function () { - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => where('foo', '==', 'bar'), - () => query2.where('foo', '==', 'bar'), - 'where', - ); - }); - - it('CollectionReference.add()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => addDoc(query, { foo: 'bar' }), - () => query2.add({ foo: 'bar' }), - 'add', - ); - }); - - it('CollectionReference.doc()', function () { - const firestore = getFirestore(); - - const query = collection(firestore, 'test'); - const query2 = withDeprecationWarningsSilenced(() => - firebase.firestore().collection('test'), - ); - - collectionRefV9Deprecation( - () => doc(query, 'bar'), - () => query2.doc('foo'), - 'doc', - ); - }); - }); - - describe('DocumentReference', function () { - it('DocumentReference.collection()', function () { - const firestore = getFirestore(); - const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); - - docRefV9Deprecation( - () => collection(firestore, 'bar'), - () => docRef2.collection('bar'), - 'collection', - ); - }); - - it('DocumentReference.delete()', function () { - const firestore = getFirestore(); - - const docRef = doc(firestore, 'some/foo'); - const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); - - docRefV9Deprecation( - () => deleteDoc(docRef), - () => docRef2.delete(), - 'delete', - ); - }); - - it('DocumentReference.get()', function () { - const firestore = getFirestore(); - - const docRef = doc(firestore, 'some/foo'); - const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); - - docRefV9Deprecation( - () => getDoc(docRef), - () => docRef2.get(), - 'get', - ); - }); - - it('DocumentReference.isEqual()', function () { - const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); - - docRefV9Deprecation( - // no equivalent method - () => {}, - () => docRef2.isEqual(docRef2), - 'isEqual', - ); - }); - - it('DocumentReference.onSnapshot()', function () { - const firestore = getFirestore(); - - const docRef = doc(firestore, 'some/foo'); - const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); - - docRefV9Deprecation( - () => onSnapshot(docRef, () => {}), - () => docRef2.onSnapshot(() => {}), - 'onSnapshot', - ); - }); - - it('DocumentReference.set()', function () { - const firestore = getFirestore(); - - const docRef = doc(firestore, 'some/foo'); - const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); - - docRefV9Deprecation( - () => setDoc(docRef, { foo: 'bar' }), - () => docRef2.set({ foo: 'bar' }), - 'set', - ); - }); - - it('DocumentReference.update()', function () { - const firestore = getFirestore(); - - const docRef = doc(firestore, 'some/foo'); - const docRef2 = withDeprecationWarningsSilenced(() => firebase.firestore().doc('some/foo')); - - docRefV9Deprecation( - () => updateDoc(docRef, { foo: 'bar' }), - () => docRef2.update({ foo: 'bar' }), - 'update', - ); - }); - }); - - it('FirestoreDocumentSnapshot.isEqual()', function () { - const firestore = getFirestore(); - // Every `FirestoreDocumentSnapshot` has been wrapped in deprecation proxy, so we use constructor directly - // for ease of mocking - const snapshot = createDeprecationProxy( - new FirestoreDocumentSnapshot( - // @ts-expect-error calling a private constructor directly which expects FirestoreInternal type - firestore, - { - data: {}, - metadata: [false, false], - path: 'foo', - }, - null, - ), - ); - - docRefV9Deprecation( - // no equivalent method - () => {}, - () => snapshot.isEqual(snapshot), - 'isEqual', - ); - }); - - describe('FieldValue', function () { - it('FieldValue.delete()', function () { - const fieldValue = firestore.FieldValue; - fieldValueV9Deprecation( - () => deleteField(), - () => fieldValue.delete(), - 'delete', - ); - }); - - it('FieldValue.increment()', function () { - const fieldValue = firestore.FieldValue; - fieldValueV9Deprecation( - () => increment(3), - () => fieldValue.increment(4), - 'increment', - ); - }); - - it('FieldValue.serverTimestamp()', function () { - const fieldValue = firestore.FieldValue; - fieldValueV9Deprecation( - () => serverTimestamp(), - () => fieldValue.serverTimestamp(), - 'serverTimestamp', - ); - }); - - it('FieldValue.arrayUnion()', function () { - const fieldValue = firestore.FieldValue; - fieldValueV9Deprecation( - () => arrayUnion('foo'), - () => fieldValue.arrayUnion('bar'), - 'arrayUnion', - ); - }); - - it('FieldValue.arrayRemove()', function () { - const fieldValue = firestore.FieldValue; - fieldValueV9Deprecation( - () => arrayRemove('foo'), - () => fieldValue.arrayRemove('bar'), - 'arrayRemove', - ); - }); - }); - - describe('statics', function () { - it('Firestore.setLogLevel()', function () { - // @ts-ignore test - jest - .spyOn(nativeModule, 'getReactNativeModule') - .mockReturnValue({ setLogLevel: jest.fn() }); - - // FIXME - the spy here is not working correctly? setLogLevel doesn't exist - // even though they are specified as jest.fn() mock - // setting it in jest.setup.ts seems to have no effect either - // staticsV9Deprecation( - // () => setLogLevel('debug'), - // () => firestore.setLogLevel('debug'), - // 'setLogLevel', - // ); - }); - - it('Filter static', function () { - staticsV9Deprecation( - // no corresponding method - () => {}, - () => firestore.Filter, - 'Filter', - ); - }); - - it('Timestamp static', function () { - staticsV9Deprecation( - () => Timestamp, - () => firestore.Timestamp, - 'Timestamp', - ); - }); - - it('FieldValue static', function () { - staticsV9Deprecation( - () => FieldValue, - () => firestore.FieldValue, - 'FieldValue', - ); - }); - - it('GeoPoint static', function () { - staticsV9Deprecation( - () => GeoPoint, - () => firestore.GeoPoint, - 'GeoPoint', - ); - }); - - it('Blob static', function () { - staticsV9Deprecation( - () => Blob, - () => firestore.Blob, - 'Blob', - ); - }); - - it('FieldPath static', function () { - staticsV9Deprecation( - () => FieldPath, - () => firestore.FieldPath, - 'FieldPath', - ); - }); - }); - - describe('Filter', function () { - it('Filter.or()', function () { - const filter = firestore.Filter; - filterV9Deprecation( - () => or(where('foo.bar', '==', null), where('foo.bar', '==', null)), - () => filter.or(filter('foo', '==', 'bar'), filter('baz', '==', 'qux')), - 'or', - ); - }); - - it('Filter.and()', function () { - const filter = firestore.Filter; - filterV9Deprecation( - () => and(where('foo.bar', '==', null), where('foo.bar', '==', null)), - () => filter.and(filter('foo', '==', 'bar'), filter('baz', '==', 'qux')), - 'and', - ); - }); - }); - - describe('FirestorePersistentCacheIndexManager', function () { - it('firestore.persistentCacheIndexManager()', function () { - const firestore = getFirestore(); - const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); - - firestoreRefV9Deprecation( - () => getPersistentCacheIndexManager(firestore), - () => firestore2.persistentCacheIndexManager(), - 'persistentCacheIndexManager', - ); - }); - - it('FirestorePersistentCacheIndexManager.enableIndexAutoCreation()', function () { - const firestore = getFirestore(); - // @ts-ignore test - firestore._settings.persistence = true; - const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); - // @ts-ignore test - firestore2._settings.persistence = true; - const indexManager2 = withDeprecationWarningsSilenced( - () => firestore2.persistentCacheIndexManager()!, - ); - persistentCacheIndexManagerV9Deprecation( - () => enablePersistentCacheIndexAutoCreation(getPersistentCacheIndexManager(firestore)!), - () => indexManager2.enableIndexAutoCreation(), - 'enableIndexAutoCreation', - ); - }); - - it('FirestorePersistentCacheIndexManager.disableIndexAutoCreation()', function () { - const firestore = getFirestore(); - // @ts-ignore test - firestore._settings.persistence = true; - const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); - // @ts-ignore test - firestore2._settings.persistence = true; - const indexManager2 = withDeprecationWarningsSilenced( - () => firestore2.persistentCacheIndexManager()!, - ); - persistentCacheIndexManagerV9Deprecation( - () => disablePersistentCacheIndexAutoCreation(getPersistentCacheIndexManager(firestore)!), - () => indexManager2.disableIndexAutoCreation(), - 'disableIndexAutoCreation', - ); - }); - - it('FirestorePersistentCacheIndexManager.deleteAllIndexes()', function () { - const firestore = getFirestore(); - // @ts-ignore test - firestore._settings.persistence = true; - const firestore2 = withDeprecationWarningsSilenced(() => firebase.firestore()); - // @ts-ignore test - firestore2._settings.persistence = true; - const indexManager2 = withDeprecationWarningsSilenced( - () => firestore2.persistentCacheIndexManager()!, - ); - persistentCacheIndexManagerV9Deprecation( - () => deleteAllPersistentCacheIndexes(getPersistentCacheIndexManager(firestore)!), - () => indexManager2.deleteAllIndexes(), - 'deleteAllIndexes', - ); - }); - }); - - describe('Timestamp', function () { - it('Timestamp.seconds', function () { - const timestamp2 = withDeprecationWarningsSilenced( - () => new firebase.firestore.Timestamp(2, 3), - ); - timestampV9Deprecation( - () => new Timestamp(2, 3).seconds, - () => timestamp2.seconds, - 'seconds', - ); - }); - - it('Timestamp.nanoseconds', function () { - const timestamp2 = withDeprecationWarningsSilenced( - () => new firebase.firestore.Timestamp(2000, 3000000), - ); - timestampV9Deprecation( - () => new Timestamp(2000, 3000000).nanoseconds, - () => timestamp2.nanoseconds, - 'nanoseconds', - ); - }); - }); - }); - describe('VectorValue (unit serializer)', function () { it('constructs and validates values', function () { const v = vector([0, 1.5, -2]); @@ -1569,4 +389,31 @@ describe('Firestore', function () { expect(getTypeMapName(arr[0][0])).toBe('vector'); }); }); + + describe('FieldValue (unit serializer)', function () { + it('serializes arrayUnion as fieldvalue without nested arrays', function () { + const serialize = require('../lib/utils/serialize'); + const { getTypeMapName } = require('../lib/utils/typemap'); + + const fieldValue = arrayUnion(3, 4); + const typed = serialize.generateNativeData(fieldValue, false); + expect(getTypeMapName(typed[0])).toBe('fieldvalue'); + + const payload = typed[1] as [string, unknown[]]; + expect(payload[0]).toBe('array_union'); + expect(Array.isArray(payload[1])).toBe(true); + expect(payload[1].every(element => Array.isArray(element) && element.length === 2)).toBe( + true, + ); + expect( + payload[1].every(element => { + const typeMap = element as [number, unknown?]; + return getTypeMapName(typeMap[0]) !== 'array'; + }), + ).toBe(true); + + const updatePayload = serialize.buildNativeMap({ foo: fieldValue }, false); + expect(getTypeMapName(updatePayload.foo[0])).toBe('fieldvalue'); + }); + }); }); diff --git a/packages/firestore/__tests__/pipelines-fluent-serialization.test.ts b/packages/firestore/__tests__/pipelines-fluent-serialization.test.ts index 95328fabcc..8e527073ae 100644 --- a/packages/firestore/__tests__/pipelines-fluent-serialization.test.ts +++ b/packages/firestore/__tests__/pipelines-fluent-serialization.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { firebase } from '../lib'; +import { getFirestore } from '../lib'; import { field, switchOn, equal, constant } from '../lib/pipelines'; import '../lib/pipelines'; import { @@ -39,7 +39,7 @@ function stripRuntimeKeys(expr: unknown): unknown { } describe('pipelines fluent serialization parity', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const cases = buildFluentParityCases(); it('defines parity cases for every EXPRESSION_METHOD_NAMES entry', function () { diff --git a/packages/firestore/__tests__/pipelines-pathological.test.ts b/packages/firestore/__tests__/pipelines-pathological.test.ts index 2e7e6409f4..0abdec4beb 100644 --- a/packages/firestore/__tests__/pipelines-pathological.test.ts +++ b/packages/firestore/__tests__/pipelines-pathological.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { firebase } from '../lib'; +import { getFirestore } from '../lib'; import { add, and, @@ -23,7 +23,7 @@ import { validateSerializedPipeline } from '../lib/pipelines/pipeline_validate'; * (the `PipelineValue` opaque-handling path) and a wide, mixed pipeline. */ describe('pipelines pathological / stress coverage', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); describe('deep expression serialization correctness', function () { it('serializes a 64-deep add() chain to the correct nested structure', function () { diff --git a/packages/firestore/__tests__/pipelines-serialization-matrix.test.ts b/packages/firestore/__tests__/pipelines-serialization-matrix.test.ts index 054fce7d4e..f1a1c37d6f 100644 --- a/packages/firestore/__tests__/pipelines-serialization-matrix.test.ts +++ b/packages/firestore/__tests__/pipelines-serialization-matrix.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { firebase } from '../lib'; +import { getFirestore } from '../lib'; import { field, timestampDiff, timestampExtract } from '../lib/pipelines'; import '../lib/pipelines'; @@ -19,7 +19,7 @@ describe('pipelines serialization matrix', function () { }); describe('timestampDiff global overload matrix', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); it('serializes (Expression, Expression, unit)', function () { expect( @@ -79,7 +79,7 @@ describe('pipelines serialization matrix', function () { }); describe('timestampExtract global overload matrix', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); it('serializes (Expression, TimePart)', function () { expect( diff --git a/packages/firestore/__tests__/pipelines.test.ts b/packages/firestore/__tests__/pipelines.test.ts index 606cec1be4..1d703ff007 100644 --- a/packages/firestore/__tests__/pipelines.test.ts +++ b/packages/firestore/__tests__/pipelines.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, jest } from '@jest/globals'; -import { firebase } from '../lib'; +import { getFirestore } from '../lib'; +import { getApp } from '@react-native-firebase/app'; import { arrayFilter, arrayFirst, @@ -32,7 +33,7 @@ import { ConstantExpression, FunctionExpression } from '../lib/pipelines/express describe('Firestore pipelines runtime', function () { it('installs pipeline() and serializes source builders', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const docRef = db.doc('firestore/a'); const query = db .collection('firestore') @@ -70,7 +71,7 @@ describe('Firestore pipelines runtime', function () { }); it('normalizes stage option keys and preserves stage order', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const pipeline = db .pipeline() @@ -100,7 +101,7 @@ describe('Firestore pipelines runtime', function () { }); it('treats unnest selectable overload as selectable, not options object', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -118,7 +119,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes rawStage params as an object so native bridges preserve named params', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -151,7 +152,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes arrayFilter as a function expression helper and fluent method', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -213,7 +214,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes currentDocument as a zero-argument function expression helper', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -238,7 +239,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes ifNull as a function expression helper and fluent method', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -292,7 +293,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes switchOn as a function expression helper', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -345,7 +346,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes coalesce as a function expression helper and fluent method', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -408,7 +409,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes newer array expression helpers with SDK-compatible arguments', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const serialized = db .pipeline() .collection('firestore') @@ -506,8 +507,8 @@ describe('Firestore pipelines runtime', function () { }); it('enforces union guards and self-cycle serialization constraints', function () { - const db: any = firebase.firestore(); - const secondaryDb: any = firebase.app('secondaryFromNative').firestore(); + const db: any = getFirestore(); + const secondaryDb: any = getFirestore(getApp('secondaryFromNative')); const base = db.pipeline().collection('firestore'); expect(() => base.union({} as any)).toThrow( @@ -526,8 +527,8 @@ describe('Firestore pipelines runtime', function () { }); it('enforces createFrom cross-firestore and query-shape guards', function () { - const db: any = firebase.firestore(); - const secondaryDb: any = firebase.app('secondaryFromNative').firestore(); + const db: any = getFirestore(); + const secondaryDb: any = getFirestore(getApp('secondaryFromNative')); const secondaryQuery = secondaryDb.collection('firestore').where('value', '==', 1); expect(() => db.pipeline().createFrom({} as any)).toThrow( @@ -540,8 +541,8 @@ describe('Firestore pipelines runtime', function () { }); it('enforces source reference affinity for collection() and documents()', function () { - const db: any = firebase.firestore(); - const secondaryDb: any = firebase.app('secondaryFromNative').firestore(); + const db: any = getFirestore(); + const secondaryDb: any = getFirestore(getApp('secondaryFromNative')); expect(() => db.pipeline().collection(secondaryDb.collection('firestore'))).toThrow( 'firebase.firestore().pipeline().collection(*) cannot use a reference from a different Firestore instance.', @@ -565,7 +566,7 @@ describe('Firestore pipelines runtime', function () { }); it('validates execute input and rejects unsupported execute options', async function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const nativeExecute = jest.fn(async () => ({ executionTime: [1735689600, 123000000], results: [{ path: 'firestore/a', data: { value: 42 } }], @@ -616,7 +617,7 @@ describe('Firestore pipelines runtime', function () { }); it('throws when pipelineExecute omits executionTime', async function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const originalNativeModule = db._nativeModule; db._nativeModule = { pipelineExecute: jest.fn(async () => ({ @@ -646,7 +647,7 @@ describe('Firestore pipelines runtime', function () { }); it('supports method-style expression chaining and ordering helper serialization', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const pipeline = db .pipeline() @@ -747,7 +748,7 @@ describe('Firestore pipelines runtime', function () { }); it('serializes subcollection detached pipelines and scalar subqueries', function () { - const db: any = firebase.firestore(); + const db: any = getFirestore(); const detached = subcollection('reviews'); expect(detached.serialize()).toEqual({ source: { source: 'subcollection', path: 'reviews' }, diff --git a/packages/firestore/consumer-type-test.ts b/packages/firestore/consumer-type-test.ts index 21e348e37b..442b15b9a0 100644 --- a/packages/firestore/consumer-type-test.ts +++ b/packages/firestore/consumer-type-test.ts @@ -1,16 +1,10 @@ /* * Consumer-facing API type tests for @react-native-firebase/firestore. - * Part 1: Namespaced API (firebase.firestore(), default firestore()). - * Part 2: Modular API (getFirestore, doc, collection, setDoc, etc. from the - * published/root package entrypoint). + * Modular API (getFirestore, doc, collection, setDoc, etc. from the + * published package entrypoint). */ -// --------------------------------------------------------------------------- -// PART 1 — NAMESPACED API -// --------------------------------------------------------------------------- -// Import namespaced API: default export and firebase, plus types used in Part 1. -import firestore, { - firebase, +import { getFirestore, connectFirestoreEmulator, setLogLevel, @@ -81,11 +75,10 @@ import firestore, { AggregateQuerySnapshot, DocumentSnapshot, DocumentReference, - Query, QueryDocumentSnapshot, } from '@react-native-firebase/firestore'; +import { getApp } from '@react-native-firebase/app'; import type { - FirebaseFirestoreTypes, Firestore, DocumentData, LoadBundleTaskProgress, @@ -94,6 +87,7 @@ import type { PartialWithFieldValue, SetOptions, Transaction, + Query, } from '@react-native-firebase/firestore'; import { execute, @@ -293,230 +287,14 @@ import type { // Reproducer for issue #8975: this should compile for consumers. export function f(_snap: DocumentSnapshot): void {} -// ----- Default export and module access ----- -void firestore().app; -void firestore.SDK_VERSION; -void firestore.firebase.SDK_VERSION; - -// ----- firebase.firestore at root ----- -void firebase.firestore().app.name; -void firebase.firestore().collection('foo'); - -// ----- firebase.firestore at app level ----- -void firebase.app().firestore().app.name; -void firebase.app().firestore().collection('foo'); - -// ----- Multi-app and database ID ----- -void firebase.firestore(firebase.app()).app.name; -void firebase.firestore(firebase.app('foo')).app.name; -// Database ID: firebase.app().firestore(databaseId) -void firebase.app().firestore('(default)').app.name; -void firebase.app().firestore('other-db').app.name; - -// ----- Default export with app arg ----- -void firestore(firebase.app()).app.name; - -// ----- Statics (FirestoreStatics) ----- -void firebase.firestore.Blob; -void firebase.firestore.FieldPath; -void firebase.firestore.FieldValue; -void firebase.firestore.GeoPoint; -void firebase.firestore.Timestamp; -void firebase.firestore.Filter; -void firebase.firestore.CACHE_SIZE_UNLIMITED; -firebase.firestore.setLogLevel('debug'); - -// ----- Firestore instance: references and batch ----- -const nsFirestore = firebase.firestore(); -void nsFirestore.collection('users'); -void nsFirestore.collection('users').doc('alice'); -void nsFirestore.collection('users').doc('alice').collection('orders'); -void nsFirestore.collectionGroup('orders'); -void nsFirestore.batch(); - -// ----- CollectionReference ----- -const nsColl = nsFirestore.collection('users'); -const nsDocRef = nsColl.doc('alice'); -const nsQuery = nsColl.where('name', '==', 'test'); - -nsDocRef.set({ name: 'Alice', count: 1 }).then(() => {}); -nsDocRef.set({ name: 'Alice' }, { merge: true }).then(() => {}); - -nsDocRef.update({ count: 2 }).then(() => {}); -nsDocRef.update('count', 3).then(() => {}); - -nsDocRef.delete().then(() => {}); - -nsColl.add({ name: 'Bob' }).then((ref: FirebaseFirestoreTypes.DocumentReference) => { - void ref.id; -}); - -nsDocRef.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - void snap.exists(); - void snap.data(); - void snap.id; - void snap.ref; - void snap.metadata; -}); -nsDocRef.get({ source: 'cache' }).then(() => {}); -nsDocRef.get({ source: 'server' }).then(() => {}); - -nsQuery.get().then((snap: FirebaseFirestoreTypes.QuerySnapshot) => { - void snap.docs; - void snap.empty; - void snap.size; - void snap.docChanges(); -}); - -// ----- DocumentSnapshot ----- -nsDocRef.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - if (snap.exists()) { - const d = snap.data(); - void d; - } - void snap.get('field'); - void snap.metadata.isEqual(snap.metadata); -}); - -// ----- onSnapshot (document) ----- -const unsubDoc1 = nsDocRef.onSnapshot((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - void snap.data(); -}); -const unsubDoc2 = nsDocRef.onSnapshot( - { includeMetadataChanges: true }, - (_snap: FirebaseFirestoreTypes.DocumentSnapshot) => {}, -); -const unsubDoc3 = nsDocRef.onSnapshot({ - next: (_snap: FirebaseFirestoreTypes.DocumentSnapshot) => {}, - error: (_e: Error) => {}, -}); -unsubDoc1(); -unsubDoc2(); -unsubDoc3(); - -// ----- onSnapshot (query) ----- -const unsubQuery1 = nsQuery.onSnapshot((snap: FirebaseFirestoreTypes.QuerySnapshot) => { - void snap.docs; -}); -const unsubQuery2 = nsQuery.onSnapshot( - { includeMetadataChanges: true }, - { next: (_snap: FirebaseFirestoreTypes.QuerySnapshot) => {}, error: (_e: Error) => {} }, -); -unsubQuery1(); -unsubQuery2(); - -// ----- Query: where, orderBy, limit, cursor ----- -const nsQuery2 = nsColl - .where('age', '>', 18) - .orderBy('age', 'desc') - .orderBy('name') - .limit(10) - .limitToLast(5) - .startAt(1) - .startAfter(2) - .endAt(10) - .endBefore(9); -void nsQuery2; - -// ----- Firestore instance: loadBundle, namedQuery, runTransaction ----- -const nsLoadTask = nsFirestore.loadBundle('bundle-data'); -void nsLoadTask.then(() => {}); - -const nsNamed = nsFirestore.namedQuery('my-query'); -void nsNamed; - -nsFirestore - .runTransaction(async (tx: FirebaseFirestoreTypes.Transaction) => { - const snap = await tx.get(nsDocRef); - if (snap.exists()) { - tx.update(nsDocRef, { count: ((snap.data() as { count?: number })?.count ?? 0) + 1 }); - } - return null; - }) - .then(() => {}); - -// ----- Firestore instance: persistence and network ----- -nsFirestore.clearPersistence().then(() => {}); -nsFirestore.waitForPendingWrites().then(() => {}); -nsFirestore.terminate().then(() => {}); -nsFirestore.useEmulator('localhost', 8080); -nsFirestore.enableNetwork().then(() => {}); -nsFirestore.disableNetwork().then(() => {}); - -// ----- Firestore instance: settings ----- -nsFirestore.settings({ persistence: true }).then(() => {}); - -// ----- Persistent cache index manager (namespaced) ----- -const nsIndexManager = nsFirestore.persistentCacheIndexManager(); -if (nsIndexManager) { - nsIndexManager.enableIndexAutoCreation().then(() => {}); - nsIndexManager.disableIndexAutoCreation().then(() => {}); - nsIndexManager.deleteAllIndexes().then(() => {}); -} - -// ----- WriteBatch (namespaced) ----- -const nsBatch = nsFirestore.batch(); -nsBatch.set(nsDocRef, { name: 'Alice' }); -nsBatch.set(nsDocRef, { name: 'Alice' }, { merge: true }); -nsBatch.update(nsDocRef, { count: 1 }); -nsBatch.delete(nsDocRef); -nsBatch.commit().then(() => {}); - -// ----- Namespaced FieldValue (statics) ----- -const nsFieldPath = new firebase.firestore.FieldPath('user', 'name'); -void nsFieldPath; -const nsBlob = firebase.firestore.Blob.fromBase64String('dGVzdA=='); -void nsBlob; -const nsGeoPoint = new firebase.firestore.GeoPoint(0, 0); -void nsGeoPoint; -const nsTimestamp = firebase.firestore.Timestamp.now(); -void nsTimestamp; -const nsDelete = firebase.firestore.FieldValue.delete(); -const nsServerTs = firebase.firestore.FieldValue.serverTimestamp(); -const nsArrayUnion = firebase.firestore.FieldValue.arrayUnion(1, 2); -const nsArrayRemove = firebase.firestore.FieldValue.arrayRemove(1); -void nsArrayRemove; -const nsIncrement = firebase.firestore.FieldValue.increment(1); - -nsDocRef - .set({ - name: 'x', - deleted: nsDelete, - ts: nsServerTs, - arr: nsArrayUnion, - cnt: nsIncrement, - }) - .then(() => {}); - -// ----- withConverter (namespaced) ----- -interface User { - name: string; - age: number; -} -const nsConverter: FirebaseFirestoreTypes.FirestoreDataConverter = { - toFirestore(u: User) { - return u; - }, - fromFirestore(snap: FirebaseFirestoreTypes.QueryDocumentSnapshot): User { - return snap.data() as User; - }, -}; -const nsCollWithConv = nsFirestore.collection('users').withConverter(nsConverter); -const nsDocWithConv = nsCollWithConv.doc('alice'); -nsDocWithConv.set({ name: 'Alice', age: 30 }).then(() => {}); -nsDocWithConv.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - const u = snap.data(); - if (u) void [u.name, u.age]; -}); - // ----- getFirestore ----- const modFirestore1 = getFirestore(); void modFirestore1.app.name; -const modFirestore2 = getFirestore(firebase.app()); +const modFirestore2 = getFirestore(getApp()); void modFirestore2.app.name; -const modFirestore3 = getFirestore(firebase.app(), 'other-db'); +const modFirestore3 = getFirestore(getApp(), 'other-db'); void modFirestore3.app.name; // ----- connectFirestoreEmulator ----- @@ -529,7 +307,7 @@ connectFirestoreEmulator(modFirestore1, 'localhost', 8080, { setLogLevel('debug'); // ----- initializeFirestore ----- -initializeFirestore(firebase.app(), { +initializeFirestore(getApp(), { cacheSizeBytes: CACHE_SIZE_UNLIMITED, }).then((fs: Firestore) => { void fs.app.name; diff --git a/packages/firestore/e2e/Aggregate/AggregateQuery.e2e.js b/packages/firestore/e2e/Aggregate/AggregateQuery.e2e.js index a3eb90ccc8..c2116da5e3 100644 --- a/packages/firestore/e2e/Aggregate/AggregateQuery.e2e.js +++ b/packages/firestore/e2e/Aggregate/AggregateQuery.e2e.js @@ -33,7 +33,7 @@ describe('getAggregateFromServer()', function () { return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { error.message.should.containEql( - 'getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `FirestoreQuery`', + 'getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `Query`', ); } @@ -42,7 +42,7 @@ describe('getAggregateFromServer()', function () { return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { error.message.should.containEql( - 'getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `FirestoreQuery`', + 'getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `Query`', ); } @@ -51,7 +51,7 @@ describe('getAggregateFromServer()', function () { return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { error.message.should.containEql( - 'getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `FirestoreQuery`', + 'getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `Query`', ); } return Promise.resolve(); diff --git a/packages/firestore/e2e/Aggregate/count.e2e.js b/packages/firestore/e2e/Aggregate/count.e2e.js index 782ae145e8..6157a8c49b 100644 --- a/packages/firestore/e2e/Aggregate/count.e2e.js +++ b/packages/firestore/e2e/Aggregate/count.e2e.js @@ -15,85 +15,8 @@ * */ const COLLECTION = 'firestore'; -const { wipe } = require('../helpers'); describe('firestore().collection().count()', function () { - before(async function () { - return await wipe(); - }); - - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).startAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - - it('gets count of collection reference - unfiltered', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); - - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.count().get(); - qs.data().count.should.eql(3); - }); - - it('gets countFromServer of collection reference - unfiltered', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); - - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.countFromServer().get(); - qs.data().count.should.eql(3); - }); - - it('gets correct count of collection reference - where equal', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/count/collection`); - - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.where('foo', '==', 3).count().get(); - qs.data().count.should.eql(1); - }); - }); - describe('modular', function () { it('throws if no argument provided', function () { const { getFirestore, collection, startAt, query } = firestoreModular; diff --git a/packages/firestore/e2e/Blob.e2e.js b/packages/firestore/e2e/Blob.e2e.js deleted file mode 100644 index 0831ccb513..0000000000 --- a/packages/firestore/e2e/Blob.e2e.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const testObject = { hello: 'world' }; -const testString = JSON.stringify(testObject); -const testBuffer = [123, 34, 104, 101, 108, 108, 111, 34, 58, 34, 119, 111, 114, 108, 100, 34, 125]; -const testBase64 = 'eyJoZWxsbyI6IndvcmxkIn0='; - -describe('firestore.Blob', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('should throw if constructed manually', function () { - try { - new firebase.firestore.Blob(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('constructor is private'); - return Promise.resolve(); - } - }); - - it('should be exported as a static', function () { - const { Blob } = firebase.firestore; - should.exist(Blob); - }); - - it('.fromBase64String() -> returns new instance of Blob', async function () { - const { Blob } = firebase.firestore; - const myBlob = Blob.fromBase64String(testBase64); - myBlob.should.be.instanceOf(Blob); - myBlob._binaryString.should.equal(testString); - should.deepEqual( - JSON.parse(myBlob._binaryString), - testObject, - 'Expected Blob _binaryString internals to serialize to json and match test object', - ); - }); - - it('.fromBase64String() -> throws if arg not typeof string and length > 0', async function () { - const { Blob } = firebase.firestore; - const myBlob = Blob.fromBase64String(testBase64); - myBlob.should.be.instanceOf(Blob); - (() => Blob.fromBase64String(1234)).should.throwError(); - (() => Blob.fromBase64String('')).should.throwError(); - }); - - it('.fromUint8Array() -> returns new instance of Blob', async function () { - const testUInt8Array = new Uint8Array(testBuffer); - const { Blob } = firebase.firestore; - const myBlob = Blob.fromUint8Array(testUInt8Array); - myBlob.should.be.instanceOf(Blob); - const json = JSON.parse(myBlob._binaryString); - json.hello.should.equal('world'); - }); - - it('.fromUint8Array() -> throws if arg not instanceof Uint8Array', async function () { - const testUInt8Array = new Uint8Array(testBuffer); - const { Blob } = firebase.firestore; - const myBlob = Blob.fromUint8Array(testUInt8Array); - myBlob.should.be.instanceOf(Blob); - (() => Blob.fromUint8Array('derp')).should.throwError(); - }); - - it('.toString() -> returns string representation of blob instance', async function () { - const { Blob } = firebase.firestore; - const myBlob = Blob.fromBase64String(testBase64); - myBlob.should.be.instanceOf(Blob); - should.equal( - myBlob.toString().includes(testBase64), - true, - 'toString() should return a string that includes the base64', - ); - }); - - it('.isEqual() -> returns true or false', async function () { - const { Blob } = firebase.firestore; - const myBlob = Blob.fromBase64String(testBase64); - const myBlob2 = Blob.fromBase64String('aGVsbG8gdGhlcmUh'); // 'hello there!' - myBlob.isEqual(myBlob).should.equal(true); - myBlob2.isEqual(myBlob).should.equal(false); - }); - - it('.isEqual() -> throws if arg not instanceof Blob', async function () { - const { Blob } = firebase.firestore; - const myBlob = Blob.fromBase64String(testBase64); - myBlob.isEqual(myBlob).should.equal(true); - (() => myBlob.isEqual('derp')).should.throwError(); - }); - - it('.toBase64() -> returns base64 string', async function () { - const { Blob } = firebase.firestore; - const myBlob = Blob.fromBase64String(testBase64); - myBlob.should.be.instanceOf(Blob); - myBlob.toBase64().should.equal(testBase64); - }); - - it('.toUint8Array() -> returns Uint8Array', async function () { - const { Blob } = firebase.firestore; - const myBlob = Blob.fromBase64String(testBase64); - const testUInt8Array = new Uint8Array(testBuffer); - const testUInt8Array2 = new Uint8Array(); - - myBlob.should.be.instanceOf(Blob); - should.deepEqual(myBlob.toUint8Array(), testUInt8Array); - should.notDeepEqual(myBlob.toUint8Array(), testUInt8Array2); - }); -}); diff --git a/packages/firestore/e2e/Bundle/loadBundle.e2e.js b/packages/firestore/e2e/Bundle/loadBundle.e2e.js index 0572aebe33..fc91d739e9 100644 --- a/packages/firestore/e2e/Bundle/loadBundle.e2e.js +++ b/packages/firestore/e2e/Bundle/loadBundle.e2e.js @@ -17,7 +17,7 @@ const { wipe, getBundle, BUNDLE_COLLECTION } = require('../helpers'); describe('firestore().loadBundle()', function () { - // Not supported on web. + // Not supported on web lite SDK. if (Platform.other) { return; } @@ -26,45 +26,6 @@ describe('firestore().loadBundle()', function () { return await wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('loads the bundle contents', async function () { - const bundle = getBundle(); - const task = firebase.firestore().loadBundle(bundle); - task.onProgress(); - const progress = await task; - const query = firebase.firestore().collection(BUNDLE_COLLECTION); - const snapshot = await query.get({ source: 'cache' }); - - progress.taskState.should.eql('Success'); - progress.documentsLoaded.should.eql(6); - snapshot.size.should.eql(6); - }); - - it('throws if invalid bundle', async function () { - try { - await firebase.firestore().loadBundle('not-a-bundle'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (_) { - /* - * Due to inconsistent error throws between Android and iOS Firebase SDK, - * it is not able to test a specific error message. - * Android SDK throws 'invalid-arguments', while iOS SDK throws 'unknown' - */ - return Promise.resolve(); - } - }); - }); - describe('modular', function () { it('loads the bundle contents', async function () { const { getFirestore, loadBundle, collection, getDocsFromCache } = firestoreModular; diff --git a/packages/firestore/e2e/Bundle/namedQuery.e2e.js b/packages/firestore/e2e/Bundle/namedQuery.e2e.js index dd09b430a9..dd9eb40ad3 100644 --- a/packages/firestore/e2e/Bundle/namedQuery.e2e.js +++ b/packages/firestore/e2e/Bundle/namedQuery.e2e.js @@ -27,97 +27,6 @@ describe('firestore().namedQuery()', function () { return await loadBundle(getFirestore(), getBundle()); }); - // FIXME named query functionality appears to have an android-only issue - // with loading and clearing and re-loading bundles. We test modular but not v8 - // APIs since they are the future, and disable the v8 compat APIs for now - // Still bears investigating, there could be a race condition either in our android - // native code or in firebase-android-sdk - xdescribe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns bundled QuerySnapshot', async function () { - const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); - const snapshot = await query.get({ source: 'cache' }); - - snapshot.constructor.name.should.eql('QuerySnapshot'); - snapshot.docs.forEach(doc => { - doc.data().number.should.equalOneOf(1, 2, 3); - doc.metadata.fromCache.should.eql(true); - }); - }); - - it('limits the number of documents in bundled QuerySnapshot', async function () { - const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); - const snapshot = await query.limit(1).get({ source: 'cache' }); - - snapshot.size.should.equal(1); - snapshot.docs[0].metadata.fromCache.should.eql(true); - }); - - // TODO: log upstream issue - this broke with BoM >= 32.0.0, source always appears to be cache now - xit('returns QuerySnapshot from firestore backend when omitting "source: cache"', async function () { - const docRef = firebase.firestore().collection(BUNDLE_COLLECTION).doc(); - await docRef.set({ number: 4 }); - - const query = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME); - const snapshot = await query.get(); - - snapshot.size.should.equal(1); - snapshot.docs[0].data().number.should.eql(4); - snapshot.docs[0].metadata.fromCache.should.eql(false); - }); - - it('calls onNext with QuerySnapshot from firestore backend', async function () { - const docRef = firebase.firestore().collection(BUNDLE_COLLECTION).doc(); - await docRef.set({ number: 5 }); - - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().namedQuery(BUNDLE_QUERY_NAME).onSnapshot(onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - // FIXME not stable on tests::test-reuse - // 5 on first run, 4 on reuse - // onNext.args[0][0].docs[0].data().number.should.eql(4); - unsub(); - }); - - it('throws if invalid query name', async function () { - const query = firebase.firestore().namedQuery('invalid-query'); - try { - await query.get({ source: 'cache' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('unknown'); - return Promise.resolve(); - } - }); - - it('calls onError if invalid query name', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().namedQuery('invalid-query').onSnapshot(onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onError); - - onNext.should.be.callCount(0); - onError.should.be.calledOnce(); - onError.args[0][0].message.should.containEql('unknown'); - unsub(); - }); - }); - describe('modular', function () { it('returns bundled QuerySnapshot', async function () { const { getFirestore, namedQuery, getDocsFromCache } = firestoreModular; diff --git a/packages/firestore/e2e/CollectionReference/add.e2e.js b/packages/firestore/e2e/CollectionReference/add.e2e.js index 9e3c200c9e..ef3cf34a75 100644 --- a/packages/firestore/e2e/CollectionReference/add.e2e.js +++ b/packages/firestore/e2e/CollectionReference/add.e2e.js @@ -22,38 +22,6 @@ describe('firestore.collection().add()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if data is not an object', function () { - try { - firebase.firestore().collection(COLLECTION).add(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object"); - return Promise.resolve(); - } - }); - - it('adds a new document', async function () { - const data = { foo: 'bar' }; - const docRef = await firebase.firestore().collection(COLLECTION).add(data); - should.equal(docRef.constructor.name, 'DocumentReference'); - const docSnap = await docRef.get(); - docSnap.data().should.eql(jet.contextify(data)); - docSnap.exists().should.eql(true); - await docRef.delete(); - }); - }); - describe('modular', function () { it('throws if data is not an object', function () { const { getFirestore, collection, addDoc } = firestoreModular; diff --git a/packages/firestore/e2e/CollectionReference/doc.e2e.js b/packages/firestore/e2e/CollectionReference/doc.e2e.js index c969463cd3..09250faf20 100644 --- a/packages/firestore/e2e/CollectionReference/doc.e2e.js +++ b/packages/firestore/e2e/CollectionReference/doc.e2e.js @@ -22,38 +22,6 @@ describe('firestore.collection().doc()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if path is not a document', function () { - try { - firebase.firestore().collection(COLLECTION).doc('bar/baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentPath' must point to a document"); - return Promise.resolve(); - } - }); - - it('generates an ID if no path is provided', function () { - const instance = firebase.firestore().collection(COLLECTION).doc(); - should.equal(20, instance.id.length); - }); - - it('uses path if provided', function () { - const instance = firebase.firestore().collection(COLLECTION).doc('bar'); - instance.id.should.eql('bar'); - }); - }); - describe('modular', function () { it('throws if path is not a document', function () { const { getFirestore, collection, doc } = firestoreModular; diff --git a/packages/firestore/e2e/CollectionReference/properties.e2e.js b/packages/firestore/e2e/CollectionReference/properties.e2e.js index ddc46b4166..45a8272207 100644 --- a/packages/firestore/e2e/CollectionReference/properties.e2e.js +++ b/packages/firestore/e2e/CollectionReference/properties.e2e.js @@ -22,48 +22,6 @@ describe('firestore.collection()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns the firestore instance', function () { - const instance = firebase.firestore().collection(COLLECTION); - instance.firestore.app.name.should.eql('[DEFAULT]'); - }); - - it('returns the collection id', function () { - const instance1 = firebase.firestore().collection(COLLECTION); - const instance2 = firebase.firestore().collection(`${COLLECTION}/bar/baz`); - instance1.id.should.eql(COLLECTION); - instance2.id.should.eql('baz'); - }); - - it('returns the collection parent', function () { - const instance1 = firebase.firestore().collection(COLLECTION); - should.equal(instance1.parent, null); - const instance2 = firebase.firestore().collection('foo').doc('bar').collection('baz'); - should.equal(instance2.parent.id, 'bar'); - }); - - it('returns the firestore path', function () { - const instance1 = firebase.firestore().collection(COLLECTION); - instance1.path.should.eql(COLLECTION); - const instance2 = firebase - .firestore() - .collection(COLLECTION) - .doc('bar') - .collection(COLLECTION); - instance2.path.should.eql(`${COLLECTION}/bar/${COLLECTION}`); - }); - }); - describe('modular', function () { it('returns the firestore instance', function () { const { getFirestore, collection } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentChange.e2e.js b/packages/firestore/e2e/DocumentChange.e2e.js index 726a8595c3..f1c168a8b7 100644 --- a/packages/firestore/e2e/DocumentChange.e2e.js +++ b/packages/firestore/e2e/DocumentChange.e2e.js @@ -22,122 +22,6 @@ describe('firestore.DocumentChange', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('.doc -> returns a DocumentSnapshot', async function () { - if (Platform.other) { - return; - } - - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - const changes = snapshot.docChanges(); - - const docChange = changes[0]; - - docChange.doc.constructor.name.should.eql('DocumentSnapshot'); - }); - - it('returns the correct metadata when adding and removing', async function () { - if (Platform.other) { - return; - } - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/docChanges/docChangesCollection`); - const doc1 = firebase.firestore().doc(`${COLLECTION}/docChanges/docChangesCollection/doc1`); - - // Set something in the database - await doc1.set({ name: 'doc1' }); - - // Subscribe to changes - const callback = sinon.spy(); - const unsub = colRef.onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); - - // Validate docChange item exists - callback.should.be.calledOnce(); - const changes1 = callback.args[0][0].docChanges(); - changes1.length.should.eql(1); - changes1[0].newIndex.should.eql(0); - changes1[0].oldIndex.should.eql(-1); - changes1[0].type.should.eql('added'); - changes1[0].doc.data().name.should.eql('doc1'); - - // Delete the document - await doc1.delete(); - await Utils.sleep(800); - - // The QuerySnapshot should be empty - callback.args[1][0].size.should.eql(0); - - // The docChanges should keep removed doc - const changes2 = callback.args[1][0].docChanges(); - changes2.length.should.eql(1); - - changes2[0].doc.data().name.should.eql('doc1'); - changes2[0].type.should.eql('removed'); - changes2[0].newIndex.should.eql(-1); - changes2[0].oldIndex.should.eql(0); - - unsub(); - }); - - it('returns the correct metadata when modifying documents', async function () { - if (Platform.other) { - return; - } - const colRef = firebase.firestore().collection(`${COLLECTION}/docChanges/docMovedCollection`); - - const doc1 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc1`); - const doc2 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc2`); - const doc3 = firebase.firestore().doc(`${COLLECTION}/docChanges/docMovedCollection/doc3`); - - await Promise.all([doc1.set({ value: 1 }), doc2.set({ value: 2 }), doc3.set({ value: 3 })]); - - // Subscribe to changes - const callback = sinon.spy(); - const unsub = colRef.orderBy('value').onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); - - // Validate docChange item exists - callback.should.be.calledOnce(); - const changes1 = callback.args[0][0].docChanges(); - changes1.length.should.eql(3); - - changes1.forEach((dc, i) => { - dc.oldIndex.should.eql(-1); - dc.newIndex.should.eql(i); - dc.doc.data().value.should.eql(i + 1); - }); - - // Update a document - await doc1.update({ value: 4 }); - - await Utils.sleep(800); - - const changes2 = callback.args[1][0].docChanges(); - changes2.length.should.eql(1); - - const dc = changes2[0]; - dc.type.should.eql('modified'); - dc.oldIndex.should.eql(0); - dc.newIndex.should.eql(2); - - unsub(); - }); - }); - describe('modular', function () { it('.doc -> returns a DocumentSnapshot', async function () { if (Platform.other) { diff --git a/packages/firestore/e2e/DocumentReference/collection.e2e.js b/packages/firestore/e2e/DocumentReference/collection.e2e.js index a2d38bb87a..581352248c 100644 --- a/packages/firestore/e2e/DocumentReference/collection.e2e.js +++ b/packages/firestore/e2e/DocumentReference/collection.e2e.js @@ -18,61 +18,6 @@ const COLLECTION = 'firestore'; describe('firestore.doc().collection()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if path is not a string', function () { - try { - firebase.firestore().doc('bar/baz').collection(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'collectionPath' must be a string value"); - return Promise.resolve(); - } - }); - - it('throws if path empty', function () { - try { - firebase.firestore().doc('bar/baz').collection(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'collectionPath' must be a non-empty string"); - return Promise.resolve(); - } - }); - - it('throws if path does not point to a collection', function () { - try { - firebase.firestore().doc('bar/baz').collection(`${COLLECTION}/bar`); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'collectionPath' must point to a collection"); - return Promise.resolve(); - } - }); - - it('returns a collection instance', function () { - const instance1 = firebase.firestore().doc(`${COLLECTION}/bar`).collection(COLLECTION); - const instance2 = firebase - .firestore() - .collection(COLLECTION) - .doc('bar') - .collection(COLLECTION); - should.equal(instance1.constructor.name, 'CollectionReference'); - should.equal(instance2.constructor.name, 'CollectionReference'); - instance1.id.should.equal(COLLECTION); - instance2.id.should.equal(COLLECTION); - }); - }); - describe('modular', function () { it('throws if path is not a string', function () { const { getFirestore, doc, collection } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentReference/delete.e2e.js b/packages/firestore/e2e/DocumentReference/delete.e2e.js index 9e0d28217b..a77b3ea76c 100644 --- a/packages/firestore/e2e/DocumentReference/delete.e2e.js +++ b/packages/firestore/e2e/DocumentReference/delete.e2e.js @@ -22,30 +22,6 @@ describe('firestore.doc().delete()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('deletes a document', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/deleteme`); - await ref.set({ foo: 'bar' }); - const snapshot1 = await ref.get(); - snapshot1.id.should.equal('deleteme'); - snapshot1.exists().should.equal(true); - await ref.delete(); - const snapshot2 = await ref.get(); - snapshot2.id.should.equal('deleteme'); - snapshot2.exists().should.equal(false); - }); - }); - describe('modular', function () { it('deletes a document', async function () { const { getFirestore, doc, setDoc, getDoc, deleteDoc } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentReference/get.e2e.js b/packages/firestore/e2e/DocumentReference/get.e2e.js index b814f0a606..fd17bb5e80 100644 --- a/packages/firestore/e2e/DocumentReference/get.e2e.js +++ b/packages/firestore/e2e/DocumentReference/get.e2e.js @@ -22,72 +22,6 @@ describe('firestore.doc().get()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if get options are not an object', function () { - try { - firebase.firestore().doc('bar/baz').get('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object is provided"); - return Promise.resolve(); - } - }); - - it('throws if get options are invalid', function () { - try { - firebase.firestore().doc('bar/baz').get({ source: 'nope' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' GetOptions.source must be one of 'default', 'server' or 'cache'", - ); - return Promise.resolve(); - } - }); - - it('gets data from default source', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/get`); - const data = { foo: 'bar', bar: 123 }; - await ref.set(data); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify(data)); - await ref.delete(); - }); - - it('gets data from the server', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/get`); - const data = { foo: 'bar', bar: 123 }; - await ref.set(data); - const snapshot = await ref.get({ source: 'server' }); - snapshot.data().should.eql(jet.contextify(data)); - snapshot.metadata.fromCache.should.equal(false); - await ref.delete(); - }); - - it('gets data from cache', async function () { - if (Platform.other) { - return; - } - const ref = firebase.firestore().doc(`${COLLECTION}/get`); - const data = { foo: 'bar', bar: 123 }; - await ref.set(data); - const snapshot = await ref.get({ source: 'cache' }); - snapshot.data().should.eql(jet.contextify(data)); - snapshot.metadata.fromCache.should.equal(true); - await ref.delete(); - }); - }); - describe('modular', function () { it('gets data from default source', async function () { const { getFirestore, doc, setDoc, getDoc, deleteDoc } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentReference/isEqual.e2e.js b/packages/firestore/e2e/DocumentReference/isEqual.e2e.js index a7fc4691b7..576e38fa98 100644 --- a/packages/firestore/e2e/DocumentReference/isEqual.e2e.js +++ b/packages/firestore/e2e/DocumentReference/isEqual.e2e.js @@ -17,50 +17,6 @@ const COLLECTION = 'firestore'; describe('firestore.doc().isEqual()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if other is not a DocumentReference', function () { - try { - firebase.firestore().doc('bar/baz').isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a DocumentReference instance"); - return Promise.resolve(); - } - }); - - it('returns false when not equal', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); - - const eql1 = docRef.isEqual(firebase.firestore().doc(`${COLLECTION}/foo`)); - const eql2 = docRef.isEqual( - firebase.firestore(firebase.app('secondaryFromNative')).doc(`${COLLECTION}/baz`), - ); - - eql1.should.be.False(); - eql2.should.be.False(); - }); - - it('returns true when equal', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); - - const eql1 = docRef.isEqual(docRef); - const eql2 = docRef.isEqual(firebase.firestore().doc(`${COLLECTION}/baz`)); - - eql1.should.be.True(); - eql2.should.be.True(); - }); - }); - describe('modular', function () { it('throws if other is not a DocumentReference', function () { const { getFirestore, doc, refEqual } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js b/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js index d78807590b..4e27da4e7f 100644 --- a/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js +++ b/packages/firestore/e2e/DocumentReference/onSnapshot.e2e.js @@ -16,439 +16,13 @@ */ const COLLECTION = 'firestore'; const NO_RULE_COLLECTION = 'no_rules'; -const { wipe, setDocumentOutOfBand } = require('../helpers'); +const { wipe } = require('../helpers'); describe('firestore().doc().onSnapshot()', function () { before(function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no arguments are provided', function () { - try { - firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected at least one argument'); - return Promise.resolve(); - } - }); - - it('returns an unsubscribe function', function () { - const unsub = firebase - .firestore() - .doc(`${COLLECTION}/foo`) - .onSnapshot(() => {}); - - unsub.should.be.a.Function(); - unsub(); - }); - - it('accepts a single callback function with snapshot', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(callback); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('DocumentSnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); - - it('accepts a single callback function with Error', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot(callback); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); - }); - - describe('multiple callbacks', function () { - if (Platform.other) { - return; - } - - it('calls onNext when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot(onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('DocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls onError with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase - .firestore() - .doc(`${NO_RULE_COLLECTION}/nope`) - .onSnapshot(onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - describe('objects of callbacks', function () { - if (Platform.other) { - return; - } - - it('calls next when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot({ - next: onNext, - error: onError, - }); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('DocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - next: onNext, - error: onError, - }); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - describe('SnapshotListenerOptions + callbacks', function () { - if (Platform.other) { - return; - } - - it('calls callback with snapshot when successful', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('DocumentSnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); - - it('calls callback with Error', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); - }); - - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('DocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - describe('SnapshotListenerOptions + object of callbacks', function () { - if (Platform.other) { - return; - } - - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/foo`).onSnapshot( - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('DocumentSnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot( - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - it('throws if SnapshotListenerOptions is invalid', function () { - try { - firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - includeMetadataChanges: 123, - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", - ); - return Promise.resolve(); - } - }); - - it("throws if SnapshotListenerOptions.source is invalid ('server')", function () { - try { - firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - source: 'server', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.source must be one of 'default' or 'cache'", - ); - return Promise.resolve(); - } - }); - - it('accepts source-only SnapshotListenerOptions', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/source-only`).onSnapshot( - { - source: 'cache', - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - unsub(); - }); - - it('accepts source + includeMetadataChanges SnapshotListenerOptions', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firebase.firestore().doc(`${COLLECTION}/source-with-metadata`).onSnapshot( - { - source: 'default', - includeMetadataChanges: true, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - unsub(); - }); - - it('cache source listeners ignore out-of-band server writes', async function () { - if (Platform.other) { - return; - } - - const docPath = `${COLLECTION}/${Utils.randString(12, '#aA')}`; - const docRef = firebase.firestore().doc(docPath); - await docRef.set({ value: 1 }); - await docRef.get(); - - const callback = sinon.spy(); - const unsub = docRef.onSnapshot({ source: 'cache' }, callback); - try { - await Utils.spyToBeCalledOnceAsync(callback); - - await setDocumentOutOfBand(docPath, { value: 2 }); - await Utils.sleep(1500); - callback.should.be.callCount(1); - - await docRef.set({ value: 3 }); - await Utils.spyToBeCalledTimesAsync(callback, 2); - callback.args[1][0].get('value').should.equal(3); - } finally { - unsub(); - } - }); - - it('default source listeners receive out-of-band server writes', async function () { - if (Platform.other) { - return; - } - - const docPath = `${COLLECTION}/${Utils.randString(12, '#aA')}`; - const docRef = firebase.firestore().doc(docPath); - await docRef.set({ value: 1 }); - await docRef.get(); - - const callback = sinon.spy(); - const unsub = docRef.onSnapshot( - { source: 'default', includeMetadataChanges: true }, - callback, - ); - try { - await Utils.spyToBeCalledOnceAsync(callback); - - await setDocumentOutOfBand(docPath, { value: 2 }); - await Utils.spyToBeCalledTimesAsync(callback, 2, 8000); - - const latestSnapshot = callback.args[callback.callCount - 1][0]; - latestSnapshot.get('value').should.equal(2); - } finally { - unsub(); - } - }); - - it('throws if next callback is invalid', function () { - try { - firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - next: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.next' or 'onNext' expected a function"); - return Promise.resolve(); - } - }); - - it('throws if error callback is invalid', function () { - try { - firebase.firestore().doc(`${NO_RULE_COLLECTION}/nope`).onSnapshot({ - error: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.error' or 'onError' expected a function"); - return Promise.resolve(); - } - }); - - it('unsubscribes from further updates', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const doc = firebase.firestore().doc(`${COLLECTION}/unsub`); - - const unsub = doc.onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); - await doc.set({ foo: 'bar' }); - unsub(); - await Utils.sleep(800); - await doc.set({ foo: 'bar2' }); - await Utils.spyToBeCalledTimesAsync(callback, 2); - callback.should.be.callCount(2); - }); - }); - describe('modular', function () { it('throws if no arguments are provided', function () { const { getFirestore, doc, onSnapshot } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentReference/properties.e2e.js b/packages/firestore/e2e/DocumentReference/properties.e2e.js index 5763c3bb27..f529634c19 100644 --- a/packages/firestore/e2e/DocumentReference/properties.e2e.js +++ b/packages/firestore/e2e/DocumentReference/properties.e2e.js @@ -18,40 +18,6 @@ const COLLECTION = 'firestore'; describe('firestore.doc()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns a Firestore instance', function () { - const instance = firebase.firestore().doc(`${COLLECTION}/bar`); - should.equal(instance.firestore.constructor.name, 'FirebaseFirestoreModule'); - }); - - it('returns the document id', function () { - const instance = firebase.firestore().doc(`${COLLECTION}/bar`); - instance.id.should.equal('bar'); - }); - - it('returns the parent collection reference', function () { - const instance = firebase.firestore().doc(`${COLLECTION}/bar`); - instance.parent.id.should.equal(COLLECTION); - }); - - it('returns the path', function () { - const instance1 = firebase.firestore().doc(`${COLLECTION}/bar`); - const instance2 = firebase.firestore().collection(COLLECTION).doc('bar'); - instance1.path.should.equal(`${COLLECTION}/bar`); - instance2.path.should.equal(`${COLLECTION}/bar`); - }); - }); - describe('modular', function () { it('returns a Firestore instance', function () { const { getFirestore, doc } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentReference/set.e2e.js b/packages/firestore/e2e/DocumentReference/set.e2e.js index 9e95187e68..bacad6b959 100644 --- a/packages/firestore/e2e/DocumentReference/set.e2e.js +++ b/packages/firestore/e2e/DocumentReference/set.e2e.js @@ -22,261 +22,6 @@ describe('firestore.doc().set()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if data is not an object', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object"); - return Promise.resolve(); - } - }); - - it('throws if options is not an object', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set({}, 'foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object"); - return Promise.resolve(); - } - }); - - it('throws if options contains both merge types', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set( - {}, - { - merge: true, - mergeFields: [], - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); - return Promise.resolve(); - } - }); - - it('throws if merge is not a boolean', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set( - {}, - { - merge: 'foo', - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.merge' must be a boolean value"); - return Promise.resolve(); - } - }); - - it('throws if mergeFields is not an array', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).set( - {}, - { - mergeFields: 'foo', - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.mergeFields' must be an array"); - return Promise.resolve(); - } - }); - - it('throws if mergeFields contains invalid data', function () { - try { - firebase - .firestore() - .doc(`${COLLECTION}/baz`) - .set( - {}, - { - mergeFields: [ - 'foo', - 'foo.bar', - new firebase.firestore.FieldPath(COLLECTION, 'baz'), - 123, - ], - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options.mergeFields' all fields must be of type string or FieldPath, but the value at index 3 was number", - ); - return Promise.resolve(); - } - }); - - it('sets new data', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/set`); - const data1 = { foo: 'bar' }; - const data2 = { foo: 'baz', bar: 123 }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.set(data2); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(data2)); - await ref.delete(); - }); - - it('merges all fields', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/merge`); - const data1 = { foo: 'bar' }; - const data2 = { bar: 'baz' }; - const merged = { ...data1, ...data2 }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.set(data2, { - merge: true, - }); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(merged)); - await ref.delete(); - }); - - it('merges specific fields', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/merge`); - const data1 = { foo: '123', bar: 123, baz: '456' }; - const data2 = { foo: '234', bar: 234, baz: '678' }; - const merged = { foo: data1.foo, bar: data2.bar, baz: data2.baz }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.set(data2, { - mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], - }); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(merged)); - await ref.delete(); - }); - - it('throws when nested undefined array value provided and ignored undefined is false', async function () { - // TODO(ehesp): Figure out how to call settings multiple times on modular SDK. - if (Platform.other) { - return; - } - - await firebase.firestore().settings({ ignoreUndefinedProperties: false }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - try { - await docRef.set({ - myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], - }); - return Promise.reject(new Error('Expected set() to throw')); - } catch (error) { - error.message.should.containEql('Unsupported field value: undefined'); - } - }); - - it('accepts undefined nested array values if ignoreUndefined is true', async function () { - // TODO(ehesp): Figure out how to call settings multiple times on modular SDK. - if (Platform.other) { - return; - } - - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - myArray: [{ name: 'Tim', location: { state: undefined, country: 'United Kingdom' } }], - }); - }); - - it('does not throw when nested undefined object value provided and ignore undefined is true', async function () { - // TODO(ehesp): Figure out how to call settings multiple times on modular SDK. - if (Platform.other) { - return; - } - - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - const docRef = firebase.firestore().doc(`${COLLECTION}/bar`); - await docRef.set({ - field1: 1, - field2: { - shouldNotWork: undefined, - }, - }); - }); - - it('filters out undefined properties when setting enabled', async function () { - // TODO(ehesp): Figure out how to call settings multiple times on modular SDK. - if (Platform.other) { - return; - } - - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - - const docRef = firebase.firestore().doc(`${COLLECTION}/ignoreUndefinedTrueProp`); - await docRef.set({ - field1: 1, - field2: undefined, - }); - - const snap = await docRef.get(); - const snapData = snap.data(); - if (!snapData) { - return Promise.reject(new Error('Snapshot not saved')); - } - - snapData.field1.should.eql(1); - snapData.hasOwnProperty('field2').should.eql(false); - }); - - it('filters out nested undefined properties when setting enabled', async function () { - // TODO(ehesp): Figure out how to call settings multiple times on modular SDK. - if (Platform.other) { - return; - } - - await firebase.firestore().settings({ ignoreUndefinedProperties: true }); - - const docRef = firebase.firestore().doc(`${COLLECTION}/ignoreUndefinedTrueNestedProp`); - await docRef.set({ - field1: 1, - field2: { - shouldBeMissing: undefined, - }, - field3: [ - { - shouldBeHere: 'Here', - shouldBeMissing: undefined, - }, - ], - }); - - const snap = await docRef.get(); - const snapData = snap.data(); - if (!snapData) { - return Promise.reject(new Error('Snapshot not saved')); - } - - snapData.field1.should.eql(1); - snapData.hasOwnProperty('field2').should.eql(true); - snapData.field2.hasOwnProperty('shouldBeMissing').should.eql(false); - snapData.hasOwnProperty('field3').should.eql(true); - snapData.field3[0].shouldBeHere.should.eql('Here'); - snapData.field3[0].hasOwnProperty('shouldBeMissing').should.eql(false); - }); - }); - describe('modular', function () { it('throws if data is not an object', function () { const { getFirestore, doc, setDoc } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentReference/update.e2e.js b/packages/firestore/e2e/DocumentReference/update.e2e.js index 8591d22916..e5686a15aa 100644 --- a/packages/firestore/e2e/DocumentReference/update.e2e.js +++ b/packages/firestore/e2e/DocumentReference/update.e2e.js @@ -22,83 +22,6 @@ describe('firestore.doc().update()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no arguments are provided', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).update(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'expected at least 1 argument but was called with 0 arguments', - ); - return Promise.resolve(); - } - }); - - it('throws if document does not exist', async function () { - try { - await firebase.firestore().doc(`${COLLECTION}/idonotexistonthedatabase`).update({}); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.code.should.containEql('firestore/not-found'); - return Promise.resolve(); - } - }); - - it('throws if field/value sequence is invalid', function () { - try { - firebase.firestore().doc(`${COLLECTION}/baz`).update('foo', 'bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('or equal numbers of key/value pairs'); - return Promise.resolve(); - } - }); - - it('updates data with an object value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/update-obj`); - const value = Date.now(); - const data1 = { foo: value }; - const data2 = { foo: 'bar' }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - await ref.update(data2); - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(data2)); - await ref.delete(); - }); - - it('updates data with an key/value pairs', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/update-obj`); - const value = Date.now(); - const data1 = { foo: value, bar: value }; - await ref.set(data1); - const snapshot1 = await ref.get(); - snapshot1.data().should.eql(jet.contextify(data1)); - - await ref.update('foo', 'bar', 'bar', 'baz', 'foo1', 'bar1'); - const expected = { - foo: 'bar', - bar: 'baz', - foo1: 'bar1', - }; - const snapshot2 = await ref.get(); - snapshot2.data().should.eql(jet.contextify(expected)); - await ref.delete(); - }); - }); - describe('modular', function () { it('throws if no arguments are provided', function () { const { getFirestore, doc, updateDoc } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentSnapshot/data.e2e.js b/packages/firestore/e2e/DocumentSnapshot/data.e2e.js index e1fbb9591f..876d387cf9 100644 --- a/packages/firestore/e2e/DocumentSnapshot/data.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/data.e2e.js @@ -25,131 +25,6 @@ describe('firestore().doc() -> snapshot.data()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns undefined if document does not exist', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - const snapshot = await ref.get(); - should.equal(snapshot.data(), undefined); - }); - - it('returns an object if exists', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/getData`); - const data = { foo: 'bar' }; - await ref.set(data); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify(data)); - await ref.delete(); - }); - - it('returns an object when document is empty', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/getData`); - const data = {}; - await ref.set(data); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify(data)); - await ref.delete(); - }); - - // it('handles SnapshotOptions', function () { - // // TODO - // }); - - it('handles all data types', async function () { - const types = { - string: '123456', - stringEmpty: '', - number: 123456, - infinity: Infinity, - minusInfinity: -Infinity, - nan: 1 + undefined, - boolTrue: true, - boolFalse: false, - map: {}, // set after - array: [], // set after, - nullValue: null, - timestamp: new firebase.firestore.Timestamp(123, 123456), - date: new Date(), - geopoint: new firebase.firestore.GeoPoint(1, 2), - reference: firebase.firestore().doc(`${COLLECTION}/foobar`), - blob: firebase.firestore.Blob.fromBase64String(blobBase64), - }; - - const map = { foo: 'bar' }; - const array = [123, '456', null]; - types.map = map; - types.array = array; - - const ref = firebase.firestore().doc(`${COLLECTION}/types`); - await ref.set(types); - const snapshot = await ref.get(); - const data = snapshot.data(); - - // String - data.string.should.be.a.String(); - data.string.should.equal(types.string); - data.stringEmpty.should.be.a.String(); - data.stringEmpty.should.equal(types.stringEmpty); - - // Number - data.number.should.be.a.Number(); - data.number.should.equal(types.number); - data.infinity.should.be.Infinity(); - should.equal(data.infinity, Number.POSITIVE_INFINITY); - data.minusInfinity.should.be.Infinity(); - should.equal(data.minusInfinity, Number.NEGATIVE_INFINITY); - data.nan.should.be.eql(NaN); - - // Boolean - data.boolTrue.should.be.a.Boolean(); - data.boolTrue.should.be.true(); - data.boolFalse.should.be.a.Boolean(); - data.boolFalse.should.be.false(); - - // Map - data.map.should.be.an.Object(); - data.map.should.eql(jet.contextify(map)); - - // Array - data.array.should.be.an.Array(); - data.array.should.eql(jet.contextify(array)); - - // Null - should.equal(data.nullValue, null); - - // Timestamp - data.timestamp.should.be.an.instanceOf(firebase.firestore.Timestamp); - data.timestamp.seconds.should.be.a.Number(); - data.timestamp.nanoseconds.should.be.a.Number(); - data.date.should.be.an.instanceOf(firebase.firestore.Timestamp); - data.date.seconds.should.be.a.Number(); - data.date.nanoseconds.should.be.a.Number(); - - // GeoPoint - data.geopoint.should.be.an.instanceOf(firebase.firestore.GeoPoint); - data.geopoint.latitude.should.be.a.Number(); - data.geopoint.longitude.should.be.a.Number(); - - // Reference - // data.reference.should.be.an.instanceOf(); - data.reference.path.should.equal(`${COLLECTION}/foobar`); - - // Blob - data.blob.toBase64.should.be.a.Function(); - - await ref.delete(); - }); - }); - describe('modular', function () { it('returns undefined if documet does not exist', async function () { const { getFirestore, doc, getDoc } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentSnapshot/get.e2e.js b/packages/firestore/e2e/DocumentSnapshot/get.e2e.js index 899f889cdb..3fc9b64a3c 100644 --- a/packages/firestore/e2e/DocumentSnapshot/get.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/get.e2e.js @@ -22,160 +22,6 @@ describe('firestore().doc() -> snapshot.get()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if invalid fieldPath argument', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' expected type string or FieldPath"); - return Promise.resolve(); - } - }); - - it('throws if fieldPath is an empty string', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get(''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if fieldPath starts with a period (.)', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get('.foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if fieldPath ends with a period (.)', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get('foo.'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if fieldPath contains ..', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - try { - snapshot.get('foo..bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('returns undefined if the data does not exist', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const snapshot = await ref.get(); - - const val1 = snapshot.get('foo'); - const val2 = snapshot.get('foo.bar'); - const val3 = snapshot.get(new firebase.firestore.FieldPath('.')); - const val4 = snapshot.get(new firebase.firestore.FieldPath('foo')); - const val5 = snapshot.get(new firebase.firestore.FieldPath('foo.bar')); - - should.equal(val1, undefined); - should.equal(val2, undefined); - should.equal(val3, undefined); - should.equal(val4, undefined); - should.equal(val5, undefined); - }); - - it('returns the correct data with string fieldPath', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const types = { - string: '12345', - number: 1234, - map: { - string: '12345', - number: 1234, - }, - array: [1, '2', null], - timestamp: new Date(), - }; - - await ref.set(types); - const snapshot = await ref.get(); - - const string1 = snapshot.get('string'); - const string2 = snapshot.get('map.string'); - const number1 = snapshot.get('number'); - const number2 = snapshot.get('map.number'); - const array = snapshot.get('array'); - - should.equal(string1, types.string); - should.equal(string2, types.string); - should.equal(number1, types.number); - should.equal(number2, types.number); - array.should.eql(jet.contextify(types.array)); - await ref.delete(); - }); - - it('returns the correct data with FieldPath', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/foo`); - const types = { - string: '12345', - number: 1234, - map: { - string: '12345', - number: 1234, - }, - array: [1, '2', null], - timestamp: new Date(), - }; - - await ref.set(types); - const snapshot = await ref.get(); - - const string1 = snapshot.get(new firebase.firestore.FieldPath('string')); - const string2 = snapshot.get(new firebase.firestore.FieldPath('map', 'string')); - const number1 = snapshot.get(new firebase.firestore.FieldPath('number')); - const number2 = snapshot.get(new firebase.firestore.FieldPath('map', 'number')); - const array = snapshot.get(new firebase.firestore.FieldPath('array')); - - should.equal(string1, types.string); - should.equal(string2, types.string); - should.equal(number1, types.number); - should.equal(number2, types.number); - array.should.eql(jet.contextify(types.array)); - await ref.delete(); - }); - }); - describe('modular', function () { it('throws if invalid fieldPath argument', async function () { const { getFirestore, doc, getDoc } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js b/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js index 008f37579f..e31f76c6f9 100644 --- a/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/isEqual.e2e.js @@ -17,58 +17,6 @@ const COLLECTION = 'firestore'; describe('firestore.doc() -> snapshot.isEqual()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if other is not a DocumentSnapshot', async function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/baz`); - - const docSnapshot = await docRef.get(); - docSnapshot.isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a DocumentSnapshot instance"); - return Promise.resolve(); - } - }); - - it('returns false when not equal', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/isEqual-false-exists`); - await docRef.set({ foo: 'bar' }); - - const docSnapshot1 = await docRef.get(); - const docSnapshot2 = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - await docRef.set({ foo: 'baz' }); - const docSnapshot3 = await docRef.get(); - - const eql1 = docSnapshot1.isEqual(docSnapshot2); - const eql2 = docSnapshot1.isEqual(docSnapshot3); - - eql1.should.be.False(); - eql2.should.be.False(); - }); - - it('returns true when equal', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/isEqual-true-exists`); - await docRef.set({ foo: 'bar' }); - - const docSnapshot = await docRef.get(); - - const eql1 = docSnapshot.isEqual(docSnapshot); - - eql1.should.be.True(); - }); - }); - describe('modular', function () { it('throws if other is not a DocumentSnapshot', async function () { const { getFirestore, doc, getDoc, snapshotEqual } = firestoreModular; diff --git a/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js b/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js index 10540e1c02..a25180d7d4 100644 --- a/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js +++ b/packages/firestore/e2e/DocumentSnapshot/properties.e2e.js @@ -22,60 +22,6 @@ describe('firestore().doc() -> snapshot', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('.exists -> returns a boolean for exists', async function () { - const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - await ref1.set({ foo: ' bar' }); - const snapshot1 = await ref1.get(); - const snapshot2 = await ref2.get(); - - snapshot1.exists().should.equal(true); - snapshot2.exists().should.equal(false); - await ref1.delete(); - }); - - it('.id -> returns the correct id', async function () { - const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - await ref1.set({ foo: ' bar' }); - const snapshot1 = await ref1.get(); - const snapshot2 = await ref2.get(); - - snapshot1.id.should.equal('exists'); - snapshot2.id.should.equal('idonotexist'); - await ref1.delete(); - }); - - it('.metadata -> returns a SnapshotMetadata instance', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/exists`); - const snapshot = await ref.get(); - snapshot.metadata.constructor.name.should.eql('SnapshotMetadata'); - }); - - it('.ref -> returns the correct document ref', async function () { - const ref1 = firebase.firestore().doc(`${COLLECTION}/exists`); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - await ref1.set({ foo: ' bar' }); - const snapshot1 = await ref1.get(); - const snapshot2 = await ref2.get(); - - snapshot1.ref.path.should.equal(`${COLLECTION}/exists`); - snapshot2.ref.path.should.equal(`${COLLECTION}/idonotexist`); - await ref1.delete(); - }); - }); - describe('modular', function () { it('.exists -> returns a boolean for exists', async function () { const { getFirestore, doc, setDoc, getDoc, deleteDoc } = firestoreModular; diff --git a/packages/firestore/e2e/FieldPath.e2e.js b/packages/firestore/e2e/FieldPath.e2e.js index cd13bc7962..9bb9ad06f2 100644 --- a/packages/firestore/e2e/FieldPath.e2e.js +++ b/packages/firestore/e2e/FieldPath.e2e.js @@ -18,115 +18,6 @@ const COLLECTION = 'firestore'; describe('firestore.FieldPath', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('should throw if no segments', function () { - try { - new firebase.firestore.FieldPath(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('cannot construct FieldPath with no segments'); - return Promise.resolve(); - } - }); - - it('should throw if any segments are empty strings', function () { - try { - new firebase.firestore.FieldPath('foo', ''); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('invalid segment at index'); - return Promise.resolve(); - } - }); - - it('should throw if any segments are not strings', function () { - try { - new firebase.firestore.FieldPath('foo', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('invalid segment at index'); - return Promise.resolve(); - } - }); - - it('should throw if string fieldPath is invalid', function () { - try { - // Dummy create - firebase.firestore().collection(COLLECTION).where('.foo', '<', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Invalid field path'); - return Promise.resolve(); - } - }); - - it('should throw if string fieldPath contains invalid characters', function () { - try { - // Dummy create - firebase.firestore().collection(COLLECTION).where('foo/bar', '<', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Paths must not contain'); - return Promise.resolve(); - } - }); - - it('should provide access to segments as array', function () { - const expect = ['foo', 'bar', 'baz']; - const path = new firebase.firestore.FieldPath('foo', 'bar', 'baz'); - path._segments.should.eql(jet.contextify(expect)); - }); - - it('should provide access to string dot notated path', function () { - const expect = 'foo.bar.baz'; - const path = new firebase.firestore.FieldPath('foo', 'bar', 'baz'); - path._toPath().should.equal(expect); - }); - - it('should return document ID path', function () { - const expect = '__name__'; - const { documentId } = firestoreModular; - const path = documentId(); - path._segments.length.should.equal(1); - path._toPath().should.equal(expect); - }); - - describe('isEqual()', function () { - it('throws if other isnt a FieldPath', function () { - try { - const path = new firebase.firestore.FieldPath('foo'); - path.isEqual({}); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected instance of FieldPath"); - return Promise.resolve(); - } - }); - - it('should return true if isEqual', function () { - const path1 = new firebase.firestore.FieldPath('foo', 'bar'); - const path2 = new firebase.firestore.FieldPath('foo', 'bar'); - path1.isEqual(path2).should.equal(true); - }); - - it('should return false if not isEqual', function () { - const path1 = new firebase.firestore.FieldPath('foo', 'bar'); - const path2 = new firebase.firestore.FieldPath('foo', 'baz'); - path1.isEqual(path2).should.equal(false); - }); - }); - }); - describe('modular', function () { it('should throw if no segments', function () { const { FieldPath } = firestoreModular; diff --git a/packages/firestore/e2e/FieldValue.e2e.js b/packages/firestore/e2e/FieldValue.e2e.js index 1a95f78a55..db2d4a6f7a 100644 --- a/packages/firestore/e2e/FieldValue.e2e.js +++ b/packages/firestore/e2e/FieldValue.e2e.js @@ -22,261 +22,6 @@ describe('firestore.FieldValue', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('should throw if constructed manually', function () { - try { - new firebase.firestore.FieldValue(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('constructor is private'); - return Promise.resolve(); - } - }); - - describe('isEqual()', function () { - it('throws if other is not a FieldValue', function () { - try { - firebase.firestore.FieldValue.increment(1).isEqual(1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a FieldValue instance"); - return Promise.resolve(); - } - }); - - it('returns false if not equal', function () { - const fv = firebase.firestore.FieldValue.increment(1); - - const fieldValue1 = firebase.firestore.FieldValue.increment(2); - const fieldValue2 = firebase.firestore.FieldValue.arrayRemove('123'); - - const eql1 = fv.isEqual(fieldValue1); - const eql2 = fv.isEqual(fieldValue2); - - eql1.should.be.False(); - eql2.should.be.False(); - }); - - it('returns true if equal', function () { - const fv = firebase.firestore.FieldValue.arrayUnion(1, '123', 3); - - const fieldValue1 = firebase.firestore.FieldValue.arrayUnion(1, '123', 3); - - const eql1 = fv.isEqual(fieldValue1); - - eql1.should.be.True(); - }); - }); - - describe('increment()', function () { - it('throws if value is not a number', function () { - try { - firebase.firestore.FieldValue.increment('1'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'n' expected a number value"); - return Promise.resolve(); - } - }); - - it('increments a number if it exists', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/increment`); - await ref.set({ foo: 2 }); - await ref.update({ foo: firebase.firestore.FieldValue.increment(1) }); - const snapshot = await ref.get(); - snapshot.data().foo.should.equal(3); - await ref.delete(); - }); - - it('sets the value if it doesnt exist or being set', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/increment`); - await ref.set({ foo: firebase.firestore.FieldValue.increment(1) }); - const snapshot = await ref.get(); - snapshot.data().foo.should.equal(1); - await ref.delete(); - }); - }); - - describe('serverTime()', function () { - it('sets a new server time value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/servertime`); - await ref.set({ foo: firebase.firestore.FieldValue.serverTimestamp() }); - const snapshot = await ref.get(); - snapshot.data().foo.seconds.should.be.a.Number(); - await ref.delete(); - }); - - it('updates a server time value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/servertime`); - await ref.set({ foo: firebase.firestore.FieldValue.serverTimestamp() }); - const snapshot1 = await ref.get(); - snapshot1.data().foo.nanoseconds.should.be.a.Number(); - const current = snapshot1.data().foo.nanoseconds; - await Utils.sleep(100); - await ref.update({ foo: firebase.firestore.FieldValue.serverTimestamp() }); - const snapshot2 = await ref.get(); - snapshot2.data().foo.nanoseconds.should.be.a.Number(); - should.equal(current === snapshot2.data().foo.nanoseconds, false); - await ref.delete(); - }); - }); - - describe('delete()', function () { - it('removes a value', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/valuedelete`); - await ref.set({ foo: 'bar', bar: 'baz' }); - await ref.update({ bar: firebase.firestore.FieldValue.delete() }); - const snapshot = await ref.get(); - snapshot.data().should.eql(jet.contextify({ foo: 'bar' })); - await ref.delete(); - }); - }); - - describe('arrayUnion()', function () { - it('throws if attempting to use own class', function () { - try { - firebase.firestore.FieldValue.arrayUnion(firebase.firestore.FieldValue.serverTimestamp()); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'FieldValue instance cannot be used with other FieldValue methods', - ); - return Promise.resolve(); - } - }); - - it('throws if using nested arrays', function () { - try { - firebase.firestore.FieldValue.arrayUnion([1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Nested arrays are not supported'); - return Promise.resolve(); - } - }); - - it('updates an existing array', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: [1, 2], - }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayUnion(3, 4), - }); - const snapshot = await ref.get(); - const expected = [1, 2, 3, 4]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); - }); - - it('sets an array if existing value isnt an array with update', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: 123, - }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayUnion(3, 4), - }); - const snapshot = await ref.get(); - const expected = [3, 4]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); - }); - - it('sets an existing array to the new array with set', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: [1, 2], - }); - await ref.set({ - foo: firebase.firestore.FieldValue.arrayUnion(3, 4), - }); - const snapshot = await ref.get(); - const expected = [3, 4]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); - }); - }); - - describe('arrayRemove()', function () { - it('throws if attempting to use own class', function () { - try { - firebase.firestore.FieldValue.arrayRemove( - firebase.firestore.FieldValue.serverTimestamp(), - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'FieldValue instance cannot be used with other FieldValue methods', - ); - return Promise.resolve(); - } - }); - - it('throws if using nested arrays', function () { - try { - firebase.firestore.FieldValue.arrayRemove([1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Nested arrays are not supported'); - return Promise.resolve(); - } - }); - - it('removes items in an array', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayremove`); - await ref.set({ - foo: [1, 2, 3, 4], - }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayRemove(3, 4), - }); - const snapshot = await ref.get(); - const expected = [1, 2]; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); - }); - - it('removes all items in the array if existing value isnt array with update', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: 123, - }); - await ref.update({ - foo: firebase.firestore.FieldValue.arrayRemove(3, 4), - }); - const snapshot = await ref.get(); - const expected = []; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); - }); - - it('removes all items in the array if existing value isnt array with set', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/arrayunion`); - await ref.set({ - foo: 123, - }); - await ref.set({ - foo: firebase.firestore.FieldValue.arrayRemove(3, 4), - }); - const snapshot = await ref.get(); - const expected = []; - snapshot.data().foo.should.eql(jet.contextify(expected)); - await ref.delete(); - }); - }); - }); - describe('modular', function () { it('should throw if constructed manually', function () { const { FieldValue } = firestoreModular; diff --git a/packages/firestore/e2e/FirestoreStatics.e2e.js b/packages/firestore/e2e/FirestoreStatics.e2e.js index 379f62797d..32341f3c45 100644 --- a/packages/firestore/e2e/FirestoreStatics.e2e.js +++ b/packages/firestore/e2e/FirestoreStatics.e2e.js @@ -16,38 +16,7 @@ */ describe('firestore.X', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('setLogLevel', function () { - it('throws if invalid level', function () { - try { - firebase.firestore.setLogLevel('verbose'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'logLevel' expected one of 'debug', 'error' or 'silent'", - ); - return Promise.resolve(); - } - }); - - it('enabled and disables logging', function () { - firebase.firestore.setLogLevel('silent'); - firebase.firestore.setLogLevel('debug'); - }); - }); - }); - - xdescribe('modular', function () { + describe('modular', function () { describe('setLogLevel', function () { it('throws if invalid level', function () { const { setLogLevel } = firestoreModular; diff --git a/packages/firestore/e2e/GeoPoint.e2e.js b/packages/firestore/e2e/GeoPoint.e2e.js index b7263aa272..a20a919ca4 100644 --- a/packages/firestore/e2e/GeoPoint.e2e.js +++ b/packages/firestore/e2e/GeoPoint.e2e.js @@ -22,128 +22,6 @@ describe('firestore.GeoPoint', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if invalid number of arguments', function () { - try { - new firebase.firestore.GeoPoint(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('constructor expected latitude and longitude values'); - return Promise.resolve(); - } - }); - - it('throws if latitude is not a number', function () { - try { - new firebase.firestore.GeoPoint('123', 0); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'latitude' must be a number value"); - return Promise.resolve(); - } - }); - - it('throws if longitude is not a number', function () { - try { - new firebase.firestore.GeoPoint(0, '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'longitude' must be a number value"); - return Promise.resolve(); - } - }); - - it('throws if latitude is not valid', function () { - try { - new firebase.firestore.GeoPoint(-100, 0); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'latitude' must be a number between -90 and 90"); - return Promise.resolve(); - } - }); - - it('throws if longitude is not valid', function () { - try { - new firebase.firestore.GeoPoint(0, 200); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'longitude' must be a number between -180 and 180"); - return Promise.resolve(); - } - }); - - it('gets the latitude value', function () { - const geo = new firebase.firestore.GeoPoint(20, 0); - geo.latitude.should.equal(20); - }); - - it('gets the longitude value', function () { - const geo = new firebase.firestore.GeoPoint(20, 15); - geo.longitude.should.equal(15); - }); - - describe('isEqual()', function () { - it('throws if other is a GeoPoint instance', function () { - try { - const geo = new firebase.firestore.GeoPoint(0, 0); - geo.isEqual(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected an instance of GeoPoint"); - return Promise.resolve(); - } - }); - - it('returns false if not the same', function () { - const geo1 = new firebase.firestore.GeoPoint(0, 0); - const geo2 = new firebase.firestore.GeoPoint(0, 1); - const equal = geo1.isEqual(geo2); - equal.should.equal(false); - }); - - it('returns true if the same', function () { - const geo1 = new firebase.firestore.GeoPoint(40, 40); - const geo2 = new firebase.firestore.GeoPoint(40, 40); - const equal = geo1.isEqual(geo2); - equal.should.equal(true); - }); - }); - - describe('toJSON()', function () { - it('returns a json representation of the GeoPoint', function () { - const geo = new firebase.firestore.GeoPoint(30, 35); - const json = geo.toJSON(); - json.type.should.eql('firestore/geoPoint/1.0'); - json.latitude.should.eql(30); - json.longitude.should.eql(35); - }); - }); - - it('sets & returns correctly', async function () { - const ref = firebase.firestore().doc(`${COLLECTION}/geopoint`); - await ref.set({ - geopoint: new firebase.firestore.GeoPoint(20, 30), - }); - const snapshot = await ref.get(); - const geo = snapshot.data().geopoint; - should.equal(geo.constructor.name, 'GeoPoint'); - geo.latitude.should.equal(20); - geo.longitude.should.equal(30); - await ref.delete(); - }); - }); - describe('modular', function () { it('throws if invalid number of arguments', function () { const { GeoPoint } = firestoreModular; diff --git a/packages/firestore/e2e/Query/endAt.e2e.js b/packages/firestore/e2e/Query/endAt.e2e.js index 3f6ef28d21..f0bd6bad34 100644 --- a/packages/firestore/e2e/Query/endAt.e2e.js +++ b/packages/firestore/e2e/Query/endAt.e2e.js @@ -22,136 +22,6 @@ describe('firestore().collection().endAt()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).endAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').endAt('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); - - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().collection(COLLECTION).doc('foo').get(); - firebase.firestore().collection(COLLECTION).endAt(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); - - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).endAt(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); - - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').endAt(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); - - it('ends at field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.orderBy('bar.value', 'desc').endAt(2).get(); - - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc3'); - qs.docs[1].id.should.eql('doc2'); - }); - - it('ends at snapshot field values', async function () { - // await Utils.sleep(3000); - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 3 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 1 } }), - ]); - - const endAt = await doc2.get(); - - const qs = await colRef.orderBy('bar.value').endAt(endAt).get(); - - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc3'); - qs.docs[1].id.should.eql('doc2'); - }); - - it('ends at snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); - - const endAt = await doc2.get(); - - const qs = await colRef.endAt(endAt).get(); - - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc1'); - qs.docs[1].id.should.eql('doc2'); - }); - }); - describe('modular', function () { it('throws if no argument provided', function () { const { getFirestore, collection, endAt, query } = firestoreModular; diff --git a/packages/firestore/e2e/Query/endBefore.e2e.js b/packages/firestore/e2e/Query/endBefore.e2e.js index d28e94c0e5..9907f0140f 100644 --- a/packages/firestore/e2e/Query/endBefore.e2e.js +++ b/packages/firestore/e2e/Query/endBefore.e2e.js @@ -22,133 +22,6 @@ describe('firestore().collection().endBefore()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).endBefore(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').endBefore('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); - - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/stuff`).get(); - - firebase.firestore().collection(COLLECTION).endBefore(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); - - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).endBefore(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); - - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').endBefore(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); - - it('ends before field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.orderBy('bar.value', 'desc').endBefore(2).get(); - - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); - }); - - it('ends before snapshot field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 3 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 1 } }), - ]); - - const endBefore = await doc2.get(); - - const qs = await colRef.orderBy('bar.value').endBefore(endBefore).get(); - - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); - }); - - it('ends before snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endBefore/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); - - const endBefore = await doc2.get(); - - const qs = await colRef.endBefore(endBefore).get(); - - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc1'); - }); - }); - describe('modular', function () { it('throws if no argument provided', function () { const { getFirestore, collection, endBefore, query } = firestoreModular; diff --git a/packages/firestore/e2e/Query/get.e2e.js b/packages/firestore/e2e/Query/get.e2e.js index 4cfae167d0..5928b01111 100644 --- a/packages/firestore/e2e/Query/get.e2e.js +++ b/packages/firestore/e2e/Query/get.e2e.js @@ -22,77 +22,6 @@ describe('firestore().collection().get()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if get options is not an object', function () { - try { - firebase.firestore().collection(COLLECTION).get(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object is provided"); - return Promise.resolve(); - } - }); - - it('throws if get options.source is not valid', function () { - try { - firebase.firestore().collection(COLLECTION).get({ - source: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' GetOptions.source must be one of 'default', 'server' or 'cache'", - ); - return Promise.resolve(); - } - }); - - it('returns a QuerySnapshot', async function () { - const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); - const colRef = docRef.collection('get'); - const snapshot = await colRef.get(); - - snapshot.constructor.name.should.eql('QuerySnapshot'); - }); - - it('returns a correct cache setting (true)', async function () { - if (Platform.other) { - return; - } - - const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); - const colRef = docRef.collection('get'); - const snapshot = await colRef.get({ - source: 'cache', - }); - - snapshot.constructor.name.should.eql('QuerySnapshot'); - snapshot.metadata.fromCache.should.be.True(); - }); - - it('returns a correct cache setting (false)', async function () { - const docRef = firebase.firestore().collection(COLLECTION).doc('nestedcollection'); - const colRef = docRef.collection('get'); - await colRef.get(); // Puts it in cache - const snapshot = await colRef.get({ - source: 'server', - }); - - snapshot.constructor.name.should.eql('QuerySnapshot'); - snapshot.metadata.fromCache.should.be.False(); - }); - }); - describe('modular', function () { it('returns a QuerySnapshot', async function () { const { getFirestore, collection, doc, getDocs } = firestoreModular; diff --git a/packages/firestore/e2e/Query/isEqual.e2e.js b/packages/firestore/e2e/Query/isEqual.e2e.js index d603571b03..e3ea8e17e6 100644 --- a/packages/firestore/e2e/Query/isEqual.e2e.js +++ b/packages/firestore/e2e/Query/isEqual.e2e.js @@ -17,127 +17,6 @@ const COLLECTION = 'firestore'; describe('firestore().collection().isEqual()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if other is not a Query', function () { - try { - firebase.firestore().collection(COLLECTION).isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a Query instance"); - return Promise.resolve(); - } - }); - - it('returns false when not equal (simple checks)', function () { - const subCol = `${COLLECTION}/isequal/simplechecks`; - const query = firebase.firestore().collection(subCol); - - const q1 = firebase.firestore(firebase.app('secondaryFromNative')).collection(subCol); - const q2 = firebase.firestore().collection(subCol).where('foo', '==', 'bar'); - const q3 = firebase.firestore().collection(subCol).orderBy('foo'); - const q4 = firebase.firestore().collection(subCol).limit(3); - - const ref1 = firebase.firestore().collection(subCol).where('bar', '==', true); - const ref2 = firebase.firestore().collection(subCol).where('bar', '==', true); - - const eql1 = query.isEqual(q1); - const eql2 = query.isEqual(q2); - const eql3 = query.isEqual(q3); - const eql4 = query.isEqual(q4); - const eql5 = ref1.isEqual(ref2); - - eql1.should.be.False(); - eql2.should.be.False(); - eql3.should.be.False(); - eql4.should.be.False(); - eql5.should.be.True(); - }); - - it('returns false when not equal (expensive checks)', function () { - const query = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('bam') - .limit(1) - .endAt(2); - - const q1 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '<', 'bar') - .orderBy('foo') - .limit(1) - .endAt(2); - - const q2 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('foob') - .limit(1) - .endAt(2); - - const q3 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(2) - .endAt(2); - - const q4 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(1) - .endAt(1); - - const eql1 = query.isEqual(q1); - const eql2 = query.isEqual(q2); - const eql3 = query.isEqual(q3); - const eql4 = query.isEqual(q4); - - eql1.should.be.False(); - eql2.should.be.False(); - eql3.should.be.False(); - eql4.should.be.False(); - }); - - it('returns true when equal', function () { - const query = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(1) - .endAt(2); - - const query2 = firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('baz') - .limit(1) - .endAt(2); - - const eql1 = query.isEqual(query2); - - eql1.should.be.True(); - }); - }); - describe('modular', function () { it('throws if other is not a Query', function () { const { getFirestore, collection, refEqual } = firestoreModular; diff --git a/packages/firestore/e2e/Query/limit.e2e.js b/packages/firestore/e2e/Query/limit.e2e.js index c09b5a3119..53ca8b3b18 100644 --- a/packages/firestore/e2e/Query/limit.e2e.js +++ b/packages/firestore/e2e/Query/limit.e2e.js @@ -22,46 +22,6 @@ describe('firestore().collection().limit()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if limit is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).limit(-1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'limit' must be a positive integer value"); - return Promise.resolve(); - } - }); - - it('sets limit on internals', async function () { - const colRef = firebase.firestore().collection(COLLECTION).limit(123); - - colRef._modifiers.options.limit.should.eql(123); - }); - - it('limits the number of documents', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - - // Add 3 - await colRef.add({}); - await colRef.add({}); - await colRef.add({}); - - const snapshot = await colRef.limit(2).get(); - snapshot.size.should.eql(2); - }); - }); - describe('modular', function () { it('throws if limit is invalid', function () { const { getFirestore, collection, limit, query } = firestoreModular; diff --git a/packages/firestore/e2e/Query/limitToLast.e2e.js b/packages/firestore/e2e/Query/limitToLast.e2e.js index fcf7007ea9..78c9032905 100644 --- a/packages/firestore/e2e/Query/limitToLast.e2e.js +++ b/packages/firestore/e2e/Query/limitToLast.e2e.js @@ -22,92 +22,6 @@ describe('firestore().collection().limitToLast()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if limitToLast is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).limitToLast(-1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'limitToLast' must be a positive integer value"); - return Promise.resolve(); - } - }); - - it('sets limitToLast on internals', async function () { - const colRef = firebase.firestore().collection(COLLECTION).limitToLast(123); - - should(colRef._modifiers.options.limitToLast).equal(123); - }); - - it('removes limit query if limitToLast is set afterwards', function () { - const colRef = firebase.firestore().collection(COLLECTION).limit(2).limitToLast(123); - - should(colRef._modifiers.options.limit).equal(undefined); - }); - - it('removes limitToLast query if limit is set afterwards', function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-limit-after`); - const colRef2 = colRef.limitToLast(123).limit(2); - - should(colRef2._modifiers.options.limitToLast).equal(undefined); - }); - - it('limitToLast the number of documents', async function () { - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - const subCol = `${COLLECTION}/${Utils.randString(12, '#aA')}/limitToLast-count`; - const colRef = firebase.firestore().collection(subCol); - - // Add 3 - await colRef.add({ count: 1 }); - await colRef.add({ count: 2 }); - await colRef.add({ count: 3 }); - - const docs = await firebase - .firestore() - .collection(subCol) - .limitToLast(2) - .orderBy('count', 'desc') - .get(); - - const results = []; - docs.forEach(doc => { - results.push(doc.data()); - }); - - should(results.length).equal(2); - - should(results[0].count).equal(2); - should(results[1].count).equal(1); - }); - - it("throws error if no 'orderBy' is set on the query", function () { - try { - firebase.firestore().collection(COLLECTION).limitToLast(3).get(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'limitToLast() queries require specifying at least one firebase.firestore().collection().orderBy() clause', - ); - return Promise.resolve(); - } - }); - }); - describe('modular', function () { it('throws if limitToLast is invalid', function () { const { getFirestore, collection, limitToLast, query } = firestoreModular; diff --git a/packages/firestore/e2e/Query/onSnapshot.e2e.js b/packages/firestore/e2e/Query/onSnapshot.e2e.js index b6e1670298..b68768799e 100644 --- a/packages/firestore/e2e/Query/onSnapshot.e2e.js +++ b/packages/firestore/e2e/Query/onSnapshot.e2e.js @@ -14,7 +14,7 @@ * limitations under the License. * */ -const { wipe, setDocumentOutOfBand } = require('../helpers'); +const { wipe } = require('../helpers'); const COLLECTION = 'firestore'; const NO_RULE_COLLECTION = 'no_rules'; @@ -23,415 +23,6 @@ describe('firestore().collection().onSnapshot()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no arguments are provided', function () { - try { - firebase.firestore().collection(COLLECTION).onSnapshot(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected at least one argument'); - return Promise.resolve(); - } - }); - - it('returns an unsubscribe function', function () { - const unsub = firebase - .firestore() - .collection(`${COLLECTION}/foo/bar1`) - .onSnapshot(() => {}); - - unsub.should.be.a.Function(); - unsub(); - }); - - it('accepts a single callback function with snapshot', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar2`).onSnapshot(callback); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); - - it('accepts a single callback function with Error', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot(callback); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); - }); - - describe('multiple callbacks', function () { - if (Platform.other) { - return; - } - - it('calls onNext when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase - .firestore() - .collection(`${COLLECTION}/foo/bar3`) - .onSnapshot(onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls onError with Error', async function () { - if (Platform.other) { - return; - } - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase - .firestore() - .collection(NO_RULE_COLLECTION) - .onSnapshot(onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - describe('objects of callbacks', function () { - if (Platform.other) { - return; - } - - it('calls next when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar4`).onSnapshot({ - next: onNext, - error: onError, - }); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - if (Platform.other) { - return; - } - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - next: onNext, - error: onError, - }); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - describe('SnapshotListenerOptions + callbacks', function () { - if (Platform.other) { - return; - } - - it('calls callback with snapshot when successful', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar5`).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); - - it('calls callback with Error', async function () { - const callback = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); - }); - - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`); - const unsub = colRef.onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - describe('SnapshotListenerOptions + object of callbacks', function () { - if (Platform.other) { - return; - } - - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(`${COLLECTION}/foo/bar7`).onSnapshot( - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); - }); - - it('throws if SnapshotListenerOptions is invalid', function () { - try { - firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - includeMetadataChanges: 123, - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", - ); - return Promise.resolve(); - } - }); - - it("throws if SnapshotListenerOptions.source is invalid ('server')", function () { - try { - firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - source: 'server', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.source must be one of 'default' or 'cache'", - ); - return Promise.resolve(); - } - }); - - it('cache source query listeners ignore out-of-band server writes', async function () { - if (Platform.other) { - return; - } - - const collectionPath = `${COLLECTION}/${Utils.randString(12, '#aA')}/cache-source`; - const colRef = firebase.firestore().collection(collectionPath); - await colRef.doc('one').set({ enabled: true }); - await colRef.get(); - - const callback = sinon.spy(); - const unsub = colRef.onSnapshot({ source: 'cache' }, callback); - try { - await Utils.spyToBeCalledOnceAsync(callback); - await setDocumentOutOfBand(`${collectionPath}/server-write`, { enabled: false }); - await Utils.sleep(1500); - callback.should.be.callCount(1); - - await colRef.doc('local-write').set({ enabled: true }); - await Utils.spyToBeCalledTimesAsync(callback, 2); - } finally { - unsub(); - } - }); - - it('default source query listeners receive out-of-band server writes', async function () { - if (Platform.other) { - return; - } - - const collectionPath = `${COLLECTION}/${Utils.randString(12, '#aA')}/cache-source-meta`; - const colRef = firebase.firestore().collection(collectionPath); - await colRef.doc('one').set({ enabled: true }); - await colRef.get(); - - const callback = sinon.spy(); - const unsub = colRef.onSnapshot( - { source: 'default', includeMetadataChanges: true }, - callback, - ); - try { - await Utils.spyToBeCalledOnceAsync(callback); - await setDocumentOutOfBand(`${collectionPath}/server-write`, { enabled: false }); - await Utils.spyToBeCalledTimesAsync(callback, 2, 8000); - } finally { - unsub(); - } - }); - - it('throws if next callback is invalid', function () { - try { - firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - next: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.next' or 'onNext' expected a function"); - return Promise.resolve(); - } - }); - - it('throws if error callback is invalid', function () { - try { - firebase.firestore().collection(NO_RULE_COLLECTION).onSnapshot({ - error: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.error' or 'onError' expected a function"); - return Promise.resolve(); - } - }); - - // FIXME test disabled due to flakiness in CI E2E tests. - // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. - it('unsubscribes from further updates', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - - const collection = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`); - - const unsub = collection.onSnapshot(callback); - await Utils.sleep(2000); - await collection.add({}); - await collection.add({}); - unsub(); - await Utils.sleep(2000); - await collection.add({}); - await Utils.sleep(2000); - callback.should.be.callCount(3); - }); - }); - describe('modular', function () { it('throws if no arguments are provided', function () { const { getFirestore, collection, onSnapshot } = firestoreModular; diff --git a/packages/firestore/e2e/Query/orderBy.e2e.js b/packages/firestore/e2e/Query/orderBy.e2e.js index 9b0e107023..d1da25d80c 100644 --- a/packages/firestore/e2e/Query/orderBy.e2e.js +++ b/packages/firestore/e2e/Query/orderBy.e2e.js @@ -22,126 +22,6 @@ describe('firestore().collection().orderBy()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if fieldPath is not valid', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' must be a string or instance of FieldPath"); - return Promise.resolve(); - } - }); - - it('throws if fieldPath string is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('.foo.bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if direction string is not valid', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo', 'up'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'directionStr' must be one of 'asc' or 'desc'"); - return Promise.resolve(); - } - }); - - it('throws if a startAt()/startAfter() has already been set', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/startATstartAfter`); - await doc.set({ foo: 'bar' }); - const snapshot = await doc.get(); - - firebase.firestore().collection(COLLECTION).startAt(snapshot).orderBy('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You must not call startAt() or startAfter()'); - return Promise.resolve(); - } - }); - - it('throws if a endAt()/endBefore() has already been set', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/endAtendBefore`); - await doc.set({ foo: 'bar' }); - const snapshot = await doc.get(); - - firebase.firestore().collection(COLLECTION).endAt(snapshot).orderBy('foo'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You must not call endAt() or endBefore()'); - return Promise.resolve(); - } - }); - - it('throws if duplicating the order field path', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo.bar').orderBy('foo.bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Order by clause cannot contain duplicate fields'); - return Promise.resolve(); - } - }); - - it('orders by a value ASC', async function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-asc`); - - await colRef.add({ value: 1 }); - await colRef.add({ value: 3 }); - await colRef.add({ value: 2 }); - - const snapshot = await colRef.orderBy('value', 'asc').get(); - const expected = [1, 2, 3]; - - snapshot.forEach((docSnap, i) => { - docSnap.data().value.should.eql(expected[i]); - }); - }); - - it('orders by a value DESC', async function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-desc`); - - await colRef.add({ value: 1 }); - await colRef.add({ value: 3 }); - await colRef.add({ value: 2 }); - - const snapshot = await colRef - .orderBy(new firebase.firestore.FieldPath('value'), 'desc') - .get(); - const expected = [3, 2, 1]; - - snapshot.forEach((docSnap, i) => { - docSnap.data().value.should.eql(expected[i]); - }); - }); - }); - describe('modular', function () { it('throws if fieldPath is not valid', function () { const { getFirestore, collection, query, orderBy } = firestoreModular; diff --git a/packages/firestore/e2e/Query/query.e2e.js b/packages/firestore/e2e/Query/query.e2e.js index 319aedd2c5..2972cd4259 100644 --- a/packages/firestore/e2e/Query/query.e2e.js +++ b/packages/firestore/e2e/Query/query.e2e.js @@ -16,141 +16,6 @@ const COLLECTION = 'firestore'; describe('FirestoreQuery/FirestoreQueryModifiers', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('should not mutate previous queries (#2691)', async function () { - const queryBefore = firebase.firestore().collection(COLLECTION).where('age', '>', 30); - const queryAfter = queryBefore.orderBy('age'); - queryBefore._modifiers._orders.length.should.equal(0); - queryBefore._modifiers._filters.length.should.equal(1); - - queryAfter._modifiers._orders.length.should.equal(1); - queryAfter._modifiers._filters.length.should.equal(1); - }); - - it('throws if where equality operator is invoked, and the where fieldPath parameter matches any orderBy parameter', async function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('foo') - .limit(1) - .endAt(2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Invalid query'); - } - - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo', '==', 'bar') - .orderBy('bar') - .orderBy('foo') - .limit(1) - .endAt(2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Invalid query'); - } - }); - - it('should allow inequality where fieldPath that does not match initial orderBy parameter', async function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-asc`); - - await colRef.add({ value: 1, foo: 'baz' }); - await colRef.add({ value: 2, foo: 'bar' }); - await colRef.add({ value: 3, foo: 'baz' }); - await colRef.add({ value: 4, foo: 'bar' }); - await colRef.add({ value: 5, foo: 'baz' }); - - const snapshot = await colRef.where('foo', '!=', 'bar').orderBy('value', 'desc').get(); - const expected = [5, 3, 1]; - - snapshot.forEach((docSnap, i) => { - docSnap.data().value.should.eql(expected[i]); - }); - }); - - it('should allow inequality where fieldPath that does not match initial orderBy parameter - multiple where filters', async function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-asc`); - - await colRef.add({ value: 1, foo: 'baz' }); - await colRef.add({ value: 2, foo: 'bar' }); - await colRef.add({ value: 3, foo: 'baz' }); - await colRef.add({ value: 4, foo: 'bar' }); - await colRef.add({ value: 5, foo: 'baz' }); - - let snapshot = await colRef - .where('foo', '!=', 'bar') - .where('value', '>', 2) - .orderBy('value', 'desc') - .get(); - let expected = [5, 3]; - - snapshot.forEach((docSnap, i) => { - docSnap.data().value.should.eql(expected[i]); - }); - - // flipped where order - snapshot = await colRef - .where('value', '>', 2) - .where('foo', '!=', 'bar') - .orderBy('value', 'desc') - .get(); - const expected2 = [5, 3]; - - snapshot.forEach((docSnap, i) => { - docSnap.data().value.should.eql(expected2[i]); - }); - }); - - it('should allow multiple inequality where fieldPath with orderBy filters', async function () { - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/order-asc`); - - await colRef.add({ value: 1, value2: 4 }); - await colRef.add({ value: 2, value2: 3 }); - await colRef.add({ value: 3, value2: 2 }); - await colRef.add({ value: 3, value2: 1 }); - - const snapshot = await colRef - .where('value', '>', 1) - .where('value2', '<', 4) - .orderBy('value', 'desc') - .orderBy('value2', 'desc') - .get(); - - const expected = [2, 1, 3]; - - snapshot.forEach((docSnap, i) => { - docSnap.data().value2.should.eql(expected[i]); - }); - }); - }); - describe('modular', function () { it('should not mutate previous queries (#2691)', async function () { const { getFirestore, collection, query, where, orderBy } = firestoreModular; diff --git a/packages/firestore/e2e/Query/startAfter.e2e.js b/packages/firestore/e2e/Query/startAfter.e2e.js index 81ac58a8a3..821b07f68a 100644 --- a/packages/firestore/e2e/Query/startAfter.e2e.js +++ b/packages/firestore/e2e/Query/startAfter.e2e.js @@ -22,149 +22,6 @@ describe('firestore().collection().startAfter()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).startAfter(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').startAfter('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); - - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/foo`).get(); - firebase.firestore().collection(COLLECTION).startAfter(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); - - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).startAfter(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); - - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').startAfter(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); - - it('starts after field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.orderBy('bar.value', 'desc').startAfter(2).get(); - - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc1'); - }); - - it('starts after snapshot field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 'a' } }), - doc2.set({ foo: 2, bar: { value: 'b' } }), - doc3.set({ foo: 3, bar: { value: 'c' } }), - ]); - - const startAfter = await doc2.get(); - - const qs = await colRef.orderBy('bar.value').startAfter(startAfter).get(); - - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); - }); - - it('startAfter snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); - - const startAfter = await doc2.get(); - - const qs = await colRef.startAfter(startAfter).get(); - - qs.docs.length.should.eql(1); - qs.docs[0].id.should.eql('doc3'); - }); - - it('runs startAfter & endBefore in the same query', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAfter/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([doc1.set({ age: 1 }), doc2.set({ age: 2 }), doc3.set({ age: 3 })]); - - const first = await doc1.get(); - const last = await doc3.get(); - - const inBetween = await colRef.orderBy('age', 'asc').startAfter(first).endBefore(last).get(); - - inBetween.docs.length.should.eql(1); - inBetween.docs[0].id.should.eql('doc2'); - }); - }); - describe('modular', function () { it('throws if no argument provided', function () { const { getFirestore, collection, query, startAfter } = firestoreModular; diff --git a/packages/firestore/e2e/Query/startAt.e2e.js b/packages/firestore/e2e/Query/startAt.e2e.js index 18854aadf4..73a6bb5848 100644 --- a/packages/firestore/e2e/Query/startAt.e2e.js +++ b/packages/firestore/e2e/Query/startAt.e2e.js @@ -22,136 +22,6 @@ describe('firestore().collection().startAt()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no argument provided', function () { - try { - firebase.firestore().collection(COLLECTION).startAt(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'Expected a DocumentSnapshot or list of field values but got undefined', - ); - return Promise.resolve(); - } - }); - - it('throws if a inconsistent order number', function () { - try { - firebase.firestore().collection(COLLECTION).orderBy('foo').startAt('bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('The number of arguments must be less than or equal'); - return Promise.resolve(); - } - }); - - it('throws if providing snapshot and field values', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/foo`).get(); - firebase.firestore().collection(COLLECTION).startAt(doc, 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected DocumentSnapshot or list of field values'); - return Promise.resolve(); - } - }); - - it('throws if provided snapshot does not exist', async function () { - try { - const doc = await firebase.firestore().doc(`${COLLECTION}/idonotexist`).get(); - firebase.firestore().collection(COLLECTION).startAt(doc); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("Can't use a DocumentSnapshot that doesn't exist"); - return Promise.resolve(); - } - }); - - it('throws if order used with snapshot but fields do not exist', async function () { - try { - const doc = firebase.firestore().doc(`${COLLECTION}/iexist`); - - await doc.set({ foo: { bar: 'baz' } }); - const snap = await doc.get(); - - firebase.firestore().collection(COLLECTION).orderBy('foo.baz').startAt(snap); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'You are trying to start or end a query using a document for which the field', - ); - return Promise.resolve(); - } - }); - - it('starts at field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAt/collection`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 1 } }), - doc2.set({ foo: 2, bar: { value: 2 } }), - doc3.set({ foo: 3, bar: { value: 3 } }), - ]); - - const qs = await colRef.orderBy('bar.value', 'desc').startAt(2).get(); - - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc2'); - qs.docs[1].id.should.eql('doc1'); - }); - - it('starts at snapshot field values', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/startAt/snapshotFields`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([ - doc1.set({ foo: 1, bar: { value: 'a' } }), - doc2.set({ foo: 2, bar: { value: 'b' } }), - doc3.set({ foo: 3, bar: { value: 'c' } }), - ]); - - const startAt = await doc2.get(); - - const qs = await colRef.orderBy('bar.value').startAt(startAt).get(); - - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc2'); - qs.docs[1].id.should.eql('doc3'); - }); - - it('startAt at snapshot', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/endsAt/snapshot`); - const doc1 = colRef.doc('doc1'); - const doc2 = colRef.doc('doc2'); - const doc3 = colRef.doc('doc3'); - - await Promise.all([doc1.set({ foo: 1 }), doc2.set({ foo: 1 }), doc3.set({ foo: 1 })]); - - const startAt = await doc2.get(); - - const qs = await colRef.startAt(startAt).get(); - - qs.docs.length.should.eql(2); - qs.docs[0].id.should.eql('doc2'); - qs.docs[1].id.should.eql('doc3'); - }); - }); - describe('modular', function () { it('throws if no argument provided', function () { const { getFirestore, collection, query, startAt } = firestoreModular; diff --git a/packages/firestore/e2e/Query/where.and.filter.e2e.js b/packages/firestore/e2e/Query/where.and.filter.e2e.js index f3fa6eddbf..6c0ec09563 100644 --- a/packages/firestore/e2e/Query/where.and.filter.e2e.js +++ b/packages/firestore/e2e/Query/where.and.filter.e2e.js @@ -16,677 +16,12 @@ */ const COLLECTION = 'firestore'; const { wipe } = require('../helpers'); -const { Filter } = firestoreModular; describe(' firestore().collection().where(AND Filters)', function () { beforeEach(async function () { return await wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if fieldPath string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('.foo.bar', '==', 1), Filter('.foo.bar', '==', 1))); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if operator string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1))); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); - - it('throws if query contains multiple array-contains', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and( - Filter('foo.bar', 'array-contains', 1), - Filter('foo.bar', 'array-contains', 1), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); - - it('throws if value is not defined', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and(Filter('foo.bar', 'array-contains'), Filter('foo.bar', 'array-contains')), - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); - - it('throws if null value and no equal operator', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', 'array-contains', null)), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); - - it('allows null to be used with equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null))); - }); - - it('allows null to be used with not equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '!=', null))); - }); - - it('allows multiple inequalities (excluding `!=`) on different paths provided', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/different-path-inequality-filter`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('allows inequality on the same path', async function () { - await firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and( - Filter('foo.bar', '>', 123), - Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), - ), - ); - }); - - it('throws if in query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123'))); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if array-contains-any query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and( - Filter('foo.bar', 'array-contains-any', '123'), - Filter('foo.bar', 'array-contains-any', '123'), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if in query array length is greater than 30', function () { - try { - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and(Filter('foo.bar', 'in', queryArray), Filter('foo.bar', 'in', queryArray)), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 10 elements in the value'); - return Promise.resolve(); - } - }); - - it('throws if query has multiple array-contains-any filter', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'array-contains-any', [1]), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); - return Promise.resolve(); - } - }); - - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where(Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2]))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where(Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2]))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where(Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2]))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'not-in', [2]), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); - - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.and( - Filter('foo.bar', '==', 1), - Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 10 elements in the value array.', - ); - return Promise.resolve(); - } - }); - - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - const { documentId } = firestoreModular; - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.and( - Filter(documentId(), '==', ['document-id']), - Filter('foo.bar', 'not-in', [1, 2, 3, 4]), - ), - ) - .orderBy('differentOrderBy', 'desc'); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); - - it("should throw error when using '!=' operator twice ", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where(Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2))); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } - }); - - it("should allow query when combining '!=' operator with any other inequality operator on a different field", async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/inequality-combine-not-equal`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '!=', 123))) - .get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - /* Queries */ - // Equals and another filter: '==', '>', '>=', '<', '<=', '!=' - it('returns with where "==" & "==" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', bar: 'baz' }; - await Promise.all([ - colRef.add({ foo: [1, '1', 'something'] }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz'))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "!=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', baz: 'baz' }; - const notExpected = { foo: 'bar', baz: 'something' }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('baz', '!=', 'something'))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & ">" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>', 2))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "<" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<', 201))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "<=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<=', 200))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & ">=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 100 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>=', 200))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - // Filters using single "array-contains", "array-contains-any", "not-in" and "in" filters - it('returns with where "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); - - const expected = [101, 102]; - const data = { foo: expected }; - - await Promise.all([colRef.add({ foo: [1, 2, 3] }), colRef.add(data), colRef.add(data)]); - - const snapshot = await colRef.where(Filter('foo', 'array-contains', 101)).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains-any`); - const expected = [101, 102, 103, 104]; - const data = { foo: expected }; - - await Promise.all([colRef.add({ foo: [1, 2, 3] }), colRef.add(data), colRef.add(data)]); - - const snapshot = await colRef.where(Filter('foo', 'array-contains-any', [120, 101])).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - const expected = 'bar'; - const data = { foo: expected }; - - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data), - colRef.add(data), - ]); - - const snapshot = await colRef.where(Filter('foo', 'not-in', ['not', 'this'])).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); - const expected1 = 'bar'; - const expected2 = 'baz'; - const data1 = { foo: expected1 }; - const data2 = { foo: expected2 }; - - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data1), - colRef.add(data2), - ]); - - const snapshot = await colRef - .where(Filter('foo', 'in', [expected1, expected2])) - .orderBy('foo') - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.eql(expected1); - snapshot.docs[1].data().foo.should.eql(expected2); - }); - - // Using AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters - it('returns with where "==" & "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.and(Filter('foo', 'array-contains', match.toString()), Filter('bar', '==', 'baz')), - ) - .get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2'], bar: 'baz' }), - colRef.add({ foo: ['2', match.toString()], bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.and( - Filter('foo', 'array-contains-any', [match.toString(), 1]), - Filter('bar', '==', 'baz'), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().bar.should.equal('baz'); - snapshot.docs[1].data().bar.should.equal('baz'); - }); - - it('returns with where "==" & "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', 'not-in', ['yolo', 'thing']), Filter('bar', '==', 'baz'))) - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.equal('bar'); - snapshot.docs[1].data().foo.should.equal('bar'); - }); - - it('returns with where "==" & "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); - - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', 'in', ['bar', 'yolo']), Filter('bar', '==', 'baz'))) - .orderBy('foo') - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.equal('bar'); - snapshot.docs[1].data().foo.should.equal('yolo'); - }); - - // Special Filter queries - it('returns when combining greater than and lesser than on the same nested field', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo.bar', '>', 1), Filter('foo.bar', '<', 3))) - .get(); - - snapshot.size.should.eql(1); - }); - - it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo.bar', '>', 1), Filter('foo.bar', '<', 3))) - .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) - .get(); - - snapshot.size.should.eql(1); - }); - - it('returns with a FieldPath', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`); - const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); - - await colRef.add({ - map: { - 'foo.bar@gmail.com': true, - }, - }); - await colRef.add({ - map: { - 'bar.foo@gmail.com': true, - }, - }); - - const snapshot = await colRef.where(Filter(fieldPath, '==', true)).get(); - snapshot.size.should.eql(1); // 2nd record should only be returned once - const data = snapshot.docs[0].data(); - should.equal(data.map['foo.bar@gmail.com'], true); - }); - }); - describe('modular', function () { it('throws if fieldPath string is invalid', function () { const { getFirestore, collection, query, and, where } = firestoreModular; diff --git a/packages/firestore/e2e/Query/where.e2e.js b/packages/firestore/e2e/Query/where.e2e.js index 52b131f22f..656d0bc8bf 100644 --- a/packages/firestore/e2e/Query/where.e2e.js +++ b/packages/firestore/e2e/Query/where.e2e.js @@ -22,561 +22,6 @@ describe('firestore().collection().where()', function () { return await wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if fieldPath is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).where(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'must be a string, instance of FieldPath or instance of Filter', - ); - return Promise.resolve(); - } - }); - - it('throws if fieldPath string is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).where('.foo.bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if operator string is invalid', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', '!'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); - - it('throws if query contains multiple array-contains', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo.bar', 'array-contains', 123) - .where('foo.bar', 'array-contains', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); - - it('throws if value is not defined', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); - - it('throws if null value and no equal operator', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains', null); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); - - it('allows null to be used with equal operator', function () { - firebase.firestore().collection(COLLECTION).where('foo.bar', '==', null); - }); - - it('allows null to be used with not equal operator', function () { - firebase.firestore().collection(COLLECTION).where('foo.bar', '!=', null); - }); - - it('allows multiple inequalities (excluding `!=`) on different paths provided', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/different-path-inequality`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef.where('foo.bar', '>', 123).where('bar', '>', 123).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('allows inequality on the same path', function () { - firebase - .firestore() - .collection(COLLECTION) - .where('foo.bar', '>', 123) - .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234); - }); - - it('throws if in query with no array value', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'in', '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if array-contains-any query with no array value', function () { - try { - firebase.firestore().collection(COLLECTION).where('foo.bar', 'array-contains-any', '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if in query array length is greater than 30', function () { - try { - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - firebase.firestore().collection(COLLECTION).where('foo.bar', 'in', queryArray); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 30 elements in the value'); - return Promise.resolve(); - } - }); - - it('throws if query has multiple array-contains-any filter', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where('foo.bar', 'array-contains-any', [1]) - .where('foo.bar', 'array-contains-any', [2]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); - return Promise.resolve(); - } - }); - - /* Queries */ - - it('returns with where equal filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equal`); - - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '==', search).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search); - }); - }); - - it('returns with where greater than filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater`); - - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search - 1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '>', search).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search + 1234); - }); - }); - - it('returns with where greater than or equal filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterequal`); - - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search - 1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '>=', search).get(); - - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.aboveOrEqual(search); - }); - }); - - it('returns with where less than filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less`); - - const search = -Date.now(); - await Promise.all([ - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search }), - ]); - - const snapshot = await colRef.where('foo', '<', search).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.be.below(search); - }); - }); - - it('returns with where less than or equal filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/lessequal`); - - const search = -Date.now(); - await Promise.all([ - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '<=', search).get(); - - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.belowOrEqual(search); - }); - }); - - it('returns when combining greater than and lesser than on the same nested field', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); - - const snapshot = await colRef - .where('foo.bar', '>', 1) - .where('foo.bar', '<', 3) - .orderBy('foo.bar') - .get(); - - snapshot.size.should.eql(1); - }); - - it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); - - const snapshot = await colRef - .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1) - .where(new firebase.firestore.FieldPath('foo', 'bar'), '<', 3) - .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) - .get(); - - snapshot.size.should.eql(1); - }); - - it('returns with where array-contains filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '2', match] }), - colRef.add({ foo: [1, '2', match.toString()] }), - colRef.add({ foo: [1, '2', match.toString()] }), - ]); - - const snapshot = await colRef.where('foo', 'array-contains', match.toString()).get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with in filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in${Date.now() + ''}`); - - await Promise.all([ - colRef.add({ status: 'Ordered' }), - colRef.add({ status: 'Ready to Ship' }), - colRef.add({ status: 'Ready to Ship' }), - colRef.add({ status: 'Incomplete' }), - ]); - - const expect = ['Ready to Ship', 'Ordered']; - const snapshot = await colRef.where('status', 'in', expect).get(); - snapshot.size.should.eql(3); - - snapshot.forEach(s => { - s.data().status.should.equalOneOf(...expect); - }); - }); - - it('returns with array-contains-any filter', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/array-contains-any${Date.now() + ''}`); - - await Promise.all([ - colRef.add({ category: ['Appliances', 'Housewares', 'Cooking'] }), - colRef.add({ category: ['Appliances', 'Electronics', 'Nursery'] }), - colRef.add({ category: ['Audio/Video', 'Electronics'] }), - colRef.add({ category: ['Beauty'] }), - ]); - - const expect = ['Appliances', 'Electronics']; - const snapshot = await colRef.where('category', 'array-contains-any', expect).get(); - snapshot.size.should.eql(3); // 2nd record should only be returned once - }); - - it('returns with a FieldPath', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`); - const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); - - await colRef.add({ - map: { - 'foo.bar@gmail.com': true, - }, - }); - await colRef.add({ - map: { - 'bar.foo@gmail.com': true, - }, - }); - - const snapshot = await colRef.where(fieldPath, '==', true).get(); - snapshot.size.should.eql(1); // 2nd record should only be returned once - const data = snapshot.docs[0].data(); - should.equal(data.map['foo.bar@gmail.com'], true); - }); - - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - const { documentId } = firestoreModular; - firebase - .firestore() - .collection(COLLECTION) - .where(documentId(), 'in', ['document-id']) - .orderBy('differentOrderBy', 'desc'); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); - - it('should correctly query integer values with in operator', async function () { - const ref = firebase.firestore().collection(`${COLLECTION}/filter/int-in${Date.now() + ''}`); - - await ref.add({ status: 1 }); - - const items = []; - await ref - .where('status', 'in', [1, 2]) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); - - items.length.should.equal(1); - }); - - it('should correctly query integer values with array-contains operator', async function () { - const ref = firebase - .firestore() - .collection(`${COLLECTION}/filter/int-array-contains${Date.now() + ''}`); - - await ref.add({ status: [1, 2, 3] }); - - const items = []; - await ref - .where('status', 'array-contains', 2) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); - - items.length.should.equal(1); - }); - - it("should correctly retrieve data when using 'not-in' operator", async function () { - const ref = firebase.firestore().collection(`${COLLECTION}/filter/not-in${Date.now() + ''}`); - - await Promise.all([ref.add({ notIn: 'here' }), ref.add({ notIn: 'now' })]); - - const result = await ref.where('notIn', 'not-in', ['here', 'there', 'everywhere']).get(); - should(result.docs.length).equal(1); - should(result.docs[0].data().notIn).equal('now'); - }); - - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where('test', 'not-in', [1]).where('test', 'not-in', [2]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where('test', '!=', 1).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where('test', 'in', [2]).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where('test', 'array-contains-any', [2]).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); - - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firebase.firestore().collection(COLLECTION); - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - try { - ref.where('test', 'not-in', queryArray); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 30 elements in the value array.', - ); - return Promise.resolve(); - } - }); - - it("should correctly retrieve data when using '!=' operator", async function () { - const ref = firebase - .firestore() - .collection(`${COLLECTION}/filter/bang-equals${Date.now() + ''}`); - - await Promise.all([ref.add({ notEqual: 'here' }), ref.add({ notEqual: 'now' })]); - - const result = await ref.where('notEqual', '!=', 'here').get(); - - should(result.docs.length).equal(1); - should(result.docs[0].data().notEqual).equal('now'); - }); - - it("should throw error when using '!=' operator twice ", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where('test', '!=', 1).where('test', '!=', 2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } - }); - - it("should allow query when combining '!=' operator with any other inequality operator on a different field", async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/inequality-combine-not-equal`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef.where('foo.bar', '>', 123).where('bar', '!=', 123).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('should handle where clause after sort by', async function () { - const ref = firebase - .firestore() - .collection(`${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); - - await ref.add({ status: 1 }); - await ref.add({ status: 2 }); - await ref.add({ status: 3 }); - - const items = []; - await ref - .orderBy('status', 'desc') - .where('status', '<=', 2) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); - - items.length.should.equal(2); - items[0].status.should.equal(2); - items[1].status.should.equal(1); - }); - }); - describe('modular', function () { it('throws if fieldPath is invalid', function () { const { getFirestore, collection, query, where } = firestoreModular; diff --git a/packages/firestore/e2e/Query/where.filter.e2e.js b/packages/firestore/e2e/Query/where.filter.e2e.js index ebe6791011..915b2dfb1a 100644 --- a/packages/firestore/e2e/Query/where.filter.e2e.js +++ b/packages/firestore/e2e/Query/where.filter.e2e.js @@ -16,412 +16,441 @@ */ const COLLECTION = 'firestore'; const { wipe } = require('../helpers'); -let Filter; +const { Filter } = firestoreModular; describe('firestore().collection().where(Filters)', function () { beforeEach(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - Filter = firebase.firestore.Filter; return await wipe(); }); - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if fieldPath string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('.foo.bar', '==', 1)); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if operator string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', '!', 1)); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); + describe('modular', function () { + it('throws if fieldPath string is invalid', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where(Filter('.foo.bar', '==', 1))); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); - it('throws if query contains multiple array-contains', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', 'array-contains', 1)) - .where(Filter('foo.bar', 'array-contains', 1)); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); + it('throws if operator string is invalid', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where(Filter('foo.bar', '!', 1))); - it('throws if value is not defined', function () { - try { - firebase.firestore().collection(COLLECTION).where(Filter('foo', '==')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); - it('throws if null value and no equal operator', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', '==', null)) - .where(Filter('foo.bar', 'array-contains', null)); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); + it('throws if query contains multiple array-contains', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where(Filter('foo.bar', 'array-contains', 1)), + where(Filter('foo.bar', 'array-contains', 1)), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); - it('allows null to be used with equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', '==', null)); - }); + it('throws if value is not defined', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where(Filter('foo', '=='))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); - it('allows null to be used with not equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', '!=', null)); - }); + it('throws if null value and no equal operator', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where(Filter('foo.bar', '==', null)), + where(Filter('foo.bar', 'array-contains', null)), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); - it('allows multiple inequalities (excluding `!=`) on different paths provided', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/different-path-inequality`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where(Filter('foo.bar', '>', 123)) - .where(Filter('bar', '>', 123)) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + it('allows null to be used with equal operator', function () { + const { getFirestore, collection, query, where } = firestoreModular; + query(collection(getFirestore(), COLLECTION), where(Filter('foo.bar', '==', null))); }); - }); - it('allows inequality on the same path', function () { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', '>', 123)) - .where(Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234)); - }); + it('allows null to be used with not equal operator', function () { + const { getFirestore, collection, query, where } = firestoreModular; + query(collection(getFirestore(), COLLECTION), where(Filter('foo.bar', '!=', null))); + }); - it('throws if in query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', 'in', '123')); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); + it('allows multiple inequalities (excluding `!=`) on different paths provided', async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/different-path-inequality`); + const expected = { foo: { bar: 300 }, bar: 200 }; + await Promise.all([ + addDoc(colRef, { foo: { bar: 1 }, bar: 1 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + + const snapshot = await query( + colRef, + where(Filter('foo.bar', '>', 123)), + where(Filter('bar', '>', 123)), + ).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); + }); - it('throws if array-contains-any query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', 'array-contains-any', '123')); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); + it('allows inequality on the same path', function () { + const { getFirestore, collection, query, where, FieldPath } = firestoreModular; + query( + collection(getFirestore(), COLLECTION), + where(Filter('foo.bar', '>', 123)), + where(Filter(new FieldPath('foo', 'bar'), '>', 1234)), + ); + }); - it('throws if in query array length is greater than 30', function () { - try { - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + it('throws if in query with no array value', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query(collection(getFirestore(), COLLECTION), where(Filter('foo.bar', 'in', '123'))); - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', 'in', queryArray)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 30 elements in the value'); - return Promise.resolve(); - } - }); + it('throws if array-contains-any query with no array value', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where(Filter('foo.bar', 'array-contains-any', '123')), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - it('throws if query has multiple array-contains-any filter', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where(Filter('foo.bar', 'array-contains-any', [1])) - .where(Filter('foo.bar', 'array-contains-any', [1])); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); - return Promise.resolve(); - } - }); + it('throws if in query array length is greater than 30', function () { + const { getFirestore, collection, query, where } = firestoreModular; + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firebase.firestore().collection(COLLECTION); + try { + query(collection(getFirestore(), COLLECTION), where(Filter('foo.bar', 'in', queryArray))); - try { - ref.where(Filter('foo.bar', 'not-in', [1])).where(Filter('foo.bar', 'not-in', [2])); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 30 elements in the value'); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it('throws if query has multiple array-contains-any filter', function () { + const { getFirestore, collection, query, where } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where(Filter('foo.bar', 'array-contains-any', [1])), + where(Filter('foo.bar', 'array-contains-any', [1])), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); - try { - ref.where(Filter('foo.bar', '!=', [1])).where(Filter('foo.bar', 'not-in', [2])); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); - return Promise.resolve(); - } - }); + it("should throw error when using 'not-in' operator twice", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query( + ref, + where(Filter('foo.bar', 'not-in', [1])), + where(Filter('foo.bar', 'not-in', [2])), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where(Filter('foo.bar', '!=', [1])), where(Filter('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); - try { - ref.where(Filter('foo.bar', 'in', [1])).where(Filter('foo.bar', 'not-in', [2])); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where(Filter('foo.bar', 'in', [1])), where(Filter('foo.bar', 'not-in', [2]))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query( + ref, + where(Filter('foo.bar', 'array-contains-any', [1])), + where(Filter('foo.bar', 'not-in', [2])), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); - try { - ref - .where(Filter('foo.bar', 'array-contains-any', [1])) - .where(Filter('foo.bar', 'not-in', [2])); + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); + try { + query( + ref, + where(Filter('foo.bar', '==', 1)), + where(Filter('foo.bar', 'not-in', queryArray)), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 30 elements in the value array.', + ); + return Promise.resolve(); + } + }); - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firebase.firestore().collection(COLLECTION); - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + const { getFirestore, collection, query, where, orderBy, documentId } = firestoreModular; + try { + query( + collection(getFirestore(), COLLECTION), + where(Filter(documentId(), '==', ['document-id'])), + where(Filter('foo.bar', 'not-in', [1, 2, 3, 4])), + orderBy('differentOrderBy', 'desc'), + ); + + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); - try { - ref.where(Filter('foo.bar', '==', 1)).where(Filter('foo.bar', 'not-in', queryArray)); + it("should throw error when using '!=' operator twice ", async function () { + const { getFirestore, collection, query, where } = firestoreModular; + const ref = collection(getFirestore(), COLLECTION); + + try { + query(ref, where(Filter('foo.bar', '!=', 1)), where(Filter('foo.baz', '!=', 2))); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 30 elements in the value array.', + it("should allow query when combining '!=' operator with any other inequality operator on a different field", async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection( + getFirestore(), + `${COLLECTION}/filter/inequality-combine-not-equal`, ); - return Promise.resolve(); - } - }); - - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - const { documentId } = firestoreModular; - firebase - .firestore() - .collection(COLLECTION) - .where(Filter(documentId(), '==', ['document-id'])) - .where(Filter('foo.bar', 'not-in', [1, 2, 3, 4])) - .orderBy('differentOrderBy', 'desc'); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); - - it("should throw error when using '!=' operator twice ", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where(Filter('foo.bar', '!=', 1)).where(Filter('foo.baz', '!=', 2)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } - }); - - it("should allow query when combining '!=' operator with any other inequality operator on a different field", async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/inequality-combine-not-equal`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - const snapshot = await colRef - .where(Filter('foo.bar', '>', 123)) - .where(Filter('bar', '!=', 123)) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + const expected = { foo: { bar: 300 }, bar: 200 }; + await Promise.all([ + addDoc(colRef, { foo: { bar: 1 }, bar: 1 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); + const snapshot = await query( + colRef, + where(Filter('foo.bar', '>', 123)), + where(Filter('bar', '!=', 123)), + ).get(); + + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - /* Queries */ + /* Queries */ - // Single Filters using '==', '>', '>=', '<', '<=', '!=' + // Single Filters using '==', '>', '>=', '<', '<=', '!=' - it('returns with where "==" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); + it('returns with where "==" filter', async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/equals`); - const expected = { foo: 'bar' }; + const expected = { foo: 'bar' }; - await Promise.all([ - colRef.add({ foo: [1, '1', 'something'] }), - colRef.add(expected), - colRef.add(expected), - ]); + await Promise.all([ + addDoc(colRef, { foo: [1, '1', 'something'] }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef.where(Filter('foo', '==', 'bar')).get(); + const snapshot = await query(colRef, where(Filter('foo', '==', 'bar'))).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "!=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-equals`); + it('returns with where "!=" filter', async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/not-equals`); - const expected = { foo: 'bar' }; + const expected = { foo: 'bar' }; - await Promise.all([ - colRef.add({ foo: 'something' }), - colRef.add(expected), - colRef.add(expected), - ]); + await Promise.all([ + addDoc(colRef, { foo: 'something' }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef.where(Filter('foo', '!=', 'something')).get(); + const snapshot = await query(colRef, where(Filter('foo', '!=', 'something'))).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where ">" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than`); + it('returns with where ">" filter', async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greater-than`); - const expected = { foo: 100 }; + const expected = { foo: 100 }; - await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 2 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef.where(Filter('foo', '>', 2)).get(); + const snapshot = await query(colRef, where(Filter('foo', '>', 2))).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "<" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than`); + it('returns with where "<" filter', async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/less-than`); - const expected = { foo: 2 }; + const expected = { foo: 2 }; - await Promise.all([colRef.add({ foo: 100 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 100 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef.where(Filter('foo', '<', 3)).get(); + const snapshot = await query(colRef, where(Filter('foo', '<', 3))).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where ">=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than-or-equal`); + it('returns with where ">=" filter', async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/greater-than-or-equal`); - const expected = { foo: 100 }; + const expected = { foo: 100 }; - await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 2 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef.where(Filter('foo', '>=', 100)).get(); + const snapshot = await query(colRef, where(Filter('foo', '>=', 100))).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); - }); - it('returns with where "<=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than-or-equal`); + it('returns with where "<=" filter', async function () { + const { getFirestore, collection, query, where, addDoc } = firestoreModular; + const colRef = collection(getFirestore(), `${COLLECTION}/filter/less-than-or-equal`); - const expected = { foo: 100 }; + const expected = { foo: 100 }; - await Promise.all([colRef.add({ foo: 101 }), colRef.add(expected), colRef.add(expected)]); + await Promise.all([ + addDoc(colRef, { foo: 101 }), + addDoc(colRef, expected), + addDoc(colRef, expected), + ]); - const snapshot = await colRef.where(Filter('foo', '<=', 100)).get(); + const snapshot = await query(colRef, where(Filter('foo', '<=', 100))).get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().should.eql(jet.contextify(expected)); + }); }); }); }); diff --git a/packages/firestore/e2e/Query/where.or.filter.e2e.js b/packages/firestore/e2e/Query/where.or.filter.e2e.js index 068e743c6e..4ada1bdd27 100644 --- a/packages/firestore/e2e/Query/where.or.filter.e2e.js +++ b/packages/firestore/e2e/Query/where.or.filter.e2e.js @@ -16,1071 +16,12 @@ */ const COLLECTION = 'firestore'; const { wipe } = require('../helpers'); -const { Filter } = firestoreModular; describe('firestore().collection().where(OR Filters)', function () { beforeEach(async function () { return await wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if using nested Filter.or() queries', async function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'foo')), - Filter.or(Filter('foo', '==', 'baz'), Filter('bar', '==', 'baz')), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('OR Filters with nested OR Filters are not supported'); - } - - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter('more', '==', 'stuff'), - ), - Filter.and( - Filter.or(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter('baz', '==', 'foo'), - ), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('OR Filters with nested OR Filters are not supported'); - } - return Promise.resolve(); - }); - - it('throws if fieldPath string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('.foo.bar', '!=', 1), Filter('.foo.bar', '==', 1)), - Filter.and(Filter('.foo.bar', '!=', 1), Filter('foo.bar', '==', 1)), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if operator string is invalid', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); - - it('throws if query contains multiple array-contains', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains', 1), - Filter('foo.bar', 'array-contains', 1), - ), - Filter.and(Filter('foo.bar', '==', 1), Filter('foo.bar', '==', 2)), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); - - it('throws if value is not defined', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', 'array-contains'), Filter('foo.bar', 'array-contains')), - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); - - it('throws if null value and no equal operator', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', 'array-contains', null)), - Filter.and(Filter('foo.bar', '!', 1), Filter('foo.bar', '!', 1)), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); - - it('allows null to be used with equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null)), - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', null)), - ), - ); - }); - - it('allows null to be used with not equal operator', function () { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '!=', null)), - Filter.and(Filter('foo.bar', '==', null), Filter('foo.bar', '==', 'something')), - ), - ); - }); - - it('allows multiple inequalities (excluding `!=`) on different paths provided', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/different-path-inequality`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123)), - Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123)), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('allows inequality on the same path', function () { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', '>', 123), - Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), - ), - Filter.and( - Filter('foo.bar', '>', 123), - Filter(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234), - ), - ), - ); - }); - - it('throws if in query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123')), - Filter.and(Filter('foo.bar', 'in', '123'), Filter('foo.bar', 'in', '123')), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if array-contains-any query with no array value', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains-any', '123'), - Filter('foo.bar', 'array-contains-any', '123'), - ), - Filter.and( - Filter('foo.bar', 'array-contains-any', '123'), - Filter('foo.bar', 'array-contains-any', '123'), - ), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if in query array length is greater than 30', function () { - try { - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and(Filter('foo.bar', 'in', queryArray), Filter('foo.bar', 'in', queryArray)), - Filter.and(Filter('foo.bar', 'in', queryArray), Filter('foo.bar', 'in', queryArray)), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 10 elements in the value'); - return Promise.resolve(); - } - }); - - it('throws if query has multiple array-contains-any filter', function () { - try { - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'array-contains-any', [1]), - ), - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'array-contains-any', [1]), - ), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); - return Promise.resolve(); - } - }); - - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2])), - Filter.and(Filter('foo.bar', 'not-in', [1]), Filter('foo.bar', 'not-in', [2])), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2])), - Filter.and(Filter('foo.bar', '!=', [1]), Filter('foo.bar', 'not-in', [2])), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2])), - Filter.and(Filter('foo.bar', 'in', [1]), Filter('foo.bar', 'not-in', [2])), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.or( - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'not-in', [2]), - ), - Filter.and( - Filter('foo.bar', 'array-contains-any', [1]), - Filter('foo.bar', 'not-in', [2]), - ), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); - - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.or( - Filter.and( - Filter('foo.bar', '==', 1), - Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - ), - Filter.and( - Filter('foo.bar', '==', 1), - Filter('foo.bar', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), - ), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 10 elements in the value array.', - ); - return Promise.resolve(); - } - }); - - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - const { documentId } = firestoreModular; - firebase - .firestore() - .collection(COLLECTION) - .where( - Filter.or( - Filter.and( - Filter(documentId(), '==', ['document-id']), - Filter('foo.bar', 'not-in', [1, 2, 3, 4]), - ), - Filter.and( - Filter(documentId(), '==', ['document-id']), - Filter('foo.bar', '==', 'something'), - ), - ), - ) - .orderBy('differentOrderBy', 'desc'); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); - - it("should throw error when using '!=' operator twice ", async function () { - const ref = firebase.firestore().collection(COLLECTION); - - try { - ref.where( - Filter.or( - Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2)), - Filter.and(Filter('foo.bar', '!=', 1), Filter('foo.baz', '!=', 2)), - ), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } - }); - - it("should allow query when combining '!=' operator with any other inequality operator on a different field", async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/filter/inequality-combine-not-equal`); - const expected = { foo: { bar: 300 }, bar: 200 }; - await Promise.all([ - colRef.add({ foo: { bar: 1 }, bar: 1 }), - colRef.add(expected), - colRef.add(expected), - ]); - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo.bar', '>', 123), Filter('bar', '>', 123)), - Filter.and(Filter('foo.bar', '!=', 1), Filter('bar', '>', 2)), - ), - ) - .get(); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - /* Queries */ - - // OR queries without ANDs - - // Equals OR another filter that works: '==', '>', '>=', '<', '<=', '!=' - - it('returns with where "==" Filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar' }; - const expected2 = { foo: 'farm' }; - - await Promise.all([ - colRef.add({ foo: 'something' }), - colRef.add(expected), - colRef.add(expected2), - ]); - - const snapshot = await colRef - .where(Filter.or(Filter('foo', '==', 'bar'), Filter('foo', '==', 'farm'))) - .get(); - - snapshot.size.should.eql(2); - const results = snapshot.docs.map(doc => doc.data().foo); - results.should.containEql('bar'); - results.should.containEql('farm'); - }); - - it('returns with where ">" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than`); - - const expected = { foo: 100 }; - - await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.or(Filter('foo', '>', 2), Filter('foo', '==', 30))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "<" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than`); - - const expected = { foo: 2 }; - - await Promise.all([colRef.add({ foo: 100 }), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.or(Filter('foo', '<', 3), Filter('foo', '==', 22))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where ">=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/greater-than-or-equal`); - - const expected = { foo: 100 }; - - await Promise.all([colRef.add({ foo: 2 }), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.or(Filter('foo', '>=', 100), Filter('foo', '==', 45))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "<=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/less-than-or-equal`); - - const expected = { foo: 100 }; - - await Promise.all([colRef.add({ foo: 101 }), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.or(Filter('foo', '<=', 100), Filter('foo', '==', 90))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - // // Equals OR another filter that works: "array-contains", "in", "array-contains-any", "not-in" - - it('returns "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains`); - - const expected = { foo: 'bar', something: [1, 2, 3] }; - - await Promise.all([ - colRef.add({ foo: 'something' }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where(Filter.or(Filter('foo', '==', 'not-this'), Filter('something', 'array-contains', 2))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - const data = s.data(); - data.foo.should.eql('bar'); - data.something[0].should.eql(1); - data.something[1].should.eql(2); - data.something[2].should.eql(3); - }); - }); - - it('returns "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/array-contains-any`); - - const expected = { foo: 'bar', something: [1, 2, 3] }; - - await Promise.all([ - colRef.add({ foo: 'something' }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter('foo', '==', 'not-this'), - Filter('something', 'array-contains-any', [2, 45]), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - const data = s.data(); - data.foo.should.eql('bar'); - data.something[0].should.eql(1); - data.something[1].should.eql(2); - data.something[2].should.eql(3); - }); - }); - - it('returns with where "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - const expected = 'bar'; - const data = { foo: expected }; - - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data), - colRef.add(data), - ]); - - const snapshot = await colRef.where(Filter('foo', 'not-in', ['not', 'this'])).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); - const expected1 = 'bar'; - const expected2 = 'baz'; - const data1 = { foo: expected1 }; - const data2 = { foo: expected2 }; - - await Promise.all([ - colRef.add({ foo: 'not' }), - colRef.add({ foo: 'this' }), - colRef.add(data1), - colRef.add(data2), - ]); - - const snapshot = await colRef - .where( - Filter.or(Filter('foo', 'in', [expected1, expected2]), Filter('foo', '==', 'not-this')), - ) - .get(); - - snapshot.size.should.eql(2); - const results = snapshot.docs.map(d => d.data().foo); - results.should.containEql(expected1); - results.should.containEql(expected2); - }); - - // OR queries with ANDs. Equals and: '==', '>', '>=', '<', '<=', '!=' - it('returns with where "==" && "==" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', bar: 'baz' }; - await Promise.all([ - colRef.add({ foo: [1, '1', 'something'] }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter.and(Filter('blah', '==', 'blah'), Filter('not', '==', 'this')), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "!=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', baz: 'baz' }; - const notExpected = { foo: 'bar', baz: 'something' }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', '==', 'bar'), Filter('baz', '!=', 'something'))) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & ">" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals-not-equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>', 2)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '>', 199)), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "<" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<', 201)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '<', 201)), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "<=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 1000 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '<=', 200)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '<=', 200)), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & ">=" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', population: 200 }; - const notExpected = { foo: 'bar', population: 100 }; - await Promise.all([colRef.add(notExpected), colRef.add(expected), colRef.add(expected)]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('population', '>=', 200)), - Filter.and(Filter('foo', '==', 'not-this'), Filter('population', '>=', 200)), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - // Using OR and AND query combinations with Equals && "array-contains", "array-contains-any", "not-in" and "in" filters - - it('returns with where "==" & "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and( - Filter('foo', 'array-contains', match.toString()), - Filter('bar', '==', 'baz'), - ), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), - ), - ) - .get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with where "==" & "array-contains-any" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2'], bar: 'baz' }), - colRef.add({ foo: ['2', match.toString()], bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and( - Filter('foo', 'array-contains-any', [match.toString(), 1]), - Filter('bar', '==', 'baz'), - ), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), - ), - ) - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().bar.should.equal('baz'); - snapshot.docs[1].data().bar.should.equal('baz'); - }); - - it('returns with where "==" & "not-in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/not-in`); - - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); - - const snapshot = await colRef - .where(Filter.and(Filter('foo', 'not-in', ['yolo', 'thing']), Filter('bar', '==', 'baz'))) - .get(); - - snapshot.size.should.eql(2); - snapshot.docs[0].data().foo.should.equal('bar'); - snapshot.docs[1].data().foo.should.equal('bar'); - }); - - it('returns with where "==" & "in" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/in`); - - await Promise.all([ - colRef.add({ foo: 'bar', bar: 'baz' }), - colRef.add({ foo: 'thing', bar: 'baz' }), - colRef.add({ foo: 'yolo', bar: 'baz' }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', 'in', ['bar', 'yolo']), Filter('bar', '==', 'baz')), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), - ), - ) - .get(); - - snapshot.size.should.eql(2); - const result = snapshot.docs.map(d => d.data().foo); - result.should.containEql('bar'); - result.should.containEql('yolo'); - }); - - // Backwards compatibility Filter queries. Add where() queries and also use multiple where() queries with Filters to check it works - - it('backwards compatible with existing where() "==" && "==" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const expected = { foo: 'bar', bar: 'baz', existing: 'where' }; - await Promise.all([ - colRef.add({ foo: [1, '1', 'something'] }), - colRef.add(expected), - colRef.add(expected), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and(Filter('foo', '==', 'bar'), Filter('bar', '==', 'baz')), - Filter.and(Filter('blah', '==', 'blah'), Filter('not', '==', 'this')), - ), - ) - .where('existing', '==', 'where') - .get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().should.eql(jet.contextify(expected)); - }); - }); - - it('backwards compatible with existing where() query, returns with where "==" & "array-contains" filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), - colRef.add({ foo: [1, '2', match.toString()], bar: 'baz', existing: 'where' }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and( - Filter('foo', 'array-contains', match.toString()), - Filter('bar', '==', 'baz'), - ), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), - ), - ) - .where('existing', '==', 'where') - .get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('backwards compatible whilst chaining Filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and( - Filter('foo', 'array-contains', match.toString()), - Filter('bar', '==', 'baz'), - ), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), - ), - ) - .where('existing', '==', 'where') - .where(Filter('another', '==', 'filter')) - .get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('backwards compatible whilst chaining AND Filter', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/filter/equals`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '1', match] }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - chain: 'and', - }), - colRef.add({ - foo: [1, '2', match.toString()], - bar: 'baz', - existing: 'where', - another: 'filter', - chain: 'and', - }), - ]); - - const snapshot = await colRef - .where( - Filter.or( - Filter.and( - Filter('foo', 'array-contains', match.toString()), - Filter('bar', '==', 'baz'), - ), - Filter.and(Filter('foo', '==', 'not-this'), Filter('bar', '==', 'baz')), - ), - ) - .where('existing', '==', 'where') - .where(Filter.and(Filter('another', '==', 'filter'), Filter('chain', '==', 'and'))) - .get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - }); - describe('modular', function () { it('throws if using nested or() queries', async function () { const { getFirestore, collection, where, or, and, query } = firestoreModular; diff --git a/packages/firestore/e2e/QuerySnapshot.e2e.js b/packages/firestore/e2e/QuerySnapshot.e2e.js index 97d32b88a4..4b9bcb545b 100644 --- a/packages/firestore/e2e/QuerySnapshot.e2e.js +++ b/packages/firestore/e2e/QuerySnapshot.e2e.js @@ -22,315 +22,6 @@ describe('firestore.QuerySnapshot', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('is returned from a collection get()', async function () { - const snapshot = await firebase.firestore().collection(COLLECTION).get(); - - snapshot.constructor.name.should.eql('QuerySnapshot'); - }); - - it('is returned from a collection onSnapshot()', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - firebase.firestore().collection(COLLECTION).onSnapshot(callback); - await Utils.spyToBeCalledOnceAsync(callback); - callback.args[0][0].constructor.name.should.eql('QuerySnapshot'); - }); - - it('returns an array of DocumentSnapshots', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.get(); - snapshot.docs.should.be.Array(); - snapshot.docs.length.should.be.aboveOrEqual(1); - snapshot.docs[0].constructor.name.should.eql('DocumentSnapshot'); - }); - - it('returns false if not empty', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.get(); - snapshot.empty.should.be.Boolean(); - snapshot.empty.should.be.False(); - }); - - it('returns true if empty', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/foo/emptycollection`); - const snapshot = await colRef.get(); - snapshot.empty.should.be.Boolean(); - snapshot.empty.should.be.True(); - }); - - it('returns a SnapshotMetadata instance', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.get(); - snapshot.metadata.constructor.name.should.eql('SnapshotMetadata'); - }); - - it('returns a Query instance', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.get(); - snapshot.query.constructor.name.should.eql('CollectionReference'); - }); - - it('returns size as a number', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.get(); - snapshot.size.should.be.Number(); - }); - - describe('docChanges()', function () { - it('throws if options is not an object', async function () { - try { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.limit(1).get(); - snapshot.docChanges(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' expected an object"); - return Promise.resolve(); - } - }); - - it('throws if options.includeMetadataChanges is not a boolean', async function () { - try { - const colRef = firebase.firestore().collection(COLLECTION); - const snapshot = await colRef.limit(1).get(); - snapshot.docChanges({ includeMetadataChanges: 123 }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.includeMetadataChanges' expected a boolean"); - return Promise.resolve(); - } - }); - - it('throws if options.includeMetadataChanges is true, but snapshot does not include those changes', async function () { - if (Platform.other) { - return; - } - - const callback = sinon.spy(); - const colRef = firebase.firestore().collection(COLLECTION); - const unsub = colRef.onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - await Utils.spyToBeCalledOnceAsync(callback); - unsub(); - const snapshot = callback.args[0][0]; - - try { - snapshot.docChanges({ includeMetadataChanges: true }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('To include metadata changes with your document changes'); - return Promise.resolve(); - } - }); - - it('returns an array of DocumentChange instances', async function () { - if (Platform.other) { - return; - } - const colRef = firebase.firestore().collection(COLLECTION); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - const changes = snapshot.docChanges(); - changes.should.be.Array(); - changes.length.should.be.eql(1); - changes[0].constructor.name.should.eql('DocumentChange'); - }); - - // FIXME flakey in CI - the changes length comes back unstable - xit('returns the correct number of document changes if listening to metadata changes', async function () { - const callback = sinon.spy(); - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-true`); - const unsub = colRef.onSnapshot({ includeMetadataChanges: true }, callback); - await colRef.add({ foo: 'bar' }); - await Utils.spyToBeCalledTimesAsync(callback, 3); - unsub(); - - const snap1 = callback.args[0][0]; - const snap2 = callback.args[1][0]; - const snap3 = callback.args[2][0]; - - snap1.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); - snap2.docChanges({ includeMetadataChanges: true }).length.should.be.eql(0); - snap3.docChanges({ includeMetadataChanges: true }).length.should.be.eql(1); - }); - - // FIXME this flakes on CI, disabling for now - xit('returns the correct number of document changes if listening to metadata changes, but not including them in docChanges', async function () { - const callback = sinon.spy(); - const colRef = firebase - .firestore() - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/metadatachanges-true-false`); - const unsub = colRef.onSnapshot({ includeMetadataChanges: true }, callback); - await Utils.sleep(1000); - await colRef.add({ foo: 'bar' }); - await Utils.spyToBeCalledTimesAsync(callback, 3, 15000); - unsub(); - - const snap1 = callback.args[0][0]; - const snap2 = callback.args[1][0]; - const snap3 = callback.args[2][0]; - - snap1.docChanges({ includeMetadataChanges: false }).length.should.be.eql(1); // FIXME when it flakes, this comes back as 0 - snap2.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); - snap3.docChanges({ includeMetadataChanges: false }).length.should.be.eql(0); - }); - }); - - describe('forEach()', function () { - it('throws if callback is not a function', async function () { - try { - const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/nonfunction`); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - snapshot.forEach(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'callback' expected a function"); - return Promise.resolve(); - } - }); - - it('calls back a function', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/function`); - await colRef.add({}); - await colRef.add({}); - const snapshot = await colRef.limit(2).get(); - const callback = sinon.spy(); - snapshot.forEach.should.be.Function(); - snapshot.forEach(callback); - await Utils.spyToBeCalledTimesAsync(callback, 2, 20000); - callback.should.be.calledTwice(); - callback.args[0][0].constructor.name.should.eql('DocumentSnapshot'); - callback.args[0][1].should.be.Number(); - callback.args[1][0].constructor.name.should.eql('DocumentSnapshot'); - callback.args[1][1].should.be.Number(); - }); - - it('provides context to the callback', async function () { - const colRef = firebase.firestore().collection(`${COLLECTION}/callbacks/function-context`); - await colRef.add({}); - const snapshot = await colRef.limit(1).get(); - const callback = sinon.spy(); - snapshot.forEach.should.be.Function(); - - class Foo {} - - snapshot.forEach(callback, Foo); - await Utils.spyToBeCalledOnceAsync(callback, 20000); - callback.should.be.calledOnce(); - callback.firstCall.thisValue.should.eql(Foo); - }); - }); - - describe('isEqual()', function () { - it('throws if other is not a QuerySnapshot', async function () { - try { - const qs = await firebase.firestore().collection(COLLECTION).get(); - qs.isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected a QuerySnapshot instance"); - return Promise.resolve(); - } - }); - - it('returns false if not equal (simple checks)', async function () { - const colRef = firebase.firestore().collection(COLLECTION); - // Ensure a doc exists - await colRef.add({}); - - const qs = await colRef.get(); - - const querySnap1 = await firebase - .firestore() - .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual`) - .get(); - - const eq1 = qs.isEqual(querySnap1); - - eq1.should.be.False(); - }); - - it('returns false if not equal (expensive checks)', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual-False`); - // Ensure a doc exists - const docRef = colRef.doc('firstdoc'); - await docRef.set({ - foo: 'bar', - bar: { - foo: 1, - }, - }); - - // Grab snapshot - const qs1 = await colRef.get(); - - // Update same collection - await docRef.update({ - bar: { - foo: 2, - }, - }); - - const qs2 = await colRef.get(); - - const eq1 = qs1.isEqual(qs2); - - eq1.should.be.False(); - }); - - it('returns true when equal', async function () { - const colRef = firebase - .firestore() - .collection(`${COLLECTION}/querysnapshot/querySnapshotIsEqual-True`); - - await Promise.all([ - colRef.add({ foo: 'bar' }), - colRef.add({ foo: 1 }), - colRef.add({ - foo: { - foo: 'bar', - }, - }), - ]); - - const qs1 = await colRef.get(); - const qs2 = await colRef.get(); - - const eq = qs1.isEqual(qs2); - - eq.should.be.True(); - }); - }); - }); - describe('modular', function () { it('is returned from a collection get()', async function () { const { getFirestore, getDocs, collection } = firestoreModular; diff --git a/packages/firestore/e2e/SecondDatabase/second.Transation.e2e.js b/packages/firestore/e2e/SecondDatabase/second.Transation.e2e.js index 6a94a8c85c..6b05dcd5c8 100644 --- a/packages/firestore/e2e/SecondDatabase/second.Transation.e2e.js +++ b/packages/firestore/e2e/SecondDatabase/second.Transation.e2e.js @@ -22,769 +22,398 @@ const COLLECTION = 'second-database'; const SECOND_DATABASE_ID = 'second-rnfb'; describe('Second Database', function () { - describe('firestore.Transaction', function () { - describe('v8 compatibility', function () { - let firestore; - - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - firestore = firebase.app().firestore(SECOND_DATABASE_ID); - }); + describe('modular', function () { + let firestore; - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); + before(function () { + const { getFirestore } = firestoreModular; + firestore = getFirestore(null, SECOND_DATABASE_ID); + }); - it('should throw if updateFunction is not a Promise', async function () { - try { - await firestore.runTransaction(() => 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'updateFunction' must return a Promise"); - return Promise.resolve(); - } + it('should throw if updateFunction is not a Promise', async function () { + const { runTransaction } = firestoreModular; + + try { + await runTransaction(firestore, () => 123); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'updateFunction' must return a Promise"); + return Promise.resolve(); + } + }); + + it('should return an instance of FirestoreTransaction', async function () { + const { runTransaction } = firestoreModular; + await runTransaction(firestore, async transaction => { + transaction.constructor.name.should.eql('Transaction'); + return null; }); + }); - it('should return an instance of FirestoreTransaction', async function () { - await firestore.runTransaction(async transaction => { - transaction.constructor.name.should.eql('Transaction'); - return null; - }); + it('should resolve with user value', async function () { + const { runTransaction } = firestoreModular; + const expected = Date.now(); + + const value = await runTransaction(firestore, async () => { + return expected; }); - it('should resolve with user value', async function () { - const expected = Date.now(); + value.should.eql(expected); + }); - const value = await firestore.runTransaction(async () => { - return expected; + it('should reject with user Error', async function () { + const { runTransaction } = firestoreModular; + const message = `Error: ${Date.now()}`; + + try { + await runTransaction(firestore, async () => { + throw new Error(message); }); + return Promise.reject(new Error('Did not throw Error.')); + } catch (error) { + error.message.should.eql(message); + return Promise.resolve(); + } + }); - value.should.eql(expected); - }); + it('should reject a native error', async function () { + const { runTransaction, doc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${NO_RULE_COLLECTION}/foo`); - it('should reject with user Error', async function () { - const message = `Error: ${Date.now()}`; + try { + await runTransaction(db, async t => { + t.set(docRef, { + foo: 'bar', + }); + }); + return Promise.reject(new Error('Did not throw Error.')); + } catch (error) { + error.code.should.eql('firestore/permission-denied'); + return Promise.resolve(); + } + }); + describe('transaction.get()', function () { + it('should throw if not providing a document reference', async function () { + const { runTransaction } = firestoreModular; try { - await firestore.runTransaction(async () => { - throw new Error(message); + await runTransaction(firestore, t => { + return t.get(123); }); - return Promise.reject(new Error('Did not throw Error.')); + return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.eql(message); + error.message.should.containEql("'documentRef' expected a DocumentReference"); return Promise.resolve(); } }); - it('should reject a native error', async function () { - const docRef = firestore.doc(`${NO_RULE_COLLECTION}/foo`); + it('should get a document and return a DocumentSnapshot', async function () { + const { runTransaction, doc, setDoc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/transactions/transaction/get-delete`); + await setDoc(docRef, {}); + + await runTransaction(db, async t => { + const docSnapshot = await t.get(docRef); + docSnapshot.constructor.name.should.eql('DocumentSnapshot'); + docSnapshot.exists().should.eql(true); + docSnapshot.id.should.eql('get-delete'); + t.delete(docRef); + }); + }); + }); + + describe('transaction.delete()', function () { + it('should throw if not providing a document reference', async function () { + const { runTransaction } = firestoreModular; try { - await firestore.runTransaction(async t => { - t.set(docRef, { - foo: 'bar', - }); + await runTransaction(firestore, async t => { + t.delete(123); }); - return Promise.reject(new Error('Did not throw Error.')); + return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.code.should.eql('firestore/permission-denied'); + error.message.should.containEql("'documentRef' expected a DocumentReference"); return Promise.resolve(); } }); - describe('transaction.get()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firestore.runTransaction(t => { - return t.get(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should get a document and return a DocumentSnapshot', async function () { - const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/get-delete`); - await docRef.set({}); - - await firestore.runTransaction(async t => { - const docSnapshot = await t.get(docRef); - docSnapshot.constructor.name.should.eql('DocumentSnapshot'); - docSnapshot.exists().should.eql(true); - docSnapshot.id.should.eql('get-delete'); + it('should delete documents', async function () { + const { runTransaction, doc, setDoc, getDoc } = firestoreModular; + const db = firestore; + const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); + await setDoc(docRef1, {}); - t.delete(docRef); - }); - }); - }); + const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); + await setDoc(docRef2, {}); - describe('transaction.delete()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firestore.runTransaction(async t => { - t.delete(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } + await runTransaction(db, async t => { + t.delete(docRef1); + t.delete(docRef2); }); - it('should delete documents', async function () { - const docRef1 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete1`); - await docRef1.set({}); - - const docRef2 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete2`); - await docRef2.set({}); - - await firestore.runTransaction(async t => { - t.delete(docRef1); - t.delete(docRef2); - }); - - const snapshot1 = await docRef1.get(); - snapshot1.exists().should.eql(false); + const snapshot1 = await getDoc(docRef1); + snapshot1.exists().should.eql(false); - const snapshot2 = await docRef2.get(); - snapshot2.exists().should.eql(false); - }); + const snapshot2 = await getDoc(docRef2); + snapshot2.exists().should.eql(false); }); + }); - describe('transaction.update()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firestore.runTransaction(async t => { - t.update(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should throw if update args are invalid', async function () { - const docRef = firestore.doc(`${COLLECTION}/foo`); - - try { - await firestore.runTransaction(async t => { - t.update(docRef, 123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('it must be an object'); - return Promise.resolve(); - } - }); - - it('should update documents', async function () { - const value = Date.now(); - - const docRef1 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete1`); - await docRef1.set({ - foo: 'bar', - bar: 'baz', - }); - - const docRef2 = firestore.doc(`${COLLECTION}/transactions/transaction/delete-delete2`); - await docRef2.set({ - foo: 'bar', - bar: 'baz', - }); - - await firestore.runTransaction(async t => { - t.update(docRef1, { - bar: value, - }); - t.update(docRef2, 'bar', value); + describe('transaction.update()', function () { + it('should throw if not providing a document reference', async function () { + const { runTransaction } = firestoreModular; + try { + await runTransaction(firestore, async t => { + t.update(123); }); - - const expected = { - foo: 'bar', - bar: value, - }; - - const snapshot1 = await docRef1.get(); - snapshot1.exists().should.eql(true); - snapshot1.data().should.eql(jet.contextify(expected)); - - const snapshot2 = await docRef2.get(); - snapshot2.exists().should.eql(true); - snapshot2.data().should.eql(jet.contextify(expected)); - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'documentRef' expected a DocumentReference"); + return Promise.resolve(); + } }); - describe('transaction.set()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firestore.runTransaction(async t => { - t.set(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should throw if set data is invalid', async function () { - const docRef = firestore.doc(`${COLLECTION}/foo`); - - try { - await firestore.runTransaction(async t => { - t.set(docRef, 123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object."); - return Promise.resolve(); - } - }); - - it('should throw if set options are invalid', async function () { - const docRef = firestore.doc(`${COLLECTION}/foo`); - - try { - await firestore.runTransaction(async t => { - t.set( - docRef, - {}, - { - merge: true, - mergeFields: [], - }, - ); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' must not contain both 'merge' & 'mergeFields'", - ); - return Promise.resolve(); - } - }); + it('should throw if update args are invalid', async function () { + const { runTransaction, doc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/foo`); - it('should set data', async function () { - const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/set`); - await docRef.set({ - foo: 'bar', + try { + await runTransaction(db, async t => { + t.update(docRef, 123); }); - const expected = { - foo: 'baz', - }; + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('it must be an object'); + return Promise.resolve(); + } + }); - await firestore.runTransaction(async t => { - t.set(docRef, expected); - }); + it('should update documents', async function () { + const { runTransaction, doc, setDoc, getDoc } = firestoreModular; + const db = firestore; + const value = Date.now(); - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); + const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); + await setDoc(docRef1, { + foo: 'bar', + bar: 'baz', }); - it('should set data with merge', async function () { - const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/set-merge`); - await docRef.set({ - foo: 'bar', - bar: 'baz', - }); - const expected = { - foo: 'bar', - bar: 'foo', - }; - - await firestore.runTransaction(async t => { - t.set( - docRef, - { - bar: 'foo', - }, - { - merge: true, - }, - ); - }); - - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); + const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); + await setDoc(docRef2, { + foo: 'bar', + bar: 'baz', }); - it('should set data with merge fields', async function () { - const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/set-mergefields`); - await docRef.set({ - foo: 'bar', - bar: 'baz', - baz: 'ben', - }); - const expected = { - foo: 'bar', - bar: 'foo', - baz: 'foo', - }; - - await firestore.runTransaction(async t => { - t.set( - docRef, - { - bar: 'foo', - baz: 'foo', - }, - { - mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], - }, - ); + await runTransaction(db, async t => { + t.update(docRef1, { + bar: value, }); - - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); + t.update(docRef2, 'bar', value); }); - it('should roll back any updates that failed', async function () { - // FIXME issue 8267 - if (Platform.other) { - this.skip(); - } + const expected = { + foo: 'bar', + bar: value, + }; - const docRef = firestore.doc(`${COLLECTION}/transactions/transaction/rollback`); + const snapshot1 = await getDoc(docRef1); + snapshot1.exists().should.eql(true); + snapshot1.data().should.eql(jet.contextify(expected)); - await docRef.set({ - turn: 0, - }); - - const prop1 = 'prop1'; - const prop2 = 'prop2'; - const turn = 0; - const errorMessage = 'turn cannot exceed 1'; - - const createTransaction = prop => { - return firestore.runTransaction(async transaction => { - const doc = await transaction.get(docRef); - const data = doc.data(); - - if (data.turn !== turn) { - throw new Error(errorMessage); - } - - const update = { - turn: turn + 1, - [prop]: 1, - }; - - transaction.update(docRef, update); - }); - }; - - const promises = [createTransaction(prop1), createTransaction(prop2)]; - - try { - await Promise.all(promises); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql(errorMessage); - } - const result = await docRef.get(); - should(result.data()).not.have.properties([prop1, prop2]); - }); + const snapshot2 = await getDoc(docRef2); + snapshot2.exists().should.eql(true); + snapshot2.data().should.eql(jet.contextify(expected)); }); }); - describe('modular', function () { - let firestore; - - before(function () { - const { getFirestore } = firestoreModular; - firestore = getFirestore(null, SECOND_DATABASE_ID); - }); - - it('should throw if updateFunction is not a Promise', async function () { + describe('transaction.set()', function () { + it('should throw if not providing a document reference', async function () { const { runTransaction } = firestoreModular; - try { - await runTransaction(firestore, () => 123); + await runTransaction(firestore, async t => { + t.set(123); + }); return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.containEql("'updateFunction' must return a Promise"); + error.message.should.containEql("'documentRef' expected a DocumentReference"); return Promise.resolve(); } }); - it('should return an instance of FirestoreTransaction', async function () { - const { runTransaction } = firestoreModular; - await runTransaction(firestore, async transaction => { - transaction.constructor.name.should.eql('Transaction'); - return null; - }); - }); - - it('should resolve with user value', async function () { - const { runTransaction } = firestoreModular; - const expected = Date.now(); - - const value = await runTransaction(firestore, async () => { - return expected; - }); - - value.should.eql(expected); - }); - - it('should reject with user Error', async function () { - const { runTransaction } = firestoreModular; - const message = `Error: ${Date.now()}`; + it('should throw if set data is invalid', async function () { + const { runTransaction, doc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/foo`); try { - await runTransaction(firestore, async () => { - throw new Error(message); + await runTransaction(db, async t => { + t.set(docRef, 123); }); - return Promise.reject(new Error('Did not throw Error.')); + return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.message.should.eql(message); + error.message.should.containEql("'data' must be an object."); return Promise.resolve(); } }); - it('should reject a native error', async function () { + it('should throw if set options are invalid', async function () { const { runTransaction, doc } = firestoreModular; const db = firestore; - const docRef = doc(db, `${NO_RULE_COLLECTION}/foo`); + const docRef = doc(db, `${COLLECTION}/foo`); try { await runTransaction(db, async t => { - t.set(docRef, { - foo: 'bar', - }); + t.set( + docRef, + {}, + { + merge: true, + mergeFields: [], + }, + ); }); - return Promise.reject(new Error('Did not throw Error.')); + return Promise.reject(new Error('Did not throw an Error.')); } catch (error) { - error.code.should.eql('firestore/permission-denied'); + error.message.should.containEql( + "'options' must not contain both 'merge' & 'mergeFields'", + ); return Promise.resolve(); } }); - describe('transaction.get()', function () { - it('should throw if not providing a document reference', async function () { - const { runTransaction } = firestoreModular; - try { - await runTransaction(firestore, t => { - return t.get(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should get a document and return a DocumentSnapshot', async function () { - const { runTransaction, doc, setDoc } = firestoreModular; - const db = firestore; - const docRef = doc(db, `${COLLECTION}/transactions/transaction/get-delete`); - await setDoc(docRef, {}); - - await runTransaction(db, async t => { - const docSnapshot = await t.get(docRef); - docSnapshot.constructor.name.should.eql('DocumentSnapshot'); - docSnapshot.exists().should.eql(true); - docSnapshot.id.should.eql('get-delete'); - - t.delete(docRef); - }); + it('should set data', async function () { + const { runTransaction, doc, getDoc, setDoc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set`); + await setDoc(docRef, { + foo: 'bar', }); - }); + const expected = { + foo: 'baz', + }; - describe('transaction.delete()', function () { - it('should throw if not providing a document reference', async function () { - const { runTransaction } = firestoreModular; - try { - await runTransaction(firestore, async t => { - t.delete(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } + await runTransaction(db, async t => { + t.set(docRef, expected); }); - it('should delete documents', async function () { - const { runTransaction, doc, setDoc, getDoc } = firestoreModular; - const db = firestore; - const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); - await setDoc(docRef1, {}); - - const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); - await setDoc(docRef2, {}); - - await runTransaction(db, async t => { - t.delete(docRef1); - t.delete(docRef2); - }); - - const snapshot1 = await getDoc(docRef1); - snapshot1.exists().should.eql(false); - - const snapshot2 = await getDoc(docRef2); - snapshot2.exists().should.eql(false); - }); + const snapshot = await getDoc(docRef); + snapshot.data().should.eql(jet.contextify(expected)); }); - describe('transaction.update()', function () { - it('should throw if not providing a document reference', async function () { - const { runTransaction } = firestoreModular; - try { - await runTransaction(firestore, async t => { - t.update(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should throw if update args are invalid', async function () { - const { runTransaction, doc } = firestoreModular; - const db = firestore; - const docRef = doc(db, `${COLLECTION}/foo`); - - try { - await runTransaction(db, async t => { - t.update(docRef, 123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('it must be an object'); - return Promise.resolve(); - } - }); - - it('should update documents', async function () { - const { runTransaction, doc, setDoc, getDoc } = firestoreModular; - const db = firestore; - const value = Date.now(); - - const docRef1 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete1`); - await setDoc(docRef1, { - foo: 'bar', - bar: 'baz', - }); - - const docRef2 = doc(db, `${COLLECTION}/transactions/transaction/delete-delete2`); - await setDoc(docRef2, { - foo: 'bar', - bar: 'baz', - }); - - await runTransaction(db, async t => { - t.update(docRef1, { - bar: value, - }); - t.update(docRef2, 'bar', value); - }); - - const expected = { - foo: 'bar', - bar: value, - }; - - const snapshot1 = await getDoc(docRef1); - snapshot1.exists().should.eql(true); - snapshot1.data().should.eql(jet.contextify(expected)); - - const snapshot2 = await getDoc(docRef2); - snapshot2.exists().should.eql(true); - snapshot2.data().should.eql(jet.contextify(expected)); - }); + it('should set data with merge', async function () { + const { runTransaction, doc, getDoc, setDoc } = firestoreModular; + const db = firestore; + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-merge`); + await setDoc(docRef, { + foo: 'bar', + bar: 'baz', + }); + const expected = { + foo: 'bar', + bar: 'foo', + }; + + await runTransaction(db, async t => { + t.set( + docRef, + { + bar: 'foo', + }, + { + merge: true, + }, + ); + }); + + const snapshot = await getDoc(docRef); + snapshot.data().should.eql(jet.contextify(expected)); }); - describe('transaction.set()', function () { - it('should throw if not providing a document reference', async function () { - const { runTransaction } = firestoreModular; - try { - await runTransaction(firestore, async t => { - t.set(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); + it('should set data with merge fields', async function () { + const { runTransaction, doc, getDoc, setDoc, FieldPath } = firestoreModular; + const db = firestore; - it('should throw if set data is invalid', async function () { - const { runTransaction, doc } = firestoreModular; - const db = firestore; - const docRef = doc(db, `${COLLECTION}/foo`); - - try { - await runTransaction(db, async t => { - t.set(docRef, 123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object."); - return Promise.resolve(); - } - }); + const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-mergefields`); + await setDoc(docRef, { + foo: 'bar', + bar: 'baz', + baz: 'ben', + }); + const expected = { + foo: 'bar', + bar: 'foo', + baz: 'foo', + }; + + await runTransaction(db, async t => { + t.set( + docRef, + { + bar: 'foo', + baz: 'foo', + }, + { + mergeFields: ['bar', new FieldPath('baz')], + }, + ); + }); + + const snapshot = await getDoc(docRef); + snapshot.data().should.eql(jet.contextify(expected)); + }); - it('should throw if set options are invalid', async function () { - const { runTransaction, doc } = firestoreModular; - const db = firestore; - const docRef = doc(db, `${COLLECTION}/foo`); - - try { - await runTransaction(db, async t => { - t.set( - docRef, - {}, - { - merge: true, - mergeFields: [], - }, - ); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' must not contain both 'merge' & 'mergeFields'", - ); - return Promise.resolve(); - } - }); + it('should roll back any updates that failed', async function () { + // FIXME issue 8267 + if (Platform.other) { + this.skip(); + } - it('should set data', async function () { - const { runTransaction, doc, getDoc, setDoc } = firestoreModular; - const db = firestore; - const docRef = doc(db, `${COLLECTION}/transactions/transaction/set`); - await setDoc(docRef, { - foo: 'bar', - }); - const expected = { - foo: 'baz', - }; + const { runTransaction, doc, getDoc, setDoc } = firestoreModular; + const db = firestore; - await runTransaction(db, async t => { - t.set(docRef, expected); - }); + const docRef = doc(db, `${COLLECTION}/transactions/transaction/rollback`); - const snapshot = await getDoc(docRef); - snapshot.data().should.eql(jet.contextify(expected)); + await setDoc(docRef, { + turn: 0, }); - it('should set data with merge', async function () { - const { runTransaction, doc, getDoc, setDoc } = firestoreModular; - const db = firestore; - const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-merge`); - await setDoc(docRef, { - foo: 'bar', - bar: 'baz', - }); - const expected = { - foo: 'bar', - bar: 'foo', - }; - - await runTransaction(db, async t => { - t.set( - docRef, - { - bar: 'foo', - }, - { - merge: true, - }, - ); - }); + const prop1 = 'prop1'; + const prop2 = 'prop2'; + const turn = 0; + const errorMessage = 'turn cannot exceed 1'; - const snapshot = await getDoc(docRef); - snapshot.data().should.eql(jet.contextify(expected)); - }); + const createTransaction = prop => { + return runTransaction(db, async transaction => { + const doc = await transaction.get(docRef); + const data = doc.data(); - it('should set data with merge fields', async function () { - const { runTransaction, doc, getDoc, setDoc, FieldPath } = firestoreModular; - const db = firestore; + if (data.turn !== turn) { + throw new Error(errorMessage); + } - const docRef = doc(db, `${COLLECTION}/transactions/transaction/set-mergefields`); - await setDoc(docRef, { - foo: 'bar', - bar: 'baz', - baz: 'ben', - }); - const expected = { - foo: 'bar', - bar: 'foo', - baz: 'foo', - }; + const update = { + turn: turn + 1, + [prop]: 1, + }; - await runTransaction(db, async t => { - t.set( - docRef, - { - bar: 'foo', - baz: 'foo', - }, - { - mergeFields: ['bar', new FieldPath('baz')], - }, - ); + transaction.update(docRef, update); }); + }; - const snapshot = await getDoc(docRef); - snapshot.data().should.eql(jet.contextify(expected)); - }); - - it('should roll back any updates that failed', async function () { - // FIXME issue 8267 - if (Platform.other) { - this.skip(); - } - - const { runTransaction, doc, getDoc, setDoc } = firestoreModular; - const db = firestore; - - const docRef = doc(db, `${COLLECTION}/transactions/transaction/rollback`); + const promises = [createTransaction(prop1), createTransaction(prop2)]; - await setDoc(docRef, { - turn: 0, - }); - - const prop1 = 'prop1'; - const prop2 = 'prop2'; - const turn = 0; - const errorMessage = 'turn cannot exceed 1'; - - const createTransaction = prop => { - return runTransaction(db, async transaction => { - const doc = await transaction.get(docRef); - const data = doc.data(); - - if (data.turn !== turn) { - throw new Error(errorMessage); - } - - const update = { - turn: turn + 1, - [prop]: 1, - }; - - transaction.update(docRef, update); - }); - }; - - const promises = [createTransaction(prop1), createTransaction(prop2)]; - - try { - await Promise.all(promises); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql(errorMessage); - } - const result = await getDoc(docRef); - should(result.data()).not.have.properties([prop1, prop2]); - }); + try { + await Promise.all(promises); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql(errorMessage); + } + const result = await getDoc(docRef); + should(result.data()).not.have.properties([prop1, prop2]); }); }); }); diff --git a/packages/firestore/e2e/SecondDatabase/second.onSnapshot.e2e.js b/packages/firestore/e2e/SecondDatabase/second.onSnapshot.e2e.js index 24bd831ac0..c127521fdb 100644 --- a/packages/firestore/e2e/SecondDatabase/second.onSnapshot.e2e.js +++ b/packages/firestore/e2e/SecondDatabase/second.onSnapshot.e2e.js @@ -23,385 +23,146 @@ const COLLECTION = 'second-database'; const SECOND_DATABASE_ID = 'second-rnfb'; describe('Second Database', function () { - describe('firestore().collection().onSnapshot()', function () { - describe('v8 compatibility', function () { - let firestore; - - beforeEach(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - firestore = firebase.app().firestore(SECOND_DATABASE_ID); - return await wipe(false, SECOND_DATABASE_ID); - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if no arguments are provided', function () { - try { - firestore.collection(COLLECTION).onSnapshot(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected at least one argument'); - return Promise.resolve(); - } - }); + describe('modular', function () { + let firestore; - it('returns an unsubscribe function', function () { - const unsub = firestore.collection(`${COLLECTION}/foo/bar1`).onSnapshot(() => {}); - - unsub.should.be.a.Function(); - unsub(); - }); - - it('accepts a single callback function with snapshot', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firestore.collection(`${COLLECTION}/foo/bar2`).onSnapshot(callback); + before(function () { + const { getFirestore } = firestoreModular; + firestore = getFirestore(null, SECOND_DATABASE_ID); + }); - await Utils.spyToBeCalledOnceAsync(callback); + beforeEach(async function () { + return await wipe(false, SECOND_DATABASE_ID); + }); - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); + it('throws if no arguments are provided', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, COLLECTION)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('expected at least one argument'); + return Promise.resolve(); + } + }); - it('accepts a single callback function with Error', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot(callback); + it('returns an unsubscribe function', function () { + const { collection, onSnapshot } = firestoreModular; + const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar1`), () => {}); - await Utils.spyToBeCalledOnceAsync(callback); + unsub.should.be.a.Function(); + unsub(); + }); - callback.should.be.calledOnce(); + it('accepts a single callback function with snapshot', async function () { + if (Platform.other) { + return; + } + const { collection, onSnapshot } = firestoreModular; + const callback = sinon.spy(); + const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar2`), callback); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(callback); - describe('multiple callbacks', function () { - if (Platform.other) { - return; - } + callback.should.be.calledOnce(); + callback.args[0][0].constructor.name.should.eql('QuerySnapshot'); + should.equal(callback.args[0][1], null); + unsub(); + }); - it('calls onNext when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firestore.collection(`${COLLECTION}/foo/bar3`).onSnapshot(onNext, onError); + describe('multiple callbacks', function () { + if (Platform.other) { + return; + } - await Utils.spyToBeCalledOnceAsync(onNext); + it('calls onNext when successful', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar3`), onNext, onError); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(onNext); - it('calls onError with Error', async function () { - if (Platform.other) { - return; - } - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot(onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - describe('objects of callbacks', function () { - if (Platform.other) { - return; - } - - it('calls next when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firestore.collection(`${COLLECTION}/foo/bar4`).onSnapshot({ - next: onNext, - error: onError, - }); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - if (Platform.other) { - return; - } - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot({ - next: onNext, - error: onError, - }); + it('calls onError with Error', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(firestore, NO_RULE_COLLECTION), onNext, onError); - await Utils.spyToBeCalledOnceAsync(onError); + await Utils.spyToBeCalledOnceAsync(onError); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); }); + }); - describe('SnapshotListenerOptions + callbacks', function () { - if (Platform.other) { - return; - } - - it('calls callback with snapshot when successful', async function () { - const callback = sinon.spy(); - const unsub = firestore.collection(`${COLLECTION}/foo/bar5`).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); + describe('objects of callbacks', function () { + if (Platform.other) { + return; + } - it('calls callback with Error', async function () { - const callback = sinon.spy(); - const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][1].code.should.containEql('firestore/permission-denied'); - should.equal(callback.args[0][0], null); - unsub(); + it('calls next when successful', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar4`), { + next: onNext, + error: onError, }); - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const colRef = firestore - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`); - const unsub = colRef.onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(onNext); - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - describe('SnapshotListenerOptions + object of callbacks', function () { - if (Platform.other) { - return; - } - - it('calls next with snapshot when successful', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firestore.collection(`${COLLECTION}/foo/bar7`).onSnapshot( - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = firestore.collection(NO_RULE_COLLECTION).onSnapshot( - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + it('calls error with Error', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + next: onNext, + error: onError, }); - }); - - it('throws if SnapshotListenerOptions is invalid', function () { - try { - firestore.collection(NO_RULE_COLLECTION).onSnapshot({ - includeMetadataChanges: 123, - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", - ); - return Promise.resolve(); - } - }); - - it('throws if next callback is invalid', function () { - try { - firestore.collection(NO_RULE_COLLECTION).onSnapshot({ - next: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.next' or 'onNext' expected a function"); - return Promise.resolve(); - } - }); - it('throws if error callback is invalid', function () { - try { - firestore.collection(NO_RULE_COLLECTION).onSnapshot({ - error: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.error' or 'onError' expected a function"); - return Promise.resolve(); - } - }); + await Utils.spyToBeCalledOnceAsync(onError); - // FIXME test disabled due to flakiness in CI E2E tests. - // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. - it('unsubscribes from further updates', async function () { - if (Platform.other) { - return; - } - const callback = sinon.spy(); - - const collection = firestore - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - .collection(`${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`); - - const unsub = collection.onSnapshot(callback); - await Utils.sleep(2000); - await collection.add({}); - await collection.add({}); + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); unsub(); - await Utils.sleep(2000); - await collection.add({}); - await Utils.sleep(2000); - callback.should.be.callCount(3); }); }); - describe('modular', function () { - let firestore; - - before(function () { - const { getFirestore } = firestoreModular; - firestore = getFirestore(null, SECOND_DATABASE_ID); - }); - - beforeEach(async function () { - return await wipe(false, SECOND_DATABASE_ID); - }); - - it('throws if no arguments are provided', function () { - const { collection, onSnapshot } = firestoreModular; - try { - onSnapshot(collection(firestore, COLLECTION)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected at least one argument'); - return Promise.resolve(); - } - }); - - it('returns an unsubscribe function', function () { - const { collection, onSnapshot } = firestoreModular; - const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar1`), () => {}); - - unsub.should.be.a.Function(); - unsub(); - }); + describe('SnapshotListenerOptions + callbacks', function () { + if (Platform.other) { + return; + } - it('accepts a single callback function with snapshot', async function () { - if (Platform.other) { - return; - } + it('calls callback with snapshot when successful', async function () { const { collection, onSnapshot } = firestoreModular; const callback = sinon.spy(); - const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar2`), callback); + const unsub = onSnapshot( + collection(firestore, `${COLLECTION}/foo/bar5`), + { + includeMetadataChanges: false, + }, + callback, + ); await Utils.spyToBeCalledOnceAsync(callback); @@ -411,290 +172,183 @@ describe('Second Database', function () { unsub(); }); - describe('multiple callbacks', function () { + it('calls next with snapshot when successful', async function () { if (Platform.other) { return; } + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const colRef = collection( + firestore, + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`, + ); + const unsub = onSnapshot( + colRef, + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); - it('calls onNext when successful', async function () { - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = onSnapshot( - collection(firestore, `${COLLECTION}/foo/bar3`), - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls onError with Error', async function () { - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = onSnapshot(collection(firestore, NO_RULE_COLLECTION), onNext, onError); - - await Utils.spyToBeCalledOnceAsync(onError); + await Utils.spyToBeCalledOnceAsync(onNext); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - describe('objects of callbacks', function () { + it('calls error with Error', async function () { if (Platform.other) { return; } + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, NO_RULE_COLLECTION), + { + includeMetadataChanges: false, + }, + onNext, + onError, + ); - it('calls next when successful', async function () { - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = onSnapshot(collection(firestore, `${COLLECTION}/foo/bar4`), { - next: onNext, - error: onError, - }); + await Utils.spyToBeCalledOnceAsync(onError); - await Utils.spyToBeCalledOnceAsync(onNext); + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); + }); + }); - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); + describe('SnapshotListenerOptions + object of callbacks', function () { + if (Platform.other) { + return; + } - it('calls error with Error', async function () { - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + it('calls next with snapshot when successful', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, `${COLLECTION}/foo/bar7`), + { + includeMetadataChanges: false, + }, + { next: onNext, error: onError, - }); + }, + ); - await Utils.spyToBeCalledOnceAsync(onError); + await Utils.spyToBeCalledOnceAsync(onNext); - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); + onNext.should.be.calledOnce(); + onError.should.be.callCount(0); + onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); + should.equal(onNext.args[0][1], undefined); + unsub(); }); - describe('SnapshotListenerOptions + callbacks', function () { - if (Platform.other) { - return; - } + it('calls error with Error', async function () { + const { collection, onSnapshot } = firestoreModular; + const onNext = sinon.spy(); + const onError = sinon.spy(); + const unsub = onSnapshot( + collection(firestore, NO_RULE_COLLECTION), + { + includeMetadataChanges: false, + }, + { + next: onNext, + error: onError, + }, + ); - it('calls callback with snapshot when successful', async function () { - const { collection, onSnapshot } = firestoreModular; - const callback = sinon.spy(); - const unsub = onSnapshot( - collection(firestore, `${COLLECTION}/foo/bar5`), - { - includeMetadataChanges: false, - }, - callback, - ); - - await Utils.spyToBeCalledOnceAsync(callback); - - callback.should.be.calledOnce(); - callback.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(callback.args[0][1], null); - unsub(); - }); + await Utils.spyToBeCalledOnceAsync(onError); - it('calls next with snapshot when successful', async function () { - if (Platform.other) { - return; - } - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const colRef = collection( - firestore, - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - `${COLLECTION}/${Utils.randString(12, '#aA')}/next-with-snapshot`, - ); - const unsub = onSnapshot( - colRef, - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); - }); - - it('calls error with Error', async function () { - if (Platform.other) { - return; - } - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = onSnapshot( - collection(firestore, NO_RULE_COLLECTION), - { - includeMetadataChanges: false, - }, - onNext, - onError, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); - }); + onError.should.be.calledOnce(); + onNext.should.be.callCount(0); + onError.args[0][0].code.should.containEql('firestore/permission-denied'); + should.equal(onError.args[0][1], undefined); + unsub(); }); + }); - describe('SnapshotListenerOptions + object of callbacks', function () { - if (Platform.other) { - return; - } - - it('calls next with snapshot when successful', async function () { - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = onSnapshot( - collection(firestore, `${COLLECTION}/foo/bar7`), - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onNext); - - onNext.should.be.calledOnce(); - onError.should.be.callCount(0); - onNext.args[0][0].constructor.name.should.eql('QuerySnapshot'); - should.equal(onNext.args[0][1], undefined); - unsub(); + it('throws if SnapshotListenerOptions is invalid', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + includeMetadataChanges: 123, }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", + ); + return Promise.resolve(); + } + }); - it('calls error with Error', async function () { - const { collection, onSnapshot } = firestoreModular; - const onNext = sinon.spy(); - const onError = sinon.spy(); - const unsub = onSnapshot( - collection(firestore, NO_RULE_COLLECTION), - { - includeMetadataChanges: false, - }, - { - next: onNext, - error: onError, - }, - ); - - await Utils.spyToBeCalledOnceAsync(onError); - - onError.should.be.calledOnce(); - onNext.should.be.callCount(0); - onError.args[0][0].code.should.containEql('firestore/permission-denied'); - should.equal(onError.args[0][1], undefined); - unsub(); + it('throws if next callback is invalid', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + next: 'foo', }); - }); - - it('throws if SnapshotListenerOptions is invalid', function () { - const { collection, onSnapshot } = firestoreModular; - try { - onSnapshot(collection(firestore, NO_RULE_COLLECTION), { - includeMetadataChanges: 123, - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' SnapshotOptions.includeMetadataChanges must be a boolean value", - ); - return Promise.resolve(); - } - }); - - it('throws if next callback is invalid', function () { - const { collection, onSnapshot } = firestoreModular; - try { - onSnapshot(collection(firestore, NO_RULE_COLLECTION), { - next: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.next' or 'onNext' expected a function"); - return Promise.resolve(); - } - }); - - it('throws if error callback is invalid', function () { - const { collection, onSnapshot } = firestoreModular; - try { - onSnapshot(collection(firestore, NO_RULE_COLLECTION), { - error: 'foo', - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'observer.error' or 'onError' expected a function"); - return Promise.resolve(); - } - }); - - // FIXME test disabled due to flakiness in CI E2E tests. - // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. - it('unsubscribes from further updates', async function () { - if (Platform.other) { - return; - } - const { collection, onSnapshot, addDoc } = firestoreModular; - const callback = sinon.spy(); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.next' or 'onNext' expected a function"); + return Promise.resolve(); + } + }); - const collectionRef = collection( - firestore, - // Firestore caches aggressively, even if you wipe the emulator, local documents are cached - // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating - `${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`, - ); + it('throws if error callback is invalid', function () { + const { collection, onSnapshot } = firestoreModular; + try { + onSnapshot(collection(firestore, NO_RULE_COLLECTION), { + error: 'foo', + }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'observer.error' or 'onError' expected a function"); + return Promise.resolve(); + } + }); - const unsub = onSnapshot(collectionRef, callback); - await Utils.sleep(2000); - await addDoc(collectionRef, {}); - await addDoc(collectionRef, {}); - unsub(); - await Utils.sleep(2000); - await addDoc(collectionRef, {}); - await Utils.sleep(2000); - callback.should.be.callCount(3); - }); + // FIXME test disabled due to flakiness in CI E2E tests. + // Registered 4 of 3 expected calls once (!?), 3 of 2 expected calls once. + it('unsubscribes from further updates', async function () { + if (Platform.other) { + return; + } + const { collection, onSnapshot, addDoc } = firestoreModular; + const callback = sinon.spy(); + + const collectionRef = collection( + firestore, + // Firestore caches aggressively, even if you wipe the emulator, local documents are cached + // between runs, so use random collections to make sure `tests:*:test-reuse` works while iterating + `${COLLECTION}/${Utils.randString(12, '#aA')}/unsubscribe-updates`, + ); + + const unsub = onSnapshot(collectionRef, callback); + await Utils.sleep(2000); + await addDoc(collectionRef, {}); + await addDoc(collectionRef, {}); + unsub(); + await Utils.sleep(2000); + await addDoc(collectionRef, {}); + await Utils.sleep(2000); + callback.should.be.callCount(3); }); }); }); diff --git a/packages/firestore/e2e/SecondDatabase/second.where.e2e.js b/packages/firestore/e2e/SecondDatabase/second.where.e2e.js index f8aa7da354..7ca0625705 100644 --- a/packages/firestore/e2e/SecondDatabase/second.where.e2e.js +++ b/packages/firestore/e2e/SecondDatabase/second.where.e2e.js @@ -21,1080 +21,560 @@ const COLLECTION = 'second-database'; const SECOND_DATABASE_ID = 'second-rnfb'; describe('Second Database', function () { - describe('firestore().collection().where()', function () { - describe('v8 compatibility', function () { - let firestore; - - beforeEach(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - firestore = firebase.app().firestore(SECOND_DATABASE_ID); - return await wipe(false, SECOND_DATABASE_ID); - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if fieldPath is invalid', function () { - try { - firestore.collection(COLLECTION).where(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'must be a string, instance of FieldPath or instance of Filter', - ); - return Promise.resolve(); - } - }); - - it('throws if fieldPath string is invalid', function () { - try { - firestore.collection(COLLECTION).where('.foo.bar'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } - }); - - it('throws if operator string is invalid', function () { - try { - firestore.collection(COLLECTION).where('foo.bar', '!'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); - - it('throws if query contains multiple array-contains', function () { - try { - firestore - .collection(COLLECTION) - .where('foo.bar', 'array-contains', 123) - .where('foo.bar', 'array-contains', 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); - - it('throws if value is not defined', function () { - try { - firestore.collection(COLLECTION).where('foo.bar', 'array-contains'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); - - it('throws if null value and no equal operator', function () { - try { - firestore.collection(COLLECTION).where('foo.bar', 'array-contains', null); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); - - it('allows null to be used with equal operator', function () { - firestore.collection(COLLECTION).where('foo.bar', '==', null); - }); - - it('allows null to be used with not equal operator', function () { - firestore.collection(COLLECTION).where('foo.bar', '!=', null); - }); - - it('allows inequality on the same path', function () { - firestore - .collection(COLLECTION) - .where('foo.bar', '>', 123) - .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1234); - }); - - it('throws if in query with no array value', function () { - try { - firestore.collection(COLLECTION).where('foo.bar', 'in', '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if array-contains-any query with no array value', function () { - try { - firestore.collection(COLLECTION).where('foo.bar', 'array-contains-any', '123'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if in query array length is greater than 30', function () { - try { - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - firestore.collection(COLLECTION).where('foo.bar', 'in', queryArray); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 30 elements in the value'); - return Promise.resolve(); - } - }); - - it('throws if query has multiple array-contains-any filter', function () { - try { - firestore - .collection(COLLECTION) - .where('foo.bar', 'array-contains-any', [1]) - .where('foo.bar', 'array-contains-any', [2]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use more than one 'array-contains-any' filter", - ); - return Promise.resolve(); - } - }); - - /* Queries */ - - it('returns with where equal filter', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/equal`); - - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '==', search).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search); - }); - }); - - it('returns with where greater than filter', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/greater`); - - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search - 1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '>', search).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search + 1234); - }); - }); - - it('returns with where greater than or equal filter', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/greaterequal`); - - const search = Date.now(); - await Promise.all([ - colRef.add({ foo: search - 1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '>=', search).get(); - - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.aboveOrEqual(search); - }); - }); - - it('returns with where less than filter', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/less`); - - const search = -Date.now(); - await Promise.all([ - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search }), - ]); - - const snapshot = await colRef.where('foo', '<', search).get(); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.be.below(search); - }); - }); - - it('returns with where less than or equal filter', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/lessequal`); + describe('modular', function () { + let firestore; - const search = -Date.now(); - await Promise.all([ - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search + -1234 }), - colRef.add({ foo: search }), - colRef.add({ foo: search + 1234 }), - ]); - - const snapshot = await colRef.where('foo', '<=', search).get(); - - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.belowOrEqual(search); - }); - }); - - it('returns when combining greater than and lesser than on the same nested field', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); - - const snapshot = await colRef - .where('foo.bar', '>', 1) - .where('foo.bar', '<', 3) - .orderBy('foo.bar') - .get(); - - snapshot.size.should.eql(1); - }); - - it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - colRef.doc('doc1').set({ foo: { bar: 1 } }), - colRef.doc('doc2').set({ foo: { bar: 2 } }), - colRef.doc('doc3').set({ foo: { bar: 3 } }), - ]); - - const snapshot = await colRef - .where(new firebase.firestore.FieldPath('foo', 'bar'), '>', 1) - .where(new firebase.firestore.FieldPath('foo', 'bar'), '<', 3) - .orderBy(new firebase.firestore.FieldPath('foo', 'bar')) - .get(); - - snapshot.size.should.eql(1); - }); - - it('returns with where array-contains filter', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/array-contains`); - - const match = Date.now(); - await Promise.all([ - colRef.add({ foo: [1, '2', match] }), - colRef.add({ foo: [1, '2', match.toString()] }), - colRef.add({ foo: [1, '2', match.toString()] }), - ]); - - const snapshot = await colRef.where('foo', 'array-contains', match.toString()).get(); - const expected = [1, '2', match.toString()]; - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); - - it('returns with in filter', async function () { - const colRef = firestore.collection(`${COLLECTION}/filter/in${Date.now() + ''}`); - - await Promise.all([ - colRef.add({ status: 'Ordered' }), - colRef.add({ status: 'Ready to Ship' }), - colRef.add({ status: 'Ready to Ship' }), - colRef.add({ status: 'Incomplete' }), - ]); - - const expect = ['Ready to Ship', 'Ordered']; - const snapshot = await colRef.where('status', 'in', expect).get(); - snapshot.size.should.eql(3); + before(function () { + const { getFirestore } = firestoreModular; + firestore = getFirestore(null, SECOND_DATABASE_ID); + }); - snapshot.forEach(s => { - s.data().status.should.equalOneOf(...expect); - }); - }); + beforeEach(async function () { + return await wipe(false, SECOND_DATABASE_ID); + }); - it('returns with array-contains-any filter', async function () { - const colRef = firestore.collection( - `${COLLECTION}/filter/array-contains-any${Date.now() + ''}`, + it('throws if fieldPath is invalid', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where(123)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'must be a string, instance of FieldPath or instance of Filter', ); + return Promise.resolve(); + } + }); - await Promise.all([ - colRef.add({ category: ['Appliances', 'Housewares', 'Cooking'] }), - colRef.add({ category: ['Appliances', 'Electronics', 'Nursery'] }), - colRef.add({ category: ['Audio/Video', 'Electronics'] }), - colRef.add({ category: ['Beauty'] }), - ]); + it('throws if fieldPath string is invalid', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('.foo.bar')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'fieldPath' Invalid field path"); + return Promise.resolve(); + } + }); - const expect = ['Appliances', 'Electronics']; - const snapshot = await colRef.where('category', 'array-contains-any', expect).get(); - snapshot.size.should.eql(3); // 2nd record should only be returned once - }); + it('throws if operator string is invalid', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', '!')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'opStr' is invalid"); + return Promise.resolve(); + } + }); - it('returns with a FieldPath', async function () { - const colRef = firestore.collection( - `${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`, + it('throws if query contains multiple array-contains', function () { + const { collection, query, where } = firestoreModular; + try { + query( + collection(firestore, COLLECTION), + where('foo.bar', 'array-contains', 123), + where('foo.bar', 'array-contains', 123), ); - const fieldPath = new firebase.firestore.FieldPath('map', 'foo.bar@gmail.com'); - - await colRef.add({ - map: { - 'foo.bar@gmail.com': true, - }, - }); - await colRef.add({ - map: { - 'bar.foo@gmail.com': true, - }, - }); - - const snapshot = await colRef.where(fieldPath, '==', true).get(); - snapshot.size.should.eql(1); // 2nd record should only be returned once - const data = snapshot.docs[0].data(); - should.equal(data.map['foo.bar@gmail.com'], true); - }); - - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - try { - const { documentId } = firestoreModular; - firestore - .collection(COLLECTION) - .where(documentId(), 'in', ['document-id']) - .orderBy('differentOrderBy', 'desc'); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('Queries only support a single array-contains filter'); + return Promise.resolve(); + } + }); - it('should correctly query integer values with in operator', async function () { - const ref = firestore.collection(`${COLLECTION}/filter/int-in${Date.now() + ''}`); + it('throws if value is not defined', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'value' argument expected"); + return Promise.resolve(); + } + }); - await ref.add({ status: 1 }); + it('throws if null value and no equal operator', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains', null)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('You can only perform equals comparisons on null'); + return Promise.resolve(); + } + }); - const items = []; - await ref - .where('status', 'in', [1, 2]) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); + it('allows null to be used with equal operator', function () { + const { collection, query, where } = firestoreModular; + query(collection(firestore, COLLECTION), where('foo.bar', '==', null)); + }); - items.length.should.equal(1); - }); + it('allows null to be used with not equal operator', function () { + const { collection, query, where } = firestoreModular; + query(collection(firestore, COLLECTION), where('foo.bar', '!=', null)); + }); - it('should correctly query integer values with array-contains operator', async function () { - const ref = firestore.collection( - `${COLLECTION}/filter/int-array-contains${Date.now() + ''}`, - ); + it('allows inequality on the same path', function () { + const { collection, query, where, FieldPath } = firestoreModular; + query( + collection(firestore, COLLECTION), + where('foo.bar', '>', 123), + where(new FieldPath('foo', 'bar'), '>', 1234), + ); + }); - await ref.add({ status: [1, 2, 3] }); + it('throws if in query with no array value', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'in', '123')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - const items = []; - await ref - .where('status', 'array-contains', 2) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); + it('throws if array-contains-any query with no array value', function () { + const { collection, query, where } = firestoreModular; + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains-any', '123')); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('A non-empty array is required'); + return Promise.resolve(); + } + }); - items.length.should.equal(1); - }); + it('throws if in query array length is greater than 30', function () { + const { collection, query, where } = firestoreModular; + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + + try { + query(collection(firestore, COLLECTION), where('foo.bar', 'in', queryArray)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql('maximum of 30 elements in the value'); + return Promise.resolve(); + } + }); - it("should correctly retrieve data when using 'not-in' operator", async function () { - const ref = firestore.collection(`${COLLECTION}/filter/not-in${Date.now() + ''}`); + it('throws if query has multiple array-contains-any filter', function () { + const { collection, query, where } = firestoreModular; + try { + query( + collection(firestore, COLLECTION), + where('foo.bar', 'array-contains-any', [1]), + where('foo.bar', 'array-contains-any', [2]), + ); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'array-contains-any' filter"); + return Promise.resolve(); + } + }); - await Promise.all([ref.add({ notIn: 'here' }), ref.add({ notIn: 'now' })]); + /* Queries */ - const result = await ref.where('notIn', 'not-in', ['here', 'there', 'everywhere']).get(); - should(result.docs.length).equal(1); - should(result.docs[0].data().notIn).equal('now'); - }); + it('returns with where equal filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/equal`); - it("should throw error when using 'not-in' operator twice", async function () { - const ref = firestore.collection(COLLECTION); + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + ]); - try { - ref.where('test', 'not-in', [1]).where('test', 'not-in', [2]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); + const snapshot = await getDocs(query(colRef, where('foo', '==', search))); - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const ref = firestore.collection(COLLECTION); - - try { - ref.where('test', '!=', 1).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); - return Promise.resolve(); - } + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search); }); + }); - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const ref = firestore.collection(COLLECTION); + it('returns with where greater than filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greater`); - try { - ref.where('test', 'in', [2]).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search - 1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + addDoc(colRef, { foo: search + 1234 }), + ]); - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const ref = firestore.collection(COLLECTION); - - try { - ref.where('test', 'array-contains-any', [2]).where('test', 'not-in', [1]); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); + const snapshot = await getDocs(query(colRef, where('foo', '>', search))); - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const ref = firestore.collection(COLLECTION); - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - try { - ref.where('test', 'not-in', queryArray); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 30 elements in the value array.', - ); - return Promise.resolve(); - } + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(search + 1234); }); + }); - it("should correctly retrieve data when using '!=' operator", async function () { - const ref = firestore.collection(`${COLLECTION}/filter/bang-equals${Date.now() + ''}`); - - await Promise.all([ref.add({ notEqual: 'here' }), ref.add({ notEqual: 'now' })]); - - const result = await ref.where('notEqual', '!=', 'here').get(); + it('returns with where greater than or equal filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greaterequal`); - should(result.docs.length).equal(1); - should(result.docs[0].data().notEqual).equal('now'); - }); + const search = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search - 1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + addDoc(colRef, { foo: search + 1234 }), + ]); - it("should throw error when using '!=' operator twice ", async function () { - const ref = firestore.collection(COLLECTION); + const snapshot = await getDocs(query(colRef, where('foo', '>=', search))); - try { - ref.where('test', '!=', 1).where('test', '!=', 2); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.aboveOrEqual(search); }); + }); - it('should handle where clause after sort by', async function () { - const ref = firestore.collection(`${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); + it('returns with where less than filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/less`); - await ref.add({ status: 1 }); - await ref.add({ status: 2 }); - await ref.add({ status: 3 }); + const search = -Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search }), + ]); - const items = []; - await ref - .orderBy('status', 'desc') - .where('status', '<=', 2) - .get() - .then($ => $.forEach(doc => items.push(doc.data()))); + const snapshot = await getDocs(query(colRef, where('foo', '<', search))); - items.length.should.equal(2); - items[0].status.should.equal(2); - items[1].status.should.equal(1); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.be.below(search); }); }); - describe('modular', function () { - let firestore; - - before(function () { - const { getFirestore } = firestoreModular; - firestore = getFirestore(null, SECOND_DATABASE_ID); - }); + it('returns with where less than or equal filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/lessequal`); - beforeEach(async function () { - return await wipe(false, SECOND_DATABASE_ID); - }); + const search = -Date.now(); + await Promise.all([ + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search + -1234 }), + addDoc(colRef, { foo: search }), + addDoc(colRef, { foo: search + 1234 }), + ]); - it('throws if fieldPath is invalid', function () { - const { collection, query, where } = firestoreModular; - try { - query(collection(firestore, COLLECTION), where(123)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'must be a string, instance of FieldPath or instance of Filter', - ); - return Promise.resolve(); - } - }); + const snapshot = await getDocs(query(colRef, where('foo', '<=', search))); - it('throws if fieldPath string is invalid', function () { - const { collection, query, where } = firestoreModular; - try { - query(collection(firestore, COLLECTION), where('.foo.bar')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'fieldPath' Invalid field path"); - return Promise.resolve(); - } + snapshot.size.should.eql(3); + snapshot.forEach(s => { + s.data().foo.should.be.belowOrEqual(search); }); + }); - it('throws if operator string is invalid', function () { - const { collection, query, where } = firestoreModular; - try { - query(collection(firestore, COLLECTION), where('foo.bar', '!')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'opStr' is invalid"); - return Promise.resolve(); - } - }); + it('returns when combining greater than and lesser than on the same nested field', async function () { + const { collection, doc, setDoc, query, where, orderBy, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greaterandless`); - it('throws if query contains multiple array-contains', function () { - const { collection, query, where } = firestoreModular; - try { - query( - collection(firestore, COLLECTION), - where('foo.bar', 'array-contains', 123), - where('foo.bar', 'array-contains', 123), - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Queries only support a single array-contains filter'); - return Promise.resolve(); - } - }); + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); - it('throws if value is not defined', function () { - const { collection, query, where } = firestoreModular; - try { - query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'value' argument expected"); - return Promise.resolve(); - } - }); + const snapshot = await getDocs( + query(colRef, where('foo.bar', '>', 1), where('foo.bar', '<', 3), orderBy('foo.bar')), + ); - it('throws if null value and no equal operator', function () { - const { collection, query, where } = firestoreModular; - try { - query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains', null)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('You can only perform equals comparisons on null'); - return Promise.resolve(); - } - }); + snapshot.size.should.eql(1); + }); - it('allows null to be used with equal operator', function () { - const { collection, query, where } = firestoreModular; - query(collection(firestore, COLLECTION), where('foo.bar', '==', null)); - }); + it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { + const { collection, doc, setDoc, query, where, getDocs, orderBy, FieldPath } = + firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/greaterandless`); - it('allows null to be used with not equal operator', function () { - const { collection, query, where } = firestoreModular; - query(collection(firestore, COLLECTION), where('foo.bar', '!=', null)); - }); + await Promise.all([ + setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), + setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), + setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), + ]); - it('allows inequality on the same path', function () { - const { collection, query, where, FieldPath } = firestoreModular; + const snapshot = await getDocs( query( - collection(firestore, COLLECTION), - where('foo.bar', '>', 123), - where(new FieldPath('foo', 'bar'), '>', 1234), - ); - }); - - it('throws if in query with no array value', function () { - const { collection, query, where } = firestoreModular; - try { - query(collection(firestore, COLLECTION), where('foo.bar', 'in', '123')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if array-contains-any query with no array value', function () { - const { collection, query, where } = firestoreModular; - try { - query(collection(firestore, COLLECTION), where('foo.bar', 'array-contains-any', '123')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('A non-empty array is required'); - return Promise.resolve(); - } - }); - - it('throws if in query array length is greater than 30', function () { - const { collection, query, where } = firestoreModular; - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - try { - query(collection(firestore, COLLECTION), where('foo.bar', 'in', queryArray)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('maximum of 30 elements in the value'); - return Promise.resolve(); - } - }); - - it('throws if query has multiple array-contains-any filter', function () { - const { collection, query, where } = firestoreModular; - try { - query( - collection(firestore, COLLECTION), - where('foo.bar', 'array-contains-any', [1]), - where('foo.bar', 'array-contains-any', [2]), - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use more than one 'array-contains-any' filter", - ); - return Promise.resolve(); - } - }); - - /* Queries */ - - it('returns with where equal filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/equal`); - - const search = Date.now(); - await Promise.all([ - addDoc(colRef, { foo: search }), - addDoc(colRef, { foo: search }), - addDoc(colRef, { foo: search + 1234 }), - ]); - - const snapshot = await getDocs(query(colRef, where('foo', '==', search))); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search); - }); - }); - - it('returns with where greater than filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/greater`); - - const search = Date.now(); - await Promise.all([ - addDoc(colRef, { foo: search - 1234 }), - addDoc(colRef, { foo: search }), - addDoc(colRef, { foo: search + 1234 }), - addDoc(colRef, { foo: search + 1234 }), - ]); - - const snapshot = await getDocs(query(colRef, where('foo', '>', search))); - - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(search + 1234); - }); - }); - - it('returns with where greater than or equal filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/greaterequal`); - - const search = Date.now(); - await Promise.all([ - addDoc(colRef, { foo: search - 1234 }), - addDoc(colRef, { foo: search }), - addDoc(colRef, { foo: search + 1234 }), - addDoc(colRef, { foo: search + 1234 }), - ]); - - const snapshot = await getDocs(query(colRef, where('foo', '>=', search))); - - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.aboveOrEqual(search); - }); - }); + colRef, + where(new FieldPath('foo', 'bar'), '>', 1), + where(new FieldPath('foo', 'bar'), '<', 3), + orderBy(new FieldPath('foo', 'bar')), + ), + ); + + snapshot.size.should.eql(1); + }); - it('returns with where less than filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/less`); + it('returns with where array-contains filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/array-contains`); - const search = -Date.now(); - await Promise.all([ - addDoc(colRef, { foo: search + -1234 }), - addDoc(colRef, { foo: search + -1234 }), - addDoc(colRef, { foo: search }), - ]); + const match = Date.now(); + await Promise.all([ + addDoc(colRef, { foo: [1, '2', match] }), + addDoc(colRef, { foo: [1, '2', match.toString()] }), + addDoc(colRef, { foo: [1, '2', match.toString()] }), + ]); - const snapshot = await getDocs(query(colRef, where('foo', '<', search))); + const snapshot = await getDocs( + query(colRef, where('foo', 'array-contains', match.toString())), + ); + const expected = [1, '2', match.toString()]; - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.be.below(search); - }); + snapshot.size.should.eql(2); + snapshot.forEach(s => { + s.data().foo.should.eql(jet.contextify(expected)); }); + }); - it('returns with where less than or equal filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/lessequal`); + it('returns with in filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection(firestore, `${COLLECTION}/filter/in${Date.now() + ''}`); - const search = -Date.now(); - await Promise.all([ - addDoc(colRef, { foo: search + -1234 }), - addDoc(colRef, { foo: search + -1234 }), - addDoc(colRef, { foo: search }), - addDoc(colRef, { foo: search + 1234 }), - ]); + await Promise.all([ + addDoc(colRef, { status: 'Ordered' }), + addDoc(colRef, { status: 'Ready to Ship' }), + addDoc(colRef, { status: 'Ready to Ship' }), + addDoc(colRef, { status: 'Incomplete' }), + ]); - const snapshot = await getDocs(query(colRef, where('foo', '<=', search))); + const expect = ['Ready to Ship', 'Ordered']; + const snapshot = await getDocs(query(colRef, where('status', 'in', expect))); + snapshot.size.should.eql(3); - snapshot.size.should.eql(3); - snapshot.forEach(s => { - s.data().foo.should.be.belowOrEqual(search); - }); + snapshot.forEach(s => { + s.data().status.should.equalOneOf(...expect); }); + }); - it('returns when combining greater than and lesser than on the same nested field', async function () { - const { collection, doc, setDoc, query, where, orderBy, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), - setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), - setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), - ]); - - const snapshot = await getDocs( - query(colRef, where('foo.bar', '>', 1), where('foo.bar', '<', 3), orderBy('foo.bar')), - ); + it('returns with array-contains-any filter', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const colRef = collection( + firestore, + `${COLLECTION}/filter/array-contains-any${Date.now() + ''}`, + ); + + await Promise.all([ + addDoc(colRef, { category: ['Appliances', 'Housewares', 'Cooking'] }), + addDoc(colRef, { category: ['Appliances', 'Electronics', 'Nursery'] }), + addDoc(colRef, { category: ['Audio/Video', 'Electronics'] }), + addDoc(colRef, { category: ['Beauty'] }), + ]); + + const expect = ['Appliances', 'Electronics']; + const snapshot = await getDocs( + query(colRef, where('category', 'array-contains-any', expect)), + ); + snapshot.size.should.eql(3); // 2nd record should only be returned once + }); - snapshot.size.should.eql(1); - }); + it('returns with a FieldPath', async function () { + const { collection, addDoc, query, where, getDocs, FieldPath } = firestoreModular; + const colRef = collection( + firestore, + `${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`, + ); + const fieldPath = new FieldPath('map', 'foo.bar@gmail.com'); + + await addDoc(colRef, { + map: { + 'foo.bar@gmail.com': true, + }, + }); + await addDoc(colRef, { + map: { + 'bar.foo@gmail.com': true, + }, + }); + + const snapshot = await getDocs(query(colRef, where(fieldPath, '==', true))); + snapshot.size.should.eql(1); // 2nd record should only be returned once + const data = snapshot.docs[0].data(); + should.equal(data.map['foo.bar@gmail.com'], true); + }); - it('returns when combining greater than and lesser than on the same nested field using FieldPath', async function () { - const { collection, doc, setDoc, query, where, getDocs, orderBy, FieldPath } = - firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/greaterandless`); - - await Promise.all([ - setDoc(doc(colRef, 'doc1'), { foo: { bar: 1 } }), - setDoc(doc(colRef, 'doc2'), { foo: { bar: 2 } }), - setDoc(doc(colRef, 'doc3'), { foo: { bar: 3 } }), - ]); - - const snapshot = await getDocs( - query( - colRef, - where(new FieldPath('foo', 'bar'), '>', 1), - where(new FieldPath('foo', 'bar'), '<', 3), - orderBy(new FieldPath('foo', 'bar')), - ), + it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { + const { collection, query, where, orderBy, documentId } = firestoreModular; + try { + query( + collection(firestore, COLLECTION), + where(documentId(), 'in', ['document-id']), + orderBy('differentOrderBy', 'desc'), ); - snapshot.size.should.eql(1); - }); - - it('returns with where array-contains filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/array-contains`); - - const match = Date.now(); - await Promise.all([ - addDoc(colRef, { foo: [1, '2', match] }), - addDoc(colRef, { foo: [1, '2', match.toString()] }), - addDoc(colRef, { foo: [1, '2', match.toString()] }), - ]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); + return Promise.resolve(); + } + }); - const snapshot = await getDocs( - query(colRef, where('foo', 'array-contains', match.toString())), - ); - const expected = [1, '2', match.toString()]; + it('should correctly query integer values with in operator', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/int-in${Date.now() + ''}`); - snapshot.size.should.eql(2); - snapshot.forEach(s => { - s.data().foo.should.eql(jet.contextify(expected)); - }); - }); + await addDoc(ref, { status: 1 }); - it('returns with in filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection(firestore, `${COLLECTION}/filter/in${Date.now() + ''}`); + const items = []; + await getDocs(query(ref, where('status', 'in', [1, 2]))).then($ => + $.forEach(doc => items.push(doc.data())), + ); - await Promise.all([ - addDoc(colRef, { status: 'Ordered' }), - addDoc(colRef, { status: 'Ready to Ship' }), - addDoc(colRef, { status: 'Ready to Ship' }), - addDoc(colRef, { status: 'Incomplete' }), - ]); + items.length.should.equal(1); + }); - const expect = ['Ready to Ship', 'Ordered']; - const snapshot = await getDocs(query(colRef, where('status', 'in', expect))); - snapshot.size.should.eql(3); + it('should correctly query integer values with array-contains operator', async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection( + firestore, + `${COLLECTION}/filter/int-array-contains${Date.now() + ''}`, + ); - snapshot.forEach(s => { - s.data().status.should.equalOneOf(...expect); - }); - }); + await addDoc(ref, { status: [1, 2, 3] }); - it('returns with array-contains-any filter', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const colRef = collection( - firestore, - `${COLLECTION}/filter/array-contains-any${Date.now() + ''}`, - ); + const items = []; + await getDocs(query(ref, where('status', 'array-contains', 2))).then($ => + $.forEach(doc => items.push(doc.data())), + ); - await Promise.all([ - addDoc(colRef, { category: ['Appliances', 'Housewares', 'Cooking'] }), - addDoc(colRef, { category: ['Appliances', 'Electronics', 'Nursery'] }), - addDoc(colRef, { category: ['Audio/Video', 'Electronics'] }), - addDoc(colRef, { category: ['Beauty'] }), - ]); + items.length.should.equal(1); + }); - const expect = ['Appliances', 'Electronics']; - const snapshot = await getDocs( - query(colRef, where('category', 'array-contains-any', expect)), - ); - snapshot.size.should.eql(3); // 2nd record should only be returned once - }); + it("should correctly retrieve data when using 'not-in' operator", async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/not-in${Date.now() + ''}`); - it('returns with a FieldPath', async function () { - const { collection, addDoc, query, where, getDocs, FieldPath } = firestoreModular; - const colRef = collection( - firestore, - `${COLLECTION}/filter/where-fieldpath${Date.now() + ''}`, - ); - const fieldPath = new FieldPath('map', 'foo.bar@gmail.com'); - - await addDoc(colRef, { - map: { - 'foo.bar@gmail.com': true, - }, - }); - await addDoc(colRef, { - map: { - 'bar.foo@gmail.com': true, - }, - }); - - const snapshot = await getDocs(query(colRef, where(fieldPath, '==', true))); - snapshot.size.should.eql(1); // 2nd record should only be returned once - const data = snapshot.docs[0].data(); - should.equal(data.map['foo.bar@gmail.com'], true); - }); + await Promise.all([addDoc(ref, { notIn: 'here' }), addDoc(ref, { notIn: 'now' })]); - it('should throw an error if you use a FieldPath on a filter in conjunction with an orderBy() parameter that is not FieldPath', async function () { - const { collection, query, where, orderBy, documentId } = firestoreModular; - try { - query( - collection(firestore, COLLECTION), - where(documentId(), 'in', ['document-id']), - orderBy('differentOrderBy', 'desc'), - ); - - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'FirestoreFieldPath' cannot be used in conjunction"); - return Promise.resolve(); - } - }); + const result = await getDocs( + query(ref, where('notIn', 'not-in', ['here', 'there', 'everywhere'])), + ); + should(result.docs.length).equal(1); + should(result.docs[0].data().notIn).equal('now'); + }); - it('should correctly query integer values with in operator', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const ref = collection(firestore, `${COLLECTION}/filter/int-in${Date.now() + ''}`); + it("should throw error when using 'not-in' operator twice", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', 'not-in', [1]), where('test', 'not-in', [2])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); - await addDoc(ref, { status: 1 }); + it("should throw error when combining 'not-in' operator with '!=' operator", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); - const items = []; - await getDocs(query(ref, where('status', 'in', [1, 2]))).then($ => - $.forEach(doc => items.push(doc.data())), + try { + query(ref, where('test', 'not-in', [1]), where('test', '!=', 1)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", ); + return Promise.resolve(); + } + }); - items.length.should.equal(1); - }); - - it('should correctly query integer values with array-contains operator', async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const ref = collection( - firestore, - `${COLLECTION}/filter/int-array-contains${Date.now() + ''}`, - ); + it("should throw error when combining 'not-in' operator with 'in' operator", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', 'in', [2]), where('test', 'not-in', [1])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); - await addDoc(ref, { status: [1, 2, 3] }); + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); - const items = []; - await getDocs(query(ref, where('status', 'array-contains', 2))).then($ => - $.forEach(doc => items.push(doc.data())), + try { + query(ref, where('test', 'array-contains-any', [2]), where('test', 'not-in', [1])); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", ); + return Promise.resolve(); + } + }); - items.length.should.equal(1); - }); - - it("should correctly retrieve data when using 'not-in' operator", async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const ref = collection(firestore, `${COLLECTION}/filter/not-in${Date.now() + ''}`); - - await Promise.all([addDoc(ref, { notIn: 'here' }), addDoc(ref, { notIn: 'now' })]); - - const result = await getDocs( - query(ref, where('notIn', 'not-in', ['here', 'there', 'everywhere'])), + it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); + + try { + query(ref, where('test', 'not-in', queryArray)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 30 elements in the value array.', ); - should(result.docs.length).equal(1); - should(result.docs[0].data().notIn).equal('now'); - }); - - it("should throw error when using 'not-in' operator twice", async function () { - const { collection, query, where } = firestoreModular; - const ref = collection(firestore, COLLECTION); - - try { - query(ref, where('test', 'not-in', [1]), where('test', 'not-in', [2])); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one 'not-in' filter."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with '!=' operator", async function () { - const { collection, query, where } = firestoreModular; - const ref = collection(firestore, COLLECTION); - - try { - query(ref, where('test', 'not-in', [1]), where('test', '!=', 1)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with '!=' inequality filters", - ); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'in' operator", async function () { - const { collection, query, where } = firestoreModular; - const ref = collection(firestore, COLLECTION); - - try { - query(ref, where('test', 'in', [2]), where('test', 'not-in', [1])); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); - return Promise.resolve(); - } - }); - - it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async function () { - const { collection, query, where } = firestoreModular; - const ref = collection(firestore, COLLECTION); - - try { - query(ref, where('test', 'array-contains-any', [2]), where('test', 'not-in', [1])); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "You cannot use 'not-in' filters with 'array-contains-any' filters.", - ); - return Promise.resolve(); - } - }); - - it("should throw error when 'not-in' filter has a list of more than 10 items", async function () { - const { collection, query, where } = firestoreModular; - const ref = collection(firestore, COLLECTION); - const queryArray = Array.from({ length: 31 }, (_, i) => i + 1); - - try { - query(ref, where('test', 'not-in', queryArray)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - 'filters support a maximum of 30 elements in the value array.', - ); - return Promise.resolve(); - } - }); + return Promise.resolve(); + } + }); - it("should correctly retrieve data when using '!=' operator", async function () { - const { collection, addDoc, query, where, getDocs } = firestoreModular; - const ref = collection(firestore, `${COLLECTION}/filter/bang-equals${Date.now() + ''}`); + it("should correctly retrieve data when using '!=' operator", async function () { + const { collection, addDoc, query, where, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/bang-equals${Date.now() + ''}`); - await Promise.all([addDoc(ref, { notEqual: 'here' }), addDoc(ref, { notEqual: 'now' })]); + await Promise.all([addDoc(ref, { notEqual: 'here' }), addDoc(ref, { notEqual: 'now' })]); - const result = await getDocs(query(ref, where('notEqual', '!=', 'here'))); + const result = await getDocs(query(ref, where('notEqual', '!=', 'here'))); - should(result.docs.length).equal(1); - should(result.docs[0].data().notEqual).equal('now'); - }); + should(result.docs.length).equal(1); + should(result.docs[0].data().notEqual).equal('now'); + }); - it("should throw error when using '!=' operator twice ", async function () { - const { collection, query, where } = firestoreModular; - const ref = collection(firestore, COLLECTION); - - try { - query(ref, where('test', '!=', 1), where('test', '!=', 2)); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("You cannot use more than one '!=' inequality filter."); - return Promise.resolve(); - } - }); + it("should throw error when using '!=' operator twice ", async function () { + const { collection, query, where } = firestoreModular; + const ref = collection(firestore, COLLECTION); + + try { + query(ref, where('test', '!=', 1), where('test', '!=', 2)); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); - it('should handle where clause after sort by', async function () { - const { collection, addDoc, query, where, orderBy, getDocs } = firestoreModular; - const ref = collection(firestore, `${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); + it('should handle where clause after sort by', async function () { + const { collection, addDoc, query, where, orderBy, getDocs } = firestoreModular; + const ref = collection(firestore, `${COLLECTION}/filter/sort-by-where${Date.now() + ''}`); - await addDoc(ref, { status: 1 }); - await addDoc(ref, { status: 2 }); - await addDoc(ref, { status: 3 }); + await addDoc(ref, { status: 1 }); + await addDoc(ref, { status: 2 }); + await addDoc(ref, { status: 3 }); - const items = []; - await getDocs(query(ref, orderBy('status', 'desc'), where('status', '<=', 2))).then($ => - $.forEach(doc => items.push(doc.data())), - ); + const items = []; + await getDocs(query(ref, orderBy('status', 'desc'), where('status', '<=', 2))).then($ => + $.forEach(doc => items.push(doc.data())), + ); - items.length.should.equal(2); - items[0].status.should.equal(2); - items[1].status.should.equal(1); - }); + items.length.should.equal(2); + items[0].status.should.equal(2); + items[1].status.should.equal(1); }); }); }); diff --git a/packages/firestore/e2e/SnapshotMetadata.e2e.js b/packages/firestore/e2e/SnapshotMetadata.e2e.js index f0657f1bb6..cedaf5895f 100644 --- a/packages/firestore/e2e/SnapshotMetadata.e2e.js +++ b/packages/firestore/e2e/SnapshotMetadata.e2e.js @@ -22,81 +22,6 @@ describe('firestore.SnapshotMetadata', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('.fromCache -> returns a boolean', async function () { - const ref1 = firebase.firestore().collection(COLLECTION); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - const colRef = await ref1.get(); - const docRef = await ref2.get(); - colRef.metadata.fromCache.should.be.Boolean(); - docRef.metadata.fromCache.should.be.Boolean(); - }); - - it('.hasPendingWrites -> returns a boolean', async function () { - const ref1 = firebase.firestore().collection(COLLECTION); - const ref2 = firebase.firestore().doc(`${COLLECTION}/idonotexist`); - const colRef = await ref1.get(); - const docRef = await ref2.get(); - colRef.metadata.hasPendingWrites.should.be.Boolean(); - docRef.metadata.hasPendingWrites.should.be.Boolean(); - }); - - describe('isEqual()', function () { - it('throws if other is not a valid type', async function () { - try { - const snapshot = await firebase.firestore().collection(COLLECTION).get(); - snapshot.metadata.isEqual(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected instance of SnapshotMetadata"); - return Promise.resolve(); - } - }); - - it('returns true if is equal', async function () { - if (Platform.other) { - return; - } - - const snapshot1 = await firebase - .firestore() - .collection(COLLECTION) - .get({ source: 'cache' }); - const snapshot2 = await firebase - .firestore() - .collection(COLLECTION) - .get({ source: 'cache' }); - snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(true); - }); - - it('returns false if not equal', async function () { - if (Platform.other) { - return; - } - - const snapshot1 = await firebase - .firestore() - .collection(COLLECTION) - .get({ source: 'cache' }); - const snapshot2 = await firebase - .firestore() - .collection(COLLECTION) - .get({ source: 'server' }); - snapshot1.metadata.isEqual(snapshot2.metadata).should.eql(false); - }); - }); - }); - describe('modular', function () { it('.fromCache -> returns a boolean', async function () { const { getFirestore, collection, doc, getDocs, getDoc } = firestoreModular; diff --git a/packages/firestore/e2e/Timestamp.e2e.js b/packages/firestore/e2e/Timestamp.e2e.js index 946012b41c..9cc81aec0c 100644 --- a/packages/firestore/e2e/Timestamp.e2e.js +++ b/packages/firestore/e2e/Timestamp.e2e.js @@ -16,168 +16,6 @@ */ describe('firestore.Timestamp', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if seconds is not a number', function () { - try { - new firebase.firestore.Timestamp('1234'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'seconds' expected a number value"); - return Promise.resolve(); - } - }); - - it('throws if nanoseconds is not a number', function () { - try { - new firebase.firestore.Timestamp(123, '456'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'nanoseconds' expected a number value"); - return Promise.resolve(); - } - }); - - it('throws if nanoseconds less than 0', function () { - try { - new firebase.firestore.Timestamp(123, -1); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'nanoseconds' out of range"); - return Promise.resolve(); - } - }); - - it('throws if nanoseconds greater than 1e9', function () { - try { - new firebase.firestore.Timestamp(123, 10000000000); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'nanoseconds' out of range"); - return Promise.resolve(); - } - }); - - it('throws if seconds less than -62135596800', function () { - try { - new firebase.firestore.Timestamp(-63135596800, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'seconds' out of range"); - return Promise.resolve(); - } - }); - - it('throws if seconds greater-equal than 253402300800', function () { - try { - new firebase.firestore.Timestamp(253402300800, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'seconds' out of range"); - return Promise.resolve(); - } - }); - - it('returns number of seconds', function () { - const ts = new firebase.firestore.Timestamp(123, 123); - ts.seconds.should.equal(123); - }); - - it('returns number of nanoseconds', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - ts.nanoseconds.should.equal(123456); - }); - - describe('isEqual()', function () { - it('throws if invalid other is procided', function () { - try { - const ts = new firebase.firestore.Timestamp(123, 1234); - ts.isEqual(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'other' expected an instance of Timestamp"); - return Promise.resolve(); - } - }); - - it('returns false if not equal', function () { - const ts1 = new firebase.firestore.Timestamp(123, 123456); - const ts2 = new firebase.firestore.Timestamp(1234, 123456); - ts1.isEqual(ts2).should.equal(false); - }); - - it('returns true if equal', function () { - const ts1 = new firebase.firestore.Timestamp(123, 123456); - const ts2 = new firebase.firestore.Timestamp(123, 123456); - ts1.isEqual(ts2).should.equal(true); - }); - }); - - describe('toDate()', function () { - it('returns a valid Date object', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - const date = ts.toDate(); - should.equal(date.constructor.name, 'Date'); - }); - }); - - describe('toMillis()', function () { - it('returns the number of milliseconds', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - const ms = ts.toMillis(); - should.equal(typeof ms, 'number'); - }); - }); - - describe('toString()', function () { - it('returns a string representation of the class', function () { - const ts = new firebase.firestore.Timestamp(123, 123456); - const str = ts.toString(); - str.should.equal(`Timestamp(seconds=${123}, nanoseconds=${123456})`); - }); - }); - - describe('Timestamp.now()', function () { - it('returns a new instance', function () { - const ts = firebase.firestore.Timestamp.now(); - should.equal(ts.constructor.name, 'Timestamp'); - }); - }); - - describe('Timestamp.fromDate()', function () { - it('throws if date is not a valid Date', function () { - try { - firebase.firestore.Timestamp.fromDate(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'date' expected a valid Date object"); - return Promise.resolve(); - } - }); - - it('returns a new instance', function () { - const ts = firebase.firestore.Timestamp.fromDate(new Date()); - should.equal(ts.constructor.name, 'Timestamp'); - }); - }); - - describe('Timestamp.fromMillis()', function () { - it('returns a new instance', function () { - const ts = firebase.firestore.Timestamp.fromMillis(123); - should.equal(ts.constructor.name, 'Timestamp'); - }); - }); - }); - describe('modular', function () { it('throws if seconds is not a number', function () { const { Timestamp } = firestoreModular; diff --git a/packages/firestore/e2e/Transaction.e2e.js b/packages/firestore/e2e/Transaction.e2e.js index 43f8afd8e8..f9ac0733cc 100644 --- a/packages/firestore/e2e/Transaction.e2e.js +++ b/packages/firestore/e2e/Transaction.e2e.js @@ -18,384 +18,6 @@ const COLLECTION = 'firestore'; const NO_RULE_COLLECTION = 'no_rules'; describe('firestore.Transaction', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('should throw if updateFunction is not a Promise', async function () { - try { - await firebase.firestore().runTransaction(() => 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'updateFunction' must return a Promise"); - return Promise.resolve(); - } - }); - - it('should return an instance of FirestoreTransaction', async function () { - await firebase.firestore().runTransaction(async transaction => { - transaction.constructor.name.should.eql('Transaction'); - return null; - }); - }); - - it('should resolve with user value', async function () { - const expected = Date.now(); - - const value = await firebase.firestore().runTransaction(async () => { - return expected; - }); - - value.should.eql(expected); - }); - - it('should reject with user Error', async function () { - const message = `Error: ${Date.now()}`; - - try { - await firebase.firestore().runTransaction(async () => { - throw new Error(message); - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (error) { - error.message.should.eql(message); - return Promise.resolve(); - } - }); - - it('should reject a native error', async function () { - const docRef = firebase.firestore().doc(`${NO_RULE_COLLECTION}/foo`); - - try { - await firebase.firestore().runTransaction(async t => { - t.set(docRef, { - foo: 'bar', - }); - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (error) { - error.code.should.eql('firestore/permission-denied'); - return Promise.resolve(); - } - }); - - describe('transaction.get()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firebase.firestore().runTransaction(t => { - return t.get(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should get a document and return a DocumentSnapshot', async function () { - const docRef = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/get-delete`); - await docRef.set({}); - - await firebase.firestore().runTransaction(async t => { - const docSnapshot = await t.get(docRef); - docSnapshot.constructor.name.should.eql('DocumentSnapshot'); - docSnapshot.exists().should.eql(true); - docSnapshot.id.should.eql('get-delete'); - - t.delete(docRef); - }); - }); - }); - - describe('transaction.delete()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firebase.firestore().runTransaction(async t => { - t.delete(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should delete documents', async function () { - const docRef1 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete1`); - await docRef1.set({}); - - const docRef2 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete2`); - await docRef2.set({}); - - await firebase.firestore().runTransaction(async t => { - t.delete(docRef1); - t.delete(docRef2); - }); - - const snapshot1 = await docRef1.get(); - snapshot1.exists().should.eql(false); - - const snapshot2 = await docRef2.get(); - snapshot2.exists().should.eql(false); - }); - }); - - describe('transaction.update()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firebase.firestore().runTransaction(async t => { - t.update(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should throw if update args are invalid', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - try { - await firebase.firestore().runTransaction(async t => { - t.update(docRef, 123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('it must be an object'); - return Promise.resolve(); - } - }); - - it('should update documents', async function () { - const value = Date.now(); - - const docRef1 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete1`); - await docRef1.set({ - foo: 'bar', - bar: 'baz', - }); - - const docRef2 = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/delete-delete2`); - await docRef2.set({ - foo: 'bar', - bar: 'baz', - }); - - await firebase.firestore().runTransaction(async t => { - t.update(docRef1, { - bar: value, - }); - t.update(docRef2, 'bar', value); - }); - - const expected = { - foo: 'bar', - bar: value, - }; - - const snapshot1 = await docRef1.get(); - snapshot1.exists().should.eql(true); - snapshot1.data().should.eql(jet.contextify(expected)); - - const snapshot2 = await docRef2.get(); - snapshot2.exists().should.eql(true); - snapshot2.data().should.eql(jet.contextify(expected)); - }); - }); - - describe('transaction.set()', function () { - it('should throw if not providing a document reference', async function () { - try { - await firebase.firestore().runTransaction(async t => { - t.set(123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected a DocumentReference"); - return Promise.resolve(); - } - }); - - it('should throw if set data is invalid', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - try { - await firebase.firestore().runTransaction(async t => { - t.set(docRef, 123); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object."); - return Promise.resolve(); - } - }); - - it('should throw if set options are invalid', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - try { - await firebase.firestore().runTransaction(async t => { - t.set( - docRef, - {}, - { - merge: true, - mergeFields: [], - }, - ); - }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options' must not contain both 'merge' & 'mergeFields'", - ); - return Promise.resolve(); - } - }); - - it('should set data', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/set`); - await docRef.set({ - foo: 'bar', - }); - const expected = { - foo: 'baz', - }; - - await firebase.firestore().runTransaction(async t => { - t.set(docRef, expected); - }); - - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); - }); - - it('should set data with merge', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/set-merge`); - await docRef.set({ - foo: 'bar', - bar: 'baz', - }); - const expected = { - foo: 'bar', - bar: 'foo', - }; - - await firebase.firestore().runTransaction(async t => { - t.set( - docRef, - { - bar: 'foo', - }, - { - merge: true, - }, - ); - }); - - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); - }); - - it('should set data with merge fields', async function () { - const docRef = firebase - .firestore() - .doc(`${COLLECTION}/transactions/transaction/set-mergefields`); - await docRef.set({ - foo: 'bar', - bar: 'baz', - baz: 'ben', - }); - const expected = { - foo: 'bar', - bar: 'foo', - baz: 'foo', - }; - - await firebase.firestore().runTransaction(async t => { - t.set( - docRef, - { - bar: 'foo', - baz: 'foo', - }, - { - mergeFields: ['bar', new firebase.firestore.FieldPath('baz')], - }, - ); - }); - - const snapshot = await docRef.get(); - snapshot.data().should.eql(jet.contextify(expected)); - }); - - it('should roll back any updates that failed', async function () { - // FIXME issue 8267 - if (Platform.other) { - this.skip(); - } - - const docRef = firebase.firestore().doc(`${COLLECTION}/transactions/transaction/rollback`); - - await docRef.set({ - turn: 0, - }); - - const prop1 = 'prop1'; - const prop2 = 'prop2'; - const turn = 0; - const errorMessage = 'turn cannot exceed 1'; - - const createTransaction = prop => { - return firebase.firestore().runTransaction(async transaction => { - const doc = await transaction.get(docRef); - const data = doc.data(); - - if (data.turn !== turn) { - throw new Error(errorMessage); - } - - const update = { - turn: turn + 1, - [prop]: 1, - }; - - transaction.update(docRef, update); - }); - }; - - const promises = [createTransaction(prop1), createTransaction(prop2)]; - - try { - await Promise.all(promises); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql(errorMessage); - } - const result = await docRef.get(); - should(result.data()).not.have.properties([prop1, prop2]); - }); - }); - }); - describe('modular', function () { it('should throw if updateFunction is not a Promise', async function () { const { getFirestore, runTransaction } = firestoreModular; diff --git a/packages/firestore/e2e/WriteBatch/commit.e2e.js b/packages/firestore/e2e/WriteBatch/commit.e2e.js index abd56c20c7..86e5a6e7f5 100644 --- a/packages/firestore/e2e/WriteBatch/commit.e2e.js +++ b/packages/firestore/e2e/WriteBatch/commit.e2e.js @@ -22,202 +22,6 @@ describe('firestore.WriteBatch.commit()', function () { return wipe(); }); - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('returns a Promise', function () { - const commit = firebase.firestore().batch().commit(); - commit.should.be.a.Promise(); - }); - - // FIXME this started to fail with dependency updates 20230628 - // firebase-tools firestore emulator allows more than 500 with an update? - // official docs still indicate that 500 is the limit, so this is likely - // an upstream bug in firestore emulator. - xit('throws if committing more than 500 writes', async function () { - const filledArray = new Array(501).fill({ foo: 'bar' }); - const batch = firebase.firestore().batch(); - - for (let i = 0; i < filledArray.length; i++) { - const doc = firebase.firestore().collection(COLLECTION).doc(i.toString()); - const filledArrayElement = filledArray[i]; - batch.set(doc, filledArrayElement); - } - - try { - await batch.commit(); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.code.should.containEql('firestore/invalid-argument'); - return Promise.resolve(); - } - }); - - it('throws if already committed', async function () { - try { - const batch = firebase.firestore().batch(); - await batch.commit(); - await batch.commit(); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql('A write batch can no longer be used'); - return Promise.resolve(); - } - }); - - it('should set & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - - const batch = firebase.firestore().batch(); - - batch.set(lRef, { name: 'London' }); - batch.set(nycRef, { name: 'New York' }); - batch.set(sfRef, { name: 'San Francisco' }); - - await batch.commit(); - - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - - lDoc.data().name.should.eql('London'); - nycDoc.data().name.should.eql('New York'); - sDoc.data().name.should.eql('San Francisco'); - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); - }); - - it('should set/merge & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); - - const batch = firebase.firestore().batch(); - - batch.set(lRef, { country: 'UK' }, { merge: true }); - batch.set(nycRef, { country: 'USA' }, { merge: true }); - batch.set(sfRef, { country: 'USA' }, { merge: true }); - - await batch.commit(); - - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - - lDoc.data().name.should.eql('London'); - lDoc.data().country.should.eql('UK'); - nycDoc.data().name.should.eql('New York'); - nycDoc.data().country.should.eql('USA'); - sDoc.data().name.should.eql('San Francisco'); - sDoc.data().country.should.eql('USA'); - - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); - }); - - it('should set/mergeFields & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); - - const batch = firebase.firestore().batch(); - - batch.set(lRef, { name: 'LON', country: 'UK' }, { mergeFields: ['country'] }); - batch.set(nycRef, { name: 'NYC', country: 'USA' }, { mergeFields: ['country'] }); - batch.set( - sfRef, - { name: 'SF', country: 'USA' }, - { mergeFields: [new firebase.firestore.FieldPath('country')] }, - ); - - await batch.commit(); - - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - - lDoc.data().name.should.eql('London'); - lDoc.data().country.should.eql('UK'); - nycDoc.data().name.should.eql('New York'); - nycDoc.data().country.should.eql('USA'); - sDoc.data().name.should.eql('San Francisco'); - sDoc.data().country.should.eql('USA'); - - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); - }); - - it('should delete & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); - - const batch = firebase.firestore().batch(); - - batch.delete(lRef); - batch.delete(nycRef); - batch.delete(sfRef); - - await batch.commit(); - - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - - lDoc.exists().should.be.False(); - nycDoc.exists().should.be.False(); - sDoc.exists().should.be.False(); - }); - - it('should update & commit', async function () { - const lRef = firebase.firestore().doc(`${COLLECTION}/LON`); - const nycRef = firebase.firestore().doc(`${COLLECTION}/NYC`); - const sfRef = firebase.firestore().doc(`${COLLECTION}/SF`); - - await Promise.all([ - lRef.set({ name: 'London' }), - nycRef.set({ name: 'New York' }), - sfRef.set({ name: 'San Francisco' }), - ]); - - const batch = firebase.firestore().batch(); - - batch.update(lRef, { name: 'LON', country: 'UK' }); - batch.update(nycRef, { name: 'NYC', country: 'USA' }); - batch.update(sfRef, 'name', 'SF', 'country', 'USA'); - - await batch.commit(); - - const [lDoc, nycDoc, sDoc] = await Promise.all([lRef.get(), nycRef.get(), sfRef.get()]); - - lDoc.data().name.should.eql('LON'); - lDoc.data().country.should.eql('UK'); - nycDoc.data().name.should.eql('NYC'); - nycDoc.data().country.should.eql('USA'); - sDoc.data().name.should.eql('SF'); - sDoc.data().country.should.eql('USA'); - - await Promise.all([lRef.delete(), nycRef.delete(), sfRef.delete()]); - }); - }); - describe('modular', function () { it('returns a Promise', function () { const { getFirestore, writeBatch } = firestoreModular; diff --git a/packages/firestore/e2e/WriteBatch/delete.e2e.js b/packages/firestore/e2e/WriteBatch/delete.e2e.js index 4be99d0cc2..9b13b9cd56 100644 --- a/packages/firestore/e2e/WriteBatch/delete.e2e.js +++ b/packages/firestore/e2e/WriteBatch/delete.e2e.js @@ -17,54 +17,6 @@ const COLLECTION = 'firestore'; describe('firestore.WriteBatch.delete()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if a DocumentReference instance is not provided', function () { - try { - firebase.firestore().batch().delete(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); - return Promise.resolve(); - } - }); - - it('throws if a DocumentReference firestore instance is different', function () { - try { - const app2 = firebase.app('secondaryFromNative'); - const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); - - firebase.firestore().batch().delete(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'documentRef' provided DocumentReference is from a different Firestore instance", - ); - return Promise.resolve(); - } - }); - - it('adds the DocumentReference to the internal writes', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - const wb = firebase.firestore().batch().delete(docRef); - wb._writes.length.should.eql(1); - const expected = { - path: `${COLLECTION}/foo`, - type: 'DELETE', - }; - wb._writes[0].should.eql(jet.contextify(expected)); - }); - }); - describe('modular', function () { it('throws if a DocumentReference instance is not provided', function () { const { getFirestore, writeBatch } = firestoreModular; diff --git a/packages/firestore/e2e/WriteBatch/set.e2e.js b/packages/firestore/e2e/WriteBatch/set.e2e.js index 1ed596122b..ff30fdb91c 100644 --- a/packages/firestore/e2e/WriteBatch/set.e2e.js +++ b/packages/firestore/e2e/WriteBatch/set.e2e.js @@ -17,203 +17,6 @@ const COLLECTION = 'firestore'; describe('firestore.WriteBatch.set()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if a DocumentReference instance is not provided', function () { - try { - firebase.firestore().batch().set(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); - return Promise.resolve(); - } - }); - - it('throws if a DocumentReference firestore instance is different', function () { - try { - const app2 = firebase.app('secondaryFromNative'); - const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); - - firebase.firestore().batch().set(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'documentRef' provided DocumentReference is from a different Firestore instance", - ); - return Promise.resolve(); - } - }); - - it('throws if a data is not an object', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - firebase.firestore().batch().set(docRef, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'data' must be an object"); - return Promise.resolve(); - } - }); - - it('throws if a options is not an object', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - firebase.firestore().batch().set(docRef, {}, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must be an object"); - return Promise.resolve(); - } - }); - - it('throws if merge and mergeFields is provided', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase - .firestore() - .batch() - .set( - docRef, - {}, - { - merge: true, - mergeFields: ['123'], - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options' must not contain both 'merge' & 'mergeFields'"); - return Promise.resolve(); - } - }); - - it('throws if merge is not a boolean', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().set( - docRef, - {}, - { - merge: 'true', - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.merge' must be a boolean value"); - return Promise.resolve(); - } - }); - - it('throws if mergeFields is not an array', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().set( - docRef, - {}, - { - mergeFields: '[]', - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.mergeFields' must be an array"); - return Promise.resolve(); - } - }); - - it('throws if mergeFields item is not valid', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - firebase - .firestore() - .batch() - .set( - docRef, - {}, - { - mergeFields: [123], - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'options.mergeFields' all fields must be of type string or FieldPath", - ); - return Promise.resolve(); - } - }); - - it('throws if string fieldpath is invalid', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - firebase - .firestore() - .batch() - .set( - docRef, - {}, - { - mergeFields: ['.foo.bar'], - }, - ); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'options.mergeFields' Invalid field path"); - return Promise.resolve(); - } - }); - - it('accepts string fieldpath & FieldPath', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - firebase - .firestore() - .batch() - .set( - docRef, - {}, - { - mergeFields: ['foo.bar', new firebase.firestore.FieldPath('foo', 'bar')], - }, - ); - }); - - it('adds the DocumentReference to the internal writes', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - - const wb = firebase - .firestore() - .batch() - .set( - docRef, - { foo: 'bar' }, - { mergeFields: [new firebase.firestore.FieldPath('foo', 'bar')] }, - ); - wb._writes.length.should.eql(1); - const expected = { - path: `${COLLECTION}/foo`, - type: 'SET', - options: jet.contextify({ - mergeFields: jet.contextify(['foo.bar']), - }), - }; - wb._writes[0].should.containEql(jet.contextify(expected)); - }); - }); - describe('modular', function () { it('throws if a DocumentReference instance is not provided', function () { const { getFirestore, writeBatch } = firestoreModular; diff --git a/packages/firestore/e2e/WriteBatch/update.e2e.js b/packages/firestore/e2e/WriteBatch/update.e2e.js index bbe7ca4e81..666d816b57 100644 --- a/packages/firestore/e2e/WriteBatch/update.e2e.js +++ b/packages/firestore/e2e/WriteBatch/update.e2e.js @@ -18,98 +18,6 @@ const COLLECTION = 'firestore'; describe('firestore.WriteBatch.update()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('throws if a DocumentReference instance is not provided', function () { - try { - firebase.firestore().batch().update(123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'documentRef' expected instance of a DocumentReference"); - return Promise.resolve(); - } - }); - - it('throws if a DocumentReference firestore instance is different', function () { - try { - const app2 = firebase.app('secondaryFromNative'); - const docRef = firebase.firestore(app2).doc(`${COLLECTION}/foo`); - - firebase.firestore().batch().update(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql( - "'documentRef' provided DocumentReference is from a different Firestore instance", - ); - return Promise.resolve(); - } - }); - - it('throws if update args are not provided', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Expected update object or list of key/value pairs'); - return Promise.resolve(); - } - }); - - it('throws if update arg is not an object', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef, 123); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('if using a single update argument, it must be an object'); - return Promise.resolve(); - } - }); - - it('throws if update key/values are invalid', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef, 'foo', 'bar', 'baz'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('equal numbers of key/value pairs'); - return Promise.resolve(); - } - }); - - it('throws if update keys are invalid', function () { - try { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - firebase.firestore().batch().update(docRef, 'foo', 'bar', 123, 'ben'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('argument at index 2 must be a string or FieldPath'); - return Promise.resolve(); - } - }); - - it('adds the DocumentReference to the internal writes', function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/foo`); - const wb = firebase.firestore().batch().update(docRef, { foo: 'bar' }); - wb._writes.length.should.eql(1); - const expected = { - path: `${COLLECTION}/foo`, - type: 'UPDATE', - }; - wb._writes[0].should.containEql(jet.contextify(expected)); - }); - }); - describe('modular', function () { it('throws if a DocumentReference instance is not provided', function () { const { getFirestore, writeBatch } = firestoreModular; diff --git a/packages/firestore/e2e/firestore.e2e.js b/packages/firestore/e2e/firestore.e2e.js index 1df4f838dc..39c057d76f 100644 --- a/packages/firestore/e2e/firestore.e2e.js +++ b/packages/firestore/e2e/firestore.e2e.js @@ -18,419 +18,6 @@ const COLLECTION = 'firestore'; const COLLECTION_GROUP = 'collectionGroup'; describe('firestore()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('namespace', function () { - // removing as pending if module.options.hasMultiAppSupport = true - it('supports multiple apps', async function () { - firebase.firestore().app.name.should.equal('[DEFAULT]'); - - firebase - .firestore(firebase.app('secondaryFromNative')) - .app.name.should.equal('secondaryFromNative'); - - firebase - .app('secondaryFromNative') - .firestore() - .app.name.should.equal('secondaryFromNative'); - }); - }); - - describe('batch()', function () {}); - - describe('clearPersistence()', function () {}); - - describe('collection()', function () {}); - - describe('collectionGroup()', function () { - it('performs a collection group query', async function () { - const docRef1 = firebase.firestore().doc(`${COLLECTION}/collectionGroup1`); - const docRef2 = firebase.firestore().doc(`${COLLECTION}/collectionGroup2`); - const docRef3 = firebase.firestore().doc(`${COLLECTION}/collectionGroup3`); - const subRef1 = docRef1.collection(COLLECTION_GROUP).doc('ref'); - const subRef2 = docRef1.collection(COLLECTION_GROUP).doc('ref2'); - const subRef3 = docRef2.collection(COLLECTION_GROUP).doc('ref'); - const subRef4 = docRef2.collection(COLLECTION_GROUP).doc('ref2'); - const subRef5 = docRef3.collection(COLLECTION_GROUP).doc('ref'); - const subRef6 = docRef3.collection(COLLECTION_GROUP).doc('ref2'); - - await Promise.all([ - subRef1.set({ value: 1 }), - subRef2.set({ value: 2 }), - - subRef3.set({ value: 1 }), - subRef4.set({ value: 2 }), - - subRef5.set({ value: 1 }), - subRef6.set({ value: 2 }), - ]); - - const querySnapshot = await firebase - .firestore() - .collectionGroup(COLLECTION_GROUP) - .where('value', '==', 2) - .get(); - - querySnapshot.forEach(ds => { - ds.data().value.should.eql(2); - }); - - querySnapshot.size.should.eql(3); - - await Promise.all([ - subRef1.delete(), - subRef2.delete(), - - subRef3.delete(), - subRef4.delete(), - - subRef5.delete(), - subRef6.delete(), - ]); - }); - - it('performs a collection group query with cursor queries', async function () { - const docRef = firebase.firestore().doc(`${COLLECTION}/collectionGroupCursor`); - - const ref1 = await docRef.collection(COLLECTION_GROUP).add({ number: 1 }); - const startAt = await docRef.collection(COLLECTION_GROUP).add({ number: 2 }); - const ref3 = await docRef.collection(COLLECTION_GROUP).add({ number: 3 }); - - const ds = await startAt.get(); - - const querySnapshot = await firebase - .firestore() - .collectionGroup(COLLECTION_GROUP) - .orderBy('number') - .startAt(ds) - .get(); - - querySnapshot.size.should.eql(2); - querySnapshot.forEach((d, i) => { - d.data().number.should.eql(i + 2); - }); - await Promise.all([ref1.delete(), ref3.delete(), startAt.delete()]); - }); - }); - - describe('disableNetwork() & enableNetwork()', function () { - it('disables and enables with no errors', async function () { - if (!(Platform.android || Platform.ios)) { - // Not supported on web lite sdk - return; - } - - await firebase.firestore().disableNetwork(); - await firebase.firestore().enableNetwork(); - }); - }); - - describe('Clear cached data persistence', function () { - // NOTE: removed as it breaks emulator tests - xit('should clear any cached data', async function () { - const db = firebase.firestore(); - const id = 'foobar'; - const ref = db.doc(`${COLLECTION}/${id}`); - await ref.set({ foo: 'bar' }); - try { - await db.clearPersistence(); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.code.should.equal('firestore/failed-precondition'); - } - const doc = await ref.get({ source: 'cache' }); - should(doc.id).equal(id); - await db.terminate(); - await db.clearPersistence(); - try { - await ref.get({ source: 'cache' }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.code.should.equal('firestore/unavailable'); - return Promise.resolve(); - } - }); - }); - - describe('wait for pending writes', function () { - xit('waits for pending writes', async function () { - const waitForPromiseMs = 500; - const testTimeoutMs = 10000; - - await firebase.firestore().disableNetwork(); - - //set up a pending write - - const db = firebase.firestore(); - const id = 'foobar'; - const ref = db.doc(`v6/${id}`); - ref.set({ foo: 'bar' }); - - //waitForPendingWrites should never resolve, but unfortunately we can only - //test that this is not returning within X ms - - let rejected = false; - const timedOutWithNetworkDisabled = await Promise.race([ - firebase - .firestore() - .waitForPendingWrites() - .then( - () => false, - () => { - rejected = true; - }, - ), - Utils.sleep(waitForPromiseMs).then(() => true), - ]); - - should(timedOutWithNetworkDisabled).equal(true); - should(rejected).equal(false); - - //if we sign in as a different user then it should reject the promise - try { - await firebase.auth().signOut(); - } catch (_) {} - await firebase.auth().signInAnonymously(); - should(rejected).equal(true); - - //now if we enable the network then waitForPendingWrites should return immediately - await firebase.firestore().enableNetwork(); - - const timedOutWithNetworkEnabled = await Promise.race([ - firebase - .firestore() - .waitForPendingWrites() - .then(() => false), - Utils.sleep(testTimeoutMs).then(() => true), - ]); - - should(timedOutWithNetworkEnabled).equal(false); - }); - }); - - describe('settings', function () { - describe('serverTimestampBehavior', function () { - // TODO flakey in new Jet setup since it conflicts with the modular tests - xit("handles 'estimate'", async function () { - // TODO(ehesp): Figure out how to call settings on other. - if (Platform.other) { - // Not supported on web lite sdk - return; - } - - firebase.firestore().settings({ serverTimestampBehavior: 'estimate' }); - const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampEstimate`); - - const promise = new Promise((resolve, reject) => { - const subscription = ref.onSnapshot(snapshot => { - try { - should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); - subscription(); - resolve(); - } catch (e) { - reject(e); - } - }, reject); - }); - - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await promise; - await ref.delete(); - }); - - // TODO flakey in new Jet setup since it conflicts with the modular tests - xit("handles 'previous'", async function () { - // TODO(ehesp): Figure out how to call settings on other. - if (Platform.other) { - // Not supported on web lite sdk - return; - } - - firebase.firestore().settings({ serverTimestampBehavior: 'previous' }); - const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampPrevious`); - - const promise = new Promise((resolve, reject) => { - let counter = 0; - let previous = null; - const subscription = ref.onSnapshot(snapshot => { - try { - switch (counter++) { - case 0: - should(snapshot.get('timestamp')).equal(null); - break; - case 1: - should(snapshot.get('timestamp')).be.an.instanceOf( - firebase.firestore.Timestamp, - ); - break; - case 2: - should(snapshot.get('timestamp')).be.an.instanceOf( - firebase.firestore.Timestamp, - ); - should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal( - true, - ); - break; - case 3: - should(snapshot.get('timestamp')).be.an.instanceOf( - firebase.firestore.Timestamp, - ); - should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal( - false, - ); - subscription(); - resolve(); - break; - } - } catch (e) { - reject(e); - } - previous = snapshot; - }, reject); - }); - - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await new Promise(resolve => setTimeout(resolve, 100)); - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await promise; - await ref.delete(); - }); - - // works in isolation but not in suite - xit("handles 'none'", async function () { - // TODO(ehesp): Figure out how to call settings on other. - if (Platform.other) { - return; - } - - firebase.firestore().settings({ serverTimestampBehavior: 'none' }); - const ref = firebase.firestore().doc(`${COLLECTION}/serverTimestampNone`); - - const promise = new Promise((resolve, reject) => { - let counter = 0; - const subscription = ref.onSnapshot(snapshot => { - try { - switch (counter++) { - case 0: - // The initial callback snapshot should have no value for the timestamp, it has not been set at all - should(snapshot.get('timestamp')).equal(null); - break; - case 1: - should(snapshot.get('timestamp')).be.an.instanceOf( - firebase.firestore.Timestamp, - ); - subscription(); - resolve(); - break; - default: - // there should only be initial callback and set callback, any other callbacks are a fail - reject(new Error('too many callbacks')); - } - } catch (e) { - reject(e); - } - }, reject); - }); - - await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); - await promise; - await ref.delete(); - }); - }); - }); - - describe('FirestorePersistentCacheIndexManager', function () { - describe('if persistence is enabled', function () { - it('should enableIndexAutoCreation()', async function () { - if (Platform.other) { - // Not supported on web lite sdk - return; - } - - const db = firebase.firestore(); - const indexManager = db.persistentCacheIndexManager(); - await indexManager.enableIndexAutoCreation(); - }); - - it('should disableIndexAutoCreation()', async function () { - if (Platform.other) { - // Not supported on web lite sdk - return; - } - const db = firebase.firestore(); - const indexManager = db.persistentCacheIndexManager(); - await indexManager.disableIndexAutoCreation(); - }); - - it('should deleteAllIndexes()', async function () { - if (Platform.other) { - // Not supported on web lite sdk - return; - } - const db = firebase.firestore(); - const indexManager = db.persistentCacheIndexManager(); - await indexManager.deleteAllIndexes(); - }); - }); - - describe('if persistence is disabled', function () { - it('should return `null` when calling `persistentCacheIndexManager()`', async function () { - if (Platform.other) { - // Not supported on web lite sdk - return; - } - const secondFirestore = firebase.app('secondaryFromNative').firestore(); - secondFirestore.settings({ persistence: false }); - const indexManager = secondFirestore.persistentCacheIndexManager(); - should.equal(indexManager, null); - }); - }); - - describe('macOS should throw exception when calling `persistentCacheIndexManager()`', function () { - it('should throw an exception when calling PersistentCacheIndexManager API', async function () { - if (!Platform.other) { - return; - } - const db = firebase.firestore(); - const indexManager = db.persistentCacheIndexManager(); - - try { - await indexManager.enableIndexAutoCreation(); - - throw new Error('Did not throw an Error.'); - } catch (e) { - e.message.should.containEql('Not supported in the lite SDK'); - } - - try { - await indexManager.deleteAllIndexes(); - - throw new Error('Did not throw an Error.'); - } catch (e) { - e.message.should.containEql('Not supported in the lite SDK'); - } - - try { - await indexManager.disableIndexAutoCreation(); - - throw new Error('Did not throw an Error.'); - } catch (e) { - e.message.should.containEql('Not supported in the lite SDK'); - } - }); - }); - }); - }); - describe('modular', function () { describe('getFirestore', function () { // removing as pending if module.options.hasMultiAppSupport = true @@ -627,10 +214,12 @@ describe('firestore()', function () { should(rejected).equal(false); //if we sign in as a different user then it should reject the promise + const { getAuth, signOut, signInAnonymously } = authModular; + const auth = getAuth(); try { - await firebase.auth().signOut(); + await signOut(auth); } catch (_) {} - await firebase.auth().signInAnonymously(); + await signInAnonymously(auth); should(rejected).equal(true); //now if we enable the network then waitForPendingWrites should return immediately diff --git a/packages/firestore/e2e/issues.e2e.js b/packages/firestore/e2e/issues.e2e.js index 45fd6a0eac..a01b6c4374 100644 --- a/packages/firestore/e2e/issues.e2e.js +++ b/packages/firestore/e2e/issues.e2e.js @@ -38,159 +38,6 @@ const testNumbers = { }; describe('firestore()', function () { - describe('v8 compatibility', function () { - before(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - after(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('issues', function () { - before(async function () { - await Promise.all([ - firebase - .firestore() - .doc(`${COLLECTION}/wbXwyLJheRfYXXWlY46j`) - .set({ index: 2, number: 2 }), - firebase - .firestore() - .doc(`${COLLECTION}/kGC5cYPN1nKnZCcAb9oQ`) - .set({ index: 6, number: 2 }), - firebase - .firestore() - .doc(`${COLLECTION}/8Ek8iWCDQPPJ5s2n8PiQ`) - .set({ index: 4, number: 2 }), - firebase - .firestore() - .doc(`${COLLECTION}/mr7MdAygvuheF6AUtWma`) - .set({ index: 1, number: 1 }), - firebase - .firestore() - .doc(`${COLLECTION}/RCO5SvNn4fdoE49OKrIV`) - .set({ index: 3, number: 1 }), - firebase - .firestore() - .doc(`${COLLECTION}/CvVG7VP1hXTtcfdUaeNl`) - .set({ index: 5, number: 1 }), - ]); - }); - - it('returns all results', async function () { - const db = firebase.firestore(); - const ref = db.collection(COLLECTION).orderBy('number', 'desc'); - const allResultsSnapshot = await ref.get(); - allResultsSnapshot.forEach((doc, i) => { - if (i === 0) { - doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); - } - if (i === 1) { - doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); - } - if (i === 2) { - doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); - } - if (i === 3) { - doc.id.should.equal('mr7MdAygvuheF6AUtWma'); - } - if (i === 4) { - doc.id.should.equal('RCO5SvNn4fdoE49OKrIV'); - } - if (i === 5) { - doc.id.should.equal('CvVG7VP1hXTtcfdUaeNl'); - } - }); - }); - - it('returns first page', async function () { - const db = firebase.firestore(); - const ref = db.collection(COLLECTION).orderBy('number', 'desc'); - const firstPageSnapshot = await ref.limit(2).get(); - should.equal(firstPageSnapshot.docs.length, 2); - firstPageSnapshot.forEach((doc, i) => { - if (i === 0) { - doc.id.should.equal('wbXwyLJheRfYXXWlY46j'); - } - if (i === 1) { - doc.id.should.equal('kGC5cYPN1nKnZCcAb9oQ'); - } - }); - }); - - it('returns second page', async function () { - const db = firebase.firestore(); - const ref = db.collection(COLLECTION).orderBy('number', 'desc'); - const firstPageSnapshot = await ref.limit(2).get(); - let lastDocument; - firstPageSnapshot.forEach(doc => { - lastDocument = doc; - }); - - const secondPageSnapshot = await ref.startAfter(lastDocument).limit(2).get(); - should.equal(secondPageSnapshot.docs.length, 2); - secondPageSnapshot.forEach((doc, i) => { - if (i === 0) { - doc.id.should.equal('8Ek8iWCDQPPJ5s2n8PiQ'); - } - if (i === 1) { - doc.id.should.equal('mr7MdAygvuheF6AUtWma'); - } - }); - }); - }); - - // TODO: conflicts with the modular tests below due to e2e tests running in the same process - xdescribe('number type consistency', function () { - before(async function () { - jsFirebase.initializeApp(FirebaseHelpers.app.config()); - jsFirebase.firestore().useEmulator(getE2eEmulatorHost(), 8080); - - // Put one example of each number in our collection using JS SDK - await Promise.all( - Object.entries(testNumbers).map(([testName, testValue]) => { - return jsFirebase - .firestore() - .doc(`${COLLECTION}/numberTestsJS/cases/${testName}`) - .set({ number: testValue }); - }), - ); - - // Put one example of each number in our collection using Native SDK - await Promise.all( - Object.entries(testNumbers).map(([testName, testValue]) => { - return firebase - .firestore() - .doc(`${COLLECTION}/numberTestsNative/cases/${testName}`) - .set({ number: testValue }); - }), - ); - }); - - it('types inserted by JS may be queried by native with filters', async function () { - const testValues = Object.values(testNumbers); - const ref = firebase - .firestore() - .collection(`${COLLECTION}/numberTestsJS/cases`) - .where('number', 'in', testValues); - typesSnap = await ref.get(); - should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); - }); - - it('types inserted by native may be queried by JS with filters', async function () { - const testValues = Object.values(testNumbers); - const ref = jsFirebase - .firestore() - .collection(`${COLLECTION}/numberTestsNative/cases`) - .where('number', 'in', testValues); - typesSnap = await ref.get(); - should.deepEqual(typesSnap.docs.map(d => d.id).sort(), Object.keys(testNumbers).sort()); - }); - }); - }); - describe('modular', function () { describe('issues', function () { before(async function () { diff --git a/packages/firestore/e2e/withConverter.e2e.js b/packages/firestore/e2e/withConverter.e2e.js index 9658e6dd8c..868274489b 100644 --- a/packages/firestore/e2e/withConverter.e2e.js +++ b/packages/firestore/e2e/withConverter.e2e.js @@ -81,16 +81,6 @@ const postConverterMerge = { }, }; -// v8 compatibility helper functions -function modifyIgnoreUndefinedProperties(db, value) { - // JS SDK settings can only be called once - if (Platform.other) { - db._settings.ignoreUndefinedProperties = value; - } else { - db.settings({ ignoreUndefinedProperties: value }); - } -} - // modular helper functions function withTestDb(fn) { return fn(getFirestore()); @@ -123,254 +113,8 @@ function withTestCollectionAndInitialData(data, fn) { } describe('firestore.withConverter', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - before(function () { - return wipe(); - }); - - it('for collection references', function () { - const firestore = firebase.firestore(); - const coll1a = firestore.collection('a'); - const coll1b = firestore.doc('a/b').parent; - const coll2 = firestore.collection('c'); - - coll1a.isEqual(coll1b).should.be.true(); - coll1a.isEqual(coll2).should.be.false(); - - const coll1c = firestore.collection('a').withConverter({ - toFirestore: data => data, - fromFirestore: snap => snap.data(), - }); - coll1a.isEqual(coll1c).should.be.false(); - - try { - coll1a.isEqual(firestore.doc('a/b')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected a Query instance.'); - return Promise.resolve(); - } - }); - - it('for document references', function () { - const firestore = firebase.firestore(); - const doc1a = firestore.doc('a/b'); - const doc1b = firestore.collection('a').doc('b'); - const doc2 = firestore.doc('a/c'); - - doc1a.isEqual(doc1b).should.be.true(); - doc1a.isEqual(doc2).should.be.false(); - - try { - const doc1c = firestore.collection('a').withConverter({ - toFirestore: data => data, - fromFirestore: snap => snap.data(), - }); - doc1a.isEqual(doc1c); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected a DocumentReference instance.'); - } - - try { - doc1a.isEqual(firestore.collection('a')); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('expected a DocumentReference instance.'); - } - return Promise.resolve(); - }); - - it('for DocumentReference.withConverter()', async function () { - const firestore = firebase.firestore(); - let docRef = firestore.doc(`${COLLECTION}/doc`); - docRef = docRef.withConverter(postConverter); - await docRef.set(new Post('post', 'author')); - const postData = await docRef.get(); - const post = postData.data(); - post.should.not.be.undefined(); - post.byline().should.equal('post, by author'); - }); - - it('for DocumentReference.withConverter(null) applies default converter', function () { - const firestore = firebase.firestore(); - const coll = firestore - .collection(COLLECTION) - .withConverter(postConverter) - .withConverter(null); - try { - return coll - .doc('post1') - .set(10) - .then(() => Promise.reject(new Error('Did not throw an Error.'))); - } catch (error) { - error.message.should.containEql( - `firebase.firestore().doc().set(*) 'data' must be an object.`, - ); - return Promise.resolve(); - } - }); - - it('for CollectionReference.withConverter()', async function () { - const firestore = firebase.firestore(); - let coll = firestore.collection(COLLECTION); - coll = coll.withConverter(postConverter); - const docRef = await coll.add(new Post('post', 'author')); - const postData = await docRef.get(); - const post = postData.data(); - post.should.not.be.undefined(); - post.byline().should.equal('post, by author'); - }); - - it('for CollectionReference.withConverter(null) applies default converter', function () { - const firestore = firebase.firestore(); - let docRef = firestore.doc(`${COLLECTION}/doc`); - try { - docRef = docRef.withConverter(postConverter).withConverter(null); - return docRef.set(10).then(() => Promise.reject(new Error('Did not throw an Error.'))); - } catch (error) { - error.message.should.containEql( - `firebase.firestore().doc().set(*) 'data' must be an object.`, - ); - return Promise.resolve(); - } - }); - - it('for Query.withConverter()', async function () { - const firestore = firebase.firestore(); - const collRef = firestore.collection(COLLECTION); - await collRef.add({ title: 'post', author: 'author' }); - let query1 = collRef.where('title', '==', 'post'); - query1 = query1.withConverter(postConverter); - const result = await query1.get(); - result.docs[0].data().should.be.an.instanceOf(Post); - result.docs[0].data().byline().should.equal('post, by author'); - }); - - it('for Query.withConverter(null) applies default converter', async function () { - const firestore = firebase.firestore(); - const collRef = firestore.collection(COLLECTION); - await collRef.add({ title: 'post', author: 'author' }); - let query1 = collRef.where('title', '==', 'post'); - query1 = query1.withConverter(postConverter).withConverter(null); - const result = await query1.get(); - result.docs[0].should.not.be.an.instanceOf(Post); - }); - - it('keeps the converter when calling parent() with a DocumentReference', function () { - const db = firebase.firestore(); - const coll = db.doc('root/doc').withConverter(postConverter); - const typedColl = coll.parent; - typedColl.isEqual(db.collection('root').withConverter(postConverter)).should.be.true(); - }); - - it('drops the converter when calling parent() with a CollectionReference', function () { - const db = firebase.firestore(); - const coll = db.collection('root/doc/parent').withConverter(postConverter); - const untypedDoc = coll.parent; - untypedDoc.isEqual(db.doc('root/doc')).should.be.true(); - }); - - it('checks converter when comparing with isEqual()', function () { - const db = firebase.firestore(); - const postConverter2 = { ...postConverter }; - - const postsCollection = db.collection('users/user1/posts').withConverter(postConverter); - const postsCollection2 = db.collection('users/user1/posts').withConverter(postConverter2); - postsCollection.isEqual(postsCollection2).should.be.false(); - - const docRef = db.doc('some/doc').withConverter(postConverter); - const docRef2 = db.doc('some/doc').withConverter(postConverter2); - docRef.isEqual(docRef2).should.be.false(); - }); - - it('requires the correct converter for Partial usage', async function () { - const db = firebase.firestore(); - const previousValue = db._settings.ignoreUndefinedProperties; - modifyIgnoreUndefinedProperties(db, false); - - const coll = db.collection('posts'); - const ref = coll.doc('post').withConverter(postConverter); - const batch = db.batch(); - - try { - batch.set(ref, { title: 'olive' }, { merge: true }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql('Unsupported field value: undefined'); - } - modifyIgnoreUndefinedProperties(db, previousValue); - return Promise.resolve(); - }); - - it('supports primitive types with valid converter', async function () { - const firestore = firebase.firestore(); - const primitiveConverter = { - toFirestore(value) { - return { value }; - }, - fromFirestore(snapshot) { - const data = snapshot.data(); - return data.value; - }, - }; - - const arrayConverter = { - toFirestore(value) { - return { values: value }; - }, - fromFirestore(snapshot) { - const data = snapshot.data(); - return data.values; - }, - }; - - const coll = firestore.collection(COLLECTION); - const ref = coll.doc('number').withConverter(primitiveConverter); - await ref.set(3); - const result = await ref.get(); - result.data().should.equal(3); - - const ref2 = coll.doc('array').withConverter(arrayConverter); - await ref2.set([1, 2, 3]); - const result2 = await ref2.get(); - result2.data().should.deepEqual([1, 2, 3]); - }); - - it('supports partials with merge', async function () { - const firestore = firebase.firestore(); - const coll = firestore.collection(COLLECTION); - const ref = coll.doc('post').withConverter(postConverterMerge); - await ref.set(new Post('walnut', 'author')); - await ref.set( - { title: 'olive', id: firebase.firestore.FieldValue.increment(2) }, - { merge: true }, - ); - const postDoc = await ref.get(); - postDoc.get('title').should.equal('olive'); - postDoc.get('author').should.equal('author'); - }); - - it('supports partials with mergeFields', async function () { - const firestore = firebase.firestore(); - const coll = firestore.collection(COLLECTION); - const ref = coll.doc('post').withConverter(postConverterMerge); - await ref.set(new Post('walnut', 'author')); - await ref.set({ title: 'olive' }, { mergeFields: ['title'] }); - const postDoc = await ref.get(); - postDoc.get('title').should.equal('olive'); - postDoc.get('author').should.equal('author'); - }); + before(function () { + return wipe(); }); describe('modular', function () { diff --git a/packages/firestore/lib/FieldPath.ts b/packages/firestore/lib/FieldPath.ts index 9bbc14cdd9..1bc2dfaec9 100644 --- a/packages/firestore/lib/FieldPath.ts +++ b/packages/firestore/lib/FieldPath.ts @@ -19,7 +19,7 @@ import { isString } from '@react-native-firebase/app/dist/module/common'; const RESERVED = new RegExp('[~*/\\[\\]]'); -export default class FieldPath { +export class FieldPath { _segments: string[]; constructor(...segments: string[]) { @@ -77,3 +77,12 @@ export function fromDotSeparatedString(path: string): FieldPath { return new FieldPath(...path.split('.')); } + +/** Serialize a stored filter/order field path for the native bridge. */ +export function fieldPathOrSegmentsToNative(fieldPath: FieldPath | string[]): string[] { + if (fieldPath instanceof FieldPath) { + return fieldPath._toArray(); + } + + return fieldPath; +} diff --git a/packages/firestore/lib/FieldValue.ts b/packages/firestore/lib/FieldValue.ts index 35fb54c0b4..2f71db8c1d 100644 --- a/packages/firestore/lib/FieldValue.ts +++ b/packages/firestore/lib/FieldValue.ts @@ -38,7 +38,7 @@ function validateArrayElements(elements: unknown[]): void { } } -export default class FieldValue { +export class FieldValue { _type: string; _elements: unknown; diff --git a/packages/firestore/lib/FirestoreAggregate.ts b/packages/firestore/lib/FirestoreAggregate.ts index 10f1f15b8a..8b3bd5229c 100644 --- a/packages/firestore/lib/FirestoreAggregate.ts +++ b/packages/firestore/lib/FirestoreAggregate.ts @@ -22,22 +22,22 @@ import type { DocumentData, Query as FirestoreQuery, } from './types/firestore'; -import FieldPath, { fromDotSeparatedString } from './FieldPath'; +import { FieldPath, fromDotSeparatedString } from './FieldPath'; import type FirestorePath from './FirestorePath'; -import type Query from './FirestoreQuery'; +import type { Query as QueryImplementation } from './FirestoreQuery'; import type QueryModifiers from './FirestoreQueryModifiers'; import type { FirestoreInternal } from './types/internal'; export class AggregateQuery { _firestore: FirestoreInternal; - _query: Query; + _query: FirestoreQuery; _collectionPath: FirestorePath; _modifiers: QueryModifiers; constructor( firestore: FirestoreInternal, - query: Query, + query: QueryImplementation, collectionPath: FirestorePath, modifiers: QueryModifiers, ) { @@ -47,7 +47,7 @@ export class AggregateQuery { this._modifiers = modifiers; } - get query(): Query { + get query(): FirestoreQuery { return this._query; } diff --git a/packages/firestore/lib/FirestoreBlob.ts b/packages/firestore/lib/FirestoreBlob.ts index 9ab810ff09..1c6730b800 100644 --- a/packages/firestore/lib/FirestoreBlob.ts +++ b/packages/firestore/lib/FirestoreBlob.ts @@ -17,7 +17,7 @@ import { Base64, isString } from '@react-native-firebase/app/dist/module/common'; -export default class Blob { +export class Blob { _binaryString: string; constructor(internal = false, binaryString?: string) { diff --git a/packages/firestore/lib/FirestoreDocumentChange.ts b/packages/firestore/lib/FirestoreDocumentChange.ts index 3a3a4304bc..f34caf4fd9 100644 --- a/packages/firestore/lib/FirestoreDocumentChange.ts +++ b/packages/firestore/lib/FirestoreDocumentChange.ts @@ -15,7 +15,6 @@ * */ -import { createDeprecationProxy } from '@react-native-firebase/app/dist/module/common'; import DocumentSnapshot from './FirestoreDocumentSnapshot'; import type { FirestoreInternal } from './types/internal'; import type { DocumentData, FirestoreDataConverter } from './types/firestore'; @@ -52,8 +51,10 @@ export default class DocumentChange { } get doc(): DocumentSnapshot { - return createDeprecationProxy( - new DocumentSnapshot(this._firestore, this._nativeData.doc, this._converter), + return new DocumentSnapshot( + this._firestore, + this._nativeData.doc, + this._converter, ) as DocumentSnapshot; } diff --git a/packages/firestore/lib/FirestoreDocumentReference.ts b/packages/firestore/lib/FirestoreDocumentReference.ts index 82197dd96b..17ec3e1cef 100644 --- a/packages/firestore/lib/FirestoreDocumentReference.ts +++ b/packages/firestore/lib/FirestoreDocumentReference.ts @@ -20,8 +20,6 @@ import { isString, isUndefined, isNull, - createDeprecationProxy, - filterModularArgument, } from '@react-native-firebase/app/dist/module/common'; import NativeError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; import { @@ -172,21 +170,18 @@ export default class DocumentReference< .documentGet(this.path, options) .then( (data: unknown) => - createDeprecationProxy( - new FirestoreDocumentSnapshotClass!( - this._firestore, - data as DocumentSnapshotNativeData, - this._converter as unknown as FirestoreDataConverter< - DocumentData, - DocumentData - > | null, - ), + new FirestoreDocumentSnapshotClass!( + this._firestore, + data as DocumentSnapshotNativeData, + this._converter as unknown as FirestoreDataConverter | null, ) as DocumentSnapshot, ); } isEqual(other: DocumentReference): boolean { - if (!(other instanceof DocumentReference)) { + const otherRef = other as DocumentReference & { type?: string }; + + if (!(other instanceof DocumentReference) || otherRef.type !== 'document') { throw new Error( "firebase.firestore().doc().isEqual(*) 'other' expected a DocumentReference instance.", ); @@ -239,15 +234,10 @@ export default class DocumentReference< } else { const snapshot = event.body.snapshot; if (!snapshot) return; - const documentSnapshot = createDeprecationProxy( - new FirestoreDocumentSnapshotClass!( - this._firestore, - snapshot, - this._converter as unknown as FirestoreDataConverter< - DocumentData, - DocumentData - > | null, - ), + const documentSnapshot = new FirestoreDocumentSnapshotClass!( + this._firestore, + snapshot, + this._converter as unknown as FirestoreDataConverter | null, ) as DocumentSnapshot; handleSuccess(documentSnapshot); } @@ -293,7 +283,7 @@ export default class DocumentReference< } update(...args: unknown[]): Promise { - const updatedArgs = filterModularArgument(args); + const updatedArgs = args; if (updatedArgs.length === 0) { throw new Error( 'firebase.firestore().doc().update(*) expected at least 1 argument but was called with 0 arguments.', diff --git a/packages/firestore/lib/FirestoreDocumentSnapshot.ts b/packages/firestore/lib/FirestoreDocumentSnapshot.ts index 6f1d558a99..288cd73377 100644 --- a/packages/firestore/lib/FirestoreDocumentSnapshot.ts +++ b/packages/firestore/lib/FirestoreDocumentSnapshot.ts @@ -17,7 +17,7 @@ import { isString } from '@react-native-firebase/app/dist/module/common'; import DocumentReference, { provideDocumentSnapshotClass } from './FirestoreDocumentReference'; -import FieldPath, { fromDotSeparatedString } from './FieldPath'; +import { FieldPath, fromDotSeparatedString } from './FieldPath'; import FirestorePath from './FirestorePath'; import SnapshotMetadata from './FirestoreSnapshotMetadata'; import type { SnapshotOptions } from './types/firestore'; diff --git a/packages/firestore/lib/FirestoreFilter.ts b/packages/firestore/lib/FirestoreFilter.ts index 4f5e1f9d26..45f13c6dba 100644 --- a/packages/firestore/lib/FirestoreFilter.ts +++ b/packages/firestore/lib/FirestoreFilter.ts @@ -24,7 +24,7 @@ import { import { fromDotSeparatedString } from './FieldPath'; import { OPERATORS } from './FirestoreQueryModifiers'; import { generateNativeData } from './utils/serialize'; -import type FieldPath from './FieldPath'; +import type { FieldPath } from './FieldPath'; import type { Primitive } from './types/firestore'; const AND_QUERY = 'AND'; @@ -36,14 +36,6 @@ type FieldFilterOperator = keyof typeof OPERATORS; /** Value allowed in a filter (primitive, object, or array for in/array-contains-any). */ type FilterValue = Primitive | Record | unknown[]; -export function Filter( - fieldPath: string | FieldPath, - operator: FieldFilterOperator, - value: FilterValue | unknown, -): _Filter { - return new _Filter(fieldPath, operator, value as FilterValue); -} - export class _Filter { fieldPath?: string | FieldPath; operator: FieldFilterOperator | FilterOperator; @@ -85,6 +77,14 @@ export class _Filter { } } +export function Filter( + fieldPath: string | FieldPath, + operator: FieldFilterOperator, + value: FilterValue | unknown, +): _Filter { + return new _Filter(fieldPath, operator, value as FilterValue); +} + Filter.and = function and(...queries: _Filter[]): _Filter { if (queries.length > 10 || queries.length < 1) { throw new Error(`Expected 1-10 instances of Filter, but got ${queries.length} Filters`); diff --git a/packages/firestore/lib/FirestoreGeoPoint.ts b/packages/firestore/lib/FirestoreGeoPoint.ts index 397c544349..386cf03e7e 100644 --- a/packages/firestore/lib/FirestoreGeoPoint.ts +++ b/packages/firestore/lib/FirestoreGeoPoint.ts @@ -22,7 +22,7 @@ import { isObject, } from '@react-native-firebase/app/dist/module/common'; -export default class GeoPoint { +export class GeoPoint { _latitude: number; _longitude: number; diff --git a/packages/firestore/lib/namespaced.ts b/packages/firestore/lib/FirestoreModule.ts similarity index 82% rename from packages/firestore/lib/namespaced.ts rename to packages/firestore/lib/FirestoreModule.ts index 4eb6c25c76..b70d388b48 100644 --- a/packages/firestore/lib/namespaced.ts +++ b/packages/firestore/lib/FirestoreModule.ts @@ -16,7 +16,6 @@ */ import { - createDeprecationProxy, isAndroid, isBoolean, isFunction, @@ -26,39 +25,26 @@ import { isUndefined, isOther, } from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, - type ModuleConfig, -} from '@react-native-firebase/app/dist/module/internal'; -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import CollectionReference from './FirestoreCollectionReference'; -import DocumentReference from './FirestoreDocumentReference'; +import { FirebaseModule, type ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import CollectionReferenceClass from './FirestoreCollectionReference'; +import DocumentReferenceClass from './FirestoreDocumentReference'; import FirestorePath from './FirestorePath'; -import FirestorePersistentCacheIndexManager from './FirestorePersistentCacheIndexManager'; -import Query from './FirestoreQuery'; +import FirestorePersistentCacheIndexManagerClass from './FirestorePersistentCacheIndexManager'; +import QueryClass from './FirestoreQuery'; import QueryModifiers from './FirestoreQueryModifiers'; import FirestoreStatics from './FirestoreStatics'; import FirestoreTransactionHandler from './FirestoreTransactionHandler'; import FirestoreWriteBatch from './FirestoreWriteBatch'; import { LoadBundleTask } from './LoadBundleTask'; import type { LoadBundleTaskProgress } from './types/firestore'; -import { version } from './version'; -import type { FirebaseFirestoreTypes } from './types/namespaced'; import type { FirestoreInternal } from './types/internal'; import fallBackModule from './web/RNFBFirestoreModule'; -// react-native at least through 0.77 does not correctly support URL.host, which -// is needed by firebase-js-sdk. It appears that in 0.80+ it is supported, so this -// (and the package.json entry for this package) should be removed when the minimum -// supported version of react-native is 0.80 or higher. -import 'react-native-url-polyfill/auto'; - const namespace = 'firestore'; -const nativeModuleNames = [ +export const nativeModuleNames = [ 'RNFBFirestoreModule', 'RNFBFirestoreCollectionModule', 'RNFBFirestoreDocumentModule', @@ -72,6 +58,14 @@ const nativeEvents = [ 'firestore_snapshots_in_sync_event', ] as const; +export const config: ModuleConfig = { + namespace, + nativeModuleName: [...nativeModuleNames], + nativeEvents: [...nativeEvents], + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: true, +}; + type FirestoreModuleSettingsState = { ignoreUndefinedProperties: boolean; persistence: boolean; @@ -80,7 +74,7 @@ type FirestoreModuleSettingsState = { /** Sync event payload from emitter when fanning out collection/document/snapshots-in-sync events. */ type FirestoreSyncEventWithListenerId = { listenerId: string | number }; -class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { +export class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { type = 'firestore' as const; _referencePath: FirestorePath; _transactionHandler: FirestoreTransactionHandler; @@ -176,7 +170,7 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { return task; } - namedQuery(queryName: string): Query | null { + namedQuery(queryName: string): QueryClass | null { if (!isString(queryName)) { throw new Error("firebase.firestore().namedQuery(*) 'queryName' must be a string value."); } @@ -185,7 +179,7 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { throw new Error("firebase.firestore().namedQuery(*) 'queryName' must be a non-empty string."); } - return new Query( + return new QueryClass( this as unknown as FirestoreInternal, this._referencePath, new QueryModifiers(), @@ -230,7 +224,7 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { return [mappedHost, port]; } - collection(collectionPath: string): CollectionReference { + collection(collectionPath: string): CollectionReferenceClass { if (!isString(collectionPath)) { throw new Error( "firebase.firestore().collection(*) 'collectionPath' must be a string value.", @@ -251,12 +245,10 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { ); } - return createDeprecationProxy( - new CollectionReference(this as unknown as FirestoreInternal, path), - ); + return new CollectionReferenceClass(this as unknown as FirestoreInternal, path); } - collectionGroup(collectionId: string): Query { + collectionGroup(collectionId: string): QueryClass { if (!isString(collectionId)) { throw new Error( "firebase.firestore().collectionGroup(*) 'collectionId' must be a string value.", @@ -275,13 +267,11 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { ); } - return createDeprecationProxy( - new Query( - this as unknown as FirestoreInternal, - this._referencePath.child(collectionId), - new QueryModifiers().asCollectionGroupQuery(), - undefined, - ), + return new QueryClass( + this as unknown as FirestoreInternal, + this._referencePath.child(collectionId), + new QueryModifiers().asCollectionGroupQuery(), + undefined, ); } @@ -289,7 +279,7 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { await this.native.disableNetwork(); } - doc(documentPath: string): DocumentReference { + doc(documentPath: string): DocumentReferenceClass { if (!isString(documentPath)) { throw new Error("firebase.firestore().doc(*) 'documentPath' must be a string value."); } @@ -304,9 +294,7 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { throw new Error("firebase.firestore().doc(*) 'documentPath' must point to a document."); } - return createDeprecationProxy( - new DocumentReference(this as unknown as FirestoreInternal, path), - ); + return new DocumentReferenceClass(this as unknown as FirestoreInternal, path); } async enableNetwork(): Promise { @@ -461,57 +449,14 @@ class FirebaseFirestoreModule extends FirebaseModule<'RNFBFirestoreModule'> { return this.native.settings(settingsToApply); } - persistentCacheIndexManager(): FirestorePersistentCacheIndexManager | null { + persistentCacheIndexManager(): FirestorePersistentCacheIndexManagerClass | null { if (this._settings.persistence === false) { return null; } - return createDeprecationProxy( - new FirestorePersistentCacheIndexManager(this as unknown as FirestoreInternal), - ); + return new FirestorePersistentCacheIndexManagerClass(this as unknown as FirestoreInternal); } } -// import { SDK_VERSION } from '@react-native-firebase/firestore'; -export const SDK_VERSION = version; - -const firestoreNamespace = createModuleNamespace({ - statics: FirestoreStatics, - version, - namespace, - nativeModuleName: [...nativeModuleNames], - nativeEvents: [...nativeEvents], - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: true, - ModuleClass: FirebaseFirestoreModule, -}); - -type FirestoreNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseFirestoreTypes.Module, - FirebaseFirestoreTypes.Statics -> & { - firestore: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseFirestoreTypes.Module, - FirebaseFirestoreTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import firestore from '@react-native-firebase/firestore'; -// firestore().X(...); -export default firestoreNamespace as unknown as FirestoreNamespace; - -// import firestore, { firebase } from '@react-native-firebase/firestore'; -// firestore().X(...); -// firebase.firestore().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'firestore', - FirebaseFirestoreTypes.Module, - FirebaseFirestoreTypes.Statics, - true - >; - // Register the interop module for non-native platforms. for (const moduleName of nativeModuleNames) { setReactNativeModule(moduleName, fallBackModule); diff --git a/packages/firestore/lib/FirestoreQuery.ts b/packages/firestore/lib/FirestoreQuery.ts index 5050588084..c41092509b 100644 --- a/packages/firestore/lib/FirestoreQuery.ts +++ b/packages/firestore/lib/FirestoreQuery.ts @@ -16,8 +16,6 @@ */ import { - createDeprecationProxy, - filterModularArgument, isArray, isNull, isObject, @@ -27,14 +25,19 @@ import { import NativeError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; import { AggregateQuery } from './FirestoreAggregate'; import DocumentSnapshot from './FirestoreDocumentSnapshot'; -import FieldPath, { fromDotSeparatedString } from './FieldPath'; +import { FieldPath, fromDotSeparatedString } from './FieldPath'; import { _Filter, generateFilters } from './FirestoreFilter'; import QueryModifiers from './FirestoreQueryModifiers'; import QuerySnapshot, { type QuerySnapshotNativeData } from './FirestoreQuerySnapshot'; import { parseSnapshotArgs, validateWithConverter } from './utils'; import type FirestorePath from './FirestorePath'; -import type { DocumentData, FirestoreDataConverter, ListenSource } from './types/firestore'; +import type { + DocumentData, + FirestoreDataConverter, + ListenSource, + Query as QueryInterface, +} from './types/firestore'; import type { FirestoreInternal, DocumentFieldValueInternal, @@ -44,10 +47,11 @@ import type { let _id = 0; -export default class Query< +/** @internal */ +export class Query< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, -> { +> implements QueryInterface { _firestore: FirestoreInternal; _collectionPath: FirestorePath; _modifiers: QueryModifiers; @@ -173,14 +177,12 @@ export default class Query< return modifiers.setFieldsCursor(cursor, allFields); } - count(): ReturnType { - return createDeprecationProxy( - new AggregateQuery( - this._firestore, - this as unknown as Query, - this._collectionPath, - this._modifiers, - ), + count(): AggregateQuery { + return new AggregateQuery( + this._firestore, + this as unknown as Query, + this._collectionPath, + this._modifiers, ); } @@ -191,38 +193,34 @@ export default class Query< endAt( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] - ): ReturnType { - return createDeprecationProxy( - new Query( - this._firestore, - this._collectionPath, - this._handleQueryCursor( - 'endAt', - docOrField as DocumentSnapshot | DocumentFieldValueInternal, - filterModularArgument(fields), - ), - this._queryName, - this._converter, + ): Query { + return new Query( + this._firestore, + this._collectionPath, + this._handleQueryCursor( + 'endAt', + docOrField as DocumentSnapshot | DocumentFieldValueInternal, + fields, ), + this._queryName, + this._converter, ); } endBefore( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] - ): ReturnType { - return createDeprecationProxy( - new Query( - this._firestore, - this._collectionPath, - this._handleQueryCursor( - 'endBefore', - docOrField as DocumentSnapshot | DocumentFieldValueInternal, - filterModularArgument(fields), - ), - this._queryName, - this._converter, + ): Query { + return new Query( + this._firestore, + this._collectionPath, + this._handleQueryCursor( + 'endBefore', + docOrField as DocumentSnapshot | DocumentFieldValueInternal, + fields, ), + this._queryName, + this._converter, ); } @@ -320,7 +318,7 @@ export default class Query< return true; } - limit(limit: number): ReturnType { + limit(limit: number): Query { if (this._modifiers.isValidLimit(limit)) { throw new Error( "firebase.firestore().collection().limit(*) 'limit' must be a positive integer value.", @@ -329,12 +327,16 @@ export default class Query< const modifiers = this._modifiers._copy().limit(limit); - return createDeprecationProxy( - new Query(this._firestore, this._collectionPath, modifiers, this._queryName, this._converter), + return new Query( + this._firestore, + this._collectionPath, + modifiers, + this._queryName, + this._converter, ); } - limitToLast(limitToLast: number): ReturnType { + limitToLast(limitToLast: number): Query { if (this._modifiers.isValidLimitToLast(limitToLast)) { throw new Error( "firebase.firestore().collection().limitToLast(*) 'limitToLast' must be a positive integer value.", @@ -343,8 +345,12 @@ export default class Query< const modifiers = this._modifiers._copy().limitToLast(limitToLast); - return createDeprecationProxy( - new Query(this._firestore, this._collectionPath, modifiers, this._queryName, this._converter), + return new Query( + this._firestore, + this._collectionPath, + modifiers, + this._queryName, + this._converter, ); } @@ -360,7 +366,7 @@ export default class Query< this._modifiers.validatelimitToLast(); try { - const options = parseSnapshotArgs(filterModularArgument(args)); + const options = parseSnapshotArgs(args); snapshotListenOptions = options.snapshotListenOptions; callback = options.callback; onNext = options.onNext; @@ -429,10 +435,7 @@ export default class Query< return unsubscribe; } - orderBy( - fieldPath: string | FieldPath, - directionStr?: string, - ): ReturnType { + orderBy(fieldPath: string | FieldPath, directionStr?: string): Query { if (!isString(fieldPath) && !(fieldPath instanceof FieldPath)) { throw new Error( "firebase.firestore().collection().orderBy(*) 'fieldPath' must be a string or instance of FieldPath.", @@ -479,38 +482,38 @@ export default class Query< throw new Error(`firebase.firestore().collection().orderBy() ${(e as Error).message}`); } - return createDeprecationProxy( - new Query(this._firestore, this._collectionPath, modifiers, this._queryName, this._converter), + return new Query( + this._firestore, + this._collectionPath, + modifiers, + this._queryName, + this._converter, ); } startAfter( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] - ): ReturnType { - return createDeprecationProxy( - new Query( - this._firestore, - this._collectionPath, - this._handleQueryCursor('startAfter', docOrField, filterModularArgument(fields)), - this._queryName, - this._converter, - ), + ): Query { + return new Query( + this._firestore, + this._collectionPath, + this._handleQueryCursor('startAfter', docOrField, fields), + this._queryName, + this._converter, ); } startAt( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] - ): ReturnType { - return createDeprecationProxy( - new Query( - this._firestore, - this._collectionPath, - this._handleQueryCursor('startAt', docOrField, filterModularArgument(fields)), - this._queryName, - this._converter, - ), + ): Query { + return new Query( + this._firestore, + this._collectionPath, + this._handleQueryCursor('startAt', docOrField, fields), + this._queryName, + this._converter, ); } @@ -518,7 +521,7 @@ export default class Query< fieldPathOrFilter: string | FieldPath | _Filter, opStr?: string, value?: DocumentFieldValueInternal, - ): ReturnType { + ): Query { if ( !isString(fieldPathOrFilter) && !(fieldPathOrFilter instanceof FieldPath) && @@ -602,8 +605,12 @@ export default class Query< throw new Error(`firebase.firestore().collection().where() ${(e as Error).message}`); } - return createDeprecationProxy( - new Query(this._firestore, this._collectionPath, modifiers, this._queryName, this._converter), + return new Query( + this._firestore, + this._collectionPath, + modifiers, + this._queryName, + this._converter, ); } @@ -639,3 +646,5 @@ export default class Query< ); } } + +export default Query; diff --git a/packages/firestore/lib/FirestoreQueryModifiers.ts b/packages/firestore/lib/FirestoreQueryModifiers.ts index b9208e8e77..0ee967320d 100644 --- a/packages/firestore/lib/FirestoreQueryModifiers.ts +++ b/packages/firestore/lib/FirestoreQueryModifiers.ts @@ -16,7 +16,7 @@ */ import { isNumber } from '@react-native-firebase/app/dist/module/common'; -import FieldPath, { DOCUMENT_ID } from './FieldPath'; +import { FieldPath, DOCUMENT_ID, fieldPathOrSegmentsToNative } from './FieldPath'; import type { DocumentFieldValueInternal, FirestoreCursorFieldsInternal, @@ -118,14 +118,14 @@ export default class QueryModifiers { get filters(): FirestoreFilterSpecInternal[] { return this._filters.map(f => ({ ...f, - fieldPath: f.fieldPath instanceof FieldPath ? f.fieldPath._toArray() : f.fieldPath, + fieldPath: fieldPathOrSegmentsToNative(f.fieldPath!), })); } get orders(): FirestoreOrderSpecInternal[] { return this._orders.map(f => ({ ...f, - fieldPath: f.fieldPath instanceof FieldPath ? f.fieldPath._toArray() : f.fieldPath, + fieldPath: fieldPathOrSegmentsToNative(f.fieldPath), })); } diff --git a/packages/firestore/lib/FirestoreQuerySnapshot.ts b/packages/firestore/lib/FirestoreQuerySnapshot.ts index 16e5f2769f..ed8ec4002b 100644 --- a/packages/firestore/lib/FirestoreQuerySnapshot.ts +++ b/packages/firestore/lib/FirestoreQuerySnapshot.ts @@ -16,7 +16,6 @@ */ import { - createDeprecationProxy, isBoolean, isFunction, isObject, @@ -26,8 +25,8 @@ import DocumentChange from './FirestoreDocumentChange'; import DocumentSnapshot from './FirestoreDocumentSnapshot'; import SnapshotMetadata from './FirestoreSnapshotMetadata'; -import type Query from './FirestoreQuery'; -import type { DocumentData, FirestoreDataConverter } from './types/firestore'; +import type { Query as QueryImplementation } from './FirestoreQuery'; +import type { DocumentData, FirestoreDataConverter, Query } from './types/firestore'; import type { FirestoreInternal } from './types/internal'; export interface QuerySnapshotNativeData { @@ -62,7 +61,7 @@ export default class QuerySnapshot< constructor( firestore: FirestoreInternal, - query: Query, + query: QueryImplementation, nativeData: QuerySnapshotNativeData, converter: FirestoreDataConverter | null, ) { @@ -77,10 +76,9 @@ export default class QuerySnapshot< converter as unknown as FirestoreDataConverter | null, ), ); - this._docs = nativeData.documents.map((doc: QuerySnapshotNativeData['documents'][0]) => - createDeprecationProxy( + this._docs = nativeData.documents.map( + (doc: QuerySnapshotNativeData['documents'][0]) => new DocumentSnapshot(firestore, doc, converter), - ), ) as DocumentSnapshot[]; this._metadata = new SnapshotMetadata(nativeData.metadata ?? [false, false]); } diff --git a/packages/firestore/lib/FirestoreStatics.ts b/packages/firestore/lib/FirestoreStatics.ts index d616374f54..47756ebdbd 100644 --- a/packages/firestore/lib/FirestoreStatics.ts +++ b/packages/firestore/lib/FirestoreStatics.ts @@ -15,15 +15,14 @@ * */ -import { createDeprecationProxy } from '@react-native-firebase/app/dist/module/common'; import { getReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import Blob from './FirestoreBlob'; -import FieldPath from './FieldPath'; -import FieldValue from './FieldValue'; +import { Blob } from './FirestoreBlob'; +import { FieldPath } from './FieldPath'; +import { FieldValue } from './FieldValue'; import { Filter } from './FirestoreFilter'; -import FirestoreGeoPoint from './FirestoreGeoPoint'; -import FirestoreTimestamp from './FirestoreTimestamp'; -import FirestoreVectorValue from './FirestoreVectorValue'; +import { GeoPoint } from './FirestoreGeoPoint'; +import { Timestamp } from './FirestoreTimestamp'; +import { VectorValue } from './FirestoreVectorValue'; import type { LogLevel } from './types/firestore'; import type { RNFBFirestoreModule } from './types/internal'; @@ -32,13 +31,13 @@ type FirestoreLogLevel = LogLevel; const FirestoreStatics = { Blob: Blob, FieldPath: FieldPath, - FieldValue: createDeprecationProxy(FieldValue), - GeoPoint: FirestoreGeoPoint, - Timestamp: createDeprecationProxy(FirestoreTimestamp), - Filter: createDeprecationProxy(Filter), - VectorValue: FirestoreVectorValue, - vector(values?: number[]): FirestoreVectorValue { - return new FirestoreVectorValue(values); + FieldValue: FieldValue, + GeoPoint: GeoPoint, + Timestamp: Timestamp, + Filter: Filter, + VectorValue: VectorValue, + vector(values?: number[]): VectorValue { + return new VectorValue(values); }, CACHE_SIZE_UNLIMITED: -1, diff --git a/packages/firestore/lib/FirestoreTimestamp.ts b/packages/firestore/lib/FirestoreTimestamp.ts index d98b4f2d76..ddf0440a03 100644 --- a/packages/firestore/lib/FirestoreTimestamp.ts +++ b/packages/firestore/lib/FirestoreTimestamp.ts @@ -20,7 +20,7 @@ import { isDate, isNumber, isObject } from '@react-native-firebase/app/dist/modu // Earliest date supported by Firestore timestamps (0001-01-01T00:00:00Z). const MIN_SECONDS = -62135596800; -export default class Timestamp { +export class Timestamp { static now(): Timestamp { return Timestamp.fromMillis(Date.now()); } diff --git a/packages/firestore/lib/FirestoreTransaction.ts b/packages/firestore/lib/FirestoreTransaction.ts index b7f6727cdb..fa2e1d3cab 100644 --- a/packages/firestore/lib/FirestoreTransaction.ts +++ b/packages/firestore/lib/FirestoreTransaction.ts @@ -15,7 +15,7 @@ * */ -import { isObject, createDeprecationProxy } from '@react-native-firebase/app/dist/module/common'; +import { isObject } from '@react-native-firebase/app/dist/module/common'; import DocumentReference from './FirestoreDocumentReference'; import DocumentSnapshot from './FirestoreDocumentSnapshot'; import type { DocumentSnapshotNativeData } from './FirestoreDocumentSnapshot'; @@ -89,13 +89,11 @@ export default class Transaction { .transactionGetDocument(this._meta.id, documentRef.path) .then( (data: unknown) => - createDeprecationProxy( - new DocumentSnapshot( - this._firestore, - data as DocumentSnapshotNativeData, - documentRef.converter, - ), - ) as DocumentSnapshotType, + new DocumentSnapshot( + this._firestore, + data as DocumentSnapshotNativeData, + documentRef.converter, + ) as unknown as DocumentSnapshotType, ); } diff --git a/packages/firestore/lib/FirestoreVectorValue.ts b/packages/firestore/lib/FirestoreVectorValue.ts index 2b2cd7aae3..6e7943f855 100644 --- a/packages/firestore/lib/FirestoreVectorValue.ts +++ b/packages/firestore/lib/FirestoreVectorValue.ts @@ -19,7 +19,7 @@ import { isArray, isNumber, isObject } from '@react-native-firebase/app/dist/mod type FirestoreVectorJson = { vectorValues: number[]; type?: string }; -export default class VectorValue { +export class VectorValue { _values: number[]; constructor(values?: number[]) { diff --git a/packages/firestore/lib/index.ts b/packages/firestore/lib/index.ts index d2025e2387..42ecce7b02 100644 --- a/packages/firestore/lib/index.ts +++ b/packages/firestore/lib/index.ts @@ -15,10 +15,25 @@ * */ -// Export modular API functions +/** + * Modular Firestore API for React Native Firebase. + * + * @packageDocumentation + */ + +import './types/internal'; +import './FieldValue'; +import './FirestoreModule'; + +export { FieldPath } from './FieldPath'; +export { FieldValue } from './FieldValue'; +export { GeoPoint } from './FirestoreGeoPoint'; +export { Timestamp } from './FirestoreTimestamp'; +export { VectorValue } from './FirestoreVectorValue'; +export { Bytes } from './modular/Bytes'; +export { AggregateField, AggregateQuerySnapshot } from './FirestoreAggregate'; export * from './modular'; -// Export modular/public type helpers. export type { FirebaseApp, Firestore, @@ -56,20 +71,13 @@ export type { UpdateData, WithFieldValue, FirestoreDataConverter, - Query, CollectionReference, DocumentReference, + Query, DocumentSnapshot, - LoadBundleTask, QueryDocumentSnapshot, QuerySnapshot, SnapshotMetadata, Transaction, WriteBatch, } from './types/firestore'; - -// Export namespaced API -export type { FirebaseFirestoreTypes } from './types/namespaced'; - -export * from './namespaced'; -export { default } from './namespaced'; diff --git a/packages/firestore/lib/modular.ts b/packages/firestore/lib/modular.ts index 6dc9573150..63add9385a 100644 --- a/packages/firestore/lib/modular.ts +++ b/packages/firestore/lib/modular.ts @@ -15,22 +15,26 @@ * */ -import { getApp, setLogLevel as appSetLogLevel } from '@react-native-firebase/app'; -import { isObject, MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; +import { getOrCreateModularInstance } from '@react-native-firebase/app/dist/module/internal'; +import { isObject } from '@react-native-firebase/app/dist/module/common'; +import { Query as QueryClass } from './FirestoreQuery'; +import { FirebaseFirestoreModule, config } from './FirestoreModule'; +import FirestoreStatics from './FirestoreStatics'; +import { LoadBundleTask } from './LoadBundleTask'; +import { version } from './version'; import { AggregateField, fieldPathFromArgument, AggregateQuerySnapshot, } from './FirestoreAggregate'; -import QueryImpl from './FirestoreQuery'; -import { LoadBundleTask } from './LoadBundleTask'; +import { PersistentCacheIndexManager } from './FirestorePersistentCacheIndexManager'; +import type { FirebaseApp } from '@react-native-firebase/app'; import type { CollectionReference, DocumentData, DocumentReference, Firestore, FirestoreSettings, - FirebaseApp, EmulatorMockTokenOptions, Query, SetOptions, @@ -40,8 +44,9 @@ import type { PartialWithFieldValue, WriteBatch, AggregateSpec, - LoadBundleTaskProgress, LogLevel, + Unsubscribe, + LoadBundleTaskProgress, } from './types/firestore'; import type { CollectionReferenceInternal, @@ -50,59 +55,76 @@ import type { ParentReferenceInternal, PersistentCacheIndexManagerInternal, QueryInternal, - QueryWithAggregateInternals, ReferenceInternal, - AppWithFirestoreInternal, FirestoreAggregateQuerySpecInternal, FirestoreAggregateQueryResultInternal, + QueryWithAggregateInternals, } from './types/internal'; -import { PersistentCacheIndexManager } from './FirestorePersistentCacheIndexManager'; -import type { FieldPath } from './modular/FieldPath'; -import type { Unsubscribe } from './types/firestore'; +import type { FieldPath } from './FieldPath'; -export { AggregateField, AggregateQuerySnapshot } from './FirestoreAggregate'; +// react-native at least through 0.77 does not correctly support URL.host, which +// is needed by firebase-js-sdk. It appears that in 0.80+ it is supported, so this +// (and the package.json entry for this package) should be removed when the minimum +// supported version of react-native is 0.80 or higher. +import 'react-native-url-polyfill/auto'; + +export const SDK_VERSION = version; -export const CACHE_SIZE_UNLIMITED = -1; const PIPELINE_RUNTIME_INSTALLER_SYMBOL = Symbol.for('RNFBFirestorePipelineRuntimeInstaller'); type GlobalWithPipelineInstaller = typeof globalThis & { [PIPELINE_RUNTIME_INSTALLER_SYMBOL]?: (firestore?: FirestoreInternal) => void; }; +export const CACHE_SIZE_UNLIMITED = -1; export function getFirestore(): Firestore; export function getFirestore(app: FirebaseApp): Firestore; export function getFirestore(app: FirebaseApp, databaseId: string): Firestore; +export function getFirestore(databaseId: string): Firestore; export function getFirestore( appOrDatabaseId?: FirebaseApp | string, databaseId?: string, ): Firestore { - const app = (name?: string) => getApp(name) as unknown as AppWithFirestoreInternal; - let firestore: Firestore; + let firestore: FirestoreInternal; if (typeof appOrDatabaseId === 'string') { - firestore = app().firestore(appOrDatabaseId); + firestore = getOrCreateModularInstance( + FirebaseFirestoreModule, + config, + undefined, + appOrDatabaseId, + ) as unknown as FirestoreInternal; } else if (appOrDatabaseId) { - if (databaseId) { - firestore = app(appOrDatabaseId.name).firestore(databaseId); - } else { - firestore = app(appOrDatabaseId.name).firestore(); - } + firestore = getOrCreateModularInstance( + FirebaseFirestoreModule, + config, + appOrDatabaseId, + databaseId, + ) as unknown as FirestoreInternal; } else if (databaseId) { - firestore = app().firestore(databaseId); + firestore = getOrCreateModularInstance( + FirebaseFirestoreModule, + config, + undefined, + databaseId, + ) as unknown as FirestoreInternal; } else { - firestore = app().firestore(); + firestore = getOrCreateModularInstance( + FirebaseFirestoreModule, + config, + ) as unknown as FirestoreInternal; } const runtimeGlobal = globalThis as GlobalWithPipelineInstaller; const installPipelineRuntime = runtimeGlobal[PIPELINE_RUNTIME_INSTALLER_SYMBOL]; if (typeof installPipelineRuntime === 'function') { try { - installPipelineRuntime(firestore as FirestoreInternal); + installPipelineRuntime(firestore); } catch { // Avoid changing getFirestore behavior if optional pipeline runtime install fails. } } - return firestore; + return firestore as Firestore; } export function doc( @@ -136,11 +158,10 @@ export function doc segment.replace(/^\/|\/$/g, '')).join('/'); } - return (parent as unknown as ParentReferenceInternal).doc.call( - parent, - resolvedPath, - MODULAR_DEPRECATION_ARG, - ) as DocumentReference; + return (parent as unknown as ParentReferenceInternal).doc(resolvedPath) as DocumentReference< + AppModelType, + DbModelType + >; } export function collection( @@ -174,10 +195,8 @@ export function collection< resolvedPath = `${resolvedPath}/${pathSegments.map(segment => segment.replace(/^\/|\/$/g, '')).join('/')}`; } - return (parent as unknown as ParentReferenceInternal).collection.call( - parent, + return (parent as unknown as ParentReferenceInternal).collection( resolvedPath, - MODULAR_DEPRECATION_ARG, ) as CollectionReference; } @@ -189,22 +208,17 @@ export function refEqual( | DocumentReference | CollectionReference, ): boolean { - return (left as unknown as ReferenceInternal).isEqual.call( - left, - right, - MODULAR_DEPRECATION_ARG, - ); + return (left as unknown as ReferenceInternal).isEqual(right); } export function collectionGroup( firestore: Firestore, collectionId: string, ): Query { - return (firestore as FirestoreInternal).collectionGroup.call( - firestore, - collectionId, - MODULAR_DEPRECATION_ARG, - ) as Query; + return (firestore as FirestoreInternal).collectionGroup(collectionId) as Query< + DocumentData, + DocumentData + >; } let snapshotInSyncListenerId = 0; @@ -254,11 +268,9 @@ export function setDoc( data: WithFieldValue | PartialWithFieldValue, options?: SetOptions, ): Promise { - return (reference as unknown as DocumentReferenceInternal).set.call( - reference, + return (reference as unknown as DocumentReferenceInternal).set( data, options, - MODULAR_DEPRECATION_ARG, ); } @@ -281,43 +293,35 @@ export function updateDoc( const ref = reference as unknown as DocumentReferenceInternal; if (!fieldOrUpdateData) { - return ref.update.call(reference, MODULAR_DEPRECATION_ARG); + return ref.update(); } if (!value) { - return ref.update.call(reference, fieldOrUpdateData, MODULAR_DEPRECATION_ARG); + return ref.update(fieldOrUpdateData); } if (!Array.isArray(moreFieldsAndValues)) { - return ref.update.call(reference, fieldOrUpdateData, value, MODULAR_DEPRECATION_ARG); + return ref.update(fieldOrUpdateData, value); } - return ref.update.call( - reference, - fieldOrUpdateData, - value, - ...moreFieldsAndValues, - MODULAR_DEPRECATION_ARG, - ); + return ref.update(fieldOrUpdateData, value, ...moreFieldsAndValues); } export function addDoc( reference: CollectionReference, data: WithFieldValue, ): Promise> { - return (reference as unknown as CollectionReferenceInternal).add.call( - reference, + return (reference as unknown as CollectionReferenceInternal).add( data, - MODULAR_DEPRECATION_ARG, ) as Promise>; } export function enableNetwork(firestore: Firestore): Promise { - return (firestore as FirestoreInternal).enableNetwork.call(firestore, MODULAR_DEPRECATION_ARG); + return (firestore as FirestoreInternal).enableNetwork(); } export function disableNetwork(firestore: Firestore): Promise { - return (firestore as FirestoreInternal).disableNetwork.call(firestore, MODULAR_DEPRECATION_ARG); + return (firestore as FirestoreInternal).disableNetwork(); } export function clearPersistence(firestore: Firestore): Promise { @@ -326,18 +330,15 @@ export function clearPersistence(firestore: Firestore): Promise { } export function clearIndexedDbPersistence(firestore: Firestore): Promise { - return (firestore as FirestoreInternal).clearPersistence.call(firestore, MODULAR_DEPRECATION_ARG); + return (firestore as FirestoreInternal).clearPersistence(); } export function terminate(firestore: Firestore): Promise { - return (firestore as FirestoreInternal).terminate.call(firestore, MODULAR_DEPRECATION_ARG); + return (firestore as FirestoreInternal).terminate(); } export function waitForPendingWrites(firestore: Firestore): Promise { - return (firestore as FirestoreInternal).waitForPendingWrites.call( - firestore, - MODULAR_DEPRECATION_ARG, - ); + return (firestore as FirestoreInternal).waitForPendingWrites(); } export async function initializeFirestore( @@ -345,10 +346,14 @@ export async function initializeFirestore( settings: FirestoreSettings, databaseId?: string, ): Promise { - const firebase = getApp(app.name) as unknown as { firestore(databaseId?: string): Firestore }; - const firestore = firebase.firestore(databaseId) as unknown as FirestoreInternal; - await firestore.settings.call(firestore, settings, MODULAR_DEPRECATION_ARG); - return firestore; + const firestore = getOrCreateModularInstance( + FirebaseFirestoreModule, + config, + app, + databaseId, + ) as unknown as FirestoreInternal; + await firestore.settings(settings); + return firestore as Firestore; } export function connectFirestoreEmulator( @@ -357,36 +362,25 @@ export function connectFirestoreEmulator( port: number, options?: { mockUserToken?: EmulatorMockTokenOptions | string }, ): void { - return (firestore as FirestoreInternal).useEmulator.call( - firestore, - host, - port, - options, - MODULAR_DEPRECATION_ARG, - ); + void options; + return (firestore as FirestoreInternal).useEmulator(host, port); } export function setLogLevel(logLevel: LogLevel): void { - return appSetLogLevel(logLevel); + return FirestoreStatics.setLogLevel(logLevel); } export function runTransaction( firestore: Firestore, updateFunction: (transaction: Transaction) => Promise, ): Promise { - return (firestore as FirestoreInternal).runTransaction.call( - firestore, - updateFunction, - MODULAR_DEPRECATION_ARG, - ) as Promise; + return (firestore as FirestoreInternal).runTransaction(updateFunction) as Promise; } export function getCountFromServer( query: Query, ): Promise }, AppModelType, DbModelType>> { - return (query as unknown as QueryInternal).count - .call(query, MODULAR_DEPRECATION_ARG) - .get() as Promise< + return (query as unknown as QueryInternal).count().get() as Promise< AggregateQuerySnapshot<{ count: AggregateField }, AppModelType, DbModelType> >; } @@ -399,17 +393,19 @@ export function getAggregateFromServer< query: Query, aggregateSpec: AggregateSpecType, ): Promise> { - if (!(query instanceof QueryImpl)) { + if (!(query instanceof QueryClass)) { throw new Error( - '`getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `FirestoreQuery`', + '`getAggregateFromServer(*, aggregateSpec)` `query` must be an instance of `Query`', ); } + const queryWithInternals = query as unknown as QueryWithAggregateInternals; + if (!isObject(aggregateSpec)) { throw new Error('`getAggregateFromServer(query, *)` `aggregateSpec` must be an object'); } - const containsOneAggregateField = Object.values(aggregateSpec).find( + const containsOneAggregateField = Object.values(aggregateSpec).some( value => value instanceof AggregateField, ); if (!containsOneAggregateField) { @@ -448,7 +444,6 @@ export function getAggregateFromServer< } } - const queryWithInternals = query as QueryWithAggregateInternals; return queryWithInternals._firestore.native .aggregateQuery( queryWithInternals._collectionPath.relativeName, @@ -485,66 +480,49 @@ export function loadBundle( bundleData: ReadableStream | ArrayBuffer | string, ): LoadBundleTask { const task = new LoadBundleTask(); - (firestore as FirestoreInternal).loadBundle - .call(firestore, bundleData, MODULAR_DEPRECATION_ARG) + (firestore as FirestoreInternal) + .loadBundle(bundleData) .then(progress => task._completeWith(progress as LoadBundleTaskProgress)) .catch(error => task._failWith(error)); return task; } export function namedQuery(firestore: Firestore, name: string): Promise { - return Promise.resolve( - (firestore as FirestoreInternal).namedQuery.call(firestore, name, MODULAR_DEPRECATION_ARG), - ); + return Promise.resolve((firestore as FirestoreInternal).namedQuery(name)); } export function writeBatch(firestore: Firestore): WriteBatch { - return (firestore as FirestoreInternal).batch.call(firestore, MODULAR_DEPRECATION_ARG); + return (firestore as FirestoreInternal).batch(); } export function getPersistentCacheIndexManager( firestore: Firestore, ): PersistentCacheIndexManager | null { - return (firestore as FirestoreInternal).persistentCacheIndexManager.call( - firestore, - MODULAR_DEPRECATION_ARG, - ); + return (firestore as FirestoreInternal).persistentCacheIndexManager(); } export function enablePersistentCacheIndexAutoCreation( indexManager: PersistentCacheIndexManager, ): Promise { - return (indexManager as PersistentCacheIndexManagerInternal).enableIndexAutoCreation.call( - indexManager, - MODULAR_DEPRECATION_ARG, - ); + return (indexManager as PersistentCacheIndexManagerInternal).enableIndexAutoCreation(); } export function disablePersistentCacheIndexAutoCreation( indexManager: PersistentCacheIndexManager, ): Promise { - return (indexManager as PersistentCacheIndexManagerInternal).disableIndexAutoCreation.call( - indexManager, - MODULAR_DEPRECATION_ARG, - ); + return (indexManager as PersistentCacheIndexManagerInternal).disableIndexAutoCreation(); } export function deleteAllPersistentCacheIndexes( indexManager: PersistentCacheIndexManager, ): Promise { - return (indexManager as PersistentCacheIndexManagerInternal).deleteAllIndexes.call( - indexManager, - MODULAR_DEPRECATION_ARG, - ); + return (indexManager as PersistentCacheIndexManagerInternal).deleteAllIndexes(); } export * from './modular/query'; export * from './modular/snapshot'; -export * from './modular/Bytes'; export * from './modular/FieldPath'; export * from './modular/FieldValue'; -export * from './modular/GeoPoint'; -export * from './modular/Timestamp'; export * from './modular/VectorValue'; export { LoadBundleTask } from './LoadBundleTask'; export { default as Transaction } from './FirestoreTransaction'; diff --git a/packages/firestore/lib/modular/Bytes.ts b/packages/firestore/lib/modular/Bytes.ts index d7e8e5a3a1..544c9e125f 100644 --- a/packages/firestore/lib/modular/Bytes.ts +++ b/packages/firestore/lib/modular/Bytes.ts @@ -15,7 +15,7 @@ * */ -import Blob from '../FirestoreBlob'; +import { Blob } from '../FirestoreBlob'; import type { FirestoreBlobInternal } from '../types/internal'; export class Bytes extends Blob { diff --git a/packages/firestore/lib/modular/FieldPath.ts b/packages/firestore/lib/modular/FieldPath.ts index 3ece42b11c..09458c8106 100644 --- a/packages/firestore/lib/modular/FieldPath.ts +++ b/packages/firestore/lib/modular/FieldPath.ts @@ -15,9 +15,7 @@ * */ -import FieldPath, { DOCUMENT_ID } from '../FieldPath'; - -export { FieldPath }; +import { DOCUMENT_ID, FieldPath } from '../FieldPath'; export function documentId(): FieldPath { return DOCUMENT_ID; diff --git a/packages/firestore/lib/modular/FieldValue.ts b/packages/firestore/lib/modular/FieldValue.ts index c2cf2e3a22..4a8947fb1c 100644 --- a/packages/firestore/lib/modular/FieldValue.ts +++ b/packages/firestore/lib/modular/FieldValue.ts @@ -15,9 +15,7 @@ * */ -import FieldValue from '../FieldValue'; - -export { FieldValue }; +import { FieldValue } from '../FieldValue'; export function deleteField(): FieldValue { return FieldValue.delete(); diff --git a/packages/firestore/lib/modular/GeoPoint.ts b/packages/firestore/lib/modular/GeoPoint.ts deleted file mode 100644 index e486271d63..0000000000 --- a/packages/firestore/lib/modular/GeoPoint.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import GeoPoint from '../FirestoreGeoPoint'; - -export { GeoPoint }; diff --git a/packages/firestore/lib/modular/Timestamp.ts b/packages/firestore/lib/modular/Timestamp.ts deleted file mode 100644 index 7c3420b4fa..0000000000 --- a/packages/firestore/lib/modular/Timestamp.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import Timestamp from '../FirestoreTimestamp'; - -export { Timestamp }; diff --git a/packages/firestore/lib/modular/VectorValue.ts b/packages/firestore/lib/modular/VectorValue.ts index c590474914..dd435b0df0 100644 --- a/packages/firestore/lib/modular/VectorValue.ts +++ b/packages/firestore/lib/modular/VectorValue.ts @@ -15,9 +15,7 @@ * */ -import VectorValue from '../FirestoreVectorValue'; - -export { VectorValue }; +import { VectorValue } from '../FirestoreVectorValue'; export function vector(values?: number[]): VectorValue { return new VectorValue(values); diff --git a/packages/firestore/lib/modular/query.ts b/packages/firestore/lib/modular/query.ts index 6a072cf59f..ddede6fee0 100644 --- a/packages/firestore/lib/modular/query.ts +++ b/packages/firestore/lib/modular/query.ts @@ -15,7 +15,6 @@ * */ -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; import { _Filter, Filter } from '../FirestoreFilter'; import type { DocumentData, @@ -30,13 +29,26 @@ import type { import type { DocumentReferenceDeleteInternal, DocumentReferenceGetInternal, - QueryFilterConstraintWithFilterInternal, QueryInternal, QueryWithMethodInternal, QueryWithWhereInternal, ReferenceIsEqualInternal, } from '../types/internal'; -import type { FieldPath } from './FieldPath'; +import type { FieldPath } from '../FieldPath'; + +function isCompositeFilterConstraint( + value: QueryFilterConstraint, +): value is QueryCompositeFilterConstraint { + return value instanceof QueryCompositeFilterConstraint; +} + +function getFilterFromConstraint(constraint: QueryFilterConstraint): _Filter { + if (isCompositeFilterConstraint(constraint)) { + return constraint._filter; + } + + return (constraint as QueryFieldFilterConstraint)._filter; +} /** * Abstraction of a constraint that can be applied to a Firestore query. @@ -86,7 +98,7 @@ abstract class QueryConstraintBase extends QueryConstraint { if (!method) { throw new Error(`Query method '${this.type}' is not available on query instance.`); } - return method.call(query, ...this._args, MODULAR_DEPRECATION_ARG); + return method.call(query, ...this._args); } } @@ -109,10 +121,10 @@ export class QueryCompositeFilterConstraint extends AppliableConstraint { // Validate nested OR filters when creating the constraint if (type === 'or') { const filters = _queryConstraints.map(constraint => { - if (constraint instanceof QueryCompositeFilterConstraint) { + if (isCompositeFilterConstraint(constraint)) { return constraint._filter; } - return (constraint as unknown as QueryFilterConstraintWithFilterInternal)._filter; + return getFilterFromConstraint(constraint); }); // This will throw if nested OR filters are detected Filter.or(...filters); @@ -124,22 +136,22 @@ export class QueryCompositeFilterConstraint extends AppliableConstraint { query: Query, ): Query { const filters = this._queryConstraints.map(constraint => { - if (constraint instanceof QueryCompositeFilterConstraint) { + if (isCompositeFilterConstraint(constraint)) { return constraint._filter; } - return (constraint as unknown as QueryFilterConstraintWithFilterInternal)._filter; + return getFilterFromConstraint(constraint); }); const _filter = this.type === 'or' ? Filter.or(...filters) : Filter.and(...filters); const where = (query as unknown as QueryWithWhereInternal).where; - return where.call(query, _filter, MODULAR_DEPRECATION_ARG); + return where.call(query, _filter); } get _filter(): _Filter { const filters = this._queryConstraints.map(constraint => { - if (constraint instanceof QueryCompositeFilterConstraint) { + if (isCompositeFilterConstraint(constraint)) { return constraint._filter; } - return (constraint as unknown as QueryFilterConstraintWithFilterInternal)._filter; + return getFilterFromConstraint(constraint); }); return this.type === 'or' ? Filter.or(...filters) : Filter.and(...filters); } @@ -180,13 +192,38 @@ export class QueryEndAtConstraint extends QueryConstraintBase { } } -export class QueryFieldFilterConstraint extends QueryConstraintBase { +export class QueryFieldFilterConstraint extends QueryConstraint { readonly type = 'where'; readonly _filter: _Filter; + private readonly _applyFilterOnly: boolean; + private readonly _fieldArgs?: [string | FieldPath, WhereFilterOp, unknown]; + + constructor( + fieldPathOrFilter: string | FieldPath | _Filter, + opStr?: WhereFilterOp, + value?: unknown, + ) { + super(); + if (fieldPathOrFilter instanceof _Filter && opStr === undefined && value === undefined) { + this._filter = fieldPathOrFilter; + this._applyFilterOnly = true; + return; + } - constructor(fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown) { - super(fieldPath, opStr, value); - this._filter = Filter(fieldPath, opStr, value); + this._filter = Filter(fieldPathOrFilter as string | FieldPath, opStr!, value); + this._applyFilterOnly = false; + this._fieldArgs = [fieldPathOrFilter as string | FieldPath, opStr!, value]; + } + + _apply( + query: Query, + ): Query { + const where = (query as unknown as QueryWithWhereInternal).where; + if (this._applyFilterOnly) { + return where.call(query, this._filter); + } + + return where.apply(query, this._fieldArgs!); } } @@ -233,8 +270,13 @@ export function where( fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown, +): QueryFieldFilterConstraint; +export function where( + fieldPath: string | FieldPath, + opStr?: WhereFilterOp, + value?: unknown, ): QueryFieldFilterConstraint { - return new QueryFieldFilterConstraint(fieldPath, opStr, value); + return new QueryFieldFilterConstraint(fieldPath as string | FieldPath | _Filter, opStr, value); } export function or(...queryConstraints: QueryFilterConstraint[]): QueryCompositeFilterConstraint { @@ -304,7 +346,7 @@ export function getDoc< reference: DocumentReference, ): Promise> { const get = (reference as unknown as DocumentReferenceGetInternal).get; - return get.call(reference, { source: 'default' }, MODULAR_DEPRECATION_ARG); + return get.call(reference, { source: 'default' }); } export function getDocFromCache< @@ -314,7 +356,7 @@ export function getDocFromCache< reference: DocumentReference, ): Promise> { const get = (reference as unknown as DocumentReferenceGetInternal).get; - return get.call(reference, { source: 'cache' }, MODULAR_DEPRECATION_ARG); + return get.call(reference, { source: 'cache' }); } export function getDocFromServer< @@ -324,7 +366,7 @@ export function getDocFromServer< reference: DocumentReference, ): Promise> { const get = (reference as unknown as DocumentReferenceGetInternal).get; - return get.call(reference, { source: 'server' }, MODULAR_DEPRECATION_ARG); + return get.call(reference, { source: 'server' }); } export function getDocs< @@ -332,7 +374,7 @@ export function getDocs< DbModelType extends DocumentData = DocumentData, >(queryRef: Query): Promise> { const get = (queryRef as unknown as QueryInternal).get; - return get.call(queryRef, { source: 'default' }, MODULAR_DEPRECATION_ARG); + return get.call(queryRef, { source: 'default' }); } export function getDocsFromCache< @@ -340,7 +382,7 @@ export function getDocsFromCache< DbModelType extends DocumentData = DocumentData, >(queryRef: Query): Promise> { const get = (queryRef as unknown as QueryInternal).get; - return get.call(queryRef, { source: 'cache' }, MODULAR_DEPRECATION_ARG); + return get.call(queryRef, { source: 'cache' }); } export function getDocsFromServer< @@ -348,7 +390,7 @@ export function getDocsFromServer< DbModelType extends DocumentData = DocumentData, >(queryRef: Query): Promise> { const get = (queryRef as unknown as QueryInternal).get; - return get.call(queryRef, { source: 'server' }, MODULAR_DEPRECATION_ARG); + return get.call(queryRef, { source: 'server' }); } export function deleteDoc< @@ -356,7 +398,7 @@ export function deleteDoc< DbModelType extends DocumentData = DocumentData, >(reference: DocumentReference): Promise { const remove = (reference as unknown as DocumentReferenceDeleteInternal).delete; - return remove.call(reference, MODULAR_DEPRECATION_ARG); + return remove.call(reference); } export function queryEqual< @@ -364,5 +406,5 @@ export function queryEqual< DbModelType extends DocumentData = DocumentData, >(left: Query, right: Query): boolean { const isEqual = (left as unknown as ReferenceIsEqualInternal).isEqual; - return isEqual.call(left, right, MODULAR_DEPRECATION_ARG); + return isEqual.call(left, right); } diff --git a/packages/firestore/lib/modular/snapshot.ts b/packages/firestore/lib/modular/snapshot.ts index 0325d8a967..0ea89e5cc4 100644 --- a/packages/firestore/lib/modular/snapshot.ts +++ b/packages/firestore/lib/modular/snapshot.ts @@ -15,7 +15,6 @@ * */ -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; import type { DocumentSnapshot, DocumentData, @@ -125,7 +124,7 @@ export function onSnapshot< ): Unsubscribe { const onSnapshotMethod = (reference as unknown as ReferenceWithOnSnapshotInternal).onSnapshot; - return onSnapshotMethod.call(reference, ...args, MODULAR_DEPRECATION_ARG); + return onSnapshotMethod.call(reference, ...args); } export function snapshotEqual( @@ -133,5 +132,5 @@ export function snapshotEqual( right: DocumentSnapshot | QuerySnapshot, ): boolean { const isEqual = (left as unknown as ReferenceIsEqualInternal).isEqual; - return isEqual.call(left, right, MODULAR_DEPRECATION_ARG); + return isEqual.call(left, right); } diff --git a/packages/firestore/lib/pipelines/expressions.ts b/packages/firestore/lib/pipelines/expressions.ts index bd114e0aac..ad3dbf27dd 100644 --- a/packages/firestore/lib/pipelines/expressions.ts +++ b/packages/firestore/lib/pipelines/expressions.ts @@ -16,7 +16,7 @@ */ import type { DocumentReference } from '../types/firestore'; -import type VectorValue from '../FirestoreVectorValue'; +import type { VectorValue } from '../FirestoreVectorValue'; import type { Bytes } from '../modular/Bytes'; /** diff --git a/packages/firestore/lib/pipelines/index.ts b/packages/firestore/lib/pipelines/index.ts index bfc0813a5d..36d2f135ff 100644 --- a/packages/firestore/lib/pipelines/index.ts +++ b/packages/firestore/lib/pipelines/index.ts @@ -17,7 +17,7 @@ import type { PipelineSource } from './pipeline-source'; import type { Pipeline } from './pipeline'; -import { installPipelineRuntime, registerPipelineRuntimeInstaller } from './pipeline_runtime'; +import { registerPipelineRuntimeInstaller } from './pipeline_runtime'; /** * @beta @@ -212,7 +212,6 @@ export { export { pipelineResultEqual } from './pipeline-result'; registerPipelineRuntimeInstaller(); -installPipelineRuntime(); declare module '../types/firestore' { /** diff --git a/packages/firestore/lib/pipelines/pipeline-result.ts b/packages/firestore/lib/pipelines/pipeline-result.ts index 7dc1fe6155..145f5df793 100644 --- a/packages/firestore/lib/pipelines/pipeline-result.ts +++ b/packages/firestore/lib/pipelines/pipeline-result.ts @@ -16,8 +16,8 @@ */ import type { DocumentData, DocumentReference } from '../types/firestore'; -import type Timestamp from '../FirestoreTimestamp'; -import type { FieldPath } from '../modular/FieldPath'; +import type { Timestamp } from '../FirestoreTimestamp'; +import type { FieldPath } from '../FieldPath'; import type { Field } from './expressions'; /** diff --git a/packages/firestore/lib/pipelines/pipeline_runtime.ts b/packages/firestore/lib/pipelines/pipeline_runtime.ts index f36fe1b395..b745dcfaa6 100644 --- a/packages/firestore/lib/pipelines/pipeline_runtime.ts +++ b/packages/firestore/lib/pipelines/pipeline_runtime.ts @@ -41,11 +41,12 @@ import type { DocumentData, } from '../types/firestore'; import FirestorePath from '../FirestorePath'; -import FirestoreTimestamp from '../FirestoreTimestamp'; +import { Timestamp as FirestoreTimestamp } from '../FirestoreTimestamp'; import DocumentReferenceClass from '../FirestoreDocumentReference'; -import FieldPath, { fromDotSeparatedString } from '../FieldPath'; +import { FieldPath, fromDotSeparatedString } from '../FieldPath'; import { extractFieldPathData } from '../utils'; import { parseNativeMap } from '../utils/serialize'; +import { getFirestore } from '../modular'; import type { AliasedAggregate, @@ -74,7 +75,6 @@ import type { PipelineUnnestOptions, } from './stage_options'; import type { PipelineExecuteOptions } from './pipeline_options'; -import { getFirestore } from '../modular'; import { validateExecuteOptions, validateSerializedPipeline } from './pipeline_validate'; import { createPipelineSubqueryExpression, type FunctionExpression } from './expressions'; diff --git a/packages/firestore/lib/pipelines/stage_options.ts b/packages/firestore/lib/pipelines/stage_options.ts index 9bb40892de..244eb964cc 100644 --- a/packages/firestore/lib/pipelines/stage_options.ts +++ b/packages/firestore/lib/pipelines/stage_options.ts @@ -17,7 +17,7 @@ import type { Pipeline } from './pipeline'; import type { DocumentReference, Query } from '../types/firestore'; -import type VectorValue from '../FirestoreVectorValue'; +import type { VectorValue } from '../FirestoreVectorValue'; import type { OneOf } from './types'; import type { Ordering, diff --git a/packages/firestore/lib/types/firestore.ts b/packages/firestore/lib/types/firestore.ts index 308774bd2d..26bf05234f 100644 --- a/packages/firestore/lib/types/firestore.ts +++ b/packages/firestore/lib/types/firestore.ts @@ -16,10 +16,11 @@ */ import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import type { FieldPath } from '../modular/FieldPath'; -import type { FieldValue } from '../modular/FieldValue'; +import type { FieldPath } from '../FieldPath'; +import type { FieldValue } from '../FieldValue'; import type { AggregateField } from '../FirestoreAggregate'; -import type { sum, average, count } from '../modular'; +import type { Query as QueryClass } from '../FirestoreQuery'; +import type { sum, average, count } from '../index'; // Canonical app/module aliases used by modular declarations. export type FirebaseApp = ReactNativeFirebase.FirebaseApp; @@ -339,7 +340,7 @@ export interface FirestoreDataConverter< ): AppModelType; } -export declare class Query< +export interface Query< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, > { @@ -355,7 +356,7 @@ export declare class Query< export declare class CollectionReference< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, -> extends Query { +> extends QueryClass { readonly type: 'collection'; id: string; parent: DocumentReference | null; diff --git a/packages/firestore/lib/types/internal.ts b/packages/firestore/lib/types/internal.ts index d0d2b5ef87..5bb75c0365 100644 --- a/packages/firestore/lib/types/internal.ts +++ b/packages/firestore/lib/types/internal.ts @@ -41,10 +41,10 @@ import type { import type { PersistentCacheIndexManager } from '../FirestorePersistentCacheIndexManager'; import type { QueryConstraint } from '../modular/query'; import type { _Filter } from '../FirestoreFilter'; -import type FirestoreTimestamp from '../FirestoreTimestamp'; -import Blob from '../FirestoreBlob'; +import type { Timestamp } from '../FirestoreTimestamp'; +import { Blob } from '../FirestoreBlob'; -/** Optional final argument passed by modular API wrappers (MODULAR_DEPRECATION_ARG). */ +/** Reserved optional trailing argument on legacy internal method signatures. */ export type FirestoreModularDeprecationArg = string; /** Query type passed to native ('collection' or 'collectionGroup'). */ @@ -351,7 +351,7 @@ export interface FirestorePipelineExecuteOptionsInternal { /** Timestamp shape received from native pipeline execution. */ export type FirestorePipelineTimestampInternal = - | FirestoreTimestamp + | Timestamp | { seconds?: number; nanoseconds?: number } | [number, number] | number; @@ -536,7 +536,7 @@ declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { } } -// Helper type for wrappers that forward MODULAR_DEPRECATION_ARG via .call(...). +// Helper type for internal wrappers that accept an optional trailing sentinel argument. export type WithModularDeprecationArg = F extends (...args: infer P) => infer R ? (...args: [...P, FirestoreModularDeprecationArg?]) => R : never; diff --git a/packages/firestore/lib/types/namespaced.ts b/packages/firestore/lib/types/namespaced.ts deleted file mode 100644 index 4aaac6f448..0000000000 --- a/packages/firestore/lib/types/namespaced.ts +++ /dev/null @@ -1,2627 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ReactNativeFirebase } from '@react-native-firebase/app'; -import type { ListenSource } from './firestore'; - -/** - * Firebase Cloud Firestore package for React Native. - * - * #### Example: Access the firebase export from the `firestore` package: - * - * ```js - * import { firebase } from '@react-native-firebase/firestore'; - * - * // firebase.firestore().X - * ``` - * - * #### Example: Using the default export from the `firestore` package: - * - * ```js - * import firestore from '@react-native-firebase/firestore'; - * - * // firestore().X - * ``` - * - * #### Example: Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/firestore'; - * - * // firebase.firestore().X - * ``` - */ -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace FirebaseFirestoreTypes { - type _FirebaseModule = ReactNativeFirebase.FirebaseModule; - /** - * An instance of Filter used to generate Firestore Filter queries. - */ - - export type QueryFilterType = 'OR' | 'AND'; - - export interface QueryFieldFilterConstraint { - fieldPath: keyof T | FieldPath; - operator: WhereFilterOp; - value: any; - } - - export interface QueryCompositeFilterConstraint { - operator: QueryFilterType; - queries: QueryFieldFilterConstraint[]; - } - - export type QueryFilterConstraint = - | QueryFieldFilterConstraint - | QueryCompositeFilterConstraint; - - /** - * The Filter functions used to generate an instance of Filter. - */ - export interface FilterFunction { - /** - * The Filter function used to generate an instance of Filter. - * e.g. Filter('name', '==', 'Ada') - */ - ( - fieldPath: keyof T | FieldPath, - operator: WhereFilterOp, - value: any, - ): QueryFieldFilterConstraint; - /** - * The Filter.or() static function used to generate a logical OR query using multiple Filter instances. - * e.g. Filter.or(Filter('name', '==', 'Ada'), Filter('name', '==', 'Bob')) - */ - or(...queries: QueryFilterConstraint[]): QueryCompositeFilterConstraint; - /** - * The Filter.and() static function used to generate a logical AND query using multiple Filter instances. - * e.g. Filter.and(Filter('name', '==', 'Ada'), Filter('name', '==', 'Bob')) - */ - and(...queries: QueryFilterConstraint[]): QueryCompositeFilterConstraint; - } - /** - * The Filter function used to generate an instance of Filter. - * e.g. Filter('name', '==', 'Ada') - */ - export declare const Filter: FilterFunction; - - /** - * An immutable object representing an array of bytes. - */ - export declare class Blob { - /** - * Creates a new Blob from the given Base64 string, converting it to bytes. - * - * @param base64 The Base64 string used to create the Blob object. - */ - static fromBase64String(base64: string): Blob; - - /** - * Creates a new Blob from the given Uint8Array. - * - * @param array The Uint8Array used to create the Blob object. - */ - static fromUint8Array(array: Uint8Array): Blob; - - /** - * Returns true if this `Blob` is equal to the provided one. - * - * @param other The `Blob` to compare against. - */ - isEqual(other: Blob): boolean; - - /** - * Returns the bytes of a Blob as a Base64-encoded string. - */ - toBase64(): string; - - /** - * Returns the bytes of a Blob in a new Uint8Array. - */ - toUint8Array(): Uint8Array; - } - - /** - * A `DocumentData` object represents the data in a document. - */ - export interface DocumentData { - [key: string]: any; - } - - /** - * A `CollectionReference` object can be used for adding documents, getting document references, and querying for - * documents (using the methods inherited from `Query`). - */ - export declare class CollectionReference< - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > extends Query { - /** - * The collection's identifier. - */ - id: string; - - /** - * A reference to the containing `DocumentReference` if this is a subcollection. If this isn't a - * subcollection, the reference is null. - */ - parent: DocumentReference | null; - - /** - * A string representing the path of the referenced collection (relative to the root of the database). - */ - path: string; - - /** - * Add a new document to this collection with the specified data, assigning it a document ID automatically. - * - * #### Example - * - * ```js - * const documentRef = await firebase.firestore().collection('users').add({ - * name: 'Ada Lovelace', - * age: 30, - * }); - * ``` - * - * @param data An Object containing the data for the new document. - */ - add(data: AppModelType): Promise>; - - /** - * Get a DocumentReference for the document within the collection at the specified path. If no - * path is specified, an automatically-generated unique ID will be used for the returned DocumentReference. - * - * #### Example - * - * ```js - * await firebase.firestore().collection('users').doc('alovelace').set({ - * name: 'Ada Lovelace', - * age: 30, - * }); - * ``` - * - * @param documentPath A slash-separated path to a document. - */ - doc(documentPath?: string): DocumentReference; - - /** - * Applies a custom data converter to this CollectionReference, allowing you - * to use your own custom model objects with Firestore. When you call add() - * on the returned CollectionReference instance, the provided converter will - * convert between Firestore data and your custom type U. - * - * Passing in `null` as the converter parameter removes the current - * converter. - * - * @param converter Converts objects to and from Firestore. Passing in - * `null` removes the current converter. - * @return A CollectionReference that uses the provided converter. - */ - withConverter(converter: null): CollectionReference; - - /** - * Applies a custom data converter to this CollectionReference, allowing you - * to use your own custom model objects with Firestore. When you call add() - * on the returned CollectionReference instance, the provided converter will - * convert between Firestore data and your custom type U. - * - * Passing in `null` as the converter parameter removes the current - * converter. - * - * @param converter Converts objects to and from Firestore. Passing in - * `null` removes the current converter. - * @return A CollectionReference that uses the provided converter. - */ - withConverter< - NewAppModelType extends DocumentData, - NewDbModelType extends DocumentData = DocumentData, - >( - converter: FirestoreDataConverter, - ): CollectionReference; - } - - /** - * A DocumentChange represents a change to the documents matching a query. It contains the document affected and the - * type of change that occurred. - */ - export interface DocumentChange< - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > { - /** - * The document affected by this change. - */ - doc: QueryDocumentSnapshot; - - /** - * The index of the changed document in the result set immediately after this `DocumentChange` - * (i.e. supposing that all prior `DocumentChange` objects and the current `DocumentChange` object have been applied). - * Is -1 for 'removed' events. - */ - newIndex: number; - - /** - * The index of the changed document in the result set immediately prior to this `DocumentChange` (i.e. - * supposing that all prior `DocumentChange` objects have been applied). Is -1 for 'added' events. - */ - oldIndex: number; - - /** - * The type of change ('added', 'modified', or 'removed'). - */ - type: DocumentChangeType; - } - - /** - * The type of a DocumentChange may be 'added', 'removed', or 'modified'. - */ - export type DocumentChangeType = 'added' | 'removed' | 'modified'; - - /** - * An options object that configures how document snapshot data is returned (e.g. server timestamps). - */ - export type SnapshotOptions = { - readonly serverTimestamps?: 'estimate' | 'previous' | 'none'; - }; - - /** - * The types for a DocumentSnapshot field that are supported by Firestore. - */ - export type DocumentFieldType = - | string - | number - | boolean - | { [key: string]: DocumentFieldType } - | DocumentFieldType[] - | null - | Timestamp - | GeoPoint - | Blob - | FieldPath - | FieldValue - | DocumentReference - | CollectionReference; - - /** - * A `DocumentReference` refers to a document location in a Firestore database and can be used to write, read, or listen - * to the location. The document at the referenced location may or may not exist. A `DocumentReference` can also be used - * to create a `CollectionReference` to a subcollection. - */ - export declare class DocumentReference< - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > { - /** - * The Firestore instance the document is in. This is useful for performing transactions, for example. - */ - firestore: Module; - - /** - * The document's identifier within its collection. - */ - id: string; - - /** - * The Collection this `DocumentReference` belongs to. - */ - parent: CollectionReference; - - /** - * A string representing the path of the referenced document (relative to the root of the database). - */ - path: string; - - /** - * Gets a `CollectionReference` instance that refers to the collection at the specified path. - * - * #### Example - * - * ```js - * const collectionRef = firebase.firestore().doc('users/alovelace').collection('orders'); - * ``` - * - * @param collectionPath A slash-separated path to a collection. - */ - collection(collectionPath: string): CollectionReference; - - /** - * Deletes the document referred to by this DocumentReference. - * - * #### Example - * - * ```js - * await firebase.firestore().doc('users/alovelace').delete(); - * ``` - * - * The Promise is resolved once the document has been successfully deleted from the backend - * (Note that it won't resolve while you're offline). - */ - delete(): Promise; - - /** - * Reads the document referred to by this DocumentReference. - * - * Note: By default, get() attempts to provide up-to-date data when possible by waiting for data - * from the server, but it may return cached data or fail if you are offline and the server cannot - * be reached. This behavior can be altered via the GetOptions parameter. - * - * #### Example - * - * ```js - * await firebase.firestore().doc('users/alovelace').get({ - * source: 'server', - * }); - * ``` - * - * @param options An object to configure the get behavior. - */ - get(options?: GetOptions): Promise>; - - /** - * Returns true if this DocumentReference is equal to the provided one. - * - * #### Example - * - * ```js - * const alovelace = firebase.firestore().doc('users/alovelace'); - * const dsmith = firebase.firestore().doc('users/dsmith'); - * - * // false - * alovelace.isEqual(dsmith); - * ``` - * - * @param other The `DocumentReference` to compare against. - */ - isEqual(other: DocumentReference): boolean; - - /** - * Attaches a listener for DocumentSnapshot events. - * - * NOTE: Although an complete callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().doc('users/alovelace') - * .onSnapshot({ - * error: (e) => console.error(e), - * next: (documentSnapshot) => {}, - * }); - * - * unsubscribe(); - * ``` - * - * @param observer A single object containing `next` and `error` callbacks. - */ - onSnapshot(observer: { - complete?: () => void; - error?: (error: Error) => void; - next?: (snapshot: DocumentSnapshot) => void; - }): () => void; - - /** - * Attaches a listener for DocumentSnapshot events with snapshot listener options. - * - * NOTE: Although an complete callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().doc('users/alovelace') - * .onSnapshot({ - * includeMetadataChanges: true, - * }, { - * error: (e) => console.error(e), - * next: (documentSnapshot) => {}, - * }); - * - * unsubscribe(); - * ``` - * - * @param options Options controlling the listen behavior. - * @param observer A single object containing `next` and `error` callbacks. - */ - onSnapshot( - options: SnapshotListenOptions, - observer: { - complete?: () => void; - error?: (error: Error) => void; - next?: (snapshot: DocumentSnapshot) => void; - }, - ): () => void; - - /** - * Attaches a listener for DocumentSnapshot events. - * - * NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().doc('users/alovelace') - * .onSnapshot( - * (documentSnapshot) => {}, // onNext - * (error) => console.error(error), // onError - * ); - * - * unsubscribe(); - * ``` - * @param onNext A callback to be called every time a new `DocumentSnapshot` is available. - * @param onError A callback to be called if the listen fails or is cancelled. No further callbacks will occur. - * @param onCompletion An optional function which will never be called. - */ - onSnapshot( - onNext: (snapshot: DocumentSnapshot) => void, - onError?: (error: Error) => void, - onCompletion?: () => void, - ): () => void; - - /** - * Attaches a listener for DocumentSnapshot events with snapshot listener options. - * - * NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().doc('users/alovelace') - * .onSnapshot( - * { source: 'cache', includeMetadataChanges: true }, // SnapshotListenerOptions - * (documentSnapshot) => {}, // onNext - * (error) => console.error(error), // onError - * ); - * - * unsubscribe(); - * ``` - * @param options Options controlling the listen behavior. - * @param onNext A callback to be called every time a new `DocumentSnapshot` is available. - * @param onError A callback to be called if the listen fails or is cancelled. No further callbacks will occur. - * @param onCompletion An optional function which will never be called. - */ - onSnapshot( - options: SnapshotListenOptions, - onNext: (snapshot: DocumentSnapshot) => void, - onError?: (error: Error) => void, - onCompletion?: () => void, - ): () => void; - - /** - * Writes to the document referred to by this DocumentReference. If the document does not yet - * exist, it will be created. If you pass SetOptions, the provided data can be merged into an - * existing document. - * - * #### Example - * - * ```js - * const user = firebase.firestore().doc('users/alovelace'); - * - * // Set new data - * await user.set({ - * name: 'Ada Lovelace', - * age: 30, - * city: 'LON', - * }); - * ``` - * - * @param data A map of the fields and values for the document. - * @param options An object to configure the set behavior. - */ - set(data: SetValue, options?: SetOptions): Promise; - - /** - * Updates fields in the document referred to by this `DocumentReference`. The update will fail - * if applied to a document that does not exist. - * - * #### Example - * - * ``` - * const user = firebase.firestore().doc('users/alovelace'); - * - * // Update age but leave other fields untouched - * await user.update({ - * age: 31, - * }); - * ``` - * - * @param data An object containing the fields and values with which to update the document. Fields can contain dots to reference nested fields within the document. - */ - update(data: Partial>): Promise; - - /** - * Updates fields in the document referred to by this DocumentReference. The update will fail if - * applied to a document that does not exist. - * - * #### Example - * - * ``` - * const user = firebase.firestore().doc('users/alovelace'); - * - * // Update age & city but leve other fields untouched - * await user.update('age', 31, 'city', 'SF'); - * ``` - * - * @param field The first field to update. - * @param value The first value. - * @param moreFieldsAndValues Additional key value pairs. - */ - update( - field: keyof DbModelType | FieldPath, - value: any, - ...moreFieldsAndValues: any[] - ): Promise; - - /** - * Removes the current converter. - * - * @param converter Passing in `null` removes the current converter. - * @return A DocumentReference that uses the provided converter. - */ - withConverter(converter: null): DocumentReference; - - /** - * Applies a custom data converter to this `DocumentReference`, allowing you - * to use your own custom model objects with Firestore. When you call setDoc - * getDoc, etc. with the returned `DocumentReference` - * instance, the provided converter will convert between Firestore data of - * type `NewDbModelType` and your custom type `NewAppModelType`. - * - * @param converter - Converts objects to and from Firestore. - * @returns A `DocumentReference` that uses the provided converter. - */ - withConverter< - NewAppModelType extends DocumentData, - NewDbModelType extends DocumentData = DocumentData, - >( - converter: FirestoreDataConverter, - ): DocumentReference; - } - - /** - * A DocumentSnapshot contains data read from a document in your Firestore database. The data can be extracted with - * .`data()` or `.get(:field)` to get a specific field. - * - * For a DocumentSnapshot that points to a non-existing document, any data access will return 'undefined'. - * You can use the `exists()` method to explicitly verify a document's existence. - */ - export interface DocumentSnapshot< - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > { - /** - * Method of the `DocumentSnapshot` that signals whether or not the data exists. True if the document exists. - */ - exists(): boolean; - - /** - * Property of the `DocumentSnapshot` that provides the document's ID. - */ - id: string; - - /** - * Metadata about the `DocumentSnapshot`, including information about its source and local modifications. - */ - metadata: SnapshotMetadata; - - /** - * The `DocumentReference` for the document included in the `DocumentSnapshot`. - */ - ref: DocumentReference; - - /** - * Retrieves all fields in the document as an Object. Returns 'undefined' if the document doesn't exist. - * - * #### Example - * - * ```js - * const user = await firebase.firestore().doc('users/alovelace').get(); - * - * console.log('User', user.data()); - * ``` - */ - data(): AppModelType | undefined; - - /** - * Retrieves the field specified by fieldPath. Returns undefined if the document or field doesn't exist. - * - * #### Example - * - * ```js - * const user = await firebase.firestore().doc('users/alovelace').get(); - * - * console.log('Address ZIP Code', user.get('address.zip')); - * ``` - * - * @param fieldPath The path (e.g. 'foo' or 'foo.bar') to a specific field. - */ - get( - fieldPath: keyof AppModelType | string | FieldPath, - ): fieldType; - - /** - * Returns true if this `DocumentSnapshot` is equal to the provided one. - * - * #### Example - * - * ```js - * const user1 = await firebase.firestore().doc('users/alovelace').get(); - * const user2 = await firebase.firestore().doc('users/dsmith').get(); - * - * // false - * user1.isEqual(user2); - * ``` - * - * @param other The `DocumentSnapshot` to compare against. - */ - isEqual(other: DocumentSnapshot): boolean; - } - - /** - * A QueryDocumentSnapshot contains data read from a document in your Firestore database as part of a query. - * The document is guaranteed to exist and its data can be extracted with .data() or .get(:field) to get a specific field. - * - * A QueryDocumentSnapshot offers the same API surface as a DocumentSnapshot. - * Since query results contain only existing documents, the exists() method will always be true and data() will never return 'undefined'. - */ - export interface QueryDocumentSnapshot< - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > extends DocumentSnapshot { - /** - * A QueryDocumentSnapshot is always guaranteed to exist. - */ - exists(): true; - - /** - * Retrieves all fields in the document as an Object. - * - * #### Example - * - * ```js - * const users = await firebase.firestore().collection('users').get(); - * - * for (const user of users.docs) { - * console.log('User', user.data()); - * } - * ``` - */ - data(): AppModelType; - } - - /** - * A FieldPath refers to a field in a document. The path may consist of a single field name (referring to a - * top-level field in the document), or a list of field names (referring to a nested field in the document). - * - * Create a FieldPath by providing field names. If more than one field name is provided, the path will point to a nested field in a document. - * - * #### Example - * - * ```js - * const user = await firebase.firestore().doc('users/alovelace').get(); - * - * // Create a new field path - * const fieldPath = new firebase.firestore.FieldPath('address', 'zip'); - * - * console.log('Address ZIP Code', user.get(fieldPath)); - * ``` - */ - export declare class FieldPath { - /** - * Returns a special sentinel `FieldPath` to refer to the ID of a document. It can be used in queries to sort or filter by the document ID. - */ - static documentId(): FieldPath; - - /** - * Creates a FieldPath from the provided field names. If more than one field name is provided, the path will point to a nested field in a document. - * - * #### Example - * - * ```js - * const fieldPath = new firebase.firestore.FieldPath('address', line', 'one'); - * ``` - * - * @param fieldNames A list of field names. - */ - constructor(...fieldNames: string[]); - - /** - * Returns true if this `FieldPath` is equal to the provided one. - * - * #### Example - * - * ```js - * const fieldPath1 = new firebase.firestore.FieldPath('address', 'zip'); - * const fieldPath2 = new firebase.firestore.FieldPath('address', line', 'one'); - * - * // false - * fieldPath1.isEqual(fieldPath2); - * ``` - * - * @param other The `FieldPath` to compare against. - */ - isEqual(other: FieldPath): boolean; - } - - /** - * Sentinel values that can be used when writing document fields with `set()` or `update()`. - * - * #### Example - * - * ```js - * const increment = firebase.firestore.FieldValue.increment(1); - * - * await firebase.firestore().doc('users/alovelace').update({ - * age: increment, // increment age by 1 - * }); - * ``` - */ - export declare class FieldValue { - /** - * Returns a special value that can be used with `set()` or `update()` that tells the server to remove the given elements - * from any array value that already exists on the server. All instances of each element specified will be removed from - * the array. If the field being modified is not already an array it will be overwritten with an empty array. - * - * #### Example - * - * ```js - * const arrayRemove = firebase.firestore.FieldValue.arrayRemove(2, '3'); - * - * // Removes the values 2 & '3' from the values array on the document - * await docRef.update({ - * values: arrayRemove, - * }); - * ``` - * - * @param elements The elements to remove from the array. - */ - static arrayRemove(...elements: any[]): FieldValue; - - /** - * Returns a special value that can be used with `set()` or `update()` that tells the server to union the given - * elements with any array value that already exists on the server. Each specified element that doesn't already exist - * in the array will be added to the end. If the field being modified is not already an array it will be overwritten - * with an array containing exactly the specified elements. - * - * #### Example - * - * ```js - * const arrayUnion = firebase.firestore.FieldValue.arrayUnion(2, '3'); - * - * // Appends the values 2 & '3' onto the values array on the document - * await docRef.update({ - * values: arrayUnion, - * }); - * ``` - * - * @param elements The elements to union into the array. - */ - static arrayUnion(...elements: any[]): FieldValue; - - /** - * Returns a sentinel for use with update() to mark a field for deletion. - * - * #### Example - * - * ```js - * const delete = firebase.firestore.FieldValue.delete(); - * - * // Deletes the name field on the document - * await docRef.update({ - * name: delete, - * }); - * ``` - */ - static delete(): FieldValue; - - /** - * Returns a special value that can be used with `set()` or `update()` that tells the server to increment the field's current value by the given value. - * - * If either the operand or the current field value uses floating point precision, all arithmetic follows IEEE 754 semantics. - * If both values are integers, values outside of JavaScript's safe number range (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`) - * are also subject to precision loss. Furthermore, once processed by the Firestore backend, all integer operations are - * capped between -2^63 and 2^63-1. - * - * If the current field value is not of type `number`, or if the field does not yet exist, the transformation sets the field to the given value. - * - * #### Example - * - * ```js - * const increment = firebase.firestore.FieldValue.increment(1); - * - * // Increment the loginCount field by 1 on the document - * await docRef.update({ - * loginCount: increment, - * }); - * ``` - * - * Please be careful using this operator. It may not be reliable enough for use in circumstances where absolute accuracy is required, - * as it appears writes to Firestore may sometimes be duplicated in situations not fully understood yet, but possibly correlated with - * write frequency. See https://github.com/invertase/react-native-firebase/discussions/5914 - * - * @param n The value to increment by. - */ - static increment(n: number): FieldValue; - - /** - * Returns a sentinel used with set() or update() to include a server-generated timestamp in the written data. - * - * #### Example - * - * ```js - * const timestamp = firebase.firestore.FieldValue.serverTimestamp(); - * - * // Set the updatedAt field to the current server time - * await docRef.update({ - * updatedAt: timestamp, - * }); - * ``` - */ - static serverTimestamp(): FieldValue; - - /** - * Returns true if this `FieldValue` is equal to the provided one. - * - * #### Example - * - * ```js - * const increment = firebase.firestore.FieldValue.increment(1); - * const timestamp = firebase.firestore.FieldValue.serverTimestamp(); - * - * // false - * increment.isEqual(timestamp); - * ``` - * - * @param other The `FieldValue` to compare against. - */ - isEqual(other: FieldValue): boolean; - } - - /** - * An immutable object representing a geo point in Firestore. The geo point is represented as latitude/longitude pair. - * - * Latitude values are in the range of [-90, 90]. Longitude values are in the range of [-180, 180]. - */ - export declare class GeoPoint { - /** - * Creates a new immutable GeoPoint object with the provided latitude and longitude values. - * - * #### Example - * - * ```js - * const geoPoint = new firebase.firestore.GeoPoint(60, -40); - * ``` - * - * @param latitude The latitude as number between -90 and 90. - * @param longitude The longitude as number between -180 and 180. - */ - constructor(latitude: number, longitude: number); - - /** - * The latitude of this `GeoPoint` instance. - */ - latitude: number; - - /** - * The longitude of this `GeoPoint` instance. - */ - longitude: number; - - /** - * Returns true if this `GeoPoint` is equal to the provided one. - * - * #### Example - * - * ```js - * const geoPoint1 = new firebase.firestore.GeoPoint(60, -40); - * const geoPoint2 = new firebase.firestore.GeoPoint(60, -20); - * - * // false - * geoPoint1.isEqual(geoPoint2); - * ``` - * - * @param other The `GeoPoint` to compare against. - */ - isEqual(other: GeoPoint): boolean; - - /** - * Returns a JSON-serializable representation of this GeoPoint. - */ - toJSON(): { latitude: number; longitude: number }; - } - - /** - * An options object that configures the behavior of get() calls on DocumentReference and Query. - * By providing a GetOptions object, these methods can be configured to fetch results only from the - * server, only from the local cache or attempt to fetch results from the server and fall back to the - * cache (which is the default). - */ - export interface GetOptions { - /** - * Describes whether we should get from server or cache. - * - * Setting to `default` (or not setting at all), causes Firestore to try to retrieve an up-to-date (server-retrieved) - * snapshot, but fall back to returning cached data if the server can't be reached. - * - * Setting to `server` causes Firestore to avoid the cache, generating an error if the server cannot be reached. Note - * that the cache will still be updated if the server request succeeds. Also note that latency-compensation still - * takes effect, so any pending write operations will be visible in the returned data (merged into the server-provided data). - * - * Setting to `cache` causes Firestore to immediately return a value from the cache, ignoring the server completely - * (implying that the returned value may be stale with respect to the value on the server.) If there is no data in the - * cache to satisfy the `get()` call, `DocumentReference.get()` will return an error and `QuerySnapshot.get()` will return an - * empty `QuerySnapshot` with no documents. - */ - source: 'default' | 'server' | 'cache'; - } - - /** - * Represents an aggregation that can be performed by Firestore. - */ - export class AggregateField<_T> { - /** A type string to uniquely identify instances of this class. */ - type = 'AggregateField'; - } - - /** - * The union of all `AggregateField` types that are supported by Firestore. - */ - export type AggregateFieldType = AggregateField; - - /** - * A type whose property values are all `AggregateField` objects. - */ - export interface AggregateSpec { - [field: string]: AggregateFieldType; - } - - /** - * A type whose keys are taken from an `AggregateSpec`, and whose values are the - * result of the aggregation performed by the corresponding `AggregateField` - * from the input `AggregateSpec`. - */ - export type AggregateSpecData = { - [P in keyof T]: T[P] extends AggregateField ? U : never; - }; - - /** - * The results of executing an aggregation query. - */ - export interface AggregateQuerySnapshot< - AggregateSpecType extends AggregateSpec, - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > { - /** - * The underlying query over which the aggregations recorded in this - * `AggregateQuerySnapshot` were performed. - */ - get query(): Query; - - /** - * Returns the results of the aggregations performed over the underlying - * query. - * - * The keys of the returned object will be the same as those of the - * `AggregateSpec` object specified to the aggregation method, and the values - * will be the corresponding aggregation result. - * - * @returns The results of the aggregations performed over the underlying - * query. - */ - data(): AggregateSpecData; - } - - /** - * The results of requesting an aggregated query. - */ - export interface AggregateQuery { - /** - * The underlying query for this instance. - */ - get query(): Query; - - /** - * Executes the query and returns the results as a AggregateQuerySnapshot. - * - * - * #### Example - * - * ```js - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .count() - * .get(); - * ``` - * - * @param options An object to configure the get behavior. - */ - get(): Promise>; - } - - /** - * A Query refers to a `Query` which you can read or listen to. You can also construct refined `Query` objects by - * adding filters and ordering. - */ - export declare class Query< - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > { - /** - * Calculates the number of documents in the result set of the given query, without actually downloading - * the documents. - * - * Using this function to count the documents is efficient because only the final count, not the - * documents' data, is downloaded. This function can even count the documents if the result set - * would be prohibitively large to download entirely (e.g. thousands of documents). - * - * The result received from the server is presented, unaltered, without considering any local state. - * That is, documents in the local cache are not taken into consideration, neither are local - * modifications not yet synchronized with the server. Previously-downloaded results, if any, - * are not used: every request using this source necessarily involves a round trip to the server. - */ - count(): AggregateQuery<{ count: AggregateField }>; - - /** - * Same as count() - */ - countFromServer(): AggregateQuery<{ count: AggregateField }>; - - /** - * Creates and returns a new Query that ends at the provided document (inclusive). The end - * position is relative to the order of the query. The document must contain all of the - * fields provided in the orderBy of this query. - * - * #### Example - * - * ```js - * const user = await firebase.firestore().doc('users/alovelace').get(); - * - * // Get all users up to a specific user in order of age - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .endAt(user); - * ``` - * - * > Cursor snapshot queries have limitations. Please see [Query limitations](/query-limitations) for more information. - * - * @param snapshot The snapshot of the document to end at. - */ - endAt(snapshot: DocumentSnapshot): Query; - - /** - * Creates and returns a new Query that ends at the provided fields relative to the order of the query. - * The order of the field values must match the order of the order by clauses of the query. - * - * #### Example - * - * ```js - * // Get all users who's age is 30 or less - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .endAt(30); - * ``` - * - * @param fieldValues The field values to end this query at, in order of the query's order by. - */ - endAt(...fieldValues: any[]): Query; - - /** - * Creates and returns a new Query that ends before the provided document (exclusive). The end - * position is relative to the order of the query. The document must contain all of the fields - * provided in the orderBy of this query. - * - * #### Example - * - * ```js - * const user = await firebase.firestore().doc('users/alovelace').get(); - * - * // Get all users up to, but not including, a specific user in order of age - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .endBefore(user); - * ``` - * - * > Cursor snapshot queries have limitations. Please see [Query limitations](/query-limitations) for more information. - * - * @param snapshot The snapshot of the document to end before. - */ - endBefore( - snapshot: DocumentSnapshot, - ): Query; - - /** - * Creates and returns a new Query that ends before the provided fields relative to the order of - * the query. The order of the field values must match the order of the order by clauses of the query. - * - * #### Example - * - * ```js - * // Get all users who's age is 29 or less - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .endBefore(30); - * ``` - * - * @param fieldValues The field values to end this query before, in order of the query's order by. - */ - endBefore(...fieldValues: any[]): Query; - - /** - * Executes the query and returns the results as a QuerySnapshot. - * - * Note: By default, get() attempts to provide up-to-date data when possible by waiting for data from the server, - * but it may return cached data or fail if you are offline and the server cannot be reached. This behavior can be - * altered via the `GetOptions` parameter. - * - * #### Example - * - * ```js - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .get({ - * source: 'server', - * }); - * ``` - * - * @param options An object to configure the get behavior. - */ - get(options?: GetOptions): Promise>; - - /** - * Returns true if this Query is equal to the provided one. - * - * #### Example - * - * ```js - * const query = firebase.firestore() - * .collection('users') - * .orderBy('age'); - * - * // false - * query.isEqual( - * firebase.firestore() - * .collection('users') - * .orderBy('name') - * ); - * ``` - * - * @param other The `Query` to compare against. - */ - isEqual(other: Query): boolean; - - /** - * Creates and returns a new Query where the results are limited to the specified number of documents. - * - * #### Example - * - * ```js - * // Get 10 users in order of age - * const querySnapshot = firebase.firestore() - * .collection('users') - * .orderBy('age') - * .limit(10) - * .get(); - * ``` - * - * @param limit The maximum number of items to return. - */ - limit(limit: number): Query; - - /** - * Creates and returns a new Query where the results are limited to the specified number of documents - * starting from the last document. The order is dependent on the second parameter for the `orderBy` - * method. If `desc` is used, the order is reversed. `orderBy` method call is required when calling `limitToLast`. - * - * #### Example - * - * ```js - * // Get the last 10 users in reverse order of age - * const querySnapshot = firebase.firestore() - * .collection('users') - * .orderBy('age', 'desc') - * .limitToLast(10) - * .get(); - * ``` - * - * @param limitToLast The maximum number of items to return. - */ - limitToLast(limitToLast: number): Query; - - /** - * Attaches a listener for `QuerySnapshot` events. - * - * > Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().collection('users') - * .onSnapshot({ - * error: (e) => console.error(e), - * next: (querySnapshot) => {}, - * }); - * - * unsubscribe(); - * ``` - * - * @param observer A single object containing `next` and `error` callbacks. - */ - onSnapshot(observer: { - complete?: () => void; - error?: (error: Error) => void; - next?: (snapshot: QuerySnapshot) => void; - }): () => void; - - /** - * Attaches a listener for `QuerySnapshot` events with snapshot listener options. - * - * > Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().collection('users') - * .onSnapshot({ - * includeMetadataChanges: true, - * }, { - * error: (e) => console.error(e), - * next: (querySnapshot) => {}, - * }); - * - * unsubscribe(); - * ``` - * - * @param options Options controlling the listen behavior. - * @param observer A single object containing `next` and `error` callbacks. - */ - onSnapshot( - options: SnapshotListenOptions, - observer: { - complete?: () => void; - error?: (error: Error) => void; - next?: (snapshot: QuerySnapshot) => void; - }, - ): () => void; - - /** - * Attaches a listener for `QuerySnapshot` events. - * - * > Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().collection('users') - * .onSnapshot( - * (querySnapshot) => {}, // onNext - * (error) => console.error(error), // onError - * ); - * - * unsubscribe(); - * ``` - * @param onNext A callback to be called every time a new `QuerySnapshot` is available. - * @param onError A callback to be called if the listen fails or is cancelled. No further callbacks will occur. - * @param onCompletion An optional function which will never be called. - */ - onSnapshot( - onNext: (snapshot: QuerySnapshot) => void, - onError?: (error: Error) => void, - onCompletion?: () => void, - ): () => void; - - /** - * Attaches a listener for `QuerySnapshot` events with snapshot listener options. - * - * NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. - * - * Returns an unsubscribe function to stop listening to events. - * - * #### Example - * - * ```js - * const unsubscribe = firebase.firestore().collection('users') - * .onSnapshot( - * { source: 'cache', includeMetadataChanges: true }, // SnapshotListenerOptions - * (querySnapshot) => {}, // onNext - * (error) => console.error(error), // onError - * ); - * - * unsubscribe(); - * ``` - * @param options Options controlling the listen behavior. - * @param onNext A callback to be called every time a new `QuerySnapshot` is available. - * @param onError A callback to be called if the listen fails or is cancelled. No further callbacks will occur. - * @param onCompletion An optional function which will never be called. - */ - onSnapshot( - options: SnapshotListenOptions, - onNext: (snapshot: QuerySnapshot) => void, - onError?: (error: Error) => void, - onCompletion?: () => void, - ): () => void; - - /** - * Creates and returns a new Query that's additionally sorted by the specified field, optionally in descending order instead of ascending. - * - * * #### Example - * - * #### Example - * - * ```js - * // Get users in order of age, descending - * const querySnapshot = firebase.firestore() - * .collection('users') - * .orderBy('age', 'desc') - * .get(); - * ``` - * - * @param fieldPath The field to sort by. Either a string or FieldPath instance. - * @param directionStr Optional direction to sort by (`asc` or `desc`). If not specified, order will be ascending. - */ - orderBy( - fieldPath: string | FieldPath, - directionStr?: 'asc' | 'desc', - ): Query; - - /** - * Creates and returns a new Query that starts after the provided document (exclusive). The start - * position is relative to the order of the query. The document must contain all of the fields - * provided in the orderBy of this query. - * - * #### Example - * - * ```js - * const user = await firebase.firestore().doc('users/alovelace').get(); - * - * // Get all users up to, but not including, a specific user in order of age - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .startAfter(user) - * .get(); - * ``` - * - * > Cursor snapshot queries have limitations. Please see [Query limitations](/query-limitations) for more information. - * - * @param snapshot The snapshot of the document to start after. - */ - startAfter( - snapshot: DocumentSnapshot, - ): Query; - - /** - * Creates and returns a new Query that starts after the provided fields relative to the order of - * the query. The order of the field values must match the order of the order by clauses of the query. - * - * #### Example - * - * ```js - * // Get all users who's age is above 30 - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .startAfter(30) - * .get(); - * ``` - * - * @param fieldValues The field values to start this query after, in order of the query's order by. - */ - startAfter(...fieldValues: any[]): Query; - - /** - * Creates and returns a new Query that starts at the provided document (inclusive). The start - * position is relative to the order of the query. The document must contain all of the - * fields provided in the orderBy of this query. - * - * #### Example - * - * ```js - * const user = await firebase.firestore().doc('users/alovelace').get(); - * - * // Get all users up to a specific user in order of age - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .startAt(user) - * .get(); - * ``` - * - * > Cursor snapshot queries have limitations. Please see [Query limitations](/query-limitations) for more information. - * - * @param snapshot The snapshot of the document to start at. - */ - startAt( - snapshot: DocumentSnapshot, - ): Query; - - /** - * Creates and returns a new Query that starts at the provided fields relative to the order of the query. - * The order of the field values must match the order of the order by clauses of the query. - * - * #### Example - * - * ```js - * // Get all users who's age is 30 or above - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .orderBy('age') - * .startAt(30) - * .get(); - * ``` - * - * @param fieldValues The field values to start this query at, in order of the query's order by. - */ - startAt(...fieldValues: any[]): Query; - - /** - * Creates and returns a new Query with the additional filter that documents must contain the specified field and - * the value should satisfy the relation constraint provided. - * - * #### Example - * - * ```js - * // Get all users who's age is 30 or above - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .where('age', '>=', 30); - * .get(); - * ``` - * - * @param fieldPath The path to compare. - * @param opStr The operation string (e.g "<", "<=", "==", ">", ">=", "!=", "array-contains", "array-contains-any", "in", "not-in"). - * @param value The comparison value. - */ - where( - fieldPath: string | FieldPath, - opStr: WhereFilterOp, - value: any, - ): Query; - - /** - * Creates and returns a new Query with the additional filter that documents must contain the specified field and - * the value should satisfy the relation constraint provided. - * - * #### Example - * - * ```js - * // Get all users who's age is 30 or above - * const querySnapshot = await firebase.firestore() - * .collection('users') - * .where(Filter('age', '>=', 30)); - * .get(); - * ``` - * - * @param filter The filter to apply to the query. - */ - where(filter: QueryFilterConstraint): Query; - - /** - * Removes the current converter. - * - * @param converter `null` removes the current converter. - * @return A Query that uses the provided converter. - */ - withConverter(converter: null): Query; - - /** - * Applies a custom data converter to this Query, allowing you to use your - * own custom model objects with Firestore. When you call get() on the - * returned Query, the provided converter will convert between Firestore - * data and your custom type U. - * - * Passing in `null` as the converter parameter removes the current - * converter. - * - * @param converter Converts objects to and from Firestore. Passing in - * `null` removes the current converter. - * @return A Query that uses the provided converter. - */ - withConverter< - NewAppModelType extends DocumentData, - NewDbModelType extends DocumentData = DocumentData, - >( - converter: FirestoreDataConverter, - ): Query; - } - - /** - * Filter conditions in a `Query.where()` clause are specified using the strings '<', '<=', '==', '>=', '>', 'array-contains', 'array-contains-any' or 'in'. - */ - export type WhereFilterOp = - | '<' - | '<=' - | '==' - | '>' - | '>=' - | '!=' - | 'array-contains' - | 'array-contains-any' - | 'in' - | 'not-in'; - - /** - * A `QuerySnapshot` contains zero or more `QueryDocumentSnapshot` objects representing the results of a query. The documents - * can be accessed as an array via the `docs` property or enumerated using the `forEach` method. The number of documents - * can be determined via the `empty` and `size` properties. - */ - export interface QuerySnapshot< - AppModelType extends DocumentData = DocumentData, - DbModelType extends DocumentData = DocumentData, - > { - /** - * An array of all the documents in the `QuerySnapshot`. - */ - docs: Array>; - - /** - * True if there are no documents in the `QuerySnapshot`. - */ - empty: boolean; - - /** - * Metadata about this snapshot, concerning its source and if it has local modifications. - */ - metadata: SnapshotMetadata; - - /** - * The query on which you called get or `onSnapshot` in order to `get` this `QuerySnapshot`. - */ - query: Query; - - /** - * The number of documents in the `QuerySnapshot`. - */ - size: number; - - /** - * Returns an array of the documents changes since the last snapshot. If this is the first snapshot, all documents - * will be in the list as added changes. - * - * To include metadata changes, ensure that the `onSnapshot()` method includes metadata changes. - * - * #### Example - * - * ```js - * firebase.firestore().collection('users') - * .onSnapshot((querySnapshot) => { - * console.log('Metadata Changes', querySnapshot.docChanges()); - * }); - * ``` - * - * #### Example - With metadata changes - * - * ```js - * firebase.firestore().collection('users') - * .onSnapshot({ includeMetadataChanges: true }, (querySnapshot) => { - * console.log('Metadata Changes', querySnapshot.docChanges({ - * includeMetadataChanges: true, - * })); - * }); - * ``` - * - * @param options `SnapshotListenOptions` that control whether metadata-only changes (i.e. only `DocumentSnapshot.metadata` changed) should trigger snapshot events. - */ - docChanges(options?: SnapshotListenOptions): Array>; - - /** - * Enumerates all of the documents in the `QuerySnapshot`. - * - * #### Example - * - * ```js - * const querySnapshot = await firebase.firestore().collection('users').get(); - * - * querySnapshot.forEach((queryDocumentSnapshot) => { - * console.log('User', queryDocumentSnapshot.data()); - * }) - * ``` - * - * @param callback A callback to be called with a `QueryDocumentSnapshot` for each document in the snapshot. - * @param thisArg The `this` binding for the callback. - */ - forEach( - callback: (result: QueryDocumentSnapshot) => void, - thisArg?: unknown, - ): void; - - /** - * Returns true if this `QuerySnapshot` is equal to the provided one. - * - * #### Example - * - * ```js - * const querySnapshot1 = await firebase.firestore().collection('users').limit(5).get(); - * const querySnapshot2 = await firebase.firestore().collection('users').limit(10).get(); - * - * // false - * querySnapshot1.isEqual(querySnapshot2); - * ``` - * - * > This operation can be resource intensive when dealing with large datasets. - * - * @param other The `QuerySnapshot` to compare against. - */ - isEqual(other: QuerySnapshot): boolean; - } - - /** - * An options object that configures the behavior of set() calls in `DocumentReference`, `WriteBatch` and `Transaction`. - * These calls can be configured to perform granular merges instead of overwriting the target documents in their entirety - * by providing a `SetOptions` with `merge: true`. - * - * Using both `merge` and `mergeFields` together will throw an error. - */ - export interface SetOptions { - /** - * Changes the behavior of a `set()` call to only replace the values specified in its data argument. - * Fields omitted from the `set()` call remain untouched. - */ - merge?: boolean; - - /** - * Changes the behavior of `set()` calls to only replace the specified field paths. - * Any field path that is not specified is ignored and remains untouched. - */ - mergeFields?: (string | FieldPath)[]; - } - - /** - * Specifies custom configurations for your Cloud Firestore instance. You must set these before invoking any other methods. - * - * Used with `firebase.firestore().settings()`. - */ - export interface Settings { - /** - * Enables or disables local persistent storage. - */ - persistence?: boolean; - - /** - * An approximate cache size threshold for the on-disk data. If the cache grows beyond this size, Firestore will start - * removing data that hasn't been recently used. The size is not a guarantee that the cache will stay below that size, - * only that if the cache exceeds the given size, cleanup will be attempted. - * - * To disable garbage collection and set an unlimited cache size, use `firebase.firestore.CACHE_SIZE_UNLIMITED`. - */ - cacheSizeBytes?: number; - - /** - * The hostname to connect to. - * - * Note: on android, hosts 'localhost' and '127.0.0.1' are automatically remapped to '10.0.2.2' (the - * "host" computer IP address for android emulators) to make the standard development experience easy. - * If you want to use the emulator on a real android device, you will need to specify the actual host - * computer IP address. Use of this property for emulator connection is deprecated. Use useEmulator instead - */ - host?: string; - - /** - * Whether to use SSL when connecting. A true value is incompatible with the firestore emulator. - */ - ssl?: boolean; - - /** - * Whether to skip nested properties that are set to undefined during object serialization. - * If set to true, these properties are skipped and not written to Firestore. - * If set to false or omitted, the SDK throws an exception when it encounters properties of type undefined. - */ - ignoreUndefinedProperties?: boolean; - - /** - * If set, controls the return value for server timestamps that have not yet been set to their final value. - * - * By specifying 'estimate', pending server timestamps return an estimate based on the local clock. - * This estimate will differ from the final value and cause these values to change once the server result becomes available. - * - * By specifying 'previous', pending timestamps will be ignored and return their previous value instead. - * - * If omitted or set to 'none', null will be returned by default until the server value becomes available. - * - */ - serverTimestampBehavior?: 'estimate' | 'previous' | 'none'; - } - - /** - * An options object that can be passed to `DocumentReference.onSnapshot()`, `Query.onSnapshot()` and `QuerySnapshot.docChanges()` - * to control which types of changes to include in the result set. - */ - export interface SnapshotListenOptions { - /** - * Include a change even if only the metadata of the query or of a document changed. Default is false. - */ - includeMetadataChanges?: boolean; - /** - * Set the source the query listens to. Default is 'default'. - */ - source?: ListenSource; - } - - /** - * Metadata about a snapshot, describing the state of the snapshot. - */ - export interface SnapshotMetadata { - /** - * True if the snapshot includes local writes (`set()` or `update()` calls) that haven't been committed to the backend yet. - * If your listener has opted into metadata updates (via `SnapshotListenOptions`) you will receive another snapshot with - * `fromCache` equal to false once the client has received up-to-date data from the backend. - */ - fromCache: boolean; - - /** - * True if the snapshot contains the result of local writes (e.g. `set()` or `update()` calls) that have not yet been - * committed to the backend. If your listener has opted into metadata updates (via `SnapshotListenOptions`) you will - * receive another snapshot with `hasPendingWrites` equal to false once the writes have been committed to the backend. - */ - hasPendingWrites: boolean; - - /** - * Returns true if this `SnapshotMetadata` is equal to the provided one. - * - * @param other The `SnapshotMetadata` to compare against. - */ - isEqual(other: SnapshotMetadata): boolean; - } - - /** - * A Timestamp represents a point in time independent of any time zone or calendar, represented as seconds and - * fractions of seconds at nanosecond resolution in UTC Epoch time. - * - * It is encoded using the Proleptic Gregorian Calendar which extends the Gregorian calendar backwards to year one. - * It is encoded assuming all minutes are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second table - * is needed for interpretation. Range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. - */ - export declare class Timestamp { - /** - * Creates a new timestamp from the given JavaScript [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). - * - * @param date The date to initialize the `Timestamp` from. - */ - static fromDate(date: Date): Timestamp; - - /** - * Creates a new timestamp from the given number of milliseconds. - * - * @param milliseconds Number of milliseconds since Unix epoch 1970-01-01T00:00:00Z. - */ - static fromMillis(milliseconds: number): Timestamp; - - /** - * Creates a new timestamp with the current date, with millisecond precision. - */ - static now(): Timestamp; - - /** - * Creates a new timestamp. - * - * @param seconds The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive. - * @param nanoseconds The non-negative fractions of a second at nanosecond resolution. Negative second values with fractions must still have non-negative nanoseconds values that count forward in time. Must be from 0 to 999,999,999 inclusive. - */ - constructor(seconds: number, nanoseconds: number); - - /** - * The number of nanoseconds of this `Timestamp`; - */ - nanoseconds: number; - - /** - * The number of seconds of this `Timestamp`. - */ - seconds: number; - - /** - * Returns true if this `Timestamp` is equal to the provided one. - * - * @param other The `Timestamp` to compare against. - */ - isEqual(other: Timestamp): boolean; - - /** - * Convert a Timestamp to a JavaScript Date object. This conversion causes a loss of precision since Date objects - * only support millisecond precision. - * - * Returns a JavaScript [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) with - * millseconds precision. - */ - toDate(): Date; - - /** - * Convert a Timestamp to a numeric timestamp (in milliseconds since epoch). This operation causes a loss of precision. - * - * The point in time corresponding to this timestamp, represented as the number of milliseconds since Unix epoch 1970-01-01T00:00:00Z. - */ - toMillis(): number; - - /** - * Convert a timestamp to a string in format "FirestoreTimestamp(seconds=`seconds`, nanoseconds=`nanoseconds`)", - * with the `seconds` and `nanoseconds` replaced by the values in the Timestamp object - */ - toString(): string; - - /** - * Convert a Timestamp to a JSON object with seconds and nanoseconds members - */ - toJSON(): { seconds: number; nanoseconds: number }; - - /** - * Converts this object to a primitive string, which allows Timestamp objects to be compared - * using the `>`, `<=`, `>=` and `>` operators. - */ - valueOf(): string; - } - - /** - * A reference to a transaction. The `Transaction` object passed to a transaction's updateFunction provides the methods to - * read and write data within the transaction context. See `Firestore.runTransaction()`. - * - * A transaction consists of any number of `get()` operations followed by any number of write operations such as `set()`, - * `update()`, or `delete()`. In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, - * if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. - * This feature ensures that the transaction runs on up-to-date and consistent data. - * - * Transactions never partially apply writes. All writes execute at the end of a successful transaction. - * - * When using transactions, note that: - * - Read operations must come before write operations. - * - A function calling a transaction (transaction function) might run more than once if a concurrent edit affects a document that the transaction reads. - * - Transaction functions should not directly modify application state (return a value from the `updateFunction`). - * - Transactions will fail when the client is offline. - */ - export interface Transaction { - /** - * Deletes the document referred to by the provided `DocumentReference`. - * - * #### Example - * - * ```js - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * await firebase.firestore().runTransaction((transaction) => { - * return transaction.delete(docRef); - * }); - * ``` - * - * @param documentRef A reference to the document to be deleted. - */ - delete(documentRef: DocumentReference): Transaction; - - /** - * Reads the document referenced by the provided `DocumentReference`. - * - * #### Example - * - * ```js - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * await firebase.firestore().runTransaction(async (transaction) => { - * const snapshot = await transaction.get(docRef); - * // use snapshot with transaction (see set() or update()) - * ... - * }); - * ``` - * - * @param documentRef A reference to the document to be read. - */ - get( - documentRef: DocumentReference, - ): Promise>; - - /** - * Writes to the document referred to by the provided `DocumentReference`. If the document does not exist yet, - * it will be created. If you pass `SetOptions`, the provided data can be merged into the existing document. - * - * #### Example - * - * ```js - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * await firebase.firestore().runTransaction((transaction) => { - * const snapshot = await transaction.get(docRef); - * const snapshotData = snapshot.data(); - * - * return transaction.set(docRef, { - * ...data, - * age: 30, // new field - * }); - * }); - * ``` - * - * @param documentRef A reference to the document to be set. - * @param data An object of the fields and values for the document. - * @param options An object to configure the set behavior. - */ - set( - documentRef: DocumentReference, - data: T, - options?: SetOptions, - ): Transaction; - - /** - * Updates fields in the document referred to by the provided `DocumentReference`. The update will fail if applied - * to a document that does not exist. - * - * #### Example - * - * ```js - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * await firebase.firestore().runTransaction((transaction) => { - * const snapshot = await transaction.get(docRef); - * - * return transaction.update(docRef, { - * age: snapshot.data().age + 1, - * }); - * }); - * ``` - * - * @param documentRef A reference to the document to be updated. - * @param data An object containing the fields and values with which to update the document. Fields can contain dots to reference nested fields within the document. - */ - update( - documentRef: DocumentReference, - data: Partial<{ [K in keyof T]: T[K] | FieldValue }>, - ): Transaction; - - /** - * Updates fields in the document referred to by the provided DocumentReference. The update will fail if applied to - * a document that does not exist. - * - * Nested fields can be updated by providing dot-separated field path strings or by providing FieldPath objects. - * - * #### Example - * - * ```js - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * await firebase.firestore().runTransaction((transaction) => { - * const snapshot = await transaction.get(docRef); - * - * return transaction.update(docRef, 'age', snapshot.data().age + 1); - * }); - * ``` - * - * @param documentRef A reference to the document to be updated. - * @param field The first field to update. - * @param value The first value. - * @param moreFieldsAndValues Additional key/value pairs. - */ - update( - documentRef: DocumentReference, - field: K | FieldPath, - value: T[K], - ...moreFieldsAndValues: any[] - ): Transaction; - } - - /** - * A write batch, used to perform multiple writes as a single atomic unit. - * - * A WriteBatch object can be acquired by calling `firestore.batch()`. It provides methods for adding - * writes to the write batch. None of the writes will be committed (or visible locally) until - * `WriteBatch.commit()` is called. - * - * Unlike transactions, write batches are persisted offline and therefore are preferable when you don't need to - * condition your writes on read data. - */ - export interface WriteBatch { - /** - * Commits all of the writes in this write batch as a single atomic unit. - * - * Returns a Promise resolved once all of the writes in the batch have been successfully written - * to the backend as an atomic unit. Note that it won't resolve while you're offline. - * - * #### Example - * - * ```js - * const batch = firebase.firestore().batch(); - * - * // Perform batch operations... - * - * await batch.commit(); - * ``` - */ - commit(): Promise; - - /** - * Deletes the document referred to by the provided `DocumentReference`. - * - * #### Example - * - * ```js - * const batch = firebase.firestore().batch(); - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * batch.delete(docRef); - * ``` - * - * @param documentRef A reference to the document to be deleted. - */ - delete(documentRef: DocumentReference): WriteBatch; - - /** - * Writes to the document referred to by the provided DocumentReference. If the document does - * not exist yet, it will be created. If you pass SetOptions, the provided data can be merged - * into the existing document. - * - * #### Example - * - * ```js - * const batch = firebase.firestore().batch(); - * const docRef = firebase.firestore().doc('users/dsmith'); - * - * batch.set(docRef, { - * name: 'David Smith', - * age: 25, - * }); - * ``` - * - * @param documentRef A reference to the document to be set. - * @param data An object of the fields and values for the document. - * @param options An object to configure the set behavior. - */ - set( - documentRef: DocumentReference, - data: T, - options?: SetOptions, - ): WriteBatch; - - /** - * Updates fields in the document referred to by the provided DocumentReference. The update will fail if applied to a document that does not exist. - * - * #### Example - * - * ```js - * const batch = firebase.firestore().batch(); - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * batch.update(docRef, { - * city: 'SF', - * }); - * ``` - * - * @param documentRef A reference to the document to be updated. - * @param data An object containing the fields and values with which to update the document. Fields can contain dots to reference nested fields within the document. - */ - update( - documentRef: DocumentReference, - data: Partial<{ [K in keyof T]: T[K] | FieldValue }>, - ): WriteBatch; - - /** - * Updates fields in the document referred to by this DocumentReference. The update will fail if applied to a document that does not exist. - * - * Nested fields can be update by providing dot-separated field path strings or by providing FieldPath objects. - * - * #### Example - * - * ```js - * const batch = firebase.firestore().batch(); - * const docRef = firebase.firestore().doc('users/alovelace'); - * - * batch.update(docRef, 'city', 'SF', 'age', 31); - * ``` - * - * @param documentRef A reference to the document to be updated. - * @param field The first field to update. - * @param value The first value. - * @param moreFieldAndValues Additional key value pairs. - */ - update( - documentRef: DocumentReference, - field: K | FieldPath, - value: T[K] | FieldValue, - ...moreFieldAndValues: any[] - ): WriteBatch; - } - - /** - * Returns the PersistentCache Index Manager used by the given Firestore object. - * The PersistentCacheIndexManager instance, or null if local persistent storage is not in use. - */ - export interface PersistentCacheIndexManager { - /** - * Enables the SDK to create persistent cache indexes automatically for local query - * execution when the SDK believes cache indexes can help improves performance. - * This feature is disabled by default. - */ - enableIndexAutoCreation(): Promise; - /** - * Stops creating persistent cache indexes automatically for local query execution. - * The indexes which have been created by calling `enableIndexAutoCreation()` still take effect. - */ - disableIndexAutoCreation(): Promise; - /** - * Removes all persistent cache indexes. Note this function also deletes indexes - * generated by `setIndexConfiguration()`, which is deprecated. - */ - deleteAllIndexes(): Promise; - } - - /** - * Represents the state of bundle loading tasks. - * - * Both 'Error' and 'Success' are sinking state: task will abort or complete and there will be no more - * updates after they are reported. - */ - export type TaskState = 'Error' | 'Running' | 'Success'; - - /** - * Represents a progress update or a final state from loading bundles. - */ - export interface LoadBundleTaskProgress { - /** - * How many bytes have been loaded. - */ - bytesLoaded: number; - - /** - * How many documents have been loaded. - */ - documentsLoaded: number; - - /** - * Current task state. - */ - taskState: TaskState; - - /** - * How many bytes are in the bundle being loaded. - */ - totalBytes: number; - - /** - * How many documents are in the bundle being loaded. - */ - totalDocuments: number; - } - - /** - * `firebase.firestore.X` - */ - export interface Statics { - /** - * Returns the `Blob` class. - */ - Blob: typeof Blob; - - /** - * Returns the `FieldPath` class. - */ - FieldPath: typeof FieldPath; - - /** - * Returns the `FieldValue` class. - */ - FieldValue: typeof FieldValue; - - /** - * Returns the `GeoPoint` class. - */ - GeoPoint: typeof GeoPoint; - - /** - * Returns the `Timestamp` class. - */ - Timestamp: typeof Timestamp; - - /** - * Returns the `Filter` function. - */ - Filter: typeof Filter; - - /** - * Used to set the cache size to unlimited when passing to `cacheSizeBytes` in - * `firebase.firestore().settings()`. - */ - CACHE_SIZE_UNLIMITED: number; - - /** - * Sets the verbosity of Cloud Firestore native device logs (debug, error, or silent). - * - * - `debug`: the most verbose logging level, primarily for debugging. - * - `error`: logs only error events. - * - `silent`: turn off logging. - * - * #### Example - * - * ```js - * firebase.firestore.setLogLevel('silent'); - * ``` - * - * @param logLevel The verbosity you set for activity and error logging. - */ - setLogLevel(logLevel: 'debug' | 'error' | 'silent'): void; - SDK_VERSION: string; - } - - /** - * Converter used by `withConverter()` to transform user objects of type T - * into Firestore data. - * - * Using the converter allows you to specify generic type arguments when - * storing and retrieving objects from Firestore. - * - * @example - * ```typescript - * class Post { - * constructor(readonly title: string, readonly author: string) {} - * - * toString(): string { - * return this.title + ', by ' + this.author; - * } - * } - * - * const postConverter = { - * toFirestore(post: Post): firebase.firestore.DocumentData { - * return {title: post.title, author: post.author}; - * }, - * fromFirestore( - * snapshot: firebase.firestore.QueryDocumentSnapshot, - * options: firebase.firestore.SnapshotOptions - * ): Post { - * const data = snapshot.data(options)!; - * return new Post(data.title, data.author); - * } - * }; - * - * const postSnap = await firebase.firestore() - * .collection('posts') - * .withConverter(postConverter) - * .doc().get(); - * const post = postSnap.data(); - * if (post !== undefined) { - * post.title; // string - * post.toString(); // Should be defined - * post.someNonExistentProperty; // TS error - * } - * ``` - */ - export interface FirestoreDataConverter< - AppModelType, - DbModelType extends DocumentData = DocumentData, - > { - /** - * Called by the Firestore SDK to convert a custom model object of type T - * into a plain JavaScript object (suitable for writing directly to the - * Firestore database). To use `set()` with `merge` and `mergeFields`, - * `toFirestore()` must be defined with `Partial`. - */ - toFirestore(modelObject: AppModelType): DocumentData; - toFirestore(modelObject: Partial, options: SetOptions): DocumentData; - - /** - * Called by the Firestore SDK to convert Firestore data into an object of - * type T. You can access your data by calling: `snapshot.data(options)`. - * - * @param snapshot A QueryDocumentSnapshot containing your data and metadata. - * @param options The SnapshotOptions from the initial call to `data()`. - */ - fromFirestore( - snapshot: QueryDocumentSnapshot, - options: SnapshotOptions, - ): AppModelType; - } - - /** - * The Firebase Cloud Firestore service is available for the default app or a given app. - * - * #### Example: Get the firestore instance for the **default app**: - * - * ```js - * const firestoreForDefaultApp = firebase.firestore(); - * ``` - * - * #### Example: Get the firestore instance for a **secondary app**: - * - * ```js - * const otherApp = firebase.app('otherApp'); - * const firestoreForOtherApp = firebase.firestore(otherApp); - * ``` - * - */ - export declare class Module implements _FirebaseModule { - /** - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * Creates a write batch, used for performing multiple writes as a single atomic operation. - * The maximum number of writes allowed in a single WriteBatch is 500, but note that each usage - * of `FieldValue.serverTimestamp()`, `FieldValue.arrayUnion()`, `FieldValue.arrayRemove()`, or `FieldValue.increment()` - * inside a WriteBatch counts as an additional write. - * - * #### Example - * - * ```js - * const batch = firebase.firestore().batch(); - * batch.delete(...); - * ``` - */ - batch(): WriteBatch; - - /** - * Gets a `CollectionReference` instance that refers to the collection at the specified path. - * - * #### Example - * - * ```js - * const collectionReference = firebase.firestore().collection('users'); - * ``` - * - * @param collectionPath A slash-separated path to a collection. - */ - collection( - collectionPath: string, - ): CollectionReference; - - /** - * Creates and returns a new Query that includes all documents in the database that are contained - * in a collection or subcollection with the given collectionId. - * - * #### Example - * - * ```js - * const collectionGroup = firebase.firestore().collectionGroup('orders'); - * ``` - * - * @param collectionId Identifies the collections to query over. Every collection or subcollection with this ID as the last segment of its path will be included. Cannot contain a slash. - */ - collectionGroup(collectionId: string): Query; - - /** - * Disables network usage for this instance. It can be re-enabled via `enableNetwork()`. While the - * network is disabled, any snapshot listeners or get() calls will return results from cache, and any - * write operations will be queued until the network is restored. - * - * Returns a promise that is resolved once the network has been disabled. - * - * #### Example - * - * ```js - * await firebase.firestore().disableNetwork(); - * ``` - */ - disableNetwork(): Promise; - - /** - * Gets a `DocumentReference` instance that refers to the document at the specified path. - * - * #### Example - * - * ```js - * const documentReference = firebase.firestore().doc('users/alovelace'); - * ``` - * - * @param documentPath A slash-separated path to a document. - */ - doc(documentPath: string): DocumentReference; - - /** - * Re-enables use of the network for this Firestore instance after a prior call to `disableNetwork()`. - * - * #### Example - * - * ```js - * await firebase.firestore().enableNetwork(); - * ``` - */ - enableNetwork(): Promise; - - /** - * Executes the given `updateFunction` and then attempts to commit the changes applied within the transaction. - * If any document read within the transaction has changed, Cloud Firestore retries the `updateFunction`. - * If it fails to commit after 5 attempts, the transaction fails. - * - * The maximum number of writes allowed in a single transaction is 500, but note that each usage of - * `FieldValue.serverTimestamp()`, `FieldValue.arrayUnion()`, `FieldValue.arrayRemove()`, or `FieldValue.increment()` - * inside a transaction counts as an additional write. - * - * #### Example - * - * ```js - * const cityRef = firebase.firestore().doc('cities/SF'); - * - * await firebase.firestore() - * .runTransaction(async (transaction) => { - * const snapshot = await transaction.get(cityRef); - * await transaction.update(cityRef, { - * population: snapshot.data().population + 1, - * }); - * }); - * ``` - */ - runTransaction(updateFunction: (transaction: Transaction) => Promise): Promise; - - /** - * Specifies custom settings to be used to configure the Firestore instance. Must be set before invoking any other methods. - * - * #### Example - * - * ```js - * const settings = { - * cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED, - * }; - * - * await firebase.firestore().settings(settings); - * ``` - * - * @param settings A `Settings` object. - */ - settings(settings: Settings): Promise; - /** - * Loads a Firestore bundle into the local cache. - * - * #### Example - * - * ```js - * const resp = await fetch('/createBundle'); - * const bundleString = await resp.text(); - * await firestore().loadBundle(bundleString); - * ``` - */ - loadBundle(bundle: string): Promise; - /** - * Reads a Firestore Query from local cache, identified by the given name. - * - * #### Example - * - * ```js - * const query = firestore().namedQuery('latest-stories-query'); - * const storiesSnap = await query.get({ source: 'cache' }); - * ``` - */ - namedQuery(name: string): Query; - /** - * Aimed primarily at clearing up any data cached from running tests. Needs to be executed before any database calls - * are made. - * - * #### Example - * - *```js - * await firebase.firestore().clearPersistence(); - * ``` - */ - clearPersistence(): Promise; - /** - * Waits until all currently pending writes for the active user have been acknowledged by the - * backend. - * - * The returned Promise resolves immediately if there are no outstanding writes. Otherwise, the - * Promise waits for all previously issued writes (including those written in a previous app - * session), but it does not wait for writes that were added after the method is called. If you - * want to wait for additional writes, call `waitForPendingWrites()` again. - * - * Any outstanding `waitForPendingWrites()` Promises are rejected when the logged-in user changes. - * - * #### Example - * - *```js - * await firebase.firestore().waitForPendingWrites(); - * ``` - */ - waitForPendingWrites(): Promise; - /** - * Typically called to ensure a new Firestore instance is initialized before calling - * `firebase.firestore().clearPersistence()`. - * - * #### Example - * - *```js - * await firebase.firestore().terminate(); - * ``` - */ - terminate(): Promise; - - /** - * Modify this Firestore instance to communicate with the Firebase Firestore emulator. - * This must be called before any other calls to Firebase Firestore to take effect. - * Do not use with production credentials as emulator traffic is not encrypted. - * - * Note: on android, hosts 'localhost' and '127.0.0.1' are automatically remapped to '10.0.2.2' (the - * "host" computer IP address for android emulators) to make the standard development experience easy. - * If you want to use the emulator on a real android device, you will need to specify the actual host - * computer IP address. - * - * @param host: emulator host (eg, 'localhost') - * @param port: emulator port (eg, 8080) - */ - useEmulator(host: string, port: number): void; - - /** - * Gets the `PersistentCacheIndexManager` instance used by this Cloud Firestore instance. - * This is not the same as Cloud Firestore Indexes. - * Persistent cache indexes are optional indexes that only exist within the SDK to assist in local query execution. - * Returns `null` if local persistent storage is not in use. - */ - persistentCacheIndexManager(): PersistentCacheIndexManager | null; - } - - /** - * Utility type to allow FieldValue and to allow Date in place of Timestamp objects. - */ - export type SetValue = T extends Timestamp - ? Timestamp | Date // allow Date in place of Timestamp - : T extends object - ? { - [P in keyof T]: SetValue | FieldValue; // allow FieldValue in place of values - } - : T; -} - -type FirestoreNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseFirestoreTypes.Module, - FirebaseFirestoreTypes.Statics -> & { - firestore: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseFirestoreTypes.Module, - FirebaseFirestoreTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -declare const defaultExport: FirestoreNamespace; - -export declare const firebase: ReactNativeFirebase.Module & { - firestore: typeof defaultExport; - app( - name?: string, - ): ReactNativeFirebase.FirebaseApp & { firestore(): FirebaseFirestoreTypes.Module }; -}; - -export declare const Filter: FirebaseFirestoreTypes.FilterFunction; - -export default defaultExport; - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ -declare module '@react-native-firebase/app' { - // eslint-disable-next-line @typescript-eslint/no-namespace -- module augmentation uses namespace - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - interface Module { - firestore: FirebaseModuleWithStaticsAndApp< - FirebaseFirestoreTypes.Module, - FirebaseFirestoreTypes.Statics - >; - } - interface FirebaseApp { - firestore(databaseId?: string): FirebaseFirestoreTypes.Module; - } - } -} diff --git a/packages/firestore/lib/utils/index.ts b/packages/firestore/lib/utils/index.ts index 7d2d2a7941..862d588f90 100644 --- a/packages/firestore/lib/utils/index.ts +++ b/packages/firestore/lib/utils/index.ts @@ -30,7 +30,7 @@ import type { ConverterWithToFirestoreInternal, PartialSnapshotObserverInternal, } from '../types/internal'; -import FieldPath, { fromDotSeparatedString } from '../FieldPath'; +import { fromDotSeparatedString, FieldPath } from '../FieldPath'; import type { ListenSource } from '../types/firestore'; export function extractFieldPathData(data: unknown, segments: string[]): unknown { diff --git a/packages/firestore/lib/utils/serialize.ts b/packages/firestore/lib/utils/serialize.ts index 070e08c0de..7db37f99b7 100644 --- a/packages/firestore/lib/utils/serialize.ts +++ b/packages/firestore/lib/utils/serialize.ts @@ -27,14 +27,14 @@ import { } from '@react-native-firebase/app/dist/module/common'; import type { Firestore } from '../types/firestore'; import type { FirestoreInternal } from '../types/internal'; -import Blob from '../FirestoreBlob'; +import { Blob } from '../FirestoreBlob'; import { DOCUMENT_ID } from '../FieldPath'; -import FirestoreGeoPoint from '../FirestoreGeoPoint'; +import { GeoPoint as FirestoreGeoPoint } from '../FirestoreGeoPoint'; import FirestorePath from '../FirestorePath'; -import FirestoreTimestamp from '../FirestoreTimestamp'; +import { Timestamp as FirestoreTimestamp } from '../FirestoreTimestamp'; import { getTypeMapInt, getTypeMapName } from './typemap'; import { Bytes } from '../modular/Bytes'; -import FirestoreVectorValue from '../FirestoreVectorValue'; +import { VectorValue as FirestoreVectorValue } from '../FirestoreVectorValue'; let FirestoreDocumentReference: | (new ( diff --git a/packages/firestore/type-test.ts b/packages/firestore/type-test.ts index 272a9b4bb0..ea89b2b63d 100644 --- a/packages/firestore/type-test.ts +++ b/packages/firestore/type-test.ts @@ -1,15 +1,9 @@ /* * Consumer-facing API type tests for @react-native-firebase/firestore. - * Part 1: Namespaced API (firebase.firestore(), default firestore()). - * Part 2: Modular API (getFirestore, doc, collection, setDoc, etc. from lib/modular.ts). + * Modular API (getFirestore, doc, collection, setDoc, etc.). */ -// --------------------------------------------------------------------------- -// PART 1 — NAMESPACED API -// --------------------------------------------------------------------------- -// Import namespaced API: default export and firebase, plus types used in Part 1. -import firestore, { - firebase, +import { getFirestore, connectFirestoreEmulator, setLogLevel, @@ -78,22 +72,20 @@ import firestore, { CACHE_SIZE_UNLIMITED, AggregateField, AggregateQuerySnapshot, - DocumentSnapshot, DocumentReference, - Query, QueryDocumentSnapshot, - Transaction, - WriteBatch, } from '.'; import type { - FirebaseFirestoreTypes, + Query, Firestore, DocumentData, LoadBundleTaskProgress, FirestoreDataConverter, WithFieldValue, PartialWithFieldValue, + DocumentSnapshot as FirestoreDocumentSnapshot, } from '.'; +import { getApp } from '@react-native-firebase/app'; import { execute, field, @@ -109,7 +101,6 @@ import { average as pipelineAverage, maximum, constant, - // comparison / logical greaterThan, equal, notEqual, @@ -136,10 +127,8 @@ import { conditional, logicalMaximum, logicalMinimum, - // ordering ascending, descending, - // arithmetic add, subtract, divide, @@ -157,7 +146,6 @@ import { pow, trunc, rand, - // aggregates count as pipelineCount, // duplicate name from main module countDistinct, countIf, @@ -166,13 +154,11 @@ import { arrayAgg, arrayAggDistinct, minimum, - // document / collection documentId as pipelineDocumentId, // duplicate name from main module collectionId, type as pipelineType, currentTimestamp, variable, - // array array, arrayFilter, arrayFirst, @@ -193,7 +179,6 @@ import { arrayGet, arrayLength, arraySum, - // map map, mapEntries, mapGet, @@ -202,7 +187,6 @@ import { mapRemove, mapSet, mapValues, - // string concat, toUpper, trim, @@ -226,7 +210,6 @@ import { regexFind, regexFindAll, regexMatch, - // timestamp timestampAdd, timestampDiff, timestampExtract, @@ -239,12 +222,10 @@ import { unixMicrosToTimestamp, unixMillisToTimestamp, unixSecondsToTimestamp, - // vector cosineDistance, dotProduct, euclideanDistance, vectorLength, - // result utility pipelineResultEqual, } from '@react-native-firebase/firestore/pipelines'; import type { @@ -289,240 +270,14 @@ import type { OneOf, } from '@react-native-firebase/firestore/pipelines'; -// ----- Default export and module access ----- -console.log(firestore().app); -console.log(firestore.SDK_VERSION); -console.log(firestore.firebase.SDK_VERSION); - -// ----- firebase.firestore at root ----- -console.log(firebase.firestore().app.name); -console.log(firebase.firestore().collection('foo')); - -// ----- firebase.firestore at app level ----- -console.log(firebase.app().firestore().app.name); -console.log(firebase.app().firestore().collection('foo')); - -// ----- Multi-app and database ID ----- -console.log(firebase.firestore(firebase.app()).app.name); -console.log(firebase.firestore(firebase.app('foo')).app.name); -// Database ID: firebase.app().firestore(databaseId) -console.log(firebase.app().firestore('(default)').app.name); -console.log(firebase.app().firestore('other-db').app.name); - -// ----- Default export with app arg ----- -console.log(firestore(firebase.app()).app.name); - -// ----- Statics (FirestoreStatics) ----- -console.log(firebase.firestore.Blob); -console.log(firebase.firestore.FieldPath); -console.log(firebase.firestore.FieldValue); -console.log(firebase.firestore.GeoPoint); -console.log(firebase.firestore.Timestamp); -console.log(firebase.firestore.Filter); -console.log(firebase.firestore.CACHE_SIZE_UNLIMITED); -firebase.firestore.setLogLevel('debug'); - -// ----- Firestore instance: references and batch ----- -const nsFirestore = firebase.firestore(); -console.log(nsFirestore.collection('users')); -console.log(nsFirestore.collection('users').doc('alice')); -console.log(nsFirestore.collection('users').doc('alice').collection('orders')); -console.log(nsFirestore.collectionGroup('orders')); -console.log(nsFirestore.batch()); - -// ----- CollectionReference ----- -const nsColl = nsFirestore.collection('users'); -const nsDocRef = nsColl.doc('alice'); -const nsQuery = nsColl.where('name', '==', 'test'); - -nsDocRef.set({ name: 'Alice', count: 1 }).then(() => {}); -nsDocRef.set({ name: 'Alice' }, { merge: true }).then(() => {}); - -nsDocRef.update({ count: 2 }).then(() => {}); -nsDocRef.update('count', 3).then(() => {}); - -nsDocRef.delete().then(() => {}); - -nsColl.add({ name: 'Bob' }).then((ref: FirebaseFirestoreTypes.DocumentReference) => { - console.log(ref.id); -}); - -nsDocRef.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - console.log(snap.exists()); - console.log(snap.data()); - console.log(snap.id); - console.log(snap.ref); - console.log(snap.metadata); -}); -nsDocRef.get({ source: 'cache' }).then(() => {}); -nsDocRef.get({ source: 'server' }).then(() => {}); - -nsQuery.get().then((snap: FirebaseFirestoreTypes.QuerySnapshot) => { - console.log(snap.docs); - console.log(snap.empty); - console.log(snap.size); - console.log(snap.docChanges()); -}); - -// ----- DocumentSnapshot ----- -nsDocRef.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - if (snap.exists()) { - const d = snap.data(); - console.log(d); - } - console.log(snap.get('field')); - console.log(snap.metadata.isEqual(snap.metadata)); -}); - -// ----- onSnapshot (document) ----- -const unsubDoc1 = nsDocRef.onSnapshot((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - console.log(snap.data()); -}); -const unsubDoc2 = nsDocRef.onSnapshot( - { includeMetadataChanges: true }, - (_snap: FirebaseFirestoreTypes.DocumentSnapshot) => {}, -); -const unsubDoc4 = nsDocRef.onSnapshot( - { source: 'cache' }, - (_snap: FirebaseFirestoreTypes.DocumentSnapshot) => {}, -); -const unsubDoc3 = nsDocRef.onSnapshot({ - next: (_snap: FirebaseFirestoreTypes.DocumentSnapshot) => {}, - error: (_e: Error) => {}, -}); -unsubDoc1(); -unsubDoc2(); -unsubDoc3(); -unsubDoc4(); - -// ----- onSnapshot (query) ----- -const unsubQuery1 = nsQuery.onSnapshot((snap: FirebaseFirestoreTypes.QuerySnapshot) => { - console.log(snap.docs); -}); -const unsubQuery2 = nsQuery.onSnapshot( - { includeMetadataChanges: true }, - { next: (_snap: FirebaseFirestoreTypes.QuerySnapshot) => {}, error: (_e: Error) => {} }, -); -const unsubQuery3 = nsQuery.onSnapshot( - { source: 'cache', includeMetadataChanges: true }, - { next: (_snap: FirebaseFirestoreTypes.QuerySnapshot) => {}, error: (_e: Error) => {} }, -); -unsubQuery1(); -unsubQuery2(); -unsubQuery3(); - -// ----- Query: where, orderBy, limit, cursor ----- -const nsQuery2 = nsColl - .where('age', '>', 18) - .orderBy('age', 'desc') - .orderBy('name') - .limit(10) - .limitToLast(5) - .startAt(1) - .startAfter(2) - .endAt(10) - .endBefore(9); -void nsQuery2; - -// ----- Firestore instance: loadBundle, namedQuery, runTransaction ----- -const nsLoadTask = nsFirestore.loadBundle('bundle-data'); -console.log(nsLoadTask.then(() => {})); - -const nsNamed = nsFirestore.namedQuery('my-query'); -console.log(nsNamed); - -nsFirestore - .runTransaction(async (tx: FirebaseFirestoreTypes.Transaction) => { - const snap = await tx.get(nsDocRef); - if (snap.exists()) { - tx.update(nsDocRef, { count: ((snap.data() as { count?: number })?.count ?? 0) + 1 }); - } - return null; - }) - .then(() => {}); - -// ----- Firestore instance: persistence and network ----- -nsFirestore.clearPersistence().then(() => {}); -nsFirestore.waitForPendingWrites().then(() => {}); -nsFirestore.terminate().then(() => {}); -nsFirestore.useEmulator('localhost', 8080); -nsFirestore.enableNetwork().then(() => {}); -nsFirestore.disableNetwork().then(() => {}); - -// ----- Firestore instance: settings ----- -nsFirestore.settings({ persistence: true }).then(() => {}); - -// ----- Persistent cache index manager (namespaced) ----- -const nsIndexManager = nsFirestore.persistentCacheIndexManager(); -if (nsIndexManager) { - nsIndexManager.enableIndexAutoCreation().then(() => {}); - nsIndexManager.disableIndexAutoCreation().then(() => {}); - nsIndexManager.deleteAllIndexes().then(() => {}); -} - -// ----- WriteBatch (namespaced) ----- -const nsBatch = nsFirestore.batch(); -nsBatch.set(nsDocRef, { name: 'Alice' }); -nsBatch.set(nsDocRef, { name: 'Alice' }, { merge: true }); -nsBatch.update(nsDocRef, { count: 1 }); -nsBatch.delete(nsDocRef); -nsBatch.commit().then(() => {}); - -// ----- Namespaced FieldValue (statics) ----- -const nsFieldPath = new firebase.firestore.FieldPath('user', 'name'); -void nsFieldPath; -const nsBlob = firebase.firestore.Blob.fromBase64String('dGVzdA=='); -void nsBlob; -const nsGeoPoint = new firebase.firestore.GeoPoint(0, 0); -void nsGeoPoint; -const nsTimestamp = firebase.firestore.Timestamp.now(); -void nsTimestamp; -const nsDelete = firebase.firestore.FieldValue.delete(); -const nsServerTs = firebase.firestore.FieldValue.serverTimestamp(); -const nsArrayUnion = firebase.firestore.FieldValue.arrayUnion(1, 2); -const nsArrayRemove = firebase.firestore.FieldValue.arrayRemove(1); -void nsArrayRemove; -const nsIncrement = firebase.firestore.FieldValue.increment(1); - -nsDocRef - .set({ - name: 'x', - deleted: nsDelete, - ts: nsServerTs, - arr: nsArrayUnion, - cnt: nsIncrement, - }) - .then(() => {}); - -// ----- withConverter (namespaced) ----- -interface User { - name: string; - age: number; -} -const nsConverter: FirebaseFirestoreTypes.FirestoreDataConverter = { - toFirestore(u: User) { - return u; - }, - fromFirestore(snap: FirebaseFirestoreTypes.QueryDocumentSnapshot): User { - return snap.data() as User; - }, -}; -const nsCollWithConv = nsFirestore.collection('users').withConverter(nsConverter); -const nsDocWithConv = nsCollWithConv.doc('alice'); -nsDocWithConv.set({ name: 'Alice', age: 30 }).then(() => {}); -nsDocWithConv.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) => { - const u = snap.data(); - if (u) console.log(u.name, u.age); -}); - // ----- getFirestore ----- const modFirestore1 = getFirestore(); console.log(modFirestore1.app.name); -const modFirestore2 = getFirestore(firebase.app()); +const modFirestore2 = getFirestore(getApp()); console.log(modFirestore2.app.name); -const modFirestore3 = getFirestore(firebase.app(), 'other-db'); +const modFirestore3 = getFirestore(getApp(), 'other-db'); console.log(modFirestore3.app.name); // ----- connectFirestoreEmulator ----- @@ -535,7 +290,7 @@ connectFirestoreEmulator(modFirestore1, 'localhost', 8080, { setLogLevel('debug'); // ----- initializeFirestore ----- -initializeFirestore(firebase.app(), { +initializeFirestore(getApp(), { cacheSizeBytes: CACHE_SIZE_UNLIMITED, }).then((fs: Firestore) => { console.log(fs.app.name); @@ -582,27 +337,22 @@ runTransaction(modFirestore1, async tx => { }).then(() => {}); // Root runtime exports must also carry the generic modular type surface. -function acceptRootDocumentSnapshot(_snap: DocumentSnapshot) {} -function acceptRootTransaction(tx: Transaction, tRef: DocumentReference) { - tx.update(tRef, { active: true }); -} +function acceptRootDocumentSnapshot(_snap: FirestoreDocumentSnapshot) {} runTransaction(modFirestore1, async tx => { - const rootTx: Transaction = tx; - const rootSnap: DocumentSnapshot = await rootTx.get(modDoc); + const rootSnap = await tx.get(modDoc); acceptRootDocumentSnapshot(rootSnap); - acceptRootTransaction(rootTx, modDoc); + tx.update(modDoc, { active: true }); if (rootSnap.exists()) { const rootData: DocumentData = rootSnap.data(); void rootData; } return rootSnap; -}).then((rootSnap: DocumentSnapshot) => { +}).then(rootSnap => { acceptRootDocumentSnapshot(rootSnap); }); -const rootBatch: WriteBatch = writeBatch(modFirestore1); +const rootBatch = writeBatch(modFirestore1); rootBatch.update(modDoc, { active: true }); void acceptRootDocumentSnapshot; -void acceptRootTransaction; void rootBatch; // ----- query, where, or, and, orderBy, startAt, startAfter, endAt, endBefore, limit, limitToLast ----- diff --git a/packages/firestore/typedoc.json b/packages/firestore/typedoc.json index f49516003f..077e6a595c 100644 --- a/packages/firestore/typedoc.json +++ b/packages/firestore/typedoc.json @@ -1,13 +1,15 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": [ - "lib/modular.ts", + "lib/index.ts", "lib/FirestoreTransaction.ts", "lib/FirestoreWriteBatch.ts", "lib/FirestoreFilter.ts", "lib/FirestoreDocumentChange.ts", "lib/FirestoreDocumentSnapshot.ts", "lib/FirestorePersistentCacheIndexManager.ts", + "lib/FirestoreAggregate.ts", + "lib/FirestoreQuery.ts", "lib/FirestoreQuerySnapshot.ts", "lib/types/firestore.ts", "lib/pipelines/index.ts", diff --git a/packages/functions/__tests__/functions.test.ts b/packages/functions/__tests__/functions.test.ts index 46b6d262cf..df7d31db1f 100644 --- a/packages/functions/__tests__/functions.test.ts +++ b/packages/functions/__tests__/functions.test.ts @@ -1,6 +1,6 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + import { - firebase, getFunctions, connectFunctionsEmulator, httpsCallable, @@ -12,76 +12,26 @@ import { } from '../lib'; import type { HttpsCallableStreamOptions, HttpsCallableStreamResult } from '../lib/types/functions'; -import functions from '../lib/namespaced'; -import { - createCheckV9Deprecation, - type CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; - -// @ts-ignore test -import { getApp, type ReactNativeFirebase } from '@react-native-firebase/app'; - -type FirebaseApp = ReactNativeFirebase.FirebaseApp; - // @ts-ignore test import FirebaseModule from '../../app/lib/internal/FirebaseModule'; describe('Cloud Functions', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.functions).toBeDefined(); - expect(app.functions().httpsCallable).toBeDefined(); - }); - - describe('useFunctionsEmulator()', function () { - it('useFunctionsEmulator -> uses 10.0.2.2', function () { - functions().useEmulator('localhost', 5001); - - // @ts-ignore - expect(functions()._useFunctionsEmulatorHost).toBe('10.0.2.2'); - - functions().useEmulator('127.0.0.1', 5001); - - // @ts-ignore - expect(functions()._useFunctionsEmulatorHost).toBe('10.0.2.2'); - }); - - it('prefers emulator to custom domain', function () { - const app = firebase.app(); - const customUrl = 'https://test.com'; - const functions = app.functions(customUrl); - - functions.useFunctionsEmulator('http://10.0.2.2'); - - // @ts-ignore - expect(functions._useFunctionsEmulatorHost).toBe('10.0.2.2'); - }); - }); - - describe('httpcallable()', function () { - it('throws an error with an incorrect timeout', function () { - const app = firebase.app(); - - // @ts-ignore - expect(() => app.functions().httpsCallable('example', { timeout: 'test' })).toThrow( - 'HttpsCallableOptions.timeout expected a Number in milliseconds', + describe('modular', function () { + beforeEach(function () { + // @ts-ignore test + jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { + return new Proxy( + {}, + { + get: () => + jest.fn().mockResolvedValue({ + constants: {}, + } as never), + }, ); }); }); - }); - describe('modular', function () { it('`getFunctions` function is properly exposed to end user', function () { expect(getFunctions).toBeDefined(); }); @@ -110,7 +60,6 @@ describe('Cloud Functions', function () { }); it('`HttpsCallable` type is properly exposed to end user', function () { - // Type check - this will fail at compile time if type is not exported const callable = (async () => { return { data: { result: 42 } }; }) as HttpsCallable<{ test: string }, { result: number }>; @@ -128,7 +77,7 @@ describe('Cloud Functions', function () { expect(callable).toBeDefined(); }); - it('`FunctionsModule` type is properly exposed to end user', function () { + it('`Functions` type is properly exposed to end user', function () { const functionsInstance: Functions = getFunctions(); expect(functionsInstance).toBeDefined(); expect(functionsInstance.httpsCallable).toBeDefined(); @@ -136,71 +85,6 @@ describe('Cloud Functions', function () { expect(functionsInstance.useFunctionsEmulator).toBeDefined(); expect(functionsInstance.useEmulator).toBeDefined(); }); - - it('`Functions` type is properly exposed to end user', function () { - const functionsInstance: Functions = getFunctions(); - expect(functionsInstance).toBeDefined(); - }); - - it('`FirebaseApp` type is properly exposed to end user', function () { - const app = getApp(); - expect(app).toBeDefined(); - expect(app.functions).toBeDefined(); - }); - }); - }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let functionsRefV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - functionsRefV9Deprecation = createCheckV9Deprecation(['functions']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - source: 'cache', - changes: [], - documents: [], - metadata: {}, - path: 'foo', - } as never), - }, - ); - }); - }); - - describe('Cloud Functions', function () { - it('useFunctionsEmulator()', function () { - const functions = (getApp() as unknown as FirebaseApp).functions(); - functionsRefV9Deprecation( - () => connectFunctionsEmulator(functions, 'localhost', 8080), - () => functions.useEmulator('localhost', 8080), - 'useEmulator', - ); - }); - - it('httpsCallable()', function () { - const functions = (getApp() as unknown as FirebaseApp).functions(); - functionsRefV9Deprecation( - () => httpsCallable(functions, 'example'), - () => functions.httpsCallable('example'), - 'httpsCallable', - ); - }); - - it('httpsCallableFromUrl()', function () { - const functions = (getApp() as unknown as FirebaseApp).functions(); - functionsRefV9Deprecation( - () => httpsCallableFromUrl(functions, 'https://example.com/example'), - () => functions.httpsCallableFromUrl('https://example.com/example'), - 'httpsCallableFromUrl', - ); - }); }); }); }); diff --git a/packages/functions/__tests__/httpsCallable-timeout-native.test.ts b/packages/functions/__tests__/httpsCallable-timeout-native.test.ts index 6edfcbb432..1206d30370 100644 --- a/packages/functions/__tests__/httpsCallable-timeout-native.test.ts +++ b/packages/functions/__tests__/httpsCallable-timeout-native.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; jest.mock('@react-native-firebase/app/dist/module/common', () => { const actualCommon = jest.requireActual( @@ -11,7 +11,8 @@ jest.mock('@react-native-firebase/app/dist/module/common', () => { }; }); -import { firebase } from '../lib'; +// @ts-ignore test +import { getFunctions, httpsCallable } from '../lib'; function timeoutFromNativeCall(nativeMock: jest.Mock): number { const call = nativeMock.mock.calls[0] as unknown[] | undefined; @@ -24,22 +25,12 @@ function timeoutFromNativeCall(nativeMock: jest.Mock): number { describe('httpsCallable timeout units on native platforms', function () { let httpsCallableNative: jest.Mock; - beforeAll(function () { - // @ts-ignore test-only global - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(function () { - // @ts-ignore test-only global - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - beforeEach(function () { httpsCallableNative = jest .fn<(...args: unknown[]) => Promise<{ data: null }>>() .mockResolvedValue({ data: null }); - const functions = firebase.app().functions(); + const functions = getFunctions(); // @ts-ignore test-only internal cache functions._nativeModule = { httpsCallable: ( @@ -53,7 +44,8 @@ describe('httpsCallable timeout units on native platforms', function () { }); it('converts milliseconds to seconds for httpsCallable (Android/iOS)', async function () { - const runner = firebase.app().functions().httpsCallable('example', { timeout: 30000 }); + const functions = getFunctions(); + const runner = httpsCallable(functions, 'example', { timeout: 30000 }); await runner(); expect(httpsCallableNative).toHaveBeenCalledTimes(1); @@ -61,9 +53,10 @@ describe('httpsCallable timeout units on native platforms', function () { }); it('does not mutate a reused options object on native platforms', async function () { + const functions = getFunctions(); const sharedOptions = { timeout: 30000 }; - const runnerA = firebase.app().functions().httpsCallable('exampleA', sharedOptions); - const runnerB = firebase.app().functions().httpsCallable('exampleB', sharedOptions); + const runnerA = httpsCallable(functions, 'exampleA', sharedOptions); + const runnerB = httpsCallable(functions, 'exampleB', sharedOptions); await runnerA(); await runnerB(); diff --git a/packages/functions/__tests__/httpsCallable-timeout-other.test.ts b/packages/functions/__tests__/httpsCallable-timeout-other.test.ts index 24b3382e7f..7a50ea2774 100644 --- a/packages/functions/__tests__/httpsCallable-timeout-other.test.ts +++ b/packages/functions/__tests__/httpsCallable-timeout-other.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; jest.mock('@react-native-firebase/app/dist/module/common', () => { const actualCommon = jest.requireActual( @@ -11,7 +11,7 @@ jest.mock('@react-native-firebase/app/dist/module/common', () => { }; }); -import { firebase } from '../lib'; +import { getFunctions, httpsCallable } from '../lib'; function timeoutFromNativeCall(nativeMock: jest.Mock): number { const call = nativeMock.mock.calls[0] as unknown[] | undefined; @@ -24,22 +24,12 @@ function timeoutFromNativeCall(nativeMock: jest.Mock): number { describe('httpsCallable timeout units on other platforms', function () { let httpsCallableNative: jest.Mock; - beforeAll(function () { - // @ts-ignore test-only global - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(function () { - // @ts-ignore test-only global - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - beforeEach(function () { httpsCallableNative = jest .fn<(...args: unknown[]) => Promise<{ data: null }>>() .mockResolvedValue({ data: null }); - const functions = firebase.app().functions(); + const functions = getFunctions(); // @ts-ignore test-only internal cache functions._nativeModule = { httpsCallable: ( @@ -53,7 +43,8 @@ describe('httpsCallable timeout units on other platforms', function () { }); it('keeps milliseconds for httpsCallable (web/macos)', async function () { - const runner = firebase.app().functions().httpsCallable('example', { timeout: 30000 }); + const functions = getFunctions(); + const runner = httpsCallable(functions, 'example', { timeout: 30000 }); await runner(); expect(httpsCallableNative).toHaveBeenCalledTimes(1); diff --git a/packages/functions/e2e/functions.e2e.js b/packages/functions/e2e/functions.e2e.js index f8c1d2874c..630fd014c6 100644 --- a/packages/functions/e2e/functions.e2e.js +++ b/packages/functions/e2e/functions.e2e.js @@ -95,362 +95,6 @@ function e2eCallableTimeoutOptions(extra = {}) { } describe('functions() modular', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('namespace', function () { - it('accepts passing in an FirebaseApp instance as first arg', async function () { - const appName = `functionsApp${FirebaseHelpers.id}2`; - const platformAppConfig = FirebaseHelpers.app.config(); - const app = await firebase.initializeApp(platformAppConfig, appName); - - const functionsForApp = firebase.functions(app); - - functionsForApp.app.should.equal(app); - functionsForApp.app.name.should.equal(app.name); - - // check from an app - app.functions().app.should.equal(app); - app.functions().app.name.should.equal(app.name); - }); - - it('accepts passing in a region string as first arg to an app', async function () { - const region = 'europe-west1'; - const functionsForRegion = firebase.app().functions(region); - - functionsForRegion._customUrlOrRegion.should.equal(region); - functionsForRegion.app.should.equal(firebase.app()); - functionsForRegion.app.name.should.equal(firebase.app().name); - - firebase.app().functions(region).app.should.equal(firebase.app()); - - firebase.app().functions(region)._customUrlOrRegion.should.equal(region); - - const functionRunner = functionsForRegion.httpsCallable( - 'testFunctionCustomRegion', - e2eCallableTimeoutOptions(), - ); - - const response = await functionRunner(); - response.data.should.equal(region); - }); - - it('accepts passing in a custom url string as first arg to an app', async function () { - const customUrl = 'https://us-central1-react-native-firebase-testing.cloudfunctions.net'; - const functionsForCustomUrl = firebase.app().functions(customUrl); - - functionsForCustomUrl._customUrlOrRegion.should.equal(customUrl); - functionsForCustomUrl.app.should.equal(firebase.app()); - functionsForCustomUrl.app.name.should.equal(firebase.app().name); - - functionsForCustomUrl.app.should.equal(firebase.app()); - - functionsForCustomUrl._customUrlOrRegion.should.equal(customUrl); - - const functionRunner = functionsForCustomUrl.httpsCallable( - 'testFunctionDefaultRegionV2', - e2eCallableTimeoutOptions(), - ); - - const response = await functionRunner(); - response.data.should.equal('null'); - }); - }); - - describe('emulator', function () { - it('configures functions emulator via deprecated method with no port', async function () { - const region = 'us-central1'; - const fnName = 'helloWorldV2'; - const functions = firebase.app().functions(region); - functions.useFunctionsEmulator('http://localhost'); - const response = await functions.httpsCallable(fnName, e2eCallableTimeoutOptions())(); - response.data.should.equal('Hello from Firebase!'); - }); - - it('configures functions emulator via deprecated method with port', async function () { - const region = 'us-central1'; - const fnName = 'helloWorldV2'; - const functions = firebase.app().functions(region); - functions.useFunctionsEmulator('http://localhost:5001'); - const response = await functions.httpsCallable(fnName, e2eCallableTimeoutOptions())(); - response.data.should.equal('Hello from Firebase!'); - }); - - it('configures functions emulator', async function () { - const region = 'us-central1'; - const fnName = 'helloWorldV2'; - const functions = firebase.app().functions(region); - functions.useEmulator('localhost', 5001); - const response = await functions.httpsCallable(fnName, e2eCallableTimeoutOptions())(); - response.data.should.equal('Hello from Firebase!'); - }); - }); - - describe('httpsCallableFromUrl()', function () { - it('Calls a function by URL', async function () { - let hostname = 'localhost'; - if (Platform.android) { - hostname = '10.0.2.2'; - } - const functionRunner = firebase - .functions() - .httpsCallableFromUrl( - `http://${hostname}:5001/react-native-firebase-testing/us-central1/helloWorldV2`, - e2eCallableTimeoutOptions(), - ); - const response = await functionRunner(); - response.data.should.equal('Hello from Firebase!'); - }); - }); - - describe('httpsCallable(fnName)(args)', function () { - it('accepts primitive args: undefined', async function () { - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const response = await functionRunner(); - response.data.should.equal('null'); - }); - - it('accepts primitive args: string', async function () { - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const response = await functionRunner('hello'); - response.data.should.equal('string'); - }); - - it('accepts primitive args: number', async function () { - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const response = await functionRunner(123); - response.data.should.equal('number'); - }); - - it('accepts primitive args: boolean', async function () { - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const response = await functionRunner(true); - response.data.should.equal('boolean'); - }); - - it('accepts primitive args: null', async function () { - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const response = await functionRunner(null); - response.data.should.equal('null'); - }); - - it('accepts array args', async function () { - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const response = await functionRunner([1, 2, 3, 4]); - response.data.should.equal('array'); - }); - - it('accepts object args', async function () { - const type = 'object'; - const inputData = SAMPLE_DATA[type]; - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const { data: outputData } = await functionRunner({ - type, - inputData, - }); - should.deepEqual(outputData, inputData); - }); - - it('accepts complex nested objects', async function () { - const type = 'deepObject'; - const inputData = SAMPLE_DATA[type]; - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const { data: outputData } = await functionRunner({ - type, - inputData, - }); - should.deepEqual(outputData, inputData); - }); - - it('accepts complex nested arrays', async function () { - const type = 'deepArray'; - const inputData = SAMPLE_DATA[type]; - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - const { data: outputData } = await functionRunner({ - type, - inputData, - }); - should.deepEqual(outputData, inputData); - }); - }); - - describe('HttpsError', function () { - it('errors return instance of HttpsError', async function () { - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - - try { - await functionRunner({}); - return Promise.reject(new Error('Function did not reject with error.')); - } catch (e) { - should.equal(e.details, null); - e.code.should.equal('invalid-argument'); - e.message.should.equal('Invalid test requested.'); - } - - return Promise.resolve(); - }); - - it('HttpsError.details -> allows returning complex data', async function () { - let type = 'deepObject'; - let inputData = SAMPLE_DATA[type]; - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - try { - await functionRunner({ - type, - inputData, - asError: true, - }); - return Promise.reject(new Error('Function did not reject with error.')); - } catch (e) { - should.deepEqual(e.details, inputData); - e.code.should.equal('cancelled'); - e.message.should.equal( - 'Response data was requested to be sent as part of an Error payload, so here we are!', - ); - } - - type = 'deepArray'; - inputData = SAMPLE_DATA[type]; - try { - await functionRunner({ - type, - inputData, - asError: true, - }); - return Promise.reject(new Error('Function did not reject with error.')); - } catch (e) { - should.deepEqual(e.details, inputData); - e.code.should.equal('cancelled'); - e.message.should.equal( - 'Response data was requested to be sent as part of an Error payload, so here we are!', - ); - } - - return Promise.resolve(); - }); - - it('HttpsError.details -> allows returning primitives', async function () { - let type = 'number'; - let inputData = SAMPLE_DATA[type]; - const functionRunner = firebase - .functions() - .httpsCallable('testFunctionDefaultRegionV2', e2eCallableTimeoutOptions()); - try { - await functionRunner({ - type, - inputData, - asError: true, - }); - return Promise.reject(new Error('Function did not reject with error.')); - } catch (e) { - e.code.should.equal('cancelled'); - e.message.should.equal( - 'Response data was requested to be sent as part of an Error payload, so here we are!', - ); - should.deepEqual(e.details, inputData); - } - - type = 'string'; - inputData = SAMPLE_DATA[type]; - try { - await functionRunner({ - type, - inputData, - asError: true, - }); - return Promise.reject(new Error('Function did not reject with error.')); - } catch (e) { - should.deepEqual(e.details, inputData); - e.code.should.equal('cancelled'); - e.message.should.equal( - 'Response data was requested to be sent as part of an Error payload, so here we are!', - ); - } - - type = 'boolean'; - inputData = SAMPLE_DATA[type]; - try { - await functionRunner({ - type, - inputData, - asError: true, - }); - return Promise.reject(new Error('Function did not reject with error.')); - } catch (e) { - should.deepEqual(e.details, inputData); - e.code.should.equal('cancelled'); - e.message.should.equal( - 'Response data was requested to be sent as part of an Error payload, so here we are!', - ); - } - - type = 'null'; - inputData = SAMPLE_DATA[type]; - try { - await functionRunner({ - type, - inputData, - asError: true, - }); - return Promise.reject(new Error('Function did not reject with error.')); - } catch (e) { - should.deepEqual(e.details, inputData); - e.code.should.equal('cancelled'); - e.message.should.equal( - 'Response data was requested to be sent as part of an Error payload, so here we are!', - ); - } - - return Promise.resolve(); - }); - - it('HttpsCallableOptions.timeout will error when timeout is exceeded', async function () { - const functionRunner = firebase.functions().httpsCallable('sleeperV2', { timeout: 1000 }); - try { - await functionRunner({ delay: 3000 }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - if (Platform.other) { - error.message.should.containEql('deadline-exceeded'); - } else { - error.message.should.containEql('DEADLINE').containEql('EXCEEDED'); - } - return Promise.resolve(); - } - }); - }); - }); - describe('modular', function () { describe('getFunctions', function () { it('pass app as argument', function () { @@ -479,8 +123,8 @@ describe('functions() modular', function () { functions.app.name.should.equal(app.name); // check from an app - app.functions().app.should.equal(app); - app.functions().app.name.should.equal(app.name); + getFunctions(app).app.should.equal(app); + getFunctions(app).app.name.should.equal(app.name); }); it('accepts passing in a region string as first arg to an app', async function () { @@ -494,9 +138,9 @@ describe('functions() modular', function () { functionsForRegion.app.should.equal(getApp()); functionsForRegion.app.name.should.equal(getApp().name); - getApp().functions(region).app.should.equal(getApp()); + getFunctions(getApp(), region).app.should.equal(getApp()); - getApp().functions(region)._customUrlOrRegion.should.equal(region); + getFunctions(getApp(), region)._customUrlOrRegion.should.equal(region); const functionRunner = httpsCallable( functionsForRegion, @@ -539,7 +183,6 @@ describe('functions() modular', function () { const { getFunctions, httpsCallable, connectFunctionsEmulator } = functionsModular; const region = 'us-central1'; const fnName = 'helloWorldV2'; - // const functions = firebase.app().functions(region); const functions = getFunctions(getApp(), region); connectFunctionsEmulator(functions, 'localhost', 5001); const response = await httpsCallable(functions, fnName, e2eCallableTimeoutOptions())(); diff --git a/packages/functions/lib/index.ts b/packages/functions/lib/index.ts index 23c93a23d7..ef47cf3006 100644 --- a/packages/functions/lib/index.ts +++ b/packages/functions/lib/index.ts @@ -15,18 +15,495 @@ * */ -// Export types from types/functions +import { isAndroid, isNumber, isOther } from '@react-native-firebase/app/dist/module/common'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import './types/internal'; +import { HttpsError, type NativeError } from './HttpsError'; +import type { FunctionsInternal } from './types/internal'; +import { version } from './version'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import fallBackModule from './web/RNFBFunctionsModule'; +import type { + HttpsCallableOptions, + HttpsCallableStreamOptions, + Functions, + HttpsCallable, +} from './types/functions'; +import type { CustomHttpsCallableOptions, FunctionsStreamingEvent } from './types/internal'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +const nativeModuleName = 'NativeRNFBTurboFunctions'; + +export const HttpsErrorCode = { + OK: 'ok', + CANCELLED: 'cancelled', + UNKNOWN: 'unknown', + INVALID_ARGUMENT: 'invalid-argument', + DEADLINE_EXCEEDED: 'deadline-exceeded', + NOT_FOUND: 'not-found', + ALREADY_EXISTS: 'already-exists', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + RESOURCE_EXHAUSTED: 'resource-exhausted', + FAILED_PRECONDITION: 'failed-precondition', + ABORTED: 'aborted', + OUT_OF_RANGE: 'out-of-range', + UNIMPLEMENTED: 'unimplemented', + INTERNAL: 'internal', + UNAVAILABLE: 'unavailable', + DATA_LOSS: 'data-loss', + UNSUPPORTED_TYPE: 'unsupported-type', + FAILED_TO_PARSE_WRAPPED_NUMBER: 'failed-to-parse-wrapped-number', + // Web codes are lowercase dasherized. + ok: 'ok', + cancelled: 'cancelled', + unknown: 'unknown', + 'invalid-argument': 'invalid-argument', + 'deadline-exceeded': 'deadline-exceeded', + 'not-found': 'not-found', + 'already-exists': 'already-exists', + 'permission-denied': 'permission-denied', + unauthenticated: 'unauthenticated', + 'resource-exhausted': 'resource-exhausted', + 'failed-precondition': 'failed-precondition', + aborted: 'aborted', + 'out-of-range': 'out-of-range', + unimplemented: 'unimplemented', + internal: 'internal', + unavailable: 'unavailable', + 'data-loss': 'data-loss', +} as const; + +function normalizeHttpsCallableTimeoutOptions(options: HttpsCallableOptions): HttpsCallableOptions { + if (!options.timeout) { + return options; + } + if (!isNumber(options.timeout)) { + throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds'); + } + if (isOther) { + return options; + } + return { + ...options, + timeout: options.timeout / 1000, + }; +} + +let _id_functions_streaming_event = 0; + +class FirebaseFunctionsModule extends FirebaseModule { + _customUrlOrRegion: string; + private _useFunctionsEmulatorHost: string | null; + private _useFunctionsEmulatorPort: number; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); + this._customUrlOrRegion = customUrlOrRegion || 'us-central1'; + this._useFunctionsEmulatorHost = null; + this._useFunctionsEmulatorPort = -1; + + this.emitter.addListener( + this.eventNameForApp('functions_streaming_event'), + (event: FunctionsStreamingEvent) => { + this.emitter.emit( + this.eventNameForApp(`functions_streaming_event:${event.listenerId}`), + event, + ); + }, + ); + } + + /** + * Private helper method to create a streaming handler for callable functions. + * This method encapsulates the common streaming logic used by both + * httpsCallable and httpsCallableFromUrl. + */ + private async _createStreamHandler( + initiateStream: (listenerId: number) => void, + ): Promise<{ stream: AsyncGenerator; data: Promise }> { + const listenerId = _id_functions_streaming_event++; + const eventName = this.eventNameForApp(`functions_streaming_event:${listenerId}`); + const nativeModule = this.native; + + // Capture JavaScript stack at stream creation time so an error can be thrown with the correct stack trace + const capturedStack = new Error().stack; + + // Queue to buffer events before iteration starts + const eventQueue: unknown[] = []; + let resolveNext: ((value: IteratorResult) => void) | null = null; + let error: HttpsError | null = null; + let done = false; + let finalData: unknown = null; + let resolveDataPromise: ((value: unknown) => void) | null = null; + let rejectDataPromise: ((reason: Error) => void) | null = null; + + const subscription = this.emitter.addListener(eventName, (event: FunctionsStreamingEvent) => { + const body = event.body; + + if (body.error) { + const { code, message, details } = body.error || {}; + error = new HttpsError( + HttpsErrorCode[code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, + message || 'Unknown error', + details ?? null, + { jsStack: capturedStack }, + ); + done = true; + subscription.remove(); + if (nativeModule.removeFunctionsStreaming) { + nativeModule.removeFunctionsStreaming(listenerId); + } + if (resolveNext) { + resolveNext({ done: true, value: undefined }); + resolveNext = null; + } + if (rejectDataPromise) { + rejectDataPromise(error); + rejectDataPromise = null; + } + return; + } + + if (body.done) { + finalData = body.data; + done = true; + subscription.remove(); + if (nativeModule.removeFunctionsStreaming) { + nativeModule.removeFunctionsStreaming(listenerId); + } + if (resolveNext) { + resolveNext({ done: true, value: undefined }); + resolveNext = null; + } + if (resolveDataPromise) { + resolveDataPromise(finalData); + resolveDataPromise = null; + } + } else if (body.data !== null && body.data !== undefined) { + // This is a chunk + if (resolveNext) { + resolveNext({ done: false, value: body.data }); + resolveNext = null; + } else { + eventQueue.push(body.data); + } + } + }); + + // Start native streaming via the provided callback + initiateStream(listenerId); + + // Use async generator function for better compatibility with Hermes/React Native + async function* streamGenerator() { + try { + while (true) { + if (error) { + const err = error as HttpsError; + throw new HttpsError( + HttpsErrorCode[err.code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, + err.message, + err.details ?? null, + { + jsStack: capturedStack, + }, + ); + } + + if (eventQueue.length > 0) { + yield eventQueue.shift(); + continue; + } + + if (error) { + const err = error as HttpsError; + throw new HttpsError( + HttpsErrorCode[err.code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, + err.message, + err.details ?? null, + { + jsStack: capturedStack, + }, + ); + } + if (done) { + return; + } + + // Wait for next event + const result = await new Promise>(resolve => { + resolveNext = resolve; + }); + + // Check result after promise resolves + if (error) { + const err = error as HttpsError; + throw new HttpsError(err.code, err.message, err.details ?? null, { + jsStack: capturedStack, + }); + } + if (result.done || done) { + return; + } + + if (result.value !== undefined && result.value !== null) { + yield result.value; + } + } + } finally { + // Cleanup when generator is closed/returned + subscription.remove(); + if (nativeModule.removeFunctionsStreaming) { + nativeModule.removeFunctionsStreaming(listenerId); + } + } + } + + const asyncIterator = streamGenerator(); + + // Create a promise that resolves with the final data when the listener receives done/error + const dataPromise = new Promise((resolve, reject) => { + resolveDataPromise = resolve; + rejectDataPromise = reject; + if (error) { + rejectDataPromise = null; + reject(error); + } else if (done) { + resolveDataPromise = null; + resolve(finalData); + } + }); + + return { + stream: asyncIterator, + data: dataPromise, + }; + } + + httpsCallable(name: string, options: HttpsCallableOptions = {}) { + const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options); + + const callableFunction = ((data?: unknown) => { + const nativePromise = this.native.httpsCallable( + this._useFunctionsEmulatorHost, + this._useFunctionsEmulatorPort, + name, + { + data, + }, + normalizedOptions, + ); + return nativePromise.catch((nativeError: NativeError) => { + const { code, message, details } = nativeError.userInfo || {}; + return Promise.reject( + new HttpsError( + HttpsErrorCode[code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, + message || nativeError.message, + details || null, + nativeError, + ), + ); + }); + }) as HttpsCallable; + + callableFunction.stream = async ( + data?: unknown, + streamOptions?: HttpsCallableStreamOptions, + ) => { + const platformOptions = !isOther + ? normalizedOptions + : ({ + ...normalizedOptions, + httpsCallableStreamOptions: streamOptions || {}, + } as CustomHttpsCallableOptions); + return this._createStreamHandler(listenerId => { + this.native.httpsCallableStream( + this._useFunctionsEmulatorHost || null, + this._useFunctionsEmulatorPort || -1, + name, + { data }, + platformOptions || {}, + listenerId, + ); + }); + }; + + return callableFunction; + } + + httpsCallableFromUrl(url: string, options: HttpsCallableOptions = {}) { + const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options); + + const callableFunction = ((data?: unknown) => { + const nativePromise = this.native.httpsCallableFromUrl( + this._useFunctionsEmulatorHost, + this._useFunctionsEmulatorPort, + url, + { + data, + }, + normalizedOptions, + ); + return nativePromise.catch((nativeError: NativeError) => { + const { code, message, details } = nativeError.userInfo || {}; + return Promise.reject( + new HttpsError( + HttpsErrorCode[code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, + message || nativeError.message, + details || null, + nativeError, + ), + ); + }); + }) as HttpsCallable; + + callableFunction.stream = async ( + data?: unknown, + streamOptions?: HttpsCallableStreamOptions, + ) => { + const platformOptions = !isOther + ? normalizedOptions + : ({ + ...normalizedOptions, + httpsCallableStreamOptions: streamOptions || {}, + } as CustomHttpsCallableOptions); + return this._createStreamHandler(listenerId => { + this.native.httpsCallableStreamFromUrl( + this._useFunctionsEmulatorHost || null, + this._useFunctionsEmulatorPort || -1, + url, + { data }, + platformOptions || {}, + listenerId, + ); + }); + }; + return callableFunction; + } + + useFunctionsEmulator(origin: string): void { + const match = /https?\:.*\/\/([^:]+):?(\d+)?/.exec(origin); + if (!match) { + throw new Error('Invalid emulator origin format'); + } + const [, host, portStr] = match; + const port = portStr ? parseInt(portStr) : 5001; + this.useEmulator(host as string, port); + } + + useEmulator(host: string, port: number): void { + if (!isNumber(port)) { + throw new Error('useEmulator port parameter must be a number'); + } + + let _host = host; + + const androidBypassEmulatorUrlRemap = + typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && + this.firebaseJson.android_bypass_emulator_url_remap; + if (!androidBypassEmulatorUrlRemap && isAndroid && _host) { + if (_host.startsWith('localhost')) { + _host = _host.replace('localhost', '10.0.2.2'); + // eslint-disable-next-line no-console + console.log( + 'Mapping functions host "localhost" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', + ); + } + if (_host.startsWith('127.0.0.1')) { + _host = _host.replace('127.0.0.1', '10.0.2.2'); + // eslint-disable-next-line no-console + console.log( + 'Mapping functions host "127.0.0.1" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', + ); + } + } + this._useFunctionsEmulatorHost = _host || null; + this._useFunctionsEmulatorPort = port || -1; + } +} + +const config: ModuleConfig = { + namespace: 'functions', + nativeModuleName, + nativeEvents: ['functions_streaming_event'], + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: true, + turboModule: true, +}; + +export const SDK_VERSION = version; +export { HttpsError }; + +/** + * Returns a {@link Functions} instance for the default or given {@link FirebaseApp}. + */ +export function getFunctions(app?: FirebaseApp, regionOrCustomDomain?: string): Functions { + return getOrCreateModularInstance( + FirebaseFunctionsModule, + config, + app, + regionOrCustomDomain, + ) as unknown as Functions; +} + +/** + * Modify this instance to communicate with the Cloud Functions emulator. + */ +export function connectFunctionsEmulator( + functionsInstance: Functions, + host: string, + port: number, +): void { + (functionsInstance as FunctionsInternal).useEmulator(host, port); +} + +/** + * Returns a reference to the callable HTTPS trigger with the given name. + */ +export function httpsCallable( + functionsInstance: Functions, + name: string, + options?: HttpsCallableOptions, +): HttpsCallable { + return (functionsInstance as FunctionsInternal).httpsCallable(name, options) as HttpsCallable< + RequestData, + ResponseData, + StreamData + >; +} + +/** + * Returns a reference to the callable HTTPS trigger with the specified url. + */ +export function httpsCallableFromUrl< + RequestData = unknown, + ResponseData = unknown, + StreamData = unknown, +>( + functionsInstance: Functions, + url: string, + options?: HttpsCallableOptions, +): HttpsCallable { + return (functionsInstance as FunctionsInternal).httpsCallableFromUrl( + url, + options, + ) as HttpsCallable; +} + export type { HttpsCallableOptions, HttpsCallable, HttpsCallableResult, Functions, - FirebaseFunctionsTypes, + FunctionsErrorCode, } from './types/functions'; -// Export modular API functions -export * from './modular'; - -// Export namespaced API -export * from './namespaced'; -export { default } from './namespaced'; +setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/functions/lib/modular.ts b/packages/functions/lib/modular.ts deleted file mode 100644 index 6013f5cf45..0000000000 --- a/packages/functions/lib/modular.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp, type FirebaseApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import type { Functions, HttpsCallableOptions, HttpsCallable } from './types/functions'; - -/** - * Returns a Functions instance for the given app. - * @param app - The FirebaseApp to use. Optional. - * @param regionOrCustomDomain - One of: a) The region the callable functions are located in (ex: us-central1) b) A custom domain hosting the callable functions (ex: https://mydomain.com). Optional. - * @returns Functions instance for the given app. - */ -export function getFunctions(app?: FirebaseApp, regionOrCustomDomain?: string): Functions { - if (app) { - return (getApp(app.name) as unknown as FirebaseApp).functions(regionOrCustomDomain); - } - - return (getApp() as unknown as FirebaseApp).functions(regionOrCustomDomain); -} - -/** - * Modify this instance to communicate with the Cloud Functions emulator. - * Note: this must be called before this instance has been used to do any operations. - * @param functionsInstance A functions instance. - * @param host The emulator host. (ex: localhost) - * @param port The emulator port. (ex: 5001) - */ -export function connectFunctionsEmulator( - functionsInstance: Functions, - host: string, - port: number, -): void { - // @ts-ignore - return functionsInstance.useEmulator.call(functionsInstance, host, port, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a reference to the callable HTTPS trigger with the given name. - * @param functionsInstance A functions instance. - * @param name The name of the trigger. - * @param options An interface for metadata about how calls should be executed. - * @returns HttpsCallable instance - */ -export function httpsCallable( - functionsInstance: Functions, - name: string, - options?: HttpsCallableOptions, -): HttpsCallable { - return functionsInstance.httpsCallable.call( - functionsInstance, - name, - options, - // @ts-ignore - MODULAR_DEPRECATION_ARG, - ) as HttpsCallable; -} - -/** - * Returns a reference to the callable HTTPS trigger with the specified url. - * @param functionsInstance A functions instance. - * @param url The url of the trigger. - * @param options An instance of HttpsCallableOptions containing metadata about how calls should be executed. - * @returns HttpsCallable instance - */ -export function httpsCallableFromUrl< - RequestData = unknown, - ResponseData = unknown, - StreamData = unknown, ->( - functionsInstance: Functions, - url: string, - options?: HttpsCallableOptions, -): HttpsCallable { - return functionsInstance.httpsCallableFromUrl.call( - functionsInstance, - url, - options, - // @ts-ignore - MODULAR_DEPRECATION_ARG, - ) as HttpsCallable; -} diff --git a/packages/functions/lib/namespaced.ts b/packages/functions/lib/namespaced.ts deleted file mode 100644 index c51ef3ea94..0000000000 --- a/packages/functions/lib/namespaced.ts +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { isAndroid, isNumber, isOther } from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; -import { HttpsError, type NativeError } from './HttpsError'; -import { version } from './version'; -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import fallBackModule from './web/RNFBFunctionsModule'; -import type { - HttpsCallableOptions, - HttpsCallableStreamOptions, - Functions, - FunctionsStatics, - HttpsCallable, -} from './types/functions'; -import type { CustomHttpsCallableOptions, FunctionsStreamingEvent } from './types/internal'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -const namespace = 'functions'; - -const nativeModuleName = 'NativeRNFBTurboFunctions'; - -export const HttpsErrorCode = { - OK: 'ok', - CANCELLED: 'cancelled', - UNKNOWN: 'unknown', - INVALID_ARGUMENT: 'invalid-argument', - DEADLINE_EXCEEDED: 'deadline-exceeded', - NOT_FOUND: 'not-found', - ALREADY_EXISTS: 'already-exists', - PERMISSION_DENIED: 'permission-denied', - UNAUTHENTICATED: 'unauthenticated', - RESOURCE_EXHAUSTED: 'resource-exhausted', - FAILED_PRECONDITION: 'failed-precondition', - ABORTED: 'aborted', - OUT_OF_RANGE: 'out-of-range', - UNIMPLEMENTED: 'unimplemented', - INTERNAL: 'internal', - UNAVAILABLE: 'unavailable', - DATA_LOSS: 'data-loss', - UNSUPPORTED_TYPE: 'unsupported-type', - FAILED_TO_PARSE_WRAPPED_NUMBER: 'failed-to-parse-wrapped-number', - // Web codes are lowercase dasherized. - ok: 'ok', - cancelled: 'cancelled', - unknown: 'unknown', - 'invalid-argument': 'invalid-argument', - 'deadline-exceeded': 'deadline-exceeded', - 'not-found': 'not-found', - 'already-exists': 'already-exists', - 'permission-denied': 'permission-denied', - unauthenticated: 'unauthenticated', - 'resource-exhausted': 'resource-exhausted', - 'failed-precondition': 'failed-precondition', - aborted: 'aborted', - 'out-of-range': 'out-of-range', - unimplemented: 'unimplemented', - internal: 'internal', - unavailable: 'unavailable', - 'data-loss': 'data-loss', -} as const; - -const statics = { - HttpsErrorCode, -}; - -function normalizeHttpsCallableTimeoutOptions(options: HttpsCallableOptions): HttpsCallableOptions { - if (!options.timeout) { - return options; - } - if (!isNumber(options.timeout)) { - throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds'); - } - if (isOther) { - return options; - } - return { - ...options, - timeout: options.timeout / 1000, - }; -} - -let _id_functions_streaming_event = 0; - -class FirebaseFunctionsModule extends FirebaseModule { - _customUrlOrRegion: string; - private _useFunctionsEmulatorHost: string | null; - private _useFunctionsEmulatorPort: number; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - customUrlOrRegion?: string | null, - ) { - super(app, config, customUrlOrRegion); - this._customUrlOrRegion = customUrlOrRegion || 'us-central1'; - this._useFunctionsEmulatorHost = null; - this._useFunctionsEmulatorPort = -1; - - this.emitter.addListener( - this.eventNameForApp('functions_streaming_event'), - (event: FunctionsStreamingEvent) => { - this.emitter.emit( - this.eventNameForApp(`functions_streaming_event:${event.listenerId}`), - event, - ); - }, - ); - } - - /** - * Private helper method to create a streaming handler for callable functions. - * This method encapsulates the common streaming logic used by both - * httpsCallable and httpsCallableFromUrl. - */ - private async _createStreamHandler( - initiateStream: (listenerId: number) => void, - ): Promise<{ stream: AsyncGenerator; data: Promise }> { - const listenerId = _id_functions_streaming_event++; - const eventName = this.eventNameForApp(`functions_streaming_event:${listenerId}`); - const nativeModule = this.native; - - // Capture JavaScript stack at stream creation time so an error can be thrown with the correct stack trace - const capturedStack = new Error().stack; - - // Queue to buffer events before iteration starts - const eventQueue: unknown[] = []; - let resolveNext: ((value: IteratorResult) => void) | null = null; - let error: HttpsError | null = null; - let done = false; - let finalData: unknown = null; - let resolveDataPromise: ((value: unknown) => void) | null = null; - let rejectDataPromise: ((reason: Error) => void) | null = null; - - const subscription = this.emitter.addListener(eventName, (event: FunctionsStreamingEvent) => { - const body = event.body; - - if (body.error) { - const { code, message, details } = body.error || {}; - error = new HttpsError( - HttpsErrorCode[code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, - message || 'Unknown error', - details ?? null, - { jsStack: capturedStack }, - ); - done = true; - subscription.remove(); - if (nativeModule.removeFunctionsStreaming) { - nativeModule.removeFunctionsStreaming(listenerId); - } - if (resolveNext) { - resolveNext({ done: true, value: undefined }); - resolveNext = null; - } - if (rejectDataPromise) { - rejectDataPromise(error); - rejectDataPromise = null; - } - return; - } - - if (body.done) { - finalData = body.data; - done = true; - subscription.remove(); - if (nativeModule.removeFunctionsStreaming) { - nativeModule.removeFunctionsStreaming(listenerId); - } - if (resolveNext) { - resolveNext({ done: true, value: undefined }); - resolveNext = null; - } - if (resolveDataPromise) { - resolveDataPromise(finalData); - resolveDataPromise = null; - } - } else if (body.data !== null && body.data !== undefined) { - // This is a chunk - if (resolveNext) { - resolveNext({ done: false, value: body.data }); - resolveNext = null; - } else { - eventQueue.push(body.data); - } - } - }); - - // Start native streaming via the provided callback - initiateStream(listenerId); - - // Use async generator function for better compatibility with Hermes/React Native - async function* streamGenerator() { - try { - while (true) { - if (error) { - const err = error as HttpsError; - throw new HttpsError( - HttpsErrorCode[err.code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, - err.message, - err.details ?? null, - { - jsStack: capturedStack, - }, - ); - } - - if (eventQueue.length > 0) { - yield eventQueue.shift(); - continue; - } - - if (error) { - const err = error as HttpsError; - throw new HttpsError( - HttpsErrorCode[err.code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, - err.message, - err.details ?? null, - { - jsStack: capturedStack, - }, - ); - } - if (done) { - return; - } - - // Wait for next event - const result = await new Promise>(resolve => { - resolveNext = resolve; - }); - - // Check result after promise resolves - if (error) { - const err = error as HttpsError; - throw new HttpsError(err.code, err.message, err.details ?? null, { - jsStack: capturedStack, - }); - } - if (result.done || done) { - return; - } - - if (result.value !== undefined && result.value !== null) { - yield result.value; - } - } - } finally { - // Cleanup when generator is closed/returned - subscription.remove(); - if (nativeModule.removeFunctionsStreaming) { - nativeModule.removeFunctionsStreaming(listenerId); - } - } - } - - const asyncIterator = streamGenerator(); - - // Create a promise that resolves with the final data when the listener receives done/error - const dataPromise = new Promise((resolve, reject) => { - resolveDataPromise = resolve; - rejectDataPromise = reject; - if (error) { - rejectDataPromise = null; - reject(error); - } else if (done) { - resolveDataPromise = null; - resolve(finalData); - } - }); - - return { - stream: asyncIterator, - data: dataPromise, - }; - } - - httpsCallable(name: string, options: HttpsCallableOptions = {}) { - const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options); - - const callableFunction = ((data?: unknown) => { - const nativePromise = this.native.httpsCallable( - this._useFunctionsEmulatorHost, - this._useFunctionsEmulatorPort, - name, - { - data, - }, - normalizedOptions, - ); - return nativePromise.catch((nativeError: NativeError) => { - const { code, message, details } = nativeError.userInfo || {}; - return Promise.reject( - new HttpsError( - HttpsErrorCode[code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, - message || nativeError.message, - details || null, - nativeError, - ), - ); - }); - }) as HttpsCallable; - - callableFunction.stream = async ( - data?: unknown, - streamOptions?: HttpsCallableStreamOptions, - ) => { - const platformOptions = !isOther - ? normalizedOptions - : ({ - ...normalizedOptions, - httpsCallableStreamOptions: streamOptions || {}, - } as CustomHttpsCallableOptions); - return this._createStreamHandler(listenerId => { - this.native.httpsCallableStream( - this._useFunctionsEmulatorHost || null, - this._useFunctionsEmulatorPort || -1, - name, - { data }, - platformOptions || {}, - listenerId, - ); - }); - }; - - return callableFunction; - } - - httpsCallableFromUrl(url: string, options: HttpsCallableOptions = {}) { - const normalizedOptions = normalizeHttpsCallableTimeoutOptions(options); - - const callableFunction = ((data?: unknown) => { - const nativePromise = this.native.httpsCallableFromUrl( - this._useFunctionsEmulatorHost, - this._useFunctionsEmulatorPort, - url, - { - data, - }, - normalizedOptions, - ); - return nativePromise.catch((nativeError: NativeError) => { - const { code, message, details } = nativeError.userInfo || {}; - return Promise.reject( - new HttpsError( - HttpsErrorCode[code as keyof typeof HttpsErrorCode] || HttpsErrorCode.UNKNOWN, - message || nativeError.message, - details || null, - nativeError, - ), - ); - }); - }) as HttpsCallable; - - callableFunction.stream = async ( - data?: unknown, - streamOptions?: HttpsCallableStreamOptions, - ) => { - const platformOptions = !isOther - ? normalizedOptions - : ({ - ...normalizedOptions, - httpsCallableStreamOptions: streamOptions || {}, - } as CustomHttpsCallableOptions); - return this._createStreamHandler(listenerId => { - this.native.httpsCallableStreamFromUrl( - this._useFunctionsEmulatorHost || null, - this._useFunctionsEmulatorPort || -1, - url, - { data }, - platformOptions || {}, - listenerId, - ); - }); - }; - return callableFunction; - } - - useFunctionsEmulator(origin: string): void { - const match = /https?\:.*\/\/([^:]+):?(\d+)?/.exec(origin); - if (!match) { - throw new Error('Invalid emulator origin format'); - } - const [, host, portStr] = match; - const port = portStr ? parseInt(portStr) : 5001; - this.useEmulator(host as string, port); - } - - useEmulator(host: string, port: number): void { - if (!isNumber(port)) { - throw new Error('useEmulator port parameter must be a number'); - } - - let _host = host; - - const androidBypassEmulatorUrlRemap = - typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && - this.firebaseJson.android_bypass_emulator_url_remap; - if (!androidBypassEmulatorUrlRemap && isAndroid && _host) { - if (_host.startsWith('localhost')) { - _host = _host.replace('localhost', '10.0.2.2'); - // eslint-disable-next-line no-console - console.log( - 'Mapping functions host "localhost" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', - ); - } - if (_host.startsWith('127.0.0.1')) { - _host = _host.replace('127.0.0.1', '10.0.2.2'); - // eslint-disable-next-line no-console - console.log( - 'Mapping functions host "127.0.0.1" to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', - ); - } - } - this._useFunctionsEmulatorHost = _host || null; - this._useFunctionsEmulatorPort = port || -1; - } -} - -// import { SDK_VERSION } from '@react-native-firebase/functions'; -export const SDK_VERSION = version; - -const functionsNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: ['functions_streaming_event'], - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: true, - ModuleClass: FirebaseFunctionsModule, - turboModule: true, -}); - -type FunctionsNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - Functions, - FunctionsStatics -> & { - functions: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import functions from '@react-native-firebase/functions'; -// functions().httpsCallable(...); -export default functionsNamespace as unknown as FunctionsNamespace; - -// import functions, { firebase } from '@react-native-firebase/functions'; -// functions().httpsCallable(...); -// firebase.functions().httpsCallable(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'functions', - Functions, - FunctionsStatics, - true - >; - -// Register the interop module for non-native platforms. -setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/functions/lib/types/functions.ts b/packages/functions/lib/types/functions.ts index 6d93fda95d..7a279452c1 100644 --- a/packages/functions/lib/types/functions.ts +++ b/packages/functions/lib/types/functions.ts @@ -188,84 +188,5 @@ export interface FunctionsStatics { /** * FirebaseApp type with functions() method. * @deprecated Import FirebaseApp from '@react-native-firebase/app' instead. - * The functions() method is added via module augmentation. */ export type FirebaseApp = ReactNativeFirebase.FirebaseApp; - -// ============ Module Augmentation ============ - -/* eslint-disable @typescript-eslint/no-namespace */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - interface Module { - functions: FirebaseModuleWithStaticsAndApp; - } - interface FirebaseApp { - functions(regionOrCustomDomain?: string): Functions; - } - } -} -/* eslint-enable @typescript-eslint/no-namespace */ - -// ============ Backwards Compatibility Namespace - to be removed with namespaced exports ============ - -// Helper types to reference outer scope types within the namespace -// These are needed because TypeScript can't directly alias types with the same name -type _HttpsCallableResult = HttpsCallableResult; -type _HttpsCallableStreamResult = HttpsCallableStreamResult< - ResponseData, - StreamData ->; -type _HttpsCallable = HttpsCallable< - RequestData, - ResponseData, - StreamData ->; -type _HttpsCallableOptions = HttpsCallableOptions; -type _HttpsCallableStreamOptions = HttpsCallableStreamOptions; -type _HttpsError = HttpsError; -type _HttpsErrorCode = HttpsErrorCode; - -/** - * @deprecated Use the exported types directly instead. - * FirebaseFunctionsTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseFunctionsTypes { - // Short name aliases referencing top-level types - export type ErrorCode = FunctionsErrorCode; - export type CallableResult = HttpsCallableResult; - export type CallableStreamResult< - ResponseData = unknown, - StreamData = unknown, - > = HttpsCallableStreamResult; - export type Callable< - RequestData = unknown, - ResponseData = unknown, - StreamData = unknown, - > = HttpsCallable; - export type CallableOptions = HttpsCallableOptions; - export type CallableStreamOptions = HttpsCallableStreamOptions; - export type Error = HttpsError; - export type ErrorCodeMap = HttpsErrorCode; - export type Statics = FunctionsStatics; - export type Module = Functions; - - // Https* aliases that reference the exported types above via helper types - // These provide backwards compatibility for code using FirebaseFunctionsTypes.HttpsCallableResult - export type HttpsCallableResult = _HttpsCallableResult; - export type HttpsCallableStreamResult< - ResponseData = unknown, - StreamData = unknown, - > = _HttpsCallableStreamResult; - export type HttpsCallable< - RequestData = unknown, - ResponseData = unknown, - StreamData = unknown, - > = _HttpsCallable; - export type HttpsCallableOptions = _HttpsCallableOptions; - export type HttpsCallableStreamOptions = _HttpsCallableStreamOptions; - export type HttpsError = _HttpsError; - export type HttpsErrorCode = _HttpsErrorCode; -} -/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/functions/lib/types/internal.ts b/packages/functions/lib/types/internal.ts index c4e066e769..23f0655b0a 100644 --- a/packages/functions/lib/types/internal.ts +++ b/packages/functions/lib/types/internal.ts @@ -15,8 +15,20 @@ * */ -import type { Functions } from 'firebase/functions'; -import type { HttpsCallableOptions, HttpsCallableStreamOptions } from './functions'; +import type { Functions as FirebaseFunctionsSdk } from 'firebase/functions'; +import type { Functions, HttpsCallableOptions, HttpsCallableStreamOptions } from './functions'; + +export interface FunctionsInternal extends Functions { + httpsCallable( + name: string, + options?: HttpsCallableOptions, + ): import('./functions').HttpsCallable; + httpsCallableFromUrl( + url: string, + options?: HttpsCallableOptions, + ): import('./functions').HttpsCallable; + useEmulator(host: string, port: number): void; +} export interface FunctionsStreamingEventBody { data?: unknown; @@ -36,12 +48,13 @@ export interface FunctionsStreamingEvent { /** * Internal type for web Functions instance with additional internal properties. - * Extends the Firebase Functions type to include properties that may be set - * internally but are not part of the public API. + * Intersects firebase-js-sdk Functions (not the RNFB modular instance) with RNFB web hacks. */ -export interface FunctionsWebInternal extends Functions { +export type FunctionsWebInternal = FirebaseFunctionsSdk & { + region?: string; + customDomain?: string | null; emulatorOrigin?: string; -} +}; /** * Internal type for custom https callable options. diff --git a/packages/functions/lib/web/RNFBFunctionsModule.ts b/packages/functions/lib/web/RNFBFunctionsModule.ts index c7d0f205af..eef1b267ba 100644 --- a/packages/functions/lib/web/RNFBFunctionsModule.ts +++ b/packages/functions/lib/web/RNFBFunctionsModule.ts @@ -132,7 +132,7 @@ function getConfiguredFunctionsInstance( let functionsInstance: FunctionsWebInternal; if (regionOrCustomDomain) { - functionsInstance = getFunctions(app, regionOrCustomDomain); + functionsInstance = getFunctions(app, regionOrCustomDomain) as FunctionsWebInternal; // Hack to work around custom domain and region not being set on the instance. if (regionOrCustomDomain.startsWith('http')) { functionsInstance.customDomain = regionOrCustomDomain; @@ -142,7 +142,7 @@ function getConfiguredFunctionsInstance( functionsInstance.customDomain = null; } } else { - functionsInstance = getFunctions(app); + functionsInstance = getFunctions(app) as FunctionsWebInternal; functionsInstance.region = 'us-central1'; functionsInstance.customDomain = null; } diff --git a/packages/functions/type-test.ts b/packages/functions/type-test.ts index aeb60caadc..218fc83980 100644 --- a/packages/functions/type-test.ts +++ b/packages/functions/type-test.ts @@ -1,145 +1,44 @@ -import functions, { - firebase, - FirebaseFunctionsTypes, - // Types - type HttpsCallableOptions, - type HttpsCallable, - type HttpsCallableResult, - type Functions, - // Modular API +import { getApp } from '@react-native-firebase/app'; +import { getFunctions, connectFunctionsEmulator, httpsCallable, httpsCallableFromUrl, + HttpsErrorCode, + SDK_VERSION, + type HttpsCallableOptions, + type HttpsCallable, + type HttpsCallableResult, + type Functions, } from '.'; -console.log(functions().app); - -// checks module exists at root -console.log(firebase.functions().app.name); - -// checks module exists at app level -console.log(firebase.app().functions().app.name); - -// app level module accepts string arg -console.log(firebase.app().functions('some-string').app.name); -console.log(firebase.app().functions('some-string').httpsCallable('foo')); - -// checks statics exist -console.log(firebase.functions.SDK_VERSION); - -// checks statics exist on defaultExport -console.log(functions.firebase.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); +const functionsInstance: Functions = getFunctions(); +console.log(functionsInstance.app.name); -// checks multi-app support exists -console.log(firebase.functions(firebase.app()).app.name); +const functionsWithApp = getFunctions(getApp()); +console.log(functionsWithApp.app.name); -// checks default export supports app arg -console.log(firebase.functions(firebase.app('foo')).app.name); +const functionsWithRegion = getFunctions(getApp(), 'us-central1'); +console.log(functionsWithRegion.app.name); -console.log(firebase.functions.HttpsErrorCode.ABORTED); +connectFunctionsEmulator(functionsInstance, 'localhost', 5001); -// test type usage -const functionsInstance: Functions = firebase.functions(); -console.log(functionsInstance.app.name); const callableOptions: HttpsCallableOptions = { timeout: 5000 }; console.log(callableOptions.timeout); -const callable: HttpsCallable = firebase.functions().httpsCallable('test'); + +const callable: HttpsCallable = httpsCallable(functionsInstance, 'test'); callable('test').then((result: HttpsCallableResult) => { - console.log('callable result:', result.data); + console.log(result.data); }); -const callableResult: HttpsCallableResult = { data: 123 }; -console.log(callableResult.data); - -// test method calls with proper types -firebase - .functions() - .httpsCallable('foo')(123) - .then((result: FirebaseFunctionsTypes.HttpsCallableResult) => { - console.log(result.data); - }) - .catch((error: { code: any; details: any }) => { - console.log(error.code); - console.log(error.details); - }); - -firebase.functions().useFunctionsEmulator('http://localhost:5001'); - -// checks all methods exist on firebase.functions() -console.log(firebase.functions().httpsCallable); -console.log(firebase.functions().httpsCallableFromUrl); -console.log(firebase.functions().useFunctionsEmulator); -console.log(firebase.functions().useEmulator); - -// checks all methods exist on default export -console.log(functions().httpsCallable); -console.log(functions().httpsCallableFromUrl); -console.log(functions().useFunctionsEmulator); -console.log(functions().useEmulator); -// test method calls with missing methods -firebase - .functions() - .httpsCallableFromUrl('https://example.com/callable')('data') - .then((result: HttpsCallableResult) => { - console.log('httpsCallableFromUrl result:', result.data); - }) - .catch((error: { code: any; details: any }) => { - console.log(error.code); - console.log(error.details); - }); - -firebase.functions().useEmulator('localhost', 5001); - -// test modular API functions -const functionsModular = getFunctions(); -const functionsModularWithApp = getFunctions(firebase.app()); -const functionsModularWithRegion = getFunctions(firebase.app(), 'us-central1'); -console.log(functionsModularWithApp.app.name); -console.log(functionsModularWithRegion.app.name); - -connectFunctionsEmulator(functionsModular, 'localhost', 5001); - -const modularCallable = httpsCallable(functionsModular, 'test', { timeout: 5000 }); -modularCallable('data') - .then((result: HttpsCallableResult) => { - console.log('modular httpsCallable result:', result.data); - }) - .catch((error: { code: any; details: any }) => { - console.log(error.code); - console.log(error.details); - }); - -const modularCallableFromUrl = httpsCallableFromUrl( - functionsModular, +const callableFromUrl = httpsCallableFromUrl( + functionsInstance, 'https://example.com/callable', { timeout: 5000 }, ); -modularCallableFromUrl('data') - .then((result: HttpsCallableResult) => { - console.log('modular httpsCallableFromUrl result:', result.data); - }) - .catch((error: { code: any; details: any }) => { - console.log(error.code); - console.log(error.details); - }); - -// test FirebaseFunctionsTypes namespace -const namespaceResult: FirebaseFunctionsTypes.HttpsCallableResult = { data: 'test' }; -console.log(namespaceResult.data); -const namespaceCallable: FirebaseFunctionsTypes.HttpsCallable = firebase - .functions() - .httpsCallable('test'); -namespaceCallable('test').then((result: FirebaseFunctionsTypes.HttpsCallableResult) => { - console.log('namespaceCallable result:', result.data); +callableFromUrl('data').then((result: HttpsCallableResult) => { + console.log(result.data); }); -const namespaceOptions: FirebaseFunctionsTypes.HttpsCallableOptions = { timeout: 5000 }; -console.log(namespaceOptions.timeout); -const namespaceErrorCode: FirebaseFunctionsTypes.ErrorCode = 'ok'; -console.log(namespaceErrorCode); -const namespaceErrorCodeMap: FirebaseFunctionsTypes.ErrorCodeMap = - firebase.functions.HttpsErrorCode; -console.log(namespaceErrorCodeMap.ABORTED); + +console.log(HttpsErrorCode.ABORTED); +console.log(SDK_VERSION); diff --git a/packages/functions/typedoc.json b/packages/functions/typedoc.json index 954869a0ab..37ef274677 100644 --- a/packages/functions/typedoc.json +++ b/packages/functions/typedoc.json @@ -1,14 +1,6 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/functions.ts"], + "entryPoints": ["lib/index.ts", "lib/types/functions.ts"], "tsconfig": "tsconfig.json", - "intentionallyNotExported": [ - "_HttpsCallable", - "_HttpsCallableOptions", - "_HttpsCallableResult", - "_HttpsCallableStreamOptions", - "_HttpsCallableStreamResult", - "_HttpsError", - "_HttpsErrorCode" - ] + "intentionallyNotExported": ["NativeError"] } diff --git a/packages/in-app-messaging/__tests__/inappmessaging.test.ts b/packages/in-app-messaging/__tests__/inappmessaging.test.ts index 75c3beb1fc..3a29cfab39 100644 --- a/packages/in-app-messaging/__tests__/inappmessaging.test.ts +++ b/packages/in-app-messaging/__tests__/inappmessaging.test.ts @@ -1,7 +1,6 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import { - firebase, getInAppMessaging, isMessagesDisplaySuppressed, setMessagesDisplaySuppressed, @@ -10,33 +9,10 @@ import { triggerEvent, } from '../lib'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; - // @ts-ignore test import FirebaseModule from '../../app/lib/internal/FirebaseModule'; describe('in-app-messaging', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.inAppMessaging).toBeDefined(); - expect(app.inAppMessaging().app).toEqual(app); - }); - }); - describe('modular', function () { beforeEach(async function () { // @ts-ignore test @@ -113,76 +89,4 @@ describe('in-app-messaging', function () { expect(isAutomaticDataCollectionEnabled(inAppMessaging)).toBe(true); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let inAppMessagingV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - inAppMessagingV9Deprecation = createCheckV9Deprecation(['inAppMessaging']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: (_target, prop) => { - if ( - prop === 'isMessagesDisplaySuppressed' || - prop === 'isAutomaticDataCollectionEnabled' - ) { - return false; - } - - return jest.fn().mockResolvedValue(null as never); - }, - }, - ); - }); - }); - - it('isMessagesDisplaySuppressed', function () { - const inAppMessaging = getInAppMessaging(); - inAppMessagingV9Deprecation( - () => isMessagesDisplaySuppressed(inAppMessaging), - () => inAppMessaging.isMessagesDisplaySuppressed, - 'isMessagesDisplaySuppressed', - ); - }); - - it('setMessagesDisplaySuppressed', function () { - const inAppMessaging = getInAppMessaging(); - inAppMessagingV9Deprecation( - () => setMessagesDisplaySuppressed(inAppMessaging, true), - () => inAppMessaging.setMessagesDisplaySuppressed(true), - 'setMessagesDisplaySuppressed', - ); - }); - - it('isAutomaticDataCollectionEnabled', function () { - const inAppMessaging = getInAppMessaging(); - inAppMessagingV9Deprecation( - () => isAutomaticDataCollectionEnabled(inAppMessaging), - () => inAppMessaging.isAutomaticDataCollectionEnabled, - 'isAutomaticDataCollectionEnabled', - ); - }); - - it('setAutomaticDataCollectionEnabled', function () { - const inAppMessaging = getInAppMessaging(); - inAppMessagingV9Deprecation( - () => setAutomaticDataCollectionEnabled(inAppMessaging, false), - () => inAppMessaging.setAutomaticDataCollectionEnabled(false), - 'setAutomaticDataCollectionEnabled', - ); - }); - - it('triggerEvent', function () { - const inAppMessaging = getInAppMessaging(); - inAppMessagingV9Deprecation( - () => triggerEvent(inAppMessaging, 'test-event'), - () => inAppMessaging.triggerEvent('test-event'), - 'triggerEvent', - ); - }); - }); }); diff --git a/packages/in-app-messaging/e2e/fiam.e2e.js b/packages/in-app-messaging/e2e/fiam.e2e.js index b58cc1c8fb..14c125c63a 100644 --- a/packages/in-app-messaging/e2e/fiam.e2e.js +++ b/packages/in-app-messaging/e2e/fiam.e2e.js @@ -16,38 +16,35 @@ */ describe('inAppMessaging()', function () { - // TODO Conflicts with Modular tests in Jet, - // Ignore for now since v8 compat going away eventually - xdescribe('v8 compatibility', function () { - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.inAppMessaging); - app.inAppMessaging().app.should.equal(app); - }); + describe('modular', function () { + it('getInAppMessaging() returns instance bound to default app', function () { + const { getInAppMessaging } = inAppMessagingModular; + const inAppMessaging = getInAppMessaging(); + + inAppMessaging.app.name.should.equal('[DEFAULT]'); }); - describe('setAutomaticDataCollectionEnabled()', function () { - // These depend on `tests/firebase.json` having `in_app_messaging_auto_collection_enabled` set to false the first time - // The setting is persisted across restarts, reset to false after for local runs where prefs are sticky + describe('setMessagesDisplaySuppressed()', function () { afterEach(async function () { - await firebase.inAppMessaging().setAutomaticDataCollectionEnabled(false); + const { getInAppMessaging, setMessagesDisplaySuppressed } = inAppMessagingModular; + await setMessagesDisplaySuppressed(getInAppMessaging(), false); }); - it('true', async function () { - should.equal(firebase.inAppMessaging().isAutomaticDataCollectionEnabled, false); - await firebase.inAppMessaging().setAutomaticDataCollectionEnabled(true); - should.equal(firebase.inAppMessaging().isAutomaticDataCollectionEnabled, true); - }); + it('updates isMessagesDisplaySuppressed', async function () { + const { getInAppMessaging, isMessagesDisplaySuppressed, setMessagesDisplaySuppressed } = + inAppMessagingModular; + const inAppMessaging = getInAppMessaging(); - it('false', async function () { - await firebase.inAppMessaging().setAutomaticDataCollectionEnabled(false); - should.equal(firebase.inAppMessaging().isAutomaticDataCollectionEnabled, false); + should.equal(isMessagesDisplaySuppressed(inAppMessaging), false); + await setMessagesDisplaySuppressed(inAppMessaging, true); + should.equal(isMessagesDisplaySuppressed(inAppMessaging), true); }); it('errors if not boolean', async function () { + const { getInAppMessaging, setMessagesDisplaySuppressed } = inAppMessagingModular; + try { - firebase.inAppMessaging().setAutomaticDataCollectionEnabled(); + await setMessagesDisplaySuppressed(getInAppMessaging()); return Promise.reject(new Error('Did not throw')); } catch (e) { e.message.should.containEql('must be a boolean'); @@ -55,21 +52,30 @@ describe('inAppMessaging()', function () { } }); }); - }); - describe('modular', function () { + describe('triggerEvent()', function () { + it('errors if not string', async function () { + const { getInAppMessaging, triggerEvent } = inAppMessagingModular; + + try { + await triggerEvent(getInAppMessaging(), 123); + return Promise.reject(new Error('Did not throw')); + } catch (e) { + e.message.should.containEql('must be a string'); + return Promise.resolve(); + } + }); + }); + // TODO flakey on Jet tests xdescribe('setAutomaticDataCollectionEnabled()', function () { // These depend on `tests/firebase.json` having `in_app_messaging_auto_collection_enabled` set to false the first time // The setting is persisted across restarts, reset to false after for local runs where prefs are sticky afterEach(async function () { - await firebase.inAppMessaging().setAutomaticDataCollectionEnabled(false); + const { getInAppMessaging, setAutomaticDataCollectionEnabled } = inAppMessagingModular; + const inAppMessaging = getInAppMessaging(); + await setAutomaticDataCollectionEnabled(inAppMessaging, false); }); - // afterEach(async function () { - // const { getInAppMessaging, setAutomaticDataCollectionEnabled } = inAppMessagingModular; - // const inAppMessaging = getInAppMessaging(); - // await setAutomaticDataCollectionEnabled(inAppMessaging, false); - // }); it('true', async function () { const { diff --git a/packages/in-app-messaging/lib/index.ts b/packages/in-app-messaging/lib/index.ts index 135ffe1173..067c6a8989 100644 --- a/packages/in-app-messaging/lib/index.ts +++ b/packages/in-app-messaging/lib/index.ts @@ -15,11 +15,146 @@ * */ -export type { InAppMessaging } from './types/in-app-messaging'; +import { isBoolean, isString } from '@react-native-firebase/app/dist/module/common'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import './types/internal'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import type { InAppMessaging } from './types/in-app-messaging'; +import fallBackModule from './web/RNFBFiamModule'; +import { version } from './version'; + +const nativeModuleName = 'RNFBFiamModule'; + +class FirebaseFiamModule extends FirebaseModule { + private _isMessagesDisplaySuppressed: boolean; + private _isAutomaticDataCollectionEnabled: boolean; + + constructor(...args: ConstructorParameters) { + super(...args); + this._isMessagesDisplaySuppressed = this.native.isMessagesDisplaySuppressed; + this._isAutomaticDataCollectionEnabled = this.native.isAutomaticDataCollectionEnabled; + } + + get isMessagesDisplaySuppressed(): boolean { + return this._isMessagesDisplaySuppressed; + } + + get isAutomaticDataCollectionEnabled(): boolean { + return this._isAutomaticDataCollectionEnabled; + } + + setMessagesDisplaySuppressed(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "getInAppMessaging().setMessagesDisplaySuppressed(*) 'enabled' must be a boolean.", + ); + } + + this._isMessagesDisplaySuppressed = enabled; + return this.native.setMessagesDisplaySuppressed(enabled); + } -export type { FirebaseInAppMessagingTypes } from './types/namespaced'; + setAutomaticDataCollectionEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "getInAppMessaging().setAutomaticDataCollectionEnabled(*) 'enabled' must be a boolean.", + ); + } -export * from './modular'; + this._isAutomaticDataCollectionEnabled = enabled; + return this.native.setAutomaticDataCollectionEnabled(enabled); + } + + triggerEvent(eventId: string): Promise { + if (!isString(eventId)) { + throw new Error("getInAppMessaging().triggerEvent(*) 'eventId' must be a string."); + } + return this.native.triggerEvent(eventId); + } +} + +const config: ModuleConfig = { + namespace: 'inAppMessaging', + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, +}; + +/** + * RN Firebase package version string exported from the modular entry point. + * + * The firebase-js-sdk does not ship a modular In-App Messaging entry point or `SDK_VERSION` export. + */ +export const SDK_VERSION = version; + +/** + * Returns the {@link InAppMessaging} instance for the default or given {@link FirebaseApp}. + * + * @param app - The Firebase `FirebaseApp` to use. When omitted, the default app is used. + * @returns The In-App Messaging service instance for that app. + */ +export function getInAppMessaging(app?: FirebaseApp): InAppMessaging { + return getOrCreateModularInstance(FirebaseFiamModule, config, app) as unknown as InAppMessaging; +} -export * from './namespaced'; -export { default } from './namespaced'; +/** + * Determines whether messages are suppressed or not. + */ +export function isMessagesDisplaySuppressed(inAppMessaging: InAppMessaging): boolean { + return inAppMessaging.isMessagesDisplaySuppressed; +} + +/** + * Enable or disable suppression of Firebase In App Messaging messages. + * + * When enabled, no in app messages will be rendered until either you disable suppression, or the app + * restarts. This state is not persisted between app restarts. + */ +export function setMessagesDisplaySuppressed( + inAppMessaging: InAppMessaging, + enabled: boolean, +): Promise { + return inAppMessaging.setMessagesDisplaySuppressed(enabled); +} + +/** + * Determines whether automatic data collection is enabled or not. + */ +export function isAutomaticDataCollectionEnabled(inAppMessaging: InAppMessaging): boolean { + return inAppMessaging.isAutomaticDataCollectionEnabled; +} + +/** + * Enable or disable automatic data collection for Firebase In-App Messaging. + * + * When enabled, generates a registration token on app startup if there is no valid one and + * generates a new token when it is deleted (which prevents `deleteInstanceId()` from stopping the + * periodic sending of data). + * + * This setting is persisted across app restarts and overrides the setting specified in your + * manifest/plist file. + */ +export function setAutomaticDataCollectionEnabled( + inAppMessaging: InAppMessaging, + enabled: boolean, +): Promise { + return inAppMessaging.setAutomaticDataCollectionEnabled(enabled); +} + +/** + * Trigger in-app messages programmatically. + */ +export function triggerEvent(inAppMessaging: InAppMessaging, eventId: string): Promise { + return inAppMessaging.triggerEvent(eventId); +} + +// Register the interop module for non-native platforms. +setReactNativeModule(nativeModuleName, fallBackModule as unknown as Record); + +export type { InAppMessaging } from './types/in-app-messaging'; diff --git a/packages/in-app-messaging/lib/modular.ts b/packages/in-app-messaging/lib/modular.ts deleted file mode 100644 index 2e2e63fefd..0000000000 --- a/packages/in-app-messaging/lib/modular.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import { - MODULAR_DEPRECATION_ARG, - withModularFlag, -} from '@react-native-firebase/app/dist/module/common'; -import type { InAppMessaging } from './types/in-app-messaging'; -import type { InAppMessagingWithDeprecationArg } from './types/internal'; - -function withModularDeprecationArg( - inAppMessaging: InAppMessaging, -): InAppMessagingWithDeprecationArg { - return inAppMessaging as InAppMessagingWithDeprecationArg; -} - -/** - * Returns the {@link InAppMessaging} instance for the default app. - */ -export function getInAppMessaging(): InAppMessaging { - return getApp().inAppMessaging(); -} - -/** - * Determines whether messages are suppressed or not. - */ -export function isMessagesDisplaySuppressed(inAppMessaging: InAppMessaging): boolean { - return withModularFlag(() => inAppMessaging.isMessagesDisplaySuppressed); -} - -/** - * Enable or disable suppression of Firebase In App Messaging messages. - * - * When enabled, no in app messages will be rendered until either you disable suppression, or the app - * restarts. This state is not persisted between app restarts. - */ -export function setMessagesDisplaySuppressed( - inAppMessaging: InAppMessaging, - enabled: boolean, -): Promise { - const internalInAppMessaging = withModularDeprecationArg(inAppMessaging); - return internalInAppMessaging.setMessagesDisplaySuppressed.call( - internalInAppMessaging, - enabled, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Determines whether automatic data collection is enabled or not. - */ -export function isAutomaticDataCollectionEnabled(inAppMessaging: InAppMessaging): boolean { - return withModularFlag(() => inAppMessaging.isAutomaticDataCollectionEnabled); -} - -/** - * Enable or disable automatic data collection for Firebase In-App Messaging. - * - * When enabled, generates a registration token on app startup if there is no valid one and - * generates a new token when it is deleted (which prevents `deleteInstanceId()` from stopping the - * periodic sending of data). - * - * This setting is persisted across app restarts and overrides the setting specified in your - * manifest/plist file. - */ -export function setAutomaticDataCollectionEnabled( - inAppMessaging: InAppMessaging, - enabled: boolean, -): Promise { - const internalInAppMessaging = withModularDeprecationArg(inAppMessaging); - return internalInAppMessaging.setAutomaticDataCollectionEnabled.call( - internalInAppMessaging, - enabled, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Trigger in-app messages programmatically. - */ -export function triggerEvent(inAppMessaging: InAppMessaging, eventId: string): Promise { - const internalInAppMessaging = withModularDeprecationArg(inAppMessaging); - return internalInAppMessaging.triggerEvent.call( - internalInAppMessaging, - eventId, - MODULAR_DEPRECATION_ARG, - ); -} diff --git a/packages/in-app-messaging/lib/namespaced.ts b/packages/in-app-messaging/lib/namespaced.ts deleted file mode 100644 index cce20c658c..0000000000 --- a/packages/in-app-messaging/lib/namespaced.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { isBoolean, isString } from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -import './types/internal'; -import type { FirebaseInAppMessagingTypes } from './types/namespaced'; -import { version } from './version'; - -const statics = {}; - -const namespace = 'inAppMessaging'; - -const nativeModuleName = 'RNFBFiamModule'; - -class FirebaseFiamModule extends FirebaseModule { - private _isMessagesDisplaySuppressed: boolean; - private _isAutomaticDataCollectionEnabled: boolean; - - constructor(...args: ConstructorParameters) { - super(...args); - this._isMessagesDisplaySuppressed = this.native.isMessagesDisplaySuppressed; - this._isAutomaticDataCollectionEnabled = this.native.isAutomaticDataCollectionEnabled; - } - - get isMessagesDisplaySuppressed(): boolean { - return this._isMessagesDisplaySuppressed; - } - - get isAutomaticDataCollectionEnabled(): boolean { - return this._isAutomaticDataCollectionEnabled; - } - - setMessagesDisplaySuppressed(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.inAppMessaging().setMessagesDisplaySuppressed(*) 'enabled' must be a boolean.", - ); - } - - this._isMessagesDisplaySuppressed = enabled; - return this.native.setMessagesDisplaySuppressed(enabled); - } - - setAutomaticDataCollectionEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.inAppMessaging().setAutomaticDataCollectionEnabled(*) 'enabled' must be a boolean.", - ); - } - - this._isAutomaticDataCollectionEnabled = enabled; - return this.native.setAutomaticDataCollectionEnabled(enabled); - } - - triggerEvent(eventId: string): Promise { - if (!isString(eventId)) { - throw new Error("firebase.inAppMessaging().triggerEvent(*) 'eventId' must be a string."); - } - return this.native.triggerEvent(eventId); - } -} - -type InAppMessagingNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseInAppMessagingTypes.Module, - FirebaseInAppMessagingTypes.Statics -> & { - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import { SDK_VERSION } from '@react-native-firebase/in-app-messaging'; -export const SDK_VERSION = version; - -const defaultExport = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: false, - hasMultiAppSupport: false, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseFiamModule, -}) as unknown as InAppMessagingNamespace; - -export default defaultExport; - -// import inAppMessaging, { firebase } from '@react-native-firebase/in-app-messaging'; -// inAppMessaging().X(...); -// firebase.inAppMessaging().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'inAppMessaging', - FirebaseInAppMessagingTypes.Module, - FirebaseInAppMessagingTypes.Statics, - false - >; diff --git a/packages/in-app-messaging/lib/types/internal.ts b/packages/in-app-messaging/lib/types/internal.ts index 95a2b27bbd..48e44ffbdb 100644 --- a/packages/in-app-messaging/lib/types/internal.ts +++ b/packages/in-app-messaging/lib/types/internal.ts @@ -17,9 +17,6 @@ import type { InAppMessaging } from './in-app-messaging'; -/** Optional final argument passed by modular API wrappers (MODULAR_DEPRECATION_ARG). */ -export type InAppMessagingModularDeprecationArg = string; - /** * Wrapped native module contract for `RNFBFiamModule`. */ @@ -40,15 +37,3 @@ declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { export interface InAppMessagingInternal extends InAppMessaging { readonly native: RNFBFiamModule; } - -export type InAppMessagingWithDeprecationArg = InAppMessagingInternal & { - setMessagesDisplaySuppressed( - enabled: boolean, - _depArg: InAppMessagingModularDeprecationArg, - ): Promise; - setAutomaticDataCollectionEnabled( - enabled: boolean, - _depArg: InAppMessagingModularDeprecationArg, - ): Promise; - triggerEvent(eventId: string, _depArg: InAppMessagingModularDeprecationArg): Promise; -}; diff --git a/packages/in-app-messaging/lib/types/namespaced.ts b/packages/in-app-messaging/lib/types/namespaced.ts deleted file mode 100644 index e7713fcb90..0000000000 --- a/packages/in-app-messaging/lib/types/namespaced.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase In-App Messaging package for React Native. - * - * #### Example 1 - * - * Access the firebase export from the `inAppMessaging` package: - * - * ```js - * import { firebase } from '@react-native-firebase/in-app-messaging'; - * - * // firebase.inAppMessaging().X - * ``` - * - * #### Example 2 - * - * Using the default export from the `in-app-messaging` package: - * - * ```js - * import inAppMessaging from '@react-native-firebase/in-app-messaging'; - * - * // inAppMessaging().X - * ``` - * - * #### Example 3 - * - * Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/in-app-messaging'; - * - * // firebase.inAppMessaging().X - * ``` - * - * @firebase in-app-messaging - */ -/** - * @deprecated Use the exported types directly instead. - * FirebaseInAppMessagingTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseInAppMessagingTypes { - /** - * @deprecated Use the exported types directly instead. FirebaseInAppMessagingTypes namespace is kept for backwards compatibility. - */ - type FirebaseModule = ReactNativeFirebase.FirebaseModule; - - /** - * @deprecated Use the default export statics instead. - */ - export interface Statics { - /** @deprecated Use the default export statics instead. */ - SDK_VERSION: string; - } - - /** - * @deprecated Use the exported `InAppMessaging` type instead. - */ - export interface Module extends FirebaseModule { - /** - * @deprecated Use the exported `InAppMessaging` type instead. - * - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * @deprecated Use the exported `InAppMessaging` type instead. - * - * Determines whether messages are suppressed or not. - */ - isMessagesDisplaySuppressed: boolean; - - /** - * @deprecated Use the exported `InAppMessaging` type instead. - * - * Determines whether automatic data collection is enabled or not. - */ - isAutomaticDataCollectionEnabled: boolean; - - /** - * @deprecated Use the exported `InAppMessaging` type instead. - * - * Enable or disable suppression of Firebase In App Messaging messages. - */ - setMessagesDisplaySuppressed(enabled: boolean): Promise; - - /** - * @deprecated Use the exported `InAppMessaging` type instead. - * - * Enable or disable automatic data collection for Firebase In-App Messaging. - */ - setAutomaticDataCollectionEnabled(enabled: boolean): Promise; - - /** - * @deprecated Use the exported `InAppMessaging` type instead. - * - * Trigger in-app messages programmatically. - */ - triggerEvent(eventId: string): Promise; - } -} - -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - interface Module { - inAppMessaging: FirebaseModuleWithStaticsAndApp< - FirebaseInAppMessagingTypes.Module, - FirebaseInAppMessagingTypes.Statics - >; - } - - interface FirebaseApp { - inAppMessaging(): FirebaseInAppMessagingTypes.Module; - } - } -} -/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/app/lib/namespaced.ts b/packages/in-app-messaging/lib/web/RNFBFiamModule.ts similarity index 59% rename from packages/app/lib/namespaced.ts rename to packages/in-app-messaging/lib/web/RNFBFiamModule.ts index 6840013a3c..4e6f7cb5cd 100644 --- a/packages/app/lib/namespaced.ts +++ b/packages/in-app-messaging/lib/web/RNFBFiamModule.ts @@ -15,10 +15,20 @@ * */ -import { getFirebaseRoot } from './internal/registry/namespace'; -import utils from './utils'; +import type { RNFBFiamModule } from '../types/internal'; -export const firebase = getFirebaseRoot(); -export { utils }; +const RNFBFiamModuleWeb: RNFBFiamModule = { + isMessagesDisplaySuppressed: false, + isAutomaticDataCollectionEnabled: false, + setMessagesDisplaySuppressed() { + return Promise.resolve(null); + }, + setAutomaticDataCollectionEnabled() { + return Promise.resolve(null); + }, + triggerEvent() { + return Promise.resolve(null); + }, +}; -export default firebase; +export default RNFBFiamModuleWeb; diff --git a/packages/in-app-messaging/tsconfig.json b/packages/in-app-messaging/tsconfig.json index db72589106..6125162fe4 100644 --- a/packages/in-app-messaging/tsconfig.json +++ b/packages/in-app-messaging/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "rootDir": ".", "paths": { + "@react-native-firebase/app/dist/module/common/*": ["../app/dist/typescript/lib/common/*"], "@react-native-firebase/app/dist/module/common": ["../app/dist/typescript/lib/common"], + "@react-native-firebase/app/dist/module/internal/*": [ + "../app/dist/typescript/lib/internal/*" + ], "@react-native-firebase/app/dist/module/internal": ["../app/dist/typescript/lib/internal"], "@react-native-firebase/app/dist/module/internal/NativeModules": [ "../app/dist/typescript/lib/internal/NativeModules" diff --git a/packages/in-app-messaging/type-test.ts b/packages/in-app-messaging/type-test.ts index 0b67d7950c..0866d69e0d 100644 --- a/packages/in-app-messaging/type-test.ts +++ b/packages/in-app-messaging/type-test.ts @@ -1,5 +1,5 @@ -import inAppMessaging, { - firebase, +import { getApp } from '@react-native-firebase/app'; +import { getInAppMessaging, isMessagesDisplaySuppressed, setMessagesDisplaySuppressed, @@ -8,79 +8,36 @@ import inAppMessaging, { triggerEvent, SDK_VERSION, type InAppMessaging, - type FirebaseInAppMessagingTypes, } from '.'; -console.log(inAppMessaging().app); +// modular API functions +const modularInAppMessaging1 = getInAppMessaging(); +console.log(modularInAppMessaging1.app.name); -// checks module exists at root -console.log(firebase.inAppMessaging().app.name); -console.log(firebase.inAppMessaging().isMessagesDisplaySuppressed); -console.log(firebase.inAppMessaging().isAutomaticDataCollectionEnabled); +const modularInAppMessaging2 = getInAppMessaging(getApp()); +console.log(modularInAppMessaging2.app.name); -// checks module exists at app level -console.log(firebase.app().inAppMessaging().app.name); -console.log(firebase.app().inAppMessaging().isMessagesDisplaySuppressed); -console.log(firebase.app().inAppMessaging().isAutomaticDataCollectionEnabled); +// modular public types +const modularInstance: InAppMessaging = getInAppMessaging(); +const modularWithNamedApp: InAppMessaging = getInAppMessaging(getApp()); +console.log(modularInstance.app.name); +console.log(modularWithNamedApp.app.name); -// checks statics exist -console.log(firebase.inAppMessaging.SDK_VERSION); +console.log(isMessagesDisplaySuppressed(modularInstance)); +console.log(isAutomaticDataCollectionEnabled(modularInstance)); -// checks statics exist on defaultExport -console.log(inAppMessaging.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks multi-app support exists (note: in-app-messaging doesn't support multi-app, but test the pattern) -console.log(firebase.inAppMessaging().app.name); - -// checks default export supports app arg -console.log(inAppMessaging().app.name); - -// checks Module instance APIs -const inAppMessagingInstance: InAppMessaging = firebase.inAppMessaging(); -console.log(inAppMessagingInstance.isMessagesDisplaySuppressed); -console.log(inAppMessagingInstance.isAutomaticDataCollectionEnabled); - -inAppMessagingInstance.setMessagesDisplaySuppressed(true).then(() => { - console.log('Messages display suppressed'); -}); - -inAppMessagingInstance.setAutomaticDataCollectionEnabled(false).then(() => { - console.log('Automatic data collection disabled'); -}); - -inAppMessagingInstance.triggerEvent('test-event').then(() => { - console.log('Event triggered'); -}); - -// checks modular API functions -const modularInAppMessaging: InAppMessaging = getInAppMessaging(); -console.log(modularInAppMessaging.app.name); - -console.log(isMessagesDisplaySuppressed(modularInAppMessaging)); - -setMessagesDisplaySuppressed(modularInAppMessaging, true).then(() => { +setMessagesDisplaySuppressed(modularInstance, true).then(() => { console.log('Modular messages display suppressed'); }); -console.log(isAutomaticDataCollectionEnabled(modularInAppMessaging)); - -setAutomaticDataCollectionEnabled(modularInAppMessaging, false).then(() => { +setAutomaticDataCollectionEnabled(modularInstance, false).then(() => { console.log('Modular automatic data collection disabled'); }); -triggerEvent(modularInAppMessaging, 'modular-test-event').then(() => { +triggerEvent(modularInstance, 'modular-test-event').then(() => { console.log('Modular event triggered'); }); // named SDK_VERSION export const sdkVersion: string = SDK_VERSION; console.log(sdkVersion); - -// deprecated namespace types remain assignable from runtime values -const namespaceModule: FirebaseInAppMessagingTypes.Module = firebase.inAppMessaging(); -const namespaceStatics: FirebaseInAppMessagingTypes.Statics = firebase.inAppMessaging; -console.log(namespaceModule.app.name); -console.log(namespaceStatics.SDK_VERSION); diff --git a/packages/in-app-messaging/typedoc.json b/packages/in-app-messaging/typedoc.json index e33bed4103..42dfa292ba 100644 --- a/packages/in-app-messaging/typedoc.json +++ b/packages/in-app-messaging/typedoc.json @@ -1,4 +1,4 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/in-app-messaging.ts"] + "entryPoints": ["lib/index.ts", "lib/types/in-app-messaging.ts"] } diff --git a/packages/installations/__tests__/installations.test.ts b/packages/installations/__tests__/installations.test.ts index 1a0f08203c..114905da8a 100644 --- a/packages/installations/__tests__/installations.test.ts +++ b/packages/installations/__tests__/installations.test.ts @@ -1,52 +1,27 @@ -import { afterAll, beforeAll, describe, expect, it, beforeEach, jest } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; -import { - firebase, - getInstallations, - deleteInstallations, - getId, - getToken, - onIdChange, -} from '../lib'; - -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; +import { getInstallations, deleteInstallations, getId, getToken, onIdChange } from '../lib'; // @ts-ignore test import FirebaseModule from '../../app/lib/internal/FirebaseModule'; describe('installations()', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.installations).toBeDefined(); - expect(app.installations().app).toEqual(app); - }); - - it('supports multiple apps', async function () { - expect(firebase.installations().app.name).toEqual('[DEFAULT]'); - expect(firebase.installations(firebase.app('secondaryFromNative')).app.name).toEqual( - 'secondaryFromNative', - ); - expect(firebase.app('secondaryFromNative').installations().app.name).toEqual( - 'secondaryFromNative', - ); + describe('modular', function () { + beforeEach(function () { + // @ts-ignore test + jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { + return new Proxy( + {}, + { + get: () => + jest.fn().mockResolvedValue({ + constants: {}, + } as never), + }, + ); + }); }); - }); - describe('modular', function () { it('`getInstallations` function is properly exposed to end user', function () { expect(getInstallations).toBeDefined(); }); @@ -71,7 +46,7 @@ describe('installations()', function () { it('returns an instance of Installations', async function () { const installations = getInstallations(); expect(installations).toBeDefined(); - // expect(installations.app).toBeDefined(); + expect(installations.app).toBeDefined(); }); }); @@ -84,55 +59,4 @@ describe('installations()', function () { }); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let installationsV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - installationsV9Deprecation = createCheckV9Deprecation(['installations']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - constants: {}, - } as never), - }, - ); - }); - }); - - it('delete', function () { - const installations = getInstallations(); - installationsV9Deprecation( - () => deleteInstallations(installations), - // @ts-expect-error Combines modular and namespace API - () => installations.delete(), - 'delete', - ); - }); - - it('getId', function () { - const installations = getInstallations(); - installationsV9Deprecation( - () => getId(installations), - // @ts-expect-error Combines modular and namespace API - () => installations.getId(), - 'getId', - ); - }); - - it('getToken', function () { - const installations = getInstallations(); - installationsV9Deprecation( - () => getToken(installations), - // @ts-expect-error Combines modular and namespace API - () => installations.getToken(), - 'getToken', - ); - }); - }); }); diff --git a/packages/installations/e2e/installations.e2e.js b/packages/installations/e2e/installations.e2e.js index 017c0ef016..3d633577b3 100644 --- a/packages/installations/e2e/installations.e2e.js +++ b/packages/installations/e2e/installations.e2e.js @@ -61,85 +61,17 @@ const ID_LENGTH = 22; const PROJECT_ID = 448618578101; // this is "magic", it's the react-native-firebase-testing project ID describe('installations() modular', function () { - // Namespaced API is being removed; modular coverage is sufficient and halves FIS load. - xdescribe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('getId()', function () { - it('returns a valid installation id', async function () { - const id = await firebase.installations().getId(); - id.should.be.a.String(); - id.length.should.be.equals(ID_LENGTH); - }); - }); - - describe('getToken()', function () { - it('returns a valid auth token with no arguments', async function () { - const id = await firebase.installations().getId(); - const token = await firebase.installations().getToken(); - token.should.be.a.String(); - token.should.not.equal(''); - const decodedToken = decodeJWT(token); - decodedToken.fid.should.equal(id); // fid == firebase installations id - decodedToken.projectNumber.should.equal(PROJECT_ID); - - // token time is "Unix epoch time", which is in seconds vs javascript milliseconds - if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) { - return Promise.reject( - new Error('Token already expired: ' + JSON.stringify(decodedToken)), - ); - } - - const token2 = await firebase.installations().getToken(true); - token2.should.be.a.String(); - token2.should.not.equal(''); - const decodedToken2 = decodeJWT(token2); - decodedToken2.fid.should.equal(id); - decodedToken2.projectNumber.should.equal(PROJECT_ID); - - // token time is "Unix epoch time", which is in seconds vs javascript milliseconds - if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) { - return Promise.reject(new Error('Token already expired')); - } - (token === token2).should.be.false(); - }); - }); - - describe('delete()', function () { - it('successfully deletes', async function () { - const id = await firebase.installations().getId(); - id.should.be.a.String(); - id.length.should.be.equals(ID_LENGTH); - await firebase.installations().delete(); - - // New id should be different - const id2 = await firebase.installations().getId(); - id2.should.be.a.String(); - id2.length.should.be.equals(ID_LENGTH); - (id === id2).should.be.false(); - - const token = await firebase.installations().getToken(false); - const decodedToken = decodeJWT(token); - decodedToken.fid.should.equal(id2); // fid == firebase installations id - - // token time is "Unix epoch time", which is in seconds vs javascript milliseconds - decodedToken.projectNumber.should.equal(PROJECT_ID); - if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) { - return Promise.reject(new Error('Token already expired')); - } - }); + describe('modular', function () { + it('supports multiple apps', function () { + const { getApp } = modular; + const { getInstallations } = installationsModular; + const installations = getInstallations(); + const secondaryInstallations = getInstallations(getApp('secondaryFromNative')); + + installations.app.name.should.equal('[DEFAULT]'); + secondaryInstallations.app.name.should.equal('secondaryFromNative'); }); - }); - describe('modular', function () { describe('getId()', function () { it('returns a valid installation id', async function () { const { getInstallations, getId } = installationsModular; diff --git a/packages/installations/lib/index.ts b/packages/installations/lib/index.ts index bc592b178f..f71489451c 100644 --- a/packages/installations/lib/index.ts +++ b/packages/installations/lib/index.ts @@ -15,13 +15,115 @@ * */ -// Export modular/public types. -export type * from './types/installations'; +import { isIOS } from '@react-native-firebase/app/dist/module/common'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import './types/internal'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import type { + IdChangeCallbackFn, + IdChangeUnsubscribeFn, + Installations, +} from './types/installations'; +import type { InstallationsInternal } from './types/internal'; +import { version } from './version'; -// Export modular API functions. -export * from './modular'; +const nativeModuleName = 'RNFBInstallationsModule'; -// Export namespaced API. -export type { FirebaseInstallationsTypes } from './types/namespaced'; -export * from './namespaced'; -export { default } from './namespaced'; +class FirebaseInstallationsModule extends FirebaseModule { + getId(): Promise { + return this.native.getId(); + } + + getToken(forceRefresh?: boolean): Promise { + if (!forceRefresh) { + return this.native.getToken(false); + } else { + return this.native.getToken(true); + } + } + + delete(): Promise { + return this.native.delete(); + } + + onIdChange(): () => void { + if (isIOS) { + return () => {}; + } + + // TODO implement change listener on Android + return () => {}; + } +} + +const config: ModuleConfig = { + namespace: 'installations', + nativeModuleName, + nativeEvents: false, // TODO implement android id change listener: ['installations_id_changed'], + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: false, +}; + +/** + * RN Firebase package version string exported from the modular entry point. + * + * The firebase-js-sdk does not export `SDK_VERSION` from `@firebase/installations`. + */ +export const SDK_VERSION = version; + +/** + * Returns the {@link Installations} instance for the default or given {@link FirebaseApp}. + * + * @param app - The Firebase `FirebaseApp` to use. When omitted, the default app is used. + * @returns The Installations service instance for that app. + */ +export function getInstallations(app?: FirebaseApp): Installations { + return getOrCreateModularInstance( + FirebaseInstallationsModule, + config, + app, + ) as unknown as Installations; +} + +/** + * Deletes the Firebase Installation and all associated data. + */ +export function deleteInstallations(installations: Installations): Promise { + return (installations as InstallationsInternal).delete(); +} + +/** + * Creates a Firebase Installation if there isn't one for the app and returns the Installation ID. + */ +export function getId(installations: Installations): Promise { + return (installations as InstallationsInternal).getId(); +} + +/** + * Returns a Firebase Installations auth token, identifying the current Firebase Installation. + */ +export function getToken(installations: Installations, forceRefresh?: boolean): Promise { + return (installations as InstallationsInternal).getToken(forceRefresh); +} + +/** + * Throw an error since react-native-firebase does not support this method. + * + * Sets a new callback that will get called when Installation ID changes. Returns an unsubscribe function that will remove the callback when called. + */ +export function onIdChange( + _installations: Installations, + _callback: IdChangeCallbackFn, +): IdChangeUnsubscribeFn { + throw new Error('onIdChange() is unsupported by the React Native Firebase SDK.'); +} + +export type { + IdChangeCallbackFn, + IdChangeUnsubscribeFn, + Installations, +} from './types/installations'; diff --git a/packages/installations/lib/modular.ts b/packages/installations/lib/modular.ts deleted file mode 100644 index 7b6247a592..0000000000 --- a/packages/installations/lib/modular.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import type { FirebaseApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import type { - IdChangeCallbackFn, - IdChangeUnsubscribeFn, - Installations, -} from './types/installations'; -import type { InstallationsInternal } from './types/internal'; - -function withModularDeprecationArg(installations: Installations): InstallationsInternal { - return installations as InstallationsInternal; -} - -/** - * Returns an instance of Installations associated with the given FirebaseApp instance. - */ -export function getInstallations(app?: FirebaseApp): Installations { - if (app) { - return getApp(app.name).installations(); - } - return getApp().installations(); -} - -/** - * Deletes the Firebase Installation and all associated data. - */ -export function deleteInstallations(installations: Installations): Promise { - const internalInstallations = withModularDeprecationArg(installations); - return internalInstallations.delete.call(internalInstallations, MODULAR_DEPRECATION_ARG); -} - -/** - * Creates a Firebase Installation if there isn't one for the app and returns the Installation ID. - */ -export function getId(installations: Installations): Promise { - const internalInstallations = withModularDeprecationArg(installations); - return internalInstallations.getId.call(internalInstallations, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a Firebase Installations auth token, identifying the current Firebase Installation. - */ -export function getToken(installations: Installations, forceRefresh?: boolean): Promise { - const internalInstallations = withModularDeprecationArg(installations); - return internalInstallations.getToken.call( - internalInstallations, - forceRefresh, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Throw an error since react-native-firebase does not support this method. - * - * Sets a new callback that will get called when Installation ID changes. Returns an unsubscribe function that will remove the callback when called. - */ -export function onIdChange( - _installations: Installations, - _callback: IdChangeCallbackFn, -): IdChangeUnsubscribeFn { - throw new Error('onIdChange() is unsupported by the React Native Firebase SDK.'); -} diff --git a/packages/installations/lib/namespaced.ts b/packages/installations/lib/namespaced.ts deleted file mode 100644 index 893e24ee78..0000000000 --- a/packages/installations/lib/namespaced.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { isIOS } from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -import { version } from './version'; -import type { FirebaseInstallationsTypes } from './types/namespaced'; - -const statics = {}; - -const namespace = 'installations'; -const nativeModuleName = 'RNFBInstallationsModule'; - -class FirebaseInstallationsModule extends FirebaseModule { - getId(): Promise { - return this.native.getId(); - } - - getToken(forceRefresh?: boolean): Promise { - if (!forceRefresh) { - return this.native.getToken(false); - } else { - return this.native.getToken(true); - } - } - - delete(): Promise { - return this.native.delete(); - } - - onIdChange(): () => void { - if (isIOS) { - return () => {}; - } - - // TODO implement change listener on Android - return () => {}; - } -} - -// import { SDK_VERSION } from '@react-native-firebase/installations'; -export const SDK_VERSION = version; - -// import installations from '@react-native-firebase/installations'; -// installations().X(...); -const installationsNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: false, // TODO implement android id change listener: ['installations_id_changed'], - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseInstallationsModule, -}); - -type InstallationsNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseInstallationsTypes.Module, - FirebaseInstallationsTypes.Statics -> & { - installations: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseInstallationsTypes.Module, - FirebaseInstallationsTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -export default installationsNamespace as unknown as InstallationsNamespace; - -// import installations, { firebase } from '@react-native-firebase/installations'; -// installations().X(...); -// firebase.installations().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'installations', - FirebaseInstallationsTypes.Module, - FirebaseInstallationsTypes.Statics, - false - >; diff --git a/packages/installations/lib/types/internal.ts b/packages/installations/lib/types/internal.ts index 2b0b4b36b6..54979c5681 100644 --- a/packages/installations/lib/types/internal.ts +++ b/packages/installations/lib/types/internal.ts @@ -2,7 +2,7 @@ * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -17,18 +17,6 @@ import type { Installations } from './installations'; -/** Optional final argument passed by modular API wrappers (MODULAR_DEPRECATION_ARG). */ -export type InstallationsModularDeprecationArg = string; - -export interface InstallationsInternal extends Installations { - delete(deprecationArg?: InstallationsModularDeprecationArg): Promise; - getId(deprecationArg?: InstallationsModularDeprecationArg): Promise; - getToken( - forceRefresh?: boolean, - deprecationArg?: InstallationsModularDeprecationArg, - ): Promise; -} - export interface RNFBInstallationsModule { getId(): Promise; getToken(forceRefresh: boolean): Promise; @@ -40,3 +28,10 @@ declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { RNFBInstallationsModule: RNFBInstallationsModule; } } + +export interface InstallationsInternal extends Installations { + getId(): Promise; + getToken(forceRefresh?: boolean): Promise; + delete(): Promise; + readonly native: RNFBInstallationsModule; +} diff --git a/packages/installations/lib/types/namespaced.ts b/packages/installations/lib/types/namespaced.ts deleted file mode 100644 index 83cc3a0820..0000000000 --- a/packages/installations/lib/types/namespaced.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase Installations package for React Native. - * - * @firebase installations - */ -/** - * @deprecated Use the exported types directly instead. - * FirebaseInstallationsTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseInstallationsTypes { - /** - * @deprecated Use the exported types directly instead. - */ - export interface Statics { - SDK_VERSION: string; - } - - /** - * The Firebase Installations service is available for the default app or a given app. - * - * @deprecated Use the exported types directly instead. FirebaseInstallationsTypes namespace is kept for backwards compatibility. - */ - export interface Module extends ReactNativeFirebase.FirebaseModule { - /** - * The current `FirebaseApp` instance for this Firebase service. - * - * @deprecated Use the exported types directly instead. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * Creates a Firebase Installation if there isn't one for the app and returns the Installation ID. - * - * @deprecated Use `getId()` from the modular API instead. - */ - getId(): Promise; - - /** - * Retrieves a valid installation auth token. - * - * @deprecated Use `getToken()` from the modular API instead. - */ - getToken(forceRefresh?: boolean): Promise; - - /** - * Deletes the Firebase Installation and all associated data from the Firebase backend. - * - * @deprecated Use `deleteInstallations()` from the modular API instead. - */ - delete(): Promise; - } - - export type InstallationsNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - Module, - Statics - > & { - installations: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; - }; -} - -declare const defaultExport: FirebaseInstallationsTypes.InstallationsNamespace; - -export declare const firebase: ReactNativeFirebase.Module & { - installations: typeof defaultExport; - app( - name?: string, - ): ReactNativeFirebase.FirebaseApp & { installations(): FirebaseInstallationsTypes.Module }; -}; - -export default defaultExport; - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - - interface Module { - installations: FirebaseModuleWithStaticsAndApp< - FirebaseInstallationsTypes.Module, - FirebaseInstallationsTypes.Statics - >; - } - - interface FirebaseApp { - installations(): FirebaseInstallationsTypes.Module; - } - } -} diff --git a/packages/installations/type-test.ts b/packages/installations/type-test.ts index 943d4fa128..2b2db7e753 100644 --- a/packages/installations/type-test.ts +++ b/packages/installations/type-test.ts @@ -1,75 +1,28 @@ -import installations, { - firebase, +import { getApp } from '@react-native-firebase/app'; +import { getInstallations, deleteInstallations, getId, getToken, onIdChange, + SDK_VERSION, + type IdChangeCallbackFn, + type IdChangeUnsubscribeFn, + type Installations, } from '.'; -import type { - FirebaseInstallationsTypes, - IdChangeCallbackFn, - IdChangeUnsubscribeFn, - Installations, -} from '.'; - -console.log(installations().app); - -// checks module exists at root -console.log(firebase.installations().app.name); - -// checks module exists at app level -console.log(firebase.app().installations().app.name); - -// checks statics exist -console.log(firebase.installations.SDK_VERSION); - -// checks statics exist on defaultExport -console.log(installations.firebase.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks multi-app support exists -console.log(firebase.installations(firebase.app()).app.name); -console.log(firebase.installations(firebase.app('foo')).app.name); - -// checks default export supports app arg -console.log(installations(firebase.app()).app.name); -console.log(installations(firebase.app('foo')).app.name); - -// checks Module instance APIs -const installationsInstance = firebase.installations(); -console.log(installationsInstance.app.name); - -installationsInstance.getId().then((id: string) => { - console.log(id); -}); - -installationsInstance.getToken().then((token: string) => { - console.log(token); -}); - -installationsInstance.getToken(true).then((token: string) => { - console.log(token); -}); -installationsInstance.delete().then(() => { - console.log('Installation deleted'); -}); - -// checks modular API functions const modularInstallations1 = getInstallations(); console.log(modularInstallations1.app.name); -const typedInstallations: Installations = modularInstallations1; -console.log(typedInstallations.app.name); -const modularInstallations2 = getInstallations(firebase.app()); +const modularInstallations2 = getInstallations(getApp()); console.log(modularInstallations2.app.name); -const modularInstallations3 = getInstallations(firebase.app('foo')); +const modularInstallations3 = getInstallations(getApp('foo')); console.log(modularInstallations3.app.name); +const typedInstallations: Installations = modularInstallations1; +console.log(typedInstallations.app.name); + getId(modularInstallations1).then((id: string) => { console.log(id); }); @@ -86,12 +39,6 @@ deleteInstallations(modularInstallations1).then(() => { console.log('Modular installation deleted'); }); -const namespacedInstallations: FirebaseInstallationsTypes.Module = installationsInstance; -namespacedInstallations.getId().then((id: string) => { - console.log(id); -}); - -// Note: onIdChange throws an error in React Native Firebase try { const onInstallationsIdChange: IdChangeCallbackFn = id => { console.log(id); @@ -104,3 +51,6 @@ try { } catch (error) { console.log('Modular onIdChange not supported'); } + +const sdkVersion: string = SDK_VERSION; +console.log(sdkVersion); diff --git a/packages/installations/typedoc.json b/packages/installations/typedoc.json index e83cc5c235..e80c7ccd5e 100644 --- a/packages/installations/typedoc.json +++ b/packages/installations/typedoc.json @@ -1,5 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/installations.ts"], + "entryPoints": ["lib/index.ts", "lib/types/installations.ts"], "tsconfig": "tsconfig.json" } diff --git a/packages/messaging/__tests__/messaging.test.ts b/packages/messaging/__tests__/messaging.test.ts index 8d2b757f3c..e7c7ac26a2 100644 --- a/packages/messaging/__tests__/messaging.test.ts +++ b/packages/messaging/__tests__/messaging.test.ts @@ -1,24 +1,6 @@ import { beforeEach, describe, expect, it, jest } from '@jest/globals'; -// Mock getFirebaseRoot before any imports that use it -jest.mock('@react-native-firebase/app/dist/module/internal', () => { - const actual = jest.requireActual('@react-native-firebase/app/dist/module/internal'); - return Object.assign({}, actual, { - getFirebaseRoot: jest.fn(() => ({ - utils: jest.fn(() => ({ - playServicesAvailability: { - isAvailable: true, - status: 0, - hasResolution: false, - isUserResolvableError: false, - error: undefined, - }, - })), - })), - }); -}); - -import messaging, { +import { getMessaging, deleteToken, getToken, @@ -53,16 +35,26 @@ import messaging, { NotificationAndroidVisibility, } from '../lib'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; - // @ts-ignore test import FirebaseModule from '../../app/lib/internal/FirebaseModule'; describe('Messaging', function () { describe('modular', function () { + beforeEach(function () { + // @ts-ignore test + jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { + return new Proxy( + {}, + { + get: () => + jest.fn().mockResolvedValue({ + result: true, + } as never), + }, + ); + }); + }); + it('`getMessaging` function is properly exposed to end user', function () { expect(getMessaging).toBeDefined(); }); @@ -201,217 +193,4 @@ describe('Messaging', function () { expect(NotificationAndroidVisibility.VISIBILITY_SECRET).toBeDefined(); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let messagingV9Deprecation: CheckV9DeprecationFunction; - let staticsV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - messagingV9Deprecation = createCheckV9Deprecation(['messaging']); - staticsV9Deprecation = createCheckV9Deprecation(['messaging', 'statics']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - result: true, - } as never), - }, - ); - }); - }); - - describe('Messaging', function () { - it('isAutoInitEnabled', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => isAutoInitEnabled(messaging), - () => messaging.isAutoInitEnabled, - 'isAutoInitEnabled', - ); - }); - - it('isDeviceRegisteredForRemoteMessages', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => isDeviceRegisteredForRemoteMessages(messaging), - () => messaging.isDeviceRegisteredForRemoteMessages, - 'isDeviceRegisteredForRemoteMessages', - ); - }); - - it('setAutoInitEnabled', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => setAutoInitEnabled(messaging, true), - () => messaging.setAutoInitEnabled(true), - 'setAutoInitEnabled', - ); - }); - - it('getInitialNotification', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => getInitialNotification(messaging), - () => messaging.getInitialNotification(), - 'getInitialNotification', - ); - }); - - it('getDidOpenSettingsForNotification', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => getDidOpenSettingsForNotification(messaging), - () => messaging.getDidOpenSettingsForNotification(), - 'getDidOpenSettingsForNotification', - ); - }); - - it('getIsHeadless', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => getIsHeadless(messaging), - () => messaging.getIsHeadless(), - 'getIsHeadless', - ); - }); - - it('requestPermission', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => requestPermission(messaging), - () => messaging.requestPermission(), - 'requestPermission', - ); - }); - - it('registerDeviceForRemoteMessages', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => registerDeviceForRemoteMessages(messaging), - () => messaging.registerDeviceForRemoteMessages(), - 'registerDeviceForRemoteMessages', - ); - }); - - it('unregisterDeviceForRemoteMessages', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => unregisterDeviceForRemoteMessages(messaging), - () => messaging.unregisterDeviceForRemoteMessages(), - 'unregisterDeviceForRemoteMessages', - ); - }); - - it('getAPNSToken', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => getAPNSToken(messaging), - () => messaging.getAPNSToken(), - 'getAPNSToken', - ); - }); - - it('setAPNSToken', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => setAPNSToken(messaging, 'token'), - () => messaging.setAPNSToken('token'), - 'setAPNSToken', - ); - }); - - it('hasPermission', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => hasPermission(messaging), - () => messaging.hasPermission(), - 'hasPermission', - ); - }); - - it('subscribeToTopic', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => subscribeToTopic(messaging, 'topic'), - () => messaging.subscribeToTopic('topic'), - 'subscribeToTopic', - ); - }); - - it('unsubscribeFromTopic', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => unsubscribeFromTopic(messaging, 'topic'), - () => messaging.unsubscribeFromTopic('topic'), - 'unsubscribeFromTopic', - ); - }); - - it('getToken', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => getToken(messaging), - () => messaging.getToken(), - 'getToken', - ); - }); - - it('deleteToken', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => deleteToken(messaging), - () => messaging.deleteToken(), - 'deleteToken', - ); - }); - - it('isSupported', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => isSupported(messaging), - () => messaging.isSupported(), - 'isSupported', - ); - }); - - it('experimentalSetDeliveryMetricsExportedToBigQueryEnabled', function () { - const messaging = getMessaging(); - messagingV9Deprecation( - () => experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, true), - () => messaging.setDeliveryMetricsExportToBigQuery(true), - 'setDeliveryMetricsExportToBigQuery', - ); - }); - - describe('statics', function () { - it('AuthorizationStatus', function () { - staticsV9Deprecation( - () => AuthorizationStatus, - () => messaging.AuthorizationStatus, - 'AuthorizationStatus', - ); - }); - - it('NotificationAndroidPriority', function () { - staticsV9Deprecation( - () => NotificationAndroidPriority, - () => messaging.NotificationAndroidPriority, - 'NotificationAndroidPriority', - ); - }); - - it('NotificationAndroidVisibility', function () { - staticsV9Deprecation( - () => NotificationAndroidVisibility, - () => messaging.NotificationAndroidVisibility, - 'NotificationAndroidVisibility', - ); - }); - }); - }); - }); }); diff --git a/packages/messaging/e2e/messaging.e2e.js b/packages/messaging/e2e/messaging.e2e.js index d5dfb1bb39..c79bda95b7 100644 --- a/packages/messaging/e2e/messaging.e2e.js +++ b/packages/messaging/e2e/messaging.e2e.js @@ -49,444 +49,6 @@ describe('messaging()', function () { }); }); - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.messaging); - app.messaging().app.should.equal(app); - }); - }); - - describe('setAutoInitEnabled()', function () { - // These depend on `tests/firebase.json` having `messaging_auto_init_enabled` set to false the first time - // The setting is persisted across restarts, reset to false after for local runs where prefs are sticky - afterEach(async function () { - await firebase.messaging().setAutoInitEnabled(false); - }); - - it('throws if enabled is not a boolean', function () { - try { - firebase.messaging().setAutoInitEnabled(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'enabled' expected a boolean value"); - return Promise.resolve(); - } - }); - - it('sets the value', async function () { - should.equal(firebase.messaging().isAutoInitEnabled, false); - await firebase.messaging().setAutoInitEnabled(true); - should.equal(firebase.messaging().isAutoInitEnabled, true); - - // Set it back to the default value for future runs in re-use mode - await firebase.messaging().setAutoInitEnabled(false); - should.equal(firebase.messaging().isAutoInitEnabled, false); - }); - }); - - describe('isDeviceRegisteredForRemoteMessages', function () { - it('returns true on android', function () { - if (Platform.android) { - should.equal(firebase.messaging().isDeviceRegisteredForRemoteMessages, true); - } else { - this.skip(); - } - }); - }); - - describe('unregisterDeviceForRemoteMessages', function () { - it('resolves on android, remains registered', async function () { - if (Platform.android) { - await firebase.messaging().unregisterDeviceForRemoteMessages(); - should.equal(firebase.messaging().isDeviceRegisteredForRemoteMessages, true); - } else { - this.skip(); - } - }); - - it('successfully unregisters on ios', async function () { - if (Platform.ios && !isCI) { - await firebase.messaging().unregisterDeviceForRemoteMessages(); - should.equal(firebase.messaging().isDeviceRegisteredForRemoteMessages, false); - tryToRegister = await isAPNSCapableSimulator(); - if (tryToRegister) { - await firebase.messaging().registerDeviceForRemoteMessages(); - should.equal(firebase.messaging().isDeviceRegisteredForRemoteMessages, true); - } - } else { - this.skip(); - } - }); - }); - - describe('hasPermission', function () { - // we request permission in a before block, so both should be truthy - it('returns truthy', async function () { - should.equal(!!(await firebase.messaging().hasPermission()), true); - }); - }); - - describe('requestPermission', function () { - // we request permission in a before block - it('resolves correctly for default request', async function () { - if (Platform.android) { - // our default resolve on android is "authorized" - should.equal(await firebase.messaging().requestPermission({ provisional: true }), 1); - } else { - // our default request on iOS results in "provisional" == 2 - // but we may have granted perms by any outside method == 1 - should.equal( - (await firebase.messaging().requestPermission({ provisional: true })) >= 1, - true, - ); - } - }); - }); - - describe('getAPNSToken', function () { - it('resolves null on android', async function () { - if (Platform.android) { - should.equal(await firebase.messaging().getAPNSToken(), null); - } else { - this.skip(); - } - }); - - it('resolves on ios with token on supported simulators', async function () { - // Make sure we are registered for remote notifications, else no token - aPNSCapableSimulator = await isAPNSCapableSimulator(); - simulator = await isSimulator(); - if (Platform.ios && !isCI && (!simulator || (simulator && aPNSCapableSimulator))) { - await firebase.messaging().registerDeviceForRemoteMessages(); - apnsToken = await firebase.messaging().getAPNSToken(); - - if (!simulator || (simulator && aPNSCapableSimulator)) { - apnsToken.should.be.a.String(); - } else { - // unsupported iOS Simulator returns null (typically, - // can attempt-but-fail if M1 Simulator but not macOS13+/ios16+, rare combo) - if (apnsToken !== null) { - apnsToken.should.be.a.String(); - } - } - } else { - this.skip(); - } - }); - }); - - describe('setAPNSToken', function () { - it('requires a token parameter', async function () { - try { - firebase.messaging().setAPNSToken(); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'token' expected a string value"); - } - try { - firebase.messaging().setAPNSToken(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'token' expected a string value"); - return Promise.resolve(); - } - }); - - it('verifies type parameter is valid if specified', async function () { - try { - firebase.messaging().setAPNSToken('typeparamtest', 123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'type' expected one of 'prod', 'sandbox', or 'unknown'"); - } - try { - firebase.messaging().setAPNSToken('typeparamtest', 'bogus'); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'type' expected one of 'prod', 'sandbox', or 'unknown'"); - } - }); - - it('resolves on android', async function () { - if (Platform.android) { - should.equal(await firebase.messaging().setAPNSToken('foo'), null); - } else { - this.skip(); - } - }); - - it('correctly sets new token on ios', async function () { - aPNSCapableSimulator = await isAPNSCapableSimulator(); - simulator = await isSimulator(); - if (Platform.ios && (!simulator || (simulator && aPNSCapableSimulator))) { - originalAPNSToken = await firebase.messaging().getAPNSToken(); - // 74657374696E67746F6B656E is hex for "testingtoken" - await firebase.messaging().setAPNSToken('74657374696E67746F6B656E', 'unknown'); - newAPNSToken = await firebase.messaging().getAPNSToken(); - newAPNSToken.should.eql('74657374696E67746F6B656E'); - newAPNSToken.should.not.eql(originalAPNSToken); - if (originalAPNSToken !== null) { - await firebase.messaging().setAPNSToken(originalAPNSToken); - } - } else { - this.skip(); - } - }); - }); - - describe('unsupported web sdk methods', function () { - it('useServiceWorker should not error when called', function () { - firebase.messaging().useServiceWorker(); - }); - - it('usePublicVapidKey should not error when called', function () { - firebase.messaging().usePublicVapidKey(); - }); - }); - - describe('getInitialNotification', function () { - it('returns null when no initial notification', async function () { - should.strictEqual(await firebase.messaging().getInitialNotification(), null); - }); - }); - - describe('deleteToken()', function () { - it('generate a new token after deleting', async function () { - aPNSCapableSimulator = await isAPNSCapableSimulator(); - simulator = await isSimulator(); - if (Platform.ios && simulator && !aPNSCapableSimulator) { - this.skip(); - } else { - const token1 = await firebase.messaging().getToken(); - should.exist(token1); - await firebase.messaging().deleteToken(); - const token2 = await firebase.messaging().getToken(); - should.exist(token2); - token1.should.not.eql(token2); - } - }); - }); - - describe('getToken()', function () { - it('should throw Error with wrong parameters types', async function () { - try { - await firebase.messaging().getToken({ appName: 33 }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'appName' expected a string"); - } - - try { - await firebase.messaging().getToken({ senderId: 33 }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'senderId' expected a string."); - return Promise.resolve(); - } - }); - }); - - describe('onMessage()', function () { - it('throws if listener is not a function', function () { - try { - firebase.messaging().onMessage({}); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'listener' expected a function"); - return Promise.resolve(); - } - }); - - xit('receives messages when the app is in the foreground', async function () { - const spy = sinon.spy(); - const unsubscribe = firebase.messaging().onMessage(spy); - if (Platform.ios) { - await firebase.messaging().registerDeviceForRemoteMessages(); - } - const token = await firebase.messaging().getToken(); - await TestsAPI.messaging().sendToDevice(token, { - data: { - foo: 'bar', - doop: 'boop', - }, - }); - await Utils.spyToBeCalledOnceAsync(spy); - unsubscribe(); - spy.firstCall.args[0].should.be.an.Object(); - spy.firstCall.args[0].data.should.be.an.Object(); - spy.firstCall.args[0].data.foo.should.eql('bar'); - }); - }); - - describe('onTokenRefresh()', function () { - it('throws if listener is not a function', function () { - try { - firebase.messaging().onTokenRefresh({}); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'listener' expected a function"); - return Promise.resolve(); - } - }); - }); - - describe('onDeletedMessages()', function () { - it('throws if listener is not a function', function () { - try { - firebase.messaging().onDeletedMessages(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'listener' expected a function"); - return Promise.resolve(); - } - }); - }); - - describe('onMessageSent()', function () { - it('throws if listener is not a function', function () { - try { - firebase.messaging().onMessageSent(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'listener' expected a function"); - return Promise.resolve(); - } - }); - }); - - describe('onSendError()', function () { - it('throws if listener is not a function', function () { - try { - firebase.messaging().onSendError('123'); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'listener' expected a function"); - return Promise.resolve(); - } - }); - }); - - describe('setBackgroundMessageHandler()', function () { - it('throws if handler is not a function', function () { - try { - firebase.messaging().setBackgroundMessageHandler('123'); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'handler' expected a function"); - return Promise.resolve(); - } - }); - }); - - describe('subscribeToTopic()', function () { - it('throws if topic is not a string', function () { - try { - firebase.messaging().subscribeToTopic(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'topic' expected a string value"); - return Promise.resolve(); - } - }); - - it('throws if topic contains a /', function () { - try { - firebase.messaging().subscribeToTopic('foo/bar'); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql('\'topic\' must not include "/"'); - return Promise.resolve(); - } - }); - }); - - describe('unsubscribeFromTopic()', function () { - it('throws if topic is not a string', function () { - try { - firebase.messaging().unsubscribeFromTopic(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'topic' expected a string value"); - return Promise.resolve(); - } - }); - - it('throws if topic contains a /', function () { - try { - firebase.messaging().unsubscribeFromTopic('foo/bar'); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql('\'topic\' must not include "/"'); - return Promise.resolve(); - } - }); - }); - - describe('setDeliveryMetricsExportToBigQuery()', function () { - afterEach(async function () { - await firebase.messaging().setDeliveryMetricsExportToBigQuery(false); - }); - - it('throws if enabled is not a boolean', function () { - try { - firebase.messaging().setDeliveryMetricsExportToBigQuery(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'enabled' expected a boolean value"); - return Promise.resolve(); - } - }); - - it('sets the value', async function () { - should.equal(firebase.messaging().isDeliveryMetricsExportToBigQueryEnabled, false); - await firebase.messaging().setDeliveryMetricsExportToBigQuery(true); - should.equal(firebase.messaging().isDeliveryMetricsExportToBigQueryEnabled, true); - }); - }); - - describe('setNotificationDelegationEnabled()', function () { - afterEach(async function () { - await firebase.messaging().setNotificationDelegationEnabled(false); - }); - - it('throws if enabled is not a boolean', function () { - try { - firebase.messaging().setNotificationDelegationEnabled(123); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'enabled' expected a boolean value"); - return Promise.resolve(); - } - }); - - it('sets the value', async function () { - should.equal(firebase.messaging().isNotificationDelegationEnabled, false); - await firebase.messaging().setNotificationDelegationEnabled(true); - should.equal(firebase.messaging().isNotificationDelegationEnabled, true); - }); - }); - - describe('isSupported()', function () { - it('should return "true" if the device or browser supports Firebase Messaging', async function () { - // For android, when the play services are available, it will return "true" - // iOS & web always return "true". Web can be fully implemented when the platform is supported - should.equal(await firebase.messaging().isSupported(), true); - }); - }); - }); - describe('firebase v9 modular API', function () { describe('getMessaging', function () { it('pass app as argument', function () { @@ -696,10 +258,8 @@ describe('messaging()', function () { if (Platform.ios && (!simulator || (simulator && aPNSCapableSimulator))) { originalAPNSToken = await getAPNSToken(getMessaging()); // 74657374696E67746F6B656E6D6F64756C6172 is hex for "testingtokenmodular" - await firebase - .messaging() - .setAPNSToken('74657374696E67746F6B656E6D6F64756C6172', 'unknown'); - newAPNSToken = await firebase.messaging().getAPNSToken(); + await setAPNSToken(getMessaging(), '74657374696E67746F6B656E6D6F64756C6172', 'unknown'); + newAPNSToken = await getAPNSToken(getMessaging()); newAPNSToken.should.eql('74657374696E67746F6B656E6D6F64756C6172'); newAPNSToken.should.not.eql(originalAPNSToken); if (originalAPNSToken !== null) { diff --git a/packages/messaging/e2e/remoteMessage.e2e.js b/packages/messaging/e2e/remoteMessage.e2e.js index 28c4ba3c1d..4f111a7a3a 100644 --- a/packages/messaging/e2e/remoteMessage.e2e.js +++ b/packages/messaging/e2e/remoteMessage.e2e.js @@ -16,41 +16,89 @@ */ describe('remoteMessage modular', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; + describe('messaging().sendMessage(*)', function () { + it('throws if used on ios', function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.ios) { + try { + sendMessage(getMessaging(), 123); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql( + 'getMessaging().sendMessage() is only supported on Android devices.', + ); + return Promise.resolve(); + } + } else { + Promise.resolve(); + } }); - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; + it('throws if no object provided', function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + try { + sendMessage(getMessaging(), 123); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql("'remoteMessage' expected an object value"); + return Promise.resolve(); + } + } else { + this.skip(); + } }); - describe('messaging().sendMessage(*)', function () { - it('throws if used on ios', function () { - if (Platform.ios) { + it('uses default values', async function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + sendMessage(getMessaging(), {}); + } else { + this.skip(); + } + }); + + describe('to', function () { + it('throws if to is not a string', function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { try { - firebase.messaging().sendMessage(123); + sendMessage(getMessaging(), { + to: 123, + }); return Promise.reject(new Error('Did not throw Error.')); } catch (e) { - e.message.should.containEql( - 'firebase.messaging().sendMessage() is only supported on Android devices.', - ); + e.message.should.containEql("'remoteMessage.to' expected a string value"); return Promise.resolve(); } } else { - Promise.resolve(); + this.skip(); + } + }); + + it('accepts custom to value', async function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + await sendMessage(getMessaging(), { + to: 'foobar', + }); + } else { + this.skip(); } }); + }); - it('throws if no object provided', function () { + describe('messageId', function () { + it('throws if messageId is not a string', function () { + const { getMessaging, sendMessage } = messagingModular; if (Platform.android) { try { - firebase.messaging().sendMessage(123); + sendMessage(getMessaging(), { + messageId: 123, + }); return Promise.reject(new Error('Did not throw Error.')); } catch (e) { - e.message.should.containEql("'remoteMessage' expected an object value"); + e.message.should.containEql("'remoteMessage.messageId' expected a string value"); return Promise.resolve(); } } else { @@ -58,246 +106,63 @@ describe('remoteMessage modular', function () { } }); - it('uses default values', async function () { + it('accepts custom messageId value', async function () { + const { getMessaging, sendMessage } = messagingModular; if (Platform.android) { - firebase.messaging().sendMessage({}); + await sendMessage(getMessaging(), { + messageId: 'foobar', + }); } else { this.skip(); } }); + }); - describe('to', function () { - it('throws if to is not a string', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - to: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.to' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom to value', async function () { - if (Platform.android) { - await firebase.messaging().sendMessage({ - to: 'foobar', - }); - } else { - this.skip(); - } - }); - }); - - describe('messageId', function () { - it('throws if messageId is not a string', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - messageId: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.messageId' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom messageId value', async function () { - if (Platform.android) { - await firebase.messaging().sendMessage({ - messageId: 'foobar', - }); - } else { - this.skip(); - } - }); - }); - - describe('ttl', function () { - it('throws if not a number', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - ttl: '123', - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("remoteMessage.ttl' expected a number value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('throws if negative number', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - ttl: -2, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.ttl' expected a positive integer value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('throws if float number', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - ttl: 123.4, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.ttl' expected a positive integer value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom ttl value', async function () { - if (Platform.android) { - await firebase.messaging().sendMessage({ - ttl: 123, - }); - } else { - this.skip(); - } - }); - }); - - describe('data', function () { - it('throws if data not an object', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - data: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.data' expected an object value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom data value', async function () { - if (Platform.android) { - await firebase.messaging().sendMessage({ - data: { - foo: 'bar', - fooObject: { image: 'testURL' }, - }, - }); - } else { - this.skip(); - } - }); - }); - - describe('collapseKey', function () { - it('throws if collapseKey is not a string', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - collapseKey: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.collapseKey' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom collapseKey value', async function () { - if (Platform.android) { - await firebase.messaging().sendMessage({ - collapseKey: 'foobar', - }); - } else { - this.skip(); - } - }); - }); - - describe('messageType', function () { - it('throws if messageType is not a string', function () { - if (Platform.android) { - try { - firebase.messaging().sendMessage({ - messageType: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.messageType' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom messageType value', async function () { - if (Platform.android) { - await firebase.messaging().sendMessage({ - messageType: 'foobar', + describe('ttl', function () { + it('throws if not a number', function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + try { + sendMessage(getMessaging(), { + ttl: '123', }); - } else { - this.skip(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql("remoteMessage.ttl' expected a number value"); + return Promise.resolve(); } - }); + } else { + this.skip(); + } }); - }); - }); - describe('modular', function () { - describe('messaging().sendMessage(*)', function () { - it('throws if used on ios', function () { + it('throws if negative number', function () { const { getMessaging, sendMessage } = messagingModular; - if (Platform.ios) { + if (Platform.android) { try { - sendMessage(getMessaging(), 123); + sendMessage(getMessaging(), { + ttl: -2, + }); return Promise.reject(new Error('Did not throw Error.')); } catch (e) { - e.message.should.containEql( - 'firebase.messaging().sendMessage() is only supported on Android devices.', - ); + e.message.should.containEql("'remoteMessage.ttl' expected a positive integer value"); return Promise.resolve(); } } else { - Promise.resolve(); + this.skip(); } }); - it('throws if no object provided', function () { + it('throws if float number', function () { const { getMessaging, sendMessage } = messagingModular; if (Platform.android) { try { - sendMessage(getMessaging(), 123); + sendMessage(getMessaging(), { + ttl: 123.4, + }); return Promise.reject(new Error('Did not throw Error.')); } catch (e) { - e.message.should.containEql("'remoteMessage' expected an object value"); + e.message.should.containEql("'remoteMessage.ttl' expected a positive integer value"); return Promise.resolve(); } } else { @@ -305,230 +170,108 @@ describe('remoteMessage modular', function () { } }); - it('uses default values', async function () { + it('accepts custom ttl value', async function () { const { getMessaging, sendMessage } = messagingModular; if (Platform.android) { - sendMessage(getMessaging(), {}); + await sendMessage(getMessaging(), { + ttl: 123, + }); } else { this.skip(); } }); + }); - describe('to', function () { - it('throws if to is not a string', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - to: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.to' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom to value', async function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - await sendMessage(getMessaging(), { - to: 'foobar', + describe('data', function () { + it('throws if data not an object', function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + try { + sendMessage(getMessaging(), { + data: 123, }); - } else { - this.skip(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql("'remoteMessage.data' expected an object value"); + return Promise.resolve(); } - }); + } else { + this.skip(); + } }); - describe('messageId', function () { - it('throws if messageId is not a string', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - messageId: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.messageId' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom messageId value', async function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - await sendMessage(getMessaging(), { - messageId: 'foobar', - }); - } else { - this.skip(); - } - }); + it('accepts custom data value', async function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + await sendMessage(getMessaging(), { + data: { + foo: 'bar', + fooObject: { image: 'testURL' }, + }, + }); + } else { + this.skip(); + } }); + }); - describe('ttl', function () { - it('throws if not a number', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - ttl: '123', - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("remoteMessage.ttl' expected a number value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('throws if negative number', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - ttl: -2, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.ttl' expected a positive integer value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('throws if float number', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - ttl: 123.4, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.ttl' expected a positive integer value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom ttl value', async function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - await sendMessage(getMessaging(), { - ttl: 123, + describe('collapseKey', function () { + it('throws if collapseKey is not a string', function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + try { + sendMessage(getMessaging(), { + collapseKey: 123, }); - } else { - this.skip(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql("'remoteMessage.collapseKey' expected a string value"); + return Promise.resolve(); } - }); + } else { + this.skip(); + } }); - describe('data', function () { - it('throws if data not an object', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - data: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.data' expected an object value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom data value', async function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - await sendMessage(getMessaging(), { - data: { - foo: 'bar', - fooObject: { image: 'testURL' }, - }, - }); - } else { - this.skip(); - } - }); + it('accepts custom collapseKey value', async function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + await sendMessage(getMessaging(), { + collapseKey: 'foobar', + }); + } else { + this.skip(); + } }); + }); - describe('collapseKey', function () { - it('throws if collapseKey is not a string', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - collapseKey: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.collapseKey' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom collapseKey value', async function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - await sendMessage(getMessaging(), { - collapseKey: 'foobar', + describe('messageType', function () { + it('throws if messageType is not a string', function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + try { + sendMessage(getMessaging(), { + messageType: 123, }); - } else { - this.skip(); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql("'remoteMessage.messageType' expected a string value"); + return Promise.resolve(); } - }); + } else { + this.skip(); + } }); - describe('messageType', function () { - it('throws if messageType is not a string', function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - try { - sendMessage(getMessaging(), { - messageType: 123, - }); - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'remoteMessage.messageType' expected a string value"); - return Promise.resolve(); - } - } else { - this.skip(); - } - }); - - it('accepts custom messageType value', async function () { - const { getMessaging, sendMessage } = messagingModular; - if (Platform.android) { - await sendMessage(getMessaging(), { - messageType: 'foobar', - }); - } else { - this.skip(); - } - }); + it('accepts custom messageType value', async function () { + const { getMessaging, sendMessage } = messagingModular; + if (Platform.android) { + await sendMessage(getMessaging(), { + messageType: 'foobar', + }); + } else { + this.skip(); + } }); }); }); diff --git a/packages/messaging/lib/index.ts b/packages/messaging/lib/index.ts index 4fe517dfd0..ac113e884b 100644 --- a/packages/messaging/lib/index.ts +++ b/packages/messaging/lib/index.ts @@ -15,7 +15,793 @@ * */ -// Export types from types/messaging +import { + hasOwnProperty, + isAndroid, + isBoolean, + isFunction, + isIOS, + isObject, + isString, + isUndefined, +} from '@react-native-firebase/app/dist/module/common'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import { getReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import './types/internal'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import { AppRegistry } from 'react-native'; +import remoteMessageOptions from './remoteMessageOptions'; +import { + AuthorizationStatus, + NotificationAndroidPriority, + NotificationAndroidVisibility, +} from './statics'; +import type { + Messaging, + RemoteMessage, + IOSPermissions, + AuthorizationStatus as AuthorizationStatusType, + NativeTokenOptions, + GetTokenOptions, + SendErrorEvent, +} from './types/messaging'; +import { version } from './version'; + +const nativeModuleName = 'RNFBMessagingModule'; + +let backgroundMessageHandler: ((remoteMessage: RemoteMessage) => Promise) | undefined; +let openSettingsForNotificationHandler: ((remoteMessage: RemoteMessage) => any) | undefined; + +class FirebaseMessagingModule extends FirebaseModule implements Messaging { + _isAutoInitEnabled: boolean; + _isDeliveryMetricsExportToBigQueryEnabled: boolean; + _isRegisteredForRemoteNotifications: boolean; + _isNotificationDelegationEnabled: boolean; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); + this._isAutoInitEnabled = + this.native.isAutoInitEnabled != null ? this.native.isAutoInitEnabled : true; + this._isDeliveryMetricsExportToBigQueryEnabled = + this.native.isDeliveryMetricsExportToBigQueryEnabled != null + ? this.native.isDeliveryMetricsExportToBigQueryEnabled + : false; + this._isRegisteredForRemoteNotifications = + this.native.isRegisteredForRemoteNotifications != null + ? this.native.isRegisteredForRemoteNotifications + : true; + this._isNotificationDelegationEnabled = + this.native.isNotificationDelegationEnabled != null + ? this.native.isNotificationDelegationEnabled + : false; + + AppRegistry.registerHeadlessTask('ReactNativeFirebaseMessagingHeadlessTask', () => { + if (!backgroundMessageHandler) { + // eslint-disable-next-line no-console + console.warn( + 'No background message handler has been set. Set a handler via the "setBackgroundMessageHandler" method.', + ); + return () => Promise.resolve(); + } + return (remoteMessage: RemoteMessage) => backgroundMessageHandler!(remoteMessage); + }); + + if (isIOS) { + this.emitter.addListener( + 'messaging_message_received_background', + (remoteMessage: RemoteMessage) => { + if (!backgroundMessageHandler) { + // eslint-disable-next-line no-console + console.warn( + 'No background message handler has been set. Set a handler via the "setBackgroundMessageHandler" method.', + ); + return Promise.resolve(); + } + + const handlerPromise = Promise.resolve(backgroundMessageHandler(remoteMessage)); + handlerPromise.finally(() => { + this.native.completeNotificationProcessing(); + }); + + return handlerPromise; + }, + ); + + this.emitter.addListener( + 'messaging_settings_for_notification_opened', + (remoteMessage: RemoteMessage) => { + if (!openSettingsForNotificationHandler) { + // eslint-disable-next-line no-console + console.warn( + 'No handler for notification settings link has been set. Set a handler via the "setOpenSettingsForNotificationsHandler" method', + ); + + return Promise.resolve(); + } + + return openSettingsForNotificationHandler(remoteMessage); + }, + ); + } + } + + get isAutoInitEnabled(): boolean { + return this._isAutoInitEnabled; + } + + /** + * @ios + */ + get isDeviceRegisteredForRemoteMessages(): boolean { + if (isAndroid) { + return true; + } + + return this._isRegisteredForRemoteNotifications; + } + + get isNotificationDelegationEnabled(): boolean { + return this._isNotificationDelegationEnabled; + } + + get isDeliveryMetricsExportToBigQueryEnabled(): boolean { + return this._isDeliveryMetricsExportToBigQueryEnabled; + } + + setAutoInitEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error("getMessaging().setAutoInitEnabled(*) 'enabled' expected a boolean value."); + } + + this._isAutoInitEnabled = enabled; + return this.native.setAutoInitEnabled(enabled); + } + + getInitialNotification(): Promise { + return this.native.getInitialNotification().then((value: RemoteMessage | null) => { + if (value) { + return value; + } + return null; + }); + } + + getDidOpenSettingsForNotification(): Promise { + if (!isIOS) return Promise.resolve(false); + return this.native.getDidOpenSettingsForNotification().then((value: boolean) => value); + } + + getIsHeadless(): Promise { + return this.native.getIsHeadless(); + } + + getToken(options?: { appName?: string; senderId?: string }): Promise { + if (!isUndefined(options?.appName) && !isString(options?.appName)) { + throw new Error("getMessaging().getToken(*) 'appName' expected a string."); + } + + if (!isUndefined(options?.senderId) && !isString(options?.senderId)) { + throw new Error("getMessaging().getToken(*) 'senderId' expected a string."); + } + + const appName = options?.appName || this.app.name; + const senderId = options?.senderId || this.app.options.messagingSenderId; + + return this.native.getToken(appName, senderId || ''); + } + + deleteToken(options?: { appName?: string; senderId?: string }): Promise { + if (!isUndefined(options?.appName) && !isString(options?.appName)) { + throw new Error("getMessaging().deleteToken(*) 'appName' expected a string."); + } + + if (!isUndefined(options?.senderId) && !isString(options?.senderId)) { + throw new Error("getMessaging().deleteToken(*) 'senderId' expected a string."); + } + + const appName = options?.appName || this.app.name; + const senderId = options?.senderId || this.app.options.messagingSenderId; + + return this.native.deleteToken(appName, senderId || ''); + } + + onMessage(listener: (message: RemoteMessage) => any): () => void { + if (!isFunction(listener)) { + throw new Error("getMessaging().onMessage(*) 'listener' expected a function."); + } + + const subscription = this.emitter.addListener('messaging_message_received', listener); + return () => subscription.remove(); + } + + onNotificationOpenedApp(listener: (message: RemoteMessage) => any): () => void { + if (!isFunction(listener)) { + throw new Error("getMessaging().onNotificationOpenedApp(*) 'listener' expected a function."); + } + + const subscription = this.emitter.addListener('messaging_notification_opened', listener); + return () => subscription.remove(); + } + + onTokenRefresh(listener: (token: string) => any): () => void { + if (!isFunction(listener)) { + throw new Error("getMessaging().onTokenRefresh(*) 'listener' expected a function."); + } + + const subscription = this.emitter.addListener( + 'messaging_token_refresh', + (event: { token: string }) => { + const { token } = event; + listener(token); + }, + ); + return () => subscription.remove(); + } + + /** + * @platform ios + * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or + * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission + * requests instead. These APIs will be removed in a future major release. + * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. + */ + requestPermission(permissions?: IOSPermissions): Promise { + if (isAndroid) { + return Promise.resolve(AuthorizationStatus.AUTHORIZED); + } + + const defaultPermissions: IOSPermissions = { + alert: true, + announcement: false, + badge: true, + carPlay: true, + provisional: false, + sound: true, + criticalAlert: false, + providesAppNotificationSettings: false, + }; + + if (!permissions) { + return this.native.requestPermission(defaultPermissions) as Promise; + } + + if (!isObject(permissions)) { + throw new Error('getMessaging().requestPermission(*) expected an object value.'); + } + + Object.entries(permissions).forEach(([key, value]) => { + if (!hasOwnProperty(defaultPermissions, key)) { + throw new Error( + `getMessaging().requestPermission(*) unexpected key "${key}" provided to permissions object.`, + ); + } + + if (!isBoolean(value)) { + throw new Error( + `getMessaging().requestPermission(*) the permission "${key}" expected a boolean value.`, + ); + } + + (defaultPermissions as Record)[key] = value; + }); + + return this.native.requestPermission(defaultPermissions) as Promise; + } + + registerDeviceForRemoteMessages(): Promise { + if (isAndroid) { + return Promise.resolve(); + } + + const autoRegister = this.firebaseJson['messaging_ios_auto_register_for_remote_messages']; + if (autoRegister === undefined || autoRegister === true) { + // eslint-disable-next-line no-console + console.warn( + `Usage of "registerDeviceForRemoteMessages()" is not required. You only need to register if auto-registration is disabled in your 'firebase.json' configuration file via the 'messaging_ios_auto_register_for_remote_messages' property.`, + ); + } + + this._isRegisteredForRemoteNotifications = true; + return this.native.registerForRemoteNotifications(); + } + + /** + * @platform ios + */ + unregisterDeviceForRemoteMessages(): Promise { + if (isAndroid) { + return Promise.resolve(); + } + this._isRegisteredForRemoteNotifications = false; + return this.native.unregisterForRemoteNotifications(); + } + + /** + * @platform ios + */ + getAPNSToken(): Promise { + if (isAndroid) { + return Promise.resolve(null); + } + return this.native.getAPNSToken(); + } + + /** + * @platform ios + */ + setAPNSToken(token: string, type?: string): Promise { + if (isUndefined(token) || !isString(token)) { + throw new Error("getMessaging().setAPNSToken(*) 'token' expected a string value."); + } + + if (!isUndefined(type) && (!isString(type) || !['prod', 'sandbox', 'unknown'].includes(type))) { + throw new Error( + "getMessaging().setAPNSToken(*) 'type' expected one of 'prod', 'sandbox', or 'unknown'.", + ); + } + + if (isAndroid) { + return Promise.resolve(); + } + + return this.native.setAPNSToken(token, type); + } + + /** + * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or + * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission + * requests instead. These APIs will be removed in a future major release. + * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. + */ + hasPermission(): Promise { + return this.native.hasPermission() as Promise; + } + + onDeletedMessages(listener: () => void): () => void { + if (!isFunction(listener)) { + throw new Error("getMessaging().onDeletedMessages(*) 'listener' expected a function."); + } + + const subscription = this.emitter.addListener('messaging_message_deleted', listener); + return () => subscription.remove(); + } + + onMessageSent(listener: (messageId: string) => any): () => void { + if (!isFunction(listener)) { + throw new Error("getMessaging().onMessageSent(*) 'listener' expected a function."); + } + + const subscription = this.emitter.addListener('messaging_message_sent', listener); + return () => { + subscription.remove(); + }; + } + + onSendError( + listener: (evt: { messageId: string; error: ReactNativeFirebase.NativeFirebaseError }) => any, + ): () => void { + if (!isFunction(listener)) { + throw new Error("getMessaging().onSendError(*) 'listener' expected a function."); + } + + const subscription = this.emitter.addListener('messaging_message_send_error', listener); + return () => subscription.remove(); + } + + setBackgroundMessageHandler(handler: (message: RemoteMessage) => Promise): void { + if (!isFunction(handler)) { + throw new Error( + "getMessaging().setBackgroundMessageHandler(*) 'handler' expected a function.", + ); + } + + backgroundMessageHandler = handler; + if (isIOS) { + this.native.signalBackgroundMessageHandlerSet(); + } + } + + setOpenSettingsForNotificationsHandler(handler: (message: RemoteMessage) => any): void { + if (!isIOS) { + return; + } + + if (!isFunction(handler)) { + throw new Error( + "getMessaging().setOpenSettingsForNotificationsHandler(*) 'handler' expected a function.", + ); + } + + openSettingsForNotificationHandler = handler; + } + + sendMessage(remoteMessage: RemoteMessage): Promise { + if (isIOS) { + throw new Error(`getMessaging().sendMessage() is only supported on Android devices.`); + } + let options; + try { + const senderId = this.app.options.messagingSenderId; + if (!senderId) { + throw new Error("'messagingSenderId' is required in Firebase app options."); + } + options = remoteMessageOptions(senderId, remoteMessage); + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e); + throw new Error(`getMessaging().sendMessage(*) ${message}.`); + } + + return this.native.sendMessage(options as unknown as Record); + } + + subscribeToTopic(topic: string): Promise { + if (!isString(topic)) { + throw new Error("getMessaging().subscribeToTopic(*) 'topic' expected a string value."); + } + + if (topic.indexOf('/') > -1) { + throw new Error('getMessaging().subscribeToTopic(*) \'topic\' must not include "/".'); + } + + return this.native.subscribeToTopic(topic); + } + + unsubscribeFromTopic(topic: string): Promise { + if (!isString(topic)) { + throw new Error("getMessaging().unsubscribeFromTopic(*) 'topic' expected a string value."); + } + + if (topic.indexOf('/') > -1) { + throw new Error('getMessaging().unsubscribeFromTopic(*) \'topic\' must not include "/".'); + } + + return this.native.unsubscribeFromTopic(topic); + } + + setDeliveryMetricsExportToBigQuery(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "getMessaging().setDeliveryMetricsExportToBigQuery(*) 'enabled' expected a boolean value.", + ); + } + + this._isDeliveryMetricsExportToBigQueryEnabled = enabled; + return this.native.setDeliveryMetricsExportToBigQuery(enabled); + } + + setNotificationDelegationEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "getMessaging().setNotificationDelegationEnabled(*) 'enabled' expected a boolean value.", + ); + } + + this._isNotificationDelegationEnabled = enabled; + if (isIOS) { + return Promise.resolve(); + } + + return this.native.setNotificationDelegationEnabled(enabled); + } + + async isSupported(): Promise { + if (isAndroid) { + const utilsNativeModule = getReactNativeModule('RNFBUtilsModule'); + if (!utilsNativeModule) { + return false; + } + const playServicesAvailability = utilsNativeModule.androidPlayServices as + | { isAvailable?: boolean } + | undefined; + return playServicesAvailability?.isAvailable ?? false; + } + return true; + } +} + +const config: ModuleConfig = { + namespace: 'messaging', + nativeModuleName, + nativeEvents: [ + 'messaging_token_refresh', + 'messaging_message_sent', + 'messaging_message_deleted', + 'messaging_message_received', + 'messaging_message_send_error', + 'messaging_notification_opened', + ...(isIOS + ? ['messaging_message_received_background', 'messaging_settings_for_notification_opened'] + : []), + ], + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, +}; + +export const SDK_VERSION = version; + +export { AuthorizationStatus, NotificationAndroidPriority, NotificationAndroidVisibility }; + +/** + * Returns the {@link Messaging} instance for the default or given {@link FirebaseApp}. + * + * @param app - The Firebase `FirebaseApp` to use. When omitted, the default app is used. + * @returns The Messaging service instance for that app. + */ +export function getMessaging(app?: FirebaseApp): Messaging { + return getOrCreateModularInstance(FirebaseMessagingModule, config, app) as unknown as Messaging; +} + +/** + * Removes access to an FCM token previously authorized by its scope. + */ +export function deleteToken( + messaging: Messaging, + tokenOptions?: NativeTokenOptions, +): Promise { + return messaging.deleteToken(tokenOptions); +} + +/** + * Returns an FCM token for this device. + */ +export function getToken( + messaging: Messaging, + options?: GetTokenOptions & NativeTokenOptions, +): Promise { + return messaging.getToken(options); +} + +/** + * When any FCM payload is received, the listener callback is called with a `RemoteMessage`. + */ +export function onMessage( + messaging: Messaging, + listener: (message: RemoteMessage) => any, +): () => void { + return messaging.onMessage(listener); +} + +/** + * When the user presses a notification displayed via FCM, this listener will be called if the app + * has opened from a background state. + */ +export function onNotificationOpenedApp( + messaging: Messaging, + listener: (message: RemoteMessage) => any, +): () => void { + return messaging.onNotificationOpenedApp(listener); +} + +/** + * Called when a new registration token is generated for the device. + */ +export function onTokenRefresh(messaging: Messaging, listener: (token: string) => any): () => void { + return messaging.onTokenRefresh(listener); +} + +/** + * On iOS, messaging permission must be requested by the current application before messages can + * be received or sent. + * + * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or + * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission + * requests instead. These APIs will be removed in a future major release. + * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. + */ +export function requestPermission( + messaging: Messaging, + iosPermissions?: IOSPermissions, +): Promise { + return messaging.requestPermission(iosPermissions); +} + +/** + * Returns whether messaging auto initialization is enabled or disabled for the device. + */ +export function isAutoInitEnabled(messaging: Messaging): boolean { + return messaging.isAutoInitEnabled; +} + +/** + * Sets whether messaging auto initialization is enabled or disabled for the device. + */ +export function setAutoInitEnabled(messaging: Messaging, enabled: boolean): Promise { + return messaging.setAutoInitEnabled(enabled); +} + +/** + * When a notification from FCM has triggered the application to open from a quit state, + * this method will return a `RemoteMessage` containing the notification data, or `null`. + */ +export function getInitialNotification(messaging: Messaging): Promise { + return messaging.getInitialNotification(); +} + +/** + * When the app is opened from iOS notifications settings from a quit state, + * this method will return `true` or `false` if the app was opened via another method. + */ +export function getDidOpenSettingsForNotification(messaging: Messaging): Promise { + return messaging.getDidOpenSettingsForNotification(); +} + +/** + * Returns whether the root view is headless or not. + */ +export function getIsHeadless(messaging: Messaging): Promise { + return messaging.getIsHeadless(); +} + +/** + * On iOS, if your app wants to receive remote messages from FCM (via APNs), you must explicitly register + * with APNs if auto-registration has been disabled. + */ +export function registerDeviceForRemoteMessages(messaging: Messaging): Promise { + return messaging.registerDeviceForRemoteMessages(); +} + +/** + * Returns a boolean value whether the user has registered for remote notifications via + * `registerDeviceForRemoteMessages()`. + */ +export function isDeviceRegisteredForRemoteMessages(messaging: Messaging): boolean { + return messaging.isDeviceRegisteredForRemoteMessages; +} + +/** + * Unregisters the app from receiving remote notifications. + */ +export function unregisterDeviceForRemoteMessages(messaging: Messaging): Promise { + return messaging.unregisterDeviceForRemoteMessages(); +} + +/** + * On iOS, it is possible to get the users APNs token. + */ +export function getAPNSToken(messaging: Messaging): Promise { + return messaging.getAPNSToken(); +} + +/** + * On iOS, sets the APNs Token received by the application delegate. + */ +export function setAPNSToken(messaging: Messaging, token: string, type?: string): Promise { + return messaging.setAPNSToken(token, type); +} + +/** + * Returns a `AuthorizationStatus` as to whether the user has messaging permission for this app. + * + * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or + * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission + * requests instead. These APIs will be removed in a future major release. + * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. + */ +export function hasPermission(messaging: Messaging): Promise { + return messaging.hasPermission(); +} + +/** + * Called when the FCM server deletes pending messages. + */ +export function onDeletedMessages(messaging: Messaging, listener: () => void): () => void { + return messaging.onDeletedMessages(listener); +} + +/** + * When sending a `RemoteMessage`, this listener is called when the message has been sent to FCM. + */ +export function onMessageSent( + messaging: Messaging, + listener: (messageId: string) => any, +): () => void { + return messaging.onMessageSent(listener); +} + +/** + * When sending a `RemoteMessage`, this listener is called when an error is thrown and the + * message could not be sent. + */ +export function onSendError( + messaging: Messaging, + listener: (evt: SendErrorEvent) => any, +): () => void { + return messaging.onSendError(listener); +} + +/** + * Set a message handler function which is called when the app is in the background or terminated. + */ +export function setBackgroundMessageHandler( + messaging: Messaging, + handler: (message: RemoteMessage) => Promise, +): void { + return messaging.setBackgroundMessageHandler(handler); +} + +/** + * Set a handler function which is called when the `${App Name} notifications settings` + * link in iOS settings is clicked. + */ +export function setOpenSettingsForNotificationsHandler( + messaging: Messaging, + handler: (message: RemoteMessage) => any, +): void { + return messaging.setOpenSettingsForNotificationsHandler(handler); +} + +/** + * Send a new `RemoteMessage` to the FCM server. + */ +export function sendMessage(messaging: Messaging, message: RemoteMessage): Promise { + return messaging.sendMessage(message); +} + +/** + * Apps can subscribe to a topic, which allows the FCM server to send targeted messages to only those + * devices subscribed to that topic. + */ +export function subscribeToTopic(messaging: Messaging, topic: string): Promise { + return messaging.subscribeToTopic(topic); +} + +/** + * Unsubscribe the device from a topic. + */ +export function unsubscribeFromTopic(messaging: Messaging, topic: string): Promise { + return messaging.unsubscribeFromTopic(topic); +} + +/** + * Returns a boolean whether message delivery metrics are exported to BigQuery. + */ +export function isDeliveryMetricsExportToBigQueryEnabled(messaging: Messaging): boolean { + return messaging.isDeliveryMetricsExportToBigQueryEnabled; +} + +/** + * Returns a boolean whether message delegation is enabled. Android only, always returns false on iOS. + */ +export function isNotificationDelegationEnabled(messaging: Messaging): boolean { + return messaging.isNotificationDelegationEnabled; +} + +/** + * Sets whether message notification delegation is enabled or disabled. + */ +export function setNotificationDelegationEnabled( + messaging: Messaging, + enabled: boolean, +): Promise { + return messaging.setNotificationDelegationEnabled(enabled); +} + +/** + * Checks if all required APIs exist in the browser. + */ +export function isSupported(messaging: Messaging): Promise { + return messaging.isSupported(); +} + +/** + * Sets whether message delivery metrics are exported to BigQuery is enabled or disabled. + */ +export function experimentalSetDeliveryMetricsExportedToBigQueryEnabled( + messaging: Messaging, + enabled: boolean, +): Promise { + return messaging.setDeliveryMetricsExportToBigQuery(enabled); +} + export type { Messaging, RemoteMessage, @@ -27,18 +813,4 @@ export type { NotificationIOSCriticalSound, IOSPermissions, SendErrorEvent, - FirebaseMessagingTypes, } from './types/messaging'; - -export { - AuthorizationStatus, - NotificationAndroidPriority, - NotificationAndroidVisibility, -} from './statics'; - -// Export modular API functions -export * from './modular'; - -// Export namespaced API -export * from './namespaced'; -export { default } from './namespaced'; diff --git a/packages/messaging/lib/modular.ts b/packages/messaging/lib/modular.ts deleted file mode 100644 index b147a83782..0000000000 --- a/packages/messaging/lib/modular.ts +++ /dev/null @@ -1,455 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import { withModularFlag } from '@react-native-firebase/app/dist/module/common'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import type { - Messaging, - RemoteMessage, - NativeTokenOptions, - GetTokenOptions, - IOSPermissions, - AuthorizationStatus, - SendErrorEvent, -} from './types/messaging'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Returns a Messaging instance for the given app. - * @param app - FirebaseApp. Optional. - * @returns Messaging instance - */ -export function getMessaging(app?: ReactNativeFirebase.FirebaseApp): Messaging { - if (app) { - return getApp(app.name).messaging(); - } - - return getApp().messaging(); -} - -/** - * Removes access to an FCM token previously authorized by its scope. - * Messages sent by the server to this token will fail. - * @param messaging - Messaging instance. - * @param tokenOptions - Options to override senderId (iOS) and projectId (Android). - * @returns Promise that resolves when the token is deleted. - */ -export function deleteToken( - messaging: Messaging, - tokenOptions?: NativeTokenOptions, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.deleteToken.call(messaging, tokenOptions, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns an FCM token for this device. Optionally, you can specify custom options for your own use case. - * @param messaging - Messaging instance. - * @param options - Options to override senderId (iOS) and appName. - * @returns Promise that resolves with the FCM token. - */ -export function getToken( - messaging: Messaging, - options?: GetTokenOptions & NativeTokenOptions, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.getToken.call(messaging, options, MODULAR_DEPRECATION_ARG); -} - -/** - * When any FCM payload is received, the listener callback is called with a `RemoteMessage`. - * > This subscriber method is only called when the app is active (in the foreground). - * @param messaging - Messaging instance. - * @param listener - Called with a `RemoteMessage` when a new FCM payload is received from the server. - * @returns Function to unsubscribe from the message listener. - */ -export function onMessage( - messaging: Messaging, - listener: (message: RemoteMessage) => any, -): () => void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.onMessage.call(messaging, listener, MODULAR_DEPRECATION_ARG); -} - -/** - * When the user presses a notification displayed via FCM, this listener will be called if the app - * has opened from a background state. - * @param messaging - Messaging instance. - * @param listener - Called with a `RemoteMessage` when a notification press opens the application. - * @returns Function to unsubscribe from the notification opened listener. - */ -export function onNotificationOpenedApp( - messaging: Messaging, - listener: (message: RemoteMessage) => any, -): () => void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.onNotificationOpenedApp.call(messaging, listener, MODULAR_DEPRECATION_ARG); -} - -/** - * Called when a new registration token is generated for the device. For example, this event can happen when a - * token expires or when the server invalidates the token. - * > This subscriber method is only called when the app is active (in the foreground). - * @param messaging - Messaging instance. - * @param listener - Called with a FCM token when the token is refreshed. - * @returns Function to unsubscribe from the token refresh listener. - */ -export function onTokenRefresh(messaging: Messaging, listener: (token: string) => any): () => void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.onTokenRefresh.call(messaging, listener, MODULAR_DEPRECATION_ARG); -} - -/** - * On iOS, messaging permission must be requested by the current application before messages can - * be received or sent. - * @param messaging - Messaging instance. - * @param iosPermissions - All the available permissions for iOS that can be requested. - * @returns Promise that resolves with the authorization status. - * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or - * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission - * requests instead. These APIs will be removed in a future major release. - * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. - */ -export function requestPermission( - messaging: Messaging, - iosPermissions?: IOSPermissions, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.requestPermission.call(messaging, iosPermissions, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns whether messaging auto initialization is enabled or disabled for the device. - * @param messaging - Messaging instance. - * @returns Boolean indicating whether auto initialization is enabled. - */ -export function isAutoInitEnabled(messaging: Messaging): boolean { - return withModularFlag(() => messaging.isAutoInitEnabled); -} - -/** - * Sets whether messaging auto initialization is enabled or disabled for the device. - * @param messaging - Messaging instance. - * @param enabled - A boolean value to enable or disable auto initialization. - * @returns Promise that resolves when the setting is updated. - */ -export function setAutoInitEnabled(messaging: Messaging, enabled: boolean): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.setAutoInitEnabled.call(messaging, enabled, MODULAR_DEPRECATION_ARG); -} - -/** - * When a notification from FCM has triggered the application to open from a quit state, - * this method will return a `RemoteMessage` containing the notification data, or `null` if - * the app was opened via another method. - * @param messaging - Messaging instance. - * @returns Promise that resolves with the initial notification or null. - */ -export function getInitialNotification(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.getInitialNotification.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * When the app is opened from iOS notifications settings from a quit state, - * this method will return `true` or `false` if the app was opened via another method. - * @param messaging - Messaging instance. - * @returns Promise that resolves with a boolean indicating if the app was opened from settings. - */ -export function getDidOpenSettingsForNotification(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.getDidOpenSettingsForNotification.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns whether the root view is headless or not - * i.e true if the app was launched in the background (for example, by data-only cloud message) - * @param messaging - Messaging instance. - * @returns Promise that resolves with a boolean indicating if the app is headless. - */ -export function getIsHeadless(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.getIsHeadless.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * On iOS, if your app wants to receive remote messages from FCM (via APNs), you must explicitly register - * with APNs if auto-registration has been disabled. - * @param messaging - Messaging instance. - * @returns Promise that resolves when the device is registered. - */ -export function registerDeviceForRemoteMessages(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.registerDeviceForRemoteMessages.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a boolean value whether the user has registered for remote notifications via - * `registerDeviceForRemoteMessages()`. For iOS. Android always returns `true`. - * @param messaging - Messaging instance. - * @returns Boolean indicating if the device is registered for remote messages. - */ -export function isDeviceRegisteredForRemoteMessages(messaging: Messaging): boolean { - return withModularFlag(() => messaging.isDeviceRegisteredForRemoteMessages); -} - -/** - * Unregisters the app from receiving remote notifications. - * @param messaging - Messaging instance. - * @returns Promise that resolves when the device is unregistered. - */ -export function unregisterDeviceForRemoteMessages(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.unregisterDeviceForRemoteMessages.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * On iOS, it is possible to get the users APNs token. This may be required if you want to send messages to your - * iOS devices without using the FCM service. - * @param messaging - Messaging instance. - * @returns Promise that resolves with the APNs token or null. - */ -export function getAPNSToken(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.getAPNSToken.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * On iOS, This method is used to set the APNs Token received by the application delegate. - * Note that the token is expected to be a hexadecimal string, as it is an NSData type in - * the underlying native firebase SDK, and raw data may only be passed as a string if it is - * hex encoded. Calling code is responsible for correct encoding, you should verify by comparing - * the results of `getAPNSToken()` with your token parameter to make sure they are equivalent. - * - * Messaging uses method swizzling to ensure that the APNs token is set automatically. - * However, if you have disabled swizzling by setting FirebaseAppDelegateProxyEnabled to NO - * in your app's Info.plist, you should manually set the APNs token in your application - * delegate's application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method. - * - * If you would like to set the type of the APNs token, rather than relying on automatic - * detection, provide a type of either 'prod', 'sandbox'. Omitting the type parameter - * or specifying 'unknown' will rely on automatic type detection based on provisioning profile. - * - * At a native level you may also call objective-c `[FIRMessaging setAPNSToken];` as needed. - * - * @param messaging - Messaging instance. - * @param token - A hexadecimal string representing your APNs token. - * @param type - Optional. A string specifying 'prod', 'sandbox' or 'unknown' token type. - * @returns Promise that resolves when the APNs token is set. - */ -export function setAPNSToken(messaging: Messaging, token: string, type?: string): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.setAPNSToken.call(messaging, token, type, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a `AuthorizationStatus` as to whether the user has messaging permission for this app. - * @param messaging - Messaging instance. - * @returns Promise that resolves with the authorization status. - * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or - * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission - * requests instead. These APIs will be removed in a future major release. - * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. - */ -export function hasPermission(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.hasPermission.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * Called when the FCM server deletes pending messages. - * @param messaging - Messaging instance. - * @param listener - Called when the FCM deletes pending messages. - * @returns Function to unsubscribe from the deleted messages listener. - */ -export function onDeletedMessages(messaging: Messaging, listener: () => void): () => void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.onDeletedMessages.call(messaging, listener, MODULAR_DEPRECATION_ARG); -} - -/** - * When sending a `RemoteMessage`, this listener is called when the message has been sent to FCM. - * @param messaging - Messaging instance. - * @param listener - Called when the FCM sends the remote message to FCM. - * @returns Function to unsubscribe from the message sent listener. - */ -export function onMessageSent( - messaging: Messaging, - listener: (messageId: string) => any, -): () => void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.onMessageSent.call(messaging, listener, MODULAR_DEPRECATION_ARG); -} - -/** - * When sending a `RemoteMessage`, this listener is called when an error is thrown and the - * message could not be sent. - * @param messaging - Messaging instance. - * @param listener - Called when the FCM sends the remote message to FCM. - * @returns Function to unsubscribe from the send error listener. - */ -export function onSendError( - messaging: Messaging, - listener: (evt: SendErrorEvent) => any, -): () => void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.onSendError.call(messaging, listener, MODULAR_DEPRECATION_ARG); -} - -/** - * Set a message handler function which is called when the app is in the background - * or terminated. In Android, a headless task is created, allowing you to access the React Native environment - * to perform tasks such as updating local storage, or sending a network request. - * @param messaging - Messaging instance. - * @param handler - Called when a message is sent and the application is in a background or terminated state. - * @returns void - */ -export function setBackgroundMessageHandler( - messaging: Messaging, - handler: (message: RemoteMessage) => Promise, -): void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.setBackgroundMessageHandler.call(messaging, handler, MODULAR_DEPRECATION_ARG); -} - -/** - * Set a handler function which is called when the `${App Name} notifications settings` - * link in iOS settings is clicked. - * @param messaging - Messaging instance. - * @param handler - Called when link in iOS settings is clicked. - * @returns void - */ -export function setOpenSettingsForNotificationsHandler( - messaging: Messaging, - handler: (message: RemoteMessage) => any, -): void { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return (messaging.setOpenSettingsForNotificationsHandler as any).call( - messaging, - handler, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Send a new `RemoteMessage` to the FCM server. - * @param messaging - Messaging instance. - * @param message - A `RemoteMessage` interface. - * @returns Promise that resolves when the message is sent. - */ -export function sendMessage(messaging: Messaging, message: RemoteMessage): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.sendMessage.call(messaging, message, MODULAR_DEPRECATION_ARG); -} - -/** - * Apps can subscribe to a topic, which allows the FCM server to send targeted messages to only those - * devices subscribed to that topic. - * @param messaging - Messaging instance. - * @param topic - The topic name. - * @returns Promise that resolves when the subscription is complete. - */ -export function subscribeToTopic(messaging: Messaging, topic: string): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.subscribeToTopic.call(messaging, topic, MODULAR_DEPRECATION_ARG); -} - -/** - * Unsubscribe the device from a topic. - * @param messaging - Messaging instance. - * @param topic - The topic name. - * @returns Promise that resolves when the unsubscription is complete. - */ -export function unsubscribeFromTopic(messaging: Messaging, topic: string): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.unsubscribeFromTopic.call(messaging, topic, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a boolean whether message delivery metrics are exported to BigQuery. - * @param messaging - Messaging instance. - * @returns Boolean indicating if message delivery metrics are exported to BigQuery. - */ -export function isDeliveryMetricsExportToBigQueryEnabled(messaging: Messaging): boolean { - return withModularFlag(() => messaging.isDeliveryMetricsExportToBigQueryEnabled); -} - -/** - * Returns a boolean whether message delegation is enabled. Android only, - * always returns false on iOS - * @param messaging - Messaging instance. - * @returns Boolean indicating if notification delegation is enabled. - */ -export function isNotificationDelegationEnabled(messaging: Messaging): boolean { - return withModularFlag(() => messaging.isNotificationDelegationEnabled); -} - -/** - * Sets whether message notification delegation is enabled or disabled. - * The value is false by default. Set this to true to allow delegation of notification to Google Play Services. - * Note if true message handlers will not function on Android, and it has no effect on iOS - * @param messaging - Messaging instance. - * @param enabled - A boolean value to enable or disable delegation of messages to Google Play Services. - * @returns Promise that resolves when the setting is updated. - */ -export function setNotificationDelegationEnabled( - messaging: Messaging, - enabled: boolean, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return (messaging.setNotificationDelegationEnabled as any).call( - messaging, - enabled, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Checks if all required APIs exist in the browser. - * @param messaging - Messaging instance. - * @returns Promise that resolves with a boolean indicating if the APIs are supported. - */ -export function isSupported(messaging: Messaging): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return messaging.isSupported.call(messaging, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets whether message delivery metrics are exported to BigQuery is enabled or disabled. - * The value is false by default. Set this to true to allow exporting of message delivery metrics to BigQuery. - * @param messaging - Messaging instance. - * @param enabled - A boolean value to enable or disable exporting of message delivery metrics to BigQuery. - * @returns Promise that resolves when the setting is updated. - */ -export function experimentalSetDeliveryMetricsExportedToBigQueryEnabled( - messaging: Messaging, - enabled: boolean, -): Promise { - // @ts-ignore - MODULAR_DEPRECATION_ARG is used internally - return (messaging.setDeliveryMetricsExportToBigQuery as any).call( - messaging, - enabled, - MODULAR_DEPRECATION_ARG, - ); -} - -export { - AuthorizationStatus, - NotificationAndroidPriority, - NotificationAndroidVisibility, -} from './statics'; diff --git a/packages/messaging/lib/namespaced.ts b/packages/messaging/lib/namespaced.ts deleted file mode 100644 index 6ab3c34f0a..0000000000 --- a/packages/messaging/lib/namespaced.ts +++ /dev/null @@ -1,593 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { - hasOwnProperty, - isAndroid, - isBoolean, - isFunction, - isIOS, - isObject, - isString, - isUndefined, -} from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import { AppRegistry } from 'react-native'; -import remoteMessageOptions from './remoteMessageOptions'; -import { version } from './version'; -import { - AuthorizationStatus, - NotificationAndroidPriority, - NotificationAndroidVisibility, -} from './statics'; -import type { - Messaging, - Statics, - RemoteMessage, - IOSPermissions, - AuthorizationStatus as AuthorizationStatusType, -} from './types/messaging'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -const statics: Partial = { - AuthorizationStatus, - NotificationAndroidPriority, - NotificationAndroidVisibility, -}; -const namespace = 'messaging'; - -const nativeModuleName = 'RNFBMessagingModule'; - -let backgroundMessageHandler: ((remoteMessage: RemoteMessage) => Promise) | undefined; -let openSettingsForNotificationHandler: ((remoteMessage: RemoteMessage) => any) | undefined; -let playServicesAvailability: any; - -class FirebaseMessagingModule extends FirebaseModule implements Messaging { - _isAutoInitEnabled: boolean; - _isDeliveryMetricsExportToBigQueryEnabled: boolean; - _isRegisteredForRemoteNotifications: boolean; - _isNotificationDelegationEnabled: boolean; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: any, - customUrlOrRegion?: string | null, - ) { - super(app, config, customUrlOrRegion); - this._isAutoInitEnabled = - this.native.isAutoInitEnabled != null ? this.native.isAutoInitEnabled : true; - this._isDeliveryMetricsExportToBigQueryEnabled = - this.native.isDeliveryMetricsExportToBigQueryEnabled != null - ? this.native.isDeliveryMetricsExportToBigQueryEnabled - : false; - this._isRegisteredForRemoteNotifications = - this.native.isRegisteredForRemoteNotifications != null - ? this.native.isRegisteredForRemoteNotifications - : true; - this._isNotificationDelegationEnabled = - this.native.isNotificationDelegationEnabled != null - ? this.native.isNotificationDelegationEnabled - : false; - - AppRegistry.registerHeadlessTask('ReactNativeFirebaseMessagingHeadlessTask', () => { - if (!backgroundMessageHandler) { - // eslint-disable-next-line no-console - console.warn( - 'No background message handler has been set. Set a handler via the "setBackgroundMessageHandler" method.', - ); - return () => Promise.resolve(); - } - return (remoteMessage: RemoteMessage) => backgroundMessageHandler!(remoteMessage); - }); - - if (isIOS) { - this.emitter.addListener( - 'messaging_message_received_background', - (remoteMessage: RemoteMessage) => { - if (!backgroundMessageHandler) { - // eslint-disable-next-line no-console - console.warn( - 'No background message handler has been set. Set a handler via the "setBackgroundMessageHandler" method.', - ); - return Promise.resolve(); - } - - // Ensure the handler is a promise - const handlerPromise = Promise.resolve(backgroundMessageHandler(remoteMessage)); - handlerPromise.finally(() => { - this.native.completeNotificationProcessing(); - }); - - return handlerPromise; - }, - ); - - this.emitter.addListener( - 'messaging_settings_for_notification_opened', - (remoteMessage: RemoteMessage) => { - if (!openSettingsForNotificationHandler) { - // eslint-disable-next-line no-console - console.warn( - 'No handler for notification settings link has been set. Set a handler via the "setOpenSettingsForNotificationsHandler" method', - ); - - return Promise.resolve(); - } - - return openSettingsForNotificationHandler(remoteMessage); - }, - ); - } - } - - get isAutoInitEnabled(): boolean { - return this._isAutoInitEnabled; - } - - /** - * @ios - */ - get isDeviceRegisteredForRemoteMessages(): boolean { - if (isAndroid) { - return true; - } - - return this._isRegisteredForRemoteNotifications; - } - - get isNotificationDelegationEnabled(): boolean { - return this._isNotificationDelegationEnabled; - } - - get isDeliveryMetricsExportToBigQueryEnabled(): boolean { - return this._isDeliveryMetricsExportToBigQueryEnabled; - } - - setAutoInitEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.messaging().setAutoInitEnabled(*) 'enabled' expected a boolean value.", - ); - } - - this._isAutoInitEnabled = enabled; - return this.native.setAutoInitEnabled(enabled); - } - - getInitialNotification(): Promise { - return this.native.getInitialNotification().then((value: RemoteMessage | null) => { - if (value) { - return value; - } - return null; - }); - } - - getDidOpenSettingsForNotification(): Promise { - if (!isIOS) return Promise.resolve(false); - return this.native.getDidOpenSettingsForNotification().then((value: boolean) => value); - } - - getIsHeadless(): Promise { - return this.native.getIsHeadless(); - } - - getToken(options?: { appName?: string; senderId?: string }): Promise { - if (!isUndefined(options?.appName) && !isString(options?.appName)) { - throw new Error("firebase.messaging().getToken(*) 'appName' expected a string."); - } - - if (!isUndefined(options?.senderId) && !isString(options?.senderId)) { - throw new Error("firebase.messaging().getToken(*) 'senderId' expected a string."); - } - - const appName = options?.appName || this.app.name; - const senderId = options?.senderId || this.app.options.messagingSenderId; - - return this.native.getToken(appName, senderId); - } - - deleteToken(options?: { appName?: string; senderId?: string }): Promise { - if (!isUndefined(options?.appName) && !isString(options?.appName)) { - throw new Error("firebase.messaging().deleteToken(*) 'appName' expected a string."); - } - - if (!isUndefined(options?.senderId) && !isString(options?.senderId)) { - throw new Error("firebase.messaging().deleteToken(*) 'senderId' expected a string."); - } - - const appName = options?.appName || this.app.name; - const senderId = options?.senderId || this.app.options.messagingSenderId; - - return this.native.deleteToken(appName, senderId); - } - - onMessage(listener: (message: RemoteMessage) => any): () => void { - if (!isFunction(listener)) { - throw new Error("firebase.messaging().onMessage(*) 'listener' expected a function."); - } - - const subscription = this.emitter.addListener('messaging_message_received', listener); - return () => subscription.remove(); - } - - onNotificationOpenedApp(listener: (message: RemoteMessage) => any): () => void { - if (!isFunction(listener)) { - throw new Error( - "firebase.messaging().onNotificationOpenedApp(*) 'listener' expected a function.", - ); - } - - const subscription = this.emitter.addListener('messaging_notification_opened', listener); - return () => subscription.remove(); - } - - onTokenRefresh(listener: (token: string) => any): () => void { - if (!isFunction(listener)) { - throw new Error("firebase.messaging().onTokenRefresh(*) 'listener' expected a function."); - } - - const subscription = this.emitter.addListener( - 'messaging_token_refresh', - (event: { token: string }) => { - const { token } = event; - listener(token); - }, - ); - return () => subscription.remove(); - } - - /** - * @platform ios - * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or - * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission - * requests instead. These APIs will be removed in a future major release. - * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. - */ - requestPermission(permissions?: IOSPermissions): Promise { - if (isAndroid) { - return Promise.resolve(AuthorizationStatus.AUTHORIZED); - } - - const defaultPermissions: IOSPermissions = { - alert: true, - announcement: false, - badge: true, - carPlay: true, - provisional: false, - sound: true, - criticalAlert: false, - providesAppNotificationSettings: false, - }; - - if (!permissions) { - return this.native.requestPermission(defaultPermissions); - } - - if (!isObject(permissions)) { - throw new Error('firebase.messaging().requestPermission(*) expected an object value.'); - } - - Object.entries(permissions).forEach(([key, value]) => { - if (!hasOwnProperty(defaultPermissions, key)) { - throw new Error( - `firebase.messaging().requestPermission(*) unexpected key "${key}" provided to permissions object.`, - ); - } - - if (!isBoolean(value)) { - throw new Error( - `firebase.messaging().requestPermission(*) the permission "${key}" expected a boolean value.`, - ); - } - - (defaultPermissions as any)[key] = value; - }); - - return this.native.requestPermission(defaultPermissions); - } - - registerDeviceForRemoteMessages(): Promise { - if (isAndroid) { - return Promise.resolve(); - } - - const autoRegister = this.firebaseJson['messaging_ios_auto_register_for_remote_messages']; - if (autoRegister === undefined || autoRegister === true) { - // eslint-disable-next-line no-console - console.warn( - `Usage of "messaging().registerDeviceForRemoteMessages()" is not required. You only need to register if auto-registration is disabled in your 'firebase.json' configuration file via the 'messaging_ios_auto_register_for_remote_messages' property.`, - ); - } - - this._isRegisteredForRemoteNotifications = true; - return this.native.registerForRemoteNotifications(); - } - - /** - * @platform ios - */ - unregisterDeviceForRemoteMessages(): Promise { - if (isAndroid) { - return Promise.resolve(); - } - this._isRegisteredForRemoteNotifications = false; - return this.native.unregisterForRemoteNotifications(); - } - - /** - * @platform ios - */ - getAPNSToken(): Promise { - if (isAndroid) { - return Promise.resolve(null); - } - return this.native.getAPNSToken(); - } - - /** - * @platform ios - */ - setAPNSToken(token: string, type?: string): Promise { - if (isUndefined(token) || !isString(token)) { - throw new Error("firebase.messaging().setAPNSToken(*) 'token' expected a string value."); - } - - if (!isUndefined(type) && (!isString(type) || !['prod', 'sandbox', 'unknown'].includes(type))) { - throw new Error( - "firebase.messaging().setAPNSToken(*) 'type' expected one of 'prod', 'sandbox', or 'unknown'.", - ); - } - - if (isAndroid) { - return Promise.resolve(); - } - - return this.native.setAPNSToken(token, type); - } - - /** - * @deprecated Use {@link https://github.com/zoontek/react-native-permissions react-native-permissions} or - * {@link https://docs.expo.dev/versions/latest/sdk/notifications/ expo-notifications} for notification permission - * requests instead. These APIs will be removed in a future major release. - * See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. - */ - hasPermission(): Promise { - return this.native.hasPermission(); - } - - // https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService.html#public-void-ondeletedmessages- - onDeletedMessages(listener: () => void): () => void { - if (!isFunction(listener)) { - throw new Error("firebase.messaging().onDeletedMessages(*) 'listener' expected a function."); - } - - const subscription = this.emitter.addListener('messaging_message_deleted', listener); - return () => subscription.remove(); - } - - // https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService.html#onMessageSent(java.lang.String) - onMessageSent(listener: (messageId: string) => any): () => void { - if (!isFunction(listener)) { - throw new Error("firebase.messaging().onMessageSent(*) 'listener' expected a function."); - } - - const subscription = this.emitter.addListener('messaging_message_sent', listener); - return () => { - subscription.remove(); - }; - } - - // https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService.html#onSendError(java.lang.String,%20java.lang.Exception) - onSendError( - listener: (evt: { messageId: string; error: ReactNativeFirebase.NativeFirebaseError }) => any, - ): () => void { - if (!isFunction(listener)) { - throw new Error("firebase.messaging().onSendError(*) 'listener' expected a function."); - } - - const subscription = this.emitter.addListener('messaging_message_send_error', listener); - return () => subscription.remove(); - } - - /** - * Set a handler that will be called when a message is received while the app is in the background. - * Should be called before the app is registered in `AppRegistry`, for example in `index.js`. - * An app is considered to be in the background if no active window is displayed. - * @param handler called with an argument of type messaging.RemoteMessage that must be async and return a Promise - */ - setBackgroundMessageHandler(handler: (message: RemoteMessage) => Promise): void { - if (!isFunction(handler)) { - throw new Error( - "firebase.messaging().setBackgroundMessageHandler(*) 'handler' expected a function.", - ); - } - - backgroundMessageHandler = handler; - if (isIOS) { - this.native.signalBackgroundMessageHandlerSet(); - } - } - - setOpenSettingsForNotificationsHandler(handler: (message: RemoteMessage) => any): void { - if (!isIOS) { - return; - } - - if (!isFunction(handler)) { - throw new Error( - "firebase.messaging().setOpenSettingsForNotificationsHandler(*) 'handler' expected a function.", - ); - } - - openSettingsForNotificationHandler = handler; - } - - sendMessage(remoteMessage: RemoteMessage): Promise { - if (isIOS) { - throw new Error(`firebase.messaging().sendMessage() is only supported on Android devices.`); - } - let options; - try { - const senderId = this.app.options.messagingSenderId; - if (!senderId) { - throw new Error("'messagingSenderId' is required in Firebase app options."); - } - options = remoteMessageOptions(senderId, remoteMessage); - } catch (e: any) { - throw new Error(`firebase.messaging().sendMessage(*) ${e.message}.`); - } - - return this.native.sendMessage(options); - } - - subscribeToTopic(topic: string): Promise { - if (!isString(topic)) { - throw new Error("firebase.messaging().subscribeToTopic(*) 'topic' expected a string value."); - } - - if (topic.indexOf('/') > -1) { - throw new Error('firebase.messaging().subscribeToTopic(*) \'topic\' must not include "/".'); - } - - return this.native.subscribeToTopic(topic); - } - - unsubscribeFromTopic(topic: string): Promise { - if (!isString(topic)) { - throw new Error( - "firebase.messaging().unsubscribeFromTopic(*) 'topic' expected a string value.", - ); - } - - if (topic.indexOf('/') > -1) { - throw new Error( - 'firebase.messaging().unsubscribeFromTopic(*) \'topic\' must not include "/".', - ); - } - - return this.native.unsubscribeFromTopic(topic); - } - - /** - * unsupported - */ - - useServiceWorker(): void { - // eslint-disable-next-line no-console - console.warn( - 'firebase.messaging().useServiceWorker() is not supported on react-native-firebase.', - ); - } - - usePublicVapidKey(): void { - // eslint-disable-next-line no-console - console.warn( - 'firebase.messaging().usePublicVapidKey() is not supported on react-native-firebase.', - ); - } - - setDeliveryMetricsExportToBigQuery(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.messaging().setDeliveryMetricsExportToBigQuery(*) 'enabled' expected a boolean value.", - ); - } - - this._isDeliveryMetricsExportToBigQueryEnabled = enabled; - return this.native.setDeliveryMetricsExportToBigQuery(enabled); - } - - setNotificationDelegationEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.messaging().setNotificationDelegationEnabled(*) 'enabled' expected a boolean value.", - ); - } - - this._isNotificationDelegationEnabled = enabled; - if (isIOS) { - return Promise.resolve(); - } - - return this.native.setNotificationDelegationEnabled(enabled); - } - - async isSupported(): Promise { - if (isAndroid) { - const firebase = getFirebaseRoot(); - const app = this.app; - // @ts-ignore - secret "app" argument to avoid deprecation warning when getApp() is called under the hood - playServicesAvailability = firebase.utils(app).playServicesAvailability; - return playServicesAvailability.isAvailable; - } - // Always return "true" for iOS. Web will be implemented when it is supported - return true; - } -} - -// import { SDK_VERSION } from '@react-native-firebase/messaging'; -export const SDK_VERSION = version; - -// import messaging from '@react-native-firebase/messaging'; -// messaging().X(...); -const messagingNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: [ - 'messaging_token_refresh', - 'messaging_message_sent', - 'messaging_message_deleted', - 'messaging_message_received', - 'messaging_message_send_error', - 'messaging_notification_opened', - ...(isIOS - ? ['messaging_message_received_background', 'messaging_settings_for_notification_opened'] - : []), - ], - hasMultiAppSupport: false, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseMessagingModule, -}); - -type MessagingNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - Messaging, - Statics -> & { - messaging: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -export default messagingNamespace as unknown as MessagingNamespace; - -// import messaging, { firebase } from '@react-native-firebase/messaging'; -// messaging().X(...); -// firebase.messaging().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'messaging', - Messaging, - Statics, - false - >; diff --git a/packages/messaging/lib/statics.ts b/packages/messaging/lib/statics.ts index cd299afcdc..9f4d2774b9 100644 --- a/packages/messaging/lib/statics.ts +++ b/packages/messaging/lib/statics.ts @@ -1,4 +1,4 @@ -// shared between namespaced and modular API +// Authorization and notification statics for modular API exports /** * Authorization status values returned by the deprecated permission APIs. diff --git a/packages/messaging/lib/types/internal.ts b/packages/messaging/lib/types/internal.ts new file mode 100644 index 0000000000..02a32bb31f --- /dev/null +++ b/packages/messaging/lib/types/internal.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import type { IOSPermissions, RemoteMessage } from './messaging'; + +/** + * Wrapped native module contract for `RNFBMessagingModule`. + */ +export interface RNFBMessagingModule { + isAutoInitEnabled?: boolean; + isDeliveryMetricsExportToBigQueryEnabled?: boolean; + isRegisteredForRemoteNotifications?: boolean; + isNotificationDelegationEnabled?: boolean; + setAutoInitEnabled(enabled: boolean): Promise; + getInitialNotification(): Promise; + getDidOpenSettingsForNotification(): Promise; + getIsHeadless(): Promise; + getToken(appName: string, senderId: string): Promise; + deleteToken(appName: string, senderId: string): Promise; + requestPermission(permissions: IOSPermissions): Promise; + registerForRemoteNotifications(): Promise; + unregisterForRemoteNotifications(): Promise; + getAPNSToken(): Promise; + setAPNSToken(token: string, type?: string): Promise; + hasPermission(): Promise; + completeNotificationProcessing(): void; + signalBackgroundMessageHandlerSet(): void; + sendMessage(options: Record): Promise; + subscribeToTopic(topic: string): Promise; + unsubscribeFromTopic(topic: string): Promise; + setDeliveryMetricsExportToBigQuery(enabled: boolean): Promise; + setNotificationDelegationEnabled(enabled: boolean): Promise; +} + +declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { + interface ReactNativeFirebaseNativeModules { + RNFBMessagingModule: RNFBMessagingModule; + } +} diff --git a/packages/messaging/lib/types/messaging.ts b/packages/messaging/lib/types/messaging.ts index 02fab12b18..e67d534619 100644 --- a/packages/messaging/lib/types/messaging.ts +++ b/packages/messaging/lib/types/messaging.ts @@ -16,11 +16,6 @@ */ import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import { - AuthorizationStatus as AuthorizationStatusConst, - NotificationAndroidPriority as NotificationAndroidPriorityConst, - NotificationAndroidVisibility as NotificationAndroidVisibilityConst, -} from '../statics'; // ============ Types ============ @@ -726,70 +721,3 @@ export interface Messaging extends ReactNativeFirebase.FirebaseModule { */ isSupported(): Promise; } - -// ============ Statics Interface ============ - -/** - * Static properties available on firebase.messaging - */ - -export interface Statics { - SDK_VERSION: string; - /** @deprecated See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. */ - AuthorizationStatus: typeof AuthorizationStatusConst; - NotificationAndroidPriority: typeof NotificationAndroidPriorityConst; - NotificationAndroidVisibility: typeof NotificationAndroidVisibilityConst; -} - -// ============ Module Augmentation ============ -/* eslint-disable @typescript-eslint/no-namespace */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - interface Module { - messaging: FirebaseModuleWithStaticsAndApp; - } - interface FirebaseApp { - messaging(): Messaging; - } - } -} - -// ============ Backwards Compatibility Namespace ============ - -/* eslint-disable @typescript-eslint/no-namespace */ -type _Messaging = Messaging; -type _MessagingStatics = Statics; -type _RemoteMessage = RemoteMessage; -type _MessagePriority = MessagePriority; -type _FcmOptions = FcmOptions; -type _NativeTokenOptions = NativeTokenOptions; -type _GetTokenOptions = GetTokenOptions; -type _Notification = Notification; -type _NotificationPayload = NotificationPayload; -type _NotificationIOSCriticalSound = NotificationIOSCriticalSound; -type _NotificationAndroidPriority = NotificationAndroidPriority; -type _NotificationAndroidVisibility = NotificationAndroidVisibility; -type _IOSPermissions = IOSPermissions; -type _AuthorizationStatus = AuthorizationStatus; -type _SendErrorEvent = SendErrorEvent; - -export namespace FirebaseMessagingTypes { - export type Module = _Messaging; - export type Statics = _MessagingStatics; - export type RemoteMessage = _RemoteMessage; - export type MessagePriority = _MessagePriority; - export type FcmOptions = _FcmOptions; - export type NativeTokenOptions = _NativeTokenOptions; - export type GetTokenOptions = _GetTokenOptions; - export type Notification = _Notification; - export type NotificationPayload = _NotificationPayload; - export type NotificationIOSCriticalSound = _NotificationIOSCriticalSound; - export type NotificationAndroidPriority = _NotificationAndroidPriority; - export type NotificationAndroidVisibility = _NotificationAndroidVisibility; - /** @deprecated See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. */ - export type IOSPermissions = _IOSPermissions; - /** @deprecated See {@link https://github.com/invertase/react-native-firebase/issues/6283 #6283}. */ - export type AuthorizationStatus = _AuthorizationStatus; - export type SendErrorEvent = _SendErrorEvent; -} -/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/messaging/type-test.ts b/packages/messaging/type-test.ts index 925dd5c46a..d111e32af0 100644 --- a/packages/messaging/type-test.ts +++ b/packages/messaging/type-test.ts @@ -1,6 +1,5 @@ -import messaging, { - firebase, - FirebaseMessagingTypes, +import { getApp } from '@react-native-firebase/app'; +import { getMessaging, deleteToken, getToken, @@ -34,226 +33,33 @@ import messaging, { AuthorizationStatus, NotificationAndroidPriority, NotificationAndroidVisibility, + SDK_VERSION, type Messaging, + type RemoteMessage, + type SendErrorEvent, } from '.'; +import type { AuthorizationStatus as MessagingAuthStatus } from './lib/types/messaging'; -console.log(messaging().app); - -// checks module exists at root -console.log(firebase.messaging().app.name); -console.log(firebase.messaging().isAutoInitEnabled); -console.log(firebase.messaging().isDeviceRegisteredForRemoteMessages); -console.log(firebase.messaging().isNotificationDelegationEnabled); -console.log(firebase.messaging().isDeliveryMetricsExportToBigQueryEnabled); - -// checks module exists at app level -console.log(firebase.app().messaging().app.name); -console.log(firebase.app().messaging().isAutoInitEnabled); - -const messagingInstance2: Messaging = firebase.messaging(); -console.log(messagingInstance2.app.name); - -// checks statics exist -console.log(firebase.messaging.SDK_VERSION); -console.log(firebase.messaging.AuthorizationStatus.AUTHORIZED); -console.log(firebase.messaging.AuthorizationStatus.DENIED); -console.log(firebase.messaging.AuthorizationStatus.NOT_DETERMINED); -console.log(firebase.messaging.AuthorizationStatus.PROVISIONAL); -console.log(firebase.messaging.NotificationAndroidPriority.PRIORITY_LOW); -console.log(firebase.messaging.NotificationAndroidPriority.PRIORITY_HIGH); -console.log(firebase.messaging.NotificationAndroidVisibility.VISIBILITY_PRIVATE); -console.log(firebase.messaging.NotificationAndroidVisibility.VISIBILITY_PUBLIC); - -// checks statics exist on defaultExport -console.log(messaging.firebase.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks default export supports app arg -console.log(messaging().app.name); - -// checks Module instance APIs -const messagingInstance = firebase.messaging(); -console.log(messagingInstance.app.name); -console.log(messagingInstance.isAutoInitEnabled); -console.log(messagingInstance.isDeviceRegisteredForRemoteMessages); -console.log(messagingInstance.isNotificationDelegationEnabled); -console.log(messagingInstance.isDeliveryMetricsExportToBigQueryEnabled); - -messagingInstance.setAutoInitEnabled(false).then(() => { - console.log('Auto init disabled'); -}); - -messagingInstance - .getInitialNotification() - .then((message: FirebaseMessagingTypes.RemoteMessage | null) => { - if (message) { - console.log(message.data); - } - }); - -messagingInstance.getDidOpenSettingsForNotification().then((opened: boolean) => { - console.log(opened); -}); - -messagingInstance.getIsHeadless().then((isHeadless: boolean) => { - console.log(isHeadless); -}); - -messagingInstance.getToken().then((token: string) => { - console.log(token); -}); - -messagingInstance.getToken({ appName: 'test', senderId: 'test-sender' }).then((token: string) => { - console.log(token); -}); - -messagingInstance.deleteToken().then(() => { - console.log('Token deleted'); -}); - -messagingInstance.deleteToken({ appName: 'test', senderId: 'test-sender' }).then(() => { - console.log('Token deleted with options'); -}); - -const unsubscribeOnMessage = messagingInstance.onMessage( - (message: FirebaseMessagingTypes.RemoteMessage) => { - console.log(message.data); - }, -); -unsubscribeOnMessage(); - -const unsubscribeOnNotificationOpenedApp = messagingInstance.onNotificationOpenedApp( - (message: FirebaseMessagingTypes.RemoteMessage) => { - console.log(message.data); - }, -); -unsubscribeOnNotificationOpenedApp(); - -const unsubscribeOnTokenRefresh = messagingInstance.onTokenRefresh((token: string) => { - console.log(token); -}); -unsubscribeOnTokenRefresh(); - -messagingInstance.requestPermission().then((status: FirebaseMessagingTypes.AuthorizationStatus) => { - console.log(status); -}); - -messagingInstance - .requestPermission({ - alert: true, - badge: true, - sound: true, - }) - .then((status: FirebaseMessagingTypes.AuthorizationStatus) => { - console.log(status); - }); - -messagingInstance.registerDeviceForRemoteMessages().then(() => { - console.log('Device registered'); -}); - -messagingInstance.unregisterDeviceForRemoteMessages().then(() => { - console.log('Device unregistered'); -}); - -messagingInstance.getAPNSToken().then((token: string | null) => { - console.log(token); -}); - -messagingInstance.setAPNSToken('test-token', 'prod').then(() => { - console.log('APNS token set'); -}); - -messagingInstance.hasPermission().then((status: FirebaseMessagingTypes.AuthorizationStatus) => { - console.log(status); -}); - -const unsubscribeOnDeletedMessages = messagingInstance.onDeletedMessages(() => { - console.log('Messages deleted'); -}); -unsubscribeOnDeletedMessages(); - -const unsubscribeOnMessageSent = messagingInstance.onMessageSent((messageId: string) => { - console.log(messageId); -}); -unsubscribeOnMessageSent(); - -const unsubscribeOnSendError = messagingInstance.onSendError( - (evt: FirebaseMessagingTypes.SendErrorEvent) => { - console.log(evt.messageId); - console.log(evt.error); - }, -); -unsubscribeOnSendError(); - -messagingInstance.setBackgroundMessageHandler( - async (message: FirebaseMessagingTypes.RemoteMessage) => { - console.log(message.data); - return Promise.resolve(); - }, -); - -messagingInstance.setOpenSettingsForNotificationsHandler( - (message: FirebaseMessagingTypes.RemoteMessage) => { - console.log(message.data); - }, -); - -messagingInstance - .sendMessage({ - data: { key: 'value' }, - notification: { - title: 'Test', - body: 'Test body', - }, - fcmOptions: {}, - }) - .then(() => { - console.log('Message sent'); - }); - -messagingInstance.subscribeToTopic('test-topic').then(() => { - console.log('Subscribed to topic'); -}); - -messagingInstance.unsubscribeFromTopic('test-topic').then(() => { - console.log('Unsubscribed from topic'); -}); - -messagingInstance.setDeliveryMetricsExportToBigQuery(true).then(() => { - console.log('Delivery metrics enabled'); -}); - -messagingInstance.setNotificationDelegationEnabled(true).then(() => { - console.log('Notification delegation enabled'); -}); - -messagingInstance.isSupported().then((supported: boolean) => { - console.log(supported); -}); - -// checks modular API functions const modularMessaging1 = getMessaging(); console.log(modularMessaging1.app.name); -const modularMessaging2 = getMessaging(firebase.app()); +const modularMessaging2 = getMessaging(getApp()); console.log(modularMessaging2.app.name); +const typedMessaging: Messaging = modularMessaging1; +console.log(typedMessaging.app.name); + console.log(isAutoInitEnabled(modularMessaging1)); setAutoInitEnabled(modularMessaging1, false).then(() => { console.log('Modular auto init disabled'); }); -getInitialNotification(modularMessaging1).then( - (message: FirebaseMessagingTypes.RemoteMessage | null) => { - if (message) { - console.log(message.data); - } - }, -); +getInitialNotification(modularMessaging1).then((message: RemoteMessage | null) => { + if (message) { + console.log(message.data); + } +}); getDidOpenSettingsForNotification(modularMessaging1).then((opened: boolean) => { console.log(opened); @@ -279,17 +85,14 @@ deleteToken(modularMessaging1, { appName: 'test', senderId: 'test-sender' }).the console.log('Modular token deleted with options'); }); -const modularUnsubscribeOnMessage = onMessage( - modularMessaging1, - (message: FirebaseMessagingTypes.RemoteMessage) => { - console.log(message.data); - }, -); +const modularUnsubscribeOnMessage = onMessage(modularMessaging1, (message: RemoteMessage) => { + console.log(message.data); +}); modularUnsubscribeOnMessage(); const modularUnsubscribeOnNotificationOpenedApp = onNotificationOpenedApp( modularMessaging1, - (message: FirebaseMessagingTypes.RemoteMessage) => { + (message: RemoteMessage) => { console.log(message.data); }, ); @@ -300,7 +103,7 @@ const modularUnsubscribeOnTokenRefresh = onTokenRefresh(modularMessaging1, (toke }); modularUnsubscribeOnTokenRefresh(); -requestPermission(modularMessaging1).then((status: FirebaseMessagingTypes.AuthorizationStatus) => { +requestPermission(modularMessaging1).then((status: MessagingAuthStatus) => { console.log(status); }); @@ -308,7 +111,7 @@ requestPermission(modularMessaging1, { alert: true, badge: true, sound: true, -}).then((status: FirebaseMessagingTypes.AuthorizationStatus) => { +}).then((status: MessagingAuthStatus) => { console.log(status); }); @@ -330,7 +133,7 @@ setAPNSToken(modularMessaging1, 'modular-test-token', 'sandbox').then(() => { console.log('Modular APNS token set'); }); -hasPermission(modularMessaging1).then((status: FirebaseMessagingTypes.AuthorizationStatus) => { +hasPermission(modularMessaging1).then((status: MessagingAuthStatus) => { console.log(status); }); @@ -346,7 +149,7 @@ modularUnsubscribeOnMessageSent(); const modularUnsubscribeOnSendError = onSendError( modularMessaging1, - (evt: FirebaseMessagingTypes.SendErrorEvent) => { + (evt: SendErrorEvent) => { console.log(evt.messageId); console.log(evt.error); }, @@ -355,18 +158,15 @@ modularUnsubscribeOnSendError(); setBackgroundMessageHandler( modularMessaging1, - async (message: FirebaseMessagingTypes.RemoteMessage) => { + async (message: RemoteMessage) => { console.log(message.data); return Promise.resolve(); }, ); -setOpenSettingsForNotificationsHandler( - modularMessaging1, - (message: FirebaseMessagingTypes.RemoteMessage) => { - console.log(message.data); - }, -); +setOpenSettingsForNotificationsHandler(modularMessaging1, (message: RemoteMessage) => { + console.log(message.data); +}); sendMessage(modularMessaging1, { data: { modularKey: 'modularValue' }, @@ -403,7 +203,9 @@ experimentalSetDeliveryMetricsExportedToBigQueryEnabled(modularMessaging1, true) console.log('Modular delivery metrics enabled'); }); -// checks modular statics exports console.log(AuthorizationStatus.AUTHORIZED); console.log(NotificationAndroidPriority.PRIORITY_DEFAULT); console.log(NotificationAndroidVisibility.VISIBILITY_PUBLIC); + +const sdkVersion: string = SDK_VERSION; +console.log(sdkVersion); diff --git a/packages/messaging/typedoc.json b/packages/messaging/typedoc.json index 40abe04f95..13d1e95610 100644 --- a/packages/messaging/typedoc.json +++ b/packages/messaging/typedoc.json @@ -1,22 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/messaging.ts"], - "tsconfig": "tsconfig.json", - "intentionallyNotExported": [ - "_AuthorizationStatus", - "_FcmOptions", - "_GetTokenOptions", - "_IOSPermissions", - "_MessagePriority", - "_Messaging", - "_NativeTokenOptions", - "_Notification", - "_NotificationAndroidPriority", - "_NotificationAndroidVisibility", - "_NotificationIOSCriticalSound", - "_NotificationPayload", - "_RemoteMessage", - "_SendErrorEvent", - "_MessagingStatics" - ] + "entryPoints": ["lib/index.ts", "lib/types/messaging.ts"], + "tsconfig": "tsconfig.json" } diff --git a/packages/ml/__tests__/ml.test.ts b/packages/ml/__tests__/ml.test.ts index caa5fbc789..d09fdb2511 100644 --- a/packages/ml/__tests__/ml.test.ts +++ b/packages/ml/__tests__/ml.test.ts @@ -1,26 +1,8 @@ -import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; -import { firebase, getML } from '../lib'; +import { getML } from '../lib'; describe('ml()', function () { - describe('namespace', function () { - beforeAll(async () => { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async () => { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.ml).toBeDefined(); - expect(app.ml().app).toEqual(app); - }); - }); - describe('modular', function () { it('`getML` function is properly exposed to end user', function () { expect(getML).toBeDefined(); diff --git a/packages/ml/e2e/ml.e2e.js b/packages/ml/e2e/ml.e2e.js index f9b4152769..01c3a5b0ba 100644 --- a/packages/ml/e2e/ml.e2e.js +++ b/packages/ml/e2e/ml.e2e.js @@ -16,36 +16,6 @@ */ describe('ml()', function () { - describe('v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.ml); - app.ml().app.should.equal(app); - }); - - it('supports multiple apps', async function () { - firebase.ml().app.name.should.equal('[DEFAULT]'); - - firebase - .ml(firebase.app('secondaryFromNative')) - .app.name.should.equal('secondaryFromNative'); - - firebase.app('secondaryFromNative').ml().app.name.should.equal('secondaryFromNative'); - }); - }); - }); - describe('modular', function () { it('supports multiple apps', function () { const { getApp } = modular; diff --git a/packages/ml/lib/index.ts b/packages/ml/lib/index.ts index ed29b146ff..ccf1a77877 100644 --- a/packages/ml/lib/index.ts +++ b/packages/ml/lib/index.ts @@ -15,11 +15,42 @@ * */ -export * from './modular'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import './types/internal'; +import type { FirebaseApp, FirebaseML } from './types/ml'; +import { version } from './version'; -export type { FirebaseApp, FirebaseML } from './types/ml'; +const nativeModuleName = 'RNFBMLModule'; + +class FirebaseMLModule extends FirebaseModule {} + +const config: ModuleConfig = { + namespace: 'ml', + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: false, +}; -export type { FirebaseMLTypes } from './types/namespaced'; +/** + * RN Firebase package version string exported from the modular entry point. + * + * The firebase-js-sdk does not ship a `firebase/ml` modular entry point or `SDK_VERSION` export. + */ +export const SDK_VERSION = version; -export * from './namespaced'; -export { default } from './namespaced'; +/** + * Returns the {@link FirebaseML} instance for the default or given {@link FirebaseApp}. + * + * @param app - The Firebase `FirebaseApp` to use. When omitted, the default app is used. + * @returns The ML service instance for that app. + */ +export function getML(app?: FirebaseApp): FirebaseML { + return getOrCreateModularInstance(FirebaseMLModule, config, app) as unknown as FirebaseML; +} + +export type { FirebaseApp, FirebaseML } from './types/ml'; diff --git a/packages/ml/lib/modular.ts b/packages/ml/lib/modular.ts deleted file mode 100644 index c43abef6a1..0000000000 --- a/packages/ml/lib/modular.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import type { FirebaseApp, FirebaseML } from './types/ml'; - -/** - * Returns the {@link FirebaseML} instance for the default or given {@link FirebaseApp}. - * - * @param app - The Firebase `FirebaseApp` to use. When omitted, the default app is used. - * @returns The ML service instance for that app. - */ -export function getML(app?: FirebaseApp): FirebaseML { - if (app) { - return getApp(app.name).ml(); - } - return getApp().ml(); -} diff --git a/packages/ml/lib/namespaced.ts b/packages/ml/lib/namespaced.ts deleted file mode 100644 index cfb964ac92..0000000000 --- a/packages/ml/lib/namespaced.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, -} from '@react-native-firebase/app/dist/module/internal'; -import './types/internal'; -import type { FirebaseMLTypes } from './types/namespaced'; -import { version } from './version'; - -const statics = { - SDK_VERSION: version, -}; - -const namespace = 'ml'; - -const nativeModuleName = 'RNFBMLModule'; - -class FirebaseMLModule extends FirebaseModule {} - -type MLNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseMLTypes.Module, - FirebaseMLTypes.Statics -> & { - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import { SDK_VERSION } from '@react-native-firebase/ml'; -export const SDK_VERSION = version; - -const defaultExport = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: false, - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseMLModule, -}) as unknown as MLNamespace; - -export default defaultExport; - -// import ml, { firebase } from '@react-native-firebase/ml'; -// ml().X(...); -// firebase.ml().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'ml', - FirebaseMLTypes.Module, - FirebaseMLTypes.Statics, - true - >; diff --git a/packages/ml/lib/types/namespaced.ts b/packages/ml/lib/types/namespaced.ts deleted file mode 100644 index 4a59240e74..0000000000 --- a/packages/ml/lib/types/namespaced.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase ML package for React Native. - * - * #### Example 1 - * - * Access the firebase export from the `ml` package: - * - * ```js - * import { firebase } from '@react-native-firebase/ml'; - * - * // firebase.ml().X - * ``` - * - * #### Example 2 - * - * Using the default export from the `ml` package: - * - * ```js - * import ml from '@react-native-firebase/ml'; - * - * // ml().X - * ``` - * - * #### Example 3 - * - * Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/ml'; - * - * // firebase.ml().X - * ``` - * - * @firebase ml - */ -/** - * @deprecated Use the exported types directly instead. - * FirebaseMLTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseMLTypes { - /** - * @deprecated Use the exported types directly instead. FirebaseMLTypes namespace is kept for backwards compatibility. - */ - type FirebaseModule = ReactNativeFirebase.FirebaseModule; - - /** - * @deprecated Use the default export statics instead. - */ - export interface Statics { - /** @deprecated Use the default export statics instead. */ - SDK_VERSION: string; - } - - /** - * @deprecated Use the exported `FirebaseML` type instead. - */ - export interface Module extends FirebaseModule { - /** - * @deprecated Use the exported `FirebaseML` type instead. - * - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - } -} - -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - interface Module { - ml: FirebaseModuleWithStaticsAndApp; - } - - interface FirebaseApp { - ml(): FirebaseMLTypes.Module; - } - } -} -/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/ml/type-test.ts b/packages/ml/type-test.ts index dd6ff54bea..a6a1dbc9d0 100644 --- a/packages/ml/type-test.ts +++ b/packages/ml/type-test.ts @@ -1,61 +1,22 @@ -import ml, { - firebase, - getML, - SDK_VERSION, - type FirebaseApp, - type FirebaseML, - type FirebaseMLTypes, -} from '.'; +import { getApp } from '@react-native-firebase/app'; +import { getML, SDK_VERSION, type FirebaseApp, type FirebaseML } from '.'; -console.log(ml().app); - -// checks module exists at root -console.log(firebase.ml().app.name); - -// checks module exists at app level -console.log(firebase.app().ml().app.name); - -// checks statics exist -console.log(firebase.ml.SDK_VERSION); - -// checks statics exist on defaultExport -console.log(ml.firebase.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks multi-app support exists -console.log(firebase.ml(firebase.app()).app.name); - -// checks default export supports app arg -console.log(ml(firebase.app()).app.name); - -// checks Module instance APIs -const mlInstance = firebase.ml(); -console.log(mlInstance.app.name); - -// checks modular API functions +// modular API functions const modularML1 = getML(); console.log(modularML1.app.name); -const modularML2 = getML(firebase.app()); +const modularML2 = getML(getApp()); console.log(modularML2.app.name); // modular public types const modularInstance: FirebaseML = getML(); -const modularWithNamedApp: FirebaseML = getML(firebase.app()); +const modularWithNamedApp: FirebaseML = getML(getApp()); console.log(modularInstance.app.name); console.log(modularWithNamedApp.app.name); -const namedApp: FirebaseApp = firebase.app(); +const namedApp: FirebaseApp = getApp(); console.log(getML(namedApp).app.name); // named SDK_VERSION export const sdkVersion: string = SDK_VERSION; console.log(sdkVersion); - -// deprecated namespace types remain assignable from runtime values -const namespaceModule: FirebaseMLTypes.Module = firebase.ml(); -const namespaceStatics: FirebaseMLTypes.Statics = firebase.ml; -console.log(namespaceModule.app.name); -console.log(namespaceStatics.SDK_VERSION); diff --git a/packages/ml/typedoc.json b/packages/ml/typedoc.json index 2568573821..b0e011e140 100644 --- a/packages/ml/typedoc.json +++ b/packages/ml/typedoc.json @@ -1,4 +1,4 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/ml.ts"] + "entryPoints": ["lib/index.ts", "lib/types/ml.ts"] } diff --git a/packages/perf/__tests__/perf.test.ts b/packages/perf/__tests__/perf.test.ts index 133685a052..4f5501b946 100644 --- a/packages/perf/__tests__/perf.test.ts +++ b/packages/perf/__tests__/perf.test.ts @@ -1,7 +1,6 @@ -import { afterAll, beforeAll, describe, expect, it, beforeEach, jest } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; -import perf, { - firebase, +import { getPerformance, initializePerformance, trace, @@ -10,142 +9,7 @@ import perf, { startScreenTrace, } from '../lib'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; - -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; - describe('Performance Monitoring', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.perf).toBeDefined(); - expect(app.perf().app).toEqual(app); - }); - - describe('setPerformanceCollectionEnabled', function () { - it('errors if not boolean', function () { - expect(async () => { - // @ts-ignore - await perf().setPerformanceCollectionEnabled(); - }).rejects.toThrow('must be a boolean'); - }); - }); - - describe('newTrace()', function () { - it('returns an instance of Trace', function () { - const trace = perf().newTrace('invertase'); - expect(trace.constructor.name).toEqual('Trace'); - - // @ts-ignore - expect(trace._identifier).toEqual('invertase'); - }); - - it('errors if identifier not a string', function () { - try { - // @ts-ignore - perf().newTrace(1337); - return Promise.reject(new Error('Did not throw')); - } catch (e: any) { - expect(e.message).toEqual( - "firebase.perf().newTrace(*) 'identifier' must be a string with a maximum length of 100 characters.", - ); - return Promise.resolve(); - } - }); - - it('errors if identifier length > 100', function () { - try { - perf().newTrace(new Array(101).fill('i').join('')); - return Promise.reject(new Error('Did not throw')); - } catch (e: any) { - expect(e.message).toEqual( - "firebase.perf().newTrace(*) 'identifier' must be a string with a maximum length of 100 characters.", - ); - return Promise.resolve(); - } - }); - }); - - describe('newHttpMetric()', function () { - it('returns an instance of HttpMetric', async function () { - const metric = perf().newHttpMetric('https://invertase.io', 'GET'); - expect(metric.constructor.name).toEqual('HttpMetric'); - - // @ts-ignore - expect(metric._url).toEqual('https://invertase.io'); - - // @ts-ignore - expect(metric._httpMethod).toEqual('GET'); - }); - - it('errors if url not a string', async function () { - try { - // @ts-ignore - perf().newHttpMetric(1337, 7331); - return Promise.reject(new Error('Did not throw')); - } catch (e: any) { - expect(e.message).toEqual("firebase.perf().newHttpMetric(*, _) 'url' must be a string."); - return Promise.resolve(); - } - }); - - it('errors if httpMethod not a string', async function () { - try { - // @ts-ignore - perf().newHttpMetric('https://invertase.io', 1337); - return Promise.reject(new Error('Did not throw')); - } catch (e: any) { - expect(e.message).toEqual( - "firebase.perf().newHttpMetric(_, *) 'httpMethod' must be one of CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE.", - ); - return Promise.resolve(); - } - }); - - it('errors if httpMethod not a valid type', async function () { - try { - // @ts-ignore - perf().newHttpMetric('https://invertase.io', 'FIRE'); - return Promise.reject(new Error('Did not throw')); - } catch (e: any) { - expect(e.message).toEqual( - "firebase.perf().newHttpMetric(_, *) 'httpMethod' must be one of CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE.", - ); - return Promise.resolve(); - } - }); - }); - - describe('setPerformanceCollectionEnabled()', function () { - it('errors if not boolean', async function () { - try { - // @ts-ignore - await firebase.perf().setPerformanceCollectionEnabled(); - return Promise.reject(new Error('Did not throw')); - } catch (e: any) { - expect(e.message).toEqual( - "firebase.perf().setPerformanceCollectionEnabled(*) 'enabled' must be a boolean.", - ); - return Promise.resolve(); - } - }); - }); - }); - describe('modular', function () { it('`getPerformance` function is properly exposed to end user', function () { expect(getPerformance).toBeDefined(); @@ -171,69 +35,4 @@ describe('Performance Monitoring', function () { expect(startScreenTrace).toBeDefined(); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let perfV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - perfV9Deprecation = createCheckV9Deprecation(['perf']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - result: true, - constants: { - isPerformanceCollectionEnabled: true, - isInstrumentationEnabled: true, - }, - } as never), - }, - ); - }); - }); - - it('newTrace()', function () { - const perf = getPerformance(); - perfV9Deprecation( - () => trace(perf, 'invertase'), - // @ts-expect-error Combines modular and namespace API - () => perf.newTrace('invertase'), - 'newTrace', - ); - }); - - it('newHttpMetric()', function () { - const perf = getPerformance(); - perfV9Deprecation( - () => httpMetric(perf, 'https://invertase.io', 'GET'), - // @ts-expect-error Combines modular and namespace API - () => perf.newHttpMetric('https://invertase.io', 'GET'), - 'newHttpMetric', - ); - }); - - it('newScreenTrace()', function () { - const perf = getPerformance(); - perfV9Deprecation( - () => newScreenTrace(perf, 'invertase'), - // @ts-expect-error Combines modular and namespace API - () => perf.newScreenTrace('invertase'), - 'newScreenTrace', - ); - }); - - it('startScreenTrace()', function () { - const perf = getPerformance(); - perfV9Deprecation( - () => startScreenTrace(perf, 'invertase'), - // @ts-expect-error Combines modular and namespace API - () => perf.startScreenTrace('invertase'), - 'startScreenTrace', - ); - }); - }); }); diff --git a/packages/perf/e2e/HttpMetric.e2e.js b/packages/perf/e2e/HttpMetric.e2e.js index e2e27ab7a6..5c44256699 100644 --- a/packages/perf/e2e/HttpMetric.e2e.js +++ b/packages/perf/e2e/HttpMetric.e2e.js @@ -18,308 +18,6 @@ const aCoolUrl = 'https://invertase.io'; describe('HttpMetric modular', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('start()', function () { - it('correctly starts with internal flag ', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - await httpMetric.start(); - httpMetric.setHttpResponseCode(200); - should.equal(httpMetric._started, true); - await Utils.sleep(75); - await httpMetric.stop(); - }); - - it('resolves null if already started', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'POST'); - await httpMetric.start(); - should.equal(httpMetric._started, true); - httpMetric.setHttpResponseCode(200); - should.equal(await httpMetric.start(), null); - await Utils.sleep(75); - await httpMetric.stop(); - }); - }); - - describe('stop()', function () { - it('correctly stops with internal flag ', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - await httpMetric.start(); - httpMetric.setHttpResponseCode(500); - httpMetric.setRequestPayloadSize(1337); - httpMetric.setResponseContentType('application/invertase'); - httpMetric.setResponsePayloadSize(1337); - httpMetric.putAttribute('foo', 'bar'); - httpMetric.putAttribute('bar', 'foo'); - await Utils.sleep(100); - await httpMetric.stop(); - should.equal(httpMetric._stopped, true); - }); - - it('resolves null if already stopped', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'POST'); - await httpMetric.start(); - await Utils.sleep(100); - await httpMetric.stop(); - should.equal(await httpMetric.stop(), null); - }); - - it('handles floating point numbers', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'POST'); - await httpMetric.start(); - const floatingPoint = 500.447553; - - httpMetric.setHttpResponseCode(floatingPoint); - httpMetric.setResponsePayloadSize(floatingPoint); - httpMetric.setRequestPayloadSize(floatingPoint); - - await Utils.sleep(100); - await httpMetric.stop(); - }); - }); - - // describe('removeAttribute()', function () { - // it('errors if not a string', async function () { - // const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - // try { - // httpMetric.putAttribute('inver', 'tase'); - // httpMetric.removeAttribute(13377331); - // return Promise.reject(new Error('Did not throw')); - // } catch (e) { - // e.message.should.containEql('must be a string'); - // return Promise.resolve(); - // } - // }); - - // it('removes an attribute', async function () { - // const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - // httpMetric.putAttribute('inver', 'tase'); - // const value = httpMetric.getAttribute('inver'); - // should.equal(value, 'tase'); - // httpMetric.removeAttribute('inver'); - // const value2 = httpMetric.getAttribute('inver'); - // should.equal(value2, null); - // }); - // }); - - describe('getAttribute()', function () { - it('should return null if attribute does not exist', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - const value = httpMetric.getAttribute('inver'); - should.equal(value, null); - }); - - it('should return an attribute string value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.putAttribute('inver', 'tase'); - const value = httpMetric.getAttribute('inver'); - should.equal(value, 'tase'); - }); - - it('errors if attribute name is not a string', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.getAttribute(1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - }); - - describe('putAttribute()', function () { - it('sets an attribute string value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.putAttribute('inver', 'tase'); - const value = httpMetric.getAttribute('inver'); - value.should.equal('tase'); - }); - - it('errors if attribute name is not a string', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.putAttribute(1337, 'invertase'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('errors if attribute value is not a string', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.putAttribute('invertase', 1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('errors if attribute name is greater than 40 characters', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.putAttribute(new Array(41).fill('1').join(''), 1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('a maximum length of 40 characters'); - return Promise.resolve(); - } - }); - - it('errors if attribute value is greater than 100 characters', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.putAttribute('invertase', new Array(101).fill('1').join('')); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('a maximum length of 100 characters'); - return Promise.resolve(); - } - }); - - it('errors if more than 5 attributes are put', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - - httpMetric.putAttribute('invertase1', '1337'); - httpMetric.putAttribute('invertase2', '1337'); - httpMetric.putAttribute('invertase3', '1337'); - httpMetric.putAttribute('invertase4', '1337'); - httpMetric.putAttribute('invertase5', '1337'); - - try { - httpMetric.putAttribute('invertase6', '1337'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('maximum number of attributes'); - return Promise.resolve(); - } - }); - }); - - // it('getAttributes()', async () => { - // const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - // httpMetric.putAttribute('inver', 'tase'); - // httpMetric.putAttribute('tase', 'baz'); - // const value = httpMetric.getAttributes(); - // JSON.parse(JSON.stringify(value)).should.deepEqual({ - // inver: 'tase', - // tase: 'baz', - // }); - // }); - - describe('setHttpResponseCode()', function () { - it('sets number value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setHttpResponseCode(500); - should.equal(httpMetric._httpResponseCode, 500); - }); - - it('sets a null value to clear value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setHttpResponseCode(null); - should.equal(httpMetric._httpResponseCode, null); - }); - - it('errors if not a number or null value', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setHttpResponseCode('500'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a number or null'); - return Promise.resolve(); - } - }); - }); - - describe('setRequestPayloadSize()', function () { - it('sets number value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setRequestPayloadSize(13377331); - should.equal(httpMetric._requestPayloadSize, 13377331); - }); - - it('sets a null value to clear value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setRequestPayloadSize(null); - should.equal(httpMetric._requestPayloadSize, null); - }); - - it('errors if not a number or null value', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setRequestPayloadSize('1337'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a number or null'); - return Promise.resolve(); - } - }); - }); - - describe('setResponsePayloadSize()', function () { - it('sets number value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setResponsePayloadSize(13377331); - should.equal(httpMetric._responsePayloadSize, 13377331); - }); - - it('sets a null value to clear value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setResponsePayloadSize(null); - should.equal(httpMetric._responsePayloadSize, null); - }); - - it('errors if not a number or null value', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setResponsePayloadSize('1337'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a number or null'); - return Promise.resolve(); - } - }); - }); - - describe('setResponseContentType()', function () { - it('sets string value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setResponseContentType('application/invertase'); - should.equal(httpMetric._responseContentType, 'application/invertase'); - }); - - it('sets a null value to clear value', async function () { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setResponseContentType(null); - should.equal(httpMetric._responseContentType, null); - }); - - it('errors if not a string or null value', async function () { - try { - const httpMetric = firebase.perf().newHttpMetric(aCoolUrl, 'GET'); - httpMetric.setResponseContentType(1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string or null'); - return Promise.resolve(); - } - }); - }); - }); - describe('modular', function () { describe('start()', function () { it('correctly starts with internal flag ', async function () { diff --git a/packages/perf/e2e/Trace.e2e.js b/packages/perf/e2e/Trace.e2e.js index c461d66f19..1fe1154547 100644 --- a/packages/perf/e2e/Trace.e2e.js +++ b/packages/perf/e2e/Trace.e2e.js @@ -16,329 +16,6 @@ */ describe('Trace modular', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('start()', function () { - it('correctly starts with internal flag ', async function () { - const trace = firebase.perf().newTrace('invertase'); - await trace.start(); - should.equal(trace._started, true); - trace.putAttribute('foo', 'bar'); - trace.putMetric('stars', 9001); - await Utils.sleep(125); - await trace.stop(); - }); - - it('resolves null if already started', async function () { - const trace = firebase.perf().newTrace('invertase'); - await trace.start(); - should.equal(trace._started, true); - should.equal(await trace.start(), null); - await Utils.sleep(125); - await trace.stop(); - }); - }); - - describe('stop()', function () { - it('correctly stops with internal flag ', async function () { - const trace = firebase.perf().newTrace('invertase'); - await trace.start(); - trace.putAttribute('foo', 'bar'); - trace.putAttribute('bar', 'foo'); - trace.putMetric('leet', 1337); - trace.putMetric('chickens', 12); - await Utils.sleep(100); - await trace.stop(); - should.equal(trace._stopped, true); - }); - - it('resolves null if already stopped', async function () { - const trace = firebase.perf().newTrace('invertase'); - await trace.start(); - await Utils.sleep(100); - await trace.stop(); - should.equal(await trace.stop(), null); - }); - }); - // describe('removeAttribute()', function () { - // it('errors if not a string', async function () { - // const trace = firebase.perf().newTrace('invertase'); - // try { - // trace.putAttribute('inver', 'tase'); - // trace.removeAttribute(13377331); - // return Promise.reject(new Error('Did not throw')); - // } catch (e) { - // e.message.should.containEql('must be a string'); - // return Promise.resolve(); - // } - // }); - // it('removes an attribute', async function () { - // const trace = firebase.perf().newTrace('invertase'); - // trace.putAttribute('inver', 'tase'); - // const value = trace.getAttribute('inver'); - // should.equal(value, 'tase'); - // trace.removeAttribute('inver'); - // const value2 = trace.getAttribute('inver'); - // should.equal(value2, null); - // }); - // }); - describe('getAttribute()', function () { - it('should return null if attribute does not exist', async function () { - const trace = firebase.perf().newTrace('invertase'); - const value = trace.getAttribute('inver'); - should.equal(value, null); - }); - - it('should return an attribute string value', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putAttribute('inver', 'tase'); - const value = trace.getAttribute('inver'); - should.equal(value, 'tase'); - }); - - it('errors if attribute name is not a string', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.getAttribute(1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - }); - - describe('putAttribute()', function () { - it('sets an attribute string value', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putAttribute('inver', 'tase'); - const value = trace.getAttribute('inver'); - value.should.equal('tase'); - }); - - it('errors if attribute name is not a string', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.putAttribute(1337, 'invertase'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('errors if attribute value is not a string', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.putAttribute('invertase', 1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('errors if attribute name is greater than 40 characters', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.putAttribute(new Array(41).fill('1').join(''), 1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('a maximum length of 40 characters'); - return Promise.resolve(); - } - }); - - it('errors if attribute value is greater than 100 characters', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.putAttribute('invertase', new Array(101).fill('1').join('')); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('a maximum length of 100 characters'); - return Promise.resolve(); - } - }); - - it('errors if more than 5 attributes are put', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putAttribute('invertase1', '1337'); - trace.putAttribute('invertase2', '1337'); - trace.putAttribute('invertase3', '1337'); - trace.putAttribute('invertase4', '1337'); - trace.putAttribute('invertase5', '1337'); - try { - trace.putAttribute('invertase6', '1337'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('maximum number of attributes'); - return Promise.resolve(); - } - }); - }); - // it('getAttributes()', function () { - // const trace = firebase.perf().newTrace('invertase'); - // trace.putAttribute('inver', 'tase'); - // trace.putAttribute('tase', 'baz'); - // const value = trace.getAttributes(); - // JSON.parse(JSON.stringify(value)).should.deepEqual({ - // inver: 'tase', - // tase: 'baz', - // }); - // }); - - describe('removeMetric()', function () { - it('errors if name not a string', async function () { - const trace = firebase.perf().newTrace('invertase'); - try { - trace.putMetric('likes', 1337); - trace.removeMetric(13377331); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('removes a metric', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric('likes', 1337); - const value = trace.getMetric('likes'); - should.equal(value, 1337); - trace.removeMetric('likes'); - const value2 = trace.getMetric('likes'); - should.equal(value2, 0); - }); - }); - - describe('getMetric()', function () { - it('should return 0 if metric does not exist', async function () { - const trace = firebase.perf().newTrace('invertase'); - const value = trace.getMetric('likes'); - should.equal(value, 0); - }); - - it('should return an metric number value', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric('likes', 7331); - const value = trace.getMetric('likes'); - should.equal(value, 7331); - }); - - it('errors if metric name is not a string', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.getMetric(1337); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - }); - - describe('putMetric()', function () { - it('sets a metric number value', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric('likes', 9001); - const value = trace.getMetric('likes'); - value.should.equal(9001); - }); - - it('overwrites existing metric if it exists', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric('likes', 1); - trace.incrementMetric('likes', 9000); - const value = trace.getMetric('likes'); - value.should.equal(9001); - trace.putMetric('likes', 1); - const value2 = trace.getMetric('likes'); - value2.should.equal(1); - }); - - it('errors if metric name is not a string', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric(1337, 7331); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('errors if metric value is not a number', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric('likes', '1337'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a number'); - return Promise.resolve(); - } - }); - }); - - describe('incrementMetric()', function () { - it('increments a metric number value', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric('likes', 9000); - trace.incrementMetric('likes', 1); - const value = trace.getMetric('likes'); - value.should.equal(9001); - }); - - it('increments a metric even if it does not already exist', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.incrementMetric('likes', 9001); - const value = trace.getMetric('likes'); - value.should.equal(9001); - }); - - it('errors if metric name is not a string', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.incrementMetric(1337, 1); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('errors if incrementBy value is not a number', async function () { - try { - const trace = firebase.perf().newTrace('invertase'); - trace.incrementMetric('likes', '1'); - return Promise.reject(new Error('Did not throw')); - } catch (e) { - e.message.should.containEql('must be a number'); - return Promise.resolve(); - } - }); - }); - - it('getMetrics()', async function () { - const trace = firebase.perf().newTrace('invertase'); - trace.putMetric('likes', 1337); - trace.putMetric('stars', 6832); - const value = trace.getMetrics(); - JSON.parse(JSON.stringify(value)).should.deepEqual({ - likes: 1337, - stars: 6832, - }); - }); - }); - describe('modular', function () { describe('start()', function () { it('correctly starts with internal flag ', async function () { diff --git a/packages/perf/e2e/perf.e2e.js b/packages/perf/e2e/perf.e2e.js index 2be866e014..cbe678e2da 100644 --- a/packages/perf/e2e/perf.e2e.js +++ b/packages/perf/e2e/perf.e2e.js @@ -16,116 +16,6 @@ */ describe('perf() modular', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('setPerformanceCollectionEnabled()', function () { - // These depend on `tests/firebase.json` having `perf_auto_collection_enabled` set to false the first time - // The setting is persisted across restarts, reset to false after for local runs where prefs are sticky - afterEach(async function () { - await firebase.perf().setPerformanceCollectionEnabled(false); - }); - - it('true', async function () { - should.equal(firebase.perf().isPerformanceCollectionEnabled, false); - await firebase.perf().setPerformanceCollectionEnabled(true); - should.equal(firebase.perf().isPerformanceCollectionEnabled, true); - }); - - it('false', async function () { - await firebase.perf().setPerformanceCollectionEnabled(false); - should.equal(firebase.perf().isPerformanceCollectionEnabled, false); - }); - }); - - describe('instrumentationEnabled', function () { - afterEach(function () { - const perf = firebase.perf(); - perf.instrumentationEnabled = false; - }); - - it('true', function () { - const perf = firebase.perf(); - - perf.instrumentationEnabled = true; - - should.equal(perf.instrumentationEnabled, true); - }); - - it('should throw Error with wrong parameter', function () { - const perf = firebase.perf(); - - try { - perf.instrumentationEnabled = 'some string'; - - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'enabled' must be a boolean"); - return Promise.resolve(); - } - }); - }); - - // TODO: flakey in Jet e2e. - xdescribe('dataCollectionEnabled', function () { - afterEach(function () { - const perf = firebase.perf(); - perf.dataCollectionEnabled = false; - }); - - it('true', function () { - const perf = firebase.perf(); - - perf.dataCollectionEnabled = true; - - should.equal(perf.dataCollectionEnabled, true); - }); - - it('should throw Error with wrong parameter', function () { - const perf = firebase.perf(); - - try { - perf.dataCollectionEnabled = 'some string'; - - return Promise.reject(new Error('Did not throw Error.')); - } catch (e) { - e.message.should.containEql("'enabled' must be a boolean"); - return Promise.resolve(); - } - }); - }); - - describe('startTrace()', function () { - it('resolves a started instance of Trace', async function () { - const trace = await firebase.perf().startTrace('invertase'); - trace.constructor.name.should.be.equal('Trace'); - trace._identifier.should.equal('invertase'); - trace._started.should.equal(true); - await trace.stop(); - }); - }); - - describe('startScreenTrace()', function () { - it('resolves a started instance of a ScreenTrace', async function () { - if (Platform.android) { - const screenTrace = await firebase.perf().startScreenTrace('FooScreen'); - screenTrace.constructor.name.should.be.equal('ScreenTrace'); - screenTrace._identifier.should.equal('FooScreen'); - await screenTrace.stop(); - screenTrace._stopped.should.equal(true); - } - }); - }); - }); - describe('modular', function () { describe('getPerformance', function () { it('pass app as argument', function () { diff --git a/packages/perf/lib/index.ts b/packages/perf/lib/index.ts index b1f7adf0bc..c6b300ef6d 100644 --- a/packages/perf/lib/index.ts +++ b/packages/perf/lib/index.ts @@ -15,11 +15,218 @@ * */ -export type * from './types/perf'; +import { isBoolean, isOneOf, isString } from '@react-native-firebase/app/dist/module/common'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import './types/internal'; +import { Platform } from 'react-native'; + +import HttpMetricImpl from './HttpMetric'; +import ScreenTraceImpl from './ScreenTrace'; +import TraceImpl from './Trace'; +import { version } from './version'; + +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import type { + FirebasePerformance, + HttpMetric, + HttpMethod, + PerformanceSettings, + PerformanceTrace, + ScreenTrace, +} from './types/perf'; +import type { PerfInternal } from './types/internal'; + +const nativeModuleName = 'RNFBPerfModule' as const; + +const VALID_HTTP_METHODS: HttpMethod[] = [ + 'CONNECT', + 'DELETE', + 'GET', + 'HEAD', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', + 'TRACE', +]; + +class FirebasePerfModule extends FirebaseModule { + private _isPerformanceCollectionEnabled: boolean; + private _instrumentationEnabled: boolean; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); + this._isPerformanceCollectionEnabled = this.native.isPerformanceCollectionEnabled; + this._instrumentationEnabled = this.native.isInstrumentationEnabled; + } + + get isPerformanceCollectionEnabled(): boolean { + return this._isPerformanceCollectionEnabled; + } + + get instrumentationEnabled(): boolean { + return this._instrumentationEnabled; + } + + set instrumentationEnabled(enabled: boolean) { + if (!isBoolean(enabled)) { + throw new Error("firebase.perf().instrumentationEnabled = 'enabled' must be a boolean."); + } + if (Platform.OS === 'ios') { + // We don't change for android as it cannot be set from code, it is set at gradle build time. + this._instrumentationEnabled = enabled; + // No need to await, as it only takes effect on the next app run. + this.native.instrumentationEnabled(enabled); + } + } + + get dataCollectionEnabled(): boolean { + return this._isPerformanceCollectionEnabled; + } + + set dataCollectionEnabled(enabled: boolean) { + if (!isBoolean(enabled)) { + throw new Error("firebase.perf().dataCollectionEnabled = 'enabled' must be a boolean."); + } + this._isPerformanceCollectionEnabled = enabled; + // Keep setter behavior fire-and-forget; callers that need completion should use setPerformanceCollectionEnabled(). + void this.native.setPerformanceCollectionEnabled(enabled); + } + + async setPerformanceCollectionEnabled(enabled: boolean): Promise { + if (!isBoolean(enabled)) { + throw new Error( + "firebase.perf().setPerformanceCollectionEnabled(*) 'enabled' must be a boolean.", + ); + } + + if (Platform.OS === 'ios') { + // '_instrumentationEnabled' is updated here as well to maintain backward compatibility. See: + // https://github.com/invertase/react-native-firebase/commit/b705622e64d6ebf4ee026d50841e2404cf692f85 + this._instrumentationEnabled = enabled; + await this.native.instrumentationEnabled(enabled); + } + + this._isPerformanceCollectionEnabled = enabled; + return this.native.setPerformanceCollectionEnabled(enabled); + } + + newTrace(identifier: string): TraceImpl { + // TODO(VALIDATION): identifier: no leading or trailing whitespace, no leading underscore '_' + if (!isString(identifier) || identifier.length > 100) { + throw new Error( + "firebase.perf().newTrace(*) 'identifier' must be a string with a maximum length of 100 characters.", + ); + } + + return new TraceImpl(this.native, identifier); + } -export * from './modular'; + startTrace(identifier: string): Promise { + const traceInstance = this.newTrace(identifier); + return traceInstance.start().then(() => traceInstance); + } -export type { FirebasePerformanceTypes } from './types/namespaced'; + newScreenTrace(identifier: string): ScreenTraceImpl { + if (!isString(identifier) || identifier.length > 100) { + throw new Error( + "firebase.perf().newScreenTrace(*) 'identifier' must be a string with a maximum length of 100 characters.", + ); + } -export * from './namespaced'; -export { default } from './namespaced'; + return new ScreenTraceImpl(this.native, identifier); + } + + startScreenTrace(identifier: string): Promise { + const screenTrace = this.newScreenTrace(identifier); + return screenTrace.start().then(() => screenTrace); + } + + newHttpMetric(url: string, httpMethod: HttpMethod): HttpMetricImpl { + if (!isString(url)) { + throw new Error("firebase.perf().newHttpMetric(*, _) 'url' must be a string."); + } + + if (!isOneOf(httpMethod, VALID_HTTP_METHODS)) { + throw new Error( + `firebase.perf().newHttpMetric(_, *) 'httpMethod' must be one of ${VALID_HTTP_METHODS.join( + ', ', + )}.`, + ); + } + + return new HttpMetricImpl(this.native, url, httpMethod); + } +} + +const config: ModuleConfig = { + namespace: 'perf', + nativeModuleName, + nativeEvents: false, + hasMultiAppSupport: false, + hasCustomUrlOrRegionSupport: false, +}; + +export const SDK_VERSION = version; + +function perfInternal(performance: FirebasePerformance): PerfInternal { + return performance as PerfInternal; +} + +export function getPerformance(app?: FirebaseApp): FirebasePerformance { + return getOrCreateModularInstance( + FirebasePerfModule, + config, + app, + ) as unknown as FirebasePerformance; +} + +export function initializePerformance( + app: FirebaseApp, + settings?: PerformanceSettings, +): FirebasePerformance { + const perf = getPerformance(app); + + if (settings?.dataCollectionEnabled !== undefined) { + perf.dataCollectionEnabled = settings.dataCollectionEnabled; + } + if (settings?.instrumentationEnabled !== undefined) { + perf.instrumentationEnabled = settings.instrumentationEnabled; + } + + return perf; +} + +export function trace(performance: FirebasePerformance, name: string): PerformanceTrace { + return perfInternal(performance).newTrace(name) as unknown as PerformanceTrace; +} + +export function httpMetric( + performance: FirebasePerformance, + url: string, + httpMethod: HttpMethod, +): HttpMetric { + return perfInternal(performance).newHttpMetric(url, httpMethod) as unknown as HttpMetric; +} + +export function newScreenTrace(performance: FirebasePerformance, screenName: string): ScreenTrace { + return perfInternal(performance).newScreenTrace(screenName) as unknown as ScreenTrace; +} + +export function startScreenTrace( + performance: FirebasePerformance, + screenName: string, +): Promise { + return perfInternal(performance).startScreenTrace(screenName) as unknown as Promise; +} + +export type * from './types/perf'; diff --git a/packages/perf/lib/modular.ts b/packages/perf/lib/modular.ts deleted file mode 100644 index b5d713e373..0000000000 --- a/packages/perf/lib/modular.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; - -import type { FirebaseApp } from '@react-native-firebase/app'; -import type { - FirebasePerformance, - HttpMetric, - HttpMethod, - PerformanceSettings, - PerformanceTrace, - ScreenTrace, -} from './types/perf'; - -import type { PerfInternal } from './types/internal'; - -function perfInternal(performance: FirebasePerformance): PerfInternal { - return performance as PerfInternal; -} - -/** - * Returns a {@link FirebasePerformance} instance for the given app. - * @param app - The FirebaseApp to use. Optional; defaults to the default app. - */ -export function getPerformance(app?: FirebaseApp): FirebasePerformance { - if (app) { - return getApp(app.name).perf() as FirebasePerformance; - } - - return getApp().perf() as FirebasePerformance; -} - -/** - * Returns a {@link FirebasePerformance} instance for the given app (aligned with the Firebase JS SDK). - * Can only be called once per app during initialization in typical usage. - * - * @param app - The FirebaseApp to use. - * @param settings - Optional {@link PerformanceSettings}. - */ -export function initializePerformance( - app: FirebaseApp, - settings?: PerformanceSettings, -): FirebasePerformance { - const perf = getPerformance(app); - - if (settings?.dataCollectionEnabled !== undefined) { - perf.dataCollectionEnabled = settings.dataCollectionEnabled; - } - if (settings?.instrumentationEnabled !== undefined) { - perf.instrumentationEnabled = settings.instrumentationEnabled; - } - - return perf; -} - -/** - * Returns a new {@link PerformanceTrace} instance. - * @param performance - The {@link FirebasePerformance} instance to use. - * @param name - The name of the trace. - */ -export function trace(performance: FirebasePerformance, name: string): PerformanceTrace { - return perfInternal(performance).newTrace(name, MODULAR_DEPRECATION_ARG); -} - -/** - * Creates an {@link HttpMetric} for a URL and HTTP method (React Native). - * @param performance - The {@link FirebasePerformance} instance to use. - * @param url - Request URL. - * @param httpMethod - HTTP method. - */ -export function httpMetric( - performance: FirebasePerformance, - url: string, - httpMethod: HttpMethod, -): HttpMetric { - return perfInternal(performance).newHttpMetric(url, httpMethod, MODULAR_DEPRECATION_ARG); -} - -/** - * Creates a {@link ScreenTrace} with the given screen name. - * Android only; throws if hardware acceleration is disabled or if Android is 9.0 or 9.1. - * @param performance - The {@link FirebasePerformance} instance to use. - * @param screenName - Screen name; no leading/trailing whitespace, no leading `_`, max length 100. - */ -export function newScreenTrace(performance: FirebasePerformance, screenName: string): ScreenTrace { - return perfInternal(performance).newScreenTrace(screenName, MODULAR_DEPRECATION_ARG); -} - -/** - * Creates a {@link ScreenTrace} and starts it immediately. - * Android only; throws if hardware acceleration is disabled or if Android is 9.0 or 9.1. - * @param performance - The {@link FirebasePerformance} instance to use. - * @param screenName - Name of the screen. - */ -export function startScreenTrace( - performance: FirebasePerformance, - screenName: string, -): Promise { - return perfInternal(performance).startScreenTrace(screenName, MODULAR_DEPRECATION_ARG); -} diff --git a/packages/perf/lib/namespaced.ts b/packages/perf/lib/namespaced.ts deleted file mode 100644 index e14770f275..0000000000 --- a/packages/perf/lib/namespaced.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { isBoolean, isOneOf, isString } from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, - type ModuleConfig, -} from '@react-native-firebase/app/dist/module/internal'; -import { Platform } from 'react-native'; - -import HttpMetric from './HttpMetric'; -import ScreenTrace from './ScreenTrace'; -import Trace from './Trace'; -import { version } from './version'; - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import type { FirebasePerformanceTypes } from './types/namespaced'; - -const statics: FirebasePerformanceTypes.Statics = { - SDK_VERSION: version, -}; - -const namespace = 'perf'; - -const nativeModuleName = 'RNFBPerfModule' as const; - -const VALID_HTTP_METHODS: FirebasePerformanceTypes.HttpMethod[] = [ - 'CONNECT', - 'DELETE', - 'GET', - 'HEAD', - 'OPTIONS', - 'PATCH', - 'POST', - 'PUT', - 'TRACE', -]; - -class FirebasePerfModule extends FirebaseModule { - private _isPerformanceCollectionEnabled: boolean; - private _instrumentationEnabled: boolean; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - customUrlOrRegion?: string | null, - ) { - super(app, config, customUrlOrRegion); - this._isPerformanceCollectionEnabled = this.native.isPerformanceCollectionEnabled; - this._instrumentationEnabled = this.native.isInstrumentationEnabled; - } - - get isPerformanceCollectionEnabled(): boolean { - return this._isPerformanceCollectionEnabled; - } - - get instrumentationEnabled(): boolean { - return this._instrumentationEnabled; - } - - set instrumentationEnabled(enabled: boolean) { - if (!isBoolean(enabled)) { - throw new Error("firebase.perf().instrumentationEnabled = 'enabled' must be a boolean."); - } - if (Platform.OS === 'ios') { - // We don't change for android as it cannot be set from code, it is set at gradle build time. - this._instrumentationEnabled = enabled; - // No need to await, as it only takes effect on the next app run. - this.native.instrumentationEnabled(enabled); - } - } - - get dataCollectionEnabled(): boolean { - return this._isPerformanceCollectionEnabled; - } - - set dataCollectionEnabled(enabled: boolean) { - if (!isBoolean(enabled)) { - throw new Error("firebase.perf().dataCollectionEnabled = 'enabled' must be a boolean."); - } - this._isPerformanceCollectionEnabled = enabled; - // Keep setter behavior fire-and-forget; callers that need completion should use setPerformanceCollectionEnabled(). - void this.native.setPerformanceCollectionEnabled(enabled); - } - - async setPerformanceCollectionEnabled(enabled: boolean): Promise { - if (!isBoolean(enabled)) { - throw new Error( - "firebase.perf().setPerformanceCollectionEnabled(*) 'enabled' must be a boolean.", - ); - } - - if (Platform.OS === 'ios') { - // '_instrumentationEnabled' is updated here as well to maintain backward compatibility. See: - // https://github.com/invertase/react-native-firebase/commit/b705622e64d6ebf4ee026d50841e2404cf692f85 - this._instrumentationEnabled = enabled; - await this.native.instrumentationEnabled(enabled); - } - - this._isPerformanceCollectionEnabled = enabled; - return this.native.setPerformanceCollectionEnabled(enabled); - } - - newTrace(identifier: string): Trace { - // TODO(VALIDATION): identifier: no leading or trailing whitespace, no leading underscore '_' - if (!isString(identifier) || identifier.length > 100) { - throw new Error( - "firebase.perf().newTrace(*) 'identifier' must be a string with a maximum length of 100 characters.", - ); - } - - return new Trace(this.native, identifier); - } - - startTrace(identifier: string): Promise { - const traceInstance = this.newTrace(identifier); - return traceInstance.start().then(() => traceInstance); - } - - newScreenTrace(identifier: string): ScreenTrace { - if (!isString(identifier) || identifier.length > 100) { - throw new Error( - "firebase.perf().newScreenTrace(*) 'identifier' must be a string with a maximum length of 100 characters.", - ); - } - - return new ScreenTrace(this.native, identifier); - } - - startScreenTrace(identifier: string): Promise { - const screenTrace = this.newScreenTrace(identifier); - return screenTrace.start().then(() => screenTrace); - } - - newHttpMetric(url: string, httpMethod: FirebasePerformanceTypes.HttpMethod): HttpMetric { - if (!isString(url)) { - throw new Error("firebase.perf().newHttpMetric(*, _) 'url' must be a string."); - } - - if (!isOneOf(httpMethod, VALID_HTTP_METHODS)) { - throw new Error( - `firebase.perf().newHttpMetric(_, *) 'httpMethod' must be one of ${VALID_HTTP_METHODS.join( - ', ', - )}.`, - ); - } - - return new HttpMetric(this.native, url, httpMethod); - } -} - -// import { SDK_VERSION } from '@react-native-firebase/perf'; -export const SDK_VERSION = version; - -export type PerfNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebasePerformanceTypes.Module, - FirebasePerformanceTypes.Statics -> & { - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -const defaultExport = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: false, - hasMultiAppSupport: false, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebasePerfModule, -}) as unknown as PerfNamespace; - -export default defaultExport; - -// import perf, { firebase } from '@react-native-firebase/perf'; -// perf().X(...); -// firebase.perf().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'perf', - FirebasePerformanceTypes.Module, - FirebasePerformanceTypes.Statics - >; diff --git a/packages/perf/lib/types/internal.ts b/packages/perf/lib/types/internal.ts index 52bc86f14b..31e074c472 100644 --- a/packages/perf/lib/types/internal.ts +++ b/packages/perf/lib/types/internal.ts @@ -23,28 +23,16 @@ import type { ScreenTrace, } from './perf'; -export type PerfModularDeprecationArg = string; - -/** - * Namespaced perf instance used by modular entrypoints; methods accept an optional modular deprecation token. - */ export interface PerfInternal extends FirebasePerformance { /** * @deprecated Prefer assigning {@link FirebasePerformance.dataCollectionEnabled}. */ setPerformanceCollectionEnabled(enabled: boolean): Promise; - newTrace(name: string, deprecationArg?: PerfModularDeprecationArg): PerformanceTrace; - startTrace(name: string, deprecationArg?: PerfModularDeprecationArg): Promise; - newScreenTrace(screenName: string, deprecationArg?: PerfModularDeprecationArg): ScreenTrace; - startScreenTrace( - screenName: string, - deprecationArg?: PerfModularDeprecationArg, - ): Promise; - newHttpMetric( - url: string, - httpMethod: HttpMethod, - deprecationArg?: PerfModularDeprecationArg, - ): HttpMetric; + newTrace(name: string): PerformanceTrace; + startTrace(name: string): Promise; + newScreenTrace(screenName: string): ScreenTrace; + startScreenTrace(screenName: string): Promise; + newHttpMetric(url: string, httpMethod: HttpMethod): HttpMetric; } export interface RNFBPerfTraceData { diff --git a/packages/perf/lib/types/namespaced.ts b/packages/perf/lib/types/namespaced.ts deleted file mode 100644 index bea342e5a6..0000000000 --- a/packages/perf/lib/types/namespaced.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase Performance Monitoring package for React Native. - * - * #### Example 1 - * - * Access the firebase export from the `perf` package: - * - * ```js - * import { firebase } from '@react-native-firebase/perf'; - * - * // firebase.perf().X - * ``` - * - * #### Example 2 - * - * Using the default export from the `perf` package: - * - * ```js - * import perf from '@react-native-firebase/perf'; - * - * // perf().X - * ``` - * - * #### Example 3 - * - * Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/perf'; - * - * // firebase.perf().X - * ``` - * - * @firebase perf - */ -/** - * @deprecated Use the modular type exports from `@react-native-firebase/perf` instead. - * `FirebasePerformanceTypes` is kept for backwards compatibility with the namespaced API. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebasePerformanceTypes { - type FirebaseModule = ReactNativeFirebase.FirebaseModule; - - /** - * @deprecated Use the `HttpMethod` type from the modular package exports instead. - */ - export type HttpMethod = - | 'GET' - | 'HEAD' - | 'PUT' - | 'POST' - | 'PATCH' - | 'TRACE' - | 'DELETE' - | 'CONNECT' - | 'OPTIONS'; - - /** - * @deprecated Use the `PerformanceTrace` type from the modular package exports instead. - */ - export interface Trace { - getAttribute(attribute: string): string | undefined; - putAttribute(attribute: string, value: string): void; - getMetric(metricName: string): number; - getMetrics(): { [key: string]: number }; - putMetric(metricName: string, value: number): void; - incrementMetric(metricName: string, incrementBy?: number): void; - removeMetric(metricName: string): void; - start(): Promise; - stop(): Promise; - } - - /** - * @deprecated Use the `ScreenTrace` type from the modular package exports instead. - */ - export interface ScreenTrace { - start(): Promise; - stop(): Promise; - } - - /** - * @deprecated Use the `HttpMetric` type from the modular package exports instead. - */ - export interface HttpMetric { - getAttribute(attribute: string): string | undefined; - getAttributes(): { [key: string]: string }; - putAttribute(attribute: string, value: string): void; - removeAttribute(attribute: string): void; - setHttpResponseCode(code: number | null): void; - setRequestPayloadSize(bytes: number | null): void; - setResponsePayloadSize(bytes: number | null): void; - setResponseContentType(contentType: string | null): void; - start(): Promise; - stop(): Promise; - } - - /** - * @deprecated Use static exports from the package root instead. - */ - export interface Statics { - SDK_VERSION: string; - } - - /** - * @deprecated Use the `FirebasePerformance` type from the modular package exports instead. - */ - export interface Module extends FirebaseModule { - app: ReactNativeFirebase.FirebaseApp; - isPerformanceCollectionEnabled: boolean; - instrumentationEnabled: boolean; - dataCollectionEnabled: boolean; - /** - * @deprecated prefer setting `dataCollectionEnabled = boolean`. - */ - setPerformanceCollectionEnabled(enabled: boolean): Promise; - newTrace(identifier: string): Trace; - startTrace(identifier: string): Promise; - newScreenTrace(identifier: string): ScreenTrace; - startScreenTrace(identifier: string): Promise; - newHttpMetric(url: string, httpMethod: HttpMethod): HttpMetric; - } -} -/* eslint-enable @typescript-eslint/no-namespace */ - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ -declare module '@react-native-firebase/app' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace ReactNativeFirebase { - import FirebaseModuleWithStatics = ReactNativeFirebase.FirebaseModuleWithStatics; - interface Module { - perf: FirebaseModuleWithStatics< - FirebasePerformanceTypes.Module, - FirebasePerformanceTypes.Statics - >; - } - interface FirebaseApp { - perf(): FirebasePerformanceTypes.Module; - } - } -} diff --git a/packages/perf/type-test.ts b/packages/perf/type-test.ts index 38d3f556f3..adf94c6e17 100644 --- a/packages/perf/type-test.ts +++ b/packages/perf/type-test.ts @@ -1,148 +1,33 @@ -import perf, { - firebase, - FirebasePerformanceTypes, +import { getApp } from '@react-native-firebase/app'; +import { getPerformance, initializePerformance, trace, httpMetric, newScreenTrace, startScreenTrace, + SDK_VERSION, + type FirebasePerformance, + type HttpMethod, + type PerformanceTrace, } from '.'; -console.log(perf().app); +const perf = getPerformance(); +console.log(perf.app.name); -// checks module exists at root -console.log(firebase.perf().app.name); -console.log(firebase.perf().isPerformanceCollectionEnabled); -console.log(firebase.perf().instrumentationEnabled); -console.log(firebase.perf().dataCollectionEnabled); +const perfWithApp = getPerformance(getApp()); +console.log(perfWithApp.app.name); -// checks module exists at app level -console.log(firebase.app().perf().app.name); -console.log(firebase.app().perf().isPerformanceCollectionEnabled); +initializePerformance(getApp(), { dataCollectionEnabled: true }); +trace(perf, 'test-trace'); +httpMetric(perf, 'https://example.com', 'GET' as HttpMethod); +newScreenTrace(perf, 'HomeScreen'); +startScreenTrace(perf, 'HomeScreen'); -// checks statics exist -console.log(firebase.perf.SDK_VERSION); +const typedPerf: FirebasePerformance = perf; +console.log(typedPerf.app.name); -// checks statics exist on defaultExport -console.log(perf.firebase.SDK_VERSION); +const perfTrace: PerformanceTrace = trace(perf, 'typed'); +console.log(perfTrace); -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks default export supports app arg -console.log(perf().app.name); - -// checks Module instance APIs -const perfInstance = firebase.perf(); -console.log(perfInstance.app.name); -console.log(perfInstance.isPerformanceCollectionEnabled); -console.log(perfInstance.instrumentationEnabled); -console.log(perfInstance.dataCollectionEnabled); - -perfInstance.setPerformanceCollectionEnabled(false).then(() => { - console.log('Performance collection disabled'); -}); - -perfInstance.dataCollectionEnabled = false; -console.log(perfInstance.dataCollectionEnabled); - -perfInstance.instrumentationEnabled = true; -console.log(perfInstance.instrumentationEnabled); - -const traceInstance = perfInstance.newTrace('test-trace'); -console.log(traceInstance.getAttribute('test')); -traceInstance.putAttribute('key', 'value'); -console.log(traceInstance.getAttribute('key')); -traceInstance.putMetric('metric', 10); -console.log(traceInstance.getMetric('metric')); -const metrics = traceInstance.getMetrics(); -console.log(metrics); -traceInstance.incrementMetric('metric', 5); -traceInstance.removeMetric('metric'); -traceInstance.start().then(() => { - console.log('Trace started'); -}); -traceInstance.stop().then(() => { - console.log('Trace stopped'); -}); - -perfInstance.startTrace('async-trace').then((startedTrace: FirebasePerformanceTypes.Trace) => { - console.log(startedTrace); - startedTrace.stop(); -}); - -const screenTrace = perfInstance.newScreenTrace('test-screen'); -screenTrace.start().then(() => { - console.log('Screen trace started'); -}); -screenTrace.stop().then(() => { - console.log('Screen trace stopped'); -}); - -perfInstance - .startScreenTrace('async-screen') - .then((startedScreenTrace: FirebasePerformanceTypes.ScreenTrace) => { - console.log(startedScreenTrace); - startedScreenTrace.stop(); - }); - -const httpMetricInstance = perfInstance.newHttpMetric('https://example.com', 'GET'); -console.log(httpMetricInstance.getAttribute('test')); -httpMetricInstance.putAttribute('key', 'value'); -const attributes = httpMetricInstance.getAttributes(); -console.log(attributes); -httpMetricInstance.removeAttribute('key'); -httpMetricInstance.setHttpResponseCode(200); -httpMetricInstance.setRequestPayloadSize(1024); -httpMetricInstance.setResponsePayloadSize(2048); -httpMetricInstance.setResponseContentType('application/json'); -httpMetricInstance.start().then(() => { - console.log('HTTP metric started'); -}); -httpMetricInstance.stop().then(() => { - console.log('HTTP metric stopped'); -}); - -// checks modular API functions -const modularPerf1 = getPerformance(); -console.log(modularPerf1.app.name); - -const modularPerf2 = getPerformance(firebase.app()); -console.log(modularPerf2.app.name); - -const initializedPerf = initializePerformance(firebase.app(), { dataCollectionEnabled: true }); -console.log(initializedPerf.app.name); - -const modularTrace = trace(modularPerf1, 'modular-trace'); -modularTrace.putAttribute('modular-key', 'modular-value'); -modularTrace.putMetric('modular-metric', 20); -modularTrace.start().then(() => { - console.log('Modular trace started'); -}); -modularTrace.stop().then(() => { - console.log('Modular trace stopped'); -}); - -const modularHttpMetric = httpMetric(modularPerf1, 'https://modular.example.com', 'POST'); -modularHttpMetric.putAttribute('modular-key', 'modular-value'); -modularHttpMetric.setHttpResponseCode(201); -modularHttpMetric.start().then(() => { - console.log('Modular HTTP metric started'); -}); -modularHttpMetric.stop().then(() => { - console.log('Modular HTTP metric stopped'); -}); - -const modularScreenTrace = newScreenTrace(modularPerf1, 'modular-screen'); -modularScreenTrace.start().then(() => { - console.log('Modular screen trace started'); -}); -modularScreenTrace.stop().then(() => { - console.log('Modular screen trace stopped'); -}); - -startScreenTrace(modularPerf1, 'modular-async-screen').then(startedModularScreenTrace => { - console.log(startedModularScreenTrace); - startedModularScreenTrace.stop(); -}); +console.log(SDK_VERSION); diff --git a/packages/perf/typedoc.json b/packages/perf/typedoc.json index 55ee480416..cce87cd759 100644 --- a/packages/perf/typedoc.json +++ b/packages/perf/typedoc.json @@ -1,5 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/perf.ts"], + "entryPoints": ["lib/index.ts", "lib/types/perf.ts"], "tsconfig": "tsconfig.json" } diff --git a/packages/phone-number-verification/lib/index.ts b/packages/phone-number-verification/lib/index.ts index eebf088d20..49e8e43cca 100644 --- a/packages/phone-number-verification/lib/index.ts +++ b/packages/phone-number-verification/lib/index.ts @@ -1,3 +1,131 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Platform } from 'react-native'; +import { getReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; + +import type { VerificationSupportResult, VerifiedPhoneNumberTokenResult } from './types/pnv'; + export { PnvErrorCode } from './types/pnv'; export type * from './types/pnv'; -export * from './modular'; + +const UNSUPPORTED_MSG = 'Firebase Phone Number Verification is only supported on Android.'; + +const NATIVE_MODULE_NAME = 'RNFBPnvModule'; + +interface NativePnvModule { + enableTestSession(token: string): Promise; + getVerificationSupportInfo(): Promise; + getVerificationSupportInfoForSimSlot(simSlot: number): Promise; + getVerifiedPhoneNumber(): Promise; + getDigitalCredentialPayload(nonce: string): Promise; + exchangeCredentialResponseForPhoneNumber( + dcApiResponse: string, + ): Promise; +} + +function getNativeModule(): NativePnvModule { + if (Platform.OS !== 'android') { + throw new Error(UNSUPPORTED_MSG); + } + const mod = getReactNativeModule(NATIVE_MODULE_NAME); + if (!mod) { + throw new Error( + `Could not find native module '${NATIVE_MODULE_NAME}'. Ensure @react-native-firebase/phone-number-verification is linked.`, + ); + } + return mod as unknown as NativePnvModule; +} + +/** + * Enables a test session for SIM-less testing. + * Must be called only once per app instance; subsequent calls will reject with + * error code `pnv/test-session-already-enabled`. + * + * @remarks + * In test mode, phone numbers follow the format: valid country code followed by all zeros. + * Requires a test token generated from the Firebase Console (7-day TTL). + * + * @param token - The test token generated from the Firebase Console. + * @throws `pnv/test-session-already-enabled` if called more than once. + * @throws `pnv/invalid-test-number-id` if the token is empty, expired, or duplicated. + * @see https://firebase.google.com/docs/phone-number-verification + */ +export function enableTestSession(token: string): Promise { + return getNativeModule().enableTestSession(token); +} + +/** + * Checks if the device's SIM card(s) support phone number verification. + * + * @remarks + * This method does not require user consent and can be called freely. + * Returns one `VerificationSupportResult` per SIM slot (or one entry if `simSlot` is specified). + * + * @param simSlot - Optional 0-based SIM slot index to query a specific slot instead of all slots. + * @returns Array of support results, one per SIM slot. + * @see https://firebase.google.com/docs/phone-number-verification + */ +export function getVerificationSupportInfo(simSlot?: number): Promise { + if (simSlot !== undefined) { + return getNativeModule().getVerificationSupportInfoForSimSlot(simSlot); + } + return getNativeModule().getVerificationSupportInfo(); +} + +/** + * Initiates the phone number verification flow, including user consent and token generation. + * A consent dialog will be presented to the user. + * + * @remarks + * The app should prepare the user for the consent screen before calling this method. + * + * @returns The verified phone number and a JWT token with full claims for server-side validation. + * @throws `pnv/carrier-not-supported` if the carrier does not support PNV. + * @throws `pnv/activity-context-required` if no foreground Activity is available. + * @see https://firebase.google.com/docs/phone-number-verification/android/get-started + */ +export function getVerifiedPhoneNumber(): Promise { + return getNativeModule().getVerifiedPhoneNumber(); +} + +/** + * Generates a digital credential payload for use with Android Credential Manager. + * Part of the custom verification flow. + * + * @param nonce - A unique value to prevent replay attacks. + * @returns The digital credential payload string. + * @see https://firebase.google.com/docs/phone-number-verification + */ +export function getDigitalCredentialPayload(nonce: string): Promise { + return getNativeModule().getDigitalCredentialPayload(nonce); +} + +/** + * Exchanges a Credential Manager response for a verified phone number. + * Part of the custom verification flow. + * + * @param dcApiResponse - The JWT from the Credential Manager response. + * @returns The verified phone number and a JWT token with full claims for server-side validation. + * @throws `pnv/invalid-digital-credential-response` if the response is invalid. + * @see https://firebase.google.com/docs/phone-number-verification + */ +export function exchangeCredentialResponseForPhoneNumber( + dcApiResponse: string, +): Promise { + return getNativeModule().exchangeCredentialResponseForPhoneNumber(dcApiResponse); +} diff --git a/packages/phone-number-verification/lib/modular.ts b/packages/phone-number-verification/lib/modular.ts deleted file mode 100644 index 9abb6f729c..0000000000 --- a/packages/phone-number-verification/lib/modular.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { Platform } from 'react-native'; -import { getReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; - -import type { VerificationSupportResult, VerifiedPhoneNumberTokenResult } from './types/pnv'; - -const UNSUPPORTED_MSG = 'Firebase Phone Number Verification is only supported on Android.'; - -const NATIVE_MODULE_NAME = 'RNFBPnvModule'; - -interface NativePnvModule { - enableTestSession(token: string): Promise; - getVerificationSupportInfo(): Promise; - getVerificationSupportInfoForSimSlot(simSlot: number): Promise; - getVerifiedPhoneNumber(): Promise; - getDigitalCredentialPayload(nonce: string): Promise; - exchangeCredentialResponseForPhoneNumber( - dcApiResponse: string, - ): Promise; -} - -function getNativeModule(): NativePnvModule { - if (Platform.OS !== 'android') { - throw new Error(UNSUPPORTED_MSG); - } - const mod = getReactNativeModule(NATIVE_MODULE_NAME); - if (!mod) { - throw new Error( - `Could not find native module '${NATIVE_MODULE_NAME}'. Ensure @react-native-firebase/phone-number-verification is linked.`, - ); - } - return mod as unknown as NativePnvModule; -} - -/** - * Enables a test session for SIM-less testing. - * Must be called only once per app instance; subsequent calls will reject with - * error code `pnv/test-session-already-enabled`. - * - * @remarks - * In test mode, phone numbers follow the format: valid country code followed by all zeros. - * Requires a test token generated from the Firebase Console (7-day TTL). - * - * @param token - The test token generated from the Firebase Console. - * @throws `pnv/test-session-already-enabled` if called more than once. - * @throws `pnv/invalid-test-number-id` if the token is empty, expired, or duplicated. - * @see https://firebase.google.com/docs/phone-number-verification - */ -export function enableTestSession(token: string): Promise { - return getNativeModule().enableTestSession(token); -} - -/** - * Checks if the device's SIM card(s) support phone number verification. - * - * @remarks - * This method does not require user consent and can be called freely. - * Returns one `VerificationSupportResult` per SIM slot (or one entry if `simSlot` is specified). - * - * @param simSlot - Optional 0-based SIM slot index to query a specific slot instead of all slots. - * @returns Array of support results, one per SIM slot. - * @see https://firebase.google.com/docs/phone-number-verification - */ -export function getVerificationSupportInfo(simSlot?: number): Promise { - if (simSlot !== undefined) { - return getNativeModule().getVerificationSupportInfoForSimSlot(simSlot); - } - return getNativeModule().getVerificationSupportInfo(); -} - -/** - * Initiates the phone number verification flow, including user consent and token generation. - * A consent dialog will be presented to the user. - * - * @remarks - * The app should prepare the user for the consent screen before calling this method. - * - * @returns The verified phone number and a JWT token with full claims for server-side validation. - * @throws `pnv/carrier-not-supported` if the carrier does not support PNV. - * @throws `pnv/activity-context-required` if no foreground Activity is available. - * @see https://firebase.google.com/docs/phone-number-verification/android/get-started - */ -export function getVerifiedPhoneNumber(): Promise { - return getNativeModule().getVerifiedPhoneNumber(); -} - -/** - * Generates a digital credential payload for use with Android Credential Manager. - * Part of the custom verification flow. - * - * @param nonce - A unique value to prevent replay attacks. - * @returns The digital credential payload string. - * @see https://firebase.google.com/docs/phone-number-verification - */ -export function getDigitalCredentialPayload(nonce: string): Promise { - return getNativeModule().getDigitalCredentialPayload(nonce); -} - -/** - * Exchanges a Credential Manager response for a verified phone number. - * Part of the custom verification flow. - * - * @param dcApiResponse - The JWT from the Credential Manager response. - * @returns The verified phone number and a JWT token with full claims for server-side validation. - * @throws `pnv/invalid-digital-credential-response` if the response is invalid. - * @see https://firebase.google.com/docs/phone-number-verification - */ -export function exchangeCredentialResponseForPhoneNumber( - dcApiResponse: string, -): Promise { - return getNativeModule().exchangeCredentialResponseForPhoneNumber(dcApiResponse); -} diff --git a/packages/phone-number-verification/typedoc.json b/packages/phone-number-verification/typedoc.json index 3216b38f8a..1a8dcae982 100644 --- a/packages/phone-number-verification/typedoc.json +++ b/packages/phone-number-verification/typedoc.json @@ -1,5 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/pnv.ts"], + "entryPoints": ["lib/index.ts"], "tsconfig": "tsconfig.json" } diff --git a/packages/remote-config/__tests__/remote-config.test.ts b/packages/remote-config/__tests__/remote-config.test.ts index fa2c2bb6c1..e5330bd1c4 100644 --- a/packages/remote-config/__tests__/remote-config.test.ts +++ b/packages/remote-config/__tests__/remote-config.test.ts @@ -14,10 +14,10 @@ * limitations under the License. * */ -import { afterAll, beforeAll, describe, expect, it, beforeEach, jest } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; +import { getApp } from '@react-native-firebase/app'; import { - firebase, getRemoteConfig, activate, ensureInitialized, @@ -34,117 +34,14 @@ import { setDefaultsFromResource, onConfigUpdate, setCustomSignals, + LastFetchStatus, + ValueSource, + SDK_VERSION, } from '../lib'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, -} from '../../app/lib/common/unitTestUtils'; - -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; +import type { RemoteConfigInternal } from '../lib/types/internal'; describe('remoteConfig()', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.remoteConfig()).toBeDefined(); - expect(app.remoteConfig().app).toEqual(app); - }); - - it('supports multiple apps', async function () { - expect(firebase.remoteConfig().app.name).toEqual('[DEFAULT]'); - expect(firebase.app('secondaryFromNative').remoteConfig().app.name).toEqual( - 'secondaryFromNative', - ); - }); - - describe('statics', function () { - it('LastFetchStatus', function () { - expect(firebase.remoteConfig.LastFetchStatus).toBeDefined(); - expect(firebase.remoteConfig.LastFetchStatus.FAILURE).toEqual('failure'); - expect(firebase.remoteConfig.LastFetchStatus.SUCCESS).toEqual('success'); - expect(firebase.remoteConfig.LastFetchStatus.NO_FETCH_YET).toEqual('no_fetch_yet'); - expect(firebase.remoteConfig.LastFetchStatus.THROTTLED).toEqual('throttled'); - }); - - it('ValueSource', function () { - expect(firebase.remoteConfig.ValueSource).toBeDefined(); - expect(firebase.remoteConfig.ValueSource.REMOTE).toEqual('remote'); - expect(firebase.remoteConfig.ValueSource.STATIC).toEqual('static'); - expect(firebase.remoteConfig.ValueSource.DEFAULT).toEqual('default'); - }); - }); - - describe('fetch()', function () { - it('it throws if expiration is not a number', function () { - expect(() => { - // @ts-ignore - incorrect argument on purpose to check validation - firebase.remoteConfig().fetch('foo'); - }).toThrow('must be a number value'); - }); - }); - - describe('setConfigSettings()', function () { - it('it throws if arg is not an object', async function () { - expect(() => { - // @ts-ignore - incorrect argument on purpose to check validation - firebase.remoteConfig().setConfigSettings('not an object'); - }).toThrow('must set an object'); - }); - - it('throws if minimumFetchIntervalMillis is not a number', async function () { - expect(() => { - // @ts-ignore - incorrect argument on purpose to check validation - firebase.remoteConfig().setConfigSettings({ minimumFetchIntervalMillis: 'potato' }); - }).toThrow('must be a number type in milliseconds.'); - }); - - it('throws if fetchTimeMillis is not a number', function () { - expect(() => { - // @ts-ignore - incorrect argument on purpose to check validation - firebase.remoteConfig().setConfigSettings({ fetchTimeMillis: 'potato' }); - }).toThrow('must be a number type in milliseconds.'); - }); - }); - - describe('setDefaults()', function () { - it('it throws if defaults object not provided', function () { - expect(() => { - // @ts-ignore - incorrect argument on purpose to check validation - firebase.remoteConfig().setDefaults('not an object'); - }).toThrow('must be an object.'); - }); - }); - - describe('setDefaultsFromResource()', function () { - it('throws if resourceName is not a string', function () { - expect(() => { - // @ts-ignore - incorrect argument on purpose to check validation - firebase.remoteConfig().setDefaultsFromResource(1337); - }).toThrow('must be a string value'); - }); - }); - - describe('getAll() should not crash', function () { - it('should return an empty object pre-fetch, pre-defaults', function () { - const config = firebase.remoteConfig().getAll(); - expect(config).toBeDefined(); - expect(config).toEqual({}); - }); - }); - }); - describe('modular', function () { it('`getRemoteConfig` function is properly exposed to end user', function () { expect(getRemoteConfig).toBeDefined(); @@ -210,133 +107,85 @@ describe('remoteConfig()', function () { expect(setCustomSignals).toBeDefined(); }); - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let remoteConfigV9Deprecation: CheckV9DeprecationFunction; + it('exports statics and SDK_VERSION', function () { + expect(LastFetchStatus).toBeDefined(); + expect(LastFetchStatus.FAILURE).toEqual('failure'); + expect(LastFetchStatus.SUCCESS).toEqual('success'); + expect(LastFetchStatus.NO_FETCH_YET).toEqual('no_fetch_yet'); + expect(LastFetchStatus.THROTTLED).toEqual('throttled'); + expect(ValueSource).toBeDefined(); + expect(ValueSource.REMOTE).toEqual('remote'); + expect(ValueSource.STATIC).toEqual('static'); + expect(ValueSource.DEFAULT).toEqual('default'); + expect(SDK_VERSION).toBeDefined(); + }); - beforeEach(function () { - remoteConfigV9Deprecation = createCheckV9Deprecation(['remoteConfig']); + it('supports multiple apps', function () { + expect(getRemoteConfig().app.name).toEqual('[DEFAULT]'); + expect(getRemoteConfig(getApp('secondaryFromNative')).app.name).toEqual( + 'secondaryFromNative', + ); + }); - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: () => - jest.fn().mockResolvedValue({ - result: true, - constants: { - lastFetchTime: Date.now(), - lastFetchStatus: 'success', - fetchTimeout: 60, - minimumFetchInterval: 12, - values: {}, - }, - } as never), - }, - ); - }); + describe('fetch()', function () { + it('it throws if expiration is not a number', function () { + expect(() => { + (getRemoteConfig() as RemoteConfigInternal).fetch('foo' as unknown as number); + }).toThrow('must be a number value'); }); + }); - describe('remoteConfig functions', function () { - it('activate()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => activate(remoteConfig), - () => legacyRemoteConfig.activate(), - 'activate', - ); - }); - - it('ensureInitialized()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => ensureInitialized(remoteConfig), - () => legacyRemoteConfig.ensureInitialized(), - 'ensureInitialized', - ); - }); - - it('fetchAndActivate()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => fetchAndActivate(remoteConfig), - () => legacyRemoteConfig.fetchAndActivate(), - 'fetchAndActivate', - ); - }); - - it('getAll()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => getAll(remoteConfig), - () => legacyRemoteConfig.getAll(), - 'getAll', - ); - }); - - it('getBoolean()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => getBoolean(remoteConfig, 'foo'), - () => legacyRemoteConfig.getBoolean('foo'), - 'getBoolean', + describe('setConfigSettings()', function () { + it('it throws if arg is not an object', function () { + expect(() => { + (getRemoteConfig() as RemoteConfigInternal).setConfigSettings( + 'not an object' as unknown as { minimumFetchIntervalMillis: number }, ); - }); + }).toThrow('must set an object'); + }); - it('getNumber()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => getNumber(remoteConfig, 'foo'), - () => legacyRemoteConfig.getNumber('foo'), - 'getNumber', - ); - }); + it('throws if minimumFetchIntervalMillis is not a number', function () { + expect(() => { + (getRemoteConfig() as RemoteConfigInternal).setConfigSettings({ + minimumFetchIntervalMillis: 'potato' as unknown as number, + }); + }).toThrow('must be a number type in milliseconds.'); + }); - it('getString()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => getString(remoteConfig, 'foo'), - () => legacyRemoteConfig.getString('foo'), - 'getString', - ); - }); + it('throws if fetchTimeMillis is not a number', function () { + expect(() => { + (getRemoteConfig() as RemoteConfigInternal).setConfigSettings({ + fetchTimeMillis: 'potato' as unknown as number, + }); + }).toThrow('must be a number type in milliseconds.'); + }); + }); - it('getValue()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => getValue(remoteConfig, 'foo'), - () => legacyRemoteConfig.getValue('foo'), - 'getValue', + describe('setDefaults()', function () { + it('it throws if defaults object not provided', function () { + expect(() => { + (getRemoteConfig() as RemoteConfigInternal).setDefaults( + 'not an object' as unknown as Record, ); - }); + }).toThrow('must be an object.'); + }); + }); - it('reset()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => reset(remoteConfig), - () => legacyRemoteConfig.reset(), - 'reset', + describe('setDefaultsFromResource()', function () { + it('throws if resourceName is not a string', function () { + expect(() => { + (getRemoteConfig() as RemoteConfigInternal).setDefaultsFromResource( + 1337 as unknown as string, ); - }); + }).toThrow('must be a string value'); + }); + }); - it('setDefaultsFromResource()', function () { - const remoteConfig = getRemoteConfig(); - const legacyRemoteConfig = firebase.remoteConfig(); - remoteConfigV9Deprecation( - () => setDefaultsFromResource(remoteConfig, 'foo'), - () => legacyRemoteConfig.setDefaultsFromResource('foo'), - 'setDefaultsFromResource', - ); - }); + describe('getAll() should not crash', function () { + it('should return an empty object pre-fetch, pre-defaults', function () { + const config = getAll(getRemoteConfig()); + expect(config).toBeDefined(); + expect(config).toEqual({}); }); }); }); diff --git a/packages/remote-config/e2e/config.e2e.js b/packages/remote-config/e2e/config.e2e.js index 4621848b7f..311a6984ee 100644 --- a/packages/remote-config/e2e/config.e2e.js +++ b/packages/remote-config/e2e/config.e2e.js @@ -16,342 +16,6 @@ */ describe('remoteConfig()', function () { - // Namespaced API is being removed; modular coverage is sufficient and halves FRC/FIS load. - xdescribe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('fetch()', function () { - it('with expiration provided', async function () { - const date = Date.now() - 30000; - await firebase.remoteConfig().ensureInitialized(); - - if (Platform.android) { - // iOS persists last fetch status so this test will fail sometimes - firebase.remoteConfig().fetchTimeMillis.should.be.a.Number(); - } - - await firebase.remoteConfig().fetch(0); - firebase - .remoteConfig() - .lastFetchStatus.should.equal(firebase.remoteConfig.LastFetchStatus.SUCCESS); - should.equal(firebase.remoteConfig().fetchTimeMillis >= date, true); - }); - - it('without expiration provided', function () { - return firebase.remoteConfig().fetch(); - }); - }); - - describe('fetchAndActivate()', function () { - it('returns true/false if activated', async function () { - const activated = await firebase.remoteConfig().fetchAndActivate(); - activated.should.be.a.Boolean(); - }); - }); - - describe('activate()', function () { - it('with expiration provided', async function () { - await firebase.remoteConfig().fetch(0); - const activated = await firebase.remoteConfig().activate(); - activated.should.be.a.Boolean(); - }); - - it('without expiration provided', async function () { - await firebase.remoteConfig().fetch(); - const activated = await firebase.remoteConfig().activate(); - activated.should.be.a.Boolean(); - }); - }); - - describe('config settings', function () { - it('should be immediately available', async function () { - firebase.remoteConfig().lastFetchStatus.should.be.a.String(); - firebase.remoteConfig().lastFetchStatus.should.equal('success'); - firebase.remoteConfig().fetchTimeMillis.should.be.a.Number(); - }); - }); - - describe('setConfigSettings()', function () { - xit('minimumFetchIntervalMillis sets correctly', async function () { - await firebase.remoteConfig().setConfigSettings({ minimumFetchIntervalMillis: 3000 }); - - firebase.remoteConfig().settings.minimumFetchIntervalMillis.should.be.equal(3000); - }); - - it('fetchTimeMillis sets correctly', async function () { - await firebase.remoteConfig().setConfigSettings({ fetchTimeMillis: 3000 }); - - firebase.remoteConfig().settings.fetchTimeMillis.should.be.equal(3000); - }); - }); - - describe('ensureInitialized()', function () { - it('should ensure remote config has been initialized and values are accessible', async function () { - const ensure = await firebase.remoteConfig().ensureInitialized(); - const number = firebase.remoteConfig().getValue('number'); - - should(ensure).equal(undefined); - number.getSource().should.equal('remote'); - number.asNumber().should.equal(1337); - }); - }); - - describe('getAll() with remote', function () { - it('should return an object of all available values', function () { - const config = firebase.remoteConfig().getAll(); - config.number.asNumber().should.equal(1337); - config.number.getSource().should.equal('remote'); - // firebase console stores as a string - config.float.asNumber().should.equal(123.456); - config.float.getSource().should.equal('remote'); - config.prefix_1.asNumber().should.equal(1); - config.prefix_1.getSource().should.equal('remote'); - }); - }); - - describe('setDefaults()', function () { - it('sets default values from key values object', async function () { - await firebase.remoteConfig().setDefaults({ - some_key: 'I do not exist', - some_key_1: 1337, - some_key_2: true, - }); - - const values = firebase.remoteConfig().getAll(); - values.some_key.asString().should.equal('I do not exist'); - values.some_key_1.asNumber().should.equal(1337); - should.equal(values.some_key_2.asBoolean(), true); - - values.some_key.getSource().should.equal('default'); - values.some_key_1.getSource().should.equal('default'); - values.some_key_2.getSource().should.equal('default'); - }); - }); - - describe('getValue()', function () { - describe('getValue().asBoolean()', function () { - it("returns 'true' for the specified keys: '1', 'true', 't', 'yes', 'y', 'on'", async function () { - //Boolean truthy values as defined by web sdk - await firebase.remoteConfig().setDefaults({ - test1: '1', - test2: 'true', - test3: 't', - test4: 'yes', - test5: 'y', - test6: 'on', - }); - - const test1 = firebase.remoteConfig().getValue('test1').asBoolean(); - - const test2 = firebase.remoteConfig().getValue('test2').asBoolean(); - const test3 = firebase.remoteConfig().getValue('test3').asBoolean(); - const test4 = firebase.remoteConfig().getValue('test4').asBoolean(); - const test5 = firebase.remoteConfig().getValue('test5').asBoolean(); - const test6 = firebase.remoteConfig().getValue('test6').asBoolean(); - - test1.should.equal(true); - test2.should.equal(true); - test3.should.equal(true); - test4.should.equal(true); - test5.should.equal(true); - test6.should.equal(true); - }); - - it("returns 'false' for values that resolve to a falsy", async function () { - await firebase.remoteConfig().setDefaults({ - test1: '2', - test2: 'foo', - }); - - const test1 = firebase.remoteConfig().getValue('test1').asBoolean(); - - const test2 = firebase.remoteConfig().getValue('test2').asBoolean(); - - test1.should.equal(false); - test2.should.equal(false); - }); - - it("returns 'false' if the source is static", function () { - const unknownKey = firebase.remoteConfig().getValue('unknownKey').asBoolean(); - - unknownKey.should.equal(false); - }); - }); - - describe('getValue().asString()', function () { - it('returns the value as a string', function () { - const config = firebase.remoteConfig().getAll(); - - config.number.asString().should.equal('1337'); - config.float.asString().should.equal('123.456'); - config.prefix_1.asString().should.equal('1'); - config.bool.asString().should.equal('true'); - }); - }); - - describe('getValue().asNumber()', function () { - it('returns the value as a number if it can be evaluated as a number', function () { - const config = firebase.remoteConfig().getAll(); - - config.number.asNumber().should.equal(1337); - config.float.asNumber().should.equal(123.456); - config.prefix_1.asNumber().should.equal(1); - }); - - it('returns the value "0" if it cannot be evaluated as a number', function () { - const config = firebase.remoteConfig().getAll(); - - config.bool.asNumber().should.equal(0); - config.string.asNumber().should.equal(0); - }); - - it("returns '0' if the source is static", function () { - const unknownKey = firebase.remoteConfig().getValue('unknownKey').asNumber(); - - unknownKey.should.equal(0); - }); - }); - - describe('getValue().getSource()', function () { - it('returns the correct source as default or remote', async function () { - await firebase.remoteConfig().setDefaults({ - test1: '2', - test2: 'foo', - }); - - const config = firebase.remoteConfig().getAll(); - - config.number.getSource().should.equal('remote'); - config.bool.getSource().should.equal('remote'); - config.string.getSource().should.equal('remote'); - - config.test1.getSource().should.equal('default'); - config.test2.getSource().should.equal('default'); - }); - }); - - it("returns an empty string for a static value for keys that doesn't exist", function () { - const configValue = firebase.remoteConfig().getValue('fourOhFour'); - configValue.getSource().should.equal('static'); - should.equal(configValue.asString(), ''); - }); - - it('errors if no key provided', async function () { - try { - firebase.remoteConfig().getValue(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - - it('errors if key not a string', async function () { - try { - firebase.remoteConfig().getValue(1234); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql('must be a string'); - return Promise.resolve(); - } - }); - }); - - describe('getAll()', function () { - it('gets all values', async function () { - const config = firebase.remoteConfig().getAll(); - - config.should.be.a.Object(); - config.should.have.keys('bool', 'string', 'number'); - - const boolValue = config.bool.asBoolean(); - const stringValue = config.string.asString(); - const numberValue = config.number.asNumber(); - - boolValue.should.be.equal(true); - stringValue.should.be.equal('invertase'); - numberValue.should.be.equal(1337); - }); - }); - - describe('setDefaultsFromResource()', function () { - if (Platform.other) { - // Not supported on Web. - return; - } - - it('sets defaults from remote_config_resource_test file', async function () { - await firebase.remoteConfig().setDefaultsFromResource('remote_config_resource_test'); - const config = firebase.remoteConfig().getAll(); - config.company.getSource().should.equal('default'); - config.company.asString().should.equal('invertase'); - }); - - it('rejects if resource not found', async function () { - let error; - try { - await firebase.remoteConfig().setDefaultsFromResource('i_do_not_exist'); - } catch (e) { - error = e; - } - if (!error) { - throw new Error('Did not reject'); - } - // TODO dasherize error namespace - error.code.should.equal('remoteConfig/resource_not_found'); - error.message.should.containEql('was not found'); - }); - }); - - describe('defaultConfig', function () { - it('gets plain key/value object of defaults', async function () { - await firebase.remoteConfig().setDefaults({ - test_key: 'foo', - }); - - should(firebase.remoteConfig().defaultConfig.test_key).equal('foo'); - }); - }); - - describe('reset()', function () { - it('resets all activated, fetched and default config', async function () { - if (Platform.android) { - await firebase.remoteConfig().setDefaults({ - some_key: 'I do not exist', - }); - - const config = firebase.remoteConfig().getAll(); - - const remoteProps = ['some_key']; - - config.should.have.keys(...remoteProps); - - await firebase.remoteConfig().reset(); - - const configRetrieveAgain = firebase.remoteConfig().getAll(); - - should(configRetrieveAgain).not.have.properties(remoteProps); - } else { - this.skip(); - } - }); - - it('returns "undefined" as reset() API is not supported on iOS', async function () { - if (Platform.ios) { - should(await firebase.remoteConfig().reset()).equal(undefined); - } - }); - }); - }); - describe('modular', function () { describe('getRemoteConfig', function () { it('pass app as argument', function () { @@ -364,10 +28,9 @@ describe('remoteConfig()', function () { }); it('no app as argument', function () { - const { getApp } = modular; const { getRemoteConfig } = remoteConfigModular; - const remoteConfig = getRemoteConfig(getApp()); + const remoteConfig = getRemoteConfig(); remoteConfig.constructor.name.should.be.equal('FirebaseConfigModule'); }); diff --git a/packages/remote-config/lib/index.ts b/packages/remote-config/lib/index.ts index 2226cfc21d..9d1a496cd7 100644 --- a/packages/remote-config/lib/index.ts +++ b/packages/remote-config/lib/index.ts @@ -15,11 +15,675 @@ * */ -// Export modular API -export * from './modular'; -export type * from './types/remote-config'; - -// Export namespaced API -export type { FirebaseRemoteConfigTypes } from './types/namespaced'; -export { SDK_VERSION, firebase } from './namespaced'; -export { default } from './namespaced'; +import { + hasOwnProperty, + isFunction, + isIOS, + isNumber, + isObject, + isString, + isUndefined, + parseListenerOrObserver, +} from '@react-native-firebase/app/dist/module/common'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import NativeFirebaseError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import RemoteConfigValue from './RemoteConfigValue'; +import { LastFetchStatus, ValueSource } from './statics'; +import type { + ConfigUpdate, + ConfigUpdateObserver, + CustomSignals, + FetchStatus, + LogLevel, + RemoteConfig, + RemoteConfigSettings, + Unsubscribe, + Value, +} from './types/remote-config'; +import type { + CallbackOrObserver, + ConfigSettingsStateInternal, + NativeRemoteConfigConstants, + NativeRemoteConfigResult, + OnConfigUpdatedListenerCallback, + RemoteConfigInternal, + RemoteConfigUpdateErrorEventInternal, + RemoteConfigUpdateErrorInternal, + RemoteConfigUpdateSuccessEventInternal, + StoredConfigValueInternal, +} from './types/internal'; +import { version } from './version'; +import fallBackModule from './web/RNFBConfigModule'; +import './types/internal'; + +type ConfigDefaults = Record; +type ConfigSettings = { + minimumFetchIntervalMillis?: number; + fetchTimeMillis?: number; + fetchTimeoutMillis?: number; +}; +type ConfigValues = Record; + +const namespace = 'remoteConfig'; +const nativeModuleName = 'RNFBConfigModule' as const; + +function isSuccessEvent( + event: RemoteConfigUpdateSuccessEventInternal | RemoteConfigUpdateErrorEventInternal, +): event is RemoteConfigUpdateSuccessEventInternal { + return event.resultType === 'success'; +} + +function toConfigUpdate(updatedKeys: string[]): ConfigUpdate { + return { + getUpdatedKeys: () => new Set(updatedKeys), + }; +} + +function toNativeFirebaseError( + errorEvent: RemoteConfigUpdateErrorInternal, +): ReactNativeFirebase.NativeFirebaseError { + return NativeFirebaseError.fromEvent( + errorEvent, + namespace, + ) as ReactNativeFirebase.NativeFirebaseError; +} + +function rc(remoteConfig: RemoteConfig): RemoteConfigInternal { + return remoteConfig as RemoteConfigInternal; +} + +class FirebaseConfigModule extends FirebaseModule { + private _settings: ConfigSettingsStateInternal; + private _lastFetchTime: number; + private _lastFetchStatus: FetchStatus; + private _values: Record; + private _configUpdateListenerCount: number; + private _nativeMutationQueue: Promise; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); + this._settings = { + // defaults to 1 minute. + fetchTimeoutMillis: 60000, + // defaults to 12 hours. + minimumFetchIntervalMillis: 43200000, + }; + this._lastFetchTime = -1; + this._lastFetchStatus = 'no_fetch_yet'; + this._values = {}; + this._configUpdateListenerCount = 0; + this._nativeMutationQueue = Promise.resolve(); + } + + get defaultConfig(): ConfigDefaults { + const updatedDefaultConfig: ConfigDefaults = {}; + Object.keys(this._values).forEach(key => { + // Need to make it an object with key and literal value. Not `Value` instance. + const configValue = this._values[key]; + if (configValue) { + updatedDefaultConfig[key] = configValue.value; + } + }); + + return updatedDefaultConfig; + } + + set defaultConfig(defaults: ConfigDefaults) { + if (!isObject(defaults)) { + throw new Error("firebase.remoteConfig().defaultConfig: 'defaults' must be an object."); + } + + // To make Firebase web v9 API compatible, we update the config first so it immediately + // updates defaults on the instance. We then pass to underlying SDK to update. We do this because + // there is no way to "await" a setter. + const nonDefaultValues = Object.fromEntries( + Object.entries(this._values).filter(([, configValue]) => configValue?.source !== 'default'), + ); + + this._values = Object.freeze({ + ...Object.fromEntries( + Object.entries(defaults).map(([key, value]) => [ + key, + { value, source: 'default' as const }, + ]), + ), + ...nonDefaultValues, + }); + void this.setDefaults(defaults, true); + } + + get settings(): RemoteConfigSettings & { fetchTimeMillis: number } { + return { + minimumFetchIntervalMillis: this._settings.minimumFetchIntervalMillis, + fetchTimeoutMillis: this._settings.fetchTimeoutMillis, + fetchTimeMillis: this._settings.fetchTimeoutMillis, + }; + } + + set settings(settings: ConfigSettings | RemoteConfigSettings) { + // To make Firebase web v9 API compatible, we update the settings first so it immediately + // updates settings on the instance. We then pass to underlying SDK to update. We do this because + // there is no way to "await" a setter. We can't delegate to `setConfigSettings()` as it is setup + // for native. + this._settings = { + minimumFetchIntervalMillis: + settings.minimumFetchIntervalMillis ?? this._settings.minimumFetchIntervalMillis, + fetchTimeoutMillis: + ('fetchTimeoutMillis' in settings ? settings.fetchTimeoutMillis : undefined) ?? + ('fetchTimeMillis' in settings ? settings.fetchTimeMillis : undefined) ?? + this._settings.fetchTimeoutMillis, + }; + void this.setConfigSettings(settings, true); + } + + getValue(key: string): Value { + if (!isString(key)) { + throw new Error("firebase.remoteConfig().getValue(): 'key' must be a string value."); + } + + if (typeof this._values === 'undefined' || !hasOwnProperty(this._values, key)) { + return new RemoteConfigValue({ + value: '', + source: 'static', + }); + } + + const configValue = this._values[key]!; + return new RemoteConfigValue({ value: `${configValue.value}`, source: configValue.source }); + } + + getBoolean(key: string): boolean { + return this.getValue(key).asBoolean(); + } + + getNumber(key: string): number { + return this.getValue(key).asNumber(); + } + + getString(key: string): string { + return this.getValue(key).asString(); + } + + getAll(): ConfigValues { + const values: ConfigValues = {}; + Object.keys(this._values).forEach(key => { + values[key] = this.getValue(key); + }); + return values; + } + + get fetchTimeMillis(): number { + // android returns -1 if no fetch yet and iOS returns 0 + return this._lastFetchTime; + } + + get lastFetchStatus(): FetchStatus { + return this._lastFetchStatus; + } + + /** + * Deletes all activated, fetched and defaults configs and resets all Firebase Remote Config settings. + * @returns {Promise} + */ + reset(): Promise { + if (isIOS) { + return Promise.resolve(); + } + + return this._enqueueNativeMutation(() => this._promiseWithConstants(this.native.reset())); + } + + setConfigSettings( + settings: ConfigSettings | RemoteConfigSettings, + fromSettingsSetter = false, + ): Promise { + const updatedSettings: { + fetchTimeout: number; + minimumFetchInterval: number; + } = { + fetchTimeout: this._settings.fetchTimeoutMillis / 1000, + minimumFetchInterval: this._settings.minimumFetchIntervalMillis / 1000, + }; + + const apiCalled = fromSettingsSetter ? 'settings' : 'setConfigSettings'; + if (!isObject(settings)) { + throw new Error(`firebase.remoteConfig().${apiCalled}(*): settings must set an object.`); + } + + if (hasOwnProperty(settings, 'minimumFetchIntervalMillis')) { + if (!isNumber(settings.minimumFetchIntervalMillis)) { + throw new Error( + `firebase.remoteConfig().${apiCalled}(): 'settings.minimumFetchIntervalMillis' must be a number type in milliseconds.`, + ); + } else { + updatedSettings.minimumFetchInterval = settings.minimumFetchIntervalMillis / 1000; + } + } + + if (hasOwnProperty(settings, 'fetchTimeMillis')) { + if (!isNumber(settings.fetchTimeMillis)) { + throw new Error( + `firebase.remoteConfig().${apiCalled}(): 'settings.fetchTimeMillis' must be a number type in milliseconds.`, + ); + } + + updatedSettings.fetchTimeout = settings.fetchTimeMillis / 1000; + } else if (hasOwnProperty(settings, 'fetchTimeoutMillis')) { + if (!isNumber(settings.fetchTimeoutMillis)) { + throw new Error( + `firebase.remoteConfig().${apiCalled}(): 'settings.fetchTimeoutMillis' must be a number type in milliseconds.`, + ); + } + + updatedSettings.fetchTimeout = settings.fetchTimeoutMillis / 1000; + } + + const nextSettings = { + fetchTimeoutMillis: updatedSettings.fetchTimeout * 1000, + minimumFetchIntervalMillis: updatedSettings.minimumFetchInterval * 1000, + }; + + // Keep JS reads in sync immediately. Native can report stale settings constants + // for this call because native setConfigSettingsAsync completes after the bridge resolves. + this._settings = nextSettings; + + return this._enqueueNativeMutation(() => + this.native.setConfigSettings(updatedSettings).then(({ result, constants }) => { + // Preserve the eagerly computed settings above and only refresh the rest of the cache. + this._updateFromConstants({ + ...constants, + fetchTimeout: undefined, + minimumFetchInterval: undefined, + }); + return result; + }), + ); + } + + /** + * Activates the Fetched RemoteConfig, so that the fetched key-values take effect. + * @returns {Promise} + */ + activate(): Promise { + return this._promiseWithConstants(this.native.activate()); + } + + /** + * Fetches parameter values for your app. + * + * @param expirationDurationSeconds + * @returns {Promise} + */ + fetch(expirationDurationSeconds?: number): Promise { + if (!isUndefined(expirationDurationSeconds) && !isNumber(expirationDurationSeconds)) { + throw new Error( + "firebase.remoteConfig().fetch(): 'expirationDurationSeconds' must be a number value.", + ); + } + + return this._promiseWithConstants( + this.native.fetch(expirationDurationSeconds !== undefined ? expirationDurationSeconds : -1), + ); + } + + fetchAndActivate(): Promise { + return this._promiseWithConstants(this.native.fetchAndActivate()); + } + + ensureInitialized(): Promise { + return this._promiseWithConstants(this.native.ensureInitialized()); + } + + /** + * Sets defaults. + * + * @param defaults + */ + setDefaults(defaults: ConfigDefaults, fromDefaultConfigSetter = false): Promise { + const apiCalled = fromDefaultConfigSetter ? 'defaultConfig' : 'setDefaults'; + if (!isObject(defaults)) { + throw new Error(`firebase.remoteConfig().${apiCalled}(): 'defaults' must be an object.`); + } + + return this._enqueueNativeMutation(() => + this._promiseWithConstants(this.native.setDefaults(defaults)), + ); + } + + /** + * Sets defaults based on resource. + * @param resourceName + */ + setDefaultsFromResource(resourceName: string): Promise { + if (!isString(resourceName)) { + throw new Error( + "firebase.remoteConfig().setDefaultsFromResource(): 'resourceName' must be a string value.", + ); + } + + return this._enqueueNativeMutation(() => + this._promiseWithConstants(this.native.setDefaultsFromResource(resourceName)), + ); + } + + /** + * Registers an observer to changes in the configuration. + * + * @param observer - The observer to be notified of config updates. + * @returns An unsubscribe function to remove the listener. + */ + onConfigUpdate(observer: ConfigUpdateObserver): Unsubscribe { + if (!isObject(observer) || !isFunction(observer.next) || !isFunction(observer.error)) { + throw new Error("'observer' expected an object with 'next' and 'error' functions."); + } + + // We maintaine our pre-web-support native interface but bend it to match + // the official JS SDK API by assuming the callback is an Observer, and sending it a ConfigUpdate + // compatible parameter that implements the `getUpdatedKeys` method + let unsubscribed = false; + const subscription = this.emitter.addListener( + this.eventNameForApp('on_config_updated'), + (event: RemoteConfigUpdateSuccessEventInternal | RemoteConfigUpdateErrorEventInternal) => { + if (isSuccessEvent(event)) { + observer.next(toConfigUpdate(event.updatedKeys)); + return; + } + + observer.error(toNativeFirebaseError(event)); + }, + ); + + if (this._configUpdateListenerCount === 0) { + this.native.onConfigUpdated(); + } + + this._configUpdateListenerCount++; + + return () => { + if (unsubscribed) { + // there is no harm in calling this multiple times to unsubscribe, + // but anything after the first call is a no-op + return; + } + + unsubscribed = true; + subscription.remove(); + this._configUpdateListenerCount--; + + if (this._configUpdateListenerCount === 0) { + this.native.removeConfigUpdateRegistration(); + } + }; + } + + /** + * Registers a listener to changes in the configuration. + * + * @param listenerOrObserver - function called on config change + * @returns unsubscribe listener + * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime + */ + onConfigUpdated(listenerOrObserver: unknown): Unsubscribe { + const listener = parseListenerOrObserver( + listenerOrObserver as CallbackOrObserver, + ) as (event?: { updatedKeys: string[] }, error?: RemoteConfigUpdateErrorInternal) => void; + let unsubscribed = false; + const subscription = this.emitter.addListener( + this.eventNameForApp('on_config_updated'), + (event: RemoteConfigUpdateSuccessEventInternal | RemoteConfigUpdateErrorEventInternal) => { + if (isSuccessEvent(event)) { + listener({ updatedKeys: event.updatedKeys }, undefined); + return; + } + + listener(undefined, { + code: event.code, + message: event.message, + nativeErrorMessage: event.nativeErrorMessage, + }); + }, + ); + + if (this._configUpdateListenerCount === 0) { + this.native.onConfigUpdated(); + } + + this._configUpdateListenerCount++; + + return () => { + if (unsubscribed) { + // there is no harm in calling this multiple times to unsubscribe, + // but anything after the first call is a no-op + return; + } + + unsubscribed = true; + subscription.remove(); + this._configUpdateListenerCount--; + + if (this._configUpdateListenerCount === 0) { + this.native.removeConfigUpdateRegistration(); + } + }; + } + + private _updateFromConstants(constants: NativeRemoteConfigConstants): void { + // Wrapped this as we update using sync getters initially for `defaultConfig` & `settings` + if (constants.lastFetchTime !== undefined) { + this._lastFetchTime = constants.lastFetchTime; + } + + // Wrapped this as we update using sync getters initially for `defaultConfig` & `settings` + if (constants.lastFetchStatus !== undefined) { + this._lastFetchStatus = constants.lastFetchStatus; + } + + if (constants.fetchTimeout !== undefined && constants.minimumFetchInterval !== undefined) { + this._settings = { + fetchTimeoutMillis: constants.fetchTimeout * 1000, + minimumFetchIntervalMillis: constants.minimumFetchInterval * 1000, + }; + } + + if (constants.values !== undefined) { + this._values = Object.freeze(constants.values); + } + } + + private _promiseWithConstants(promise: Promise>): Promise { + return promise.then(({ result, constants }) => { + this._updateFromConstants(constants); + return result; + }); + } + + private _enqueueNativeMutation(task: () => Promise): Promise { + // Some callers (like property setters) discard the returned promise; serialize all mutations + // so later writes like reset() cannot be overwritten by an earlier async completion. + const next = this._nativeMutationQueue.then(task, task); + this._nativeMutationQueue = next.then( + () => undefined, + () => undefined, + ); + return next; + } +} + +const config: ModuleConfig = { + namespace, + nativeModuleName, + nativeEvents: ['on_config_updated'], + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: false, +}; + +export { LastFetchStatus, ValueSource }; +export const SDK_VERSION = version; + +/** + * Returns a RemoteConfig instance for the given app. + * @param app - FirebaseApp. Optional. + */ +export function getRemoteConfig(app?: FirebaseApp): RemoteConfig { + return getOrCreateModularInstance(FirebaseConfigModule, config, app) as unknown as RemoteConfig; +} + +/** + * Returns a Boolean which resolves to true if the current call + * activated the fetched configs. + */ +export function activate(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).activate(); +} + +/** + * Ensures the last activated config are available to the getters. + */ +export function ensureInitialized(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).ensureInitialized(); +} + +/** + * Performs a fetch and returns a Boolean which resolves to true + * if the current call activated the fetched configs. + */ +export function fetchAndActivate(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).fetchAndActivate(); +} + +/** + * Fetches and caches configuration from the Remote Config service. + */ +export function fetchConfig(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).fetch(); +} + +/** + * Gets all config. + */ +export function getAll(remoteConfig: RemoteConfig): Record { + return rc(remoteConfig).getAll(); +} + +/** + * Gets the value for the given key as a boolean. + */ +export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean { + return rc(remoteConfig).getBoolean(key); +} + +/** + * Gets the value for the given key as a number. + */ +export function getNumber(remoteConfig: RemoteConfig, key: string): number { + return rc(remoteConfig).getNumber(key); +} + +/** + * Gets the value for the given key as a string. + */ +export function getString(remoteConfig: RemoteConfig, key: string): string { + return rc(remoteConfig).getString(key); +} + +/** + * Gets the value for the given key. + */ +export function getValue(remoteConfig: RemoteConfig, key: string): Value { + return rc(remoteConfig).getValue(key); +} + +/** + * Defines the log level to use. + */ +export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void { + void remoteConfig; + void logLevel; + // Intentionally ignored on native. The modular API matches the JS SDK and returns void. +} + +/** + * Checks two different things. + * 1. Check if IndexedDB exists in the browser environment. + * 2. Check if the current browser context allows IndexedDB open() calls. + */ +export function isSupported(): Promise { + // always return "true" for now. Web only. + return Promise.resolve(true); +} + +/** + * Deletes all activated, fetched and defaults configs and + * resets all Firebase Remote Config settings. + * Android only. iOS does not reset anything. + */ +export function reset(remoteConfig: RemoteConfig): Promise { + return rc(remoteConfig).reset(); +} + +/** + * Sets defaults based on a native resource. + */ +export function setDefaultsFromResource( + remoteConfig: RemoteConfig, + resourceName: string, +): Promise { + return rc(remoteConfig).setDefaultsFromResource(resourceName); +} + +/** + * Registers a listener to changes in the configuration. + * + */ +export function onConfigUpdate( + remoteConfig: RemoteConfig, + observer: ConfigUpdateObserver, +): Unsubscribe { + return rc(remoteConfig).onConfigUpdate(observer); +} + +/** + * Sets the custom signals for the app instance. + */ +export async function setCustomSignals( + remoteConfig: RemoteConfig, + customSignals: CustomSignals, +): Promise { + for (const [key, value] of Object.entries(customSignals)) { + if (typeof value !== 'string' && typeof value !== 'number' && value !== null) { + throw new Error( + `firebase.remoteConfig().setCustomSignals(): Invalid type for custom signal '${key}': ${typeof value}. Expected 'string', 'number', or 'null'.`, + ); + } + } + + return rc(remoteConfig)._promiseWithConstants( + rc(remoteConfig).native.setCustomSignals(customSignals), + ); +} + +export type { + ConfigUpdate, + ConfigUpdateObserver, + CustomSignals, + FetchStatus, + LogLevel, + RemoteConfig, + RemoteConfigSettings, + Unsubscribe, + Value, +} from './types/remote-config'; + +// Register the interop module for non-native platforms. +setReactNativeModule(nativeModuleName, fallBackModule as unknown as Record); diff --git a/packages/remote-config/lib/modular.ts b/packages/remote-config/lib/modular.ts deleted file mode 100644 index 51ec3a7d9f..0000000000 --- a/packages/remote-config/lib/modular.ts +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { FirebaseApp } from '@firebase/app'; -import { getApp } from '@react-native-firebase/app'; -import { - MODULAR_DEPRECATION_ARG, - withModularFlag, -} from '@react-native-firebase/app/dist/module/common'; -import type { - ConfigUpdateObserver, - CustomSignals, - LogLevel, - RemoteConfig, - Unsubscribe, - Value, -} from './types/remote-config'; -import type { AppWithRemoteConfigInternal, RemoteConfigInternal } from './types/internal'; - -function rc(remoteConfig: RemoteConfig): RemoteConfigInternal { - return remoteConfig as RemoteConfigInternal; -} - -export type { CustomSignals } from './types/remote-config'; - -/** - * Returns a RemoteConfig instance for the given app. - * @param app - FirebaseApp. Optional. - */ -export function getRemoteConfig(app?: FirebaseApp): RemoteConfig { - if (app) { - return withModularFlag(() => - (getApp(app.name) as unknown as AppWithRemoteConfigInternal).remoteConfig( - MODULAR_DEPRECATION_ARG, - ), - ); - } - - return withModularFlag(() => - (getApp() as unknown as AppWithRemoteConfigInternal).remoteConfig(MODULAR_DEPRECATION_ARG), - ); -} - -/** - * Returns a Boolean which resolves to true if the current call - * activated the fetched configs. - */ -export function activate(remoteConfig: RemoteConfig): Promise { - return rc(remoteConfig).activate.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Ensures the last activated config are available to the getters. - */ -export function ensureInitialized(remoteConfig: RemoteConfig): Promise { - return rc(remoteConfig).ensureInitialized.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Performs a fetch and returns a Boolean which resolves to true - * if the current call activated the fetched configs. - */ -export function fetchAndActivate(remoteConfig: RemoteConfig): Promise { - return rc(remoteConfig).fetchAndActivate.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Fetches and caches configuration from the Remote Config service. - */ -export function fetchConfig(remoteConfig: RemoteConfig): Promise { - return rc(remoteConfig).fetch.call(remoteConfig, undefined, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets all config. - */ -export function getAll(remoteConfig: RemoteConfig): Record { - return rc(remoteConfig).getAll.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key as a boolean. - */ -export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean { - return rc(remoteConfig).getBoolean.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key as a number. - */ -export function getNumber(remoteConfig: RemoteConfig, key: string): number { - return rc(remoteConfig).getNumber.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key as a string. - */ -export function getString(remoteConfig: RemoteConfig, key: string): string { - return rc(remoteConfig).getString.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Gets the value for the given key. - */ -export function getValue(remoteConfig: RemoteConfig, key: string): Value { - return rc(remoteConfig).getValue.call(remoteConfig, key, MODULAR_DEPRECATION_ARG); -} - -/** - * Defines the log level to use. - */ -export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void { - void remoteConfig; - void logLevel; - // Intentionally ignored on native. The modular API matches the JS SDK and returns void. -} - -/** - * Checks two different things. - * 1. Check if IndexedDB exists in the browser environment. - * 2. Check if the current browser context allows IndexedDB open() calls. - */ -export function isSupported(): Promise { - // always return "true" for now. Web only. - return Promise.resolve(true); -} - -/** - * Deletes all activated, fetched and defaults configs and - * resets all Firebase Remote Config settings. - * Android only. iOS does not reset anything. - */ -export function reset(remoteConfig: RemoteConfig): Promise { - return rc(remoteConfig).reset.call(remoteConfig, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets defaults based on a native resource. - */ -export function setDefaultsFromResource( - remoteConfig: RemoteConfig, - resourceName: string, -): Promise { - return rc(remoteConfig).setDefaultsFromResource.call( - remoteConfig, - resourceName, - MODULAR_DEPRECATION_ARG, - ); -} - -/** - * Registers a listener to changes in the configuration. - * - */ -export function onConfigUpdate( - remoteConfig: RemoteConfig, - observer: ConfigUpdateObserver, -): Unsubscribe { - return rc(remoteConfig).onConfigUpdate.call(remoteConfig, observer, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the custom signals for the app instance. - */ -export async function setCustomSignals( - remoteConfig: RemoteConfig, - customSignals: CustomSignals, -): Promise { - for (const [key, value] of Object.entries(customSignals)) { - if (typeof value !== 'string' && typeof value !== 'number' && value !== null) { - throw new Error( - `firebase.remoteConfig().setCustomSignals(): Invalid type for custom signal '${key}': ${typeof value}. Expected 'string', 'number', or 'null'.`, - ); - } - } - - return withModularFlag(() => - rc(remoteConfig)._promiseWithConstants( - rc(remoteConfig).native.setCustomSignals(customSignals), - MODULAR_DEPRECATION_ARG, - ), - ); -} diff --git a/packages/remote-config/lib/namespaced.ts b/packages/remote-config/lib/namespaced.ts deleted file mode 100644 index be5af1370b..0000000000 --- a/packages/remote-config/lib/namespaced.ts +++ /dev/null @@ -1,552 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { - hasOwnProperty, - isFunction, - isIOS, - isNumber, - isObject, - isString, - isUndefined, - parseListenerOrObserver, -} from '@react-native-firebase/app/dist/module/common'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, - type ModuleConfig, -} from '@react-native-firebase/app/dist/module/internal'; -import NativeFirebaseError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import RemoteConfigValue from './RemoteConfigValue'; -import { LastFetchStatus, ValueSource } from './statics'; -import type { - ConfigUpdate, - ConfigUpdateObserver, - FetchStatus, - RemoteConfigSettings, -} from './types/remote-config'; -import type { FirebaseRemoteConfigTypes } from './types/namespaced'; -import type { - ConfigSettingsStateInternal, - NativeRemoteConfigConstants, - NativeRemoteConfigResult, - RemoteConfigUpdateErrorEventInternal, - RemoteConfigUpdateErrorInternal, - RemoteConfigUpdateSuccessEventInternal, - StoredConfigValueInternal, -} from './types/internal'; -import { version } from './version'; -import fallBackModule from './web/RNFBConfigModule'; - -type ConfigDefaults = FirebaseRemoteConfigTypes.ConfigDefaults; -type ConfigSettings = FirebaseRemoteConfigTypes.ConfigSettings; -type ConfigValue = FirebaseRemoteConfigTypes.ConfigValue; -type ConfigValues = FirebaseRemoteConfigTypes.ConfigValues; -type LastFetchStatusType = FirebaseRemoteConfigTypes.LastFetchStatusType; - -function isSuccessEvent( - event: RemoteConfigUpdateSuccessEventInternal | RemoteConfigUpdateErrorEventInternal, -): event is RemoteConfigUpdateSuccessEventInternal { - return event.resultType === 'success'; -} - -function toConfigUpdate(updatedKeys: string[]): ConfigUpdate { - return { - getUpdatedKeys: () => new Set(updatedKeys), - }; -} - -function toNativeFirebaseError( - errorEvent: RemoteConfigUpdateErrorInternal, -): ReactNativeFirebase.NativeFirebaseError { - return NativeFirebaseError.fromEvent( - errorEvent, - namespace, - ) as ReactNativeFirebase.NativeFirebaseError; -} - -const statics = { - LastFetchStatus, - ValueSource, -}; - -const namespace = 'remoteConfig'; -const nativeModuleName = 'RNFBConfigModule' as const; - -class FirebaseConfigModule extends FirebaseModule { - private _settings: ConfigSettingsStateInternal; - private _lastFetchTime: number; - private _lastFetchStatus: FetchStatus; - private _values: Record; - private _configUpdateListenerCount: number; - private _nativeMutationQueue: Promise; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - customUrlOrRegion?: string | null, - ) { - super(app, config, customUrlOrRegion); - this._settings = { - // defaults to 1 minute. - fetchTimeoutMillis: 60000, - // defaults to 12 hours. - minimumFetchIntervalMillis: 43200000, - }; - this._lastFetchTime = -1; - this._lastFetchStatus = 'no_fetch_yet'; - this._values = {}; - this._configUpdateListenerCount = 0; - this._nativeMutationQueue = Promise.resolve(); - } - - get defaultConfig(): ConfigDefaults { - const updatedDefaultConfig: ConfigDefaults = {}; - Object.keys(this._values).forEach(key => { - // Need to make it an object with key and literal value. Not `Value` instance. - const configValue = this._values[key]; - if (configValue) { - updatedDefaultConfig[key] = configValue.value; - } - }); - - return updatedDefaultConfig; - } - - set defaultConfig(defaults: ConfigDefaults) { - if (!isObject(defaults)) { - throw new Error("firebase.remoteConfig().defaultConfig: 'defaults' must be an object."); - } - - // To make Firebase web v9 API compatible, we update the config first so it immediately - // updates defaults on the instance. We then pass to underlying SDK to update. We do this because - // there is no way to "await" a setter. - const nonDefaultValues = Object.fromEntries( - Object.entries(this._values).filter(([, configValue]) => configValue?.source !== 'default'), - ); - - this._values = Object.freeze({ - ...Object.fromEntries( - Object.entries(defaults).map(([key, value]) => [ - key, - { value, source: 'default' as const }, - ]), - ), - ...nonDefaultValues, - }); - void this.setDefaults(defaults, true); - } - - get settings(): RemoteConfigSettings & { fetchTimeMillis: number } { - return { - minimumFetchIntervalMillis: this._settings.minimumFetchIntervalMillis, - fetchTimeoutMillis: this._settings.fetchTimeoutMillis, - fetchTimeMillis: this._settings.fetchTimeoutMillis, - }; - } - - set settings(settings: ConfigSettings | RemoteConfigSettings) { - // To make Firebase web v9 API compatible, we update the settings first so it immediately - // updates settings on the instance. We then pass to underlying SDK to update. We do this because - // there is no way to "await" a setter. We can't delegate to `setConfigSettings()` as it is setup - // for native. - this._settings = { - minimumFetchIntervalMillis: - settings.minimumFetchIntervalMillis ?? this._settings.minimumFetchIntervalMillis, - fetchTimeoutMillis: - ('fetchTimeoutMillis' in settings ? settings.fetchTimeoutMillis : undefined) ?? - ('fetchTimeMillis' in settings ? settings.fetchTimeMillis : undefined) ?? - this._settings.fetchTimeoutMillis, - }; - void this.setConfigSettings(settings, true); - } - - getValue(key: string): ConfigValue { - if (!isString(key)) { - throw new Error("firebase.remoteConfig().getValue(): 'key' must be a string value."); - } - - if (typeof this._values === 'undefined' || !hasOwnProperty(this._values, key)) { - return new RemoteConfigValue({ - value: '', - source: 'static', - }); - } - - const configValue = this._values[key]!; - return new RemoteConfigValue({ value: `${configValue.value}`, source: configValue.source }); - } - - getBoolean(key: string): boolean { - return this.getValue(key).asBoolean(); - } - - getNumber(key: string): number { - return this.getValue(key).asNumber(); - } - - getString(key: string): string { - return this.getValue(key).asString(); - } - - getAll(): ConfigValues { - const values: ConfigValues = {}; - Object.keys(this._values).forEach(key => { - values[key] = this.getValue(key); - }); - return values; - } - - get fetchTimeMillis(): number { - // android returns -1 if no fetch yet and iOS returns 0 - return this._lastFetchTime; - } - - get lastFetchStatus(): LastFetchStatusType { - return this._lastFetchStatus; - } - - /** - * Deletes all activated, fetched and defaults configs and resets all Firebase Remote Config settings. - * @returns {Promise} - */ - reset(): Promise { - if (isIOS) { - return Promise.resolve(); - } - - return this._enqueueNativeMutation(() => this._promiseWithConstants(this.native.reset())); - } - - setConfigSettings( - settings: ConfigSettings | RemoteConfigSettings, - fromSettingsSetter = false, - ): Promise { - const updatedSettings: { - fetchTimeout: number; - minimumFetchInterval: number; - } = { - fetchTimeout: this._settings.fetchTimeoutMillis / 1000, - minimumFetchInterval: this._settings.minimumFetchIntervalMillis / 1000, - }; - - const apiCalled = fromSettingsSetter ? 'settings' : 'setConfigSettings'; - if (!isObject(settings)) { - throw new Error(`firebase.remoteConfig().${apiCalled}(*): settings must set an object.`); - } - - if (hasOwnProperty(settings, 'minimumFetchIntervalMillis')) { - if (!isNumber(settings.minimumFetchIntervalMillis)) { - throw new Error( - `firebase.remoteConfig().${apiCalled}(): 'settings.minimumFetchIntervalMillis' must be a number type in milliseconds.`, - ); - } else { - updatedSettings.minimumFetchInterval = settings.minimumFetchIntervalMillis / 1000; - } - } - - if (hasOwnProperty(settings, 'fetchTimeMillis')) { - if (!isNumber(settings.fetchTimeMillis)) { - throw new Error( - `firebase.remoteConfig().${apiCalled}(): 'settings.fetchTimeMillis' must be a number type in milliseconds.`, - ); - } - - updatedSettings.fetchTimeout = settings.fetchTimeMillis / 1000; - } else if (hasOwnProperty(settings, 'fetchTimeoutMillis')) { - if (!isNumber(settings.fetchTimeoutMillis)) { - throw new Error( - `firebase.remoteConfig().${apiCalled}(): 'settings.fetchTimeoutMillis' must be a number type in milliseconds.`, - ); - } - - updatedSettings.fetchTimeout = settings.fetchTimeoutMillis / 1000; - } - - const nextSettings = { - fetchTimeoutMillis: updatedSettings.fetchTimeout * 1000, - minimumFetchIntervalMillis: updatedSettings.minimumFetchInterval * 1000, - }; - - // Keep JS reads in sync immediately. Native can report stale settings constants - // for this call because native setConfigSettingsAsync completes after the bridge resolves. - this._settings = nextSettings; - - return this._enqueueNativeMutation(() => - this.native.setConfigSettings(updatedSettings).then(({ result, constants }) => { - // Preserve the eagerly computed settings above and only refresh the rest of the cache. - this._updateFromConstants({ - ...constants, - fetchTimeout: undefined, - minimumFetchInterval: undefined, - }); - return result; - }), - ); - } - - /** - * Activates the Fetched RemoteConfig, so that the fetched key-values take effect. - * @returns {Promise} - */ - activate(): Promise { - return this._promiseWithConstants(this.native.activate()); - } - - /** - * Fetches parameter values for your app. - * - * @param expirationDurationSeconds - * @returns {Promise} - */ - fetch(expirationDurationSeconds?: number): Promise { - if (!isUndefined(expirationDurationSeconds) && !isNumber(expirationDurationSeconds)) { - throw new Error( - "firebase.remoteConfig().fetch(): 'expirationDurationSeconds' must be a number value.", - ); - } - - return this._promiseWithConstants( - this.native.fetch(expirationDurationSeconds !== undefined ? expirationDurationSeconds : -1), - ); - } - - fetchAndActivate(): Promise { - return this._promiseWithConstants(this.native.fetchAndActivate()); - } - - ensureInitialized(): Promise { - return this._promiseWithConstants(this.native.ensureInitialized()); - } - - /** - * Sets defaults. - * - * @param defaults - */ - setDefaults(defaults: ConfigDefaults, fromDefaultConfigSetter = false): Promise { - const apiCalled = fromDefaultConfigSetter ? 'defaultConfig' : 'setDefaults'; - if (!isObject(defaults)) { - throw new Error(`firebase.remoteConfig().${apiCalled}(): 'defaults' must be an object.`); - } - - return this._enqueueNativeMutation(() => - this._promiseWithConstants(this.native.setDefaults(defaults)), - ); - } - - /** - * Sets defaults based on resource. - * @param resourceName - */ - setDefaultsFromResource(resourceName: string): Promise { - if (!isString(resourceName)) { - throw new Error( - "firebase.remoteConfig().setDefaultsFromResource(): 'resourceName' must be a string value.", - ); - } - - return this._enqueueNativeMutation(() => - this._promiseWithConstants(this.native.setDefaultsFromResource(resourceName)), - ); - } - - /** - * Registers an observer to changes in the configuration. - * - * @param observer - The observer to be notified of config updates. - * @returns An unsubscribe function to remove the listener. - */ - onConfigUpdate(observer: ConfigUpdateObserver): () => void { - if (!isObject(observer) || !isFunction(observer.next) || !isFunction(observer.error)) { - throw new Error("'observer' expected an object with 'next' and 'error' functions."); - } - - // We maintaine our pre-web-support native interface but bend it to match - // the official JS SDK API by assuming the callback is an Observer, and sending it a ConfigUpdate - // compatible parameter that implements the `getUpdatedKeys` method - let unsubscribed = false; - const subscription = this.emitter.addListener( - this.eventNameForApp('on_config_updated'), - (event: RemoteConfigUpdateSuccessEventInternal | RemoteConfigUpdateErrorEventInternal) => { - if (isSuccessEvent(event)) { - observer.next(toConfigUpdate(event.updatedKeys)); - return; - } - - observer.error(toNativeFirebaseError(event)); - }, - ); - - if (this._configUpdateListenerCount === 0) { - this.native.onConfigUpdated(); - } - - this._configUpdateListenerCount++; - - return () => { - if (unsubscribed) { - // there is no harm in calling this multiple times to unsubscribe, - // but anything after the first call is a no-op - return; - } - - unsubscribed = true; - subscription.remove(); - this._configUpdateListenerCount--; - - if (this._configUpdateListenerCount === 0) { - this.native.removeConfigUpdateRegistration(); - } - }; - } - - /** - * Registers a listener to changes in the configuration. - * - * @param listenerOrObserver - function called on config change - * @returns unsubscribe listener - * @deprecated use official firebase-js-sdk onConfigUpdate now that web supports realtime - */ - onConfigUpdated(listenerOrObserver: unknown): () => void { - const listener = parseListenerOrObserver( - listenerOrObserver as FirebaseRemoteConfigTypes.CallbackOrObserver, - ) as (event?: { updatedKeys: string[] }, error?: RemoteConfigUpdateErrorInternal) => void; - let unsubscribed = false; - const subscription = this.emitter.addListener( - this.eventNameForApp('on_config_updated'), - (event: RemoteConfigUpdateSuccessEventInternal | RemoteConfigUpdateErrorEventInternal) => { - if (isSuccessEvent(event)) { - listener({ updatedKeys: event.updatedKeys }, undefined); - return; - } - - listener(undefined, { - code: event.code, - message: event.message, - nativeErrorMessage: event.nativeErrorMessage, - }); - }, - ); - - if (this._configUpdateListenerCount === 0) { - this.native.onConfigUpdated(); - } - - this._configUpdateListenerCount++; - - return () => { - if (unsubscribed) { - // there is no harm in calling this multiple times to unsubscribe, - // but anything after the first call is a no-op - return; - } - - unsubscribed = true; - subscription.remove(); - this._configUpdateListenerCount--; - - if (this._configUpdateListenerCount === 0) { - this.native.removeConfigUpdateRegistration(); - } - }; - } - - private _updateFromConstants(constants: NativeRemoteConfigConstants): void { - // Wrapped this as we update using sync getters initially for `defaultConfig` & `settings` - if (constants.lastFetchTime !== undefined) { - this._lastFetchTime = constants.lastFetchTime; - } - - // Wrapped this as we update using sync getters initially for `defaultConfig` & `settings` - if (constants.lastFetchStatus !== undefined) { - this._lastFetchStatus = constants.lastFetchStatus; - } - - if (constants.fetchTimeout !== undefined && constants.minimumFetchInterval !== undefined) { - this._settings = { - fetchTimeoutMillis: constants.fetchTimeout * 1000, - minimumFetchIntervalMillis: constants.minimumFetchInterval * 1000, - }; - } - - if (constants.values !== undefined) { - this._values = Object.freeze(constants.values); - } - } - - private _promiseWithConstants(promise: Promise>): Promise { - return promise.then(({ result, constants }) => { - this._updateFromConstants(constants); - return result; - }); - } - - private _enqueueNativeMutation(task: () => Promise): Promise { - // Some callers (like property setters) discard the returned promise; serialize all mutations - // so later writes like reset() cannot be overwritten by an earlier async completion. - const next = this._nativeMutationQueue.then(task, task); - this._nativeMutationQueue = next.then( - () => undefined, - () => undefined, - ); - return next; - } -} - -// import { SDK_VERSION } from '@react-native-firebase/remote-config'; -export const SDK_VERSION = version; - -const remoteConfigNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeModuleName, - nativeEvents: ['on_config_updated'], - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: false, - ModuleClass: FirebaseConfigModule, -}); - -type RemoteConfigNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseRemoteConfigTypes.Module, - FirebaseRemoteConfigTypes.Statics -> & { - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import remoteConfig from '@react-native-firebase/remote-config'; -// remoteConfig().X(...); -export default remoteConfigNamespace as unknown as RemoteConfigNamespace; - -// import remoteConfig, { firebase } from '@react-native-firebase/remote-config'; -// remoteConfig().X(...); -// firebase.remoteConfig().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'remoteConfig', - FirebaseRemoteConfigTypes.Module, - FirebaseRemoteConfigTypes.Statics, - false - >; - -// Register the interop module for non-native platforms. -setReactNativeModule(nativeModuleName, fallBackModule as unknown as Record); diff --git a/packages/remote-config/lib/types/internal.ts b/packages/remote-config/lib/types/internal.ts index 2e289c99a7..0c6cb54184 100644 --- a/packages/remote-config/lib/types/internal.ts +++ b/packages/remote-config/lib/types/internal.ts @@ -2,7 +2,7 @@ * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -24,21 +24,17 @@ import type { Value, ValueSource, } from './remote-config'; -import type { FirebaseRemoteConfigTypes } from './namespaced'; export type ConfigValueSourceInternal = ValueSource; export type LastFetchStatusInternal = FetchStatus; -export type RemoteConfigModularDeprecationArg = string; +export type OnConfigUpdatedListenerCallback = ( + event?: { updatedKeys: string[] }, + error?: RemoteConfigUpdateErrorInternal, +) => void; -export type WithRemoteConfigDeprecationArg = F extends (...args: infer P) => infer R - ? (...args: [...P, RemoteConfigModularDeprecationArg?]) => R - : never; - -export interface AppWithRemoteConfigInternal { - remoteConfig(deprecationArg?: RemoteConfigModularDeprecationArg): RemoteConfig; -} +export type CallbackOrObserver any> = T | { next: T }; export interface ConfigSettingsStateInternal { fetchTimeoutMillis: number; @@ -106,19 +102,16 @@ export interface RemoteConfigInternal extends RemoteConfig { defaultConfig: { [key: string]: string | number | boolean; }; - activate(deprecationArg?: RemoteConfigModularDeprecationArg): Promise; - ensureInitialized(deprecationArg?: RemoteConfigModularDeprecationArg): Promise; - fetchAndActivate(deprecationArg?: RemoteConfigModularDeprecationArg): Promise; - fetch( - expirationDurationSeconds?: number, - deprecationArg?: RemoteConfigModularDeprecationArg, - ): Promise; - getAll(deprecationArg?: RemoteConfigModularDeprecationArg): Record; - getBoolean(key: string, deprecationArg?: RemoteConfigModularDeprecationArg): boolean; - getNumber(key: string, deprecationArg?: RemoteConfigModularDeprecationArg): number; - getString(key: string, deprecationArg?: RemoteConfigModularDeprecationArg): string; - getValue(key: string, deprecationArg?: RemoteConfigModularDeprecationArg): Value; - reset(deprecationArg?: RemoteConfigModularDeprecationArg): Promise; + activate(): Promise; + ensureInitialized(): Promise; + fetchAndActivate(): Promise; + fetch(expirationDurationSeconds?: number): Promise; + getAll(): Record; + getBoolean(key: string): boolean; + getNumber(key: string): number; + getString(key: string): string; + getValue(key: string): Value; + reset(): Promise; setConfigSettings( settings: | RemoteConfigSettings @@ -127,35 +120,22 @@ export interface RemoteConfigInternal extends RemoteConfig { fetchTimeMillis?: number; fetchTimeoutMillis?: number; }, - deprecationArg?: RemoteConfigModularDeprecationArg, + fromSettingsSetter?: boolean, ): Promise; setDefaults( defaults: { [key: string]: string | number | boolean; }, - deprecationArg?: RemoteConfigModularDeprecationArg, + fromDefaultConfigSetter?: boolean, ): Promise; - setDefaultsFromResource( - resourceName: string, - deprecationArg?: RemoteConfigModularDeprecationArg, - ): Promise; - onConfigUpdate( - observer: ConfigUpdateObserver, - deprecationArg?: RemoteConfigModularDeprecationArg, - ): Unsubscribe; + setDefaultsFromResource(resourceName: string): Promise; + onConfigUpdate(observer: ConfigUpdateObserver): Unsubscribe; onConfigUpdated( - listenerOrObserver: FirebaseRemoteConfigTypes.CallbackOrObserver, - deprecationArg?: RemoteConfigModularDeprecationArg, + listenerOrObserver: CallbackOrObserver, ): Unsubscribe; readonly native: RNFBConfigModule; - _promiseWithConstants( - promise: Promise>, - deprecationArg?: RemoteConfigModularDeprecationArg, - ): Promise; - _promiseWithConstants( - promise: Promise>, - deprecationArg?: RemoteConfigModularDeprecationArg, - ): Promise; + _promiseWithConstants(promise: Promise>): Promise; + _promiseWithConstants(promise: Promise>): Promise; } declare module '@react-native-firebase/app/dist/module/internal/NativeModules' { diff --git a/packages/remote-config/lib/types/namespaced.ts b/packages/remote-config/lib/types/namespaced.ts deleted file mode 100644 index 9182bc32ba..0000000000 --- a/packages/remote-config/lib/types/namespaced.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ReactNativeFirebase } from '@react-native-firebase/app'; - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace FirebaseRemoteConfigTypes { - import FirebaseModule = ReactNativeFirebase.FirebaseModule; - - /** - * @deprecated Use modular log-level types instead. - */ - export type RemoteConfigLogLevel = 'debug' | 'error' | 'silent'; - - /** - * @deprecated Use modular constants instead. - */ - export interface LastFetchStatus { - SUCCESS: 'success'; - FAILURE: 'failure'; - THROTTLED: 'throttled'; - NO_FETCH_YET: 'no_fetch_yet'; - } - - /** - * @deprecated Use modular constants instead. - */ - export interface ValueSource { - REMOTE: 'remote'; - DEFAULT: 'default'; - STATIC: 'static'; - } - - /** - * @deprecated Namespaced statics remain for backwards compatibility. - */ - export interface Statics { - ValueSource: ValueSource; - LastFetchStatus: LastFetchStatus; - SDK_VERSION: string; - } - - /** - * @deprecated Use modular `Value` access patterns instead. - */ - export interface ConfigValue { - getSource(): 'remote' | 'default' | 'static'; - asBoolean(): true | false; - asNumber(): number; - asString(): string; - } - - /** - * @deprecated Use modular APIs instead. - */ - export interface ConfigValues { - [key: string]: ConfigValue; - } - - /** - * @deprecated Use modular APIs instead. - */ - export interface ConfigSettings { - minimumFetchIntervalMillis?: number; - fetchTimeMillis?: number; - } - - /** - * @deprecated Use modular APIs instead. - */ - export interface ConfigDefaults { - [key: string]: number | string | boolean; - } - - export type LastFetchStatusType = 'success' | 'failure' | 'no_fetch_yet' | 'throttled'; - - export interface ConfigUpdate { - getUpdatedKeys(): Set; - } - - export interface ConfigUpdateObserver { - next: (configUpdate: ConfigUpdate) => void; - error: (error: ReactNativeFirebase.NativeFirebaseError) => void; - complete: () => void; - } - - export type Unsubscribe = () => void; - - /** - * @deprecated Use the package default export or modular APIs instead. - */ - export declare class Module extends FirebaseModule { - app: ReactNativeFirebase.FirebaseApp; - fetchTimeMillis: number; - lastFetchStatus: LastFetchStatusType; - settings: ConfigSettings; - defaultConfig: ConfigDefaults; - setConfigSettings(configSettings: ConfigSettings): Promise; - setDefaults(defaults: ConfigDefaults): Promise; - setDefaultsFromResource(resourceName: string): Promise; - onConfigUpdate(observer: ConfigUpdateObserver): Unsubscribe; - onConfigUpdated(listener: CallbackOrObserver): () => void; - activate(): Promise; - ensureInitialized(): Promise; - fetch(expirationDurationSeconds?: number): Promise; - fetchAndActivate(): Promise; - getAll(): ConfigValues; - getValue(key: string): ConfigValue; - getBoolean(key: string): boolean; - getString(key: string): string; - getNumber(key: string): number; - reset(): Promise; - } - - export type CallbackOrObserver any> = T | { next: T }; - - export type OnConfigUpdatedListenerCallback = ( - event?: { updatedKeys: string[] }, - error?: { - code: string; - message: string; - nativeErrorMessage: string; - }, - ) => void; -} - -declare module '@react-native-firebase/app' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace ReactNativeFirebase { - import FirebaseModuleWithStatics = ReactNativeFirebase.FirebaseModuleWithStatics; - - interface Module { - /** - * @deprecated Use the package default export or modular APIs instead. - */ - remoteConfig: FirebaseModuleWithStatics< - FirebaseRemoteConfigTypes.Module, - FirebaseRemoteConfigTypes.Statics - >; - } - - interface FirebaseApp { - /** - * @deprecated Use the package default export or modular APIs instead. - */ - remoteConfig(): FirebaseRemoteConfigTypes.Module; - } - } -} diff --git a/packages/remote-config/type-test.ts b/packages/remote-config/type-test.ts index 49c1af4c42..91b40a4495 100644 --- a/packages/remote-config/type-test.ts +++ b/packages/remote-config/type-test.ts @@ -1,6 +1,5 @@ -import remoteConfig, { - firebase, - FirebaseRemoteConfigTypes, +import { getApp } from '@react-native-firebase/app'; +import { getRemoteConfig, activate, ensureInitialized, @@ -17,6 +16,9 @@ import remoteConfig, { setDefaultsFromResource, onConfigUpdate, setCustomSignals, + LastFetchStatus, + ValueSource, + SDK_VERSION, type ConfigUpdateObserver, type CustomSignals, type FetchStatus, @@ -27,24 +29,17 @@ import remoteConfig, { type Value, } from '.'; -const typedRemoteConfig: RemoteConfig = getRemoteConfig(); +const remoteConfig = getRemoteConfig(); +console.log(remoteConfig.app.name); + +const remoteConfigWithApp = getRemoteConfig(getApp()); +console.log(remoteConfigWithApp.app.name); + +const typedRemoteConfig: RemoteConfig = remoteConfig; const typedConfigSettings: RemoteConfigSettings = { minimumFetchIntervalMillis: 30000, fetchTimeoutMillis: 60000, }; -const typedLegacyConfigSettings: FirebaseRemoteConfigTypes.ConfigSettings = { - minimumFetchIntervalMillis: 30000, - fetchTimeMillis: 60000, -}; -const typedNamespacedConfigSettings: FirebaseRemoteConfigTypes.ConfigSettings = { - minimumFetchIntervalMillis: 30000, - fetchTimeMillis: 60000, -}; -const typedLegacyConfigDefaults: FirebaseRemoteConfigTypes.ConfigDefaults = { - enabled: true, - retries: 1, - title: 'remote', -}; const typedConfigDefaults: Record = { enabled: true, retries: 1, @@ -52,15 +47,9 @@ const typedConfigDefaults: Record = { }; const typedConfigValue: Value = getValue(typedRemoteConfig, 'typed'); const typedConfigValues: Record = getAll(typedRemoteConfig); -const typedLegacyConfigValue: FirebaseRemoteConfigTypes.ConfigValue = - remoteConfig().getValue('typed'); -const typedLegacyConfigValues: FirebaseRemoteConfigTypes.ConfigValues = remoteConfig().getAll(); const typedCustomSignals: CustomSignals = { signal: 'value', number: 1, reset: null }; const typedLastFetchStatus: FetchStatus = typedRemoteConfig.lastFetchStatus; -const typedLegacyLastFetchStatus: FirebaseRemoteConfigTypes.LastFetchStatusType = - remoteConfig().lastFetchStatus; const typedLogLevel: LogLevel = 'debug'; -const typedLegacyLogLevel: FirebaseRemoteConfigTypes.RemoteConfigLogLevel = 'debug'; const typedUnsubscribe: Unsubscribe = () => {}; const typedObserver: ConfigUpdateObserver = { next: configUpdate => { @@ -75,198 +64,75 @@ const typedObserver: ConfigUpdateObserver = { }; console.log(typedConfigSettings); -console.log(typedLegacyConfigSettings); -console.log(typedLegacyConfigDefaults); console.log(typedConfigDefaults); console.log(typedConfigValue.asString()); console.log(typedConfigValues); -console.log(typedLegacyConfigValue.asString()); -console.log(typedLegacyConfigValues); console.log(typedCustomSignals); console.log(typedLastFetchStatus); -console.log(typedLegacyLastFetchStatus); console.log(typedLogLevel); -console.log(typedLegacyLogLevel); typedUnsubscribe(); onConfigUpdate(typedRemoteConfig, typedObserver); -console.log(remoteConfig().app); - -// checks module exists at root -console.log(firebase.remoteConfig().app.name); -console.log(firebase.remoteConfig().fetchTimeMillis); -console.log(firebase.remoteConfig().lastFetchStatus); -console.log(firebase.remoteConfig().settings); -console.log(firebase.remoteConfig().defaultConfig); - -// checks module exists at app level -console.log(firebase.app().remoteConfig().app.name); - -// checks statics exist -console.log(firebase.remoteConfig.SDK_VERSION); -console.log(firebase.remoteConfig.ValueSource.REMOTE); -console.log(firebase.remoteConfig.ValueSource.DEFAULT); -console.log(firebase.remoteConfig.ValueSource.STATIC); -console.log(firebase.remoteConfig.LastFetchStatus.SUCCESS); -console.log(firebase.remoteConfig.LastFetchStatus.FAILURE); -console.log(firebase.remoteConfig.LastFetchStatus.THROTTLED); -console.log(firebase.remoteConfig.LastFetchStatus.NO_FETCH_YET); - -// checks statics exist on defaultExport -console.log(remoteConfig.firebase.SDK_VERSION); +typedRemoteConfig.settings = typedConfigSettings; +typedRemoteConfig.defaultConfig = typedConfigDefaults; -// checks root exists -console.log(firebase.SDK_VERSION); +console.log(typedRemoteConfig.fetchTimeMillis); +console.log(typedRemoteConfig.settings.minimumFetchIntervalMillis); +console.log(typedRemoteConfig.defaultConfig.enabled); -// checks multi-app support exists -console.log(firebase.remoteConfig(firebase.app()).app.name); - -// checks default export supports app arg -console.log(remoteConfig(firebase.app()).app.name); - -// checks Module instance APIs -const remoteConfigInstance = firebase.remoteConfig(); -console.log(remoteConfigInstance.app.name); -console.log(remoteConfigInstance.fetchTimeMillis); -console.log(remoteConfigInstance.lastFetchStatus); -console.log(remoteConfigInstance.settings); -console.log(remoteConfigInstance.defaultConfig); - -remoteConfigInstance.setConfigSettings(typedNamespacedConfigSettings).then(() => { - console.log('Config settings set'); -}); - -remoteConfigInstance.settings = { minimumFetchIntervalMillis: 60000 }; - -remoteConfigInstance.setDefaults(typedConfigDefaults).then(() => { - console.log('Defaults set'); -}); - -remoteConfigInstance.defaultConfig = { key: 'value' }; - -remoteConfigInstance.setDefaultsFromResource('config_resource').then(() => { - console.log('Defaults from resource set'); -}); - -const unsubscribeOnConfigUpdate = remoteConfigInstance.onConfigUpdate({ - next: (configUpdate: FirebaseRemoteConfigTypes.ConfigUpdate) => { - console.log(configUpdate.getUpdatedKeys()); - }, - error: (error: any) => { - console.log(error); - }, - complete: () => { - console.log('Complete'); - }, -}); -unsubscribeOnConfigUpdate(); - -const unsubscribeOnConfigUpdated = remoteConfigInstance.onConfigUpdated( - ( - event?: { updatedKeys: string[] }, - error?: { code: string; message: string; nativeErrorMessage: string }, - ) => { - if (event) { - console.log(event.updatedKeys); - } - if (error) { - console.log(error); - } - }, -); -unsubscribeOnConfigUpdated(); - -remoteConfigInstance.activate().then((activated: boolean) => { +activate(remoteConfig).then((activated: boolean) => { console.log(activated); }); -remoteConfigInstance.ensureInitialized().then(() => { - console.log('Initialized'); -}); - -remoteConfigInstance.fetch().then(() => { - console.log('Fetched'); -}); - -remoteConfigInstance.fetch(300).then(() => { - console.log('Fetched with expiration'); -}); - -remoteConfigInstance.fetchAndActivate().then((activated: boolean) => { - console.log(activated); -}); - -const allValues = remoteConfigInstance.getAll(); -console.log(allValues); - -const configValue = remoteConfigInstance.getValue('key'); -console.log(configValue.getSource()); -console.log(configValue.asBoolean()); -console.log(configValue.asNumber()); -console.log(configValue.asString()); - -console.log(remoteConfigInstance.getBoolean('key')); -console.log(remoteConfigInstance.getString('key')); -console.log(remoteConfigInstance.getNumber('key')); - -remoteConfigInstance.reset().then(() => { - console.log('Reset'); -}); - -// checks modular API functions -const modularRemoteConfig1 = getRemoteConfig(); -console.log(modularRemoteConfig1.app.name); - -const modularRemoteConfig2 = getRemoteConfig(firebase.app()); -console.log(modularRemoteConfig2.app.name); - -activate(modularRemoteConfig1).then((activated: boolean) => { - console.log(activated); -}); - -ensureInitialized(modularRemoteConfig1).then(() => { +ensureInitialized(remoteConfig).then(() => { console.log('Modular initialized'); }); -fetchAndActivate(modularRemoteConfig1).then((activated: boolean) => { +fetchAndActivate(remoteConfig).then((activated: boolean) => { console.log(activated); }); -fetchConfig(modularRemoteConfig1).then(() => { +fetchConfig(remoteConfig).then(() => { console.log('Modular fetch config'); }); -const modularAllValues = getAll(modularRemoteConfig1); -console.log(modularAllValues); - -console.log(getBoolean(modularRemoteConfig1, 'key')); -console.log(getString(modularRemoteConfig1, 'key')); -console.log(getNumber(modularRemoteConfig1, 'key')); +console.log(getBoolean(remoteConfig, 'key')); +console.log(getString(remoteConfig, 'key')); +console.log(getNumber(remoteConfig, 'key')); -const modularConfigValue = getValue(modularRemoteConfig1, 'key'); +const modularConfigValue = getValue(remoteConfig, 'key'); console.log(modularConfigValue.getSource()); console.log(modularConfigValue.asBoolean()); -setLogLevel(modularRemoteConfig1, typedLogLevel); -setLogLevel(modularRemoteConfig1, 'error'); +setLogLevel(remoteConfig, typedLogLevel); +setLogLevel(remoteConfig, 'error'); isSupported().then((supported: boolean) => { console.log(supported); }); -reset(modularRemoteConfig1).then(() => { +reset(remoteConfig).then(() => { console.log('Modular reset'); }); -setDefaultsFromResource(modularRemoteConfig1, 'modular_resource').then(() => { +setDefaultsFromResource(remoteConfig, 'modular_resource').then(() => { console.log('Modular defaults from resource set'); }); -const modularUnsubscribeOnConfigUpdate = onConfigUpdate(modularRemoteConfig1, typedObserver); +const modularUnsubscribeOnConfigUpdate = onConfigUpdate(remoteConfig, typedObserver); modularUnsubscribeOnConfigUpdate(); -setCustomSignals(modularRemoteConfig1, typedCustomSignals).then(() => { +setCustomSignals(remoteConfig, typedCustomSignals).then(() => { console.log('Modular custom signals set'); }); -// checks modular statics exports +console.log(LastFetchStatus.SUCCESS); +console.log(LastFetchStatus.FAILURE); +console.log(LastFetchStatus.THROTTLED); +console.log(LastFetchStatus.NO_FETCH_YET); +console.log(ValueSource.REMOTE); +console.log(ValueSource.DEFAULT); +console.log(ValueSource.STATIC); + +const sdkVersion: string = SDK_VERSION; +console.log(sdkVersion); diff --git a/packages/remote-config/typedoc.json b/packages/remote-config/typedoc.json index 0c501664d8..15f113c075 100644 --- a/packages/remote-config/typedoc.json +++ b/packages/remote-config/typedoc.json @@ -1,5 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/modular.ts", "lib/types/remote-config.ts"], + "entryPoints": ["lib/index.ts", "lib/types/remote-config.ts"], "tsconfig": "tsconfig.json" } diff --git a/packages/storage/__tests__/storage.test.ts b/packages/storage/__tests__/storage.test.ts index 0c6b17acdc..bcb4a782e5 100644 --- a/packages/storage/__tests__/storage.test.ts +++ b/packages/storage/__tests__/storage.test.ts @@ -1,7 +1,6 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; -import storage, { - firebase, +import { getStorage, connectStorageEmulator, ref, @@ -27,69 +26,7 @@ import storage, { TaskState, } from '../lib'; -import { - createCheckV9Deprecation, - CheckV9DeprecationFunction, - withDeprecationWarningsSilenced, -} from '../../app/lib/common/unitTestUtils'; - -// @ts-ignore test -import FirebaseModule from '../../app/lib/internal/FirebaseModule'; - describe('Storage', function () { - describe('namespace', function () { - beforeAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterAll(async function () { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - it('accessible from firebase.app()', function () { - const app = firebase.app(); - expect(app.storage).toBeDefined(); - expect(app.storage().useEmulator).toBeDefined(); - }); - - describe('useEmulator()', function () { - it('useEmulator requires a string host', function () { - // @ts-ignore because we pass an invalid argument... - expect(() => storage().useEmulator()).toThrow( - 'firebase.storage().useEmulator() takes a non-empty host', - ); - expect(() => storage().useEmulator('', -1)).toThrow( - 'firebase.storage().useEmulator() takes a non-empty host', - ); - // @ts-ignore because we pass an invalid argument... - expect(() => storage().useEmulator(123)).toThrow( - 'firebase.storage().useEmulator() takes a non-empty host', - ); - }); - - it('useEmulator requires a host and port', function () { - expect(() => storage().useEmulator('', 9000)).toThrow( - 'firebase.storage().useEmulator() takes a non-empty host and port', - ); - // No port - // @ts-ignore because we pass an invalid argument... - expect(() => storage().useEmulator('localhost')).toThrow( - 'firebase.storage().useEmulator() takes a non-empty host and port', - ); - }); - - it('useEmulator -> remaps Android loopback to host', function () { - const foo = storage().useEmulator('localhost', 9000); - expect(foo).toEqual(['10.0.2.2', 9000]); - - const bar = storage().useEmulator('127.0.0.1', 9000); - expect(bar).toEqual(['10.0.2.2', 9000]); - }); - }); - }); - describe('modular', function () { it('`getStorage` function is properly exposed to end user', function () { expect(getStorage).toBeDefined(); @@ -175,8 +112,9 @@ describe('Storage', function () { expect(writeToFile).toBeDefined(); }); - it('`toString` function is properly exposed to end user', function () { - expect(toString).toBeDefined(); + it('storage reference `toString` is available', function () { + const storage = getStorage(); + expect(ref(storage, 'path').toString()).toBeDefined(); }); it('`setMaxDownloadRetryTime` function is properly exposed to end user', function () { @@ -202,283 +140,4 @@ describe('Storage', function () { expect(TaskState.SUCCESS).toBeDefined(); }); }); - - describe('test `console.warn` is called for RNFB v8 API & not called for v9 API', function () { - let storageV9Deprecation: CheckV9DeprecationFunction; - let storageRefV9Deprecation: CheckV9DeprecationFunction; - let staticsV9Deprecation: CheckV9DeprecationFunction; - - beforeEach(function () { - storageV9Deprecation = createCheckV9Deprecation(['storage']); - - storageRefV9Deprecation = createCheckV9Deprecation(['storage', 'Reference']); - - staticsV9Deprecation = createCheckV9Deprecation(['storage', 'statics']); - - // @ts-ignore test - jest.spyOn(FirebaseModule.prototype, 'native', 'get').mockImplementation(() => { - return new Proxy( - {}, - { - get: (_target, prop) => { - // Handle list operations specially - if (prop === 'list' || prop === 'listAll') { - return jest.fn().mockResolvedValue({ - items: [], - prefixes: [], - nextPageToken: null, - } as never); - } - // Default mock for other operations - return jest.fn().mockResolvedValue({ - source: 'cache', - changes: [], - documents: [], - metadata: {}, - path: 'foo', - } as never); - }, - }, - ); - }); - }); - - describe('Storage', function () { - it('useStorageEmulator()', function () { - const storage = getStorage(); - storageV9Deprecation( - () => connectStorageEmulator(storage, 'localhost', 8080), - // @ts-expect-error Combines modular and namespace API - () => storage.useEmulator('localhost', 8080), - 'useEmulator', - ); - }); - - it('ref()', function () { - const storage = getStorage(); - storageV9Deprecation( - () => ref(storage, 'foo'), - // @ts-expect-error Combines modular and namespace API - () => storage.ref('foo'), - 'ref', - ); - }); - - it('ref() with full URL', function () { - const storage = getStorage(); - storageV9Deprecation( - () => ref(storage, 'gs://flutterfire-e2e-tests.appspot.com/flutter-tsts'), - // @ts-expect-error Combines modular and namespace API - () => storage.refFromURL('gs://flutterfire-e2e-tests.appspot.com/flutter-tsts'), - 'refFromURL', - ); - }); - - it('setMaxOperationRetryTime()', function () { - const storage = getStorage(); - storageV9Deprecation( - () => setMaxOperationRetryTime(storage, 1000), - // @ts-expect-error Combines modular and namespace API - () => storage.setMaxOperationRetryTime(1000), - 'setMaxOperationRetryTime', - ); - }); - - it('setMaxUploadRetryTime()', function () { - const storage = getStorage(); - storageV9Deprecation( - () => setMaxUploadRetryTime(storage, 1000), - // @ts-expect-error Combines modular and namespace API - () => storage.setMaxUploadRetryTime(1000), - 'setMaxUploadRetryTime', - ); - }); - - it('setMaxDownloadRetryTime()', function () { - const storage = getStorage(); - storageV9Deprecation( - () => setMaxDownloadRetryTime(storage, 1000), - // @ts-expect-error Combines modular and namespace API - () => storage.setMaxDownloadRetryTime(1000), - 'setMaxDownloadRetryTime', - ); - }); - - it('delete()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => deleteObject(storageRef), - () => namespacedStorageRef.delete(), - 'delete', - ); - }); - - it('getDownloadURL()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => getDownloadURL(storageRef), - () => namespacedStorageRef.getDownloadURL(), - 'getDownloadURL', - ); - }); - - it('getMetadata()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => getMetadata(storageRef), - () => namespacedStorageRef.getMetadata(), - 'getMetadata', - ); - }); - - it('list()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => list(storageRef), - () => namespacedStorageRef.list(), - 'list', - ); - }); - - it('listAll()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => listAll(storageRef), - () => namespacedStorageRef.listAll(), - 'listAll', - ); - }); - - it('updateMetadata()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => updateMetadata(storageRef, {}), - () => namespacedStorageRef.updateMetadata({}), - 'updateMetadata', - ); - }); - - it('put()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => uploadBytesResumable(storageRef, new Blob(['foo']), {}), - () => namespacedStorageRef.put(new Blob(['foo']), {}), - 'put', - ); - }); - - it('putString()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => uploadString(storageRef, 'foo', StringFormat.RAW), - () => namespacedStorageRef.putString('foo', StringFormat.RAW), - 'putString', - ); - }); - - it('putFile()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => putFile(storageRef, 'foo', {}), - () => namespacedStorageRef.putFile('foo', {}), - 'putFile', - ); - }); - - it('writeToFile()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => writeToFile(storageRef, 'foo'), - () => namespacedStorageRef.writeToFile('foo'), - 'writeToFile', - ); - }); - - it('toString()', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - expect(typeof storageRef.toString()).toBe('string'); - expect(storageRef.toString()).toContain('gs://'); - }); - - it('ref() with child path', function () { - const storage = getStorage(); - const storageRef = ref(storage, 'foo'); - const namespacedStorageRef = withDeprecationWarningsSilenced(() => - firebase.storage().ref('foo'), - ); - storageRefV9Deprecation( - () => ref(storageRef, 'bar'), - () => namespacedStorageRef.child('bar'), - 'child', - ); - }); - }); - - describe('statics', function () { - it('StringFormat static', function () { - staticsV9Deprecation( - () => StringFormat.RAW, - () => firebase.storage.StringFormat.RAW, - 'StringFormat', - ); - }); - - it('TaskEvent static', function () { - staticsV9Deprecation( - () => TaskEvent.STATE_CHANGED, - () => firebase.storage.TaskEvent.STATE_CHANGED, - 'TaskEvent', - ); - }); - - it('TaskState static', function () { - staticsV9Deprecation( - () => TaskState.SUCCESS, - () => firebase.storage.TaskState.SUCCESS, - 'TaskState', - ); - }); - }); - }); }); diff --git a/packages/storage/e2e/StorageReference.e2e.js b/packages/storage/e2e/StorageReference.e2e.js index 1813075111..d277cf159d 100644 --- a/packages/storage/e2e/StorageReference.e2e.js +++ b/packages/storage/e2e/StorageReference.e2e.js @@ -18,688 +18,6 @@ const { PATH, seed, WRITE_ONLY_NAME } = require('./helpers'); describe('storage() -> StorageReference', function () { - describe('firebase v8 compatibility', function () { - before(async function () { - await seed(PATH); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - after(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('toString()', function () { - it('returns the correct bucket path to the file', function () { - const app = firebase.app(); - firebase - .storage() - .ref('/uploadNope.jpeg') - .toString() - .should.equal(`gs://${app.options.storageBucket}/uploadNope.jpeg`); - }); - }); - - describe('properties', function () { - describe('fullPath', function () { - it('returns the full path as a string', function () { - firebase - .storage() - .ref('/foo/uploadNope.jpeg') - .fullPath.should.equal('foo/uploadNope.jpeg'); - firebase - .storage() - .ref('foo/uploadNope.jpeg') - .fullPath.should.equal('foo/uploadNope.jpeg'); - }); - }); - - describe('storage', function () { - it('returns the instance of storage', function () { - firebase.storage().ref('/foo/uploadNope.jpeg').storage.ref.should.be.a.Function(); - }); - }); - - describe('bucket', function () { - it('returns the storage bucket as a string', function () { - const app = firebase.app(); - firebase - .storage() - .ref('/foo/uploadNope.jpeg') - .bucket.should.equal(app.options.storageBucket); - }); - }); - - describe('name', function () { - it('returns the file name as a string', function () { - const ref = firebase.storage().ref('/foo/uploadNope.jpeg'); - ref.name.should.equal('uploadNope.jpeg'); - }); - }); - - describe('parent', function () { - it('returns the parent directory as a reference', function () { - firebase.storage().ref('/foo/uploadNope.jpeg').parent.fullPath.should.equal('foo'); - }); - - it('returns null if already at root', function () { - const ref = firebase.storage().ref('/'); - should.equal(ref.parent, null); - }); - }); - - describe('root', function () { - it('returns a reference to the root of the bucket', function () { - firebase.storage().ref('/foo/uploadNope.jpeg').root.fullPath.should.equal('/'); - }); - }); - }); - - describe('child()', function () { - it('returns a reference to a child path', function () { - const parentRef = firebase.storage().ref('/foo'); - const childRef = parentRef.child('someFile.json'); - childRef.fullPath.should.equal('foo/someFile.json'); - }); - }); - - describe('delete()', function () { - it('should delete a file', async function () { - const storageReference = firebase.storage().ref(`${PATH}/deleteMe.txt`); - await storageReference.putString('Delete File'); - await storageReference.delete(); - try { - await storageReference.getMetadata(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/object-not-found'); - if (!Platform.other) { - error.message.should.equal( - '[storage/object-not-found] No object exists at the desired reference.', - ); - } - return Promise.resolve(); - } - }); - - it('throws error if file does not exist', async function () { - const storageReference = firebase.storage().ref(`${PATH}/iDoNotExist.txt`); - - try { - await storageReference.delete(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/object-not-found'); - if (!Platform.other) { - error.message.should.equal( - '[storage/object-not-found] No object exists at the desired reference.', - ); - } - return Promise.resolve(); - } - }); - - it('throws error if no write permission', async function () { - const storageReference = firebase.storage().ref('/uploadNope.jpeg'); - - try { - await storageReference.delete(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - if (!Platform.other) { - error.message.should.equal( - '[storage/unauthorized] User is not authorized to perform the desired action.', - ); - } - return Promise.resolve(); - } - }); - }); - - describe('getDownloadURL', function () { - it('should return a download url for a file', async function () { - // This is frequently flaky in CI - but works sometimes. Skipping only in CI for now. - if (!isCI) { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - const downloadUrl = await storageReference.getDownloadURL(); - downloadUrl.should.be.a.String(); - downloadUrl.should.containEql('file1.txt'); - downloadUrl.should.containEql(firebase.app().options.projectId); - } else { - this.skip(); - } - }); - - it('throws error if file does not exist', async function () { - const storageReference = firebase.storage().ref(`${PATH}/iDoNotExist.txt`); - try { - await storageReference.getDownloadURL(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/object-not-found'); - if (!Platform.other) { - error.message.should.equal( - '[storage/object-not-found] No object exists at the desired reference.', - ); - } - return Promise.resolve(); - } - }); - - it('throws error if no read permission', async function () { - const storageReference = firebase.storage().ref(WRITE_ONLY_NAME); - try { - await storageReference.getDownloadURL(); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - if (!Platform.other) { - error.message.should.equal( - '[storage/unauthorized] User is not authorized to perform the desired action.', - ); - } - return Promise.resolve(); - } - }); - }); - - describe('getMetadata', function () { - it('should return a metadata for a file', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - const metadata = await storageReference.getMetadata(); - metadata.generation.should.be.a.String(); - metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); - if (Platform.android || Platform.other) { - metadata.name.should.equal('file1.txt'); - } else { - // FIXME on ios file comes through as fully-qualified - // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 - metadata.name.should.equal(`${PATH}/list/file1.txt`); - } - metadata.size.should.be.a.Number(); - should.equal(metadata.size > 0, true); - metadata.updated.should.be.a.String(); - metadata.timeCreated.should.be.a.String(); - metadata.contentEncoding.should.be.a.String(); - metadata.contentDisposition.should.be.a.String(); - metadata.contentType.should.equal('text/plain'); - metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); - metadata.metageneration.should.be.a.String(); - metadata.md5Hash.should.be.a.String(); - // TODO against cloud storage cacheControl comes back null/undefined by default. Emulator has a difference - // https://github.com/firebase/firebase-tools/issues/3398#issuecomment-1159821364 - // should.equal(metadata.cacheControl, undefined); - should.equal(metadata.contentLanguage, null); - // should.equal(metadata.customMetadata, null); - }); - }); - - describe('list', function () { - it('should return list results', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result = await storageReference.list(); - result.constructor.name.should.eql('StorageListResult'); - result.should.have.property('nextPageToken'); - result.items.should.be.Array(); - result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('Reference'); - result.prefixes.should.be.Array(); - result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('Reference'); - }); - - it('throws if options is not an object', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - storageReference.list(123); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'options' expected an object value"); - return Promise.resolve(); - } - }); - - describe('maxResults', function () { - it('should limit with maxResults are passed', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result = await storageReference.list({ - maxResults: 1, - }); - result.nextPageToken.should.be.String(); - result.items.should.be.Array(); - result.items.length.should.eql(1); - result.items[0].constructor.name.should.eql('Reference'); - result.prefixes.should.be.Array(); - // todo length? - }); - - it('throws if maxResults is not a number', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/list`); - storageReference.list({ - maxResults: '123', - }); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'options.maxResults' expected a number value"); - return Promise.resolve(); - } - }); - - it('throws if maxResults is not a valid number', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/list`); - storageReference.list({ - maxResults: 2000, - }); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql( - "'options.maxResults' expected a number value between 1-1000", - ); - return Promise.resolve(); - } - }); - }); - - describe('pageToken', function () { - it('throws if pageToken is not a string', function () { - try { - const storageReference = firebase.storage().ref(`${PATH}/list`); - storageReference.list({ - pageToken: 123, - }); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'options.pageToken' expected a string value"); - return Promise.resolve(); - } - }); - - it('should return and use a page token', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result1 = await storageReference.list({ - maxResults: 1, - }); - const item1 = result1.items[0].fullPath; - const result2 = await storageReference.list({ - maxResults: 1, - pageToken: result1.nextPageToken, - }); - const item2 = result2.items[0].fullPath; - if (item1 === item2) { - throw new Error('Expected item results to be different.'); - } - }); - }); - }); - - describe('listAll', function () { - it('should return all results', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list`); - const result = await storageReference.listAll(); - should.equal(result.nextPageToken, null); - result.items.should.be.Array(); - result.items.length.should.be.greaterThan(0); - result.items[0].constructor.name.should.eql('Reference'); - result.prefixes.should.be.Array(); - result.prefixes.length.should.be.greaterThan(0); - result.prefixes[0].constructor.name.should.eql('Reference'); - }); - - it('should not crash if the user is not allowed to list the directory', async function () { - const storageReference = firebase.storage().ref('/forbidden'); - try { - await storageReference.listAll(); - return Promise.reject(new Error('listAll on a forbidden directory succeeded')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - if (!Platform.other) { - error.message.should.equal( - '[storage/unauthorized] User is not authorized to perform the desired action.', - ); - } - return Promise.resolve(); - } - }); - }); - - describe('updateMetadata', function () { - it('should return the updated metadata for a file', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - let metadata = await storageReference.updateMetadata({ - cacheControl: 'cache-control', - contentDisposition: 'content-disposition', - contentEncoding: 'application/octet-stream', - contentLanguage: 'de', - contentType: 'content-type-a', - customMetadata: { - a: 'b', - y: 'z', - }, - }); - // Things that are set automagically for us - metadata.generation.should.be.a.String(); - metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); - if (Platform.android || Platform.other) { - metadata.name.should.equal('file1.txt'); - } else { - // FIXME on ios file comes through as fully-qualified - // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 - metadata.name.should.equal(`${PATH}/list/file1.txt`); - } - metadata.size.should.be.a.Number(); - should.equal(metadata.size > 0, true); - metadata.updated.should.be.a.String(); - metadata.timeCreated.should.be.a.String(); - metadata.metageneration.should.be.a.String(); - metadata.md5Hash.should.be.a.String(); - metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); - // Things we just updated - metadata.cacheControl.should.equals('cache-control'); - metadata.contentDisposition.should.equal('content-disposition'); - metadata.contentEncoding.should.equal('application/octet-stream'); - metadata.contentLanguage.should.equal('de'); - metadata.contentType.should.equal('content-type-a'); - metadata.customMetadata.should.be.an.Object(); - metadata.customMetadata.a.should.equal('b'); - metadata.customMetadata.y.should.equal('z'); - Object.keys(metadata.customMetadata).length.should.equal(2); - // Now let's make sure removing metadata works - metadata = await storageReference.updateMetadata({ - contentType: null, - cacheControl: null, - contentDisposition: null, - contentEncoding: null, - contentLanguage: null, - customMetadata: null, - }); - // Things that are set automagically for us and are not updatable - metadata.generation.should.be.a.String(); - metadata.fullPath.should.equal(`${PATH}/list/file1.txt`); - if (Platform.android || Platform.other) { - metadata.name.should.equal('file1.txt'); - } else { - // FIXME on ios file comes through as fully-qualified - // https://github.com/firebase/firebase-ios-sdk/issues/9849#issuecomment-1159819958 - metadata.name.should.equal(`${PATH}/list/file1.txt`); - } - metadata.size.should.be.a.Number(); - should.equal(metadata.size > 0, true); - metadata.updated.should.be.a.String(); - metadata.timeCreated.should.be.a.String(); - metadata.metageneration.should.be.a.String(); - metadata.md5Hash.should.be.a.String(); - metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`); - // Things that we may set (or remove) - should.equal(metadata.cacheControl, undefined); - should.equal(metadata.contentDisposition, undefined); - should.equal(metadata.contentEncoding, 'identity'); - should.equal(metadata.contentLanguage, undefined); - should.equal(metadata.contentType, undefined); - should.equal(metadata.customMetadata, undefined); - }); - - it('should set update or remove customMetadata properties correctly', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - let metadata = await storageReference.updateMetadata({ - contentType: 'application/octet-stream', - customMetadata: { - keepMe: 'please', - removeMeFirstTime: 'please', - removeMeSecondTime: 'please', - }, - }); - Object.keys(metadata.customMetadata).length.should.equal(3); - metadata.customMetadata.keepMe.should.equal('please'); - metadata.customMetadata.removeMeFirstTime.should.equal('please'); - metadata.customMetadata.removeMeSecondTime.should.equal('please'); - metadata = await storageReference.updateMetadata({ - contentType: 'application/octet-stream', - customMetadata: { - keepMe: 'please', - removeMeFirstTime: null, - removeMeSecondTime: 'please', - }, - }); - Object.keys(metadata.customMetadata).length.should.equal(2); - metadata.customMetadata.keepMe.should.equal('please'); - metadata.customMetadata.removeMeSecondTime.should.equal('please'); - metadata = await storageReference.updateMetadata({ - contentType: 'application/octet-stream', - customMetadata: { - keepMe: 'please', - removeMeSecondTime: null, - }, - }); - Object.keys(metadata.customMetadata).length.should.equal(1); - metadata.customMetadata.keepMe.should.equal('please'); - }); - - it('should error if updateMetadata includes md5Hash', async function () { - const storageReference = firebase.storage().ref(`${PATH}/list/file1.txt`); - try { - await storageReference.updateMetadata({ - md5Hash: '0xDEADBEEF', - }); - return Promise.reject(new Error('Did not throw on invalid updateMetadata')); - } catch (e) { - e.message.should.containEql('md5Hash may only be set on upload, not on updateMetadata'); - return Promise.resolve(); - } - }); - }); - - describe('putFile', function () { - it('errors if file path is not a string', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile(1337); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('expects a string value'); - return Promise.resolve(); - } - }); - - it('errors if metadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', 123); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('must be an object value'); - return Promise.resolve(); - } - }); - - it('errors if metadata contains an unsupported property', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', { foo: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql("unknown property 'foo'"); - return Promise.resolve(); - } - }); - - it('errors if metadata property value is not a string or null value', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', { contentType: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('should be a string or null value'); - return Promise.resolve(); - } - }); - - it('errors if metadata.customMetadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putFile('foo', { customMetadata: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - 'customMetadata must be an object of keys and string values', - ); - return Promise.resolve(); - } - }); - // TODO check an metaData:md5Hash property passes through correcty on putFile - }); - - describe('putString', function () { - it('errors if metadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', 123); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('must be an object value'); - return Promise.resolve(); - } - }); - - it('errors if metadata contains an unsupported property', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', { foo: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql("unknown property 'foo'"); - return Promise.resolve(); - } - }); - - it('errors if metadata property value is not a string or null value', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', { contentType: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('should be a string or null value'); - return Promise.resolve(); - } - }); - - it('errors if metadata.customMetadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.putString('foo', 'raw', { customMetadata: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - 'customMetadata must be an object of keys and string values', - ); - return Promise.resolve(); - } - }); - - it('allows valid metadata properties for upload', async function () { - const storageReference = firebase.storage().ref(`${PATH}/metadataTest.txt`); - await storageReference.putString('foo', 'raw', { - contentType: 'text/plain', - md5Hash: '123412341234', - cacheControl: 'true', - contentDisposition: 'disposed', - contentEncoding: 'application/octet-stream', - contentLanguage: 'de', - customMetadata: { - customMetadata1: 'metadata1value', - }, - }); - }); - }); - - describe('put', function () { - it('errors if metadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new ArrayBuffer(), 123); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('must be an object value'); - return Promise.resolve(); - } - }); - - it('errors if metadata contains an unsupported property', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new ArrayBuffer(), { foo: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql("unknown property 'foo'"); - return Promise.resolve(); - } - }); - - it('errors if metadata property value is not a string or null value', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new ArrayBuffer(), { contentType: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql('should be a string or null value'); - return Promise.resolve(); - } - }); - - it('errors if metadata.customMetadata is not an object', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - storageReference.put(new ArrayBuffer(), { customMetadata: true }); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - 'customMetadata must be an object of keys and string values', - ); - return Promise.resolve(); - } - }); - - it('allows valid metadata properties for upload', async function () { - const storageReference = firebase.storage().ref(`${PATH}/metadataTest.jpeg`); - await storageReference.put(new ArrayBuffer(), { - contentType: 'image/jpg', - md5Hash: '123412341234', - cacheControl: 'true', - contentDisposition: 'disposed', - contentEncoding: 'application/octet-stream', - contentLanguage: 'de', - customMetadata: { - customMetadata1: 'metadata1value', - }, - }); - }); - }); - - describe('put secondaryApp', function () { - it('allows valid metadata properties for upload', async function () { - const storageReference = firebase - .storage(firebase.app('secondaryFromNative')) - // .storage() - .ref(`${PATH}/metadataTest.jpeg`); - await storageReference.put(new ArrayBuffer(), { - contentType: 'image/jpg', - md5Hash: '123412341234', - cacheControl: 'true', - contentDisposition: 'disposed', - contentEncoding: 'application/octet-stream', - contentLanguage: 'de', - customMetadata: { - customMetadata1: 'metadata1value', - }, - }); - }); - }); - }); - describe('StorageReference modular', function () { let secondStorage; const secondStorageBucket = 'gs://react-native-firebase-testing'; diff --git a/packages/storage/e2e/StorageTask.e2e.js b/packages/storage/e2e/StorageTask.e2e.js index 6d942d40a4..1655d3c6c8 100644 --- a/packages/storage/e2e/StorageTask.e2e.js +++ b/packages/storage/e2e/StorageTask.e2e.js @@ -16,6 +16,7 @@ */ const { PATH, seed, WRITE_ONLY_NAME } = require('./helpers'); +const { FilePath } = modular; function snapshotProperties(snapshot) { snapshot.should.have.property('state'); @@ -27,764 +28,7 @@ function snapshotProperties(snapshot) { } describe('storage() -> StorageTask', function () { - describe('storage() -> StorageTask modular', function () { - describe('firebase v8 compatibility', function () { - before(async function () { - await seed(PATH); - - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - after(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('writeToFile()', function () { - if (Platform.other) return; - // TODO - followup - the storage emulator currently inverts not-found / permission error conditions - // this one returns the permission denied against live storage, but object not found against emulator - xit('errors if permission denied', async function () { - try { - await firebase - .storage() - .ref('/not.jpg') - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/not.jpg`); - return Promise.reject(new Error('No permission denied error')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - error.message.includes('not authorized').should.be.true(); - return Promise.resolve(); - } - }); - - it('downloads a file', async function () { - if (Platform.other) return; - const meta = await firebase - .storage() - .ref(`${PATH}/list/file1.txt`) - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/file1.txt`); - meta.state.should.eql(firebase.storage.TaskState.SUCCESS); - meta.bytesTransferred.should.eql(meta.totalBytes); - }); - }); - - describe('putString()', function () { - it('uploads a raw string', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putString.json`) - .putString(jsonDerulo, firebase.storage.StringFormat.RAW, { - contentType: 'application/json', - }); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a data_url formatted string', async function () { - const dataUrl = 'data:application/json;base64,eyJmb28iOiJiYXNlNjQifQ=='; - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringDataURL.json`) - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a url encoded data_url formatted string', async function () { - const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/helloWorld.html`) - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('when using data_url it still sets the content type if metadata is provided', async function () { - const dataUrl = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'; - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/helloWorld.html`) - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL, { - // TODO(salakar) automate test metadata is preserved when auto setting mediatype - customMetadata: { - hello: 'world', - }, - }); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a base64 string', async function () { - const base64String = 'eyJmb28iOiJiYXNlNjQifQ=='; - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringBase64.json`) - .putString(base64String, firebase.storage.StringFormat.BASE64, { - contentType: 'application/json', - }); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads a base64url string', async function () { - const base64UrlString = 'eyJmb28iOiJiYXNlNjQifQ'; - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringBase64Url.json`) - .putString(base64UrlString, firebase.storage.StringFormat.BASE64URL, { - contentType: 'application/json', - }); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('throws an error on invalid data_url', async function () { - const dataUrl = ''; - try { - await firebase - .storage() - .ref('/a.b') - .putString(dataUrl, firebase.storage.StringFormat.DATA_URL); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql('invalid data_url string provided'); - return Promise.resolve(); - } - }); - - it('throws if string arg is not a valid string', async function () { - try { - await firebase.storage().ref('/a.b').putString(1, 'base64'); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql("'string' expects a string value"); - return Promise.resolve(); - } - }); - - it('throws an error on invalid string format', async function () { - try { - await firebase.storage().ref('/a.b').putString('fooby', 'abc'); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql("'format' provided is invalid, must be one of"); - return Promise.resolve(); - } - }); - - it('throws an error if metadata is not an object', async function () { - try { - await firebase.storage().ref('/a.b').putString('fooby', 'raw', 1234); - return Promise.reject(new Error('Did not throw!')); - } catch (error) { - error.message.should.containEql('must be an object value if provided'); - return Promise.resolve(); - } - }); - }); - - describe('put()', function () { - it('uploads a Blob', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - const bob = new Blob([jsonDerulo], { - type: 'application/json', - }); - - // works every time if you sleep - iOS code is racy around Blob creation/usage - await Utils.sleep(100); - - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringBlob.json`) - .put(bob); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads an ArrayBuffer', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - const arrayBuffer = new ArrayBuffer(jsonDerulo.length); - const arrayBufferView = new Uint8Array(arrayBuffer); - for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { - arrayBufferView[i] = jsonDerulo.charCodeAt(i); - } - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringArrayBuffer.json`) - .put(arrayBuffer, { - contentType: 'application/json', - }); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('uploads an Uint8Array', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - const arrayBuffer = new ArrayBuffer(jsonDerulo.length); - const unit8Array = new Uint8Array(arrayBuffer); - for (let i = 0, strLen = jsonDerulo.length; i < strLen; i++) { - unit8Array[i] = jsonDerulo.charCodeAt(i); - } - const uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/putStringUint8Array.json`) - .put(unit8Array, { - contentType: 'application/json', - }); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('should have access to the snapshot values outside of the Task thennable', async function () { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - const bob = new Blob([jsonDerulo], { - type: 'application/json', - }); - - // works every time if you sleep - iOS code is racy around Blob creation/usage - await Utils.sleep(100); - - const uploadTaskSnapshot = firebase.storage().ref(`${PATH}/putStringBlob.json`).put(bob); - await uploadTaskSnapshot; - const snapshot = uploadTaskSnapshot.snapshot; - snapshotProperties(snapshot); - }); - }); - - describe('upload tasks', function () { - // before(async function () { - // // TODO we need some semi-large assets to upload and download I think? - // await firebase - // .storage() - // .ref('/ok.jpeg') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); - // await firebase - // .storage() - // .ref('/cat.gif') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/cat.gif`); - // await firebase - // .storage() - // .ref('/hei.heic') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); - // }); - // TODO followup - works against live storage but emulator inverts permission / not found errors - xit('errors if permission denied', async function () { - try { - await firebase - .storage() - .ref('/uploadNope.jpeg') - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); - return Promise.reject(new Error('No permission denied error')); - } catch (error) { - error.code.should.equal('storage/unauthorized'); - error.message.includes('not authorized').should.be.true(); - return Promise.resolve(); - } - }); - // TODO followup - works against live storage but emulator inverts permission / not found errors - xit('supports thenable .catch()', async function () { - const out = await firebase - .storage() - .ref('/uploadNope.jpeg') - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`) - .catch(error => { - error.code.should.equal('storage/unauthorized'); - error.message.includes('not authorized').should.be.true(); - return 1; - }); - should.equal(out, 1); - }); - // TODO we don't have files seeded on the device, but could do so from test helpers - xit('uploads files with contentType detection', async function () { - let uploadTaskSnapshot = await firebase - .storage() - .ref(`${PATH}/uploadOk.jpeg`) - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - uploadTaskSnapshot.metadata.contentType.should.equal('image/jpeg'); - uploadTaskSnapshot = await firebase - .storage() - .ref('/uploadCat.gif') - // uri decode test - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}%2Fcat.gif`); - uploadTaskSnapshot.metadata.should.be.an.Object(); - uploadTaskSnapshot.metadata.contentType.should.equal('image/gif'); - if (Platform.ios) { - uploadTaskSnapshot = await firebase - .storage() - .ref('/uploadHei.heic') - .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); - uploadTaskSnapshot.metadata.should.be.an.Object(); - uploadTaskSnapshot.metadata.contentType.should.equal('image/heic'); - } - }); - - it('uploads a file without read permission', async function () { - const uploadTaskSnapshot = await firebase - .storage() - .ref(WRITE_ONLY_NAME) - .putString('Just a string to put in a file for upload'); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - }); - - it('should have access to the snapshot values outside of the Task thennable', async function () { - const uploadTaskSnapshot = firebase - .storage() - .ref(`${PATH}/putStringBlob.json`) - .putString('Just a string to put in a file for upload'); - await uploadTaskSnapshot; - const snapshot = uploadTaskSnapshot.snapshot; - snapshotProperties(snapshot); - }); - - it('should have access to the snapshot values outside of the event subscriber', async function () { - const { getStorage, ref, uploadString, TaskState } = storageModular; - const uploadTaskSnapshot = uploadString( - ref(getStorage(), `${PATH}/putStringBlob.json`), - 'Just a string to put in a file for upload', - ); - const { resolve, promise } = Promise.defer(); - uploadTaskSnapshot.on('state_changed', { - next: snapshot => { - if (snapshot.state === TaskState.SUCCESS) { - snapshotProperties(snapshot); - resolve(); - } - }, - }); - await promise; - }); - }); - - describe('on()', function () { - before(async function () { - await firebase - .storage() - .ref(`${PATH}/ok.jpeg`) - .putString('Just a string to put in a file for upload'); - }); - - it('throws an Error if event is invalid', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - const task = storageReference.putFile('abc'); - task.on('foo'); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - "event argument must be a string with a value of 'state_changed'", - ); - return Promise.resolve(); - } - }); - - it('throws an Error if nextOrObserver is invalid', async function () { - const storageReference = firebase.storage().ref(`${PATH}/ok.jpeg`); - try { - const task = storageReference.putFile('abc'); - task.on('state_changed', 'not a fn'); - return Promise.reject(new Error('Did not error!')); - } catch (error) { - error.message.should.containEql( - "'nextOrObserver' must be a Function, an Object or Null", - ); - return Promise.resolve(); - } - }); - - it('observer calls error callback', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile/putFile - const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; - const task = ref.putFile(path); - task.on('state_changed', { - error: error => { - error.code.should.containEql('storage/file-not-found'); - resolve(); - }, - }); - try { - await task; - } catch (error) { - error.code.should.containEql('storage/file-not-found'); - } - await promise; - }); - - it('observer: calls next callback', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile/putFile - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - task.on('state_changed', { - next: snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - }); - await task; - await promise; - }); - - it('observer: calls completion callback', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - task.on('state_changed', { - complete: snapshot => { - snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); - resolve(); - }, - }); - await task; - await promise; - }); - - it('calls error callback', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of putFile - const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; - const task = ref.putFile(path); - task.on( - 'state_changed', - null, - error => { - error.code.should.containEql('storage/file-not-found'); - resolve(); - }, - null, - ); - try { - await task; - } catch (error) { - error.code.should.containEql('storage/file-not-found'); - } - await promise; - }); - - it('calls next callback', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - task.on('state_changed', snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }); - await task; - await promise; - }); - - it('calls completion callback', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - task.on('state_changed', null, null, snapshot => { - snapshot.state.should.equal(firebase.storage.TaskState.SUCCESS); - resolve(); - }); - await task; - await promise; - }); - - it('returns a subscribe fn', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - const subscribe = task.on('state_changed'); - subscribe(null, null, snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }); - await task; - await promise; - }); - - it('returns a subscribe fn supporting observer usage syntax', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; - const task = ref.writeToFile(path); - const subscribe = task.on('state_changed'); - subscribe({ - complete: snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - }); - await task; - await promise; - }); - - it('listens to download state', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of writeToFile - const ref = firebase.storage().ref(`${PATH}/ok.jpeg`); - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; - const unsubscribe = ref.writeToFile(path).on( - 'state_changed', - snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - error => { - unsubscribe(); - reject(error); - }, - ); - await promise; - }); - - it('listens to upload state', async function () { - if (Platform.other) return; // TODO refactor to use putString instead of putFile - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; - const ref = firebase.storage().ref(`${PATH}/uploadOk.jpeg`); - const unsubscribe = ref.putFile(path).on( - 'state_changed', - snapshot => { - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - resolve(); - } - }, - error => { - unsubscribe(); - reject(error); - }, - ); - await promise; - }); - }); - // TODO get files staged for emulator testing - xdescribe('pause() resume()', function () { - it('successfully pauses and resumes an upload', async function testRunner() { - this.timeout(100 * 1000); - await firebase - .storage() - .ref(Platform.ios ? '/smallFileTest.png' : '/cat.gif') - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`); - const ref = firebase.storage().ref('/uploadCat.gif'); - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`; - const uploadTask = ref.putFile(path); - let hadRunningStatus = false; - let hadPausedStatus = false; - let hadResumedStatus = false; - uploadTask.on( - 'state_changed', - snapshot => { - // 1) pause when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - if (Platform.android) { - uploadTask.pause(); - } else { - // TODO (salakar) submit bug report to Firebase iOS SDK repo (pausing immediately after the first progress event will fail the upload with an unknown error) - setTimeout(() => { - uploadTask.pause(); - }, 750); - } - } - // 2) resume when we receive first paused event - if (snapshot.state === firebase.storage.TaskState.PAUSED) { - hadPausedStatus = true; - uploadTask.resume(); - } - // 3) track that we resumed on 2nd running status whilst paused - if ( - snapshot.state === firebase.storage.TaskState.RUNNING && - hadRunningStatus && - hadPausedStatus && - !hadResumedStatus - ) { - hadResumedStatus = true; - } - // 4) finally confirm we received all statuses - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - should.equal(hadRunningStatus, true); - should.equal(hadPausedStatus, true); - should.equal(hadResumedStatus, true); - resolve(); - } - }, - error => { - reject(error); - }, - ); - await promise; - }); - - it('successfully pauses and resumes a download', async function () { - const ref = firebase.storage().ref(Platform.ios ? '/1mbTestFile.gif' : '/cat.gif'); - const { resolve, reject, promise } = Promise.defer(); - // random file name as Android does not allow overriding if file already exists - const path = `${ - firebase.utils.FilePath.DOCUMENT_DIRECTORY - }/invertase/pauseDownload${Math.round(Math.random() * 1000)}.gif`; - const downloadTask = ref.writeToFile(path); - let hadRunningStatus = false; - let hadPausedStatus = false; - let hadResumedStatus = false; - downloadTask.on( - 'state_changed', - snapshot => { - // TODO(salakar) validate snapshot props - // 1) pause when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - downloadTask.pause(); - } - // 2) resume when we receive first paused event - if (snapshot.state === firebase.storage.TaskState.PAUSED) { - hadPausedStatus = true; - downloadTask.resume(); - } - // 3) track that we resumed on 2nd running status whilst paused - if ( - snapshot.state === firebase.storage.TaskState.RUNNING && - hadRunningStatus && - hadPausedStatus && - !hadResumedStatus - ) { - hadResumedStatus = true; - } - // 4) finally confirm we received all statuses - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - should.equal(hadRunningStatus, true); - should.equal(hadPausedStatus, true); - should.equal(hadResumedStatus, true); - resolve(); - } - }, - error => { - reject(error); - }, - ); - await promise; - }); - }); - - describe('cancel()', function () { - // TODO stage a file big enough to test upload cancel - xit('successfully cancels an upload', async function () { - await firebase - .storage() - .ref('/1mbTestFile.gif') - .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`); - const ref = firebase.storage().ref('/successful-cancel.jpg'); - //Upload and cancel - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`; - const uploadTask = ref.putFile(path); - let hadRunningStatus = false; - let hadCancelledStatus = false; - uploadTask.on( - 'state_changed', - snapshot => { - // TODO(salakar) validate snapshot props - // 1) cancel it when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - uploadTask.cancel(); - } - // 2) confirm cancellation - if (snapshot.state === firebase.storage.TaskState.CANCELLED) { - should.equal(hadRunningStatus, true); - hadCancelledStatus = true; - } - if (snapshot.state === firebase.storage.TaskState.ERROR) { - throw new Error('Should not error if cancelled?'); - } - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - reject(new Error('UploadTask did not cancel!')); - } - }, - error => { - should.equal(hadRunningStatus, true); - should.equal(hadCancelledStatus, true); - error.code.should.equal('storage/cancelled'); - error.message.should.containEql('User cancelled the operation.'); - resolve(); - }, - ); - await promise; - }); - }); - // TODO stage a file big enough to cancel a download - xit('successfully cancels a download', async function () { - await Utils.sleep(10000); - const ref = firebase.storage().ref('/1mbTestFile.gif'); - const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/testUploadFile.jpg`; - const downloadTask = ref.writeToFile(path); - let hadRunningStatus = false; - let hadCancelledStatus = false; - downloadTask.on( - 'state_changed', - snapshot => { - // TODO(salakar) validate snapshot props - // 1) cancel it when we receive first running event - if (snapshot.state === firebase.storage.TaskState.RUNNING && !hadRunningStatus) { - hadRunningStatus = true; - downloadTask.cancel(); - } - // 2) confirm cancellation - if (snapshot.state === firebase.storage.TaskState.CANCELLED) { - should.equal(hadRunningStatus, true); - hadCancelledStatus = true; - } - if (snapshot.state === firebase.storage.TaskState.ERROR) { - throw new Error('Should not error if cancelled?'); - } - if (snapshot.state === firebase.storage.TaskState.SUCCESS) { - reject(new Error('DownloadTask did not cancel!')); - } - }, - error => { - should.equal(hadRunningStatus, true); - should.equal(hadCancelledStatus, true); - error.code.should.equal('storage/cancelled'); - error.message.should.containEql('User cancelled the operation.'); - resolve(); - }, - ); - await promise; - }); - }); - }); + describe('storage() -> StorageTask modular', function () {}); describe('StorageTask modular', function () { before(async function () { @@ -798,9 +42,7 @@ describe('storage() -> StorageTask', function () { xit('errors if permission denied', async function () { const { getStorage, ref } = storageModular; try { - await ref(getStorage(), '/not.jpg').writeToFile( - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/not.jpg`, - ); + await ref(getStorage(), '/not.jpg').writeToFile(`${FilePath.DOCUMENT_DIRECTORY}/not.jpg`); return Promise.reject(new Error('No permission denied error')); } catch (error) { error.code.should.equal('storage/unauthorized'); @@ -815,7 +57,7 @@ describe('storage() -> StorageTask', function () { const meta = await writeToFile( ref(getStorage(), `${PATH}/list/file1.txt`), - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/file1.txt`, + `${FilePath.DOCUMENT_DIRECTORY}/file1.txt`, ); meta.state.should.eql(TaskState.SUCCESS); @@ -1076,15 +318,15 @@ describe('storage() -> StorageTask', function () { // await firebase // .storage() // .ref('/ok.jpeg') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); + // .writeToFile(`${FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`); // await firebase // .storage() // .ref('/cat.gif') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/cat.gif`); + // .writeToFile(`${FilePath.DOCUMENT_DIRECTORY}/cat.gif`); // await firebase // .storage() // .ref('/hei.heic') - // .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`); + // .writeToFile(`${FilePath.DOCUMENT_DIRECTORY}/hei.heic`); // }); // TODO followup - works against live storage but emulator inverts permission / not found errors @@ -1093,7 +335,7 @@ describe('storage() -> StorageTask', function () { try { await putFile( ref(getStorage(), '/uploadNope.json'), - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, + `${FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, ); return Promise.reject(new Error('No permission denied error')); @@ -1110,7 +352,7 @@ describe('storage() -> StorageTask', function () { await putFile( ref(getStorage(), '/uploadNope.json'), - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, + `${FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, ).catch(error => { error.code.should.equal('storage/unauthorized'); error.message.includes('not authorized').should.be.true(); @@ -1121,21 +363,21 @@ describe('storage() -> StorageTask', function () { // TODO we don't have files seeded on the device, but could do so from test helpers xit('uploads files with contentType detection', async function () { - const { getStorage, ref, putFile } = storageModular; + const { getStorage, ref, putFile, TaskState } = storageModular; let uploadTaskSnapshot = await putFile( ref(getStorage(), `${PATH}/uploadOk.jpeg`), - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, + `${FilePath.DOCUMENT_DIRECTORY}/ok.jpeg`, ); - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); + uploadTaskSnapshot.state.should.eql(TaskState.SUCCESS); uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); uploadTaskSnapshot.metadata.should.be.an.Object(); uploadTaskSnapshot.metadata.contentType.should.equal('image/jpeg'); uploadTaskSnapshot = await putFile( ref(getStorage(), '/uploadCat.gif'), - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}%2Fcat.gif`, + `${FilePath.DOCUMENT_DIRECTORY}%2Fcat.gif`, ); uploadTaskSnapshot.metadata.should.be.an.Object(); @@ -1144,7 +386,7 @@ describe('storage() -> StorageTask', function () { if (Platform.ios) { uploadTaskSnapshot = await putFile( ref(getStorage(), '/uploadHei.heic'), - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/hei.heic`, + `${FilePath.DOCUMENT_DIRECTORY}/hei.heic`, ); uploadTaskSnapshot.metadata.should.be.an.Object(); @@ -1203,7 +445,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, uploadString } = storageModular; await uploadString( - ref(getStorage(), `${PATH}/ok.json`), + ref(getStorage(), `${PATH}/ok.jpeg`), 'Just a string to put in a file for upload', ); }); @@ -1243,7 +485,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, putFile, TaskEvent } = storageModular; const storageRef = ref(getStorage(), `${PATH}/uploadOk.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; const task = putFile(storageRef, path); task.on(TaskEvent.STATE_CHANGED, { @@ -1268,7 +510,7 @@ describe('storage() -> StorageTask', function () { const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; const task = writeToFile(storageRef, path); task.on('state_changed', { next: snapshot => { @@ -1287,7 +529,7 @@ describe('storage() -> StorageTask', function () { const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; const task = writeToFile(storageRef, path); task.on('state_changed', { @@ -1306,7 +548,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, putFile } = storageModular; const storageRef = ref(getStorage(), `${PATH}/uploadOk.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/notFoundFooFile.bar`; const task = putFile(storageRef, path); task.on( @@ -1334,7 +576,7 @@ describe('storage() -> StorageTask', function () { const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; const task = writeToFile(storageRef, path); task.on('state_changed', snapshot => { @@ -1352,7 +594,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, writeToFile, TaskState } = storageModular; const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; const task = writeToFile(storageRef, path); task.on('state_changed', null, null, snapshot => { @@ -1369,7 +611,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, writeToFile, TaskState } = storageModular; const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; const task = writeToFile(storageRef, path); const subscribe = task.on('state_changed'); @@ -1389,7 +631,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, writeToFile, TaskState } = storageModular; const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.jpeg`; const task = writeToFile(storageRef, path); const subscribe = task.on('state_changed'); @@ -1411,7 +653,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, writeToFile, TaskState } = storageModular; const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; const unsubscribe = writeToFile(storageRef, path).on( 'state_changed', @@ -1434,7 +676,7 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, putFile, TaskState } = storageModular; const storageRef = ref(getStorage(), `${PATH}/ok.jpeg`); const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/onDownload.gif`; const unsubscribe = putFile(storageRef, path).on( 'state_changed', @@ -1460,15 +702,12 @@ describe('storage() -> StorageTask', function () { const { getStorage, ref, writeToFile, putFile, TaskState } = storageModular; const storageRef = ref(getStorage(), Platform.ios ? '/smallFileTest.png' : '/cat.gif'); - await writeToFile( - storageRef, - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`, - ); + await writeToFile(storageRef, `${FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`); const ref2 = ref(getStorage(), '/uploadCat.gif'); const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/pauseUpload_test1.gif`; const uploadTask = putFile(ref2, path); let hadRunningStatus = false; @@ -1532,7 +771,7 @@ describe('storage() -> StorageTask', function () { // random file name as Android does not allow overriding if file already exists const path = `${ - firebase.utils.FilePath.DOCUMENT_DIRECTORY + FilePath.DOCUMENT_DIRECTORY }/invertase/pauseDownload${Math.round(Math.random() * 1000)}.gif`; const downloadTask = writeToFile(storageRef, path); @@ -1588,16 +827,13 @@ describe('storage() -> StorageTask', function () { xit('successfully cancels an upload', async function () { const { getStorage, ref, writeToFile, putFile, TaskState } = storageModular; const storageRef = ref(getStorage(), Platform.ios ? '/1mbTestFile.gif' : '/cat.gif'); - await writeToFile( - storageRef, - `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`, - ); + await writeToFile(storageRef, `${FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`); const ref2 = ref(getStorage(), '/successful-cancel.jpg'); //Upload and cancel const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/pauseUpload.gif`; const uploadTask = putFile(ref2, path); let hadRunningStatus = false; @@ -1646,7 +882,7 @@ describe('storage() -> StorageTask', function () { const storageRef = ref(getStorage(), '/1mbTestFile.gif'); await Utils.sleep(10000); const { resolve, reject, promise } = Promise.defer(); - const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/testUploadFile.jpg`; + const path = `${FilePath.DOCUMENT_DIRECTORY}/testUploadFile.jpg`; const downloadTask = writeToFile(storageRef, path); let hadRunningStatus = false; diff --git a/packages/storage/e2e/storage.e2e.js b/packages/storage/e2e/storage.e2e.js index 1a32242712..81ef09dc8e 100644 --- a/packages/storage/e2e/storage.e2e.js +++ b/packages/storage/e2e/storage.e2e.js @@ -17,237 +17,6 @@ const { PATH } = require('./helpers'); describe('storage() modular', function () { - describe('firebase v8 compatibility', function () { - beforeEach(async function beforeEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; - }); - - afterEach(async function afterEachTest() { - // @ts-ignore - globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = false; - }); - - describe('storage()', function () { - describe('namespace', function () { - it('accessible from firebase.app()', function () { - const app = firebase.app(); - should.exist(app.storage); - app.storage().app.should.equal(app); - }); - - it('supports multiple apps', async function () { - firebase.storage().app.name.should.equal('[DEFAULT]'); - - firebase - .storage(firebase.app('secondaryFromNative')) - .app.name.should.equal('secondaryFromNative'); - - firebase - .app('secondaryFromNative') - .storage() - .app.name.should.equal('secondaryFromNative'); - }); - - it('supports specifying a bucket', async function () { - const bucket = 'gs://react-native-firebase-testing'; - const defaultInstance = firebase.storage(); - defaultInstance.app.name.should.equal('[DEFAULT]'); - should.equal( - defaultInstance._customUrlOrRegion, - 'gs://react-native-firebase-testing.appspot.com', - ); - firebase.storage().app.name.should.equal('[DEFAULT]'); - const instanceForBucket = firebase.app().storage(bucket); - instanceForBucket._customUrlOrRegion.should.equal(bucket); - }); - - it('throws an error if an invalid bucket url is provided', async function () { - const bucket = 'invalid://react-native-firebase-testing'; - try { - firebase.app().storage(bucket); - return Promise.reject(new Error('Did not throw.')); - } catch (error) { - error.message.should.containEql("bucket url must be a string and begin with 'gs://'"); - return Promise.resolve(); - } - }); - - // FIXME on android this is unathorized against emulator but works on iOS? - xit('uploads to a custom bucket when specified', async function () { - if (Platform.ios) { - const jsonDerulo = JSON.stringify({ foo: 'bar' }); - const bucket = 'gs://react-native-firebase-testing'; - - const uploadTaskSnapshot = await firebase - .app() - .storage(bucket) - .ref(`${PATH}/putStringCustomBucket.json`) - .putString(jsonDerulo, firebase.storage.StringFormat.RAW, { - contentType: 'application/json', - }); - - uploadTaskSnapshot.state.should.eql(firebase.storage.TaskState.SUCCESS); - uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); - uploadTaskSnapshot.metadata.should.be.an.Object(); - } else { - this.skip(); - } - }); - }); - - describe('ref', function () { - it('uses default path if none provided', async function () { - const ref = firebase.storage().ref(); - ref.fullPath.should.equal('/'); - }); - - it('accepts a custom path', async function () { - const ref = firebase.storage().ref('foo/bar/baz.png'); - ref.fullPath.should.equal('foo/bar/baz.png'); - }); - - it('strips leading / from custom path', async function () { - const ref = firebase.storage().ref('/foo/bar/baz.png'); - ref.fullPath.should.equal('foo/bar/baz.png'); - }); - - it('throws an error if custom path not a string', async function () { - try { - firebase.storage().ref({ derp: true }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'path' must be a string value"); - return Promise.resolve(); - } - }); - }); - - describe('refFromURL', function () { - it('accepts a gs url', async function () { - const url = 'gs://foo/bar/baz.png'; - const ref = firebase.storage().refFromURL(url); - ref.toString().should.equal(url); - }); - - it('accepts a https url', async function () { - const url = - 'https://firebasestorage.googleapis.com/v0/b/react-native-firebase-testing.appspot.com/o/1mbTestFile.gif?alt=media'; - const ref = firebase.storage().refFromURL(url); - ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); - ref.name.should.equal('1mbTestFile.gif'); - ref - .toString() - .should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); - }); - - it('accepts a https encoded url', async function () { - const url = - 'https%3A%2F%2Ffirebasestorage.googleapis.com%2Fv0%2Fb%2Freact-native-firebase-testing.appspot.com%2Fo%2F1mbTestFile.gif%3Falt%3Dmedia'; - const ref = firebase.storage().refFromURL(url); - ref.bucket.should.equal('react-native-firebase-testing.appspot.com'); - ref.name.should.equal('1mbTestFile.gif'); - ref - .toString() - .should.equal('gs://react-native-firebase-testing.appspot.com/1mbTestFile.gif'); - }); - - it('throws an error if https url could not be parsed', async function () { - try { - firebase.storage().refFromURL('https://invertase.io'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("unable to parse 'url'"); - return Promise.resolve(); - } - }); - - it('accepts a gs url without a fullPath', async function () { - const url = 'gs://some-bucket'; - const ref = firebase.storage().refFromURL(url); - ref.toString().should.equal(url); - }); - - it('throws an error if url is not a string', async function () { - try { - firebase.storage().refFromURL({ derp: true }); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("'url' must be a string value"); - return Promise.resolve(); - } - }); - - it('throws an error if url does not start with gs:// or https://', async function () { - try { - firebase.storage().refFromURL('bs://foo/bar/cat.gif'); - return Promise.reject(new Error('Did not throw an Error.')); - } catch (error) { - error.message.should.containEql("begin with 'gs://'"); - return Promise.resolve(); - } - }); - }); - - describe('setMaxOperationRetryTime', function () { - it('should set', async function () { - await firebase.storage().setMaxOperationRetryTime(120000); - firebase.storage().maxOperationRetryTime.should.equal(120000); - await firebase.storage().setMaxOperationRetryTime(100000); - firebase.storage().maxOperationRetryTime.should.equal(100000); - }); - - it('throws if time is not a number value', async function () { - try { - await firebase.storage().setMaxOperationRetryTime('im a teapot'); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'time' must be a number value"); - return Promise.resolve(); - } - }); - }); - - describe('setMaxUploadRetryTime', function () { - it('should set', async function () { - await firebase.storage().setMaxUploadRetryTime(600000); - firebase.storage().maxUploadRetryTime.should.equal(600000); - await firebase.storage().setMaxUploadRetryTime(120000); - firebase.storage().maxUploadRetryTime.should.equal(120000); - }); - - it('throws if time is not a number value', async function () { - try { - await firebase.storage().setMaxUploadRetryTime('im a teapot'); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'time' must be a number value"); - return Promise.resolve(); - } - }); - }); - - describe('setMaxDownloadRetryTime', function () { - it('should set', async function () { - await firebase.storage().setMaxDownloadRetryTime(600000); - firebase.storage().maxDownloadRetryTime.should.equal(600000); - await firebase.storage().setMaxDownloadRetryTime(120000); - firebase.storage().maxDownloadRetryTime.should.equal(120000); - }); - - it('throws if time is not a number value', async function () { - try { - await firebase.storage().setMaxDownloadRetryTime('im a teapot'); - return Promise.reject(new Error('Did not throw')); - } catch (error) { - error.message.should.containEql("'time' must be a number value"); - return Promise.resolve(); - } - }); - }); - }); - }); - describe('modular', function () { describe('getStorage', function () { it('pass app as argument', function () { @@ -297,7 +66,7 @@ describe('storage() modular', function () { // FIXME on android this is unathorized against emulator but works on iOS? xit('uploads to a custom bucket when specified', async function () { if (Platform.ios) { - const { getStorage, ref, uploadString } = storageModular; + const { getStorage, ref, uploadString, StringFormat, TaskState } = storageModular; const jsonDerulo = JSON.stringify({ foo: 'bar' }); const bucket = 'gs://react-native-firebase-testing'; const storage = getStorage(null, bucket); @@ -305,13 +74,13 @@ describe('storage() modular', function () { const uploadTaskSnapshot = await uploadString( storageReference, jsonDerulo, - firebase.storage.StringFormat.RAW, + StringFormat.RAW, { contentType: 'application/json', }, ); - uploadTaskSnapshot.state.should.eql(storage.TaskState.SUCCESS); + uploadTaskSnapshot.state.should.eql(TaskState.SUCCESS); uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes); uploadTaskSnapshot.metadata.should.be.an.Object(); } else { diff --git a/packages/storage/lib/StorageReference.ts b/packages/storage/lib/StorageReference.ts index e14e10b93c..ba1400f941 100644 --- a/packages/storage/lib/StorageReference.ts +++ b/packages/storage/lib/StorageReference.ts @@ -29,7 +29,6 @@ import { pathParent, ReferenceBase, toFilePath, - isModularCall, } from '@react-native-firebase/app/dist/module/common'; import StorageDownloadTask from './StorageDownloadTask'; import StorageListResult, { provideStorageReferenceClass } from './StorageListResult'; @@ -144,13 +143,10 @@ export default class Reference extends ReferenceBase implements StorageReference maxResults: 1000, }; - const isModular = isModularCall(arguments); - if (options) { if (hasOwnProperty(options, 'maxResults')) { const maxResults = options.maxResults; - // Modular typings allow `null` for omission - ignore safely. - if (isModular && (maxResults === null || isUndefined(maxResults))) { + if (maxResults === null || isUndefined(maxResults)) { // no-op (keep default) } else { if (!isNumber(maxResults) || !isInteger(maxResults)) { @@ -171,8 +167,7 @@ export default class Reference extends ReferenceBase implements StorageReference if (hasOwnProperty(options, 'pageToken')) { const pageToken = options.pageToken; - // Modular typings allow `null` for omission - ignore safely. - if (isModular && (pageToken === null || isUndefined(pageToken))) { + if (pageToken === null || isUndefined(pageToken)) { // no-op } else if (!isUndefined(pageToken) && pageToken !== null) { if (!isString(pageToken)) { diff --git a/packages/storage/lib/index.ts b/packages/storage/lib/index.ts index 6bb8a01850..4ae26dc0a0 100644 --- a/packages/storage/lib/index.ts +++ b/packages/storage/lib/index.ts @@ -15,13 +15,504 @@ * */ -// Export modular types from types/storage +import { isAndroid, isNumber, isString } from '@react-native-firebase/app/dist/module/common'; +import type { FirebaseApp } from '@react-native-firebase/app'; +import { + FirebaseModule, + getOrCreateModularInstance, +} from '@react-native-firebase/app/dist/module/internal'; +import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; +import Reference from './StorageReference'; +import { getGsUrlParts, getHttpUrlParts, handleStorageEvent } from './utils'; +import { version } from './version'; +import fallBackModule from './web/RNFBStorageModule'; +import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; +import './types/internal'; +import type { + EmulatorMockTokenOptions, + FirebaseStorage, + FullMetadata, + ListOptions, + ListResult, + SettableMetadata, + StorageReference, + Task, + TaskResult, + UploadMetadata, +} from './types/storage'; +import type { StorageInternal, StorageReferenceInternal } from './types/internal'; + +const nativeEvents = ['storage_event']; +const nativeModuleName = 'RNFBStorageModule'; + +const config: ModuleConfig = { + namespace: 'storage', + nativeEvents, + nativeModuleName, + hasMultiAppSupport: true, + hasCustomUrlOrRegionSupport: true, + disablePrependCustomUrlOrRegion: true, +}; + +class FirebaseStorageModule extends FirebaseModule { + emulatorHost: string | undefined; + emulatorPort: number; + _maxUploadRetryTime: number; + _maxDownloadRetryTime: number; + _maxOperationRetryTime: number; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + bucketUrl?: string | null, + ) { + super(app, config, bucketUrl ?? undefined); + if (bucketUrl == null) { + this._customUrlOrRegion = `gs://${app.options.storageBucket}`; + } else if (!isString(bucketUrl) || !bucketUrl.startsWith('gs://')) { + throw new Error( + "firebase.app().storage(*) bucket url must be a string and begin with 'gs://'", + ); + } + + const storageEvent = nativeEvents[0]; + + if (!storageEvent) { + throw new Error('storage_event is not defined in nativeEvents'); + } + + this.emitter.addListener( + this.eventNameForApp(storageEvent), + handleStorageEvent.bind(null, this), + ); + + // Emulator instance vars needed to send through on iOS, iOS does not persist emulator state between calls + this.emulatorHost = undefined; + this.emulatorPort = 0; + this._maxUploadRetryTime = this.native.maxUploadRetryTime || 0; + this._maxDownloadRetryTime = this.native.maxDownloadRetryTime || 0; + this._maxOperationRetryTime = this.native.maxOperationRetryTime || 0; + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setmaxuploadretrytime + */ + get maxUploadRetryTime(): number { + return this._maxUploadRetryTime; + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setmaxdownloadretrytime + */ + get maxDownloadRetryTime(): number { + return this._maxDownloadRetryTime; + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#maxoperationretrytime + */ + get maxOperationRetryTime(): number { + return this._maxOperationRetryTime; + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#ref + */ + ref(path: string = '/'): Reference { + if (!isString(path)) { + throw new Error("firebase.storage().ref(*) 'path' must be a string value."); + } + return new Reference(this, path) as Reference; + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#refFromURL + */ + refFromURL(url: string): Reference { + if (!isString(url) || (!url.startsWith('gs://') && !url.startsWith('http'))) { + throw new Error( + "firebase.storage().refFromURL(*) 'url' must be a string value and begin with 'gs://' or 'https://'.", + ); + } + + let path: string; + let bucket: string; + + if (url.startsWith('http')) { + const parts = getHttpUrlParts(url); + if (!parts) { + throw new Error( + "firebase.storage().refFromURL(*) unable to parse 'url', ensure it's a valid storage url'.", + ); + } + ({ bucket, path } = parts); + } else { + ({ bucket, path } = getGsUrlParts(url)); + } + + const storageInstance = getOrCreateModularInstance( + FirebaseStorageModule, + config, + this.app, + bucket, + ) as unknown as StorageInternal; + return new Reference(storageInstance, path); + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxOperationRetryTime + */ + setMaxOperationRetryTime(time: number): Promise { + if (!isNumber(time)) { + throw new Error( + "firebase.storage().setMaxOperationRetryTime(*) 'time' must be a number value.", + ); + } + + this._maxOperationRetryTime = time; + return this.native.setMaxOperationRetryTime(time); + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxUploadRetryTime + */ + setMaxUploadRetryTime(time: number): Promise { + if (!isNumber(time)) { + throw new Error("firebase.storage().setMaxUploadRetryTime(*) 'time' must be a number value."); + } + + this._maxUploadRetryTime = time; + return this.native.setMaxUploadRetryTime(time); + } + + /** + * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxDownloadRetryTime + */ + setMaxDownloadRetryTime(time: number): Promise { + if (!isNumber(time)) { + throw new Error( + "firebase.storage().setMaxDownloadRetryTime(*) 'time' must be a number value.", + ); + } + + this._maxDownloadRetryTime = time; + return this.native.setMaxDownloadRetryTime(time); + } + + useEmulator(host: string, port: number, _options?: EmulatorMockTokenOptions): void { + if (!host || !isString(host) || !port || !isNumber(port)) { + throw new Error('firebase.storage().useEmulator() takes a non-empty host and port'); + } + + let _host = host; + + const androidBypassEmulatorUrlRemap = + typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && + this.firebaseJson.android_bypass_emulator_url_remap; + if (!androidBypassEmulatorUrlRemap && isAndroid && _host) { + if (_host === 'localhost' || _host === '127.0.0.1') { + _host = '10.0.2.2'; + // eslint-disable-next-line no-console + console.log( + 'Mapping storage host to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', + ); + } + } + this.emulatorHost = host; + this.emulatorPort = port; + this.native.useEmulator(_host, port, this._customUrlOrRegion); + // @ts-ignore undocumented return, just used to unit test android host remapping + return [_host, port]; + } +} + +export const SDK_VERSION = version; + +export function getStorage(app?: FirebaseApp, bucketUrl?: string): FirebaseStorage { + return getOrCreateModularInstance( + FirebaseStorageModule, + config, + app, + bucketUrl, + ) as unknown as FirebaseStorage; +} + export type * from './types/storage'; +export { StringFormat, TaskEvent, TaskState } from './StorageStatics'; + +function isUrl(path?: string): boolean { + if (typeof path !== 'string') { + return false; + } + + return /^[A-Za-z]+:\/\//.test(decodeURIComponent(path)); +} + +/** + * Modify this Storage instance to communicate with the Firebase Storage emulator. + * @param storage - Storage instance. + * @param host - emulator host (e.g. - 'localhost') + * @param port - emulator port (e.g. - 9199) + * @param options - `EmulatorMockTokenOptions` instance. Optional. Web only. + * @returns {void} + */ +export function connectStorageEmulator( + storage: FirebaseStorage, + host: string, + port: number, + options?: EmulatorMockTokenOptions, +): void { + return (storage as StorageInternal).useEmulator(host, port, options); +} + +/** + * Returns a StorageReference for the given URL or path in the default bucket. + * @param storage - FirebaseStorage instance. + * @param url - Optional gs:// or https:// URL, or path. If empty, returns root reference. + * @returns {StorageReference} + */ +export function ref(storage: FirebaseStorage, url?: string): StorageReference; +/** + * Returns a StorageReference for the given path, or the same reference if path is omitted. + * @param storageRef - StorageReference instance. + * @param path - Optional child path. If omitted, returns the same reference. + * @returns {StorageReference} + */ +export function ref(storageRef: StorageReference, path?: string): StorageReference; +export function ref( + storageOrRef: FirebaseStorage | StorageReference, + path?: string, +): StorageReference { + // ref(parentRef, path) → child reference; ref(parentRef) → same reference (firebase-js-sdk overload) + if (typeof (storageOrRef as StorageReference).fullPath === 'string') { + if (path === undefined) { + return storageOrRef as StorageReference; + } + return (storageOrRef as StorageReferenceInternal).child(path); + } + + const storage = storageOrRef as FirebaseStorage; + + if (path != null && isUrl(path)) { + return (storage as StorageInternal).refFromURL(path); + } + return (storage as StorageInternal).ref(path); +} -// Export modular API functions -export * from './modular'; +/** + * Deletes the object at this reference's location. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function deleteObject(storageRef: StorageReference): Promise { + return (storageRef as StorageReferenceInternal).delete(); +} + +/** + * Downloads the data at the object's location. Returns an error if the object is not found. + * @param _storageRef - Storage `Reference` instance. + * @param _maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. + * @returns {Promise} + */ +export function getBlob( + _storageRef: StorageReference, + _maxDownloadSizeBytes?: number, +): Promise { + throw new Error('`getBlob()` is not implemented'); +} + +/** + * Downloads the data at the object's location. Returns an error if the object is not found. + * @param _storageRef - Storage `Reference` instance. + * @param _maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. + * @returns {Promise} + */ +export function getBytes( + _storageRef: StorageReference, + _maxDownloadSizeBytes?: number, +): Promise { + throw new Error('`getBytes()` is not implemented'); +} + +/** + * Deletes the object at this reference's location. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function getDownloadURL(storageRef: StorageReference): Promise { + return (storageRef as StorageReferenceInternal).getDownloadURL(); +} + +/** + * Fetches metadata for the object at this location, if one exists. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export function getMetadata(storageRef: StorageReference): Promise { + return (storageRef as StorageReferenceInternal).getMetadata(); +} + +/** + * Downloads the data at the object's location. This API is only available in Nodejs. + * @param _storageRef - Storage `Reference` instance. + * @param _maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. + * @returns {NodeJS.ReadableStream;} + */ +export function getStream( + _storageRef: StorageReference, + _maxDownloadSizeBytes?: number, +): NodeJS.ReadableStream { + throw new Error('`getStream()` is not implemented'); +} + +/** + * List items (files) and prefixes (folders) under this storage reference + * @param storageRef - Storage `Reference` instance. + * @param options - Storage `ListOptions` instance. The options list() accepts. + * @returns {Promise} + */ +export async function list( + storageRef: StorageReference, + options?: ListOptions, +): Promise { + const result = await (storageRef as StorageReferenceInternal).list(options); + + if (result.nextPageToken === null) { + delete result.nextPageToken; + } + return result; +} + +/** + * List all items (files) and prefixes (folders) under this storage reference. + * @param storageRef - Storage `Reference` instance. + * @returns {Promise} + */ +export async function listAll(storageRef: StorageReference): Promise { + const result = await (storageRef as StorageReferenceInternal).listAll(); + + if (result.nextPageToken === null) { + delete result.nextPageToken; + } + return result; +} + +/** + * Updates the metadata for this object. + * @param storageRef - Storage `Reference` instance. + * @param metadata - A Storage `SettableMetadata` instance to update. + * @returns {Promise} + */ +export function updateMetadata( + storageRef: StorageReference, + metadata: SettableMetadata, +): Promise { + return (storageRef as StorageReferenceInternal).updateMetadata(metadata); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param _storageRef - Storage `Reference` instance. + * @param _data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. + * @param _metadata - A Storage `UploadMetadata` instance to update. Optional. + * @returns {Promise} + */ +export async function uploadBytes( + _storageRef: StorageReference, + _data: Blob | Uint8Array | ArrayBuffer, + _metadata?: UploadMetadata, +): Promise { + throw new Error('`uploadBytes()` is not implemented'); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. + * @param metadata - A Storage `UploadMetadata` instance to update. Optional. + * @returns {Task} + */ +export function uploadBytesResumable( + storageRef: StorageReference, + data: Blob | Uint8Array | ArrayBuffer, + metadata?: UploadMetadata, +): Task { + return (storageRef as StorageReferenceInternal).put(data, metadata); +} + +/** + * Uploads data to this object's location. The upload is not resumable. + * @param storageRef - Storage `Reference` instance. + * @param data - The string to upload. + * @param format - The format of the string to upload ('raw' | 'base64' | 'base64url' | 'data_url'). Optional. + * @param metadata - A Storage `UploadMetadata` instance to update. Optional. + * @returns {Task} + */ +export function uploadString( + storageRef: StorageReference, + data: string, + format?: 'raw' | 'base64' | 'base64url' | 'data_url', + metadata?: UploadMetadata, +): Task { + return (storageRef as StorageReferenceInternal).putString(data, format, metadata); +} + +// Methods not on the Firebase JS SDK below + +/** + * Sets the maximum time in milliseconds to retry a download if a failure occurs.. android & iOS only. + * @param storage - Storage instance. + * @param time - The new maximum operation retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxOperationRetryTime(storage: FirebaseStorage, time: number): Promise { + return (storage as StorageInternal).setMaxOperationRetryTime(time); +} + +/** + * Sets the maximum time in milliseconds to retry an upload if a failure occurs. android & iOS only. + * @param storage - Storage instance. + * @param time - The new maximum operation retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxUploadRetryTime(storage: FirebaseStorage, time: number): Promise { + return (storage as StorageInternal).setMaxUploadRetryTime(time); +} + +/** + * Puts a file from local disk onto the storage bucket. + * @param storageRef - Storage Reference instance. + * @param filePath The local file path to upload to the bucket at the reference location. + * @param metadata Any additional `UploadMetadata` for this task. + * @returns {Task} + */ +export function putFile( + storageRef: StorageReference, + filePath: string, + metadata?: UploadMetadata, +): Task { + return (storageRef as StorageReferenceInternal).putFile(filePath, metadata); +} + +/** + * Downloads a file to the specified local file path on the device. + * @param storageRef - Storage Reference instance. + * @param filePath The local file path to upload to on the device. + * @returns {Task} + */ +export function writeToFile(storageRef: StorageReference, filePath: string): Task { + return (storageRef as StorageReferenceInternal).writeToFile(filePath); +} + +/** + * Sets the maximum time in milliseconds to retry a download if a failure occurs. + * @param storage - Storage instance. + * @param time - The new maximum download retry time in milliseconds. + * @returns {Promise} + */ +export function setMaxDownloadRetryTime(storage: FirebaseStorage, time: number): Promise { + return (storage as StorageInternal).setMaxDownloadRetryTime(time); +} -// Export namespaced API -export type { FirebaseStorageTypes } from './types/namespaced'; -export * from './namespaced'; -export { default } from './namespaced'; +setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/storage/lib/modular.ts b/packages/storage/lib/modular.ts deleted file mode 100644 index f7af854a22..0000000000 --- a/packages/storage/lib/modular.ts +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { getApp } from '@react-native-firebase/app'; -import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common'; -import type { FirebaseApp } from '@react-native-firebase/app'; -import type { - FirebaseStorage, - StorageReference, - FullMetadata, - ListResult, - ListOptions, - TaskResult, - Task, - SettableMetadata, - UploadMetadata, - EmulatorMockTokenOptions, -} from './types/storage'; -import type { StorageReferenceInternal, StorageInternal } from './types/internal'; - -type WithModularDeprecationArg = F extends (...args: infer P) => infer R - ? (...args: [...P, typeof MODULAR_DEPRECATION_ARG]) => R - : never; - -function isUrl(path?: string): boolean { - if (typeof path !== 'string') { - return false; - } - - return /^[A-Za-z]+:\/\//.test(decodeURIComponent(path)); -} - -/** - * Returns a Storage instance for the given app. - * @param app - FirebaseApp. Optional. - * @param bucketUrl - Storage bucket URL. Optional. - * @returns {Storage} - */ -export function getStorage(app?: FirebaseApp, bucketUrl?: string): FirebaseStorage { - if (app) { - if (bucketUrl != null) { - return getApp(app.name).storage(bucketUrl); - } - - return getApp(app.name).storage(); - } - - if (bucketUrl != null) { - return getApp().storage(bucketUrl); - } - - return getApp().storage(); -} - -/** - * Modify this Storage instance to communicate with the Firebase Storage emulator. - * @param storage - Storage instance. - * @param host - emulator host (e.g. - 'localhost') - * @param port - emulator port (e.g. - 9199) - * @param options - `EmulatorMockTokenOptions` instance. Optional. Web only. - * @returns {void} - */ -export function connectStorageEmulator( - storage: FirebaseStorage, - host: string, - port: number, - options?: EmulatorMockTokenOptions, -): void { - return ( - (storage as StorageInternal).useEmulator as WithModularDeprecationArg< - StorageInternal['useEmulator'] - > - ).call(storage, host, port, options, MODULAR_DEPRECATION_ARG); -} - -/** - * Returns a StorageReference for the given URL or path in the default bucket. - * @param storage - FirebaseStorage instance. - * @param url - Optional gs:// or https:// URL, or path. If empty, returns root reference. - * @returns {StorageReference} - */ -export function ref(storage: FirebaseStorage, url?: string): StorageReference; -/** - * Returns a StorageReference for the given path, or the same reference if path is omitted. - * @param storageRef - StorageReference instance. - * @param path - Optional child path. If omitted, returns the same reference. - * @returns {StorageReference} - */ -export function ref(storageRef: StorageReference, path?: string): StorageReference; -export function ref( - storageOrRef: FirebaseStorage | StorageReference, - path?: string, -): StorageReference { - // ref(parentRef, path) → child reference; ref(parentRef) → same reference (firebase-js-sdk overload) - if (typeof (storageOrRef as StorageReference).fullPath === 'string') { - if (path === undefined) { - return storageOrRef as StorageReference; - } - return ( - (storageOrRef as StorageReferenceInternal).child as WithModularDeprecationArg< - StorageReferenceInternal['child'] - > - ).call(storageOrRef, path, MODULAR_DEPRECATION_ARG); - } - - const storage = storageOrRef as FirebaseStorage; - - if (path != null && isUrl(path)) { - return ( - (storage as StorageInternal).refFromURL as WithModularDeprecationArg< - StorageInternal['refFromURL'] - > - ).call(storage, path, MODULAR_DEPRECATION_ARG); - } - return ( - (storage as StorageInternal).ref as WithModularDeprecationArg - ).call(storage, path, MODULAR_DEPRECATION_ARG); -} - -/** - * Deletes the object at this reference's location. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export function deleteObject(storageRef: StorageReference): Promise { - return ( - (storageRef as StorageReferenceInternal).delete as WithModularDeprecationArg< - StorageReferenceInternal['delete'] - > - ).call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Downloads the data at the object's location. Returns an error if the object is not found. - * @param _storageRef - Storage `Reference` instance. - * @param _maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. - * @returns {Promise} - */ -export function getBlob( - _storageRef: StorageReference, - _maxDownloadSizeBytes?: number, -): Promise { - throw new Error('`getBlob()` is not implemented'); -} - -/** - * Downloads the data at the object's location. Returns an error if the object is not found. - * @param _storageRef - Storage `Reference` instance. - * @param _maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. - * @returns {Promise} - */ -export function getBytes( - _storageRef: StorageReference, - _maxDownloadSizeBytes?: number, -): Promise { - throw new Error('`getBytes()` is not implemented'); -} - -/** - * Deletes the object at this reference's location. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export function getDownloadURL(storageRef: StorageReference): Promise { - return ( - (storageRef as StorageReferenceInternal).getDownloadURL as WithModularDeprecationArg< - StorageReferenceInternal['getDownloadURL'] - > - ).call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Fetches metadata for the object at this location, if one exists. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export function getMetadata(storageRef: StorageReference): Promise { - return ( - (storageRef as StorageReferenceInternal).getMetadata as WithModularDeprecationArg< - StorageReferenceInternal['getMetadata'] - > - ).call(storageRef, MODULAR_DEPRECATION_ARG); -} - -/** - * Downloads the data at the object's location. This API is only available in Nodejs. - * @param _storageRef - Storage `Reference` instance. - * @param _maxDownloadSizeBytes - The maximum allowed size in bytes to retrieve. Web only. - * @returns {NodeJS.ReadableStream;} - */ -export function getStream( - _storageRef: StorageReference, - _maxDownloadSizeBytes?: number, -): NodeJS.ReadableStream { - throw new Error('`getStream()` is not implemented'); -} - -/** - * List items (files) and prefixes (folders) under this storage reference - * @param storageRef - Storage `Reference` instance. - * @param options - Storage `ListOptions` instance. The options list() accepts. - * @returns {Promise} - */ -export async function list( - storageRef: StorageReference, - options?: ListOptions, -): Promise { - const result = await ( - (storageRef as StorageReferenceInternal).list as WithModularDeprecationArg< - StorageReferenceInternal['list'] - > - ).call(storageRef, options, MODULAR_DEPRECATION_ARG); - - if (result.nextPageToken === null) { - delete result.nextPageToken; - } - return result; -} - -/** - * List all items (files) and prefixes (folders) under this storage reference. - * @param storageRef - Storage `Reference` instance. - * @returns {Promise} - */ -export async function listAll(storageRef: StorageReference): Promise { - const result = await ( - (storageRef as StorageReferenceInternal).listAll as WithModularDeprecationArg< - StorageReferenceInternal['listAll'] - > - ).call(storageRef, MODULAR_DEPRECATION_ARG); - - if (result.nextPageToken === null) { - delete result.nextPageToken; - } - return result; -} - -/** - * Updates the metadata for this object. - * @param storageRef - Storage `Reference` instance. - * @param metadata - A Storage `SettableMetadata` instance to update. - * @returns {Promise} - */ -export function updateMetadata( - storageRef: StorageReference, - metadata: SettableMetadata, -): Promise { - return ( - (storageRef as StorageReferenceInternal).updateMetadata as WithModularDeprecationArg< - StorageReferenceInternal['updateMetadata'] - > - ).call(storageRef, metadata, MODULAR_DEPRECATION_ARG); -} - -/** - * Uploads data to this object's location. The upload is not resumable. - * @param _storageRef - Storage `Reference` instance. - * @param _data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. - * @param _metadata - A Storage `UploadMetadata` instance to update. Optional. - * @returns {Promise} - */ -export async function uploadBytes( - _storageRef: StorageReference, - _data: Blob | Uint8Array | ArrayBuffer, - _metadata?: UploadMetadata, -): Promise { - throw new Error('`uploadBytes()` is not implemented'); -} - -/** - * Uploads data to this object's location. The upload is not resumable. - * @param storageRef - Storage `Reference` instance. - * @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location. - * @param metadata - A Storage `UploadMetadata` instance to update. Optional. - * @returns {Task} - */ -export function uploadBytesResumable( - storageRef: StorageReference, - data: Blob | Uint8Array | ArrayBuffer, - metadata?: UploadMetadata, -): Task { - return ( - (storageRef as StorageReferenceInternal).put as WithModularDeprecationArg< - StorageReferenceInternal['put'] - > - ).call(storageRef, data, metadata, MODULAR_DEPRECATION_ARG); -} - -/** - * Uploads data to this object's location. The upload is not resumable. - * @param storageRef - Storage `Reference` instance. - * @param data - The string to upload. - * @param format - The format of the string to upload ('raw' | 'base64' | 'base64url' | 'data_url'). Optional. - * @param metadata - A Storage `UploadMetadata` instance to update. Optional. - * @returns {Task} - */ -export function uploadString( - storageRef: StorageReference, - data: string, - format?: 'raw' | 'base64' | 'base64url' | 'data_url', - metadata?: UploadMetadata, -): Task { - return ( - (storageRef as StorageReferenceInternal).putString as WithModularDeprecationArg< - StorageReferenceInternal['putString'] - > - ).call(storageRef, data, format, metadata, MODULAR_DEPRECATION_ARG); -} - -// Methods not on the Firebase JS SDK below - -/** - * Sets the maximum time in milliseconds to retry a download if a failure occurs.. android & iOS only. - * @param storage - Storage instance. - * @param time - The new maximum operation retry time in milliseconds. - * @returns {Promise} - */ -export function setMaxOperationRetryTime(storage: FirebaseStorage, time: number): Promise { - return ( - (storage as StorageInternal).setMaxOperationRetryTime as WithModularDeprecationArg< - StorageInternal['setMaxOperationRetryTime'] - > - ).call(storage, time, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the maximum time in milliseconds to retry an upload if a failure occurs. android & iOS only. - * @param storage - Storage instance. - * @param time - The new maximum operation retry time in milliseconds. - * @returns {Promise} - */ -export function setMaxUploadRetryTime(storage: FirebaseStorage, time: number): Promise { - return ( - (storage as StorageInternal).setMaxUploadRetryTime as WithModularDeprecationArg< - StorageInternal['setMaxUploadRetryTime'] - > - ).call(storage, time, MODULAR_DEPRECATION_ARG); -} - -/** - * Puts a file from local disk onto the storage bucket. - * @param storageRef - Storage Reference instance. - * @param filePath The local file path to upload to the bucket at the reference location. - * @param metadata Any additional `UploadMetadata` for this task. - * @returns {Task} - */ -export function putFile( - storageRef: StorageReference, - filePath: string, - metadata?: UploadMetadata, -): Task { - return ( - (storageRef as StorageReferenceInternal).putFile as WithModularDeprecationArg< - StorageReferenceInternal['putFile'] - > - ).call(storageRef, filePath, metadata, MODULAR_DEPRECATION_ARG); -} - -/** - * Downloads a file to the specified local file path on the device. - * @param storageRef - Storage Reference instance. - * @param filePath The local file path to upload to on the device. - * @returns {Task} - */ -export function writeToFile(storageRef: StorageReference, filePath: string): Task { - return ( - (storageRef as StorageReferenceInternal).writeToFile as WithModularDeprecationArg< - StorageReferenceInternal['writeToFile'] - > - ).call(storageRef, filePath, MODULAR_DEPRECATION_ARG); -} - -/** - * Sets the maximum time in milliseconds to retry a download if a failure occurs. - * @param storage - Storage instance. - * @param time - The new maximum download retry time in milliseconds. - * @returns {Promise} - */ -export function setMaxDownloadRetryTime(storage: FirebaseStorage, time: number): Promise { - return ( - (storage as StorageInternal).setMaxDownloadRetryTime as WithModularDeprecationArg< - StorageInternal['setMaxDownloadRetryTime'] - > - ).call(storage, time, MODULAR_DEPRECATION_ARG); -} - -export { StringFormat, TaskEvent, TaskState } from './StorageStatics'; diff --git a/packages/storage/lib/namespaced.ts b/packages/storage/lib/namespaced.ts deleted file mode 100644 index 9349db280c..0000000000 --- a/packages/storage/lib/namespaced.ts +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { - isAndroid, - isNumber, - isString, - createDeprecationProxy, -} from '@react-native-firebase/app/dist/module/common'; - -import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule'; -import { - createModuleNamespace, - FirebaseModule, - getFirebaseRoot, - type ModuleConfig, -} from '@react-native-firebase/app/dist/module/internal'; -import type { ReactNativeFirebase } from '@react-native-firebase/app'; -import Reference from './StorageReference'; -import { StringFormat, TaskEvent, TaskState } from './StorageStatics'; -import { getGsUrlParts, getHttpUrlParts, handleStorageEvent } from './utils'; -import { version } from './version'; -import fallBackModule from './web/RNFBStorageModule'; -import type { StorageInternal } from './types/internal'; -import type { FirebaseStorageTypes } from './types/namespaced'; - -const statics = { - StringFormat, - TaskEvent, - TaskState, -}; - -const namespace = 'storage'; -const nativeEvents = ['storage_event']; -const nativeModuleName = 'RNFBStorageModule'; - -class FirebaseStorageModule extends FirebaseModule { - emulatorHost: string | undefined; - emulatorPort: number; - _maxUploadRetryTime: number; - _maxDownloadRetryTime: number; - _maxOperationRetryTime: number; - - constructor( - app: ReactNativeFirebase.FirebaseAppBase, - config: ModuleConfig, - bucketUrl?: string | null, - ) { - super(app, config, bucketUrl ?? undefined); - if (bucketUrl == null) { - this._customUrlOrRegion = `gs://${app.options.storageBucket}`; - } else if (!isString(bucketUrl) || !bucketUrl.startsWith('gs://')) { - throw new Error( - "firebase.app().storage(*) bucket url must be a string and begin with 'gs://'", - ); - } - - const storageEvent = nativeEvents[0]; - - if (!storageEvent) { - throw new Error('storage_event is not defined in nativeEvents'); - } - - this.emitter.addListener( - this.eventNameForApp(storageEvent), - handleStorageEvent.bind(null, this), - ); - - // Emulator instance vars needed to send through on iOS, iOS does not persist emulator state between calls - this.emulatorHost = undefined; - this.emulatorPort = 0; - this._maxUploadRetryTime = this.native.maxUploadRetryTime || 0; - this._maxDownloadRetryTime = this.native.maxDownloadRetryTime || 0; - this._maxOperationRetryTime = this.native.maxOperationRetryTime || 0; - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setmaxuploadretrytime - */ - get maxUploadRetryTime(): number { - return this._maxUploadRetryTime; - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setmaxdownloadretrytime - */ - get maxDownloadRetryTime(): number { - return this._maxDownloadRetryTime; - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#maxoperationretrytime - */ - get maxOperationRetryTime(): number { - return this._maxOperationRetryTime; - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#ref - */ - ref(path: string = '/'): Reference { - if (!isString(path)) { - throw new Error("firebase.storage().ref(*) 'path' must be a string value."); - } - return createDeprecationProxy(new Reference(this, path)) as Reference; - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#refFromURL - */ - refFromURL(url: string): Reference { - if (!isString(url) || (!url.startsWith('gs://') && !url.startsWith('http'))) { - throw new Error( - "firebase.storage().refFromURL(*) 'url' must be a string value and begin with 'gs://' or 'https://'.", - ); - } - - let path: string; - let bucket: string; - - if (url.startsWith('http')) { - const parts = getHttpUrlParts(url); - if (!parts) { - throw new Error( - "firebase.storage().refFromURL(*) unable to parse 'url', ensure it's a valid storage url'.", - ); - } - ({ bucket, path } = parts); - } else { - ({ bucket, path } = getGsUrlParts(url)); - } - - const storageInstance = this.app.storage(bucket); - return new Reference(storageInstance as unknown as StorageInternal, path); - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxOperationRetryTime - */ - setMaxOperationRetryTime(time: number): Promise { - if (!isNumber(time)) { - throw new Error( - "firebase.storage().setMaxOperationRetryTime(*) 'time' must be a number value.", - ); - } - - this._maxOperationRetryTime = time; - return this.native.setMaxOperationRetryTime(time); - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxUploadRetryTime - */ - setMaxUploadRetryTime(time: number): Promise { - if (!isNumber(time)) { - throw new Error("firebase.storage().setMaxUploadRetryTime(*) 'time' must be a number value."); - } - - this._maxUploadRetryTime = time; - return this.native.setMaxUploadRetryTime(time); - } - - /** - * @url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxDownloadRetryTime - */ - setMaxDownloadRetryTime(time: number): Promise { - if (!isNumber(time)) { - throw new Error( - "firebase.storage().setMaxDownloadRetryTime(*) 'time' must be a number value.", - ); - } - - this._maxDownloadRetryTime = time; - return this.native.setMaxDownloadRetryTime(time); - } - - useEmulator( - host: string, - port: number, - _options?: FirebaseStorageTypes.EmulatorMockTokenOptions, - ): void { - if (!host || !isString(host) || !port || !isNumber(port)) { - throw new Error('firebase.storage().useEmulator() takes a non-empty host and port'); - } - - let _host = host; - - const androidBypassEmulatorUrlRemap = - typeof this.firebaseJson.android_bypass_emulator_url_remap === 'boolean' && - this.firebaseJson.android_bypass_emulator_url_remap; - if (!androidBypassEmulatorUrlRemap && isAndroid && _host) { - if (_host === 'localhost' || _host === '127.0.0.1') { - _host = '10.0.2.2'; - // eslint-disable-next-line no-console - console.log( - 'Mapping storage host to "10.0.2.2" for android emulators. Use real IP on real devices. You can bypass this behaviour with "android_bypass_emulator_url_remap" flag.', - ); - } - } - this.emulatorHost = host; - this.emulatorPort = port; - this.native.useEmulator(_host, port, this._customUrlOrRegion); - // @ts-ignore undocumented return, just used to unit test android host remapping - return [_host, port]; - } -} - -// import { SDK_VERSION } from '@react-native-firebase/storage'; -export const SDK_VERSION = version; - -const storageNamespace = createModuleNamespace({ - statics, - version, - namespace, - nativeEvents, - nativeModuleName, - hasMultiAppSupport: true, - hasCustomUrlOrRegionSupport: true, - disablePrependCustomUrlOrRegion: true, - ModuleClass: FirebaseStorageModule, -}); - -type StorageNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseStorageTypes.Module, - FirebaseStorageTypes.Statics -> & { - storage: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseStorageTypes.Module, - FirebaseStorageTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -// import storage from '@react-native-firebase/storage'; -// storage().X(...); -export default storageNamespace as unknown as StorageNamespace; - -// import storage, { firebase } from '@react-native-firebase/storage'; -// storage().X(...); -// firebase.storage().X(...); -export const firebase = - getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< - 'storage', - FirebaseStorageTypes.Module, - FirebaseStorageTypes.Statics, - true - >; - -setReactNativeModule(nativeModuleName, fallBackModule); diff --git a/packages/storage/lib/types/namespaced.ts b/packages/storage/lib/types/namespaced.ts deleted file mode 100644 index ad4cb19d07..0000000000 --- a/packages/storage/lib/types/namespaced.ts +++ /dev/null @@ -1,1248 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import type { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase Cloud Storage package for React Native. - * - * #### Example 1 - * - * Access the firebase export from the `storage` package: - * - * ```js - * import { firebase } from '@react-native-firebase/storage'; - * - * // firebase.storage().X - * ``` - * - * #### Example 2 - * - * Using the default export from the `storage` package: - * - * ```js - * import storage from '@react-native-firebase/storage'; - * - * // storage().X - * ``` - * - * #### Example 3 - * - * Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/storage'; - * - * // firebase.storage().X - * ``` - * - * @firebase storage - */ -/** - * @deprecated Use the exported types directly instead. - * FirebaseStorageTypes namespace is kept for backwards compatibility. - */ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace FirebaseStorageTypes { - type FirebaseModule = ReactNativeFirebase.FirebaseModule; - type NativeFirebaseError = ReactNativeFirebase.NativeFirebaseError; - - /** - * Possible string formats used for uploading via `StorageReference.putString()` - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * ```js - * firebase.storage.StringFormat; - * ``` - */ - export interface StringFormat { - /** - * Raw string format. - * - * #### Usage - * - * ```js - * firebase.storage.StringFormat.RAW; - * ``` - * - * #### Example String Format - * - * ```js - * const sampleString = ''; - * ``` - */ - RAW: 'raw'; - - /** - * Base64 string format. - * - * Learn more about Base64 [on the Mozilla Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding) - * - * #### Usage - * - * ```js - * firebase.storage.StringFormat.BASE64; - * ``` - * - * #### Example String Format - * - * ```js - * const sampleString = 'PEZvbyBCYXI+'; - * ``` - * - */ - BASE64: 'base64'; - - /** - * Base64Url string format. - * - * #### Usage - * - * ```js - * firebase.storage.StringFormat.BASE64URL; - * ``` - * - * #### Example String Format - * - * ```js - * const sampleString = 'PEZvbyBCYXI-'; - * ``` - * - */ - BASE64URL: 'base64url'; - - /** - * Data URL string format. - * - * #### Usage - * - * ```js - * firebase.storage.StringFormat.DATA_URL; - * ``` - * - * #### Example String Format - * - * ```js - * const sampleString = 'data:text/plain;base64,PEZvbyBCYXI+'; - * ``` - */ - DATA_URL: 'data_url'; - } - - /** - * An event to subscribe to that is triggered on a Upload or Download task. - * - * Event subscription is created via `StorageTask.on()`. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * ```js - * firebase.storage.TaskEvent; - * ``` - */ - /** - * An event that is triggered on a task. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export type TaskEvent = 'state_changed'; - - /** - * Static properties for task events (runtime object). - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface TaskEventStatic { - STATE_CHANGED: TaskEvent; - } - - /** - * A collection of properties that indicates the current tasks state. - * - * An event subscription is created via `StorageTask.on()`. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * ```js - * firebase.storage.TaskEvent; - * ``` - */ - export interface TaskState { - /** - * Task has been cancelled by the user. - */ - CANCELLED: 'canceled'; - - /** - * An Error occurred, see TaskSnapshot.error for details. - */ - ERROR: 'error'; - - /** - * Task has been paused. Resume the task via `StorageTask.resume()`. - */ - PAUSED: 'paused'; - - /** - * Task is running. Pause the task via `StorageTask.pause()` - */ - RUNNING: 'running'; - - /** - * Task has completed successfully. - */ - SUCCESS: 'success'; - } - - /** - * Represents the current state of a running upload. - * - * Note: The Firebase JS SDK uses "canceled" (one L). React Native Firebase historically used "cancelled" (two L). - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export type TaskStateType = 'running' | 'paused' | 'success' | 'canceled' | 'error'; - - /** - * Cloud Storage statics. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * #### Example - * - * ```js - * firebase.storage; - * ``` - */ - export interface Statics { - /** - * Possible string formats used for uploading via `StorageReference.putString()` - * - * #### Example - * - * ```js - * firebase.storage.StringFormat; - * ``` - */ - StringFormat: StringFormat; - - /** - * A collection of properties that indicates the current tasks state. - * - * #### Example - * - * ```js - * firebase.storage.TaskState; - * ``` - */ - TaskState: TaskState; - - /** - * An event to subscribe to that is triggered on a Upload or Download task. - * - * #### Example - * - * ```js - * firebase.storage.TaskEvent; - * ``` - */ - TaskEvent: TaskEventStatic; - SDK_VERSION: string; - } - - /** - * Object metadata that can be set at any time. - * - * This is used in updateMetadata, put, putString & putFile. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface SettableMetadata { - /** - * The 'Cache-Control' HTTP header that will be set on the storage object when it's requested. - * - * #### Example 1 - * - * To turn off caching, you can set the following cacheControl value. - * - * ```js - * { - * cacheControl: 'no-store', - * } - * ``` - * - * #### Example 2 - * - * To aggressively cache an object, e.g. static assets, you can set the following cacheControl value. - * - * ```js - * { - * cacheControl: 'public, max-age=31536000', - * } - * ``` - * - * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) - */ - cacheControl?: string | undefined; - - /** - * The 'Content-Disposition' HTTP header that will be set on the storage object when it's requested. - * - * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) - */ - contentDisposition?: string | undefined; - - /** - * The 'Content-Encoding' HTTP header that will be used on the storage object when it's requested. - * - * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding) - */ - contentEncoding?: string | undefined; - - /** - * The 'Content-Language' HTTP header that will be set on the storage object when it's requested. - * - * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language) - */ - contentLanguage?: string | undefined; - - /** - * The 'Content-Type' HTTP header that will be set on the object when it's requested. - * - * This is used to indicate the media type (or MIME type) of the object. When uploading a file - * Firebase Cloud Storage for React Native will attempt to automatically detect this if `contentType` - * is not already set, if it fails to detect a media type it will default to `application/octet-stream`. - * - * For `DATA_URL` string formats uploaded via `putString` this will also be automatically extracted if available. - * - * #### Example - * - * Setting the content type as JSON, e.g. for when uploading a JSON string via `putString`. - * - * ```js - * { - * contentType: 'application/json', - * } - * ``` - * - * [Learn more about this header on Mozilla.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - */ - contentType?: string | undefined; - - /** - * Additional user-defined custom metadata for this storage object. - * - * All values must be strings. - * - * #### Example - * - * Adding a user controlled NSFW meta data field. - * - * ```js - * { - * customMetadata: { - * 'nsfw': 'true' - * }, - * } - */ - customMetadata?: - | { - [key: string]: string; - } - | undefined; - } - - /** - * Object metadata that can be set at upload. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface UploadMetadata extends SettableMetadata { - /** - * A Base64-encoded MD5 hash of the object being uploaded. - */ - md5Hash?: string | undefined; - } - - /** - * The full readable metadata returned by `TaskSnapshot.metadata` or `StorageReference.getMetadata()`. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface FullMetadata extends UploadMetadata { - /** - * The bucket this storage object is contained in. - * - * #### Example Value - * - * ``` - * gs://my-project-storage-bucket - * ``` - */ - bucket: string; - - /** - * The full path to this storage object in its bucket. - * - * #### Example Value - * - * ``` - * invertase/logo.png - * ``` - */ - fullPath: string; - - /** - * Storage object generation values enable users to uniquely identify data resources, e.g. object versioning. - * - * Read more on generation on the [Google Cloud Storage documentation](https://cloud.google.com/storage/docs/generations-preconditions). - */ - generation: string; - - /** - * Storage object metageneration values enable users to uniquely identify data resources, e.g. object versioning. - * - * Read more on metageneration on the [Google Cloud Storage documentation](https://cloud.google.com/storage/docs/generations-preconditions). - */ - metageneration: string; - - /** - * The short name of storage object in its bucket, e.g. it's file name. - * - * #### Example Value - * - * ``` - * logo.png - * ``` - */ - name: string; - - /** - * The size of this storage object in bytes. - */ - size: number; - - /** - * A date string representing when this storage object was created. - * - * #### Example Value - * - * ``` - * 2019-05-02T00:34:56.264Z - * ``` - */ - timeCreated: string; - - /** - * A date string representing when this storage object was last updated. - * - * #### Example Value - * - * ``` - * 2019-05-02T00:35:56.264Z - * ``` - */ - updated: string; - - /** - * Tokens to allow access to the download URL. - */ - downloadTokens: string[] | undefined; - - /** - * `StorageReference` associated with this upload. - */ - ref?: Reference | undefined; - } - - /** - * Represents a reference to a Google Cloud Storage object in React Native Firebase. - * - * A reference can be used to upload and download storage objects, get/set storage object metadata, retrieve storage object download urls and delete storage objects. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * #### Example 1 - * - * Get a reference to a specific storage path. - * - * ```js - * const ref = firebase.storage().ref('invertase/logo.png'); - * ``` - * - * #### Example 2 - * - * Get a reference to a specific storage path on another bucket in the same firebase project. - * - * ```js - * const ref = firebase.storage().refFromURL('gs://other-bucket/invertase/logo.png'); - * ``` - */ - export interface Reference { - /** - * The name of the bucket containing this reference's object. - */ - bucket: string; - /** - * A reference pointing to the parent location of this reference, or null if this reference is the root. - */ - parent: Reference | null; - /** - * The full path of this object. - */ - fullPath: string; - /** - * The short name of this object, which is the last component of the full path. For example, - * if fullPath is 'full/path/image.png', name is 'image.png'. - */ - name: string; - /** - * A reference to the root of this reference's bucket. - */ - root: Reference; - /** - * The storage service associated with this reference. - */ - storage: Module; - - /** - * Returns a gs:// URL for this object in the form `gs://///`. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/logo.png'); - * console.log('Full path: ', ref.toString()); // gs://invertase.io/invertase/logo.png - * ``` - */ - toString(): string; - - /** - * Returns a reference to a relative path from this reference. - * - * #### Example - * - * ```js - * const parent = firebase.storage().ref('invertase'); - * const ref = parent.child('logo.png'); - * ``` - * - * @param path The relative path from this reference. Leading, trailing, and consecutive slashes are removed. - */ - child(path: string): Reference; - - /** - * Deletes the object at this reference's location. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/logo.png'); - * await ref.delete(); - * ``` - */ - delete(): Promise; - - /** - * Fetches a long lived download URL for this object. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/logo.png'); - * const url = await ref.getDownloadURL(); - * ``` - */ - getDownloadURL(): Promise; - - /** - * Fetches metadata for the object at this location, if one exists. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/logo.png'); - * const metadata = await ref.getMetadata(); - * console.log('Cache control: ', metadata.cacheControl); - * ``` - */ - getMetadata(): Promise; - - /** - * List items (files) and prefixes (folders) under this storage reference. - * - * List API is only available for Firebase Rules Version 2. - * - * GCS is a key-blob store. Firebase Storage imposes the semantic of '/' delimited folder structure. - * Refer to GCS's List API if you want to learn more. - * - * To adhere to Firebase Rules's Semantics, Firebase Storage does not support objects whose paths - * end with "/" or contain two consecutive "/"s. Firebase Storage List API will filter these unsupported objects. - * list() may fail if there are too many unsupported objects in the bucket. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('/'); - * const results = await ref.list({ - * maxResults: 30, - * }); - * ``` - * - * @param options An optional ListOptions interface. - */ - list(options?: ListOptions): Promise; - - /** - * List all items (files) and prefixes (folders) under this storage reference. - * - * This is a helper method for calling list() repeatedly until there are no more results. The default pagination size is 1000. - * - * Note: The results may not be consistent if objects are changed while this operation is running. - * - * Warning: `listAll` may potentially consume too many resources if there are too many results. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('/'); - * const results = await ref.listAll(); - * ``` - */ - listAll(): Promise; - - /** - * Puts a file from local disk onto the storage bucket. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/new-logo.png'); - * const path = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/new-logo.png`; - * const task = ref.putFile(path, { - * cacheControl: 'no-store', // disable caching - * }); - * ``` - * - * @param localFilePath The local file path to upload to the bucket at the reference location. - * @param metadata Any additional `UploadMetadata` for this task. - */ - putFile(localFilePath: string, metadata?: UploadMetadata): Task; - - /** - * Downloads a file to the specified local file path on the device. - * - * #### Example - * - * Get a Download Storage task to download a file: - * - * ```js - * const downloadTo = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/foobar.json`; - * - * const task = firebase.storage().ref('/foo/bar.json').writeToFile(downloadTo); - * ``` - * @param localFilePath - */ - writeToFile(localFilePath: string): Task; - - /** - * Puts data onto the storage bucket. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/new-logo.png'); - * const task = ref.put(BLOB, { - * cacheControl: 'no-store', // disable caching - * }); - * ``` - * - * @param data The data to upload to the storage bucket at the reference location. - * @param metadata - */ - put(data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): Task; - - /** - * Puts a string on the storage bucket. Depending on the string type, set a {@link StringFormat} type. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/new-logo.png'); - * const task = ref.putString('PEZvbyBCYXI+', firebase.storage.StringFormat.BASE64, { - * cacheControl: 'no-store', // disable caching - * }); - * ``` - * - * @param data The string data, must match the format provided. - * @param format The format type of the string, e.g. a Base64 format string. - * @param metadata Any additional `UploadMetadata` for this task. - */ - putString( - data: string, - format?: 'raw' | 'base64' | 'base64url' | 'data_url', - metadata?: UploadMetadata, - ): Task; - - /** - * Updates the metadata for this reference object on the storage bucket. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('invertase/nsfw-logo.png'); - * const updatedMetadata = await ref.updateMetadata({ - * customMetadata: { - * 'nsfw': 'true', - * } - * }); - * ``` - * - * @param metadata A `SettableMetadata` instance to update. - */ - updateMetadata(metadata: SettableMetadata): Promise; - } - - /** - * The snapshot observer returned from a {@link Task#on} listener. - * - * #### Example - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * ```js - * const ref = firebase.storage().ref(...); - * const task = ref.put(...) - * - * task.on('state_changed', { - * next(taskSnapshot) { - * console.log(taskSnapshot.state); - * }, - * error(error) { - * console.error(error.message); - * }, - * complete() { - * console.log('Task complete'); - * }, - * }) - * ``` - */ - export interface TaskSnapshotObserver { - /** - * Called when the task state changes. - * - * @param taskSnapshot A `TaskSnapshot` for the event. - */ - next?: (taskSnapshot: TaskSnapshot) => void; - - /** - * Called when the task errors. - * - * @param error A JavaScript error. - */ - error?: (error: NativeFirebaseError) => void; - - /** - * Called when the task has completed successfully. - */ - complete?: () => void; - } - - /** - * Storage Task used for Uploading or Downloading files. - * - * #### Example 1 - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * Get a Upload Storage task to upload a string: - * - * ```js - * const string = '{ "foo": 1 }'; - * const task = firebase - * .storage() - * .ref('/foo/bar.json') - * .putString(string); - * ``` - * - * #### Example 2 - * - * Get a Download Storage task to download a file: - * - * ```js - * const downloadTo = `${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/bar.json`; - * - * const task = firebase - * .storage() - * .ref('/foo/bar.json') - * .writeToFile(downloadTo); - * ``` - */ - export interface Task { - /** - * Initial state of Task.snapshot is `null`. Once uploading begins, it updates to a `TaskSnapshot` object. - */ - snapshot: null | TaskSnapshot; - - /** - * Pause the current Download or Upload task. - * - * #### Example - * - * Pause a running task inside a state changed listener: - * - * ```js - * task.on('state_changed', taskSnapshot => { - * if (taskSnapshot.state === firebase.storage.TaskState.RUNNING) { - * console.log('Pausing my task!'); - * task.pause(); - * } - * }); - * ``` - * - */ - pause(): Promise; - - /** - * Resume the current Download or Upload task. - * - * #### Example - * - * Resume a previously paused task inside a state changed listener: - * - * ```js - * task.on('state_changed', taskSnapshot => { - * // ... pause me ... - * if (taskSnapshot.state === firebase.storage.TaskState.PAUSED) { - * console.log('Resuming my task!'); - * task.resume(); - * } - * }); - * ``` - * - */ - resume(): Promise; - - /** - * Cancel the current Download or Upload task. - * - * - * #### Example - * - * Cancel a task inside a state changed listener: - * - * ```js - * task.on('state_changed', taskSnapshot => { - * console.log('Cancelling my task!'); - * task.cancel(); - * }); - * ``` - * - */ - cancel(): Promise; - - /** - * Task event handler called when state has changed on the task. - * - * #### Example - * - * ```js - * const task = firebase - * .storage() - * .ref('/foo/bar.json') - * .writeToFile(downloadTo); - * - * task.on('state_changed', (taskSnapshot) => { - * console.log(taskSnapshot.state); - * }); - * - * task.then(() => {] - * console.log('Task complete'); - * }) - * .catch((error) => { - * console.error(error.message); - * }); - * ``` - * - * @param event The event name to handle, always `state_changed`. - * @param nextOrObserver The optional event observer function. - * @param error An optional JavaScript error handler. - * @param complete An optional complete handler function. - */ - on( - event: 'state_changed', - nextOrObserver?: TaskSnapshotObserver | null | ((a: TaskSnapshot) => any), - error?: ((a: NativeFirebaseError) => any) | null, - complete?: (() => void) | null, - ): () => void; - - // /** - // * @ignore May not exist in RN JS Environment yet so we'll hide from docs. - // */ - // finally(onFinally?: (() => void) | undefined | null): Promise; - - then( - onFulfilled?: ((a: TaskSnapshot) => any) | null, - onRejected?: ((a: NativeFirebaseError) => any) | null, - ): Promise; - - catch(onRejected: (a: NativeFirebaseError) => any): Promise; - } - - /** - * A TaskSnapshot provides information about a storage tasks state. - * - * #### Example 1 - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * ```js - * firebase - * .storage() - * .ref('/foo/bar.json') - * .putString(JSON.stringify({ foo: 'bar' })) - * .then((taskSnapshot) => { - * if (taskSnapshot.state === firebase.storage.TaskState.SUCCESS) { - * console.log('Total bytes uploaded: ', taskSnapshot.totalBytes); - * } - * }); - * ``` - * - * #### Example 2 - * - * ```js - * const task = firebase - * .storage() - * .ref('/foo/bar.json') - * .putString(JSON.stringify({ foo: 'bar' })); - * - * task.on('state_changed', taskSnapshot => { - * if (taskSnapshot.state === firebase.storage.TaskState.PAUSED) { - * console.log('Resuming my task!'); - * task.resume(); - * } - * }); - * ``` - */ - export interface TaskSnapshot { - /** - * The number of bytes currently transferred. - */ - bytesTransferred: number; - - /** - * The metadata of the tasks via a {@link FullMetadata} interface. - */ - metadata: FullMetadata; - - /** - * The {@link Reference} of the task. - */ - ref: Reference; - - /** - * The current state of the task snapshot. - */ - state: TaskState; - - /** - * The parent {@link Task} of this snapshot. - */ - task: Task; - - /** - * The total amount of bytes for this task. - */ - totalBytes: number; - - /** - * If the {@link TaskSnapshot#state} is `error`, returns a JavaScript error of the - * current task snapshot. - */ - error?: NativeFirebaseError; - } - - /** - * Result returned from a non-resumable upload. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface TaskResult { - /** - * The metadata of the tasks via a {@link FullMetadata} interface. - */ - metadata: FullMetadata; - - /** - * The {@link Reference} of the task. - */ - ref: Reference; - } - - /** - * The options `list()` accepts. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface ListOptions { - /** - * If set, limits the total number of `prefixes` and `items` to return. The default and maximum maxResults is 1000. - */ - maxResults?: number; - - /** - * The `nextPageToken` from a previous call to `list()`. If provided, listing is resumed from the previous position. - */ - pageToken?: string | null; - } - - /** - * Result returned by `list()`. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface ListResult { - /** - * Objects in this directory. You can call `getMetadata()` and `getDownloadUrl()` on them. - */ - items: Reference[]; - - /** - * If set, there might be more results for this list. Use this token to resume the list. - */ - nextPageToken: string | null; - - /** - * References to prefixes (sub-folders). You can call `list()` on them to get its contents. - * - * Folders are implicit based on '/' in the object paths. For example, if a bucket has two objects '/a/b/1' and '/a/b/2', list('/a') will return '/a/b' as a prefix. - */ - prefixes: Reference[]; - } - - /** - * Storage Emulator options. Web only. - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - */ - export interface EmulatorMockTokenOptions { - /** - * the mock auth token to use for unit testing Security Rules. - */ - mockUserToken?: string; - } - - /** - * The Cloud Storage service is available for the default app, a given app or a specific storage bucket. - * - * #### Example 1 - * - * @deprecated Use the exported types directly instead. FirebaseStorageTypes namespace is kept for backwards compatibility. - * - * Get the storage instance for the **default app**: - * - * ```js - * const storageForDefaultApp = firebase.storage(); - * ``` - * - * #### Example 2 - * - * Get the storage instance for a **secondary app**: - * - * ```js - * const otherApp = firebase.app('otherApp'); - * const storageForOtherApp = firebase.storage(otherApp); - * ``` - * - * #### Example 3 - * - * Get the storage instance for a **specific storage bucket**: - * - * ```js - * const defaultApp = firebase.app(); - * const storageForBucket = defaultApp.storage('gs://another-bucket-url'); - * - * const otherApp = firebase.app('otherApp'); - * const storageForOtherAppBucket = otherApp.storage('gs://another-bucket-url'); - * ``` - * - */ - export interface Module extends FirebaseModule { - /** - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * Returns the current maximum time in milliseconds to retry an upload if a failure occurs. - * - * #### Example - * - * ```js - * const uploadRetryTime = firebase.storage().maxUploadRetryTime; - * ``` - */ - maxUploadRetryTime: number; - - /** - * Sets the maximum time in milliseconds to retry an upload if a failure occurs. - * - * #### Example - * - * ```js - * await firebase.storage().setMaxUploadRetryTime(25000); - * ``` - * - * @param time The new maximum upload retry time in milliseconds. - */ - setMaxUploadRetryTime(time: number): Promise; - - /** - * Returns the current maximum time in milliseconds to retry a download if a failure occurs. - * - * #### Example - * - * ```js - * const downloadRetryTime = firebase.storage().maxUploadRetryTime; - * ``` - */ - maxDownloadRetryTime: number; - - /** - * Sets the maximum time in milliseconds to retry a download if a failure occurs. - * - * #### Example - * - * ```js - * await firebase.storage().setMaxDownloadRetryTime(25000); - * ``` - * - * @param time The new maximum download retry time in milliseconds. - */ - setMaxDownloadRetryTime(time: number): Promise; - - /** - * Returns the current maximum time in milliseconds to retry operations other than upload and download if a failure occurs. - * - * #### Example - * - * ```js - * const maxOperationRetryTime = firebase.storage().maxOperationRetryTime; - * ``` - */ - maxOperationRetryTime: number; - - /** - * Sets the maximum time in milliseconds to retry operations other than upload and download if a failure occurs. - * - * #### Example - * - * ```js - * await firebase.storage().setMaxOperationRetryTime(5000); - * ``` - * - * @param time The new maximum operation retry time in milliseconds. - */ - setMaxOperationRetryTime(time: number): Promise; - - /** - * Returns a new {@link Reference} instance. - * - * #### Example - * - * ```js - * const ref = firebase.storage().ref('cats.gif'); - * ``` - * - * @param path An optional string pointing to a location on the storage bucket. If no path - * is provided, the returned reference will be the bucket root path. - */ - ref(path?: string): Reference; - - /** - * Returns a new {@link Reference} instance from a storage bucket URL. - * - * #### Example - * - * ```js - * const gsUrl = 'gs://react-native-firebase-testing/cats.gif'; - * const httpUrl = 'https://firebasestorage.googleapis.com/v0/b/react-native-firebase-testing.appspot.com/o/cats.gif'; - * - * const refFromGsUrl = firebase.storage().refFromURL(gsUrl); - * // or - * const refFromHttpUrl = firebase.storage().refFromURL(httpUrl); - * ``` - * - * @param url A storage bucket URL pointing to a single file or location. Must be either a `gs://` url or an `http` url, - * e.g. `gs://assets/logo.png` or `https://firebasestorage.googleapis.com/v0/b/react-native-firebase-testing.appspot.com/o/cats.gif`. - */ - refFromURL(url: string): Reference; - - /** - * Modify this Storage instance to communicate with the Firebase Storage emulator. - * This must be called synchronously immediately following the first call to firebase.storage(). - * Do not use with production credentials as emulator traffic is not encrypted. - * - * Note: on android, hosts 'localhost' and '127.0.0.1' are automatically remapped to '10.0.2.2' (the - * "host" computer IP address for android emulators) to make the standard development experience easy. - * If you want to use the emulator on a real android device, you will need to specify the actual host - * computer IP address. - * - * @param host emulator host (eg, 'localhost') - * @param port emulator port (eg, 9199) - */ - useEmulator(host: string, port: number): void; - } -} - -type StorageNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< - FirebaseStorageTypes.Module, - FirebaseStorageTypes.Statics -> & { - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -declare const defaultExport: StorageNamespace; - -export declare const firebase: ReactNativeFirebase.Module & { - storage: typeof defaultExport; - app(name?: string): ReactNativeFirebase.FirebaseApp & { storage(): FirebaseStorageTypes.Module }; -}; - -export default defaultExport; - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ - -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; - interface Module { - storage: FirebaseModuleWithStaticsAndApp< - FirebaseStorageTypes.Module, - FirebaseStorageTypes.Statics - >; - } - interface FirebaseApp { - storage(bucket?: string): FirebaseStorageTypes.Module; - } - } -} diff --git a/packages/storage/type-test.ts b/packages/storage/type-test.ts index 5ee59d8547..ce354c5a63 100644 --- a/packages/storage/type-test.ts +++ b/packages/storage/type-test.ts @@ -1,310 +1,55 @@ -import storage, { - firebase, - // Types - type FirebaseStorage, - type FirebaseStorageTypes, - // Modular API +import { getApp } from '@react-native-firebase/app'; +import { getStorage, connectStorageEmulator, + ref, deleteObject, - getBlob, - getBytes, getDownloadURL, getMetadata, - getStream, list, listAll, - updateMetadata, - uploadBytes, uploadBytesResumable, uploadString, - ref, setMaxOperationRetryTime, setMaxUploadRetryTime, + setMaxDownloadRetryTime, putFile, writeToFile, - setMaxDownloadRetryTime, StringFormat, TaskEvent, TaskState, + SDK_VERSION, + type FirebaseStorage, + type StorageReference, } from '.'; -import { FullMetadata, ListResult, TaskResult, TaskSnapshot } from './lib/types/storage'; - -console.log(storage().app); - -// checks module exists at root -console.log(firebase.storage().app.name); -console.log(firebase.storage().maxUploadRetryTime); -console.log(firebase.storage().maxDownloadRetryTime); -console.log(firebase.storage().maxOperationRetryTime); - -// checks module exists at app level -console.log(firebase.app().storage().app.name); - -// checks custom URL support -console.log(firebase.app().storage('gs://custom-bucket').app.name); - -// checks statics exist -console.log(firebase.storage.SDK_VERSION); -console.log(firebase.storage.StringFormat.RAW); -console.log(firebase.storage.StringFormat.BASE64); -console.log(firebase.storage.StringFormat.BASE64URL); -console.log(firebase.storage.StringFormat.DATA_URL); -console.log(firebase.storage.TaskState.CANCELLED); -console.log(firebase.storage.TaskState.ERROR); -console.log(firebase.storage.TaskState.PAUSED); -console.log(firebase.storage.TaskState.RUNNING); -console.log(firebase.storage.TaskState.SUCCESS); -console.log(firebase.storage.TaskEvent.STATE_CHANGED); - -// checks statics exist on defaultExport -console.log(storage.firebase.SDK_VERSION); - -// checks root exists -console.log(firebase.SDK_VERSION); - -// checks multi-app support exists -console.log(firebase.storage(firebase.app()).app.name); - -// checks default export supports app arg -console.log(storage(firebase.app()).app.name); -// checks Module instance APIs -const storageInstance = firebase.storage(); -console.log(storageInstance.app.name); -console.log(storageInstance.maxUploadRetryTime); -console.log(storageInstance.maxDownloadRetryTime); -console.log(storageInstance.maxOperationRetryTime); +const storage = getStorage(); +console.log(storage.app.name); -storageInstance.setMaxUploadRetryTime(25000).then(() => { - console.log('Max upload retry time set'); -}); +const storageWithApp = getStorage(getApp()); +console.log(storageWithApp.app.name); -storageInstance.setMaxDownloadRetryTime(25000).then(() => { - console.log('Max download retry time set'); -}); +connectStorageEmulator(storage, 'localhost', 9199); -storageInstance.setMaxOperationRetryTime(5000).then(() => { - console.log('Max operation retry time set'); -}); - -const storageRef = storageInstance.ref('test/path'); -console.log(storageRef.bucket); +const storageRef: StorageReference = ref(storage, 'path/to/file'); console.log(storageRef.fullPath); -console.log(storageRef.name); -console.log(storageRef.root); -console.log(storageRef.storage); - -const refFromURLInstance = storageInstance.refFromURL('gs://bucket/path/to/file'); -console.log(refFromURLInstance.fullPath); - -storageInstance.useEmulator('localhost', 9199); - -// checks Reference instance APIs -const reference = storageInstance.ref('test'); -console.log(reference.bucket); -console.log(reference.fullPath); -console.log(reference.name); -console.log(reference.parent); -console.log(reference.root); -console.log(reference.storage); - -console.log(reference.toString()); - -const childRef = reference.child('child'); -console.log(childRef.fullPath); - -reference.delete().then(() => { - console.log('Deleted'); -}); - -reference.getDownloadURL().then((url: string) => { - console.log(url); -}); - -reference.getMetadata().then((metadata: FirebaseStorageTypes.FullMetadata) => { - console.log(metadata); -}); - -reference.list({ maxResults: 10 }).then((result: FirebaseStorageTypes.ListResult) => { - console.log(result.items); - console.log(result.prefixes); - console.log(result.nextPageToken); -}); - -reference.listAll().then((result: FirebaseStorageTypes.ListResult) => { - console.log(result.items); - console.log(result.prefixes); -}); - -const putFileTask = reference.putFile('/local/path', { cacheControl: 'no-cache' }); -putFileTask.pause().then(() => { - console.log('Paused'); -}); -putFileTask.resume().then((resumed: boolean) => { - console.log(resumed); -}); -putFileTask.cancel().then((cancelled: boolean) => { - console.log(cancelled); -}); - -const writeToFileTask = reference.writeToFile('/local/path'); -writeToFileTask.then((snapshot: FirebaseStorageTypes.TaskSnapshot) => { - console.log(snapshot.bytesTransferred); -}); - -const putTask = reference.put(new Blob(), { cacheControl: 'no-cache' }); -putTask.on('state_changed', (snapshot: FirebaseStorageTypes.TaskSnapshot) => { - console.log(snapshot.state); -}); - -const putStringTask = reference.putString('data', 'raw', { cacheControl: 'no-cache' }); -putStringTask.then((snapshot: FirebaseStorageTypes.TaskSnapshot) => { - console.log(snapshot.bytesTransferred); -}); -reference - .updateMetadata({ cacheControl: 'no-cache' }) - .then((metadata: FirebaseStorageTypes.FullMetadata) => { - console.log(metadata); - }); +deleteObject(storageRef); +getDownloadURL(storageRef).then(url => console.log(url)); +getMetadata(storageRef).then(metadata => console.log(metadata.name)); +list(storageRef).then(result => console.log(result.items.length)); +listAll(storageRef).then(result => console.log(result.items.length)); +uploadBytesResumable(storageRef, new Uint8Array([1, 2, 3])); +uploadString(storageRef, 'hello', StringFormat.RAW); +setMaxOperationRetryTime(storage, 1000); +setMaxUploadRetryTime(storage, 1000); +setMaxDownloadRetryTime(storage, 1000); +putFile(storageRef, '/local/path'); +writeToFile(storageRef, '/local/path'); -// checks modular API functions -const modularStorage1 = getStorage(); -console.log(modularStorage1.app.name); - -const modularStorage2 = getStorage(firebase.app()); -console.log(modularStorage2.app.name); - -const modularStorage3 = getStorage(firebase.app(), 'gs://custom-bucket'); -console.log(modularStorage3.app.name); - -connectStorageEmulator(modularStorage1, 'localhost', 9199); - -const modularRef1 = ref(modularStorage1, 'modular/path'); -console.log(modularRef1.fullPath); - -const modularRef2 = ref(modularStorage1); -console.log(modularRef2.fullPath); - -deleteObject(modularRef1).then(() => { - console.log('Modular deleted'); -}); - -getBlob(modularRef1).then((blob: Blob) => { - console.log(blob); -}); - -getBlob(modularRef1, 1024).then((blob: Blob) => { - console.log(blob); -}); - -getBytes(modularRef1).then((bytes: ArrayBuffer) => { - console.log(bytes); -}); - -getBytes(modularRef1, 2048).then((bytes: ArrayBuffer) => { - console.log(bytes); -}); - -getDownloadURL(modularRef1).then((url: string) => { - console.log(url); -}); - -getMetadata(modularRef1).then((metadata: FullMetadata) => { - console.log(metadata); -}); - -const modularStream = getStream(modularRef1); -console.log(modularStream); - -list(modularRef1, { maxResults: 10 }).then((result: ListResult) => { - console.log(result.items); -}); - -listAll(modularRef1).then((result: ListResult) => { - console.log(result.items); -}); - -updateMetadata(modularRef1, { cacheControl: 'no-cache' }).then((metadata: FullMetadata) => { - console.log(metadata); -}); - -uploadBytes(modularRef1, new Blob(), { cacheControl: 'no-cache' }).then((result: TaskResult) => { - console.log(result); -}); - -const modularUploadBytesResumable = uploadBytesResumable(modularRef1, new Blob(), { - cacheControl: 'no-cache', -}); -modularUploadBytesResumable.pause().then(() => { - console.log('Modular paused'); -}); - -const modularUploadString = uploadString(modularRef1, 'modular-data', 'base64', { - cacheControl: 'no-cache', -}); -modularUploadString.then((snapshot: TaskSnapshot) => { - console.log(snapshot); -}); - -const modularRefFromURL = ref(modularStorage1, 'gs://bucket/path'); -console.log(modularRefFromURL.fullPath); - -setMaxOperationRetryTime(modularStorage1, 5000).then(() => { - console.log('Modular max operation retry time set'); -}); - -setMaxUploadRetryTime(modularStorage1, 25000).then(() => { - console.log('Modular max upload retry time set'); -}); - -const modularPutFile = putFile(modularRef1, '/local/path', { cacheControl: 'no-cache' }); -modularPutFile.then((snapshot: TaskSnapshot) => { - console.log(snapshot); -}); - -const modularWriteToFile = writeToFile(modularRef1, '/local/path'); -modularWriteToFile.then((snapshot: TaskSnapshot) => { - console.log(snapshot); -}); - -console.log(modularRef1.toString()); - -const modularChildRef = ref(modularRef1, 'child'); -console.log(modularChildRef.fullPath); - -setMaxDownloadRetryTime(modularStorage1, 25000).then(() => { - console.log('Modular max download retry time set'); -}); - -// checks modular statics exports -console.log(StringFormat.RAW); console.log(TaskEvent.STATE_CHANGED); -console.log(TaskState.SUCCESS); - -// Test type usage -const storageInstance2: FirebaseStorage = firebase.storage(); -console.log(storageInstance2.app.name); - -// Test backwards compatibility types -const legacyType: FirebaseStorageTypes.Module = firebase.storage(); -console.log(legacyType.app.name); -const legacyReference: FirebaseStorageTypes.Reference = firebase.storage().ref('test'); -console.log(legacyReference.fullPath); -const legacyMetadata: FirebaseStorageTypes.FullMetadata = {} as FirebaseStorageTypes.FullMetadata; -console.log(legacyMetadata); -const legacySettableMetadata: FirebaseStorageTypes.SettableMetadata = - {} as FirebaseStorageTypes.SettableMetadata; -console.log(legacySettableMetadata); -const legacyListResult: FirebaseStorageTypes.ListResult = {} as FirebaseStorageTypes.ListResult; -console.log(legacyListResult); -const legacyTaskSnapshot: FirebaseStorageTypes.TaskSnapshot = - {} as FirebaseStorageTypes.TaskSnapshot; -console.log(legacyTaskSnapshot); -const legacyTask: FirebaseStorageTypes.Task = {} as FirebaseStorageTypes.Task; -console.log(legacyTask); +console.log(TaskState.RUNNING); +console.log(SDK_VERSION); -// Test SDK_VERSION -const sdkVersion: string = storage.SDK_VERSION; -console.log(sdkVersion); +const typedStorage: FirebaseStorage = storage; +console.log(typedStorage.app.name); diff --git a/packages/storage/typedoc.json b/packages/storage/typedoc.json index e44c99c618..fa85dfe65f 100644 --- a/packages/storage/typedoc.json +++ b/packages/storage/typedoc.json @@ -1,6 +1,5 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["lib/index.ts"], - "intentionallyNotExported": ["StorageNamespace"] - + "entryPoints": ["lib/index.ts", "lib/types/storage.ts"], + "tsconfig": "tsconfig.json" } diff --git a/packages/vertexai/lib/public-types.ts b/packages/vertexai/lib/public-types.ts index 1ce05549ae..1bbcf28792 100644 --- a/packages/vertexai/lib/public-types.ts +++ b/packages/vertexai/lib/public-types.ts @@ -15,8 +15,8 @@ * limitations under the License. */ import { AI, AIErrorCode } from '@react-native-firebase/ai'; -import { FirebaseAuthTypes } from '@react-native-firebase/auth'; -import { FirebaseAppCheckTypes } from '@react-native-firebase/app-check'; +import type { Auth } from '@react-native-firebase/auth'; +import type { AppCheck } from '@react-native-firebase/app-check'; /** * @deprecated Use the new {@link AI | AI} instead. The Vertex AI in Firebase SDK has been @@ -33,8 +33,8 @@ export type VertexAI = AI; */ export interface VertexAIOptions { location?: string; - appCheck?: FirebaseAppCheckTypes.Module | null; - auth?: FirebaseAuthTypes.Module | null; + appCheck?: AppCheck | null; + auth?: Auth | null; } export type VertexAIErrorCode = AIErrorCode; diff --git a/tests/globals.js b/tests/globals.js index 8003dfb2f8..40501c2520 100644 --- a/tests/globals.js +++ b/tests/globals.js @@ -48,9 +48,6 @@ import shouldMatchers from 'should'; // [TEST->Finish][✅] uploads a base64url string globalThis.RNFBDebug = false; -// this may be used to locate modular API errors quickly -globalThis.RNFB_MODULAR_DEPRECATION_STRICT_MODE = true; - // Needed for Platform.Other session storage import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -58,7 +55,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import '@react-native-firebase/analytics'; import '@react-native-firebase/app-check'; import '@react-native-firebase/app-distribution'; -import '@react-native-firebase/app/lib/utils'; import '@react-native-firebase/auth'; import '@react-native-firebase/crashlytics'; import '@react-native-firebase/database'; @@ -70,7 +66,7 @@ import '@react-native-firebase/messaging'; import '@react-native-firebase/ml'; import '@react-native-firebase/remote-config'; import '@react-native-firebase/storage'; -import firebase, * as modular from '@react-native-firebase/app'; +import * as modular from '@react-native-firebase/app'; import * as analyticsModular from '@react-native-firebase/analytics'; import * as appCheckModular from '@react-native-firebase/app-check'; import * as appDistributionModular from '@react-native-firebase/app-distribution'; @@ -355,12 +351,6 @@ Object.defineProperty(global, 'TestAdminApi', { }, }); -Object.defineProperty(global, 'firebase', { - get() { - return firebase; - }, -}); - Object.defineProperty(global, 'NativeModules', { get() { return new Proxy(