Merge pull request #1056 from invertase/functions

Functions Implementation
This commit is contained in:
Michael Diarmid
2018-05-06 15:50:12 +01:00
committed by GitHub
28 changed files with 4763 additions and 163 deletions

1
.gitignore vendored
View File

@@ -96,3 +96,4 @@ tests/ios/Fabric.framework/Fabric
bridge/android/app/.classpath
bridge/android/app/.project
**/.vscode
bridge/functions/firebase-debug.log

View File

@@ -102,6 +102,7 @@ dependencies {
compileOnly "com.google.firebase:firebase-ads:$firebaseVersion"
compileOnly "com.google.firebase:firebase-firestore:$firebaseVersion"
compileOnly "com.google.firebase:firebase-invites:$firebaseVersion"
compileOnly "com.google.firebase:firebase-functions:$firebaseVersion"
compileOnly('com.crashlytics.sdk.android:crashlytics:2.9.1@aar') {
transitive = true
}

View File

@@ -4,38 +4,24 @@ import android.app.ActivityManager;
import android.content.Context;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import javax.annotation.Nullable;
@SuppressWarnings("WeakerAccess")
public class Utils {
private static final String TAG = "Utils";
// TODO NOTE
public static void todoNote(final String tag, final String name, final Callback callback) {
Log.e(tag, "The method " + name + " has not yet been implemented.");
Log.e(tag, "Feel free to contribute to finish the method in the source.");
WritableMap errorMap = Arguments.createMap();
errorMap.putString("error", "unimplemented");
callback.invoke(null, errorMap);
}
/**
* send a JS event
**/
@@ -50,160 +36,114 @@ public class Utils {
}
/**
* @param key map key
* @param value map value
* @param map map
* Takes a value and calls the appropriate setter for its type on the target map + key
*
* @param key String key to set on target map
* @param value Object value to set on target map
* @param map WritableMap target map to write the value to
*/
public static void mapPutValue(String key, Object value, WritableMap map) {
String type = value != null ? value.getClass().getName() : "";
switch (type) {
case "java.lang.Boolean":
map.putBoolean(key, (Boolean) value);
break;
case "java.lang.Long":
Long longVal = (Long) value;
map.putDouble(key, (double) longVal);
break;
case "java.lang.Double":
map.putDouble(key, (Double) value);
break;
case "java.lang.String":
map.putString(key, (String) value);
break;
default:
map.putString(key, null);
@SuppressWarnings("unchecked")
public static void mapPutValue(String key, @Nullable Object value, WritableMap map) {
if (value == null) {
map.putNull(key);
} else {
String type = value.getClass().getName();
switch (type) {
case "java.lang.Boolean":
map.putBoolean(key, (Boolean) value);
break;
case "java.lang.Long":
Long longVal = (Long) value;
map.putDouble(key, (double) longVal);
break;
case "java.lang.Float":
float floatVal = (float) value;
map.putDouble(key, (double) floatVal);
break;
case "java.lang.Double":
map.putDouble(key, (Double) value);
break;
case "java.lang.Integer":
map.putInt(key, (int) value);
break;
case "java.lang.String":
map.putString(key, (String) value);
break;
default:
if (List.class.isAssignableFrom(value.getClass())) {
map.putArray(key, Arguments.makeNativeArray((List<Object>) value));
} else if (Map.class.isAssignableFrom(value.getClass())) {
map.putMap(key, Arguments.makeNativeMap((Map<String, Object>) value));
} else {
Log.d(TAG, "utils:mapPutValue:unknownType:" + type);
map.putNull(key);
}
}
}
}
/**
* Convert a ReadableMap to a WritableMap for the purposes of re-sending back to JS
* TODO This is now a legacy util - internally uses RN functionality
*
* @param map
* @return
* @param map ReadableMap
* @return WritableMap
*/
public static WritableMap readableMapToWritableMap(ReadableMap map) {
WritableMap writableMap = Arguments.createMap();
ReadableMapKeySetIterator iterator = map.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
ReadableType type = map.getType(key);
switch (type) {
case Null:
writableMap.putNull(key);
break;
case Boolean:
writableMap.putBoolean(key, map.getBoolean(key));
break;
case Number:
writableMap.putDouble(key, map.getDouble(key));
break;
case String:
writableMap.putString(key, map.getString(key));
break;
case Map:
writableMap.putMap(key, readableMapToWritableMap(map.getMap(key)));
break;
case Array:
// TODO writableMap.putArray(key, readableArrayToWritableArray(map.getArray(key)));
break;
default:
throw new IllegalArgumentException("Could not convert object with key: " + key + ".");
}
}
// https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java#L54
writableMap.merge(map);
return writableMap;
}
/**
* Convert a ReadableMap into a native Java Map
* TODO This is now a legacy util - internally uses RN functionality
*
* @param readableMap ReadableMap
* @return Map
*/
public static Map<String, Object> recursivelyDeconstructReadableMap(ReadableMap readableMap) {
Map<String, Object> deconstructedMap = new HashMap<>();
if (readableMap == null) {
return deconstructedMap;
}
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
ReadableType type = readableMap.getType(key);
switch (type) {
case Null:
deconstructedMap.put(key, null);
break;
case Boolean:
deconstructedMap.put(key, readableMap.getBoolean(key));
break;
case Number:
deconstructedMap.put(key, readableMap.getDouble(key));
break;
case String:
deconstructedMap.put(key, readableMap.getString(key));
break;
case Map:
deconstructedMap.put(key, Utils.recursivelyDeconstructReadableMap(readableMap.getMap(key)));
break;
case Array:
deconstructedMap.put(key, Utils.recursivelyDeconstructReadableArray(readableMap.getArray(key)));
break;
default:
throw new IllegalArgumentException("Could not convert object with key: " + key + ".");
}
}
return deconstructedMap;
// https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java#L216
return readableMap.toHashMap();
}
/**
* Convert a ReadableArray into a native Java Map
* TODO This is now a legacy util - internally uses RN functionality
*
* @param readableArray ReadableArray
* @return List<Object>
*/
public static List<Object> recursivelyDeconstructReadableArray(ReadableArray readableArray) {
List<Object> deconstructedList = new ArrayList<>(readableArray.size());
for (int i = 0; i < readableArray.size(); i++) {
ReadableType indexType = readableArray.getType(i);
switch (indexType) {
case Null:
deconstructedList.add(i, null);
break;
case Boolean:
deconstructedList.add(i, readableArray.getBoolean(i));
break;
case Number:
deconstructedList.add(i, readableArray.getDouble(i));
break;
case String:
deconstructedList.add(i, readableArray.getString(i));
break;
case Map:
deconstructedList.add(i, Utils.recursivelyDeconstructReadableMap(readableArray.getMap(i)));
break;
case Array:
deconstructedList.add(i, Utils.recursivelyDeconstructReadableArray(readableArray.getArray(i)));
break;
default:
throw new IllegalArgumentException("Could not convert object at index " + i + ".");
}
}
return deconstructedList;
// https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java#L175
return readableArray.toArrayList();
}
/**
* We need to check if app is in foreground otherwise the app will crash.
* http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
*
* @param context Context
* @return boolean
*/
public static boolean isAppInForeground(Context context) {
/**
We need to check if app is in foreground otherwise the app will crash.
http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
**/
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses =
activityManager.getRunningAppProcesses();
if (appProcesses == null) {
return false;
}
if (activityManager == null) return false;
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
if (appProcesses == null) return false;
final String packageName = context.getPackageName();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.importance ==
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
appProcess.processName.equals(packageName)) {
if (
appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
&& appProcess.processName.equals(packageName)
) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,104 @@
package io.invertase.firebase.functions;
import android.support.annotation.NonNull;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.WritableMap;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.functions.FirebaseFunctions;
import com.google.firebase.functions.FirebaseFunctionsException;
import com.google.firebase.functions.HttpsCallableReference;
import com.google.firebase.functions.HttpsCallableResult;
import io.invertase.firebase.Utils;
public class RNFirebaseFunctions extends ReactContextBaseJavaModule {
private static final String DATA_KEY = "data";
private static final String CODE_KEY = "code";
private static final String MSG_KEY = "message";
private static final String ERROR_KEY = "__error";
private static final String DETAILS_KEY = "details";
private static final String TAG = "RNFirebaseFunctions";
RNFirebaseFunctions(ReactApplicationContext reactContext) {
super(reactContext);
Log.d(TAG, "New instance");
}
@Override
public String getName() {
return TAG;
}
@ReactMethod
public void httpsCallable(final String name, ReadableMap wrapper, final Promise promise) {
Object input = wrapper.toHashMap().get(DATA_KEY);
Log.d(TAG, "function:call:input:" + name + ":" + (input != null ? input.toString() : "null"));
HttpsCallableReference httpsCallableReference = FirebaseFunctions
.getInstance()
.getHttpsCallable(name);
httpsCallableReference
.call(input)
.addOnSuccessListener(new OnSuccessListener<HttpsCallableResult>() {
@Override
public void onSuccess(HttpsCallableResult httpsCallableResult) {
WritableMap map = Arguments.createMap();
Object result = httpsCallableResult.getData();
Log.d(
TAG,
"function:call:onSuccess:" + name
);
Log.d(
TAG,
"function:call:onSuccess:result:type:" + name + ":" + (result != null ? result.getClass().getName() : "null")
);
Log.d(
TAG,
"function:call:onSuccess:result:data:" + name + ":" + (result != null ? result.toString() : "null")
);
Utils.mapPutValue(DATA_KEY, result, map);
promise.resolve(map);
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception exception) {
Log.d(TAG, "function:call:onFailure:" + name, exception);
String message;
Object details = null;
String code = "UNKNOWN";
WritableMap map = Arguments.createMap();
if (exception instanceof FirebaseFunctionsException) {
FirebaseFunctionsException ffe = (FirebaseFunctionsException) exception;
details = ffe.getDetails();
code = ffe.getCode().name();
message = ffe.getLocalizedMessage();
} else {
message = exception.getLocalizedMessage();
}
Utils.mapPutValue(CODE_KEY, code, map);
Utils.mapPutValue(MSG_KEY, message, map);
Utils.mapPutValue(ERROR_KEY, true, map);
Utils.mapPutValue(DETAILS_KEY, details, map);
promise.resolve(map);
}
});
}
}

View File

@@ -0,0 +1,37 @@
package io.invertase.firebase.functions;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.bridge.ReactApplicationContext;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
@SuppressWarnings("unused")
public class RNFirebaseFunctionsPackage implements ReactPackage {
public RNFirebaseFunctionsPackage() {
}
/**
* @param reactContext react application context that can be used to create modules
* @return list of native modules to register with the newly created catalyst instance
*/
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new RNFirebaseFunctions(reactContext));
return modules;
}
/**
* @param reactContext
* @return a list of view managers that should be registered with {@link UIManagerModule}
*/
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

5
bridge/.firebaserc Normal file
View File

@@ -0,0 +1,5 @@
{
"projects": {
"default": "rnfirebase-b9ad4"
}
}

View File

@@ -89,23 +89,24 @@ dependencies {
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') {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.google.android.gms:play-services-base:$firebaseVersion"
implementation "com.google.firebase:firebase-ads:$firebaseVersion"
implementation "com.google.firebase:firebase-auth:$firebaseVersion"
implementation "com.google.firebase:firebase-config:$firebaseVersion"
implementation "com.google.firebase:firebase-core:$firebaseVersion"
implementation "com.google.firebase:firebase-crash:$firebaseVersion"
implementation "com.google.firebase:firebase-database:$firebaseVersion"
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
implementation "com.google.firebase:firebase-perf:$firebaseVersion"
implementation "com.google.firebase:firebase-storage:$firebaseVersion"
implementation "com.google.firebase:firebase-firestore:$firebaseVersion"
implementation "com.google.firebase:firebase-invites:$firebaseVersion"
implementation "com.google.firebase:firebase-functions:$firebaseVersion"
implementation('com.crashlytics.sdk.android:crashlytics:2.9.1@aar') {
transitive = true
}
compile "com.android.support:appcompat-v7:27.1.0"
implementation "com.android.support:appcompat-v7:27.1.0"
implementation fileTree(dir: "libs", include: ["*.jar"])
androidTestImplementation(project(path: ":detox"))
androidTestImplementation 'junit:junit:4.12'

View File

@@ -18,6 +18,7 @@ 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.functions.RNFirebaseFunctionsPackage;
import io.invertase.firebase.instanceid.RNFirebaseInstanceIdPackage;
import io.invertase.firebase.invites.RNFirebaseInvitesPackage;
import io.invertase.firebase.links.RNFirebaseLinksPackage;
@@ -51,6 +52,7 @@ public class MainApplication extends Application implements ReactApplication {
new RNFirebaseCrashlyticsPackage(),
new RNFirebaseDatabasePackage(),
new RNFirebaseFirestorePackage(),
new RNFirebaseFunctionsPackage(),
new RNFirebaseInstanceIdPackage(),
new RNFirebaseInvitesPackage(),
new RNFirebaseLinksPackage(),

View File

@@ -0,0 +1,202 @@
const TEST_DATA = TestHelpers.functions.data;
describe('functions()', () => {
describe('httpsCallable(fnName)(args)', () => {
it('accepts primitive args: undefined', async () => {
const functionRunner = firebase.functions().httpsCallable('runTest');
const response = await functionRunner();
response.data.should.equal('null');
});
it('accepts primitive args: string', async () => {
const functionRunner = firebase.functions().httpsCallable('runTest');
const response = await functionRunner('hello');
response.data.should.equal('string');
});
it('accepts primitive args: number', async () => {
const functionRunner = firebase.functions().httpsCallable('runTest');
const response = await functionRunner(123);
response.data.should.equal('number');
});
it('accepts primitive args: boolean', async () => {
const functionRunner = firebase.functions().httpsCallable('runTest');
const response = await functionRunner(true);
response.data.should.equal('boolean');
});
it('accepts primitive args: null', async () => {
const functionRunner = firebase.functions().httpsCallable('runTest');
const response = await functionRunner(null);
response.data.should.equal('null');
});
it('accepts array args', async () => {
const functionRunner = firebase.functions().httpsCallable('runTest');
const response = await functionRunner([1, 2, 3, 4]);
response.data.should.equal('array');
});
it('accepts object args', async () => {
const type = 'simpleObject';
const inputData = TEST_DATA[type];
const functionRunner = firebase.functions().httpsCallable('runTest');
const { data: outputData } = await functionRunner({
type,
inputData,
});
should.deepEqual(outputData, inputData);
});
it('accepts complex nested objects', async () => {
const type = 'advancedObject';
const inputData = TEST_DATA[type];
const functionRunner = firebase.functions().httpsCallable('runTest');
const { data: outputData } = await functionRunner({
type,
inputData,
});
should.deepEqual(outputData, inputData);
});
it('accepts complex nested arrays', async () => {
const type = 'advancedArray';
const inputData = TEST_DATA[type];
const functionRunner = firebase.functions().httpsCallable('runTest');
const { data: outputData } = await functionRunner({
type,
inputData,
});
should.deepEqual(outputData, inputData);
});
});
describe('HttpsError', () => {
it('errors return instance of HttpsError', async () => {
const functionRunner = firebase.functions().httpsCallable('runTest');
try {
await functionRunner({});
return Promise.reject(new Error('Function did not reject with error.'));
} catch (e) {
should.equal(e.details, null);
e.code.should.equal('invalid-argument');
e.message.should.equal('Invalid test requested.');
}
return Promise.resolve();
});
it('HttpsError.details -> allows returning complex data', async () => {
let type = 'advancedObject';
let inputData = TEST_DATA[type];
const functionRunner = firebase.functions().httpsCallable('runTest');
try {
await functionRunner({
type,
inputData,
asError: true,
});
return Promise.reject(new Error('Function did not reject with error.'));
} catch (e) {
should.deepEqual(e.details, inputData);
e.code.should.equal('cancelled');
e.message.should.equal(
'Response data was requested to be sent as part of an Error payload, so here we are!'
);
}
type = 'advancedArray';
inputData = TEST_DATA[type];
try {
await functionRunner({
type,
inputData,
asError: true,
});
return Promise.reject(new Error('Function did not reject with error.'));
} catch (e) {
should.deepEqual(e.details, inputData);
e.code.should.equal('cancelled');
e.message.should.equal(
'Response data was requested to be sent as part of an Error payload, so here we are!'
);
}
return Promise.resolve();
});
it('HttpsError.details -> allows returning primitives', async () => {
let type = 'number';
let inputData = TEST_DATA[type];
const functionRunner = firebase.functions().httpsCallable('runTest');
try {
await functionRunner({
type,
inputData,
asError: true,
});
return Promise.reject(new Error('Function did not reject with error.'));
} catch (e) {
e.code.should.equal('cancelled');
e.message.should.equal(
'Response data was requested to be sent as part of an Error payload, so here we are!'
);
should.deepEqual(e.details, inputData);
}
type = 'string';
inputData = TEST_DATA[type];
try {
await functionRunner({
type,
inputData,
asError: true,
});
return Promise.reject(new Error('Function did not reject with error.'));
} catch (e) {
should.deepEqual(e.details, inputData);
e.code.should.equal('cancelled');
e.message.should.equal(
'Response data was requested to be sent as part of an Error payload, so here we are!'
);
}
type = 'boolean';
inputData = TEST_DATA[type];
try {
await functionRunner({
type,
inputData,
asError: true,
});
return Promise.reject(new Error('Function did not reject with error.'));
} catch (e) {
should.deepEqual(e.details, inputData);
e.code.should.equal('cancelled');
e.message.should.equal(
'Response data was requested to be sent as part of an Error payload, so here we are!'
);
}
type = 'null';
inputData = TEST_DATA[type];
try {
await functionRunner({
type,
inputData,
asError: true,
});
return Promise.reject(new Error('Function did not reject with error.'));
} catch (e) {
should.deepEqual(e.details, inputData);
e.code.should.equal('cancelled');
e.message.should.equal(
'Response data was requested to be sent as part of an Error payload, so here we are!'
);
}
return Promise.resolve();
});
});
});

View File

@@ -1,5 +1,6 @@
--recursive
--timeout 120000
--reporter list
--slow 600
--bail
--exit

1
bridge/firebase.json Normal file
View File

@@ -0,0 +1 @@
{}

64
bridge/functions/index.js Normal file
View File

@@ -0,0 +1,64 @@
const assert = require('assert');
const functions = require('firebase-functions');
const TEST_DATA = require('./test-data');
exports.runTest = functions.https.onCall(data => {
console.log(Date.now(), data);
if (typeof data === 'undefined') {
return 'undefined';
}
if (typeof data === 'string') {
return 'string';
}
if (typeof data === 'number') {
return 'number';
}
if (typeof data === 'boolean') {
return 'boolean';
}
if (data === null) {
return 'null';
}
if (Array.isArray(data)) {
return 'array';
}
const { type, asError, inputData } = data;
if (!Object.hasOwnProperty.call(TEST_DATA, type)) {
throw new functions.https.HttpsError(
'invalid-argument',
'Invalid test requested.'
);
}
const outputData = TEST_DATA[type];
try {
assert.deepEqual(outputData, inputData);
} catch (e) {
console.error(e);
throw new functions.https.HttpsError(
'invalid-argument',
'Input and Output types did not match.',
e.message
);
}
// all good
if (asError) {
throw new functions.https.HttpsError(
'cancelled',
'Response data was requested to be sent as part of an Error payload, so here we are!',
outputData
);
}
return outputData;
});

3866
bridge/functions/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"firebase-admin": "~5.12.0",
"firebase-functions": "^1.0.1"
},
"private": true
}

View File

@@ -0,0 +1,41 @@
module.exports = {
number: 1234,
string: 'acde',
boolean: true,
null: null,
simpleObject: {
number: 1234,
string: 'acde',
boolean: true,
null: null,
},
simpleArray: [1234, 'acde', true, null],
advancedObject: {
array: [1234, 'acde', false, null],
object: {
number: 1234,
string: 'acde',
boolean: true,
null: null,
array: [1234, 'acde', true, null],
},
number: 1234,
string: 'acde',
boolean: true,
null: null,
},
advancedArray: [
1234,
'acde',
true,
null,
[1234, 'acde', true, null],
{
number: 1234,
string: 'acde',
boolean: true,
null: null,
array: [1234, 'acde', true, null],
},
],
};

View File

@@ -94,6 +94,9 @@ console.log = (...args) => {
};
global.TestHelpers = {
functions: {
data: require('./../functions/test-data'),
},
firestore: require('./firestore'),
database: require('./database'),
};

View File

@@ -23,6 +23,7 @@ target 'testing' do
pod 'Firebase/Core'
pod 'Firebase/Crash'
pod 'Firebase/Database'
pod 'Firebase/Functions'
pod 'Firebase/DynamicLinks'
pod 'Firebase/Firestore'
pod 'Firebase/Invites'

View File

@@ -29,6 +29,9 @@ PODS:
- Firebase/Firestore (4.13.0):
- Firebase/Core
- FirebaseFirestore (= 0.11.0)
- Firebase/Functions (4.13.0):
- Firebase/Core
- FirebaseFunctions (= 1.0.0)
- Firebase/Invites (4.13.0):
- Firebase/Core
- FirebaseInvites (= 2.0.2)
@@ -76,6 +79,10 @@ PODS:
- gRPC-ProtoRPC (~> 1.0)
- leveldb-library (~> 1.18)
- Protobuf (~> 3.5)
- FirebaseFunctions (1.0.0):
- FirebaseAnalytics (~> 4.1)
- FirebaseCore (~> 4.0)
- GTMSessionFetcher/Core (~> 1.1)
- FirebaseInstanceID (2.0.10):
- FirebaseCore (~> 4.0)
- FirebaseInvites (2.0.2):
@@ -201,7 +208,8 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- RNFirebase (4.0.4):
- RNFirebase (4.0.7):
- Firebase/Core
- React
- yoga (0.55.3.React)
@@ -215,6 +223,7 @@ DEPENDENCIES:
- Firebase/Database
- Firebase/DynamicLinks
- Firebase/Firestore
- Firebase/Functions
- Firebase/Invites
- Firebase/Messaging
- Firebase/Performance
@@ -248,6 +257,7 @@ SPEC CHECKSUMS:
FirebaseDatabase: 5f0bc6134c5c237cf55f9e1249d406770a75eafd
FirebaseDynamicLinks: 38b68641d24e78d0277a9205d988ce22875d5a25
FirebaseFirestore: e92a096ce80c7b4b905d4e9d41dbd944adc9d2a5
FirebaseFunctions: 3745fada03bd706a9b5c0b9ae7b2d490fa594d21
FirebaseInstanceID: 8d20d890d65c917f9f7d9950b6e10a760ad34321
FirebaseInvites: ae15e0636f9eb42bdf5c1ef4c8f7bd4a88f9878b
FirebaseMessaging: 75cdb862e86c30e0913a2ff307e48d49357c5b73
@@ -269,9 +279,9 @@ SPEC CHECKSUMS:
nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3
Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03
React: 573d89cf10312b17920df6328eaf9ab8059283bf
RNFirebase: 8d3b25b57e3fb7afa371959147d23d39e70e150a
RNFirebase: 05ef1e1c1681af418f3500fda194a2d17d45be07
yoga: 9403c2451c1b47d8cee3e4f1b6fd0ececc63839c
PODFILE CHECKSUM: d75cf458a6205ebf9ad72c6f3bba5d5d338f9e60
PODFILE CHECKSUM: 582ceaad051470812ad9203e13b5ea8ad20c78ac
COCOAPODS: 1.3.1

View File

@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
17AF4F6B1F59CDBF00C02336 /* RNFirebaseLinks.m in Sources */ = {isa = PBXBuildFile; fileRef = 17AF4F6A1F59CDBF00C02336 /* RNFirebaseLinks.m */; };
27540F9A209F3641001F4AF4 /* RNFirebaseFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 27540F99209F3641001F4AF4 /* RNFirebaseFunctions.m */; };
8300A7AE1F31E143001B16AB /* RNFirebaseDatabaseReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 8300A7AD1F31E143001B16AB /* RNFirebaseDatabaseReference.m */; };
8323CF061F6FBD870071420B /* BannerComponent.m in Sources */ = {isa = PBXBuildFile; fileRef = 8323CEFF1F6FBD870071420B /* BannerComponent.m */; };
8323CF071F6FBD870071420B /* NativeExpressComponent.m in Sources */ = {isa = PBXBuildFile; fileRef = 8323CF011F6FBD870071420B /* NativeExpressComponent.m */; };
@@ -51,6 +52,8 @@
134814201AA4EA6300B7C361 /* libRNFirebase.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNFirebase.a; sourceTree = BUILT_PRODUCTS_DIR; };
17AF4F691F59CDBF00C02336 /* RNFirebaseLinks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseLinks.h; sourceTree = "<group>"; };
17AF4F6A1F59CDBF00C02336 /* RNFirebaseLinks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseLinks.m; sourceTree = "<group>"; };
27540F98209F361B001F4AF4 /* RNFirebaseFunctions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNFirebaseFunctions.h; sourceTree = "<group>"; };
27540F99209F3641001F4AF4 /* RNFirebaseFunctions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseFunctions.m; sourceTree = "<group>"; };
8300A7AC1F31E143001B16AB /* RNFirebaseDatabaseReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFirebaseDatabaseReference.h; sourceTree = "<group>"; };
8300A7AD1F31E143001B16AB /* RNFirebaseDatabaseReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFirebaseDatabaseReference.m; sourceTree = "<group>"; };
8323CEFE1F6FBD870071420B /* BannerComponent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BannerComponent.h; sourceTree = "<group>"; };
@@ -133,9 +136,20 @@
path = RNFirebase/links;
sourceTree = "<group>";
};
27540F97209F35DF001F4AF4 /* functions */ = {
isa = PBXGroup;
children = (
27540F98209F361B001F4AF4 /* RNFirebaseFunctions.h */,
27540F99209F3641001F4AF4 /* RNFirebaseFunctions.m */,
);
name = functions;
path = RNFirebase/functions;
sourceTree = "<group>";
};
58B511D21A9E6C8500147676 = {
isa = PBXGroup;
children = (
27540F97209F35DF001F4AF4 /* functions */,
83AAA0762063DEC2007EC5F7 /* invites */,
838E372420231E15004DCD3A /* notifications */,
838E372020231DF0004DCD3A /* instanceid */,
@@ -381,6 +395,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
27540F9A209F3641001F4AF4 /* RNFirebaseFunctions.m in Sources */,
838E372320231DF0004DCD3A /* RNFirebaseInstanceId.m in Sources */,
839D916E1EF3E20B0077C7C8 /* RNFirebaseAdMobRewardedVideo.m in Sources */,
839D916C1EF3E20B0077C7C8 /* RNFirebaseAdMob.m in Sources */,

View File

@@ -0,0 +1,20 @@
#ifndef RNFirebaseFunctions_h
#define RNFirebaseFunctions_h
#import <Foundation/Foundation.h>
#if __has_include(<FirebaseFunctions/FIRFunctions.h>)
#import <React/RCTBridgeModule.h>
@interface RNFirebaseFunctions : NSObject <RCTBridgeModule> {
}
@end
#else
@interface RNFirebaseFunctions : NSObject
@end
#endif
#endif

View File

@@ -0,0 +1,114 @@
#import "RNFirebaseFunctions.h"
#if __has_include(<FirebaseFunctions/FIRFunctions.h>)
#import <FirebaseFunctions/FIRFunctions.h>
#import <FirebaseFunctions/FIRHTTPSCallable.h>
#import <FirebaseFunctions/FIRError.h>
@implementation RNFirebaseFunctions
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(httpsCallable:
(NSString *) name
wrapper:
(NSDictionary *) wrapper
resolver:
(RCTPromiseResolveBlock) resolve
rejecter:
(RCTPromiseRejectBlock) reject
) {
FIRFunctions *functions = [FIRFunctions functions];
[[functions HTTPSCallableWithName:name] callWithObject:[wrapper valueForKey:@"data"] completion:^(FIRHTTPSCallableResult * _Nullable result, NSError * _Nullable error) {
if (error) {
NSObject *details = [NSNull null];
NSString *message = error.localizedDescription;
if (error.domain == FIRFunctionsErrorDomain) {
details = error.userInfo[FIRFunctionsErrorDetailsKey];
if (details == nil) {
details = [NSNull null];
}
}
resolve(@{
@"__error": @true,
@"code": [self getErrorCodeName:error],
@"message": message,
@"details": details
});
} else {
resolve(@{ @"data": [result data] });
}
}];
}
- (NSString *)getErrorCodeName:(NSError *)error {
NSString *code = @"UNKNOWN";
switch (error.code) {
case FIRFunctionsErrorCodeOK:
code = @"OK";
break;
case FIRFunctionsErrorCodeCancelled:
code = @"CANCELLED";
break;
case FIRFunctionsErrorCodeUnknown:
code = @"UNKNOWN";
break;
case FIRFunctionsErrorCodeInvalidArgument:
code = @"INVALID_ARGUMENT";
break;
case FIRFunctionsErrorCodeDeadlineExceeded:
code = @"DEADLINE_EXCEEDED";
break;
case FIRFunctionsErrorCodeNotFound:
code = @"NOT_FOUND";
break;
case FIRFunctionsErrorCodeAlreadyExists:
code = @"ALREADY_EXISTS";
break;
case FIRFunctionsErrorCodePermissionDenied:
code = @"PERMISSION_DENIED";
break;
case FIRFunctionsErrorCodeResourceExhausted:
code = @"RESOURCE_EXHAUSTED";
break;
case FIRFunctionsErrorCodeFailedPrecondition:
code = @"FAILED_PRECONDITION";
break;
case FIRFunctionsErrorCodeAborted:
code = @"ABORTED";
break;
case FIRFunctionsErrorCodeOutOfRange:
code = @"OUT_OF_RANGE";
break;
case FIRFunctionsErrorCodeUnimplemented:
code = @"UNIMPLEMENTED";
break;
case FIRFunctionsErrorCodeInternal:
code = @"INTERNAL";
break;
case FIRFunctionsErrorCodeUnavailable:
code = @"UNAVAILABLE";
break;
case FIRFunctionsErrorCodeDataLoss:
code = @"DATA_LOSS";
break;
case FIRFunctionsErrorCodeUnauthenticated:
code = @"UNAUTHENTICATED";
break;
default:
break;
}
return code;
}
@end
#else
@implementation RNFirebaseFunctions
@end
#endif

View File

@@ -16,6 +16,7 @@ import Crash, { NAMESPACE as CrashNamespace } from '../crash';
import Crashlytics, { NAMESPACE as CrashlyticsNamespace } from '../crashlytics';
import Database, { NAMESPACE as DatabaseNamespace } from '../database';
import Firestore, { NAMESPACE as FirestoreNamespace } from '../firestore';
import Functions, { NAMESPACE as FunctionsNamespace } from '../functions';
import InstanceId, { NAMESPACE as InstanceIdNamespace } from '../iid';
import Invites, { NAMESPACE as InvitesNamespace } from '../invites';
import Links, { NAMESPACE as LinksNamespace } from '../links';
@@ -45,6 +46,7 @@ export default class App {
crashlytics: () => Crashlytics;
database: () => Database;
firestore: () => Firestore;
functions: () => Functions;
iid: () => InstanceId;
invites: () => Invites;
links: () => Links;
@@ -85,6 +87,7 @@ export default class App {
this.crashlytics = APPS.appModule(this, CrashlyticsNamespace, Crashlytics);
this.database = APPS.appModule(this, DatabaseNamespace, Database);
this.firestore = APPS.appModule(this, FirestoreNamespace, Firestore);
this.functions = APPS.appModule(this, FunctionsNamespace, Functions);
this.iid = APPS.appModule(this, InstanceIdNamespace, InstanceId);
this.invites = APPS.appModule(this, InvitesNamespace, Invites);
this.links = APPS.appModule(this, LinksNamespace, Links);

View File

@@ -38,6 +38,10 @@ import {
statics as FirestoreStatics,
MODULE_NAME as FirestoreModuleName,
} from '../firestore';
import {
statics as FunctionsStatics,
MODULE_NAME as FunctionsModuleName,
} from '../functions';
import {
statics as InstanceIdStatics,
MODULE_NAME as InstanceIdModuleName,
@@ -81,6 +85,7 @@ import type {
DatabaseModule,
FirebaseOptions,
FirestoreModule,
FunctionsModule,
InstanceIdModule,
InvitesModule,
LinksModule,
@@ -102,6 +107,7 @@ class Firebase {
crashlytics: CrashlyticsModule;
database: DatabaseModule;
firestore: FirestoreModule;
functions: FunctionsModule;
iid: InstanceIdModule;
invites: InvitesModule;
links: LinksModule;
@@ -146,6 +152,11 @@ class Firebase {
FirestoreStatics,
FirestoreModuleName
);
this.functions = APPS.moduleAndStatics(
'functions',
FunctionsStatics,
FunctionsModuleName
);
this.iid = APPS.moduleAndStatics(
'iid',
InstanceIdStatics,

View File

@@ -0,0 +1,11 @@
import type { FunctionsErrorCode } from './types.flow';
export default class HttpsError extends Error {
+details: ?any;
+code: FunctionsErrorCode;
constructor(code: FunctionsErrorCode, message?: string, details?: any) {
super(message);
this.details = details;
this.code = code;
}
}

View File

@@ -0,0 +1,89 @@
/**
* @flow
* Functions representation wrapper
*/
import ModuleBase from '../../utils/ModuleBase';
import { isObject } from '../../utils';
import { getNativeModule } from '../../utils/native';
import type App from '../core/app';
import HttpsError from './HttpsError';
import type {
HttpsCallable,
HttpsErrorCode,
HttpsCallablePromise,
} from './types.flow';
export const NAMESPACE = 'functions';
export const MODULE_NAME = 'RNFirebaseFunctions';
export default class Functions extends ModuleBase {
constructor(app: App) {
super(app, {
multiApp: false,
hasShards: false,
namespace: NAMESPACE,
moduleName: MODULE_NAME,
});
}
/**
* -------------
* PUBLIC API
* -------------
*/
/**
* Returns a reference to the callable https trigger with the given name.
* @param name The name of the trigger.
*/
httpsCallable(name: string): HttpsCallable {
return (data?: any): HttpsCallablePromise => {
const promise = getNativeModule(this).httpsCallable(name, { data });
return promise.then(this._errorOrResult);
};
}
/**
* -------------
* INTERNALS
* -------------
*/
_errorOrResult(possibleError): HttpsCallablePromise {
if (isObject(possibleError) && possibleError.__error) {
const { code, message, details } = possibleError;
return Promise.reject(
new HttpsError(
statics.HttpsErrorCode[code] || statics.HttpsErrorCode.UNKNOWN,
message,
details
)
);
}
return Promise.resolve(possibleError);
}
}
export const statics: { HttpsErrorCode: HttpsErrorCode } = {
HttpsErrorCode: {
OK: 'ok',
CANCELLED: 'cancelled',
UNKNOWN: 'unknown',
INVALID_ARGUMENT: 'invalid-argument',
DEADLINE_EXCEEDED: 'deadline-exceeded',
NOT_FOUND: 'not-found',
ALREADY_EXISTS: 'already-exists',
PERMISSION_DENIED: 'permission-denied',
UNAUTHENTICATED: 'unauthenticated',
RESOURCE_EXHAUSTED: 'resource-exhausted',
FAILED_PRECONDITION: 'failed-precondition',
ABORTED: 'aborted',
OUT_OF_RANGE: 'out-of-range',
UNIMPLEMENTED: 'unimplemented',
INTERNAL: 'internal',
UNAVAILABLE: 'unavailable',
DATA_LOSS: 'data-loss',
},
};

View File

@@ -0,0 +1,30 @@
export type HttpsCallableResult = {
data: Object,
};
export type FunctionsErrorCode =
| 'ok'
| 'cancelled'
| 'unknown'
| 'invalid-argument'
| 'deadline-exceeded'
| 'not-found'
| 'already-exists'
| 'permission-denied'
| 'resource-exhausted'
| 'failed-precondition'
| 'aborted'
| 'out-of-range'
| 'unimplemented'
| 'internal'
| 'unavailable'
| 'data-loss'
| 'unauthenticated';
export type HttpsCallablePromise =
| Promise<HttpsCallableResult>
| Promise<HttpsError>;
export type HttpsCallable = (data?: any) => HttpsCallablePromise;
export type HttpsErrorCode = { [name: string]: FunctionsErrorCode };

View File

@@ -15,6 +15,8 @@ import type Database from '../modules/database';
import { typeof statics as DatabaseStatics } from '../modules/database';
import type Firestore from '../modules/firestore';
import { typeof statics as FirestoreStatics } from '../modules/firestore';
import type Functions from '../modules/functions';
import { typeof statics as FunctionsStatics } from '../modules/functions';
import type InstanceId from '../modules/iid';
import { typeof statics as InstanceIdStatics } from '../modules/iid';
import type Invites from '../modules/invites';
@@ -63,6 +65,7 @@ export type FirebaseModuleName =
| 'RNFirebaseCrashlytics'
| 'RNFirebaseDatabase'
| 'RNFirebaseFirestore'
| 'RNFirebaseFunctions'
| 'RNFirebaseInstanceId'
| 'RNFirebaseInvites'
| 'RNFirebaseLinks'
@@ -81,6 +84,7 @@ export type FirebaseNamespace =
| 'crashlytics'
| 'database'
| 'firestore'
| 'functions'
| 'iid'
| 'invites'
| 'links'
@@ -171,6 +175,13 @@ export type FirestoreModule = {
nativeModuleExists: boolean,
} & FirestoreStatics;
/* Functions types */
export type FunctionsModule = {
(): Functions,
nativeModuleExists: boolean,
} & FunctionsStatics;
/* InstanceId types */
export type InstanceIdModule = {

View File

@@ -107,7 +107,7 @@ export default {
const _name = (name || DEFAULT_APP_NAME).toUpperCase();
// return an existing app if found
// todo in v4 remove deprecation and throw an error
// TODO in v5 remove deprecation and throw an error
if (APPS[_name]) {
console.warn(INTERNALS.STRINGS.WARN_INITIALIZE_DEPRECATION);
return APPS[_name];