2
0
mirror of synced 2025-01-12 07:04:21 +00:00

Merge pull request #920 from invertase/bridge-detox

Merge new testing infra - bridge
This commit is contained in:
Michael Diarmid 2018-03-28 16:46:12 +01:00 committed by GitHub
commit ce0f8f19d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
173 changed files with 31838 additions and 122 deletions

4
.gitignore vendored
View File

@ -74,6 +74,10 @@ tests/build
tests/android/app/build
tests/ios/Pods
tests/firebase
tests-new/build
tests-new/android/app/build
tests-new/ios/Pods
tests-new/firebase
.gradle
local.properties
*.iml

View File

@ -74,6 +74,7 @@ docs
coverage
yarn.lock
tests
bridge/
lib/.watchmanconfig
buddybuild_postclone.sh
bin/test.js

View File

@ -3,7 +3,6 @@ package io.invertase.firebase;
import android.content.Context;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.UIManagerModule;

View File

@ -3,7 +3,6 @@ package io.invertase.firebase.storage;
import android.support.annotation.RequiresPermission;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.UIManagerModule;

18
bridge/.babelrc Normal file
View File

@ -0,0 +1,18 @@
{
"presets": [
"react-native"
],
"env": {
"development": {
"plugins": [
["istanbul", {
"useInlineSourceMaps": true,
"instrument": true,
"include": [
"firebase"
]
}]
]
}
}
}

6
bridge/.buckconfig Executable file
View File

@ -0,0 +1,6 @@
[android]
target = Google Inc.:Google APIs:23
[maven_repositories]
central = https://repo1.maven.org/maven2

10
bridge/.editorconfig Normal file
View File

@ -0,0 +1,10 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

39
bridge/.eslintrc Normal file
View File

@ -0,0 +1,39 @@
{
"extends": [
"airbnb",
"prettier",
"prettier/flowtype",
"prettier/react"
],
"parser": "babel-eslint",
"plugins": [
"flowtype",
"prettier"
],
"env": {
"es6": true,
"jasmine": true
},
"rules": {
"prettier/prettier": ["error", {
"trailingComma": "es5",
"singleQuote": true
}],
"react/forbid-prop-types": "warn",
"react/jsx-filename-extension": [
"off", { "extensions": [".js", ".jsx"] }
],
"class-methods-use-this": 0,
"no-console": 0,
"no-plusplus": 0,
"no-undef": 0,
"no-underscore-dangle": "off",
"no-use-before-define": 0
},
"globals": {
"__DEV__": true,
"window": true
}
}

97
bridge/.flowconfig Executable file
View File

@ -0,0 +1,97 @@
[ignore]
# We fork some components by platform.
.*/*.web.js
.*/*.android.js
# Some modules have their own node_modules with overlap
.*/node_modules/node-haste/.*
# Ugh
.*/node_modules/babel.*
.*/node_modules/babylon.*
.*/node_modules/invariant.*
# Ignore react and fbjs where there are overlaps, but don't ignore
# anything that react-native relies on
.*/node_modules/fbjs/lib/Map.js
.*/node_modules/fbjs/lib/ErrorUtils.js
# Flow has a built-in definition for the 'react' module which we prefer to use
# over the currently-untyped source
.*/node_modules/react/react.js
.*/node_modules/react/lib/React.js
.*/node_modules/react/lib/ReactDOM.js
.*/__mocks__/.*
.*/__tests__/.*
.*/commoner/test/source/widget/share.js
# Ignore commoner tests
.*/node_modules/commoner/test/.*
# See https://github.com/facebook/flow/issues/442
.*/react-tools/node_modules/commoner/lib/reader.js
# Ignore jest
.*/node_modules/jest-cli/.*
# Ignore Website
.*/website/.*
# Ignore generators
.*/local-cli/generator.*
# Ignore BUCK generated folders
.*\.buckd/
# Ignore RNPM
.*/local-cli/rnpm/.*
.*/node_modules/is-my-json-valid/test/.*\.json
.*/node_modules/iconv-lite/encodings/tables/.*\.json
.*/node_modules/y18n/test/.*\.json
.*/node_modules/spdx-license-ids/spdx-license-ids.json
.*/node_modules/spdx-exceptions/index.json
.*/node_modules/resolve/test/subdirs/node_modules/a/b/c/x.json
.*/node_modules/resolve/lib/core.json
.*/node_modules/jsonparse/samplejson/.*\.json
.*/node_modules/json5/test/.*\.json
.*/node_modules/ua-parser-js/test/.*\.json
.*/node_modules/builtin-modules/builtin-modules.json
.*/node_modules/binary-extensions/binary-extensions.json
.*/node_modules/url-regex/tlds.json
.*/node_modules/joi/.*\.json
.*/node_modules/isemail/.*\.json
.*/node_modules/tr46/.*\.json
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow
flow/
[options]
module.system=haste
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
munge_underscores=true
module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-5]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-5]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
[version]
^0.25.0

40
bridge/.gitignore vendored Executable file
View File

@ -0,0 +1,40 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IJ
#
.idea
.gradle
local.properties
# node.js
#
node_modules/
npm-debug.log
# BUCK
buck-out/
\.buckd/
android/app/libs
android/keystores/debug.keystore

1
bridge/.watchmanconfig Executable file
View File

@ -0,0 +1 @@
{}

99
bridge/README.md Executable file
View File

@ -0,0 +1,99 @@
# React Native Firebase - Testing Project
## Requirements
* Make sure you have Xcode installed (tested with Xcode 8.1-8.2).
* make sure you have node installed (`brew install node`, node 7.6.0 and up is required.
* Make sure you have react-native dependencies installed:
* react-native-cli is installed (`npm install -g react-native-cli`)
* watchman is installed (`brew install watchman`)
* [appleSimUtils](https://github.com/wix/AppleSimulatorUtils)
* detox-cli `npm install -g detox-cli`
### Step 1: Npm install
* Run `npm install`.
## To test Release build of your app
### Step 2: Build
* Build the demo project
```sh
detox build --configuration ios.sim.release
```
### Step 3: Test
* Run tests on the demo project
```sh
detox test --configuration ios.sim.release
```
This action will open a new simulator and run the tests on it.
## To test Debug build of your app
### Step 2: Build
* Build the demo project
```sh
detox build --configuration ios.sim.debug
```
### Step 3: Test
* start react-native packager
```sh
npm run start
```
* Run tests on the demo project
```sh
detox test --configuration ios.sim.debug
```
This action will open a new simulator and run the tests on it.
### TODO - Troubleshooting
Gradle issues... https://stackoverflow.com/questions/46917365/error-could-not-initialize-class-com-android-sdklib-repository-androidsdkhandle?rq=1
mac: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"`
windows `"C://Program Files/Java/jdk_1.x_"`
android sdk root `export ANDROID_SDK_ROOT="/Users/mike/Library/Android/sdk"`
Add platform-tools to your path
echo 'export ANDROID_HOME=/Users/$USER/Library/Android/sdk' >> ~/.bash_profile
echo 'export PATH=${PATH}:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools' >> ~/.bash_profile
Name: TestingAVD
CPU/ABI: null (null)
Path: /Users/mike/.android/avd/Actually_THIS_one.avd
Error: Failed to parse properties from /Users/mike/.android/avd/Actually_THIS_one.avd/config.ini
#### Running specific tests
Add a `--grep` to e2e/mocha.opts file, e.g. `--grep auth` for all tests that have auth in the file path or tests descriptions.
#### Running Node debugger
Add `--inspect` to e2e/mocha.opts file
To open node debugger tools on chrome navigate to chrome://inspect/#devices and click the `Open dedicated DevTools for Node` link.
Add the default connection of `localhost:9229` if you haven't already - then the debugger will automatically connect everytime you start tests with inspect flag.
#### Mocha options
See https://mochajs.org/#usage

View File

@ -0,0 +1,10 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

66
bridge/android/app/BUCK Executable file
View File

@ -0,0 +1,66 @@
import re
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
android_library(
name = 'all-libs',
exported_deps = lib_deps
)
android_library(
name = 'app-code',
srcs = glob([
'src/main/java/**/*.java',
]),
deps = [
':all-libs',
':build_config',
':res',
],
)
android_build_config(
name = 'build_config',
package = 'com.example',
)
android_resource(
name = 'res',
res = 'src/main/res',
package = 'com.example',
)
android_binary(
name = 'app',
package_type = 'debug',
manifest = 'src/main/AndroidManifest.xml',
keystore = '//android/keystores:debug',
deps = [
':app-code',
],
)

124
bridge/android/app/build.gradle Executable file
View File

@ -0,0 +1,124 @@
apply plugin: "com.android.application"
apply plugin: "com.google.firebase.firebase-perf"
apply plugin: 'io.fabric'
import com.android.build.OutputFile
project.ext.react = [
entryFile: "index.js"
]
apply from: "../../node_modules/react-native/react.gradle"
def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion 27
buildToolsVersion '27.0.2'
defaultConfig {
applicationId "com.testing"
minSdkVersion 18
targetSdkVersion 27
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
missingDimensionStrategy "minReactNative", "minReactNative46"
multiDexEnabled true
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86"
}
}
signingConfigs {
release {
storeFile file("keystore.jks")
storePassword "12345678"
keyAlias "key0"
keyPassword "12345678"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a": 1, "x86": 2]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
}
project.ext.firebaseVersion = '12.0.0'
dependencies {
compile project(':bridge')
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation(project(':react-native-firebase')) {
transitive = false
}
implementation project(':bridge')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.google.android.gms:play-services-base:$firebaseVersion"
compile "com.google.firebase:firebase-ads:$firebaseVersion"
compile "com.google.firebase:firebase-auth:$firebaseVersion"
compile "com.google.firebase:firebase-config:$firebaseVersion"
compile "com.google.firebase:firebase-core:$firebaseVersion"
compile "com.google.firebase:firebase-crash:$firebaseVersion"
compile "com.google.firebase:firebase-database:$firebaseVersion"
compile "com.google.firebase:firebase-messaging:$firebaseVersion"
compile "com.google.firebase:firebase-perf:$firebaseVersion"
compile "com.google.firebase:firebase-storage:$firebaseVersion"
compile "com.google.firebase:firebase-firestore:$firebaseVersion"
compile "com.google.firebase:firebase-invites:$firebaseVersion"
compile('com.crashlytics.sdk.android:crashlytics:2.9.1@aar') {
transitive = true
}
compile "com.android.support:appcompat-v7:27.1.0"
implementation fileTree(dir: "libs", include: ["*.jar"])
androidTestImplementation(project(path: ":detox"))
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test:rules:1.0.1'
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'

View File

@ -0,0 +1,152 @@
{
"project_info": {
"project_number": "305229645282",
"firebase_url": "https://rnfirebase-b9ad4.firebaseio.com",
"project_id": "rnfirebase-b9ad4",
"storage_bucket": "rnfirebase-b9ad4.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:305229645282:android:efe37851d57e1d05",
"android_client_info": {
"package_name": "com.reactnativefirebasedemo"
}
},
"oauth_client": [
{
"client_id": "305229645282-5fgq5kq024eqpvji5o0i7jq7q7bnnpl9.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.reactnativefirebasedemo",
"certificate_hash": "1f92c8aab0a091a3aaccfa144bf402bb97273494"
}
},
{
"client_id": "305229645282-cvp6v0iogjjuuvi5g2dcb3lrr9n884a3.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.reactnativefirebasedemo",
"certificate_hash": "859f2afac694e21d26ca67e750c9875107c2e755"
}
},
{
"client_id": "305229645282-j8ij0jev9ut24odmlk9i215pas808ugn.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCzbBYFyX8d6VdSu7T4s10IWYbPc-dguwM"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "305229645282-t29pn6o2t7se1f7rvrfsll4r0pvd6fb6.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.invertase.RNFirebaseTests"
}
},
{
"client_id": "305229645282-j8ij0jev9ut24odmlk9i215pas808ugn.apps.googleusercontent.com",
"client_type": 3
}
]
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:305229645282:android:c9de0f8cb930daf5",
"android_client_info": {
"package_name": "com.reactnativefirebaseexamples"
}
},
"oauth_client": [
{
"client_id": "305229645282-hu7tr12kgn5lfhq82l51b1sh66aaue5f.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.reactnativefirebaseexamples",
"certificate_hash": "1f92c8aab0a091a3aaccfa144bf402bb97273494"
}
},
{
"client_id": "305229645282-j8ij0jev9ut24odmlk9i215pas808ugn.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCzbBYFyX8d6VdSu7T4s10IWYbPc-dguwM"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 2,
"other_platform_oauth_client": [
{
"client_id": "305229645282-t29pn6o2t7se1f7rvrfsll4r0pvd6fb6.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.invertase.RNFirebaseTests"
}
},
{
"client_id": "305229645282-j8ij0jev9ut24odmlk9i215pas808ugn.apps.googleusercontent.com",
"client_type": 3
}
]
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:305229645282:android:af36d4d29a83e04c",
"android_client_info": {
"package_name": "com.testing"
}
},
"oauth_client": [
{
"client_id": "305229645282-j8ij0jev9ut24odmlk9i215pas808ugn.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCzbBYFyX8d6VdSu7T4s10IWYbPc-dguwM"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

BIN
bridge/android/app/keystore.jks Executable file

Binary file not shown.

63
bridge/android/app/proguard-rules.pro vendored Executable file
View File

@ -0,0 +1,63 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
# okhttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**

View File

@ -0,0 +1,24 @@
package com.testing;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import com.wix.detox.Detox;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
@Test
public void runDetoxTests() throws InterruptedException {
Detox.runTests(mActivityRule);
}
}

View File

@ -0,0 +1,71 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.testing">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:name="com.testing.MainApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/AppTheme">
<service
android:name="io.invertase.firebase.messaging.RNFirebaseMessagingService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service android:name="io.invertase.firebase.messaging.RNFirebaseInstanceIdService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
</service>
<service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" />
<receiver android:name="io.invertase.firebase.notifications.RNFirebaseNotificationReceiver"/>
<receiver android:enabled="true" android:exported="true" android:name="io.invertase.firebase.notifications.RNFirebaseNotificationsRebootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- App Links -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="je786.app.goo.gl" android:scheme="http"/>
<data android:host="je786.app.goo.gl" android:scheme="https"/>
</intent-filter>
<activity
android:name="com.testing.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>

View File

@ -0,0 +1,80 @@
package com.testing;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity {
public static final int PERMISSION_REQ_CODE = 1234;
public static final int OVERLAY_PERMISSION_REQ_CODE = 1235;
String[] perms = {
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"
};
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "testing";
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
checkWindowPerms();
}
public void checkWindowPerms() {
// Checking if device version > 22 and we need to use new permission model
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
// Checking if we can draw window overlay
if (!Settings.canDrawOverlays(this)) {
// Requesting permission for window overlay(needed for all react-native apps)
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
}
for (String perm : perms) {
// Checking each permission and if denied then requesting permissions
if (checkSelfPermission(perm) == PackageManager.PERMISSION_DENIED) {
requestPermissions(perms, PERMISSION_REQ_CODE);
break;
}
}
}
}
// Window overlay permission intent result
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
checkWindowPerms();
}
}
// Permission results
@Override
public void onRequestPermissionsResult(int permsRequestCode, String[] permissions, int[] grantResults) {
switch (permsRequestCode) {
case PERMISSION_REQ_CODE:
// example how to get result of permissions requests (there can be more then one permission dialog)
// boolean readAccepted = grantResults[0]==PackageManager.PERMISSION_GRANTED;
// boolean writeAccepted = grantResults[1]==PackageManager.PERMISSION_GRANTED;
// checking permissions to prevent situation when user denied some permission
checkWindowPerms();
break;
}
}
}

View File

@ -0,0 +1,78 @@
package com.testing;
import android.app.Application;
import com.facebook.react.ReactApplication;
import io.invertase.bridge.RNBridgePackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import io.invertase.bridge.RNBridgePackage;
import io.invertase.firebase.RNFirebasePackage;
import io.invertase.firebase.admob.RNFirebaseAdMobPackage;
import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage;
import io.invertase.firebase.auth.RNFirebaseAuthPackage;
import io.invertase.firebase.config.RNFirebaseRemoteConfigPackage;
import io.invertase.firebase.crash.RNFirebaseCrashPackage;
import io.invertase.firebase.database.RNFirebaseDatabasePackage;
import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage;
import io.invertase.firebase.firestore.RNFirebaseFirestorePackage;
import io.invertase.firebase.instanceid.RNFirebaseInstanceIdPackage;
import io.invertase.firebase.invites.RNFirebaseInvitesPackage;
import io.invertase.firebase.links.RNFirebaseLinksPackage;
import io.invertase.firebase.messaging.RNFirebaseMessagingPackage;
import io.invertase.firebase.notifications.RNFirebaseNotificationsPackage;
import io.invertase.firebase.perf.RNFirebasePerformancePackage;
import io.invertase.firebase.storage.RNFirebaseStoragePackage;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNBridgePackage(),
new RNBridgePackage(),
new RNFirebasePackage(),
new RNFirebaseAdMobPackage(),
new RNFirebaseAnalyticsPackage(),
new RNFirebaseAuthPackage(),
new RNFirebaseRemoteConfigPackage(),
new RNFirebaseCrashPackage(),
new RNFirebaseCrashlyticsPackage(),
new RNFirebaseDatabasePackage(),
new RNFirebaseFirestorePackage(),
new RNFirebaseInstanceIdPackage(),
new RNFirebaseInvitesPackage(),
new RNFirebaseLinksPackage(),
new RNFirebaseMessagingPackage(),
new RNFirebaseNotificationsPackage(),
new RNFirebasePerformancePackage(),
new RNFirebaseStoragePackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">RNF Test</string>
</resources>

View File

@ -0,0 +1,8 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

50
bridge/android/build.gradle Executable file
View File

@ -0,0 +1,50 @@
buildscript {
repositories {
jcenter()
google()
maven {
url 'https://maven.fabric.io/public'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.google.gms:google-services:3.1.2'
classpath 'com.google.firebase:firebase-plugins:1.1.1'
classpath 'io.fabric.tools:gradle:1.25.1'
}
}
allprojects {
repositories {
mavenLocal()
jcenter()
google()
maven {
url "$rootDir/../node_modules/react-native/android"
}
}
}
subprojects {
ext {
compileSdk = 27
buildTools = "27.0.2"
minSdk = 18
targetSdk = 26
}
afterEvaluate { project ->
if (!project.name.equalsIgnoreCase("app")
&& project.hasProperty("android")) {
android {
compileSdkVersion compileSdk
buildToolsVersion buildTools
defaultConfig {
minSdkVersion minSdk
targetSdkVersion targetSdk
}
}
}
}
}

View File

@ -0,0 +1,24 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useDeprecatedNdk=true
android.enableAapt2=false

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Sat Sep 09 20:32:40 IDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-all.zip

164
bridge/android/gradlew vendored Executable file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
bridge/android/gradlew.bat vendored Executable file
View File

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

8
bridge/android/keystores/BUCK Executable file
View File

@ -0,0 +1,8 @@
keystore(
name = 'debug',
store = 'debug.keystore',
properties = 'debug.keystore.properties',
visibility = [
'PUBLIC',
],
)

View File

@ -0,0 +1,4 @@
key.store=debug.keystore
key.alias=androiddebugkey
key.store.password=android
key.alias.password=android

14
bridge/android/settings.gradle Executable file
View File

@ -0,0 +1,14 @@
rootProject.name = 'RNFTests'
include ':bridge'
project(':bridge').projectDir = new File(rootProject.projectDir, '../node_modules/bridge/android')
include ':react-native-firebase'
project(':react-native-firebase').projectDir = new File(rootProject.projectDir, './../../android')
include ':bridge'
project(':bridge').projectDir = new File(rootProject.projectDir, '../node_modules/bridge/android')
include ':app'
include ':detox'
project(':detox').projectDir = new File(rootProject.projectDir, '../node_modules/detox/android/detox')

39
bridge/app.js Executable file
View File

@ -0,0 +1,39 @@
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
// must import before all else
import React, { Component } from 'react';
import { AppRegistry, Text, View } from 'react-native';
import bridge from 'bridge/platform/react-native';
import firebase from './firebase';
require('sinon');
require('should-sinon');
require('should');
class Root extends Component {
constructor(props) {
super(props);
this.state = {
message: '',
};
bridge.setBridgeProperty('root', this);
bridge.setBridgeProperty('module', firebase);
}
render() {
return (
<View>
<Text testID="messageText">{this.state.message}</Text>
</View>
);
}
}
AppRegistry.registerComponent('testing', () => Root);

View File

@ -0,0 +1,224 @@
describe('.auth()', () => {
beforeEach(async () => {
await device.reloadReactNative();
// bridge.root.setState({ message: this.currentTest.title });
});
describe('.signInAnonymously()', () => {
it('it should sign in anonymously', () => {
const successCb = currentUser => {
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);
return firebase.auth().signOut();
};
return firebase
.auth()
.signInAnonymously()
.then(successCb);
});
});
describe('.signInAnonymouslyAndRetrieveData()', () => {
it('it should sign in anonymously', () => {
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()
.signInAnonymouslyAndRetrieveData()
.then(successCb);
});
});
describe('.signInWithEmailAndPassword()', () => {
it('it should login with email and password', () => {
const email = 'test@test.com';
const pass = 'test1234';
const successCb = currentUser => {
currentUser.should.be.an.Object();
currentUser.uid.should.be.a.String();
currentUser.toJSON().should.be.an.Object();
currentUser.toJSON().email.should.eql('test@test.com');
currentUser.isAnonymous.should.equal(false);
currentUser.providerId.should.equal('firebase');
currentUser.should.equal(firebase.auth().currentUser);
return firebase.auth().signOut();
};
return firebase
.auth()
.signInWithEmailAndPassword(email, pass)
.then(successCb);
});
it('it should error on login if user is disabled', () => {
const email = 'disabled@account.com';
const pass = 'test1234';
const successCb = () => Promise.reject(new Error('Did not error.'));
const failureCb = error => {
error.code.should.equal('auth/user-disabled');
error.message.should.equal(
'The user account has been disabled by an administrator.'
);
return Promise.resolve();
};
return firebase
.auth()
.signInWithEmailAndPassword(email, pass)
.then(successCb)
.catch(failureCb);
});
it('it should error on login if password incorrect', () => {
const email = 'test@test.com';
const pass = 'test1234666';
const successCb = () => Promise.reject(new Error('Did not error.'));
const failureCb = error => {
error.code.should.equal('auth/wrong-password');
error.message.should.equal(
'The password is invalid or the user does not have a password.'
);
return Promise.resolve();
};
return firebase
.auth()
.signInWithEmailAndPassword(email, pass)
.then(successCb)
.catch(failureCb);
});
it('it should error on login if user not found', () => {
const email = 'randomSomeone@fourOhFour.com';
const pass = 'test1234';
const successCb = () => Promise.reject(new Error('Did not error.'));
const failureCb = error => {
error.code.should.equal('auth/user-not-found');
error.message.should.equal(
'There is no user record corresponding to this identifier. The user may have been deleted.'
);
return Promise.resolve();
};
return firebase
.auth()
.signInWithEmailAndPassword(email, pass)
.then(successCb)
.catch(failureCb);
});
});
describe('.onAuthStateChanged()', () => {
it('calls callback with the current user and when auth state changes', async () => {
await firebase.auth().signInAnonymouslyAndRetrieveData();
// 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 new Promise(resolve => {
setTimeout(() => resolve(), 100);
});
// Assertions
callback.should.be.calledWith(null);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
});
it('stops listening when unsubscribe called', async () => {
await firebase.auth().signInAnonymouslyAndRetrieveData();
// 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 new Promise(resolve => {
setTimeout(() => resolve(), 100);
});
// Assertions
// callback.should.be.calledWith(null);
callback.should.be.calledTwice();
// Unsubscribe
unsubscribe();
// Sign back in
await firebase.auth().signInAnonymouslyAndRetrieveData();
// Assertions
callback.should.be.calledTwice();
// Tear down
await firebase.auth().signOut();
});
});
});

80
bridge/e2e/bridge.spec.js Executable file
View File

@ -0,0 +1,80 @@
const should = require('should');
describe('bridge', () => {
beforeEach(async function beforeEach() {
await device.reloadReactNative();
bridge.root.setState({ message: this.currentTest.title });
});
it('should provide -> global.bridge', () => {
should(bridge).not.be.undefined();
return Promise.resolve();
});
// main react-native module you're testing on
// in our case react-native-firebase
it('should provide -> bridge.module', () => {
should(bridge.module).not.be.undefined();
return Promise.resolve();
});
// react-native module access
it('should provide -> bridge.rn', () => {
should(bridge.rn).not.be.undefined();
should(bridge.rn.Platform.OS).be.a.String();
should(bridge.rn.Platform.OS).equal(device.getPlatform());
return Promise.resolve();
});
// 'global' context of the app's JS environment
it('should provide -> bridge.context', () => {
should(bridge.context).not.be.undefined();
should(bridge.context.setTimeout).be.a.Function();
should(bridge.context.window).be.a.Object();
// etc ... e.g. __coverage__ is here also if covering
return Promise.resolve();
});
// the apps root component
// allows you to read and set state if required
it('should provide -> bridge.root', async () => {
should(bridge.root).not.be.undefined();
should(bridge.root.setState).be.a.Function();
should(bridge.root.state).be.a.Object();
// test setting state
await new Promise(resolve =>
bridge.root.setState({ message: 'hello world' }, resolve)
);
should(bridge.root.state.message).equal('hello world');
return Promise.resolve();
});
// we shim our own reloadReactNative functionality as the detox reloadReactNative built-in
// hangs often and seems unpredictable - todo: investigate & PR if solution found
// reloadReactNative is replaced on init with bridge.root automatically
it('should allow reloadReactNative usage without breaking remote debug', async () => {
should(bridge.reload).be.a.Function();
// and check it works without breaking anything
await device.reloadReactNative();
should(bridge.reload).be.a.Function();
return Promise.resolve();
});
it('should allow launchApp usage without breaking remote debug', async () => {
should(bridge.module).not.be.undefined();
should(bridge.reload).be.a.Function();
should(bridge.rn).not.be.undefined();
should(bridge.rn.Platform.OS).be.a.String();
should(bridge.rn.Platform.OS).equal(device.getPlatform());
await device.launchApp({ newInstance: true });
should(bridge.module).not.be.undefined();
should(bridge.reload).be.a.Function();
should(bridge.rn).not.be.undefined();
should(bridge.rn.Platform.OS).be.a.String();
should(bridge.rn.Platform.OS).equal(device.getPlatform());
return Promise.resolve();
});
});

View File

@ -0,0 +1,77 @@
const should = require('should');
describe('firestore.runTransaction', () => {
beforeEach(async () => {
await device.reloadReactNative();
});
it('should set, update and delete transactionally and allow a return value', async () => {
let deleteMe = false;
const firestore = firebase.firestore();
const docRef = firestore
.collection('transactions')
.doc(Date.now().toString());
const updateFunction = async transaction => {
const doc = await transaction.get(docRef);
if (doc.exists && deleteMe) {
transaction.delete(docRef);
return 'bye';
}
if (!doc.exists) {
transaction.set(docRef, { value: 1 });
return 1;
}
const newValue = doc.data().value + 1;
if (newValue > 2) {
return Promise.reject(new Error('Value should not be greater than 2!'));
}
transaction.update(docRef, {
value: newValue,
somethingElse: 'update',
});
return newValue;
};
// set tests
const val1 = await firestore.runTransaction(updateFunction);
should.equal(val1, 1);
const doc1 = await docRef.get();
doc1.data().value.should.equal(1);
should.equal(doc1.data().somethingElse, undefined);
// update
const val2 = await firestore.runTransaction(updateFunction);
should.equal(val2, 2);
const doc2 = await docRef.get();
doc2.data().value.should.equal(2);
doc2.data().somethingElse.should.equal('update');
// rejecting / cancelling transaction
let didReject = false;
try {
await firestore.runTransaction(updateFunction);
} catch (e) {
didReject = true;
}
should.equal(didReject, true);
const doc3 = await docRef.get();
doc3.data().value.should.equal(2);
doc3.data().somethingElse.should.equal('update');
// delete
deleteMe = true;
const val4 = await firestore.runTransaction(updateFunction);
should.equal(val4, 'bye');
const doc4 = await docRef.get();
should.equal(doc4.exists, false);
return Promise.resolve('Test Completed');
});
});

20
bridge/e2e/init.js Executable file
View File

@ -0,0 +1,20 @@
const detox = require('detox');
const config = require('../package.json').detox;
global.sinon = require('sinon');
require('should-sinon');
global.should = require('should');
before(async () => {
await detox.init(config);
});
after(async () => {
await detox.cleanup();
});
Object.defineProperty(global, 'firebase', {
get() {
return bridge.module;
},
});

6
bridge/e2e/mocha.opts Executable file
View File

@ -0,0 +1,6 @@
--recursive
--timeout 120000
--slow 2200
--bail
--exit
--require bridge/platform/node

View File

@ -0,0 +1,175 @@
import { NativeModules } from 'react-native';
import INTERNALS from './internals';
import { isObject, isAndroid } from './utils';
import AdMob, { statics as AdMobStatics } from './modules/admob';
import Auth, { statics as AuthStatics } from './modules/auth';
import Analytics from './modules/analytics';
import Crash from './modules/crash';
import Performance from './modules/perf';
import RemoteConfig from './modules/config';
import Storage, { statics as StorageStatics } from './modules/storage';
import Database, { statics as DatabaseStatics } from './modules/database';
import Messaging, { statics as MessagingStatics } from './modules/messaging';
import Firestore, { statics as FirestoreStatics } from './modules/firestore';
import Links, { statics as LinksStatics } from './modules/links';
import Utils, { statics as UtilsStatics } from './modules/utils';
const FirebaseCoreModule = NativeModules.RNFirebase;
export default class FirebaseApp {
constructor(name: string, options: Object = {}) {
this._name = name;
this._namespaces = {};
this._options = Object.assign({}, options);
// native ios/android to confirm initialized
this._initialized = false;
this._nativeInitialized = false;
// modules
this.admob = this._staticsOrModuleInstance(AdMobStatics, AdMob);
this.auth = this._staticsOrModuleInstance(AuthStatics, Auth);
this.analytics = this._staticsOrModuleInstance({}, Analytics);
this.config = this._staticsOrModuleInstance({}, RemoteConfig);
this.crash = this._staticsOrModuleInstance({}, Crash);
this.database = this._staticsOrModuleInstance(DatabaseStatics, Database);
this.firestore = this._staticsOrModuleInstance(FirestoreStatics, Firestore);
this.links = this._staticsOrModuleInstance(LinksStatics, Links);
this.messaging = this._staticsOrModuleInstance(MessagingStatics, Messaging);
this.perf = this._staticsOrModuleInstance({}, Performance);
this.storage = this._staticsOrModuleInstance(StorageStatics, Storage);
this.utils = this._staticsOrModuleInstance(UtilsStatics, Utils);
this._extendedProps = {};
}
/**
*
* @param native
* @private
*/
_initializeApp(native = false) {
if (native) {
// for apps already initialized natively that
// we have info from RN constants
this._initialized = true;
this._nativeInitialized = true;
} else {
FirebaseCoreModule.initializeApp(this._name, this._options, (error, result) => {
this._initialized = true;
INTERNALS.SharedEventEmitter.emit(`AppReady:${this._name}`, { error, result });
});
}
}
/**
*
* @return {*}
*/
get name() {
if (this._name === INTERNALS.STRINGS.DEFAULT_APP_NAME) {
// ios and android firebase sdk's return different
// app names - so we just return what the web sdk
// would if it was default.
return '[DEFAULT]';
}
return this._name;
}
/**
*
* @return {*}
*/
get options() {
return Object.assign({}, this._options);
}
/**
* Undocumented firebase web sdk method that allows adding additional properties onto
* a firebase app instance.
*
* See: https://github.com/firebase/firebase-js-sdk/blob/master/tests/app/firebase_app.test.ts#L328
*
* @param props
*/
extendApp(props: Object) {
if (!isObject(props)) throw new Error(INTERNALS.ERROR_MISSING_ARG('Object', 'extendApp'));
const keys = Object.keys(props);
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i];
if (!this._extendedProps[key] && Object.hasOwnProperty.call(this, key)) {
throw new Error(INTERNALS.ERROR_PROTECTED_PROP(key));
}
this[key] = props[key];
this._extendedProps[key] = true;
}
}
/**
*
* @return {Promise}
*/
delete() {
throw new Error(INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('app', 'delete'));
// TODO only the ios sdk currently supports delete, add back in when android also supports it
// if (this._name === INTERNALS.STRINGS.DEFAULT_APP_NAME && this._nativeInitialized) {
// return Promise.reject(
// new Error('Unable to delete the default native firebase app instance.'),
// );
// }
//
// return FirebaseCoreModule.deleteApp(this._name);
}
/**
*
* @return {*}
*/
onReady(): Promise {
if (this._initialized) return Promise.resolve(this);
return new Promise((resolve, reject) => {
INTERNALS.SharedEventEmitter.once(`AppReady:${this._name}`, ({ error }) => {
if (error) return reject(new Error(error)); // error is a string as it's from native
return resolve(this); // return app
});
});
}
/**
*
* @param name
* @param statics
* @param InstanceClass
* @return {function()}
* @private
*/
_staticsOrModuleInstance(statics = {}, InstanceClass): Function {
const getInstance = () => {
const _name = `_${InstanceClass._NAMESPACE}`;
if (isAndroid && InstanceClass._NAMESPACE !== Utils._NAMESPACE && !INTERNALS.FLAGS.checkedPlayServices) {
INTERNALS.FLAGS.checkedPlayServices = true;
this.utils().checkPlayServicesAvailability();
}
if (!this._namespaces[_name]) {
this._namespaces[_name] = new InstanceClass(this, this._options);
}
return this._namespaces[_name];
};
Object.assign(getInstance, statics, {
nativeModuleExists: !!NativeModules[InstanceClass._NATIVE_MODULE],
});
return getInstance;
}
}

228
bridge/firebase/firebase.js Normal file
View File

@ -0,0 +1,228 @@
/**
* @providesModule Firebase
* @flow
*/
import { NativeModules, NativeEventEmitter } from 'react-native';
import INTERNALS from './internals';
import FirebaseApp from './firebase-app';
import { isObject, isString, isAndroid } from './utils';
// module imports
import AdMob, { statics as AdMobStatics } from './modules/admob';
import Auth, { statics as AuthStatics } from './modules/auth';
import Analytics from './modules/analytics';
import Crash from './modules/crash';
import Performance from './modules/perf';
import Links, { statics as LinksStatics } from './modules/links';
import RemoteConfig from './modules/config';
import Storage, { statics as StorageStatics } from './modules/storage';
import Database, { statics as DatabaseStatics } from './modules/database';
import Messaging, { statics as MessagingStatics } from './modules/messaging';
import Firestore, { statics as FirestoreStatics } from './modules/firestore';
import Utils, { statics as UtilsStatics } from './modules/utils';
const FirebaseCoreModule = NativeModules.RNFirebase;
class FirebaseCore {
constructor() {
this._nativeEmitters = {};
this._nativeSubscriptions = {};
if (!FirebaseCoreModule) {
throw (new Error(INTERNALS.STRINGS.ERROR_MISSING_CORE));
}
this._initializeNativeApps();
// modules
this.admob = this._appNamespaceOrStatics(AdMobStatics, AdMob);
this.auth = this._appNamespaceOrStatics(AuthStatics, Auth);
this.analytics = this._appNamespaceOrStatics({}, Analytics);
this.config = this._appNamespaceOrStatics({}, RemoteConfig);
this.crash = this._appNamespaceOrStatics({}, Crash);
this.database = this._appNamespaceOrStatics(DatabaseStatics, Database);
this.firestore = this._appNamespaceOrStatics(FirestoreStatics, Firestore);
this.links = this._appNamespaceOrStatics(LinksStatics, Links);
this.messaging = this._appNamespaceOrStatics(MessagingStatics, Messaging);
this.perf = this._appNamespaceOrStatics(DatabaseStatics, Performance);
this.storage = this._appNamespaceOrStatics(StorageStatics, Storage);
this.utils = this._appNamespaceOrStatics(UtilsStatics, Utils);
}
/**
* Bootstraps all native app instances that were discovered on boot
* @private
*/
_initializeNativeApps() {
for (let i = 0, len = FirebaseCoreModule.apps.length; i < len; i++) {
const app = FirebaseCoreModule.apps[i];
const options = Object.assign({}, app);
delete options.name;
INTERNALS.APPS[app.name] = new FirebaseApp(app.name, options);
INTERNALS.APPS[app.name]._initializeApp(true);
}
}
/**
* Web SDK initializeApp
*
* @param options
* @param name
* @return {*}
*/
initializeApp(options: Object = {}, name: string): FirebaseApp {
if (name && !isString(name)) {
throw new Error(INTERNALS.STRINGS.ERROR_INIT_STRING_NAME);
}
const _name = (name || INTERNALS.STRINGS.DEFAULT_APP_NAME).toUpperCase();
// return an existing app if found
// todo in v4 remove deprecation and throw an error
if (INTERNALS.APPS[_name]) {
console.warn(INTERNALS.STRINGS.WARN_INITIALIZE_DEPRECATION);
return INTERNALS.APPS[_name];
}
// only validate if app doesn't already exist
// to allow apps already initialized natively
// to still go through init without erroring (backwards compatibility)
if (!isObject(options)) {
throw new Error(INTERNALS.STRINGS.ERROR_INIT_OBJECT);
}
if (!options.apiKey) {
throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('apiKey'));
}
if (!options.appId) {
throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('appId'));
}
if (!options.databaseURL) {
throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('databaseURL'));
}
if (!options.messagingSenderId) {
throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('messagingSenderId'));
}
if (!options.projectId) {
throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('projectId'));
}
if (!options.storageBucket) {
throw new Error(INTERNALS.STRINGS.ERROR_MISSING_OPT('storageBucket'));
}
INTERNALS.APPS[_name] = new FirebaseApp(_name, options);
// only initialize if certain props are available
if (options.databaseURL && options.apiKey) {
INTERNALS.APPS[_name]._initializeApp();
}
return INTERNALS.APPS[_name];
}
/**
* Retrieves a Firebase app instance.
*
* When called with no arguments, the default app is returned.
* When an app name is provided, the app corresponding to that name is returned.
*
* @param name
* @return {*}
*/
app(name?: string): FirebaseApp {
const _name = name ? name.toUpperCase() : INTERNALS.STRINGS.DEFAULT_APP_NAME;
const app = INTERNALS.APPS[_name];
if (!app) throw new Error(INTERNALS.STRINGS.ERROR_APP_NOT_INIT(_name));
return app;
}
/**
* A (read-only) array of all initialized apps.
* @return {Array}
*/
get apps(): Array<Object> {
return Object.values(INTERNALS.APPS);
}
/*
* INTERNALS
*/
/**
* Subscribe to a native event for js side distribution by appName
* React Native events are hard set at compile - cant do dynamic event names
* so we use a single event send it to js and js then internally can prefix it
* and distribute dynamically.
*
* @param eventName
* @param nativeEmitter
* @private
*/
_subscribeForDistribution(eventName, nativeEmitter) {
if (!this._nativeSubscriptions[eventName]) {
nativeEmitter.addListener(eventName, (event) => {
if (event.appName) {
// native event has an appName property - auto prefix and internally emit
INTERNALS.SharedEventEmitter.emit(`${event.appName}-${eventName}`, event);
} else {
// standard event - no need to prefix
INTERNALS.SharedEventEmitter.emit(eventName, event);
}
});
this._nativeSubscriptions[eventName] = true;
}
}
/**
*
* @param statics
* @param InstanceClass
* @return {function(FirebaseApp=)}
* @private
*/
_appNamespaceOrStatics(statics = {}, InstanceClass): Function {
const namespace = InstanceClass._NAMESPACE;
const getNamespace = (app?: FirebaseApp) => {
let _app = app;
// throw an error if it's not a valid app instance
if (_app && !(_app instanceof FirebaseApp)) throw new Error(INTERNALS.STRINGS.ERROR_NOT_APP(namespace));
// default to the 'DEFAULT' app if no arg provided - will throw an error
// if default app not initialized
else if (!_app) _app = this.app(INTERNALS.STRINGS.DEFAULT_APP_NAME);
return INTERNALS.APPS[_app._name][namespace](_app);
};
Object.assign(getNamespace, statics, {
nativeModuleExists: !!NativeModules[InstanceClass._NATIVE_MODULE],
});
return getNamespace;
}
/**
*
* @param name
* @param nativeModule
* @return {*}
* @private
*/
_getOrSetNativeEmitter(name, nativeModule) {
if (this._nativeEmitters[name]) {
return this._nativeEmitters[name];
}
return this._nativeEmitters[name] = new NativeEventEmitter(nativeModule);
}
}
export default new FirebaseCore();

50
bridge/firebase/flow.js Normal file
View File

@ -0,0 +1,50 @@
/* eslint-disable */
// declare module 'react-native' {
// // noinspection ES6ConvertVarToLetConst
// declare var exports: any;
// }
declare type AuthResultType = {
authenticated: boolean,
user: Object|null
} | null;
declare type CredentialType = {
providerId: string,
token: string,
secret: string
};
declare type DatabaseListener = {
listenerId: number;
eventName: string;
successCallback: Function;
failureCallback?: Function;
};
declare type DatabaseModifier = {
type: 'orderBy' | 'limit' | 'filter';
name?: string;
key?: string;
limit?: number;
value?: any;
valueType?: string;
};
declare type GoogleApiAvailabilityType = {
status: number,
isAvailable: boolean,
isUserResolvableError?: boolean,
hasResolution?: boolean,
error?: string
};
declare class FirebaseError {
message: string,
name: string,
code: string,
stack: string,
path: string,
details: string,
modifiers: string
};

1368
bridge/firebase/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

80
bridge/firebase/index.js Normal file
View File

@ -0,0 +1,80 @@
/**
* @flow
*/
import firebase from './modules/core/firebase';
export default firebase;
/*
* Export App types
*/
export type { default as App } from './modules/core/app';
/*
* Export Auth types
*/
export type {
ActionCodeInfo,
ActionCodeSettings,
AdditionalUserInfo,
AuthCredential,
UserCredential,
UserInfo,
UserMetadata,
} from './modules/auth/types';
export type {
default as ConfirmationResult,
} from './modules/auth/phone/ConfirmationResult';
export type { default as User } from './modules/auth/User';
/*
* Export Database types
*/
export type { default as DataSnapshot } from './modules/database/DataSnapshot';
export type { default as OnDisconnect } from './modules/database/OnDisconnect';
export type { default as Reference } from './modules/database/Reference';
export type { default as DataQuery } from './modules/database/Query';
/*
* Export Firestore types
*/
export type {
DocumentListenOptions,
QueryListenOptions,
SetOptions,
SnapshotMetadata,
} from './modules/firestore/types';
export type {
default as CollectionReference,
} from './modules/firestore/CollectionReference';
export type {
default as DocumentChange,
} from './modules/firestore/DocumentChange';
export type {
default as DocumentReference,
} from './modules/firestore/DocumentReference';
export type {
default as DocumentSnapshot,
} from './modules/firestore/DocumentSnapshot';
export type { default as FieldPath } from './modules/firestore/FieldPath';
export type { default as FieldValue } from './modules/firestore/FieldValue';
export type { default as GeoPoint } from './modules/firestore/GeoPoint';
export type { default as Query } from './modules/firestore/Query';
export type {
default as QuerySnapshot,
} from './modules/firestore/QuerySnapshot';
export type { default as WriteBatch } from './modules/firestore/WriteBatch';
/*
* Export Messaging types
*/
export type {
default as RemoteMessage,
} from './modules/messaging/RemoteMessage';
/*
* Export Notifications types
*/
export type {
default as Notification,
} from './modules/notifications/Notification';

View File

@ -0,0 +1,239 @@
import { Platform, NativeModules } from 'react-native';
import EventEmitter from './utils/emitter/EventEmitter';
import SyncTree from './utils/SyncTree';
const DEFAULT_APP_NAME = Platform.OS === 'ios' ? '__FIRAPP_DEFAULT' : '[DEFAULT]';
const NAMESPACE_PODS = {
admob: 'Firebase/AdMob',
analytics: 'Firebase/Analytics',
auth: 'Firebase/Auth',
config: 'Firebase/RemoteConfig',
crash: 'Firebase/Crash',
database: 'Firebase/Database',
links: 'Firebase/DynamicLinks',
messaging: 'Firebase/Messaging',
perf: 'Firebase/Performance',
storage: 'Firebase/Storage',
};
const GRADLE_DEPS = {
admob: 'ads',
};
const PLAY_SERVICES_CODES = {
1: {
code: 'SERVICE_MISSING',
message: 'Google Play services is missing on this device.',
},
2: {
code: 'SERVICE_VERSION_UPDATE_REQUIRED',
message: 'The installed version of Google Play services on this device is out of date.',
},
3: {
code: 'SERVICE_DISABLED',
message: 'The installed version of Google Play services has been disabled on this device.',
},
9: {
code: 'SERVICE_INVALID',
message: 'The version of the Google Play services installed on this device is not authentic.',
},
18: {
code: 'SERVICE_UPDATING',
message: 'Google Play services is currently being updated on this device.',
},
19: {
code: 'SERVICE_MISSING_PERMISSION',
message: 'Google Play service doesn\'t have one or more required permissions.',
},
};
export default {
// default options
OPTIONS: {
logLevel: 'warn',
errorOnMissingPlayServices: true,
promptOnMissingPlayServices: true,
},
FLAGS: {
checkedPlayServices: false,
},
// track all initialized firebase apps
APPS: {
[DEFAULT_APP_NAME]: null,
},
STRINGS: {
WARN_INITIALIZE_DEPRECATION: 'Deprecation: Calling \'initializeApp()\' for apps that are already initialised natively ' +
'is unnecessary, use \'firebase.app()\' instead to access the already initialized default app instance.',
/**
* @return {string}
*/
get ERROR_MISSING_CORE() {
if (Platform.OS === 'ios') {
return 'RNFirebase core module was not found natively on iOS, ensure you have ' +
'correctly included the RNFirebase pod in your projects `Podfile` and have run `pod install`.' +
'\r\n\r\n See http://invertase.link/ios for the ios setup guide.';
}
return 'RNFirebase core module was not found natively on Android, ensure you have ' +
'correctly added the RNFirebase and Firebase gradle dependencies to your `android/app/build.gradle` file.' +
'\r\n\r\n See http://invertase.link/android for the android setup guide.';
},
ERROR_INIT_OBJECT: 'Firebase.initializeApp(options <-- requires a valid configuration object.',
ERROR_INIT_STRING_NAME: 'Firebase.initializeApp(options, name <-- requires a valid string value.',
/**
* @return {string}
*/
ERROR_MISSING_CB(method) {
return `Missing required callback for method ${method}().`;
},
/**
* @return {string}
*/
ERROR_MISSING_ARG(type, method) {
return `Missing required argument of type '${type}' for method '${method}()'.`;
},
/**
* @return {string}
*/
ERROR_MISSING_ARG_NAMED(name, type, method) {
return `Missing required argument '${name}' of type '${type}' for method '${method}()'.`;
},
/**
* @return {string}
*/
ERROR_ARG_INVALID_VALUE(name, expected, got) {
return `Invalid value for argument '${name}' expected value '${expected}' but got '${got}'.`;
},
/**
* @return {string}
*/
ERROR_PROTECTED_PROP(name) {
return `Property '${name}' is protected and can not be overridden by extendApp.`;
},
/**
* @return {string}
* @param namespace
* @param nativeModule
*/
ERROR_MISSING_MODULE(namespace, nativeModule) {
const snippet = `firebase.${namespace}()`;
if (Platform.OS === 'ios') {
return `You attempted to use a firebase module that's not installed natively on your iOS project by calling ${snippet}.` +
'\r\n\r\nEnsure you have the required Firebase iOS SDK pod for this module included in your Podfile, in this instance ' +
`confirm you've added "pod '${NAMESPACE_PODS[namespace]}'" to your Podfile` +
'\r\n\r\nSee http://invertase.link/ios for full setup instructions.';
}
const fbSDKDep = `'com.google.firebase:firebase-${GRADLE_DEPS[namespace] || namespace}'`;
const rnFirebasePackage = `'io.invertase.firebase.${namespace}.${nativeModule}Package'`;
const newInstance = `'new ${nativeModule}Package()'`;
return `You attempted to use a firebase module that's not installed on your Android project by calling ${snippet}.` +
`\r\n\r\nEnsure you have:\r\n\r\n1) Installed the required Firebase Android SDK dependency ${fbSDKDep} in your 'android/app/build.gradle' ` +
`file.\r\n\r\n2) Imported the ${rnFirebasePackage} module in your 'MainApplication.java' file.\r\n\r\n3) Added the ` +
`${newInstance} line inside of the RN 'getPackages()' method list.` +
'\r\n\r\nSee http://invertase.link/android for full setup instructions.';
},
/**
* @return {string}
*/
ERROR_APP_NOT_INIT(appName) {
return `The [${appName}] firebase app has not been initialized!`;
},
/**
* @param optName
* @return {string}
* @constructor
*/
ERROR_MISSING_OPT(optName) {
return `Failed to initialize app. FirebaseOptions missing or invalid '${optName}' property.`;
},
/**
* @return {string}
*/
ERROR_NOT_APP(namespace) {
return `Invalid FirebaseApp instance passed to firebase.${namespace}(app <--).`;
},
/**
* @return {string}
*/
ERROR_UNSUPPORTED_CLASS_METHOD(className, method) {
return `${className}.${method}() is unsupported by the native Firebase SDKs.`;
},
/**
* @return {string}
*/
ERROR_UNSUPPORTED_CLASS_PROPERTY(className, property) {
return `${className}.${property} is unsupported by the native Firebase SDKs.`;
},
/**
* @return {string}
*/
ERROR_UNSUPPORTED_MODULE_METHOD(module, method) {
return `firebase.${module._NAMESPACE}().${method}() is unsupported by the native Firebase SDKs.`;
},
/**
* @return {string}
*/
ERROR_PLAY_SERVICES(statusCode) {
const knownError = PLAY_SERVICES_CODES[statusCode];
let start = 'Google Play Services is required to run firebase services on android but a valid installation was not found on this device.';
if (statusCode === 2) {
start = 'Google Play Services is out of date and may cause some firebase services like authentication to hang when used. It is recommended that you update it.';
}
// eslint-disable-next-line prefer-template
return `${start}\r\n\r\n` +
'-------------------------\r\n' +
(knownError ?
`${knownError.code}: ${knownError.message} (code ${statusCode})` :
`A specific play store availability reason reason was not available (unknown code: ${statusCode || null})`
) +
'\r\n-------------------------' +
'\r\n\r\n' +
'For more information on how to resolve this issue, configure Play Services checks or for guides on how to validate Play Services on your users devices see the link below:' +
'\r\n\r\nhttp://invertase.link/play-services';
},
DEFAULT_APP_NAME,
},
SharedEventEmitter: new EventEmitter(),
SyncTree: NativeModules.RNFirebaseDatabase ? new SyncTree(NativeModules.RNFirebaseDatabase) : null,
// internal utils
deleteApp(name: String) {
const app = this.APPS[name];
if (!app) return Promise.resolve();
// https://firebase.google.com/docs/reference/js/firebase.app.App#delete
return app.delete().then(() => {
delete this.APPS[name];
return true;
});
},
};

View File

@ -0,0 +1,100 @@
import React from 'react';
import { ViewPropTypes, requireNativeComponent } from 'react-native';
import PropTypes from 'prop-types';
import EventTypes, { NativeExpressEventTypes } from './EventTypes';
import { nativeToJSError } from '../../utils';
import AdRequest from './AdRequest';
import VideoOptions from './VideoOptions';
const adMobPropTypes = {
...ViewPropTypes,
size: PropTypes.string.isRequired,
unitId: PropTypes.string.isRequired,
/* eslint-disable react/forbid-prop-types */
request: PropTypes.object,
video: PropTypes.object,
/* eslint-enable react/forbid-prop-types */
};
Object.keys(EventTypes).forEach(eventType => {
adMobPropTypes[eventType] = PropTypes.func;
});
Object.keys(NativeExpressEventTypes).forEach(eventType => {
adMobPropTypes[eventType] = PropTypes.func;
});
const nativeComponents = {};
function getNativeComponent(name) {
if (nativeComponents[name]) return nativeComponents[name];
const component = requireNativeComponent(name, AdMobComponent, {
nativeOnly: {
onBannerEvent: true,
},
});
nativeComponents[name] = component;
return component;
}
class AdMobComponent extends React.Component {
static propTypes = adMobPropTypes;
static defaultProps = {
request: new AdRequest().addTestDevice().build(),
video: new VideoOptions().build(),
};
constructor(props) {
super(props);
this.state = {
width: 0,
height: 0,
};
this.nativeView = getNativeComponent(props.class);
}
/**
* Handle a single banner event and pass to
* any props watching it
* @param nativeEvent
*/
onBannerEvent = ({ nativeEvent }) => {
if (this.props[nativeEvent.type]) {
if (nativeEvent.type === 'onAdFailedToLoad') {
const { code, message } = nativeEvent.payload;
this.props[nativeEvent.type](nativeToJSError(code, message));
} else {
this.props[nativeEvent.type](nativeEvent.payload || {});
}
}
if (nativeEvent.type === 'onSizeChange')
this.updateSize(nativeEvent.payload);
};
/**
* Set the JS size of the loaded banner
* @param width
* @param height
*/
updateSize = ({ width, height }) => {
this.setState({ width, height });
};
/**
* Render the native component
* @returns {XML}
*/
render() {
return (
<this.nativeView
{...this.props}
style={[this.props.style, { ...this.state }]}
onBannerEvent={this.onBannerEvent}
/>
);
}
}
export default AdMobComponent;

View File

@ -0,0 +1,58 @@
export default class AdRequest {
constructor() {
this._props = {
keywords: [],
testDevices: [],
};
}
build() {
return this._props;
}
addTestDevice(deviceId?: string) {
this._props.testDevices.push(deviceId || 'DEVICE_ID_EMULATOR');
return this;
}
addKeyword(keyword: string) {
this._props.keywords.push(keyword);
return this;
}
setBirthday() {
// TODO
}
setContentUrl(url: string) {
this._props.contentUrl = url;
return this;
}
setGender(gender: 'male | female | unknown') {
const genders = ['male', 'female', 'unknown'];
if (genders.includes(gender)) {
this._props.gender = gender;
}
return this;
}
setLocation() {
// TODO
}
setRequestAgent(requestAgent: string) {
this._props.requestAgent = requestAgent;
return this;
}
setIsDesignedForFamilies(isDesignedForFamilies: boolean) {
this._props.isDesignedForFamilies = isDesignedForFamilies;
return this;
}
tagForChildDirectedTreatment(tagForChildDirectedTreatment: boolean) {
this._props.tagForChildDirectedTreatment = tagForChildDirectedTreatment;
return this;
}
}

View File

@ -0,0 +1,14 @@
import React from 'react';
import AdMobComponent from './AdMobComponent';
function Banner({ ...props }) {
return <AdMobComponent {...props} class="RNFirebaseAdMobBanner" />;
}
Banner.propTypes = AdMobComponent.propTypes;
Banner.defaultProps = {
size: 'SMART_BANNER',
};
export default Banner;

View File

@ -0,0 +1,23 @@
/**
* @flow
*/
export default {
onAdLoaded: 'onAdLoaded',
onAdOpened: 'onAdOpened',
onAdLeftApplication: 'onAdLeftApplication',
onAdClosed: 'onAdClosed',
onAdFailedToLoad: 'onAdFailedToLoad',
};
export const NativeExpressEventTypes = {
onVideoEnd: 'onVideoEnd',
onVideoMute: 'onVideoMute',
onVideoPause: 'onVideoPause',
onVideoPlay: 'onVideoPlay',
onVideoStart: 'onVideoStart',
};
export const RewardedVideoEventTypes = {
onRewarded: 'onRewarded',
onRewardedVideoStarted: 'onRewardedVideoStarted',
};

View File

@ -0,0 +1,119 @@
import { Platform } from 'react-native';
import { statics } from './';
import AdRequest from './AdRequest';
import { SharedEventEmitter } from '../../utils/events';
import { getNativeModule } from '../../utils/native';
import { nativeToJSError } from '../../utils';
import type AdMob from './';
let subscriptions = [];
export default class Interstitial {
_admob: AdMob;
constructor(admob: AdMob, adUnit: string) {
// Interstitials on iOS require a new instance each time
if (Platform.OS === 'ios') {
getNativeModule(admob).clearInterstitial(adUnit);
}
for (let i = 0, len = subscriptions.length; i < len; i++) {
subscriptions[i].remove();
}
subscriptions = [];
this._admob = admob;
this.adUnit = adUnit;
this.loaded = false;
SharedEventEmitter.removeAllListeners(`interstitial_${adUnit}`);
SharedEventEmitter.addListener(
`interstitial_${adUnit}`,
this._onInterstitialEvent
);
}
/**
* Handle a JS emit event
* @param event
* @private
*/
_onInterstitialEvent = event => {
const eventType = `interstitial:${this.adUnit}:${event.type}`;
let emitData = Object.assign({}, event);
switch (event.type) {
case 'onAdLoaded':
this.loaded = true;
break;
case 'onAdFailedToLoad':
emitData = nativeToJSError(event.payload.code, event.payload.message);
emitData.type = event.type;
break;
default:
}
SharedEventEmitter.emit(eventType, emitData);
SharedEventEmitter.emit(`interstitial:${this.adUnit}:*`, emitData);
};
/**
* Load an ad with an instance of AdRequest
* @param request
* @returns {*}
*/
loadAd(request?: AdRequest) {
let adRequest = request;
if (!adRequest || !Object.keys(adRequest)) {
adRequest = new AdRequest().addTestDevice().build();
}
return getNativeModule(this._admob).interstitialLoadAd(
this.adUnit,
adRequest
);
}
/**
* Return a local instance of isLoaded
* @returns {boolean}
*/
isLoaded() {
return this.loaded;
}
/**
* Show the advert - will only show if loaded
* @returns {*}
*/
show() {
if (this.loaded) {
getNativeModule(this._admob).interstitialShowAd(this.adUnit);
}
}
/**
* Listen to an Ad event
* @param eventType
* @param listenerCb
* @returns {null}
*/
on(eventType, listenerCb) {
if (!statics.EventTypes[eventType]) {
console.warn(
`Invalid event type provided, must be one of: ${Object.keys(
statics.EventTypes
).join(', ')}`
);
return null;
}
const sub = SharedEventEmitter.addListener(
`interstitial:${this.adUnit}:${eventType}`,
listenerCb
);
subscriptions.push(sub);
return sub;
}
}

View File

@ -0,0 +1,14 @@
import React from 'react';
import AdMobComponent from './AdMobComponent';
function NativeExpress({ ...props }) {
return <AdMobComponent {...props} class="RNFirebaseAdMobNativeExpress" />;
}
NativeExpress.propTypes = AdMobComponent.propTypes;
NativeExpress.defaultProps = {
size: 'SMART_BANNER',
};
export default NativeExpress;

View File

@ -0,0 +1,118 @@
import { statics } from './';
import AdRequest from './AdRequest';
import { SharedEventEmitter } from '../../utils/events';
import { getNativeModule } from '../../utils/native';
import { nativeToJSError } from '../../utils';
import type AdMob from './';
let subscriptions = [];
export default class RewardedVideo {
_admob: AdMob;
constructor(admob: AdMob, adUnit: string) {
for (let i = 0, len = subscriptions.length; i < len; i++) {
subscriptions[i].remove();
}
subscriptions = [];
this._admob = admob;
this.adUnit = adUnit;
this.loaded = false;
SharedEventEmitter.removeAllListeners(`rewarded_video_${adUnit}`);
SharedEventEmitter.addListener(
`rewarded_video_${adUnit}`,
this._onRewardedVideoEvent
);
}
/**
* Handle a JS emit event
* @param event
* @private
*/
_onRewardedVideoEvent = event => {
const eventType = `rewarded_video:${this.adUnit}:${event.type}`;
let emitData = Object.assign({}, event);
switch (event.type) {
case 'onAdLoaded':
this.loaded = true;
break;
case 'onAdFailedToLoad':
emitData = nativeToJSError(event.payload.code, event.payload.message);
emitData.type = event.type;
break;
default:
}
SharedEventEmitter.emit(eventType, emitData);
SharedEventEmitter.emit(`rewarded_video:${this.adUnit}:*`, emitData);
};
/**
* Load an ad with an instance of AdRequest
* @param request
* @returns {*}
*/
loadAd(request?: AdRequest) {
let adRequest = request;
if (!adRequest || !Object.keys(adRequest)) {
adRequest = new AdRequest().addTestDevice().build();
}
return getNativeModule(this._admob).rewardedVideoLoadAd(
this.adUnit,
adRequest
);
}
/**
* Return a local instance of isLoaded
* @returns {boolean}
*/
isLoaded() {
return this.loaded;
}
/**
* Show the advert - will only show if loaded
* @returns {*}
*/
show() {
if (this.loaded) {
getNativeModule(this._admob).rewardedVideoShowAd(this.adUnit);
}
}
/**
* Listen to an Ad event
* @param eventType
* @param listenerCb
* @returns {null}
*/
on(eventType, listenerCb) {
const types = {
...statics.EventTypes,
...statics.RewardedVideoEventTypes,
};
if (!types[eventType]) {
console.warn(
`Invalid event type provided, must be one of: ${Object.keys(types).join(
', '
)}`
);
return null;
}
const sub = SharedEventEmitter.addListener(
`rewarded_video:${this.adUnit}:${eventType}`,
listenerCb
);
subscriptions.push(sub);
return sub;
}
}

View File

@ -0,0 +1,16 @@
export default class VideoOptions {
constructor() {
this._props = {
startMuted: true,
};
}
build() {
return this._props;
}
setStartMuted(muted: boolean = true) {
this._props.startMuted = muted;
return this;
}
}

View File

@ -0,0 +1,121 @@
/**
* @flow
* AdMob representation wrapper
*/
import { SharedEventEmitter } from '../../utils/events';
import { getLogger } from '../../utils/log';
import { getNativeModule } from '../../utils/native';
import ModuleBase from '../../utils/ModuleBase';
import Interstitial from './Interstitial';
import RewardedVideo from './RewardedVideo';
import AdRequest from './AdRequest';
import VideoOptions from './VideoOptions';
import Banner from './Banner';
import NativeExpress from './NativeExpress';
import EventTypes, {
NativeExpressEventTypes,
RewardedVideoEventTypes,
} from './EventTypes';
import type App from '../core/app';
type NativeEvent = {
adUnit: string,
payload: Object,
type: string,
};
const NATIVE_EVENTS = ['interstitial_event', 'rewarded_video_event'];
export const MODULE_NAME = 'RNFirebaseAdMob';
export const NAMESPACE = 'admob';
export default class AdMob extends ModuleBase {
_appId: ?string;
_initialized: boolean;
constructor(app: App) {
super(app, {
events: NATIVE_EVENTS,
moduleName: MODULE_NAME,
multiApp: false,
hasShards: false,
namespace: NAMESPACE,
});
this._initialized = false;
this._appId = null;
SharedEventEmitter.addListener(
'interstitial_event',
this._onInterstitialEvent.bind(this)
);
SharedEventEmitter.addListener(
'rewarded_video_event',
this._onRewardedVideoEvent.bind(this)
);
}
_onInterstitialEvent(event: NativeEvent): void {
const { adUnit } = event;
const jsEventType = `interstitial_${adUnit}`;
if (SharedEventEmitter.listeners(jsEventType).length === 0) {
// TODO
}
SharedEventEmitter.emit(jsEventType, event);
}
_onRewardedVideoEvent(event: NativeEvent): void {
const { adUnit } = event;
const jsEventType = `rewarded_video_${adUnit}`;
if (SharedEventEmitter.listeners(jsEventType).length === 0) {
// TODO
}
SharedEventEmitter.emit(jsEventType, event);
}
initialize(appId: string): void {
if (this._initialized) {
getLogger(this).warn('AdMob has already been initialized!');
} else {
this._initialized = true;
this._appId = appId;
getNativeModule(this).initialize(appId);
}
}
openDebugMenu(): void {
if (!this._initialized) {
getLogger(this).warn(
'AdMob needs to be initialized before opening the dev menu!'
);
} else {
getLogger(this).info('Opening debug menu');
getNativeModule(this).openDebugMenu(this._appId);
}
}
interstitial(adUnit: string): Interstitial {
return new Interstitial(this, adUnit);
}
rewarded(adUnit: string): RewardedVideo {
return new RewardedVideo(this, adUnit);
}
}
export const statics = {
Banner,
NativeExpress,
AdRequest,
VideoOptions,
EventTypes,
RewardedVideoEventTypes,
NativeExpressEventTypes,
};

View File

@ -0,0 +1,167 @@
/**
* @flow
* Analytics representation wrapper
*/
import ModuleBase from '../../utils/ModuleBase';
import { getNativeModule } from '../../utils/native';
import { isString, isObject } from '../../utils';
import type App from '../core/app';
const AlphaNumericUnderscore = /^[a-zA-Z0-9_]+$/;
const ReservedEventNames = [
'app_clear_data',
'app_uninstall',
'app_update',
'error',
'first_open',
'in_app_purchase',
'notification_dismiss',
'notification_foreground',
'notification_open',
'notification_receive',
'os_update',
'session_start',
'user_engagement',
];
export const MODULE_NAME = 'RNFirebaseAnalytics';
export const NAMESPACE = 'analytics';
export default class Analytics extends ModuleBase {
constructor(app: App) {
super(app, {
moduleName: MODULE_NAME,
multiApp: false,
hasShards: false,
namespace: NAMESPACE,
});
}
/**
* Logs an app event.
* @param {string} name
* @param params
* @return {Promise}
*/
logEvent(name: string, params: Object = {}): void {
if (!isString(name)) {
throw new Error(
`analytics.logEvent(): First argument 'name' is required and must be a string value.`
);
}
if (typeof params !== 'undefined' && !isObject(params)) {
throw new Error(
`analytics.logEvent(): Second optional argument 'params' must be an object if provided.`
);
}
// check name is not a reserved event name
if (ReservedEventNames.includes(name)) {
throw new Error(
`analytics.logEvent(): event name '${name}' is a reserved event name and can not be used.`
);
}
// name format validation
if (!AlphaNumericUnderscore.test(name)) {
throw new Error(
`analytics.logEvent(): Event name '${name}' is invalid. Names should contain 1 to 32 alphanumeric characters or underscores.`
);
}
// maximum number of allowed params check
if (params && Object.keys(params).length > 25)
throw new Error(
'analytics.logEvent(): Maximum number of parameters exceeded (25).'
);
// Parameter names can be up to 24 characters long and must start with an alphabetic character
// and contain only alphanumeric characters and underscores. Only String, long and double param
// types are supported. String parameter values can be up to 36 characters long. The "firebase_"
// prefix is reserved and should not be used for parameter names.
getNativeModule(this).logEvent(name, params);
}
/**
* Sets whether analytics collection is enabled for this app on this device.
* @param enabled
*/
setAnalyticsCollectionEnabled(enabled: boolean): void {
getNativeModule(this).setAnalyticsCollectionEnabled(enabled);
}
/**
* Sets the current screen name, which specifies the current visual context in your app.
* @param screenName
* @param screenClassOverride
*/
setCurrentScreen(screenName: string, screenClassOverride: string): void {
getNativeModule(this).setCurrentScreen(screenName, screenClassOverride);
}
/**
* Sets the minimum engagement time required before starting a session. The default value is 10000 (10 seconds).
* @param milliseconds
*/
setMinimumSessionDuration(milliseconds: number = 10000): void {
getNativeModule(this).setMinimumSessionDuration(milliseconds);
}
/**
* Sets the duration of inactivity that terminates the current session. The default value is 1800000 (30 minutes).
* @param milliseconds
*/
setSessionTimeoutDuration(milliseconds: number = 1800000): void {
getNativeModule(this).setSessionTimeoutDuration(milliseconds);
}
/**
* Sets the user ID property.
* @param id
*/
setUserId(id: string | null): void {
if (id !== null && !isString(id)) {
throw new Error(
'analytics.setUserId(): The supplied userId must be a string value or null.'
);
}
getNativeModule(this).setUserId(id);
}
/**
* Sets a user property to a given value.
* @param name
* @param value
*/
setUserProperty(name: string, value: string | null): void {
if (value !== null && !isString(value)) {
throw new Error(
'analytics.setUserProperty(): The supplied property must be a string value or null.'
);
}
getNativeModule(this).setUserProperty(name, value);
}
/**
* Sets multiple user properties to the supplied values.
* @RNFirebaseSpecific
* @param object
*/
setUserProperties(object: Object): void {
Object.keys(object).forEach(property => {
const value = object[property];
if (value !== null && !isString(value)) {
throw new Error(
`analytics.setUserProperties(): The property with name '${property}' must be a string value or null.`
);
}
getNativeModule(this).setUserProperty(property, object[property]);
});
}
}
export const statics = {};

View File

@ -0,0 +1,37 @@
/**
* @flow
* ConfirmationResult representation wrapper
*/
import { getNativeModule } from '../../utils/native';
import type Auth from './';
import type User from './User';
export default class ConfirmationResult {
_auth: Auth;
_verificationId: string;
/**
*
* @param auth
* @param verificationId The phone number authentication operation's verification ID.
*/
constructor(auth: Auth, verificationId: string) {
this._auth = auth;
this._verificationId = verificationId;
}
/**
*
* @param verificationCode
* @return {*}
*/
confirm(verificationCode: string): Promise<User> {
return getNativeModule(this._auth)
._confirmVerificationCode(verificationCode)
.then(user => this._auth._setUser(user));
}
get verificationId(): string | null {
return this._verificationId;
}
}

View File

@ -0,0 +1,347 @@
// @flow
import INTERNALS from '../../utils/internals';
import { SharedEventEmitter } from '../../utils/events';
import {
generatePushID,
isFunction,
isAndroid,
isIOS,
isString,
nativeToJSError,
} from '../../utils';
import { getNativeModule } from '../../utils/native';
import type Auth from './';
type PhoneAuthSnapshot = {
state: 'sent' | 'timeout' | 'verified' | 'error',
verificationId: string,
code: string | null,
error: Error | null,
};
type PhoneAuthError = {
code: string | null,
verificationId: string,
message: string | null,
stack: string | null,
};
export default class PhoneAuthListener {
_auth: Auth;
_timeout: number;
_publicEvents: Object;
_internalEvents: Object;
_reject: Function | null;
_resolve: Function | null;
_credential: Object | null;
_promise: Promise<*> | null;
_phoneAuthRequestKey: string;
/**
*
* @param auth
* @param phoneNumber
* @param timeout
*/
constructor(auth: Auth, phoneNumber: string, timeout?: number) {
this._auth = auth;
this._reject = null;
this._resolve = null;
this._promise = null;
this._credential = null;
this._timeout = timeout || 20; // 20 secs
this._phoneAuthRequestKey = generatePushID();
// internal events
this._internalEvents = {
codeSent: `phone:auth:${this._phoneAuthRequestKey}:onCodeSent`,
verificationFailed: `phone:auth:${
this._phoneAuthRequestKey
}:onVerificationFailed`,
verificationComplete: `phone:auth:${
this._phoneAuthRequestKey
}:onVerificationComplete`,
codeAutoRetrievalTimeout: `phone:auth:${
this._phoneAuthRequestKey
}:onCodeAutoRetrievalTimeout`,
};
// user observer events
this._publicEvents = {
// error cb
error: `phone:auth:${this._phoneAuthRequestKey}:error`,
// observer
event: `phone:auth:${this._phoneAuthRequestKey}:event`,
// success cb
success: `phone:auth:${this._phoneAuthRequestKey}:success`,
};
// setup internal event listeners
this._subscribeToEvents();
// start verification flow natively
if (isAndroid) {
getNativeModule(this._auth).verifyPhoneNumber(
phoneNumber,
this._phoneAuthRequestKey,
this._timeout
);
}
if (isIOS) {
getNativeModule(this._auth).verifyPhoneNumber(
phoneNumber,
this._phoneAuthRequestKey
);
}
}
/**
* Subscribes to all EE events on this._internalEvents
* @private
*/
_subscribeToEvents() {
const events = Object.keys(this._internalEvents);
for (let i = 0, len = events.length; i < len; i++) {
const type = events[i];
SharedEventEmitter.once(
this._internalEvents[type],
// $FlowExpectedError: Flow doesn't support indexable signatures on classes: https://github.com/facebook/flow/issues/1323
this[`_${type}Handler`].bind(this)
);
}
}
/**
* Subscribe a users listener cb to the snapshot events.
* @param observer
* @private
*/
_addUserObserver(observer) {
SharedEventEmitter.addListener(this._publicEvents.event, observer);
}
/**
* Send a snapshot event to users event observer.
* @param snapshot PhoneAuthSnapshot
* @private
*/
_emitToObservers(snapshot: PhoneAuthSnapshot) {
SharedEventEmitter.emit(this._publicEvents.event, snapshot);
}
/**
* Send a error snapshot event to any subscribed errorCb's
* @param snapshot
* @private
*/
_emitToErrorCb(snapshot) {
const { error } = snapshot;
if (this._reject) this._reject(error);
SharedEventEmitter.emit(this._publicEvents.error, error);
}
/**
* Send a success snapshot event to any subscribed completeCb's
* @param snapshot
* @private
*/
_emitToSuccessCb(snapshot) {
if (this._resolve) this._resolve(snapshot);
SharedEventEmitter.emit(this._publicEvents.success, snapshot);
}
/**
* Removes all listeners for this phone auth instance
* @private
*/
_removeAllListeners() {
setTimeout(() => {
// move to next event loop - not sure if needed
// internal listeners
Object.values(this._internalEvents).forEach(event => {
SharedEventEmitter.removeAllListeners(event);
});
// user observer listeners
Object.values(this._publicEvents).forEach(publicEvent => {
SharedEventEmitter.removeAllListeners(publicEvent);
});
}, 0);
}
/**
* Create a new internal deferred promise, if not already created
* @private
*/
_promiseDeferred() {
if (!this._promise) {
this._promise = new Promise((resolve, reject) => {
this._resolve = result => {
this._resolve = null;
return resolve(result);
};
this._reject = possibleError => {
this._reject = null;
return reject(possibleError);
};
});
}
}
/* --------------------------
--- INTERNAL EVENT HANDLERS
---------------------------- */
/**
* Internal code sent event handler
* @private
* @param credential
*/
_codeSentHandler(credential) {
const snapshot: PhoneAuthSnapshot = {
verificationId: credential.verificationId,
code: null,
error: null,
state: 'sent',
};
this._emitToObservers(snapshot);
if (isIOS) {
this._emitToSuccessCb(snapshot);
}
if (isAndroid) {
// android can auto retrieve so we don't emit to successCb immediately,
// if auto retrieve times out then that will emit to successCb
}
}
/**
* Internal code auto retrieve timeout event handler
* @private
* @param credential
*/
_codeAutoRetrievalTimeoutHandler(credential) {
const snapshot: PhoneAuthSnapshot = {
verificationId: credential.verificationId,
code: null,
error: null,
state: 'timeout',
};
this._emitToObservers(snapshot);
this._emitToSuccessCb(snapshot);
}
/**
* Internal verification complete event handler
* @param credential
* @private
*/
_verificationCompleteHandler(credential) {
const snapshot: PhoneAuthSnapshot = {
verificationId: credential.verificationId,
code: credential.code || null,
error: null,
state: 'verified',
};
this._emitToObservers(snapshot);
this._emitToSuccessCb(snapshot);
this._removeAllListeners();
}
/**
* Internal verification failed event handler
* @param state
* @private
*/
_verificationFailedHandler(state) {
const snapshot: PhoneAuthSnapshot = {
verificationId: state.verificationId,
code: null,
error: null,
state: 'error',
};
const { code, message, nativeErrorMessage } = state.error;
snapshot.error = nativeToJSError(code, message, { nativeErrorMessage });
this._emitToObservers(snapshot);
this._emitToErrorCb(snapshot);
this._removeAllListeners();
}
/* -------------
-- PUBLIC API
--------------*/
on(
event: string,
observer: () => PhoneAuthSnapshot,
errorCb?: () => PhoneAuthError,
successCb?: () => PhoneAuthSnapshot
): this {
if (!isString(event)) {
throw new Error(
INTERNALS.STRINGS.ERROR_MISSING_ARG_NAMED('event', 'string', 'on')
);
}
if (event !== 'state_changed') {
throw new Error(
INTERNALS.STRINGS.ERROR_ARG_INVALID_VALUE(
'event',
'state_changed',
event
)
);
}
if (!isFunction(observer)) {
throw new Error(
INTERNALS.STRINGS.ERROR_MISSING_ARG_NAMED('observer', 'function', 'on')
);
}
this._addUserObserver(observer);
if (isFunction(errorCb)) {
SharedEventEmitter.once(this._publicEvents.error, errorCb);
}
if (isFunction(successCb)) {
SharedEventEmitter.once(this._publicEvents.success, successCb);
}
return this;
}
/**
* Promise .then proxy
* @param fn
*/
then(fn: () => PhoneAuthSnapshot) {
this._promiseDeferred();
// $FlowFixMe: Unsure how to annotate `bind` here
if (this._promise) return this._promise.then.bind(this._promise)(fn);
return undefined; // will never get here - just to keep flow happy
}
/**
* Promise .catch proxy
* @param fn
*/
catch(fn: () => Error) {
this._promiseDeferred();
// $FlowFixMe: Unsure how to annotate `bind` here
if (this._promise) return this._promise.catch.bind(this._promise)(fn);
return undefined; // will never get here - just to keep flow happy
}
}

View File

@ -0,0 +1,334 @@
/**
* @flow
* User representation wrapper
*/
import INTERNALS from '../../utils/internals';
import { getNativeModule } from '../../utils/native';
import type Auth from './';
import type {
ActionCodeSettings,
AuthCredential,
NativeUser,
UserCredential,
UserInfo,
UserMetadata,
} from './types';
type UpdateProfile = {
displayName?: string,
photoURL?: string,
};
export default class User {
_auth: Auth;
_user: NativeUser;
/**
*
* @param auth Instance of Authentication class
* @param user user result object from native
*/
constructor(auth: Auth, user: NativeUser) {
this._auth = auth;
this._user = user;
}
/**
* PROPERTIES
*/
get displayName(): ?string {
return this._user.displayName || null;
}
get email(): ?string {
return this._user.email || null;
}
get emailVerified(): boolean {
return this._user.emailVerified || false;
}
get isAnonymous(): boolean {
return this._user.isAnonymous || false;
}
get metadata(): UserMetadata {
return this._user.metadata;
}
get phoneNumber(): ?string {
return this._user.phoneNumber || null;
}
get photoURL(): ?string {
return this._user.photoURL || null;
}
get providerData(): Array<UserInfo> {
return this._user.providerData;
}
get providerId(): string {
return this._user.providerId;
}
get uid(): string {
return this._user.uid;
}
/**
* METHODS
*/
/**
* Delete the current user
* @return {Promise}
*/
delete(): Promise<void> {
return getNativeModule(this._auth)
.delete()
.then(() => {
this._auth._setUser();
});
}
/**
* get the token of current user
* @return {Promise}
*/
getIdToken(forceRefresh: boolean = false): Promise<string> {
return getNativeModule(this._auth).getToken(forceRefresh);
}
/**
* get the token of current user
* @deprecated Deprecated getToken in favor of getIdToken.
* @return {Promise}
*/
getToken(forceRefresh: boolean = false): Promise<Object> {
console.warn(
'Deprecated firebase.User.prototype.getToken in favor of firebase.User.prototype.getIdToken.'
);
return getNativeModule(this._auth).getToken(forceRefresh);
}
/**
* @deprecated Deprecated linkWithCredential in favor of linkAndRetrieveDataWithCredential.
* @param credential
*/
linkWithCredential(credential: AuthCredential): Promise<User> {
console.warn(
'Deprecated firebase.User.prototype.linkWithCredential in favor of firebase.User.prototype.linkAndRetrieveDataWithCredential.'
);
return getNativeModule(this._auth)
.linkWithCredential(
credential.providerId,
credential.token,
credential.secret
)
.then(user => this._auth._setUser(user));
}
/**
*
* @param credential
*/
linkAndRetrieveDataWithCredential(
credential: AuthCredential
): Promise<UserCredential> {
return getNativeModule(this._auth)
.linkAndRetrieveDataWithCredential(
credential.providerId,
credential.token,
credential.secret
)
.then(userCredential => this._auth._setUserCredential(userCredential));
}
/**
* Re-authenticate a user with a third-party authentication provider
* @return {Promise} A promise resolved upon completion
*/
reauthenticateWithCredential(credential: AuthCredential): Promise<void> {
console.warn(
'Deprecated firebase.User.prototype.reauthenticateWithCredential in favor of firebase.User.prototype.reauthenticateAndRetrieveDataWithCredential.'
);
return getNativeModule(this._auth)
.reauthenticateWithCredential(
credential.providerId,
credential.token,
credential.secret
)
.then(user => {
this._auth._setUser(user);
});
}
/**
* Re-authenticate a user with a third-party authentication provider
* @return {Promise} A promise resolved upon completion
*/
reauthenticateAndRetrieveDataWithCredential(
credential: AuthCredential
): Promise<UserCredential> {
return getNativeModule(this._auth)
.reauthenticateAndRetrieveDataWithCredential(
credential.providerId,
credential.token,
credential.secret
)
.then(userCredential => this._auth._setUserCredential(userCredential));
}
/**
* Reload the current user
* @return {Promise}
*/
reload(): Promise<void> {
return getNativeModule(this._auth)
.reload()
.then(user => {
this._auth._setUser(user);
});
}
/**
* Send verification email to current user.
*/
sendEmailVerification(
actionCodeSettings?: ActionCodeSettings
): Promise<void> {
return getNativeModule(this._auth)
.sendEmailVerification(actionCodeSettings)
.then(user => {
this._auth._setUser(user);
});
}
toJSON(): Object {
return Object.assign({}, this._user);
}
/**
*
* @param providerId
* @return {Promise.<TResult>|*}
*/
unlink(providerId: string): Promise<User> {
return getNativeModule(this._auth)
.unlink(providerId)
.then(user => this._auth._setUser(user));
}
/**
* Update the current user's email
*
* @param {string} email The user's _new_ email
* @return {Promise} A promise resolved upon completion
*/
updateEmail(email: string): Promise<void> {
return getNativeModule(this._auth)
.updateEmail(email)
.then(user => {
this._auth._setUser(user);
});
}
/**
* Update the current user's password
* @param {string} password the new password
* @return {Promise}
*/
updatePassword(password: string): Promise<void> {
return getNativeModule(this._auth)
.updatePassword(password)
.then(user => {
this._auth._setUser(user);
});
}
/**
* Update the current user's profile
* @param {Object} updates An object containing the keys listed [here](https://firebase.google.com/docs/auth/ios/manage-users#update_a_users_profile)
* @return {Promise}
*/
updateProfile(updates: UpdateProfile = {}): Promise<void> {
return getNativeModule(this._auth)
.updateProfile(updates)
.then(user => {
this._auth._setUser(user);
});
}
/**
* KNOWN UNSUPPORTED METHODS
*/
linkWithPhoneNumber() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD(
'User',
'linkWithPhoneNumber'
)
);
}
linkWithPopup() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('User', 'linkWithPopup')
);
}
linkWithRedirect() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD(
'User',
'linkWithRedirect'
)
);
}
reauthenticateWithPhoneNumber() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD(
'User',
'reauthenticateWithPhoneNumber'
)
);
}
reauthenticateWithPopup() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD(
'User',
'reauthenticateWithPopup'
)
);
}
reauthenticateWithRedirect() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD(
'User',
'reauthenticateWithRedirect'
)
);
}
updatePhoneNumber() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD(
'User',
'updatePhoneNumber'
)
);
}
get refreshToken(): string {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_PROPERTY('User', 'refreshToken')
);
}
}

View File

@ -0,0 +1,526 @@
/**
* @flow
* Auth representation wrapper
*/
import User from './User';
import ModuleBase from '../../utils/ModuleBase';
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import { getLogger } from '../../utils/log';
import { getNativeModule } from '../../utils/native';
import INTERNALS from '../../utils/internals';
import ConfirmationResult from './phone/ConfirmationResult';
import PhoneAuthListener from './phone/PhoneAuthListener';
// providers
import EmailAuthProvider from './providers/EmailAuthProvider';
import PhoneAuthProvider from './providers/PhoneAuthProvider';
import GoogleAuthProvider from './providers/GoogleAuthProvider';
import GithubAuthProvider from './providers/GithubAuthProvider';
import OAuthProvider from './providers/OAuthProvider';
import TwitterAuthProvider from './providers/TwitterAuthProvider';
import FacebookAuthProvider from './providers/FacebookAuthProvider';
import type {
ActionCodeInfo,
ActionCodeSettings,
AuthCredential,
NativeUser,
NativeUserCredential,
UserCredential,
} from './types';
import type App from '../core/app';
type AuthState = {
user?: NativeUser,
};
const NATIVE_EVENTS = [
'auth_state_changed',
'auth_id_token_changed',
'phone_auth_state_changed',
];
export const MODULE_NAME = 'RNFirebaseAuth';
export const NAMESPACE = 'auth';
export default class Auth extends ModuleBase {
_authResult: boolean;
_languageCode: string;
_user: User | null;
constructor(app: App) {
super(app, {
events: NATIVE_EVENTS,
moduleName: MODULE_NAME,
multiApp: true,
hasShards: false,
namespace: NAMESPACE,
});
this._user = null;
this._authResult = false;
this._languageCode =
getNativeModule(this).APP_LANGUAGE[app._name] ||
getNativeModule(this).APP_LANGUAGE['[DEFAULT]'];
SharedEventEmitter.addListener(
// sub to internal native event - this fans out to
// public event name: onAuthStateChanged
getAppEventName(this, 'auth_state_changed'),
(state: AuthState) => {
this._setUser(state.user);
SharedEventEmitter.emit(
getAppEventName(this, 'onAuthStateChanged'),
this._user
);
}
);
SharedEventEmitter.addListener(
// sub to internal native event - this fans out to
// public events based on event.type
getAppEventName(this, 'phone_auth_state_changed'),
(event: Object) => {
const eventKey = `phone:auth:${event.requestKey}:${event.type}`;
SharedEventEmitter.emit(eventKey, event.state);
}
);
SharedEventEmitter.addListener(
// sub to internal native event - this fans out to
// public event name: onIdTokenChanged
getAppEventName(this, 'auth_id_token_changed'),
(auth: AuthState) => {
this._setUser(auth.user);
SharedEventEmitter.emit(
getAppEventName(this, 'onIdTokenChanged'),
this._user
);
}
);
getNativeModule(this).addAuthStateListener();
getNativeModule(this).addIdTokenListener();
}
_setUser(user: ?NativeUser): ?User {
this._authResult = true;
this._user = user ? new User(this, user) : null;
SharedEventEmitter.emit(getAppEventName(this, 'onUserChanged'), this._user);
return this._user;
}
_setUserCredential(userCredential: NativeUserCredential): UserCredential {
const user = new User(this, userCredential.user);
this._authResult = true;
this._user = user;
SharedEventEmitter.emit(getAppEventName(this, 'onUserChanged'), this._user);
return {
additionalUserInfo: userCredential.additionalUserInfo,
user,
};
}
/*
* WEB API
*/
/**
* Listen for auth changes.
* @param listener
*/
onAuthStateChanged(listener: Function) {
getLogger(this).info('Creating onAuthStateChanged listener');
SharedEventEmitter.addListener(
getAppEventName(this, 'onAuthStateChanged'),
listener
);
if (this._authResult) listener(this._user || null);
return () => {
getLogger(this).info('Removing onAuthStateChanged listener');
SharedEventEmitter.removeListener(
getAppEventName(this, 'onAuthStateChanged'),
listener
);
};
}
/**
* Listen for id token changes.
* @param listener
*/
onIdTokenChanged(listener: Function) {
getLogger(this).info('Creating onIdTokenChanged listener');
SharedEventEmitter.addListener(
getAppEventName(this, 'onIdTokenChanged'),
listener
);
if (this._authResult) listener(this._user || null);
return () => {
getLogger(this).info('Removing onIdTokenChanged listener');
SharedEventEmitter.removeListener(
getAppEventName(this, 'onIdTokenChanged'),
listener
);
};
}
/**
* Listen for user changes.
* @param listener
*/
onUserChanged(listener: Function) {
getLogger(this).info('Creating onUserChanged listener');
SharedEventEmitter.addListener(
getAppEventName(this, 'onUserChanged'),
listener
);
if (this._authResult) listener(this._user || null);
return () => {
getLogger(this).info('Removing onUserChanged listener');
SharedEventEmitter.removeListener(
getAppEventName(this, 'onUserChanged'),
listener
);
};
}
/**
* Sign the current user out
* @return {Promise}
*/
signOut(): Promise<void> {
return getNativeModule(this)
.signOut()
.then(() => {
this._setUser();
});
}
/**
* Sign a user in anonymously
* @deprecated Deprecated signInAnonymously in favor of signInAnonymouslyAndRetrieveData.
* @return {Promise} A promise resolved upon completion
*/
signInAnonymously(): Promise<User> {
console.warn(
'Deprecated firebase.User.prototype.signInAnonymously in favor of firebase.User.prototype.signInAnonymouslyAndRetrieveData.'
);
return getNativeModule(this)
.signInAnonymously()
.then(user => this._setUser(user));
}
/**
* Sign a user in anonymously
* @return {Promise} A promise resolved upon completion
*/
signInAnonymouslyAndRetrieveData(): Promise<UserCredential> {
return getNativeModule(this)
.signInAnonymouslyAndRetrieveData()
.then(userCredential => this._setUserCredential(userCredential));
}
/**
* Create a user with the email/password functionality
* @deprecated Deprecated createUserWithEmailAndPassword in favor of createUserAndRetrieveDataWithEmailAndPassword.
* @param {string} email The user's email
* @param {string} password The user's password
* @return {Promise} A promise indicating the completion
*/
createUserWithEmailAndPassword(
email: string,
password: string
): Promise<User> {
console.warn(
'Deprecated firebase.User.prototype.createUserWithEmailAndPassword in favor of firebase.User.prototype.createUserAndRetrieveDataWithEmailAndPassword.'
);
return getNativeModule(this)
.createUserWithEmailAndPassword(email, password)
.then(user => this._setUser(user));
}
/**
* Create a user with the email/password functionality
* @param {string} email The user's email
* @param {string} password The user's password
* @return {Promise} A promise indicating the completion
*/
createUserAndRetrieveDataWithEmailAndPassword(
email: string,
password: string
): Promise<UserCredential> {
return getNativeModule(this)
.createUserAndRetrieveDataWithEmailAndPassword(email, password)
.then(userCredential => this._setUserCredential(userCredential));
}
/**
* Sign a user in with email/password
* @deprecated Deprecated signInWithEmailAndPassword in favor of signInAndRetrieveDataWithEmailAndPassword
* @param {string} email The user's email
* @param {string} password The user's password
* @return {Promise} A promise that is resolved upon completion
*/
signInWithEmailAndPassword(email: string, password: string): Promise<User> {
console.warn(
'Deprecated firebase.User.prototype.signInWithEmailAndPassword in favor of firebase.User.prototype.signInAndRetrieveDataWithEmailAndPassword.'
);
return getNativeModule(this)
.signInWithEmailAndPassword(email, password)
.then(user => this._setUser(user));
}
/**
* Sign a user in with email/password
* @param {string} email The user's email
* @param {string} password The user's password
* @return {Promise} A promise that is resolved upon completion
*/
signInAndRetrieveDataWithEmailAndPassword(
email: string,
password: string
): Promise<UserCredential> {
return getNativeModule(this)
.signInAndRetrieveDataWithEmailAndPassword(email, password)
.then(userCredential => this._setUserCredential(userCredential));
}
/**
* Sign the user in with a custom auth token
* @deprecated Deprecated signInWithCustomToken in favor of signInAndRetrieveDataWithCustomToken
* @param {string} customToken A self-signed custom auth token.
* @return {Promise} A promise resolved upon completion
*/
signInWithCustomToken(customToken: string): Promise<User> {
console.warn(
'Deprecated firebase.User.prototype.signInWithCustomToken in favor of firebase.User.prototype.signInAndRetrieveDataWithCustomToken.'
);
return getNativeModule(this)
.signInWithCustomToken(customToken)
.then(user => this._setUser(user));
}
/**
* Sign the user in with a custom auth token
* @param {string} customToken A self-signed custom auth token.
* @return {Promise} A promise resolved upon completion
*/
signInAndRetrieveDataWithCustomToken(
customToken: string
): Promise<UserCredential> {
return getNativeModule(this)
.signInAndRetrieveDataWithCustomToken(customToken)
.then(userCredential => this._setUserCredential(userCredential));
}
/**
* Sign the user in with a third-party authentication provider
* @deprecated Deprecated signInWithCredential in favor of signInAndRetrieveDataWithCredential.
* @return {Promise} A promise resolved upon completion
*/
signInWithCredential(credential: AuthCredential): Promise<User> {
console.warn(
'Deprecated firebase.User.prototype.signInWithCredential in favor of firebase.User.prototype.signInAndRetrieveDataWithCredential.'
);
return getNativeModule(this)
.signInWithCredential(
credential.providerId,
credential.token,
credential.secret
)
.then(user => this._setUser(user));
}
/**
* Sign the user in with a third-party authentication provider
* @return {Promise} A promise resolved upon completion
*/
signInAndRetrieveDataWithCredential(
credential: AuthCredential
): Promise<UserCredential> {
return getNativeModule(this)
.signInAndRetrieveDataWithCredential(
credential.providerId,
credential.token,
credential.secret
)
.then(userCredential => this._setUserCredential(userCredential));
}
/**
* Asynchronously signs in using a phone number.
*
*/
signInWithPhoneNumber(phoneNumber: string): Promise<ConfirmationResult> {
return getNativeModule(this)
.signInWithPhoneNumber(phoneNumber)
.then(result => new ConfirmationResult(this, result.verificationId));
}
/**
* Returns a PhoneAuthListener to listen to phone verification events,
* on the final completion event a PhoneAuthCredential can be generated for
* authentication purposes.
*
* @param phoneNumber
* @param autoVerifyTimeout Android Only
* @returns {PhoneAuthListener}
*/
verifyPhoneNumber(
phoneNumber: string,
autoVerifyTimeout?: number
): PhoneAuthListener {
return new PhoneAuthListener(this, phoneNumber, autoVerifyTimeout);
}
/**
* Send reset password instructions via email
* @param {string} email The email to send password reset instructions
*/
sendPasswordResetEmail(
email: string,
actionCodeSettings?: ActionCodeSettings
): Promise<void> {
return getNativeModule(this).sendPasswordResetEmail(
email,
actionCodeSettings
);
}
/**
* Completes the password reset process, given a confirmation code and new password.
*
* @link https://firebase.google.com/docs/reference/js/firebase.auth.Auth#confirmPasswordReset
* @param code
* @param newPassword
* @return {Promise.<Null>}
*/
confirmPasswordReset(code: string, newPassword: string): Promise<void> {
return getNativeModule(this).confirmPasswordReset(code, newPassword);
}
/**
* Applies a verification code sent to the user by email or other out-of-band mechanism.
*
* @link https://firebase.google.com/docs/reference/js/firebase.auth.Auth#applyActionCode
* @param code
* @return {Promise.<Null>}
*/
applyActionCode(code: string): Promise<void> {
return getNativeModule(this).applyActionCode(code);
}
/**
* Checks a verification code sent to the user by email or other out-of-band mechanism.
*
* @link https://firebase.google.com/docs/reference/js/firebase.auth.Auth#checkActionCode
* @param code
* @return {Promise.<any>|Promise<ActionCodeInfo>}
*/
checkActionCode(code: string): Promise<ActionCodeInfo> {
return getNativeModule(this).checkActionCode(code);
}
/**
* Returns a list of authentication providers that can be used to sign in a given user (identified by its main email address).
* @return {Promise}
*/
fetchProvidersForEmail(email: string): Promise<string[]> {
return getNativeModule(this).fetchProvidersForEmail(email);
}
verifyPasswordResetCode(code: string): Promise<string> {
return getNativeModule(this).verifyPasswordResetCode(code);
}
/**
* Sets the language for the auth module
* @param code
* @returns {*}
*/
set languageCode(code: string) {
this._languageCode = code;
getNativeModule(this).setLanguageCode(code);
}
/**
* Get the currently signed in user
* @return {Promise}
*/
get currentUser(): User | null {
return this._user;
}
get languageCode(): string {
return this._languageCode;
}
/**
* KNOWN UNSUPPORTED METHODS
*/
getRedirectResult() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'auth',
'getRedirectResult'
)
);
}
setPersistence() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'auth',
'setPersistence'
)
);
}
signInWithPopup() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'auth',
'signInWithPopup'
)
);
}
signInWithRedirect() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'auth',
'signInWithRedirect'
)
);
}
// firebase issue - https://github.com/invertase/react-native-firebase/pull/655#issuecomment-349904680
useDeviceLanguage() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'auth',
'useDeviceLanguage'
)
);
}
}
export const statics = {
EmailAuthProvider,
PhoneAuthProvider,
GoogleAuthProvider,
GithubAuthProvider,
TwitterAuthProvider,
FacebookAuthProvider,
OAuthProvider,
PhoneAuthState: {
CODE_SENT: 'sent',
AUTO_VERIFY_TIMEOUT: 'timeout',
AUTO_VERIFIED: 'verified',
ERROR: 'error',
},
};

View File

@ -0,0 +1,37 @@
/**
* @flow
* ConfirmationResult representation wrapper
*/
import { getNativeModule } from '../../../utils/native';
import type Auth from '../';
import type User from '../User';
export default class ConfirmationResult {
_auth: Auth;
_verificationId: string;
/**
*
* @param auth
* @param verificationId The phone number authentication operation's verification ID.
*/
constructor(auth: Auth, verificationId: string) {
this._auth = auth;
this._verificationId = verificationId;
}
/**
*
* @param verificationCode
* @return {*}
*/
confirm(verificationCode: string): Promise<User> {
return getNativeModule(this._auth)
._confirmVerificationCode(verificationCode)
.then(user => this._auth._setUser(user));
}
get verificationId(): string | null {
return this._verificationId;
}
}

View File

@ -0,0 +1,347 @@
// @flow
import INTERNALS from '../../../utils/internals';
import { SharedEventEmitter } from '../../../utils/events';
import {
generatePushID,
isFunction,
isAndroid,
isIOS,
isString,
nativeToJSError,
} from '../../../utils';
import { getNativeModule } from '../../../utils/native';
import type Auth from '../';
type PhoneAuthSnapshot = {
state: 'sent' | 'timeout' | 'verified' | 'error',
verificationId: string,
code: string | null,
error: Error | null,
};
type PhoneAuthError = {
code: string | null,
verificationId: string,
message: string | null,
stack: string | null,
};
export default class PhoneAuthListener {
_auth: Auth;
_timeout: number;
_publicEvents: Object;
_internalEvents: Object;
_reject: Function | null;
_resolve: Function | null;
_credential: Object | null;
_promise: Promise<*> | null;
_phoneAuthRequestKey: string;
/**
*
* @param auth
* @param phoneNumber
* @param timeout
*/
constructor(auth: Auth, phoneNumber: string, timeout?: number) {
this._auth = auth;
this._reject = null;
this._resolve = null;
this._promise = null;
this._credential = null;
this._timeout = timeout || 20; // 20 secs
this._phoneAuthRequestKey = generatePushID();
// internal events
this._internalEvents = {
codeSent: `phone:auth:${this._phoneAuthRequestKey}:onCodeSent`,
verificationFailed: `phone:auth:${
this._phoneAuthRequestKey
}:onVerificationFailed`,
verificationComplete: `phone:auth:${
this._phoneAuthRequestKey
}:onVerificationComplete`,
codeAutoRetrievalTimeout: `phone:auth:${
this._phoneAuthRequestKey
}:onCodeAutoRetrievalTimeout`,
};
// user observer events
this._publicEvents = {
// error cb
error: `phone:auth:${this._phoneAuthRequestKey}:error`,
// observer
event: `phone:auth:${this._phoneAuthRequestKey}:event`,
// success cb
success: `phone:auth:${this._phoneAuthRequestKey}:success`,
};
// setup internal event listeners
this._subscribeToEvents();
// start verification flow natively
if (isAndroid) {
getNativeModule(this._auth).verifyPhoneNumber(
phoneNumber,
this._phoneAuthRequestKey,
this._timeout
);
}
if (isIOS) {
getNativeModule(this._auth).verifyPhoneNumber(
phoneNumber,
this._phoneAuthRequestKey
);
}
}
/**
* Subscribes to all EE events on this._internalEvents
* @private
*/
_subscribeToEvents() {
const events = Object.keys(this._internalEvents);
for (let i = 0, len = events.length; i < len; i++) {
const type = events[i];
SharedEventEmitter.once(
this._internalEvents[type],
// $FlowExpectedError: Flow doesn't support indexable signatures on classes: https://github.com/facebook/flow/issues/1323
this[`_${type}Handler`].bind(this)
);
}
}
/**
* Subscribe a users listener cb to the snapshot events.
* @param observer
* @private
*/
_addUserObserver(observer) {
SharedEventEmitter.addListener(this._publicEvents.event, observer);
}
/**
* Send a snapshot event to users event observer.
* @param snapshot PhoneAuthSnapshot
* @private
*/
_emitToObservers(snapshot: PhoneAuthSnapshot) {
SharedEventEmitter.emit(this._publicEvents.event, snapshot);
}
/**
* Send a error snapshot event to any subscribed errorCb's
* @param snapshot
* @private
*/
_emitToErrorCb(snapshot) {
const { error } = snapshot;
if (this._reject) this._reject(error);
SharedEventEmitter.emit(this._publicEvents.error, error);
}
/**
* Send a success snapshot event to any subscribed completeCb's
* @param snapshot
* @private
*/
_emitToSuccessCb(snapshot) {
if (this._resolve) this._resolve(snapshot);
SharedEventEmitter.emit(this._publicEvents.success, snapshot);
}
/**
* Removes all listeners for this phone auth instance
* @private
*/
_removeAllListeners() {
setTimeout(() => {
// move to next event loop - not sure if needed
// internal listeners
Object.values(this._internalEvents).forEach(event => {
SharedEventEmitter.removeAllListeners(event);
});
// user observer listeners
Object.values(this._publicEvents).forEach(publicEvent => {
SharedEventEmitter.removeAllListeners(publicEvent);
});
}, 0);
}
/**
* Create a new internal deferred promise, if not already created
* @private
*/
_promiseDeferred() {
if (!this._promise) {
this._promise = new Promise((resolve, reject) => {
this._resolve = result => {
this._resolve = null;
return resolve(result);
};
this._reject = possibleError => {
this._reject = null;
return reject(possibleError);
};
});
}
}
/* --------------------------
--- INTERNAL EVENT HANDLERS
---------------------------- */
/**
* Internal code sent event handler
* @private
* @param credential
*/
_codeSentHandler(credential) {
const snapshot: PhoneAuthSnapshot = {
verificationId: credential.verificationId,
code: null,
error: null,
state: 'sent',
};
this._emitToObservers(snapshot);
if (isIOS) {
this._emitToSuccessCb(snapshot);
}
if (isAndroid) {
// android can auto retrieve so we don't emit to successCb immediately,
// if auto retrieve times out then that will emit to successCb
}
}
/**
* Internal code auto retrieve timeout event handler
* @private
* @param credential
*/
_codeAutoRetrievalTimeoutHandler(credential) {
const snapshot: PhoneAuthSnapshot = {
verificationId: credential.verificationId,
code: null,
error: null,
state: 'timeout',
};
this._emitToObservers(snapshot);
this._emitToSuccessCb(snapshot);
}
/**
* Internal verification complete event handler
* @param credential
* @private
*/
_verificationCompleteHandler(credential) {
const snapshot: PhoneAuthSnapshot = {
verificationId: credential.verificationId,
code: credential.code || null,
error: null,
state: 'verified',
};
this._emitToObservers(snapshot);
this._emitToSuccessCb(snapshot);
this._removeAllListeners();
}
/**
* Internal verification failed event handler
* @param state
* @private
*/
_verificationFailedHandler(state) {
const snapshot: PhoneAuthSnapshot = {
verificationId: state.verificationId,
code: null,
error: null,
state: 'error',
};
const { code, message, nativeErrorMessage } = state.error;
snapshot.error = nativeToJSError(code, message, { nativeErrorMessage });
this._emitToObservers(snapshot);
this._emitToErrorCb(snapshot);
this._removeAllListeners();
}
/* -------------
-- PUBLIC API
--------------*/
on(
event: string,
observer: () => PhoneAuthSnapshot,
errorCb?: () => PhoneAuthError,
successCb?: () => PhoneAuthSnapshot
): this {
if (!isString(event)) {
throw new Error(
INTERNALS.STRINGS.ERROR_MISSING_ARG_NAMED('event', 'string', 'on')
);
}
if (event !== 'state_changed') {
throw new Error(
INTERNALS.STRINGS.ERROR_ARG_INVALID_VALUE(
'event',
'state_changed',
event
)
);
}
if (!isFunction(observer)) {
throw new Error(
INTERNALS.STRINGS.ERROR_MISSING_ARG_NAMED('observer', 'function', 'on')
);
}
this._addUserObserver(observer);
if (isFunction(errorCb)) {
SharedEventEmitter.once(this._publicEvents.error, errorCb);
}
if (isFunction(successCb)) {
SharedEventEmitter.once(this._publicEvents.success, successCb);
}
return this;
}
/**
* Promise .then proxy
* @param fn
*/
then(fn: () => PhoneAuthSnapshot) {
this._promiseDeferred();
// $FlowFixMe: Unsure how to annotate `bind` here
if (this._promise) return this._promise.then.bind(this._promise)(fn);
return undefined; // will never get here - just to keep flow happy
}
/**
* Promise .catch proxy
* @param fn
*/
catch(fn: () => Error) {
this._promiseDeferred();
// $FlowFixMe: Unsure how to annotate `bind` here
if (this._promise) return this._promise.catch.bind(this._promise)(fn);
return undefined; // will never get here - just to keep flow happy
}
}

View File

@ -0,0 +1,27 @@
/**
* @flow
* EmailAuthProvider representation wrapper
*/
import type { AuthCredential } from '../types';
const providerId = 'password';
export default class EmailAuthProvider {
constructor() {
throw new Error(
'`new EmailAuthProvider()` is not supported on the native Firebase SDKs.'
);
}
static get PROVIDER_ID(): string {
return providerId;
}
static credential(email: string, password: string): AuthCredential {
return {
token: email,
secret: password,
providerId,
};
}
}

View File

@ -0,0 +1,27 @@
/**
* @flow
* FacebookAuthProvider representation wrapper
*/
import type { AuthCredential } from '../types';
const providerId = 'facebook.com';
export default class FacebookAuthProvider {
constructor() {
throw new Error(
'`new FacebookAuthProvider()` is not supported on the native Firebase SDKs.'
);
}
static get PROVIDER_ID(): string {
return providerId;
}
static credential(token: string): AuthCredential {
return {
token,
secret: '',
providerId,
};
}
}

View File

@ -0,0 +1,27 @@
/**
* @flow
* GithubAuthProvider representation wrapper
*/
import type { AuthCredential } from '../types';
const providerId = 'github.com';
export default class GithubAuthProvider {
constructor() {
throw new Error(
'`new GithubAuthProvider()` is not supported on the native Firebase SDKs.'
);
}
static get PROVIDER_ID(): string {
return providerId;
}
static credential(token: string): AuthCredential {
return {
token,
secret: '',
providerId,
};
}
}

View File

@ -0,0 +1,27 @@
/**
* @flow
* EmailAuthProvider representation wrapper
*/
import type { AuthCredential } from '../types';
const providerId = 'google.com';
export default class GoogleAuthProvider {
constructor() {
throw new Error(
'`new GoogleAuthProvider()` is not supported on the native Firebase SDKs.'
);
}
static get PROVIDER_ID(): string {
return providerId;
}
static credential(token: string, secret: string): AuthCredential {
return {
token,
secret,
providerId,
};
}
}

View File

@ -0,0 +1,27 @@
/**
* @flow
* OAuthProvider representation wrapper
*/
import type { AuthCredential } from '../types';
const providerId = 'oauth';
export default class OAuthProvider {
constructor() {
throw new Error(
'`new OAuthProvider()` is not supported on the native Firebase SDKs.'
);
}
static get PROVIDER_ID(): string {
return providerId;
}
static credential(idToken: string, accessToken: string): AuthCredential {
return {
token: idToken,
secret: accessToken,
providerId,
};
}
}

View File

@ -0,0 +1,27 @@
/**
* @flow
* PhoneAuthProvider representation wrapper
*/
import type { AuthCredential } from '../types';
const providerId = 'phone';
export default class PhoneAuthProvider {
constructor() {
throw new Error(
'`new PhoneAuthProvider()` is not supported on the native Firebase SDKs.'
);
}
static get PROVIDER_ID(): string {
return providerId;
}
static credential(verificationId: string, code: string): AuthCredential {
return {
token: verificationId,
secret: code,
providerId,
};
}
}

View File

@ -0,0 +1,27 @@
/**
* @flow
* TwitterAuthProvider representation wrapper
*/
import type { AuthCredential } from '../types';
const providerId = 'twitter.com';
export default class TwitterAuthProvider {
constructor() {
throw new Error(
'`new TwitterAuthProvider()` is not supported on the native Firebase SDKs.'
);
}
static get PROVIDER_ID(): string {
return providerId;
}
static credential(token: string, secret: string): AuthCredential {
return {
token,
secret,
providerId,
};
}
}

View File

@ -0,0 +1,75 @@
/**
* @flow
*/
import type User from './User';
export type ActionCodeInfo = {
data: {
email?: string,
fromEmail?: string,
},
operation: 'PASSWORD_RESET' | 'VERIFY_EMAIL' | 'RECOVER_EMAIL',
};
export type ActionCodeSettings = {
android: {
installApp?: boolean,
minimumVersion?: string,
packageName: string,
},
handleCodeInApp?: boolean,
iOS: {
bundleId?: string,
},
url: string,
};
export type AdditionalUserInfo = {
isNewUser: boolean,
profile?: Object,
providerId: string,
username?: string,
};
export type AuthCredential = {
providerId: string,
token: string,
secret: string,
};
export type UserCredential = {|
additionalUserInfo?: AdditionalUserInfo,
user: User,
|};
export type UserInfo = {
displayName?: string,
email?: string,
phoneNumber?: string,
photoURL?: string,
providerId: string,
uid: string,
};
export type UserMetadata = {
creationTime?: string,
lastSignInTime?: string,
};
export type NativeUser = {
displayName?: string,
email?: string,
emailVerified?: boolean,
isAnonymous?: boolean,
metadata: UserMetadata,
phoneNumber?: string,
photoURL?: string,
providerData: UserInfo[],
providerId: string,
uid: string,
};
export type NativeUserCredential = {|
additionalUserInfo?: AdditionalUserInfo,
user: NativeUser,
|};

View File

@ -0,0 +1,21 @@
/**
* @flow
*/
// todo move out
export class ReferenceBase extends Base {
constructor(path: string) {
super();
this.path = path || '/';
}
/**
* The last part of a Reference's path (after the last '/')
* The key of a root Reference is null.
* @type {String}
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#key}
*/
get key(): string | null {
return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1);
}
}

View File

@ -0,0 +1,185 @@
/**
* @flow
* Remote Config representation wrapper
*/
import { getLogger } from '../../utils/log';
import ModuleBase from '../../utils/ModuleBase';
import { getNativeModule } from '../../utils/native';
import type App from '../core/app';
type NativeValue = {
stringValue?: string,
numberValue?: number,
dataValue?: Object,
boolValue?: boolean,
source:
| 'remoteConfigSourceRemote'
| 'remoteConfigSourceDefault'
| ' remoteConfigSourceStatic',
};
export const MODULE_NAME = 'RNFirebaseRemoteConfig';
export const NAMESPACE = 'config';
/**
* @class Config
*/
export default class RemoteConfig extends ModuleBase {
_developerModeEnabled: boolean;
constructor(app: App) {
super(app, {
moduleName: MODULE_NAME,
multiApp: false,
hasShards: false,
namespace: NAMESPACE,
});
this._developerModeEnabled = false;
}
/**
* Converts a native map to single JS value
* @param nativeValue
* @returns {*}
* @private
*/
_nativeValueToJS(nativeValue: NativeValue) {
return {
source: nativeValue.source,
val() {
if (
nativeValue.boolValue !== null &&
(nativeValue.stringValue === 'true' ||
nativeValue.stringValue === 'false' ||
nativeValue.stringValue === null)
)
return nativeValue.boolValue;
if (
nativeValue.numberValue !== null &&
nativeValue.numberValue !== undefined &&
(nativeValue.stringValue == null ||
nativeValue.stringValue === '' ||
nativeValue.numberValue.toString() === nativeValue.stringValue)
)
return nativeValue.numberValue;
if (
nativeValue.dataValue !== nativeValue.stringValue &&
(nativeValue.stringValue == null || nativeValue.stringValue === '')
)
return nativeValue.dataValue;
return nativeValue.stringValue;
},
};
}
/**
* Enable Remote Config developer mode to allow for frequent refreshes of the cache
*/
enableDeveloperMode() {
if (!this._developerModeEnabled) {
getLogger(this).debug('Enabled developer mode');
getNativeModule(this).enableDeveloperMode();
this._developerModeEnabled = true;
}
}
/**
* Fetches Remote Config data
* Call activateFetched to make fetched data available in app
* @returns {*|Promise.<String>}:
*/
fetch(expiration?: number) {
if (expiration !== undefined) {
getLogger(this).debug(
`Fetching remote config data with expiration ${expiration.toString()}`
);
return getNativeModule(this).fetchWithExpirationDuration(expiration);
}
getLogger(this).debug('Fetching remote config data');
return getNativeModule(this).fetch();
}
/**
* Applies Fetched Config data to the Active Config
* @returns {*|Promise.<Bool>}
* resolves if there was a Fetched Config, and it was activated,
* rejects if no Fetched Config was found, or the Fetched Config was already activated.
*/
activateFetched() {
getLogger(this).debug('Activating remote config');
return getNativeModule(this).activateFetched();
}
/**
* Gets the config value of the default namespace.
* @param key: Config key
* @returns {*|Promise.<Object>}, will always resolve
* Object looks like
* {
* "stringValue" : stringValue,
* "numberValue" : numberValue,
* "dataValue" : dataValue,
* "boolValue" : boolValue,
* "source" : OneOf<String>(remoteConfigSourceRemote|remoteConfigSourceDefault|remoteConfigSourceStatic)
* }
*/
getValue(key: string) {
return getNativeModule(this)
.getValue(key || '')
.then(this._nativeValueToJS);
}
/**
* Gets the config value of the default namespace.
* @param keys: Config key
* @returns {*|Promise.<Object>}, will always resolve.
* Result will be a dictionary of key and config objects
* Object looks like
* {
* "stringValue" : stringValue,
* "numberValue" : numberValue,
* "dataValue" : dataValue,
* "boolValue" : boolValue,
* "source" : OneOf<String>(remoteConfigSourceRemote|remoteConfigSourceDefault|remoteConfigSourceStatic)
* }
*/
getValues(keys: Array<string>) {
return getNativeModule(this)
.getValues(keys || [])
.then(nativeValues => {
const values: { [string]: Object } = {};
for (let i = 0, len = keys.length; i < len; i++) {
values[keys[i]] = this._nativeValueToJS(nativeValues[i]);
}
return values;
});
}
/**
* Get the set of parameter keys that start with the given prefix, from the default namespace
* @param prefix: The key prefix to look for. If prefix is nil or empty, returns all the keys.
* @returns {*|Promise.<Array<String>>}
*/
getKeysByPrefix(prefix?: string) {
return getNativeModule(this).getKeysByPrefix(prefix);
}
/**
* Sets config defaults for parameter keys and values in the default namespace config.
* @param defaults: A dictionary mapping a String key to a Object values.
*/
setDefaults(defaults: Object) {
getNativeModule(this).setDefaults(defaults);
}
/**
* Sets default configs from plist for default namespace;
* @param resource: The plist file name or resource ID
*/
setDefaultsFromResource(resource: string | number) {
getNativeModule(this).setDefaultsFromResource(resource);
}
}
export const statics = {};

View File

@ -0,0 +1,196 @@
/*
* @flow
*/
import { NativeModules } from 'react-native';
import APPS from '../../utils/apps';
import { SharedEventEmitter } from '../../utils/events';
import INTERNALS from '../../utils/internals';
import { isObject } from '../../utils';
import AdMob, { NAMESPACE as AdmobNamespace } from '../admob';
import Auth, { NAMESPACE as AuthNamespace } from '../auth';
import Analytics, { NAMESPACE as AnalyticsNamespace } from '../analytics';
import Config, { NAMESPACE as ConfigNamespace } from '../config';
import Crash, { NAMESPACE as CrashNamespace } from '../crash';
import Crashlytics, {
NAMESPACE as CrashlyticsNamespace,
} from '../fabric/crashlytics';
import Database, { NAMESPACE as DatabaseNamespace } from '../database';
import Firestore, { NAMESPACE as FirestoreNamespace } from '../firestore';
import InstanceId, { NAMESPACE as InstanceIdNamespace } from '../instanceid';
import Invites, { NAMESPACE as InvitesNamespace } from '../invites';
import Links, { NAMESPACE as LinksNamespace } from '../links';
import Messaging, { NAMESPACE as MessagingNamespace } from '../messaging';
import Notifications, {
NAMESPACE as NotificationsNamespace,
} from '../notifications';
import Performance, { NAMESPACE as PerfNamespace } from '../perf';
import Storage, { NAMESPACE as StorageNamespace } from '../storage';
import Utils, { NAMESPACE as UtilsNamespace } from '../utils';
import type { FirebaseOptions } from '../../types';
const FirebaseCoreModule = NativeModules.RNFirebase;
export default class App {
_extendedProps: { [string]: boolean };
_initialized: boolean = false;
_name: string;
_nativeInitialized: boolean = false;
_options: FirebaseOptions;
admob: () => AdMob;
analytics: () => Analytics;
auth: () => Auth;
config: () => Config;
crash: () => Crash;
database: () => Database;
fabric: {
crashlytics: () => Crashlytics,
};
firestore: () => Firestore;
instanceid: () => InstanceId;
invites: () => Invites;
links: () => Links;
messaging: () => Messaging;
notifications: () => Notifications;
perf: () => Performance;
storage: () => Storage;
utils: () => Utils;
constructor(
name: string,
options: FirebaseOptions,
fromNative: boolean = false
) {
this._name = name;
this._options = Object.assign({}, options);
if (fromNative) {
this._initialized = true;
this._nativeInitialized = true;
} else if (options.databaseURL && options.apiKey) {
FirebaseCoreModule.initializeApp(
this._name,
this._options,
(error, result) => {
this._initialized = true;
SharedEventEmitter.emit(`AppReady:${this._name}`, { error, result });
}
);
}
// modules
this.admob = APPS.appModule(this, AdmobNamespace, AdMob);
this.analytics = APPS.appModule(this, AnalyticsNamespace, Analytics);
this.auth = APPS.appModule(this, AuthNamespace, Auth);
this.config = APPS.appModule(this, ConfigNamespace, Config);
this.crash = APPS.appModule(this, CrashNamespace, Crash);
this.database = APPS.appModule(this, DatabaseNamespace, Database);
this.fabric = {
crashlytics: APPS.appModule(this, CrashlyticsNamespace, Crashlytics),
};
this.firestore = APPS.appModule(this, FirestoreNamespace, Firestore);
this.instanceid = APPS.appModule(this, InstanceIdNamespace, InstanceId);
this.invites = APPS.appModule(this, InvitesNamespace, Invites);
this.links = APPS.appModule(this, LinksNamespace, Links);
this.messaging = APPS.appModule(this, MessagingNamespace, Messaging);
this.notifications = APPS.appModule(
this,
NotificationsNamespace,
Notifications
);
this.perf = APPS.appModule(this, PerfNamespace, Performance);
this.storage = APPS.appModule(this, StorageNamespace, Storage);
this.utils = APPS.appModule(this, UtilsNamespace, Utils);
this._extendedProps = {};
}
/**
*
* @return {*}
*/
get name(): string {
return this._name;
}
/**
*
* @return {*}
*/
get options(): FirebaseOptions {
return Object.assign({}, this._options);
}
/**
* Undocumented firebase web sdk method that allows adding additional properties onto
* a firebase app instance.
*
* See: https://github.com/firebase/firebase-js-sdk/blob/master/tests/app/firebase_app.test.ts#L328
*
* @param props
*/
extendApp(props: Object) {
if (!isObject(props)) {
throw new Error(
INTERNALS.STRINGS.ERROR_MISSING_ARG('Object', 'extendApp')
);
}
const keys = Object.keys(props);
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i];
if (!this._extendedProps[key] && Object.hasOwnProperty.call(this, key)) {
throw new Error(INTERNALS.STRINGS.ERROR_PROTECTED_PROP(key));
}
// $FlowExpectedError: Flow doesn't support indexable signatures on classes: https://github.com/facebook/flow/issues/1323
this[key] = props[key];
this._extendedProps[key] = true;
}
}
/**
*
* @return {Promise}
*/
delete() {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_CLASS_METHOD('app', 'delete')
);
// TODO only the ios sdk currently supports delete, add back in when android also supports it
// if (this._name === APPS.DEFAULT_APP_NAME && this._nativeInitialized) {
// return Promise.reject(
// new Error('Unable to delete the default native firebase app instance.'),
// );
// }
//
// return FirebaseCoreModule.deleteApp(this._name);
}
/**
*
* @return {*}
*/
onReady(): Promise<App> {
if (this._initialized) return Promise.resolve(this);
return new Promise((resolve, reject) => {
SharedEventEmitter.once(`AppReady:${this._name}`, ({ error }) => {
if (error) return reject(new Error(error)); // error is a string as it's from native
return resolve(this); // return app
});
});
}
/**
* toString returns the name of the app.
*
* @return {string}
*/
toString() {
return this._name;
}
}

View File

@ -0,0 +1,226 @@
/**
* @flow
*/
import { NativeModules } from 'react-native';
import APPS from '../../utils/apps';
import INTERNALS from '../../utils/internals';
import App from './app';
import VERSION from '../../version';
// module imports
import {
statics as AdMobStatics,
MODULE_NAME as AdmobModuleName,
} from '../admob';
import { statics as AuthStatics, MODULE_NAME as AuthModuleName } from '../auth';
import {
statics as AnalyticsStatics,
MODULE_NAME as AnalyticsModuleName,
} from '../analytics';
import {
statics as ConfigStatics,
MODULE_NAME as ConfigModuleName,
} from '../config';
import {
statics as CrashStatics,
MODULE_NAME as CrashModuleName,
} from '../crash';
import {
statics as CrashlyticsStatics,
MODULE_NAME as CrashlyticsModuleName,
} from '../fabric/crashlytics';
import {
statics as DatabaseStatics,
MODULE_NAME as DatabaseModuleName,
} from '../database';
import {
statics as FirestoreStatics,
MODULE_NAME as FirestoreModuleName,
} from '../firestore';
import {
statics as InstanceIdStatics,
MODULE_NAME as InstanceIdModuleName,
} from '../instanceid';
import {
statics as InvitesStatics,
MODULE_NAME as InvitesModuleName,
} from '../invites';
import {
statics as LinksStatics,
MODULE_NAME as LinksModuleName,
} from '../links';
import {
statics as MessagingStatics,
MODULE_NAME as MessagingModuleName,
} from '../messaging';
import {
statics as NotificationsStatics,
MODULE_NAME as NotificationsModuleName,
} from '../notifications';
import {
statics as PerformanceStatics,
MODULE_NAME as PerfModuleName,
} from '../perf';
import {
statics as StorageStatics,
MODULE_NAME as StorageModuleName,
} from '../storage';
import {
statics as UtilsStatics,
MODULE_NAME as UtilsModuleName,
} from '../utils';
import type {
AdMobModule,
AnalyticsModule,
AuthModule,
ConfigModule,
CrashModule,
DatabaseModule,
FabricModule,
FirebaseOptions,
FirestoreModule,
InstanceIdModule,
InvitesModule,
LinksModule,
MessagingModule,
NotificationsModule,
PerformanceModule,
StorageModule,
UtilsModule,
} from '../../types';
const FirebaseCoreModule = NativeModules.RNFirebase;
class Firebase {
admob: AdMobModule;
analytics: AnalyticsModule;
auth: AuthModule;
config: ConfigModule;
crash: CrashModule;
database: DatabaseModule;
fabric: FabricModule;
firestore: FirestoreModule;
instanceid: InstanceIdModule;
invites: InvitesModule;
links: LinksModule;
messaging: MessagingModule;
notifications: NotificationsModule;
perf: PerformanceModule;
storage: StorageModule;
utils: UtilsModule;
constructor() {
if (!FirebaseCoreModule) {
throw new Error(INTERNALS.STRINGS.ERROR_MISSING_CORE);
}
APPS.initializeNativeApps();
// modules
this.admob = APPS.moduleAndStatics('admob', AdMobStatics, AdmobModuleName);
this.analytics = APPS.moduleAndStatics(
'analytics',
AnalyticsStatics,
AnalyticsModuleName
);
this.auth = APPS.moduleAndStatics('auth', AuthStatics, AuthModuleName);
this.config = APPS.moduleAndStatics(
'config',
ConfigStatics,
ConfigModuleName
);
this.crash = APPS.moduleAndStatics('crash', CrashStatics, CrashModuleName);
this.database = APPS.moduleAndStatics(
'database',
DatabaseStatics,
DatabaseModuleName
);
this.fabric = {
crashlytics: APPS.moduleAndStatics(
'crashlytics',
CrashlyticsStatics,
CrashlyticsModuleName
),
};
this.firestore = APPS.moduleAndStatics(
'firestore',
FirestoreStatics,
FirestoreModuleName
);
this.instanceid = APPS.moduleAndStatics(
'instanceid',
InstanceIdStatics,
InstanceIdModuleName
);
this.invites = APPS.moduleAndStatics(
'invites',
InvitesStatics,
InvitesModuleName
);
this.links = APPS.moduleAndStatics('links', LinksStatics, LinksModuleName);
this.messaging = APPS.moduleAndStatics(
'messaging',
MessagingStatics,
MessagingModuleName
);
this.notifications = APPS.moduleAndStatics(
'notifications',
NotificationsStatics,
NotificationsModuleName
);
this.perf = APPS.moduleAndStatics(
'perf',
PerformanceStatics,
PerfModuleName
);
this.storage = APPS.moduleAndStatics(
'storage',
StorageStatics,
StorageModuleName
);
this.utils = APPS.moduleAndStatics('utils', UtilsStatics, UtilsModuleName);
}
/**
* Web SDK initializeApp
*
* @param options
* @param name
* @return {*}
*/
initializeApp(options: FirebaseOptions, name: string): App {
return APPS.initializeApp(options, name);
}
/**
* Retrieves a Firebase app instance.
*
* When called with no arguments, the default app is returned.
* When an app name is provided, the app corresponding to that name is returned.
*
* @param name
* @return {*}
*/
app(name?: string): App {
return APPS.app(name);
}
/**
* A (read-only) array of all initialized apps.
* @return {Array}
*/
get apps(): Array<App> {
return APPS.apps();
}
/**
* The current SDK version.
* @return {string}
*/
get SDK_VERSION(): string {
return VERSION;
}
}
export default new Firebase();

View File

@ -0,0 +1,87 @@
/**
* @flow
* Crash Reporting representation wrapper
*/
import ModuleBase from '../../utils/ModuleBase';
import { getNativeModule } from '../../utils/native';
import type App from '../core/app';
import type { FirebaseError } from '../../types';
export const MODULE_NAME = 'RNFirebaseCrash';
export const NAMESPACE = 'crash';
export default class Crash extends ModuleBase {
constructor(app: App) {
super(app, {
moduleName: MODULE_NAME,
multiApp: false,
hasShards: false,
namespace: NAMESPACE,
});
}
/**
* Enables/Disables crash reporting
* @param enabled
*/
setCrashCollectionEnabled(enabled: boolean): void {
getNativeModule(this).setCrashCollectionEnabled(enabled);
}
/**
* Returns whether or not crash reporting is currently enabled
* @returns {Promise.<boolean>}
*/
isCrashCollectionEnabled(): Promise<boolean> {
return getNativeModule(this).isCrashCollectionEnabled();
}
/**
* Logs a message that will appear in a subsequent crash report.
* @param {string} message
*/
log(message: string): void {
getNativeModule(this).log(message);
}
/**
* Logs a message that will appear in a subsequent crash report as well as in logcat.
* NOTE: Android only functionality. iOS will just log the message.
* @param {string} message
* @param {number} level
* @param {string} tag
*/
logcat(level: number, tag: string, message: string): void {
getNativeModule(this).logcat(level, tag, message);
}
/**
* Generates a crash report for the given message. This method should be used for unexpected
* exceptions where recovery is not possible.
* NOTE: on iOS, this will cause the app to crash as it's the only way to ensure the exception
* gets sent to Firebase. Otherwise it just gets lost as a log message.
* @param {Error} error
* @param maxStackSize
*/
report(error: FirebaseError, maxStackSize: number = 10): void {
if (!error || !error.message) return;
let errorMessage = `Message: ${error.message}\r\n`;
if (error.code) {
errorMessage = `${errorMessage}Code: ${error.code}\r\n`;
}
const stackRows = error.stack.split('\n');
errorMessage = `${errorMessage}\r\nStack: \r\n`;
for (let i = 0, len = stackRows.length; i < len; i++) {
if (i === maxStackSize) break;
errorMessage = `${errorMessage} - ${stackRows[i]}\r\n`;
}
getNativeModule(this).report(errorMessage);
}
}
export const statics = {};

View File

@ -0,0 +1,146 @@
/**
* @flow
* DataSnapshot representation wrapper
*/
import { isObject, deepGet, deepExists } from './../../utils';
import type Reference from './Reference';
/**
* @class DataSnapshot
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot
*/
export default class DataSnapshot {
ref: Reference;
key: string;
_value: any;
_priority: any;
_childKeys: Array<string>;
constructor(ref: Reference, snapshot: Object) {
this.key = snapshot.key;
if (ref.key !== snapshot.key) {
this.ref = ref.child(snapshot.key);
} else {
this.ref = ref;
}
// internal use only
this._value = snapshot.value;
this._priority = snapshot.priority === undefined ? null : snapshot.priority;
this._childKeys = snapshot.childKeys || [];
}
/**
* Extracts a JavaScript value from a DataSnapshot.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#val
* @returns {any}
*/
val(): any {
// clone via JSON stringify/parse - prevent modification of this._value
if (isObject(this._value) || Array.isArray(this._value))
return JSON.parse(JSON.stringify(this._value));
return this._value;
}
/**
* Gets another DataSnapshot for the location at the specified relative path.
* @param path
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#forEach
* @returns {Snapshot}
*/
child(path: string): DataSnapshot {
const value = deepGet(this._value, path);
const childRef = this.ref.child(path);
return new DataSnapshot(childRef, {
value,
key: childRef.key,
exists: value !== null,
// todo this is wrong - child keys needs to be the ordered keys, from FB
// todo potential solution is build up a tree/map of a snapshot and its children
// todo natively and send that back to JS to be use in this class.
childKeys: isObject(value) ? Object.keys(value) : [],
});
}
/**
* Returns true if this DataSnapshot contains any data.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#exists
* @returns {boolean}
*/
exists(): boolean {
return this._value !== null;
}
/**
* Enumerates the top-level children in the DataSnapshot.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#forEach
* @param action
*/
forEach(action: (key: any) => any): boolean {
if (!this._childKeys.length) return false;
let cancelled = false;
for (let i = 0, len = this._childKeys.length; i < len; i++) {
const key = this._childKeys[i];
const childSnapshot = this.child(key);
const returnValue = action(childSnapshot);
if (returnValue === true) {
cancelled = true;
break;
}
}
return cancelled;
}
/**
* Gets the priority value of the data in this DataSnapshot.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#getPriority
* @returns {String|Number|null}
*/
getPriority(): string | number | null {
return this._priority;
}
/**
* Returns true if the specified child path has (non-null) data.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#hasChild
* @param path
* @returns {Boolean}
*/
hasChild(path: string): boolean {
return deepExists(this._value, path);
}
/**
* Returns whether or not the DataSnapshot has any non-null child properties.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#hasChildren
* @returns {boolean}
*/
hasChildren(): boolean {
return this.numChildren() > 0;
}
/**
* Returns the number of child properties of this DataSnapshot.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#numChildren
* @returns {Number}
*/
numChildren(): number {
if (!isObject(this._value)) return 0;
return Object.keys(this._value).length;
}
/**
* Returns a JSON-serializable representation of this object.
* @link https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#toJSON
* @returns {any}
*/
toJSON(): Object {
return this.val();
}
}

View File

@ -0,0 +1,68 @@
/**
* @flow
* OnDisconnect representation wrapper
*/
import { typeOf } from '../../utils';
import { getNativeModule } from '../../utils/native';
import type Database from './';
import type Reference from './Reference';
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect
* @class OmDisconnect
*/
export default class OnDisconnect {
_database: Database;
ref: Reference;
path: string;
/**
*
* @param ref
*/
constructor(ref: Reference) {
this.ref = ref;
this.path = ref.path;
this._database = ref._database;
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#set
* @param value
* @returns {*}
*/
set(value: string | Object): Promise<void> {
return getNativeModule(this._database).onDisconnectSet(this.path, {
type: typeOf(value),
value,
});
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#update
* @param values
* @returns {*}
*/
update(values: Object): Promise<void> {
return getNativeModule(this._database).onDisconnectUpdate(
this.path,
values
);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#remove
* @returns {*}
*/
remove(): Promise<void> {
return getNativeModule(this._database).onDisconnectRemove(this.path);
}
/**
* @url https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect#cancel
* @returns {*}
*/
cancel(): Promise<void> {
return getNativeModule(this._database).onDisconnectCancel(this.path);
}
}

View File

@ -0,0 +1,108 @@
/**
* @flow
* Query representation wrapper
*/
import { objectToUniqueId } from '../../utils';
import type { DatabaseModifier } from '../../types';
import type Reference from './Reference';
// todo doc methods
/**
* @class Query
*/
export default class Query {
_reference: Reference;
modifiers: Array<DatabaseModifier>;
constructor(ref: Reference, existingModifiers?: Array<DatabaseModifier>) {
this.modifiers = existingModifiers ? [...existingModifiers] : [];
this._reference = ref;
}
/**
*
* @param name
* @param key
* @return {Reference|*}
*/
orderBy(name: string, key?: string) {
this.modifiers.push({
id: `orderBy-${name}:${key || ''}`,
type: 'orderBy',
name,
key,
});
return this._reference;
}
/**
*
* @param name
* @param limit
* @return {Reference|*}
*/
limit(name: string, limit: number) {
this.modifiers.push({
id: `limit-${name}:${limit}`,
type: 'limit',
name,
limit,
});
return this._reference;
}
/**
*
* @param name
* @param value
* @param key
* @return {Reference|*}
*/
filter(name: string, value: any, key?: string) {
this.modifiers.push({
id: `filter-${name}:${objectToUniqueId(value)}:${key || ''}`,
type: 'filter',
name,
value,
valueType: typeof value,
key,
});
return this._reference;
}
/**
*
* @return {[*]}
*/
getModifiers(): Array<DatabaseModifier> {
return [...this.modifiers];
}
/**
*
* @return {*}
*/
queryIdentifier() {
// sort modifiers to enforce ordering
const sortedModifiers = this.getModifiers().sort((a, b) => {
if (a.id < b.id) return -1;
if (a.id > b.id) return 1;
return 0;
});
// Convert modifiers to unique key
let key = '{';
for (let i = 0; i < sortedModifiers.length; i++) {
if (i !== 0) key += ',';
key += sortedModifiers[i].id;
}
key += '}';
return key;
}
}

View File

@ -0,0 +1,894 @@
/**
* @flow
* Database Reference representation wrapper
*/
import Query from './Query';
import DataSnapshot from './DataSnapshot';
import OnDisconnect from './OnDisconnect';
import { getLogger } from '../../utils/log';
import { getNativeModule } from '../../utils/native';
import ReferenceBase from '../../utils/ReferenceBase';
import {
promiseOrCallback,
isFunction,
isObject,
isString,
tryJSONParse,
tryJSONStringify,
generatePushID,
} from '../../utils';
import SyncTree from '../../utils/SyncTree';
import type Database from './';
import type { DatabaseModifier, FirebaseError } from '../../types';
// track all event registrations by path
let listeners = 0;
/**
* Enum for event types
* @readonly
* @enum {String}
*/
const ReferenceEventTypes = {
value: 'value',
child_added: 'child_added',
child_removed: 'child_removed',
child_changed: 'child_changed',
child_moved: 'child_moved',
};
type DatabaseListener = {
listenerId: number,
eventName: string,
successCallback: Function,
failureCallback?: Function,
};
/**
* @typedef {String} ReferenceLocation - Path to location in the database, relative
* to the root reference. Consists of a path where segments are separated by a
* forward slash (/) and ends in a ReferenceKey - except the root location, which
* has no ReferenceKey.
*
* @example
* // root reference location: '/'
* // non-root reference: '/path/to/referenceKey'
*/
/**
* @typedef {String} ReferenceKey - Identifier for each location that is unique to that
* location, within the scope of its parent. The last part of a ReferenceLocation.
*/
/**
* Represents a specific location in your Database that can be used for
* reading or writing data.
*
* You can reference the root using firebase.database().ref() or a child location
* by calling firebase.database().ref("child/path").
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference
* @class Reference
* @extends ReferenceBase
*/
export default class Reference extends ReferenceBase {
_database: Database;
_promise: ?Promise<*>;
_query: Query;
_refListeners: { [listenerId: number]: DatabaseListener };
constructor(
database: Database,
path: string,
existingModifiers?: Array<DatabaseModifier>
) {
super(path);
this._promise = null;
this._refListeners = {};
this._database = database;
this._query = new Query(this, existingModifiers);
getLogger(database).debug('Created new Reference', this._getRefKey());
}
/**
* 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. Additionally, while a location is kept synced,
* it will not be evicted from the persistent disk cache.
*
* @link https://firebase.google.com/docs/reference/android/com/google/firebase/database/Query.html#keepSynced(boolean)
* @param bool
* @returns {*}
*/
keepSynced(bool: boolean): Promise<void> {
return getNativeModule(this._database).keepSynced(
this._getRefKey(),
this.path,
this._query.getModifiers(),
bool
);
}
/**
* Writes data to this Database location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#set
* @param value
* @param onComplete
* @returns {Promise}
*/
set(value: any, onComplete?: Function): Promise<void> {
return promiseOrCallback(
getNativeModule(this._database).set(
this.path,
this._serializeAnyType(value)
),
onComplete
);
}
/**
* Sets a priority for the data at this Database location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#setPriority
* @param priority
* @param onComplete
* @returns {Promise}
*/
setPriority(
priority: string | number | null,
onComplete?: Function
): Promise<void> {
const _priority = this._serializeAnyType(priority);
return promiseOrCallback(
getNativeModule(this._database).setPriority(this.path, _priority),
onComplete
);
}
/**
* Writes data the Database location. Like set() but also specifies the priority for that data.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#setWithPriority
* @param value
* @param priority
* @param onComplete
* @returns {Promise}
*/
setWithPriority(
value: any,
priority: string | number | null,
onComplete?: Function
): Promise<void> {
const _value = this._serializeAnyType(value);
const _priority = this._serializeAnyType(priority);
return promiseOrCallback(
getNativeModule(this._database).setWithPriority(
this.path,
_value,
_priority
),
onComplete
);
}
/**
* Writes multiple values to the Database at once.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#update
* @param val
* @param onComplete
* @returns {Promise}
*/
update(val: Object, onComplete?: Function): Promise<void> {
const value = this._serializeObject(val);
return promiseOrCallback(
getNativeModule(this._database).update(this.path, value),
onComplete
);
}
/**
* Removes the data at this Database location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#remove
* @param onComplete
* @return {Promise}
*/
remove(onComplete?: Function): Promise<void> {
return promiseOrCallback(
getNativeModule(this._database).remove(this.path),
onComplete
);
}
/**
* Atomically modifies the data at this location.
*
* @link https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction
* @param transactionUpdate
* @param onComplete
* @param applyLocally
*/
transaction(
transactionUpdate: Function,
onComplete: (
error: ?Error,
committed: boolean,
snapshot: ?DataSnapshot
) => *,
applyLocally: boolean = false
) {
if (!isFunction(transactionUpdate)) {
return Promise.reject(
new Error('Missing transactionUpdate function argument.')
);
}
return new Promise((resolve, reject) => {
const onCompleteWrapper = (error, committed, snapshotData) => {
if (isFunction(onComplete)) {
if (error) {
onComplete(error, committed, null);
} else {
onComplete(null, committed, new DataSnapshot(this, snapshotData));
}
}
if (error) return reject(error);
return resolve({
committed,
snapshot: new DataSnapshot(this, snapshotData),
});
};
// start the transaction natively
this._database._transactionHandler.add(
this,
transactionUpdate,
onCompleteWrapper,
applyLocally
);
});
}
/**
*
* @param eventName
* @param successCallback
* @param cancelOrContext
* @param context
* @returns {Promise.<any>}
*/
once(
eventName: string = 'value',
successCallback: (snapshot: DataSnapshot) => void,
cancelOrContext: (error: FirebaseError) => void,
context?: Object
) {
return getNativeModule(this._database)
.once(this._getRefKey(), this.path, this._query.getModifiers(), eventName)
.then(({ snapshot }) => {
const _snapshot = new DataSnapshot(this, snapshot);
if (isFunction(successCallback)) {
if (isObject(cancelOrContext))
successCallback.bind(cancelOrContext)(_snapshot);
if (context && isObject(context))
successCallback.bind(context)(_snapshot);
successCallback(_snapshot);
}
return _snapshot;
})
.catch(error => {
if (isFunction(cancelOrContext)) return cancelOrContext(error);
throw error;
});
}
/**
*
* @param value
* @param onComplete
* @returns {*}
*/
push(value: any, onComplete?: Function): Reference | Promise<void> {
if (value === null || value === undefined) {
return new Reference(
this._database,
`${this.path}/${generatePushID(this._database._serverTimeOffset)}`
);
}
const newRef = new Reference(
this._database,
`${this.path}/${generatePushID(this._database._serverTimeOffset)}`
);
const promise = newRef.set(value);
// if callback provided then internally call the set promise with value
if (isFunction(onComplete)) {
return (
promise
// $FlowExpectedError: Reports that onComplete can change to null despite the null check: https://github.com/facebook/flow/issues/1655
.then(() => onComplete(null, newRef))
// $FlowExpectedError: Reports that onComplete can change to null despite the null check: https://github.com/facebook/flow/issues/1655
.catch(error => onComplete(error, null))
);
}
// otherwise attach promise to 'thenable' reference and return the
// new reference
newRef._setThenable(promise);
return newRef;
}
/**
* MODIFIERS
*/
/**
*
* @returns {Reference}
*/
orderByKey(): Reference {
return this.orderBy('orderByKey');
}
/**
*
* @returns {Reference}
*/
orderByPriority(): Reference {
return this.orderBy('orderByPriority');
}
/**
*
* @returns {Reference}
*/
orderByValue(): Reference {
return this.orderBy('orderByValue');
}
/**
*
* @param key
* @returns {Reference}
*/
orderByChild(key: string): Reference {
return this.orderBy('orderByChild', key);
}
/**
*
* @param name
* @param key
* @returns {Reference}
*/
orderBy(name: string, key?: string): Reference {
const newRef = new Reference(
this._database,
this.path,
this._query.getModifiers()
);
newRef._query.orderBy(name, key);
return newRef;
}
/**
* LIMITS
*/
/**
*
* @param limit
* @returns {Reference}
*/
limitToLast(limit: number): Reference {
return this.limit('limitToLast', limit);
}
/**
*
* @param limit
* @returns {Reference}
*/
limitToFirst(limit: number): Reference {
return this.limit('limitToFirst', limit);
}
/**
*
* @param name
* @param limit
* @returns {Reference}
*/
limit(name: string, limit: number): Reference {
const newRef = new Reference(
this._database,
this.path,
this._query.getModifiers()
);
newRef._query.limit(name, limit);
return newRef;
}
/**
* FILTERS
*/
/**
*
* @param value
* @param key
* @returns {Reference}
*/
equalTo(value: any, key?: string): Reference {
return this.filter('equalTo', value, key);
}
/**
*
* @param value
* @param key
* @returns {Reference}
*/
endAt(value: any, key?: string): Reference {
return this.filter('endAt', value, key);
}
/**
*
* @param value
* @param key
* @returns {Reference}
*/
startAt(value: any, key?: string): Reference {
return this.filter('startAt', value, key);
}
/**
*
* @param name
* @param value
* @param key
* @returns {Reference}
*/
filter(name: string, value: any, key?: string): Reference {
const newRef = new Reference(
this._database,
this.path,
this._query.getModifiers()
);
newRef._query.filter(name, value, key);
return newRef;
}
/**
*
* @returns {OnDisconnect}
*/
onDisconnect(): OnDisconnect {
return new OnDisconnect(this);
}
/**
* Creates a Reference to a child of the current Reference, using a relative path.
* No validation is performed on the path to ensure it has a valid format.
* @param {String} path relative to current ref's location
* @returns {!Reference} A new Reference to the path provided, relative to the current
* Reference
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#child}
*/
child(path: string): Reference {
return new Reference(this._database, `${this.path}/${path}`);
}
/**
* Return the ref as a path string
* @returns {string}
*/
toString(): string {
return `${this._database.databaseUrl}/${this.path}`;
}
/**
* Returns whether another Reference represent the same location and are from the
* same instance of firebase.app.App - multiple firebase apps not currently supported.
* @param {Reference} otherRef - Other reference to compare to this one
* @return {Boolean} Whether otherReference is equal to this one
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#isEqual}
*/
isEqual(otherRef: Reference): boolean {
return (
!!otherRef &&
otherRef.constructor === Reference &&
otherRef.key === this.key &&
this._query.queryIdentifier() === otherRef._query.queryIdentifier()
);
}
/**
* GETTERS
*/
/**
* The parent location of a Reference, or null for the root Reference.
* @type {Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#parent}
*/
get parent(): Reference | null {
if (this.path === '/') return null;
return new Reference(
this._database,
this.path.substring(0, this.path.lastIndexOf('/'))
);
}
/**
* A reference to itself
* @type {!Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#ref}
*/
get ref(): Reference {
return this;
}
/**
* Reference to the root of the database: '/'
* @type {!Reference}
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#root}
*/
get root(): Reference {
return new Reference(this._database, '/');
}
/**
* Access then method of promise if set
* @return {*}
*/
then(fnResolve: any => any, fnReject: any => any) {
if (isFunction(fnResolve) && this._promise && this._promise.then) {
return this._promise.then.bind(this._promise)(
result => {
this._promise = null;
return fnResolve(result);
},
possibleErr => {
this._promise = null;
if (isFunction(fnReject)) {
return fnReject(possibleErr);
}
throw possibleErr;
}
);
}
throw new Error("Cannot read property 'then' of undefined.");
}
/**
* Access catch method of promise if set
* @return {*}
*/
catch(fnReject: any => any) {
if (isFunction(fnReject) && this._promise && this._promise.catch) {
return this._promise.catch.bind(this._promise)(possibleErr => {
this._promise = null;
return fnReject(possibleErr);
});
}
throw new Error("Cannot read property 'catch' of undefined.");
}
/**
* INTERNALS
*/
/**
* Generate a unique registration key.
*
* @return {string}
*/
_getRegistrationKey(eventType: string): string {
return `$${this._database.databaseUrl}$/${
this.path
}$${this._query.queryIdentifier()}$${listeners}$${eventType}`;
}
/**
* Generate a string that uniquely identifies this
* combination of path and query modifiers
*
* @return {string}
* @private
*/
_getRefKey() {
return `$${this._database.databaseUrl}$/${
this.path
}$${this._query.queryIdentifier()}`;
}
/**
* Set the promise this 'thenable' reference relates to
* @param promise
* @private
*/
_setThenable(promise: Promise<*>) {
this._promise = promise;
}
/**
*
* @param obj
* @returns {Object}
* @private
*/
_serializeObject(obj: Object) {
if (!isObject(obj)) return obj;
// json stringify then parse it calls toString on Objects / Classes
// that support it i.e new Date() becomes a ISO string.
return tryJSONParse(tryJSONStringify(obj));
}
/**
*
* @param value
* @returns {*}
* @private
*/
_serializeAnyType(value: any) {
if (isObject(value)) {
return {
type: 'object',
value: this._serializeObject(value),
};
}
return {
type: typeof value,
value,
};
}
/**
* Register a listener for data changes at the current ref's location.
* The primary method of reading data from a Database.
*
* Listeners can be unbound using {@link off}.
*
* Event Types:
*
* - value: {@link callback}.
* - child_added: {@link callback}
* - child_removed: {@link callback}
* - child_changed: {@link callback}
* - child_moved: {@link callback}
*
* @param {ReferenceEventType} eventType - Type of event to attach a callback for.
* @param {ReferenceEventCallback} callback - Function that will be called
* when the event occurs with the new data.
* @param {cancelCallbackOrContext=} cancelCallbackOrContext - Optional callback that is called
* if the event subscription fails. {@link cancelCallbackOrContext}
* @param {*=} context - Optional object to bind the callbacks to when calling them.
* @returns {ReferenceEventCallback} callback function, unmodified (unbound), for
* convenience if you want to pass an inline function to on() and store it later for
* removing using off().
*
* {@link https://firebase.google.com/docs/reference/js/firebase.database.Reference#on}
*/
on(
eventType: string,
callback: DataSnapshot => any,
cancelCallbackOrContext?: Object => any | Object,
context?: Object
): Function {
if (!eventType) {
throw new Error(
'Query.on failed: Function called with 0 arguments. Expects at least 2.'
);
}
if (!isString(eventType) || !ReferenceEventTypes[eventType]) {
throw new Error(
`Query.on failed: First argument must be a valid string event type: "${Object.keys(
ReferenceEventTypes
).join(', ')}"`
);
}
if (!callback) {
throw new Error(
'Query.on failed: Function called with 1 argument. Expects at least 2.'
);
}
if (!isFunction(callback)) {
throw new Error(
'Query.on failed: Second argument must be a valid function.'
);
}
if (
cancelCallbackOrContext &&
!isFunction(cancelCallbackOrContext) &&
!isObject(context) &&
!isObject(cancelCallbackOrContext)
) {
throw new Error(
'Query.on failed: Function called with 3 arguments, but third optional argument `cancelCallbackOrContext` was not a function.'
);
}
if (
cancelCallbackOrContext &&
!isFunction(cancelCallbackOrContext) &&
context
) {
throw new Error(
'Query.on failed: Function called with 4 arguments, but third optional argument `cancelCallbackOrContext` was not a function.'
);
}
const eventRegistrationKey = this._getRegistrationKey(eventType);
const registrationCancellationKey = `${eventRegistrationKey}$cancelled`;
const _context =
cancelCallbackOrContext && !isFunction(cancelCallbackOrContext)
? cancelCallbackOrContext
: context;
const registrationObj = {
eventType,
ref: this,
path: this.path,
key: this._getRefKey(),
appName: this._database.app.name,
dbURL: this._database.databaseUrl,
eventRegistrationKey,
};
SyncTree.addRegistration({
...registrationObj,
listener: _context ? callback.bind(_context) : callback,
});
if (cancelCallbackOrContext && isFunction(cancelCallbackOrContext)) {
// cancellations have their own separate registration
// as these are one off events, and they're not guaranteed
// to occur either, only happens on failure to register on native
SyncTree.addRegistration({
ref: this,
once: true,
path: this.path,
key: this._getRefKey(),
appName: this._database.app.name,
dbURL: this._database.databaseUrl,
eventType: `${eventType}$cancelled`,
eventRegistrationKey: registrationCancellationKey,
listener: _context
? cancelCallbackOrContext.bind(_context)
: cancelCallbackOrContext,
});
}
// initialise the native listener if not already listening
getNativeModule(this._database).on({
eventType,
path: this.path,
key: this._getRefKey(),
appName: this._database.app.name,
modifiers: this._query.getModifiers(),
hasCancellationCallback: isFunction(cancelCallbackOrContext),
registration: {
eventRegistrationKey,
key: registrationObj.key,
registrationCancellationKey,
},
});
// increment number of listeners - just s short way of making
// every registration unique per .on() call
listeners += 1;
// return original unbound successCallback for
// the purposes of calling .off(eventType, callback) at a later date
return callback;
}
/**
* 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 or callback is specified, all callbacks for the Reference will be removed.
* @param eventType
* @param originalCallback
*/
off(eventType?: string = '', originalCallback?: () => any) {
if (!arguments.length) {
// Firebase Docs:
// if no eventType or callback is specified, all callbacks for the Reference will be removed.
return SyncTree.removeListenersForRegistrations(
SyncTree.getRegistrationsByPath(this.path)
);
}
/*
* VALIDATE ARGS
*/
if (
eventType &&
(!isString(eventType) || !ReferenceEventTypes[eventType])
) {
throw new Error(
`Query.off failed: First argument must be a valid string event type: "${Object.keys(
ReferenceEventTypes
).join(', ')}"`
);
}
if (originalCallback && !isFunction(originalCallback)) {
throw new Error(
'Query.off failed: Function called with 2 arguments, but second optional argument was not a function.'
);
}
// Firebase Docs:
// 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.
// Remove only a single registration
if (eventType && originalCallback) {
const registration = SyncTree.getOneByPathEventListener(
this.path,
eventType,
originalCallback
);
if (!registration) return [];
// remove the paired cancellation registration if any exist
SyncTree.removeListenersForRegistrations([`${registration}$cancelled`]);
// remove only the first registration to match firebase web sdk
// call multiple times to remove multiple registrations
return SyncTree.removeListenerRegistrations(originalCallback, [
registration,
]);
}
// Firebase Docs:
// If a callback is not specified, all callbacks for the specified eventType will be removed.
const registrations = SyncTree.getRegistrationsByPathEvent(
this.path,
eventType
);
SyncTree.removeListenersForRegistrations(
SyncTree.getRegistrationsByPathEvent(this.path, `${eventType}$cancelled`)
);
return SyncTree.removeListenersForRegistrations(registrations);
}
}

View File

@ -0,0 +1,128 @@
/**
* @flow
* Database representation wrapper
*/
import { NativeModules } from 'react-native';
import Reference from './Reference';
import TransactionHandler from './transaction';
import ModuleBase from '../../utils/ModuleBase';
import { getNativeModule } from '../../utils/native';
import type App from '../core/app';
import firebase from '../core/firebase';
const NATIVE_EVENTS = [
'database_transaction_event',
// 'database_server_offset', // TODO
];
export const MODULE_NAME = 'RNFirebaseDatabase';
export const NAMESPACE = 'database';
/**
* @class Database
*/
export default class Database extends ModuleBase {
_offsetRef: Reference;
_serverTimeOffset: number;
_transactionHandler: TransactionHandler;
_serviceUrl: string;
constructor(appOrUrl: App | string, options: Object = {}) {
let app;
let serviceUrl;
if (typeof appOrUrl === 'string') {
app = firebase.app();
serviceUrl = appOrUrl.endsWith('/') ? appOrUrl : `${appOrUrl}/`;
} else {
app = appOrUrl;
serviceUrl = app.options.databaseURL;
}
super(
app,
{
events: NATIVE_EVENTS,
moduleName: MODULE_NAME,
multiApp: true,
hasShards: true,
namespace: NAMESPACE,
},
serviceUrl
);
this._serviceUrl = serviceUrl;
this._transactionHandler = new TransactionHandler(this);
if (options.persistence) {
getNativeModule(this).setPersistence(options.persistence);
}
// server time listener
// setTimeout used to avoid setPersistence race conditions
// todo move this and persistence to native side, create a db configure() method natively perhaps?
// todo and then native can call setPersistence and then emit offset events
setTimeout(() => {
this._serverTimeOffset = 0;
this._offsetRef = this.ref('.info/serverTimeOffset');
this._offsetRef.on('value', snapshot => {
this._serverTimeOffset = snapshot.val() || this._serverTimeOffset;
});
}, 1);
}
/**
*
* @return {number}
*/
getServerTime(): number {
return new Date(Date.now() + this._serverTimeOffset);
}
/**
*
*/
goOnline(): void {
getNativeModule(this).goOnline();
}
/**
*
*/
goOffline(): void {
getNativeModule(this).goOffline();
}
/**
* Returns a new firebase reference instance
* @param path
* @returns {Reference}
*/
ref(path: string): Reference {
return new Reference(this, path);
}
/**
* Returns the database url
* @returns {string}
*/
get databaseUrl(): string {
return this._serviceUrl;
}
}
export const statics = {
ServerValue: NativeModules.RNFirebaseDatabase
? {
TIMESTAMP: NativeModules.RNFirebaseDatabase.serverValueTimestamp || {
'.sv': 'timestamp',
},
}
: {},
enableLogging(enabled: boolean) {
if (NativeModules[MODULE_NAME]) {
NativeModules[MODULE_NAME].enableLogging(enabled);
}
},
};

View File

@ -0,0 +1,164 @@
/**
* @flow
* Database Transaction representation wrapper
*/
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import { getLogger } from '../../utils/log';
import { getNativeModule } from '../../utils/native';
import type Database from './';
let transactionId = 0;
/**
* Uses the push id generator to create a transaction id
* @returns {number}
* @private
*/
const generateTransactionId = (): number => transactionId++;
/**
* @class TransactionHandler
*/
export default class TransactionHandler {
_database: Database;
_transactions: { [number]: Object };
constructor(database: Database) {
this._transactions = {};
this._database = database;
SharedEventEmitter.addListener(
getAppEventName(this._database, 'database_transaction_event'),
this._handleTransactionEvent.bind(this)
);
}
/**
* Add a new transaction and start it natively.
* @param reference
* @param transactionUpdater
* @param onComplete
* @param applyLocally
*/
add(
reference: Object,
transactionUpdater: Function,
onComplete?: Function,
applyLocally?: boolean = false
) {
const id = generateTransactionId();
this._transactions[id] = {
id,
reference,
transactionUpdater,
onComplete,
applyLocally,
completed: false,
started: true,
};
getNativeModule(this._database).transactionStart(
reference.path,
id,
applyLocally
);
}
/**
* INTERNALS
*/
/**
*
* @param event
* @returns {*}
* @private
*/
_handleTransactionEvent(event: Object = {}) {
switch (event.type) {
case 'update':
return this._handleUpdate(event);
case 'error':
return this._handleError(event);
case 'complete':
return this._handleComplete(event);
default:
getLogger(this._database).warn(
`Unknown transaction event type: '${event.type}'`,
event
);
return undefined;
}
}
/**
*
* @param event
* @private
*/
_handleUpdate(event: Object = {}) {
let newValue;
const { id, value } = event;
try {
const transaction = this._transactions[id];
if (!transaction) return;
newValue = transaction.transactionUpdater(value);
} finally {
let abort = false;
if (newValue === undefined) {
abort = true;
}
getNativeModule(this._database).transactionTryCommit(id, {
value: newValue,
abort,
});
}
}
/**
*
* @param event
* @private
*/
_handleError(event: Object = {}) {
const transaction = this._transactions[event.id];
if (transaction && !transaction.completed) {
transaction.completed = true;
try {
transaction.onComplete(event.error, false, null);
} finally {
setImmediate(() => {
delete this._transactions[event.id];
});
}
}
}
/**
*
* @param event
* @private
*/
_handleComplete(event: Object = {}) {
const transaction = this._transactions[event.id];
if (transaction && !transaction.completed) {
transaction.completed = true;
try {
transaction.onComplete(
null,
event.committed,
Object.assign({}, event.snapshot)
);
} finally {
setImmediate(() => {
delete this._transactions[event.id];
});
}
}
}
}

View File

@ -0,0 +1,83 @@
/**
* @flow
* Crash Reporting representation wrapper
*/
import ModuleBase from '../../../utils/ModuleBase';
import { getNativeModule } from '../../../utils/native';
import type App from '../../core/app';
export const MODULE_NAME = 'RNFirebaseCrashlytics';
export const NAMESPACE = 'crashlytics';
export default class Crashlytics extends ModuleBase {
constructor(app: App) {
super(app, {
moduleName: MODULE_NAME,
multiApp: false,
hasShards: false,
namespace: NAMESPACE,
});
}
/**
* Forces a crash. Useful for testing your application is set up correctly.
*/
crash(): void {
getNativeModule(this).crash();
}
/**
* Logs a message that will appear in any subsequent crash reports.
* @param {string} message
*/
log(message: string): void {
getNativeModule(this).log(message);
}
/**
* Logs a non fatal exception.
* @param {string} code
* @param {string} message
*/
recordError(code: number, message: string): void {
getNativeModule(this).recordError(code, message);
}
/**
* Set a boolean value to show alongside any subsequent crash reports.
*/
setBoolValue(key: string, value: boolean): void {
getNativeModule(this).setBoolValue(key, value);
}
/**
* Set a float value to show alongside any subsequent crash reports.
*/
setFloatValue(key: string, value: number): void {
getNativeModule(this).setFloatValue(key, value);
}
/**
* Set an integer value to show alongside any subsequent crash reports.
*/
setIntValue(key: string, value: number): void {
getNativeModule(this).setIntValue(key, value);
}
/**
* Set a string value to show alongside any subsequent crash reports.
*/
setStringValue(key: string, value: string): void {
getNativeModule(this).setStringValue(key, value);
}
/**
* Set the user ID to show alongside any subsequent crash reports.
*/
setUserIdentifier(userId: string): void {
getNativeModule(this).setUserIdentifier(userId);
}
}
export const statics = {};

View File

@ -0,0 +1,109 @@
/**
* @flow
* CollectionReference representation wrapper
*/
import DocumentReference from './DocumentReference';
import Query from './Query';
import { firestoreAutoId } from '../../utils';
import type Firestore from './';
import type {
QueryDirection,
QueryListenOptions,
QueryOperator,
} from './types';
import type FieldPath from './FieldPath';
import type Path from './Path';
import type { Observer, ObserverOnError, ObserverOnNext } from './Query';
import type QuerySnapshot from './QuerySnapshot';
/**
* @class CollectionReference
*/
export default class CollectionReference {
_collectionPath: Path;
_firestore: Firestore;
_query: Query;
constructor(firestore: Firestore, collectionPath: Path) {
this._collectionPath = collectionPath;
this._firestore = firestore;
this._query = new Query(firestore, collectionPath);
}
get firestore(): Firestore {
return this._firestore;
}
get id(): string | null {
return this._collectionPath.id;
}
get parent(): DocumentReference | null {
const parentPath = this._collectionPath.parent();
return parentPath
? new DocumentReference(this._firestore, parentPath)
: null;
}
add(data: Object): Promise<DocumentReference> {
const documentRef = this.doc();
return documentRef.set(data).then(() => Promise.resolve(documentRef));
}
doc(documentPath?: string): DocumentReference {
const newPath = documentPath || firestoreAutoId();
const path = this._collectionPath.child(newPath);
if (!path.isDocument) {
throw new Error('Argument "documentPath" must point to a document.');
}
return new DocumentReference(this._firestore, path);
}
// From Query
endAt(...snapshotOrVarArgs: any[]): Query {
return this._query.endAt(snapshotOrVarArgs);
}
endBefore(...snapshotOrVarArgs: any[]): Query {
return this._query.endBefore(snapshotOrVarArgs);
}
get(): Promise<QuerySnapshot> {
return this._query.get();
}
limit(limit: number): Query {
return this._query.limit(limit);
}
onSnapshot(
optionsOrObserverOrOnNext: QueryListenOptions | Observer | ObserverOnNext,
observerOrOnNextOrOnError?: Observer | ObserverOnNext | ObserverOnError,
onError?: ObserverOnError
): () => void {
return this._query.onSnapshot(
optionsOrObserverOrOnNext,
observerOrOnNextOrOnError,
onError
);
}
orderBy(fieldPath: string | FieldPath, directionStr?: QueryDirection): Query {
return this._query.orderBy(fieldPath, directionStr);
}
startAfter(...snapshotOrVarArgs: any[]): Query {
return this._query.startAfter(snapshotOrVarArgs);
}
startAt(...snapshotOrVarArgs: any[]): Query {
return this._query.startAt(snapshotOrVarArgs);
}
where(fieldPath: string, opStr: QueryOperator, value: any): Query {
return this._query.where(fieldPath, opStr, value);
}
}

View File

@ -0,0 +1,41 @@
/**
* @flow
* DocumentChange representation wrapper
*/
import DocumentSnapshot from './DocumentSnapshot';
import type Firestore from './';
import type { NativeDocumentChange } from './types';
/**
* @class DocumentChange
*/
export default class DocumentChange {
_document: DocumentSnapshot;
_newIndex: number;
_oldIndex: number;
_type: string;
constructor(firestore: Firestore, nativeData: NativeDocumentChange) {
this._document = new DocumentSnapshot(firestore, nativeData.document);
this._newIndex = nativeData.newIndex;
this._oldIndex = nativeData.oldIndex;
this._type = nativeData.type;
}
get doc(): DocumentSnapshot {
return this._document;
}
get newIndex(): number {
return this._newIndex;
}
get oldIndex(): number {
return this._oldIndex;
}
get type(): string {
return this._type;
}
}

View File

@ -0,0 +1,260 @@
/**
* @flow
* DocumentReference representation wrapper
*/
import CollectionReference from './CollectionReference';
import DocumentSnapshot from './DocumentSnapshot';
import { parseUpdateArgs } from './utils';
import { buildNativeMap } from './utils/serialize';
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import { getLogger } from '../../utils/log';
import { firestoreAutoId, isFunction, isObject } from '../../utils';
import { getNativeModule } from '../../utils/native';
import type Firestore from './';
import type {
DocumentListenOptions,
NativeDocumentSnapshot,
SetOptions,
} from './types';
import type Path from './Path';
type ObserverOnError = Object => void;
type ObserverOnNext = DocumentSnapshot => void;
type Observer = {
error?: ObserverOnError,
next: ObserverOnNext,
};
/**
* @class DocumentReference
*/
export default class DocumentReference {
_documentPath: Path;
_firestore: Firestore;
constructor(firestore: Firestore, documentPath: Path) {
this._documentPath = documentPath;
this._firestore = firestore;
}
get firestore(): Firestore {
return this._firestore;
}
get id(): string | null {
return this._documentPath.id;
}
get parent(): CollectionReference {
const parentPath = this._documentPath.parent();
// $FlowExpectedError: parentPath can never be null
return new CollectionReference(this._firestore, parentPath);
}
get path(): string {
return this._documentPath.relativeName;
}
collection(collectionPath: string): CollectionReference {
const path = this._documentPath.child(collectionPath);
if (!path.isCollection) {
throw new Error('Argument "collectionPath" must point to a collection.');
}
return new CollectionReference(this._firestore, path);
}
delete(): Promise<void> {
return getNativeModule(this._firestore).documentDelete(this.path);
}
get(): Promise<DocumentSnapshot> {
return getNativeModule(this._firestore)
.documentGet(this.path)
.then(result => new DocumentSnapshot(this._firestore, result));
}
onSnapshot(
optionsOrObserverOrOnNext:
| DocumentListenOptions
| Observer
| ObserverOnNext,
observerOrOnNextOrOnError?: Observer | ObserverOnNext | ObserverOnError,
onError?: ObserverOnError
) {
let observer: Observer;
let docListenOptions = {};
// Called with: onNext, ?onError
if (isFunction(optionsOrObserverOrOnNext)) {
if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) {
throw new Error(
'DocumentReference.onSnapshot failed: Second argument must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: optionsOrObserverOrOnNext,
error: observerOrOnNextOrOnError,
};
} else if (
optionsOrObserverOrOnNext &&
isObject(optionsOrObserverOrOnNext)
) {
// Called with: Observer
if (optionsOrObserverOrOnNext.next) {
if (isFunction(optionsOrObserverOrOnNext.next)) {
if (
optionsOrObserverOrOnNext.error &&
!isFunction(optionsOrObserverOrOnNext.error)
) {
throw new Error(
'DocumentReference.onSnapshot failed: Observer.error must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: optionsOrObserverOrOnNext.next,
error: optionsOrObserverOrOnNext.error,
};
} else {
throw new Error(
'DocumentReference.onSnapshot failed: Observer.next must be a valid function.'
);
}
} else if (
Object.prototype.hasOwnProperty.call(
optionsOrObserverOrOnNext,
'includeMetadataChanges'
)
) {
docListenOptions = optionsOrObserverOrOnNext;
// Called with: Options, onNext, ?onError
if (isFunction(observerOrOnNextOrOnError)) {
if (onError && !isFunction(onError)) {
throw new Error(
'DocumentReference.onSnapshot failed: Third argument must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: observerOrOnNextOrOnError,
error: onError,
};
// Called with Options, Observer
} else if (
observerOrOnNextOrOnError &&
isObject(observerOrOnNextOrOnError) &&
observerOrOnNextOrOnError.next
) {
if (isFunction(observerOrOnNextOrOnError.next)) {
if (
observerOrOnNextOrOnError.error &&
!isFunction(observerOrOnNextOrOnError.error)
) {
throw new Error(
'DocumentReference.onSnapshot failed: Observer.error must be a valid function.'
);
}
observer = {
next: observerOrOnNextOrOnError.next,
error: observerOrOnNextOrOnError.error,
};
} else {
throw new Error(
'DocumentReference.onSnapshot failed: Observer.next must be a valid function.'
);
}
} else {
throw new Error(
'DocumentReference.onSnapshot failed: Second argument must be a function or observer.'
);
}
} else {
throw new Error(
'DocumentReference.onSnapshot failed: First argument must be a function, observer or options.'
);
}
} else {
throw new Error(
'DocumentReference.onSnapshot failed: Called with invalid arguments.'
);
}
const listenerId = firestoreAutoId();
const listener = (nativeDocumentSnapshot: NativeDocumentSnapshot) => {
const documentSnapshot = new DocumentSnapshot(
this.firestore,
nativeDocumentSnapshot
);
observer.next(documentSnapshot);
};
// Listen to snapshot events
SharedEventEmitter.addListener(
getAppEventName(this._firestore, `onDocumentSnapshot:${listenerId}`),
listener
);
// Listen for snapshot error events
if (observer.error) {
SharedEventEmitter.addListener(
getAppEventName(
this._firestore,
`onDocumentSnapshotError:${listenerId}`
),
observer.error
);
}
// Add the native listener
getNativeModule(this._firestore).documentOnSnapshot(
this.path,
listenerId,
docListenOptions
);
// Return an unsubscribe method
return this._offDocumentSnapshot.bind(this, listenerId, listener);
}
set(data: Object, options?: SetOptions): Promise<void> {
const nativeData = buildNativeMap(data);
return getNativeModule(this._firestore).documentSet(
this.path,
nativeData,
options
);
}
update(...args: any[]): Promise<void> {
const data = parseUpdateArgs(args, 'DocumentReference.update');
const nativeData = buildNativeMap(data);
return getNativeModule(this._firestore).documentUpdate(
this.path,
nativeData
);
}
/**
* INTERNALS
*/
/**
* Remove document snapshot listener
* @param listener
*/
_offDocumentSnapshot(listenerId: string, listener: Function) {
getLogger(this._firestore).info('Removing onDocumentSnapshot listener');
SharedEventEmitter.removeListener(
getAppEventName(this._firestore, `onDocumentSnapshot:${listenerId}`),
listener
);
SharedEventEmitter.removeListener(
getAppEventName(this._firestore, `onDocumentSnapshotError:${listenerId}`),
listener
);
getNativeModule(this._firestore).documentOffSnapshot(this.path, listenerId);
}
}

View File

@ -0,0 +1,68 @@
/**
* @flow
* DocumentSnapshot representation wrapper
*/
import DocumentReference from './DocumentReference';
import FieldPath from './FieldPath';
import Path from './Path';
import { isObject } from '../../utils';
import { parseNativeMap } from './utils/serialize';
import type Firestore from './';
import type { NativeDocumentSnapshot, SnapshotMetadata } from './types';
const extractFieldPathData = (data: Object | void, segments: string[]): any => {
if (!data || !isObject(data)) {
return undefined;
}
const pathValue = data[segments[0]];
if (segments.length === 1) {
return pathValue;
}
return extractFieldPathData(pathValue, segments.slice(1));
};
/**
* @class DocumentSnapshot
*/
export default class DocumentSnapshot {
_data: Object | void;
_metadata: SnapshotMetadata;
_ref: DocumentReference;
constructor(firestore: Firestore, nativeData: NativeDocumentSnapshot) {
this._data = parseNativeMap(firestore, nativeData.data);
this._metadata = nativeData.metadata;
this._ref = new DocumentReference(
firestore,
Path.fromName(nativeData.path)
);
}
get exists(): boolean {
return this._data !== undefined;
}
get id(): string | null {
return this._ref.id;
}
get metadata(): SnapshotMetadata {
return this._metadata;
}
get ref(): DocumentReference {
return this._ref;
}
data(): Object | void {
return this._data;
}
get(fieldPath: string | FieldPath): any {
if (fieldPath instanceof FieldPath) {
return extractFieldPathData(this._data, fieldPath._segments);
}
return this._data ? this._data[fieldPath] : undefined;
}
}

View File

@ -0,0 +1,22 @@
/**
* @flow
* FieldPath representation wrapper
*/
/**
* @class FieldPath
*/
export default class FieldPath {
_segments: string[];
constructor(...segments: string[]) {
// TODO: Validation
this._segments = segments;
}
static documentId(): FieldPath {
return DOCUMENT_ID;
}
}
export const DOCUMENT_ID = new FieldPath('__name__');

View File

@ -0,0 +1,17 @@
/**
* @flow
* FieldValue representation wrapper
*/
export default class FieldValue {
static delete(): FieldValue {
return DELETE_FIELD_VALUE;
}
static serverTimestamp(): FieldValue {
return SERVER_TIMESTAMP_FIELD_VALUE;
}
}
export const DELETE_FIELD_VALUE = new FieldValue();
export const SERVER_TIMESTAMP_FIELD_VALUE = new FieldValue();

View File

@ -0,0 +1,29 @@
/**
* @flow
* GeoPoint representation wrapper
*/
/**
* @class GeoPoint
*/
export default class GeoPoint {
_latitude: number;
_longitude: number;
constructor(latitude: number, longitude: number) {
// TODO: Validation
// validate.isNumber('latitude', latitude);
// validate.isNumber('longitude', longitude);
this._latitude = latitude;
this._longitude = longitude;
}
get latitude(): number {
return this._latitude;
}
get longitude(): number {
return this._longitude;
}
}

View File

@ -0,0 +1,50 @@
/**
* @flow
* Path representation wrapper
*/
/**
* @class Path
*/
export default class Path {
_parts: string[];
constructor(pathComponents: string[]) {
this._parts = pathComponents;
}
get id(): string | null {
return this._parts.length > 0 ? this._parts[this._parts.length - 1] : null;
}
get isDocument(): boolean {
return this._parts.length > 0 && this._parts.length % 2 === 0;
}
get isCollection(): boolean {
return this._parts.length % 2 === 1;
}
get relativeName(): string {
return this._parts.join('/');
}
child(relativePath: string): Path {
return new Path(this._parts.concat(relativePath.split('/')));
}
parent(): Path | null {
return this._parts.length > 0
? new Path(this._parts.slice(0, this._parts.length - 1))
: null;
}
/**
*
* @package
*/
static fromName(name: string): Path {
const parts = name.split('/');
return parts.length === 0 ? new Path([]) : new Path(parts);
}
}

View File

@ -0,0 +1,459 @@
/**
* @flow
* Query representation wrapper
*/
import DocumentSnapshot from './DocumentSnapshot';
import FieldPath from './FieldPath';
import QuerySnapshot from './QuerySnapshot';
import { buildNativeArray, buildTypeMap } from './utils/serialize';
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import { getLogger } from '../../utils/log';
import { firestoreAutoId, isFunction, isObject } from '../../utils';
import { getNativeModule } from '../../utils/native';
import type Firestore from './';
import type Path from './Path';
import type {
QueryDirection,
QueryOperator,
QueryListenOptions,
} from './types';
const DIRECTIONS: { [QueryDirection]: string } = {
ASC: 'ASCENDING',
asc: 'ASCENDING',
DESC: 'DESCENDING',
desc: 'DESCENDING',
};
const OPERATORS: { [QueryOperator]: string } = {
'=': 'EQUAL',
'==': 'EQUAL',
'>': 'GREATER_THAN',
'>=': 'GREATER_THAN_OR_EQUAL',
'<': 'LESS_THAN',
'<=': 'LESS_THAN_OR_EQUAL',
};
type NativeFieldPath = {|
elements?: string[],
string?: string,
type: 'fieldpath' | 'string',
|};
type FieldFilter = {|
fieldPath: NativeFieldPath,
operator: string,
value: any,
|};
type FieldOrder = {|
direction: string,
fieldPath: NativeFieldPath,
|};
type QueryOptions = {
endAt?: any[],
endBefore?: any[],
limit?: number,
offset?: number,
selectFields?: string[],
startAfter?: any[],
startAt?: any[],
};
export type ObserverOnError = Object => void;
export type ObserverOnNext = QuerySnapshot => void;
export type Observer = {
error?: ObserverOnError,
next: ObserverOnNext,
};
const buildNativeFieldPath = (
fieldPath: string | FieldPath
): NativeFieldPath => {
if (fieldPath instanceof FieldPath) {
return {
elements: fieldPath._segments,
type: 'fieldpath',
};
}
return {
string: fieldPath,
type: 'string',
};
};
/**
* @class Query
*/
export default class Query {
_fieldFilters: FieldFilter[];
_fieldOrders: FieldOrder[];
_firestore: Firestore;
_iid: number;
_queryOptions: QueryOptions;
_referencePath: Path;
constructor(
firestore: Firestore,
path: Path,
fieldFilters?: FieldFilter[],
fieldOrders?: FieldOrder[],
queryOptions?: QueryOptions
) {
this._fieldFilters = fieldFilters || [];
this._fieldOrders = fieldOrders || [];
this._firestore = firestore;
this._queryOptions = queryOptions || {};
this._referencePath = path;
}
get firestore(): Firestore {
return this._firestore;
}
endAt(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
endAt: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
endBefore(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
endBefore: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
get(): Promise<QuerySnapshot> {
return getNativeModule(this._firestore)
.collectionGet(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions
)
.then(nativeData => new QuerySnapshot(this._firestore, this, nativeData));
}
limit(limit: number): Query {
// TODO: Validation
// validate.isInteger('n', n);
const options = {
...this._queryOptions,
limit,
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
onSnapshot(
optionsOrObserverOrOnNext: QueryListenOptions | Observer | ObserverOnNext,
observerOrOnNextOrOnError?: Observer | ObserverOnNext | ObserverOnError,
onError?: ObserverOnError
) {
let observer: Observer;
let queryListenOptions = {};
// Called with: onNext, ?onError
if (isFunction(optionsOrObserverOrOnNext)) {
if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) {
throw new Error(
'Query.onSnapshot failed: Second argument must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: optionsOrObserverOrOnNext,
error: observerOrOnNextOrOnError,
};
} else if (
optionsOrObserverOrOnNext &&
isObject(optionsOrObserverOrOnNext)
) {
// Called with: Observer
if (optionsOrObserverOrOnNext.next) {
if (isFunction(optionsOrObserverOrOnNext.next)) {
if (
optionsOrObserverOrOnNext.error &&
!isFunction(optionsOrObserverOrOnNext.error)
) {
throw new Error(
'Query.onSnapshot failed: Observer.error must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: optionsOrObserverOrOnNext.next,
error: optionsOrObserverOrOnNext.error,
};
} else {
throw new Error(
'Query.onSnapshot failed: Observer.next must be a valid function.'
);
}
} else if (
Object.prototype.hasOwnProperty.call(
optionsOrObserverOrOnNext,
'includeDocumentMetadataChanges'
) ||
Object.prototype.hasOwnProperty.call(
optionsOrObserverOrOnNext,
'includeQueryMetadataChanges'
)
) {
queryListenOptions = optionsOrObserverOrOnNext;
// Called with: Options, onNext, ?onError
if (isFunction(observerOrOnNextOrOnError)) {
if (onError && !isFunction(onError)) {
throw new Error(
'Query.onSnapshot failed: Third argument must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: observerOrOnNextOrOnError,
error: onError,
};
// Called with Options, Observer
} else if (
observerOrOnNextOrOnError &&
isObject(observerOrOnNextOrOnError) &&
observerOrOnNextOrOnError.next
) {
if (isFunction(observerOrOnNextOrOnError.next)) {
if (
observerOrOnNextOrOnError.error &&
!isFunction(observerOrOnNextOrOnError.error)
) {
throw new Error(
'Query.onSnapshot failed: Observer.error must be a valid function.'
);
}
observer = {
next: observerOrOnNextOrOnError.next,
error: observerOrOnNextOrOnError.error,
};
} else {
throw new Error(
'Query.onSnapshot failed: Observer.next must be a valid function.'
);
}
} else {
throw new Error(
'Query.onSnapshot failed: Second argument must be a function or observer.'
);
}
} else {
throw new Error(
'Query.onSnapshot failed: First argument must be a function, observer or options.'
);
}
} else {
throw new Error(
'Query.onSnapshot failed: Called with invalid arguments.'
);
}
const listenerId = firestoreAutoId();
const listener = nativeQuerySnapshot => {
const querySnapshot = new QuerySnapshot(
this._firestore,
this,
nativeQuerySnapshot
);
observer.next(querySnapshot);
};
// Listen to snapshot events
SharedEventEmitter.addListener(
getAppEventName(this._firestore, `onQuerySnapshot:${listenerId}`),
listener
);
// Listen for snapshot error events
if (observer.error) {
SharedEventEmitter.addListener(
getAppEventName(this._firestore, `onQuerySnapshotError:${listenerId}`),
observer.error
);
}
// Add the native listener
getNativeModule(this._firestore).collectionOnSnapshot(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId,
queryListenOptions
);
// Return an unsubscribe method
return this._offCollectionSnapshot.bind(this, listenerId, listener);
}
orderBy(
fieldPath: string | FieldPath,
directionStr?: QueryDirection = 'asc'
): Query {
// TODO: Validation
// validate.isFieldPath('fieldPath', fieldPath);
// validate.isOptionalFieldOrder('directionStr', directionStr);
if (
this._queryOptions.startAt ||
this._queryOptions.startAfter ||
this._queryOptions.endAt ||
this._queryOptions.endBefore
) {
throw new Error(
'Cannot specify an orderBy() constraint after calling ' +
'startAt(), startAfter(), endBefore() or endAt().'
);
}
const newOrder: FieldOrder = {
direction: DIRECTIONS[directionStr],
fieldPath: buildNativeFieldPath(fieldPath),
};
const combinedOrders = this._fieldOrders.concat(newOrder);
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
combinedOrders,
this._queryOptions
);
}
startAfter(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
startAfter: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
startAt(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
startAt: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
where(
fieldPath: string | FieldPath,
opStr: QueryOperator,
value: any
): Query {
// TODO: Validation
// validate.isFieldPath('fieldPath', fieldPath);
// validate.isFieldFilter('fieldFilter', opStr, value);
const nativeValue = buildTypeMap(value);
const newFilter: FieldFilter = {
fieldPath: buildNativeFieldPath(fieldPath),
operator: OPERATORS[opStr],
value: nativeValue,
};
const combinedFilters = this._fieldFilters.concat(newFilter);
return new Query(
this.firestore,
this._referencePath,
combinedFilters,
this._fieldOrders,
this._queryOptions
);
}
/**
* INTERNALS
*/
_buildOrderByOption(snapshotOrVarArgs: any[]) {
// TODO: Validation
let values;
if (
snapshotOrVarArgs.length === 1 &&
snapshotOrVarArgs[0] instanceof DocumentSnapshot
) {
const docSnapshot: DocumentSnapshot = snapshotOrVarArgs[0];
values = [];
for (let i = 0; i < this._fieldOrders.length; i++) {
const fieldOrder = this._fieldOrders[i];
if (
fieldOrder.fieldPath.type === 'string' &&
fieldOrder.fieldPath.string
) {
values.push(docSnapshot.get(fieldOrder.fieldPath.string));
} else if (fieldOrder.fieldPath.fieldpath) {
const fieldPath = new FieldPath(...fieldOrder.fieldPath.fieldpath);
values.push(docSnapshot.get(fieldPath));
}
}
} else {
values = snapshotOrVarArgs;
}
return buildNativeArray(values);
}
/**
* Remove query snapshot listener
* @param listener
*/
_offCollectionSnapshot(listenerId: string, listener: Function) {
getLogger(this._firestore).info('Removing onQuerySnapshot listener');
SharedEventEmitter.removeListener(
getAppEventName(this._firestore, `onQuerySnapshot:${listenerId}`),
listener
);
SharedEventEmitter.removeListener(
getAppEventName(this._firestore, `onQuerySnapshotError:${listenerId}`),
listener
);
getNativeModule(this._firestore).collectionOffSnapshot(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId
);
}
}

View File

@ -0,0 +1,78 @@
/**
* @flow
* QuerySnapshot representation wrapper
*/
import DocumentChange from './DocumentChange';
import DocumentSnapshot from './DocumentSnapshot';
import type Firestore from './';
import type {
NativeDocumentChange,
NativeDocumentSnapshot,
SnapshotMetadata,
} from './types';
import type Query from './Query';
type NativeQuerySnapshot = {
changes: NativeDocumentChange[],
documents: NativeDocumentSnapshot[],
metadata: SnapshotMetadata,
};
/**
* @class QuerySnapshot
*/
export default class QuerySnapshot {
_changes: DocumentChange[];
_docs: DocumentSnapshot[];
_metadata: SnapshotMetadata;
_query: Query;
constructor(
firestore: Firestore,
query: Query,
nativeData: NativeQuerySnapshot
) {
this._changes = nativeData.changes.map(
change => new DocumentChange(firestore, change)
);
this._docs = nativeData.documents.map(
doc => new DocumentSnapshot(firestore, doc)
);
this._metadata = nativeData.metadata;
this._query = query;
}
get docChanges(): DocumentChange[] {
return this._changes;
}
get docs(): DocumentSnapshot[] {
return this._docs;
}
get empty(): boolean {
return this._docs.length === 0;
}
get metadata(): SnapshotMetadata {
return this._metadata;
}
get query(): Query {
return this._query;
}
get size(): number {
return this._docs.length;
}
forEach(callback: DocumentSnapshot => any) {
// TODO: Validation
// validate.isFunction('callback', callback);
this._docs.forEach(doc => {
callback(doc);
});
}
}

View File

@ -0,0 +1,151 @@
/**
* @flow
* Firestore Transaction representation wrapper
*/
import { parseUpdateArgs } from './utils';
import { buildNativeMap } from './utils/serialize';
import type Firestore from './';
import type { TransactionMeta } from './TransactionHandler';
import type DocumentReference from './DocumentReference';
import DocumentSnapshot from './DocumentSnapshot';
import { getNativeModule } from '../../utils/native';
type Command = {
type: 'set' | 'update' | 'delete',
path: string,
data?: { [string]: any },
options?: SetOptions | {},
};
type SetOptions = {
merge: boolean,
};
// TODO docs state all get requests must be made FIRST before any modifications
// TODO so need to validate that
/**
* @class Transaction
*/
export default class Transaction {
_pendingResult: ?any;
_firestore: Firestore;
_meta: TransactionMeta;
_commandBuffer: Array<Command>;
constructor(firestore: Firestore, meta: TransactionMeta) {
this._meta = meta;
this._commandBuffer = [];
this._firestore = firestore;
this._pendingResult = undefined;
}
/**
* -------------
* INTERNAL API
* -------------
*/
/**
* Clears the command buffer and any pending result in prep for
* the next transaction iteration attempt.
*
* @private
*/
_prepare() {
this._commandBuffer = [];
this._pendingResult = undefined;
}
/**
* -------------
* PUBLIC API
* -------------
*/
/**
* Reads the document referenced by the provided DocumentReference.
*
* @param documentRef DocumentReference A reference to the document to be retrieved. Value must not be null.
*
* @returns Promise<DocumentSnapshot>
*/
get(documentRef: DocumentReference): Promise<DocumentSnapshot> {
// todo validate doc ref
return getNativeModule(this._firestore)
.transactionGetDocument(this._meta.id, documentRef.path)
.then(result => new DocumentSnapshot(this._firestore, result));
}
/**
* Writes to the document referred to by the provided DocumentReference.
* If the document does not exist yet, it will be created. If you pass options,
* the provided data can be merged into the existing document.
*
* @param documentRef DocumentReference A reference to the document to be created. Value must not be null.
* @param data Object An object of the fields and values for the document.
* @param options SetOptions An object to configure the set behavior.
* Pass {merge: true} to only replace the values specified in the data argument.
* Fields omitted will remain untouched.
*
* @returns {Transaction}
*/
set(
documentRef: DocumentReference,
data: Object,
options?: SetOptions
): Transaction {
// todo validate doc ref
// todo validate data is object
this._commandBuffer.push({
type: 'set',
path: documentRef.path,
data: buildNativeMap(data),
options: options || {},
});
return this;
}
/**
* 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 updated by providing dot-separated field path strings or by providing FieldPath objects.
*
* @param documentRef DocumentReference A reference to the document to be updated. Value must not be null.
* @param args any Either an object containing all of the fields and values to update,
* or a series of arguments alternating between fields (as string or FieldPath
* objects) and values.
*
* @returns {Transaction}
*/
update(documentRef: DocumentReference, ...args: Array<any>): Transaction {
// todo validate doc ref
const data = parseUpdateArgs(args, 'Transaction.update');
this._commandBuffer.push({
type: 'update',
path: documentRef.path,
data: buildNativeMap(data),
});
return this;
}
/**
* Deletes the document referred to by the provided DocumentReference.
*
* @param documentRef DocumentReference A reference to the document to be deleted. Value must not be null.
*
* @returns {Transaction}
*/
delete(documentRef: DocumentReference): Transaction {
// todo validate doc ref
this._commandBuffer.push({
type: 'delete',
path: documentRef.path,
});
return this;
}
}

View File

@ -0,0 +1,241 @@
/**
* @flow
* Firestore Transaction representation wrapper
*/
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import { getNativeModule } from '../../utils/native';
import Transaction from './Transaction';
import type Firestore from './';
let transactionId = 0;
/**
* Uses the push id generator to create a transaction id
* @returns {number}
* @private
*/
const generateTransactionId = (): number => transactionId++;
export type TransactionMeta = {
id: number,
stack: string[],
reject?: Function,
resolve?: Function,
transaction: Transaction,
updateFunction: (transaction: Transaction) => Promise<any>,
};
type TransactionEvent = {
id: number,
type: 'update' | 'error' | 'complete',
error: ?{ code: string, message: string },
};
/**
* @class TransactionHandler
*/
export default class TransactionHandler {
_firestore: Firestore;
_pending: {
[number]: {
meta: TransactionMeta,
transaction: Transaction,
},
};
constructor(firestore: Firestore) {
this._pending = {};
this._firestore = firestore;
SharedEventEmitter.addListener(
getAppEventName(this._firestore, 'firestore_transaction_event'),
this._handleTransactionEvent.bind(this)
);
}
/**
* -------------
* INTERNAL API
* -------------
*/
/**
* Add a new transaction and start it natively.
* @param updateFunction
*/
_add(
updateFunction: (transaction: Transaction) => Promise<any>
): Promise<any> {
const id = generateTransactionId();
// $FlowExpectedError: Transaction has to be populated
const meta: TransactionMeta = {
id,
updateFunction,
stack: new Error().stack
.split('\n')
.slice(4)
.join('\n'),
};
this._pending[id] = {
meta,
transaction: new Transaction(this._firestore, meta),
};
// deferred promise
return new Promise((resolve, reject) => {
getNativeModule(this._firestore).transactionBegin(id);
meta.resolve = r => {
resolve(r);
this._remove(id);
};
meta.reject = e => {
reject(e);
this._remove(id);
};
});
}
/**
* Destroys a local instance of a transaction meta
*
* @param id
* @private
*/
_remove(id) {
getNativeModule(this._firestore).transactionDispose(id);
delete this._pending[id];
}
/**
* -------------
* EVENTS
* -------------
*/
/**
* Handles incoming native transaction events and distributes to correct
* internal handler by event.type
*
* @param event
* @returns {*}
* @private
*/
_handleTransactionEvent(event: TransactionEvent) {
// eslint-disable-next-line default-case
switch (event.type) {
case 'update':
this._handleUpdate(event);
break;
case 'error':
this._handleError(event);
break;
case 'complete':
this._handleComplete(event);
break;
}
}
/**
* Handles incoming native transaction update events
*
* @param event
* @private
*/
async _handleUpdate(event: TransactionEvent) {
const { id } = event;
// abort if no longer exists js side
if (!this._pending[id]) return this._remove(id);
const { meta, transaction } = this._pending[id];
const { updateFunction, reject } = meta;
// clear any saved state from previous transaction runs
transaction._prepare();
let finalError;
let updateFailed;
let pendingResult;
// run the users custom update functionality
try {
const possiblePromise = updateFunction(transaction);
// validate user has returned a promise in their update function
// TODO must it actually return a promise? Can't find any usages of it without one...
if (!possiblePromise || !possiblePromise.then) {
finalError = new Error(
'Update function for `firestore.runTransaction(updateFunction)` must return a Promise.'
);
} else {
pendingResult = await possiblePromise;
}
} catch (exception) {
// exception can still be falsey if user `Promise.reject();` 's with no args
// so we track the exception with a updateFailed boolean to ensure no fall-through
updateFailed = true;
finalError = exception;
}
// reject the final promise and remove from native
// update is failed when either the users updateFunction
// throws an error or rejects a promise
if (updateFailed) {
// $FlowExpectedError: Reject will always be present
return reject(finalError);
}
// capture the resolved result as we'll need this
// to resolve the runTransaction() promise when
// native emits that the transaction is final
transaction._pendingResult = pendingResult;
// send the buffered update/set/delete commands for native to process
return getNativeModule(this._firestore).transactionApplyBuffer(
id,
transaction._commandBuffer
);
}
/**
* Handles incoming native transaction error events
*
* @param event
* @private
*/
_handleError(event: TransactionEvent) {
const { id, error } = event;
const { meta } = this._pending[id];
if (meta && error) {
const { code, message } = error;
// build a JS error and replace its stack
// with the captured one at start of transaction
// so it's actually relevant to the user
const errorWithStack = new Error(message);
// $FlowExpectedError: code is needed for Firebase errors
errorWithStack.code = code;
// $FlowExpectedError: stack should be a stack trace
errorWithStack.stack = meta.stack;
// $FlowExpectedError: Reject will always be present
meta.reject(errorWithStack);
}
}
/**
* Handles incoming native transaction complete events
*
* @param event
* @private
*/
_handleComplete(event: TransactionEvent) {
const { id } = event;
const { meta, transaction } = this._pending[id];
if (meta) {
const pendingResult = transaction._pendingResult;
// $FlowExpectedError: Resolve will always be present
meta.resolve(pendingResult);
}
}
}

View File

@ -0,0 +1,76 @@
/**
* @flow
* WriteBatch representation wrapper
*/
import { parseUpdateArgs } from './utils';
import { buildNativeMap } from './utils/serialize';
import { getNativeModule } from '../../utils/native';
import type DocumentReference from './DocumentReference';
import type Firestore from './';
import type { SetOptions } from './types';
type DocumentWrite = {
data?: Object,
options?: Object,
path: string,
type: 'DELETE' | 'SET' | 'UPDATE',
};
/**
* @class WriteBatch
*/
export default class WriteBatch {
_firestore: Firestore;
_writes: DocumentWrite[];
constructor(firestore: Firestore) {
this._firestore = firestore;
this._writes = [];
}
commit(): Promise<void> {
return getNativeModule(this._firestore).documentBatch(this._writes);
}
delete(docRef: DocumentReference): WriteBatch {
// TODO: Validation
// validate.isDocumentReference('docRef', docRef);
// validate.isOptionalPrecondition('deleteOptions', deleteOptions);
this._writes.push({
path: docRef.path,
type: 'DELETE',
});
return this;
}
set(docRef: DocumentReference, data: Object, options?: SetOptions) {
// TODO: Validation
// validate.isDocumentReference('docRef', docRef);
// validate.isDocument('data', data);
// validate.isOptionalPrecondition('options', writeOptions);
const nativeData = buildNativeMap(data);
this._writes.push({
data: nativeData,
options,
path: docRef.path,
type: 'SET',
});
return this;
}
update(docRef: DocumentReference, ...args: any[]): WriteBatch {
// TODO: Validation
// validate.isDocumentReference('docRef', docRef);
const data = parseUpdateArgs(args, 'WriteBatch.update');
this._writes.push({
data: buildNativeMap(data),
path: docRef.path,
type: 'UPDATE',
});
return this;
}
}

View File

@ -0,0 +1,247 @@
/**
* @flow
* Firestore representation wrapper
*/
import { NativeModules } from 'react-native';
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import ModuleBase from '../../utils/ModuleBase';
import CollectionReference from './CollectionReference';
import DocumentReference from './DocumentReference';
import FieldPath from './FieldPath';
import FieldValue from './FieldValue';
import GeoPoint from './GeoPoint';
import Path from './Path';
import WriteBatch from './WriteBatch';
import TransactionHandler from './TransactionHandler';
import Transaction from './Transaction';
import INTERNALS from '../../utils/internals';
import type DocumentSnapshot from './DocumentSnapshot';
import type App from '../core/app';
import type QuerySnapshot from './QuerySnapshot';
type CollectionSyncEvent = {
appName: string,
querySnapshot?: QuerySnapshot,
error?: Object,
listenerId: string,
path: string,
};
type DocumentSyncEvent = {
appName: string,
documentSnapshot?: DocumentSnapshot,
error?: Object,
listenerId: string,
path: string,
};
const NATIVE_EVENTS = [
'firestore_transaction_event',
'firestore_document_sync_event',
'firestore_collection_sync_event',
];
export const MODULE_NAME = 'RNFirebaseFirestore';
export const NAMESPACE = 'firestore';
/**
* @class Firestore
*/
export default class Firestore extends ModuleBase {
_referencePath: Path;
_transactionHandler: TransactionHandler;
constructor(app: App) {
super(app, {
events: NATIVE_EVENTS,
moduleName: MODULE_NAME,
multiApp: true,
hasShards: false,
namespace: NAMESPACE,
});
this._referencePath = new Path([]);
this._transactionHandler = new TransactionHandler(this);
SharedEventEmitter.addListener(
// sub to internal native event - this fans out to
// public event name: onCollectionSnapshot
getAppEventName(this, 'firestore_collection_sync_event'),
this._onCollectionSyncEvent.bind(this)
);
SharedEventEmitter.addListener(
// sub to internal native event - this fans out to
// public event name: onDocumentSnapshot
getAppEventName(this, 'firestore_document_sync_event'),
this._onDocumentSyncEvent.bind(this)
);
}
/**
* -------------
* PUBLIC API
* -------------
*/
/**
* Creates a write batch, used for performing multiple writes as a single atomic operation.
*
* @returns {WriteBatch}
*/
batch(): WriteBatch {
return new WriteBatch(this);
}
/**
* Gets a CollectionReference instance that refers to the collection at the specified path.
*
* @param collectionPath
* @returns {CollectionReference}
*/
collection(collectionPath: string): CollectionReference {
const path = this._referencePath.child(collectionPath);
if (!path.isCollection) {
throw new Error('Argument "collectionPath" must point to a collection.');
}
return new CollectionReference(this, path);
}
/**
* Gets a DocumentReference instance that refers to the document at the specified path.
*
* @param documentPath
* @returns {DocumentReference}
*/
doc(documentPath: string): DocumentReference {
const path = this._referencePath.child(documentPath);
if (!path.isDocument) {
throw new Error('Argument "documentPath" must point to a document.');
}
return new DocumentReference(this, path);
}
/**
* 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.
*
* @param updateFunction
* @returns {void|Promise<any>}
*/
runTransaction(
updateFunction: (transaction: Transaction) => Promise<any>
): Promise<any> {
return this._transactionHandler._add(updateFunction);
}
/**
* -------------
* UNSUPPORTED
* -------------
*/
setLogLevel(): void {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'firestore',
'setLogLevel'
)
);
}
enableNetwork(): void {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'firestore',
'enableNetwork'
)
);
}
disableNetwork(): void {
throw new Error(
INTERNALS.STRINGS.ERROR_UNSUPPORTED_MODULE_METHOD(
'firestore',
'disableNetwork'
)
);
}
/**
* -------------
* MISC
* -------------
*/
enablePersistence(): Promise<void> {
throw new Error('Persistence is enabled by default on the Firestore SDKs');
}
settings(): void {
throw new Error('firebase.firestore().settings() coming soon');
}
/**
* -------------
* INTERNALS
* -------------
*/
/**
* Internal collection sync listener
*
* @param event
* @private
*/
_onCollectionSyncEvent(event: CollectionSyncEvent) {
if (event.error) {
SharedEventEmitter.emit(
getAppEventName(this, `onQuerySnapshotError:${event.listenerId}`),
event.error
);
} else {
SharedEventEmitter.emit(
getAppEventName(this, `onQuerySnapshot:${event.listenerId}`),
event.querySnapshot
);
}
}
/**
* Internal document sync listener
*
* @param event
* @private
*/
_onDocumentSyncEvent(event: DocumentSyncEvent) {
if (event.error) {
SharedEventEmitter.emit(
getAppEventName(this, `onDocumentSnapshotError:${event.listenerId}`),
event.error
);
} else {
SharedEventEmitter.emit(
getAppEventName(this, `onDocumentSnapshot:${event.listenerId}`),
event.documentSnapshot
);
}
}
}
export const statics = {
FieldPath,
FieldValue,
GeoPoint,
enableLogging(enabled: boolean) {
if (NativeModules[MODULE_NAME]) {
NativeModules[MODULE_NAME].enableLogging(enabled);
}
},
};

Some files were not shown because too many files have changed in this diff Show More