diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 56b20748..567c9001 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -2,7 +2,6 @@ name: ⚠️ Bug/Issue report about: Please provide as much detail as possible to help us with a bug or issue. Issues will be closed if they do not follow the template. - --- @@ -47,14 +46,14 @@ The issue list of this repo is exclusively for bug reports. -7. Are you using `typescript`? +7. Are you using `typescript`? - --- + Loving `react-native-firebase`? Please consider supporting them with any of the below: - - 👉 Back financially via [Open Collective](https://opencollective.com/react-native-firebase/donate) - - 👉 Follow [`React Native Firebase`](https://twitter.com/rnfirebase) and [`Invertase`](https://twitter.com/invertaseio) on Twitter - - 👉 Star this repo on GitHub ⭐️ +* 👉 Back financially via [Open Collective](https://opencollective.com/react-native-firebase/donate) +* 👉 Follow [`React Native Firebase`](https://twitter.com/rnfirebase) and [`Invertase`](https://twitter.com/invertaseio) on Twitter +* 👉 Star this repo on GitHub ⭐️ diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 79c6fe08..eae3cc09 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,7 +1,7 @@ --- name: 🎁 Feature request -about: Please create feature requests on our canny board: [https://react-native-firebase.canny.io/feature-requests](https://react-native-firebase.canny.io/feature-requests) +about: For feature requests please visit our [Feature Request Board](https://boards.invertase.io/react-native-firebase). --- -[https://react-native-firebase.canny.io/feature-requests](https://react-native-firebase.canny.io/feature-requests) +For feature requests please visit our [Feature Request Board](https://boards.invertase.io/react-native-firebase). diff --git a/README.md b/README.md index efc70c90..21503692 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a | **Crashlytics**           | ❌ | ✅ | ✅ | ❌ | | **Crash Reporting** | ✅ | ✅ | ✅ | ❌ | | **Dynamic Links** | ❌ | ✅ | ✅ | ❌ | -| **[Functions Callable](https://firebase.googleblog.com/2018/04/launching-cloud-functions-for-firebase-1-0.html?m=1)**             |   ❌   |   ❌   | ✅ |   ✅   | +| **[Functions Callable](https://firebase.googleblog.com/2018/04/launching-cloud-functions-for-firebase-1-0.html?m=1)**             |   ❌   |   ❌   | ✅ |   ✅   | | **Invites** | ❌ | ❌ | ✅ | ❌ | | **Instance ID**          | ❌ | ❌ | **?** | ❌ | | **Performance Monitoring** | ✅ | ✅ | ✅ | ❌ | @@ -64,17 +64,17 @@ All in all, RNFirebase provides much faster performance (~2x) over the web SDK a > The table below shows the supported versions of React Native and the Firebase SDKs for different versions of `react-native-firebase`. -| | 2.2.x | 3.3.x | 4.0.x | 4.1.x | 4.2.x | -| -------------------- | -------- | -------- | -------- | -------- | -------- | -| React Native | 0.47 + | 0.50 + | 0.52 + | 0.52 + | 0.52-55.x | -| Firebase Android SDK | 11.0.0 + | 11.8.0 + | 12.0.0 + | 15.0.0 + | 15.0.0 + | -| Firebase iOS SDK | 4.0.0 + | 4.7.0 + | 4.11.0 + | 4.13.0 + | 5.0.0 + | +| | 2.2.x | 3.3.x | 4.0.x | 4.1.x | 4.2.x | +| -------------------- | -------- | -------- | -------- | -------- | --------- | +| React Native | 0.47 + | 0.50 + | 0.52 + | 0.52 + | 0.52-55.x | +| Firebase Android SDK | 11.0.0 + | 11.8.0 + | 12.0.0 + | 15.0.0 + | 15.0.0 + | +| Firebase iOS SDK | 4.0.0 + | 4.7.0 + | 4.11.0 + | 4.13.0 + | 5.0.0 + | --- ## Documentation -To check out our latest docs, visit [rnfirebase.io](https://rnfirebase.io) +To check out our latest docs, visit [https://invertase.io/oss/react-native-firebase](https://invertase.io/oss/react-native-firebase) ## Questions @@ -86,7 +86,7 @@ Please make sure to complete the issue template before opening an issue. Issues ## Feature Requests -For feature requests please use our [Canny Board](http://invertase.link/requests). +For feature requests please visit our [Feature Request Board](https://boards.invertase.io/react-native-firebase). ## Changelog diff --git a/android/build.gradle b/android/build.gradle index acb6ecc1..15dd4337 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -76,6 +76,15 @@ rootProject.gradle.buildFinished { buildResult -> } } +repositories { + google() + jcenter() + maven { + url "$rootDir/../node_modules/react-native/android" + name 'React Native (local)' + } +} + def supportVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION dependencies { diff --git a/android/src/main/java/io/invertase/firebase/Utils.java b/android/src/main/java/io/invertase/firebase/Utils.java index 6e84a96f..0d8eba07 100644 --- a/android/src/main/java/io/invertase/firebase/Utils.java +++ b/android/src/main/java/io/invertase/firebase/Utils.java @@ -1,20 +1,21 @@ package io.invertase.firebase; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.Context; +import android.os.AsyncTask; import android.util.Log; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + import java.util.List; import java.util.Map; -import com.facebook.react.bridge.Arguments; -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.ReadableArray; - import javax.annotation.Nullable; @@ -156,4 +157,12 @@ public class Utils { return false; } + + public static int getResId(Context ctx, String resName) { + int resourceId = ctx.getResources().getIdentifier(resName, "string", ctx.getPackageName()); + if (resourceId == 0) { + Log.e(TAG, "resource " + resName + " could not be found"); + } + return resourceId; + } } diff --git a/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfig.java b/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfig.java index c2b4acd6..4e1c5c09 100644 --- a/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfig.java +++ b/android/src/main/java/io/invertase/firebase/config/RNFirebaseRemoteConfig.java @@ -16,6 +16,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.google.firebase.FirebaseApp; import com.google.firebase.remoteconfig.FirebaseRemoteConfig; +import com.google.firebase.remoteconfig.FirebaseRemoteConfigFetchThrottledException; import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue; import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings; @@ -119,9 +120,13 @@ class RNFirebaseRemoteConfig extends ReactContextBaseJavaModule { @Override public void onComplete(@NonNull Task task) { if (task.isSuccessful()) { - promise.resolve("remoteConfigFetchStatusSuccess"); + promise.resolve(null); } else { - promise.reject("config/failure", task.getException().getMessage(), task.getException()); + if (task.getException() instanceof FirebaseRemoteConfigFetchThrottledException) { + promise.reject("config/throttled", "fetch() operation cannot be completed successfully, due to throttling.", task.getException()); + } else { + promise.reject("config/failure", "fetch() operation cannot be completed successfully.", task.getException()); + } } } }); @@ -154,7 +159,6 @@ class RNFirebaseRemoteConfig extends ReactContextBaseJavaModule { map.putNull(NUMBER_VALUE); } - // TODO check with ios switch (value.getSource()) { case FirebaseRemoteConfig.VALUE_SOURCE_DEFAULT: map.putString(SOURCE, "default"); diff --git a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java index 3d99b522..947992c8 100644 --- a/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java +++ b/android/src/main/java/io/invertase/firebase/database/RNFirebaseDatabaseReference.java @@ -3,8 +3,12 @@ package io.invertase.firebase.database; import java.util.Map; import java.util.List; import java.util.HashMap; +import java.lang.ref.WeakReference; import android.util.Log; +import android.os.AsyncTask; +import android.annotation.SuppressLint; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.facebook.react.bridge.Promise; @@ -33,6 +37,46 @@ class RNFirebaseDatabaseReference { private HashMap childEventListeners = new HashMap<>(); private HashMap valueEventListeners = new HashMap<>(); + /** + * AsyncTask to convert DataSnapshot instances to WritableMap instances. + * + * Introduced due to https://github.com/invertase/react-native-firebase/issues/1284 + */ + private static class DataSnapshotToMapAsyncTask extends AsyncTask { + + private WeakReference reactContextWeakReference; + private WeakReference referenceWeakReference; + + DataSnapshotToMapAsyncTask(ReactContext context, RNFirebaseDatabaseReference reference) { + referenceWeakReference = new WeakReference<>(reference); + reactContextWeakReference = new WeakReference<>(context); + } + + @Override + protected final WritableMap doInBackground(Object... params) { + DataSnapshot dataSnapshot = (DataSnapshot) params[0]; + @Nullable String previousChildName = (String) params[1]; + + try { + return RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName); + } catch (RuntimeException e) { + if (isAvailable()) { + reactContextWeakReference.get().handleException(e); + } + throw e; + } + } + + @Override + protected void onPostExecute(WritableMap writableMap) { + // do nothing as overridden on usage + } + + Boolean isAvailable() { + return reactContextWeakReference.get() != null && referenceWeakReference.get() != null; + } + } + /** * RNFirebase wrapper around FirebaseDatabaseReference, * handles Query generation and event listeners. @@ -130,15 +174,22 @@ class RNFirebaseDatabaseReference { * @param promise */ private void addOnceValueEventListener(final Promise promise) { + @SuppressLint("StaticFieldLeak") + final DataSnapshotToMapAsyncTask asyncTask = new DataSnapshotToMapAsyncTask(reactContext, this) { + @Override + protected void onPostExecute(WritableMap writableMap) { + if (this.isAvailable()) promise.resolve(writableMap); + } + }; + ValueEventListener onceValueEventListener = new ValueEventListener() { @Override - public void onDataChange(DataSnapshot dataSnapshot) { - WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, null); - promise.resolve(data); + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { + asyncTask.execute(dataSnapshot, null); } @Override - public void onCancelled(DatabaseError error) { + public void onCancelled(@NonNull DatabaseError error) { RNFirebaseDatabase.handlePromise(promise, error); } }; @@ -156,7 +207,7 @@ class RNFirebaseDatabaseReference { private void addChildOnceEventListener(final String eventName, final Promise promise) { ChildEventListener childEventListener = new ChildEventListener() { @Override - public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { + public void onChildAdded(@NonNull DataSnapshot dataSnapshot, String previousChildName) { if ("child_added".equals(eventName)) { query.removeEventListener(this); WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName); @@ -165,7 +216,7 @@ class RNFirebaseDatabaseReference { } @Override - public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { + public void onChildChanged(@NonNull DataSnapshot dataSnapshot, String previousChildName) { if ("child_changed".equals(eventName)) { query.removeEventListener(this); WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName); @@ -174,7 +225,7 @@ class RNFirebaseDatabaseReference { } @Override - public void onChildRemoved(DataSnapshot dataSnapshot) { + public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) { if ("child_removed".equals(eventName)) { query.removeEventListener(this); WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, null); @@ -183,7 +234,7 @@ class RNFirebaseDatabaseReference { } @Override - public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { + public void onChildMoved(@NonNull DataSnapshot dataSnapshot, String previousChildName) { if ("child_moved".equals(eventName)) { query.removeEventListener(this); WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName); @@ -192,7 +243,7 @@ class RNFirebaseDatabaseReference { } @Override - public void onCancelled(DatabaseError error) { + public void onCancelled(@NonNull DatabaseError error) { query.removeEventListener(this); RNFirebaseDatabase.handlePromise(promise, error); } @@ -243,35 +294,35 @@ class RNFirebaseDatabaseReference { if (!hasEventListener(eventRegistrationKey)) { ChildEventListener childEventListener = new ChildEventListener() { @Override - public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { + public void onChildAdded(@NonNull DataSnapshot dataSnapshot, String previousChildName) { if ("child_added".equals(eventType)) { handleDatabaseEvent("child_added", registration, dataSnapshot, previousChildName); } } @Override - public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { + public void onChildChanged(@NonNull DataSnapshot dataSnapshot, String previousChildName) { if ("child_changed".equals(eventType)) { handleDatabaseEvent("child_changed", registration, dataSnapshot, previousChildName); } } @Override - public void onChildRemoved(DataSnapshot dataSnapshot) { + public void onChildRemoved(@NonNull DataSnapshot dataSnapshot) { if ("child_removed".equals(eventType)) { handleDatabaseEvent("child_removed", registration, dataSnapshot, null); } } @Override - public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { + public void onChildMoved(@NonNull DataSnapshot dataSnapshot, String previousChildName) { if ("child_moved".equals(eventType)) { handleDatabaseEvent("child_moved", registration, dataSnapshot, previousChildName); } } @Override - public void onCancelled(DatabaseError error) { + public void onCancelled(@NonNull DatabaseError error) { removeEventListener(eventRegistrationKey); handleDatabaseError(registration, error); } @@ -292,12 +343,12 @@ class RNFirebaseDatabaseReference { if (!hasEventListener(eventRegistrationKey)) { ValueEventListener valueEventListener = new ValueEventListener() { @Override - public void onDataChange(DataSnapshot dataSnapshot) { + public void onDataChange(@NonNull DataSnapshot dataSnapshot) { handleDatabaseEvent("value", registration, dataSnapshot, null); } @Override - public void onCancelled(DatabaseError error) { + public void onCancelled(@NonNull DatabaseError error) { removeEventListener(eventRegistrationKey); handleDatabaseError(registration, error); } @@ -314,16 +365,23 @@ class RNFirebaseDatabaseReference { * @param dataSnapshot * @param previousChildName */ - private void handleDatabaseEvent(String eventType, ReadableMap registration, DataSnapshot dataSnapshot, @Nullable String previousChildName) { - WritableMap event = Arguments.createMap(); - WritableMap data = RNFirebaseDatabaseUtils.snapshotToMap(dataSnapshot, previousChildName); + private void handleDatabaseEvent(final String eventType, final ReadableMap registration, DataSnapshot dataSnapshot, @Nullable String previousChildName) { + @SuppressLint("StaticFieldLeak") + DataSnapshotToMapAsyncTask asyncTask = new DataSnapshotToMapAsyncTask(reactContext, this) { + @Override + protected void onPostExecute(WritableMap data) { + if (this.isAvailable()) { + WritableMap event = Arguments.createMap(); + event.putMap("data", data); + event.putString("key", key); + event.putString("eventType", eventType); + event.putMap("registration", Utils.readableMapToWritableMap(registration)); + Utils.sendEvent(reactContext, "database_sync_event", event); + } + } + }; - event.putMap("data", data); - event.putString("key", key); - event.putString("eventType", eventType); - event.putMap("registration", Utils.readableMapToWritableMap(registration)); - - Utils.sendEvent(reactContext, "database_sync_event", event); + asyncTask.execute(dataSnapshot, previousChildName); } /** diff --git a/android/src/main/java/io/invertase/firebase/firestore/DocumentSnapshotSerializeAsyncTask.java b/android/src/main/java/io/invertase/firebase/firestore/DocumentSnapshotSerializeAsyncTask.java new file mode 100644 index 00000000..ebf77162 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/firestore/DocumentSnapshotSerializeAsyncTask.java @@ -0,0 +1,47 @@ +package io.invertase.firebase.firestore; + +import android.os.AsyncTask; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.google.firebase.firestore.DocumentSnapshot; + +import java.lang.ref.WeakReference; + +class DocumentSnapshotSerializeAsyncTask extends AsyncTask { + private WeakReference reactContextWeakReference; + private WeakReference referenceWeakReference; + + DocumentSnapshotSerializeAsyncTask( + ReactContext context, + RNFirebaseFirestoreDocumentReference reference + ) { + referenceWeakReference = new WeakReference<>(reference); + reactContextWeakReference = new WeakReference<>(context); + } + + @Override + protected final WritableMap doInBackground(Object... params) { + DocumentSnapshot querySnapshot = (DocumentSnapshot) params[0]; + + try { + return FirestoreSerialize.snapshotToWritableMap(querySnapshot); + } catch (RuntimeException e) { + if (isAvailable()) { + reactContextWeakReference.get().handleException(e); + } else { + throw e; + } + return null; + } + } + + @Override + protected void onPostExecute(WritableMap writableMap) { + // do nothing as overridden on usage + } + + private Boolean isAvailable() { + return reactContextWeakReference.get() != null && referenceWeakReference.get() != null; + } +} diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index 90291179..53913db9 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -52,18 +52,16 @@ public class FirestoreSerialize { if (documentSnapshot.exists()) { documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); } - // metadata - if (documentSnapshot.getMetadata() != null) { - WritableMap metadata = Arguments.createMap(); - metadata.putBoolean("fromCache", documentSnapshot.getMetadata().isFromCache()); - metadata.putBoolean("hasPendingWrites", documentSnapshot.getMetadata().hasPendingWrites()); - documentMap.putMap(KEY_METADATA, metadata); - } + // metadata + WritableMap metadata = Arguments.createMap(); + metadata.putBoolean("fromCache", documentSnapshot.getMetadata().isFromCache()); + metadata.putBoolean("hasPendingWrites", documentSnapshot.getMetadata().hasPendingWrites()); + documentMap.putMap(KEY_METADATA, metadata); return documentMap; } - public static WritableMap snapshotToWritableMap(QuerySnapshot querySnapshot) { + static WritableMap snapshotToWritableMap(QuerySnapshot querySnapshot) { WritableMap queryMap = Arguments.createMap(); List documentChanges = querySnapshot.getDocumentChanges(); @@ -78,12 +76,10 @@ public class FirestoreSerialize { queryMap.putArray(KEY_DOCUMENTS, documents); // metadata - if (querySnapshot.getMetadata() != null) { - WritableMap metadata = Arguments.createMap(); - metadata.putBoolean("fromCache", querySnapshot.getMetadata().isFromCache()); - metadata.putBoolean("hasPendingWrites", querySnapshot.getMetadata().hasPendingWrites()); - queryMap.putMap(KEY_METADATA, metadata); - } + WritableMap metadata = Arguments.createMap(); + metadata.putBoolean("fromCache", querySnapshot.getMetadata().isFromCache()); + metadata.putBoolean("hasPendingWrites", querySnapshot.getMetadata().hasPendingWrites()); + queryMap.putMap(KEY_METADATA, metadata); return queryMap; } @@ -94,7 +90,7 @@ public class FirestoreSerialize { * @param documentChanges List * @return WritableArray */ - static WritableArray documentChangesToWritableArray(List documentChanges) { + private static WritableArray documentChangesToWritableArray(List documentChanges) { WritableArray documentChangesWritable = Arguments.createArray(); for (DocumentChange documentChange : documentChanges) { documentChangesWritable.pushMap(documentChangeToWritableMap(documentChange)); @@ -108,7 +104,7 @@ public class FirestoreSerialize { * @param documentChange DocumentChange * @return WritableMap */ - static WritableMap documentChangeToWritableMap(DocumentChange documentChange) { + private static WritableMap documentChangeToWritableMap(DocumentChange documentChange) { WritableMap documentChangeMap = Arguments.createMap(); switch (documentChange.getType()) { @@ -122,8 +118,10 @@ public class FirestoreSerialize { documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "modified"); } - documentChangeMap.putMap(KEY_DOC_CHANGE_DOCUMENT, - snapshotToWritableMap(documentChange.getDocument())); + documentChangeMap.putMap( + KEY_DOC_CHANGE_DOCUMENT, + snapshotToWritableMap(documentChange.getDocument()) + ); documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex()); documentChangeMap.putInt(KEY_DOC_CHANGE_OLD_INDEX, documentChange.getOldIndex()); @@ -136,7 +134,7 @@ public class FirestoreSerialize { * @param map Map * @return WritableMap */ - static WritableMap objectMapToWritable(Map map) { + private static WritableMap objectMapToWritable(Map map) { WritableMap writableMap = Arguments.createMap(); for (Map.Entry entry : map.entrySet()) { WritableMap typeMap = buildTypeMap(entry.getValue()); @@ -224,7 +222,10 @@ public class FirestoreSerialize { return typeMap; } - static Map parseReadableMap(FirebaseFirestore firestore, ReadableMap readableMap) { + static Map parseReadableMap( + FirebaseFirestore firestore, + ReadableMap readableMap + ) { Map map = new HashMap<>(); if (readableMap != null) { ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); @@ -290,7 +291,10 @@ public class FirestoreSerialize { } } - public static List parseDocumentBatches(FirebaseFirestore firestore, ReadableArray readableArray) { + static List parseDocumentBatches( + FirebaseFirestore firestore, + ReadableArray readableArray + ) { List writes = new ArrayList<>(readableArray.size()); for (int i = 0; i < readableArray.size(); i++) { Map write = new HashMap<>(); diff --git a/android/src/main/java/io/invertase/firebase/firestore/QuerySnapshotSerializeAsyncTask.java b/android/src/main/java/io/invertase/firebase/firestore/QuerySnapshotSerializeAsyncTask.java new file mode 100644 index 00000000..4ddeab5f --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/firestore/QuerySnapshotSerializeAsyncTask.java @@ -0,0 +1,47 @@ +package io.invertase.firebase.firestore; + +import android.os.AsyncTask; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.google.firebase.firestore.QuerySnapshot; + +import java.lang.ref.WeakReference; + +class QuerySnapshotSerializeAsyncTask extends AsyncTask { + private WeakReference reactContextWeakReference; + private WeakReference referenceWeakReference; + + QuerySnapshotSerializeAsyncTask( + ReactContext context, + RNFirebaseFirestoreCollectionReference reference + ) { + referenceWeakReference = new WeakReference<>(reference); + reactContextWeakReference = new WeakReference<>(context); + } + + @Override + protected final WritableMap doInBackground(Object... params) { + QuerySnapshot querySnapshot = (QuerySnapshot) params[0]; + + try { + return FirestoreSerialize.snapshotToWritableMap(querySnapshot); + } catch (RuntimeException e) { + if (isAvailable()) { + reactContextWeakReference.get().handleException(e); + } else { + throw e; + } + return null; + } + } + + @Override + protected void onPostExecute(WritableMap writableMap) { + // do nothing as overridden on usage + } + + private Boolean isAvailable() { + return reactContextWeakReference.get() != null && referenceWeakReference.get() != null; + } +} diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java index 9387d275..d0d61fbc 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -18,13 +18,13 @@ import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; import com.google.android.gms.tasks.Task; import com.google.firebase.FirebaseApp; -import com.google.firebase.firestore.FirebaseFirestoreSettings; -import com.google.firebase.firestore.Transaction; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FieldValue; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.FirebaseFirestoreSettings; import com.google.firebase.firestore.SetOptions; +import com.google.firebase.firestore.Transaction; import com.google.firebase.firestore.WriteBatch; import java.util.HashMap; @@ -48,6 +48,184 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { * REACT NATIVE METHODS */ + /** + * Generates a js-like error from an exception and rejects the provided promise with it. + * + * @param exception Exception Exception normally from a task result. + * @param promise Promise react native promise + */ + static void promiseRejectException(Promise promise, FirebaseFirestoreException exception) { + WritableMap jsError = getJSError(exception); + promise.reject( + jsError.getString("code"), + jsError.getString("message"), + exception + ); + } + + /** + * Get a database instance for a specific firebase app instance + * + * @param appName appName + * @return FirebaseFirestore + */ + static FirebaseFirestore getFirestoreForApp(String appName) { + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + return FirebaseFirestore.getInstance(firebaseApp); + } + + /** + * Convert as firebase DatabaseError instance into a writable map + * with the correct web-like error codes. + * + * @param nativeException nativeException + * @return WritableMap + */ + static WritableMap getJSError(FirebaseFirestoreException nativeException) { + WritableMap errorMap = Arguments.createMap(); + errorMap.putInt("nativeErrorCode", nativeException.getCode().value()); + errorMap.putString("nativeErrorMessage", nativeException.getMessage()); + + String code; + String message; + String service = "Firestore"; + + // TODO: Proper error mappings + switch (nativeException.getCode()) { + case OK: + code = ErrorUtils.getCodeWithService(service, "ok"); + message = ErrorUtils.getMessageWithService("Ok.", service, code); + break; + case CANCELLED: + code = ErrorUtils.getCodeWithService(service, "cancelled"); + message = ErrorUtils.getMessageWithService("The operation was cancelled.", service, code); + break; + case UNKNOWN: + code = ErrorUtils.getCodeWithService(service, "unknown"); + message = ErrorUtils.getMessageWithService( + "Unknown error or an error from a different error domain.", + service, + code + ); + break; + case INVALID_ARGUMENT: + code = ErrorUtils.getCodeWithService(service, "invalid-argument"); + message = ErrorUtils.getMessageWithService( + "Client specified an invalid argument.", + service, + code + ); + break; + case DEADLINE_EXCEEDED: + code = ErrorUtils.getCodeWithService(service, "deadline-exceeded"); + message = ErrorUtils.getMessageWithService( + "Deadline expired before operation could complete.", + service, + code + ); + break; + case NOT_FOUND: + code = ErrorUtils.getCodeWithService(service, "not-found"); + message = ErrorUtils.getMessageWithService( + "Some requested document was not found.", + service, + code + ); + break; + case ALREADY_EXISTS: + code = ErrorUtils.getCodeWithService(service, "already-exists"); + message = ErrorUtils.getMessageWithService( + "Some document that we attempted to create already exists.", + service, + code + ); + break; + case PERMISSION_DENIED: + code = ErrorUtils.getCodeWithService(service, "permission-denied"); + message = ErrorUtils.getMessageWithService( + "The caller does not have permission to execute the specified operation.", + service, + code + ); + break; + case RESOURCE_EXHAUSTED: + code = ErrorUtils.getCodeWithService(service, "resource-exhausted"); + message = ErrorUtils.getMessageWithService( + "Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.", + service, + code + ); + break; + case FAILED_PRECONDITION: + code = ErrorUtils.getCodeWithService(service, "failed-precondition"); + message = ErrorUtils.getMessageWithService( + "Operation was rejected because the system is not in a state required for the operation`s execution.", + service, + code + ); + break; + case ABORTED: + code = ErrorUtils.getCodeWithService(service, "aborted"); + message = ErrorUtils.getMessageWithService( + "The operation was aborted, typically due to a concurrency issue like transaction aborts, etc.", + service, + code + ); + break; + case OUT_OF_RANGE: + code = ErrorUtils.getCodeWithService(service, "out-of-range"); + message = ErrorUtils.getMessageWithService( + "Operation was attempted past the valid range.", + service, + code + ); + break; + case UNIMPLEMENTED: + code = ErrorUtils.getCodeWithService(service, "unimplemented"); + message = ErrorUtils.getMessageWithService( + "Operation is not implemented or not supported/enabled.", + service, + code + ); + break; + case INTERNAL: + code = ErrorUtils.getCodeWithService(service, "internal"); + message = ErrorUtils.getMessageWithService("Internal errors.", service, code); + break; + case UNAVAILABLE: + code = ErrorUtils.getCodeWithService(service, "unavailable"); + message = ErrorUtils.getMessageWithService( + "The service is currently unavailable.", + service, + code + ); + break; + case DATA_LOSS: + code = ErrorUtils.getCodeWithService(service, "data-loss"); + message = ErrorUtils.getMessageWithService( + "Unrecoverable data loss or corruption.", + service, + code + ); + break; + case UNAUTHENTICATED: + code = ErrorUtils.getCodeWithService(service, "unauthenticated"); + message = ErrorUtils.getMessageWithService( + "The request does not have valid authentication credentials for the operation.", + service, + code + ); + break; + default: + code = ErrorUtils.getCodeWithService(service, "unknown"); + message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code); + } + + errorMap.putString("code", code); + errorMap.putString("message", message); + return errorMap; + } + @ReactMethod public void disableNetwork(String appName, final Promise promise) { getFirestoreForApp(appName).disableNetwork().addOnCompleteListener(new OnCompleteListener() { @@ -58,7 +236,10 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { promise.resolve(null); } else { Log.e(TAG, "disableNetwork:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); + RNFirebaseFirestore.promiseRejectException( + promise, + (FirebaseFirestoreException) task.getException() + ); } } }); @@ -83,35 +264,55 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { promise.resolve(null); } else { Log.e(TAG, "enableNetwork:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); + RNFirebaseFirestore.promiseRejectException( + promise, + (FirebaseFirestoreException) task.getException() + ); } } }); } @ReactMethod - public void collectionGet(String appName, String path, ReadableArray filters, - ReadableArray orders, ReadableMap options, ReadableMap getOptions, - final Promise promise) { - RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options); + public void collectionGet( + String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options, ReadableMap getOptions, + final Promise promise + ) { + RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath( + appName, + path, + filters, + orders, + options + ); ref.get(getOptions, promise); } @ReactMethod - public void collectionOffSnapshot(String appName, String path, ReadableArray filters, - ReadableArray orders, ReadableMap options, String listenerId) { + public void collectionOffSnapshot( + String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options, String listenerId + ) { RNFirebaseFirestoreCollectionReference.offSnapshot(listenerId); } @ReactMethod - public void collectionOnSnapshot(String appName, String path, ReadableArray filters, - ReadableArray orders, ReadableMap options, String listenerId, - ReadableMap queryListenOptions) { - RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath(appName, path, filters, orders, options); + public void collectionOnSnapshot( + String appName, String path, ReadableArray filters, + ReadableArray orders, ReadableMap options, String listenerId, + ReadableMap queryListenOptions + ) { + RNFirebaseFirestoreCollectionReference ref = getCollectionForAppPath( + appName, + path, + filters, + orders, + options + ); ref.onSnapshot(listenerId, queryListenOptions); } - @ReactMethod public void documentBatch(final String appName, final ReadableArray writes, final Promise promise) { @@ -166,7 +367,12 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { } @ReactMethod - public void documentGet(String appName, String path, ReadableMap getOptions, final Promise promise) { + public void documentGet( + String appName, + String path, + ReadableMap getOptions, + final Promise promise + ) { RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.get(getOptions, promise); } @@ -177,18 +383,31 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { } @ReactMethod - public void documentOnSnapshot(String appName, String path, String listenerId, - ReadableMap docListenOptions) { + public void documentOnSnapshot( + String appName, String path, String listenerId, + ReadableMap docListenOptions + ) { RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.onSnapshot(listenerId, docListenOptions); } @ReactMethod - public void documentSet(String appName, String path, ReadableMap data, ReadableMap options, final Promise promise) { + public void documentSet( + String appName, + String path, + ReadableMap data, + ReadableMap options, + final Promise promise + ) { RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); ref.set(data, options, promise); } + + /* + * Transaction Methods + */ + @ReactMethod public void documentUpdate(String appName, String path, ReadableMap data, final Promise promise) { RNFirebaseFirestoreDocumentReference ref = getDocumentForAppPath(appName, path); @@ -214,18 +433,17 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { } else { firestoreSettings.setSslEnabled(firestore.getFirestoreSettings().isSslEnabled()); } - if (settings.hasKey("timestampsInSnapshots")) { - // TODO: Not supported on Android yet - } + +// if (settings.hasKey("timestampsInSnapshots")) { +// // TODO: Not supported on Android yet +// } firestore.setFirestoreSettings(firestoreSettings.build()); promise.resolve(null); } - /** * Try clean up previous transactions on reload - * */ @Override public void onCatalystInstanceDestroy() { @@ -239,23 +457,22 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { transactionHandlers.clear(); } - - /* - * Transaction Methods - */ - - /** * Calls the internal Firestore Transaction classes instance .get(ref) method and resolves with * the DocumentSnapshot. * - * @param appName - * @param transactionId - * @param path - * @param promise + * @param appName appName + * @param transactionId transactionId + * @param path path + * @param promise promise */ @ReactMethod - public void transactionGetDocument(String appName, int transactionId, String path, final Promise promise) { + public void transactionGetDocument( + String appName, + int transactionId, + String path, + final Promise promise + ) { RNFirebaseFirestoreTransactionHandler handler = transactionHandlers.get(transactionId); if (handler == null) { @@ -269,11 +486,16 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { } } + + /* + * INTERNALS/UTILS + */ + /** * Aborts any pending signals and deletes the transaction handler. * - * @param appName - * @param transactionId + * @param appName appName + * @param transactionId transactionId */ @ReactMethod public void transactionDispose(String appName, int transactionId) { @@ -288,12 +510,16 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { /** * Signals to transactionHandler that the command buffer is ready. * - * @param appName - * @param transactionId - * @param commandBuffer + * @param appName appName + * @param transactionId transactionId + * @param commandBuffer commandBuffer */ @ReactMethod - public void transactionApplyBuffer(String appName, int transactionId, ReadableArray commandBuffer) { + public void transactionApplyBuffer( + String appName, + int transactionId, + ReadableArray commandBuffer + ) { RNFirebaseFirestoreTransactionHandler handler = transactionHandlers.get(transactionId); if (handler != null) { @@ -304,12 +530,16 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { /** * Begin a new transaction via AsyncTask 's * - * @param appName - * @param transactionId + * @param appName appName + * @param transactionId transactionId */ @ReactMethod public void transactionBegin(final String appName, int transactionId) { - final RNFirebaseFirestoreTransactionHandler transactionHandler = new RNFirebaseFirestoreTransactionHandler(appName, transactionId); + final RNFirebaseFirestoreTransactionHandler transactionHandler = new RNFirebaseFirestoreTransactionHandler( + appName, + transactionId + ); + transactionHandlers.put(transactionId, transactionHandler); AsyncTask.execute(new Runnable() { @@ -327,7 +557,11 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { @Override public void run() { WritableMap eventMap = transactionHandler.createEventMap(null, "update"); - Utils.sendEvent(getReactApplicationContext(), "firestore_transaction_event", eventMap); + Utils.sendEvent( + getReactApplicationContext(), + "firestore_transaction_event", + eventMap + ); } }); @@ -336,12 +570,18 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { // exit early if aborted - has to throw an exception otherwise will just keep trying ... if (transactionHandler.aborted) { - throw new FirebaseFirestoreException("abort", FirebaseFirestoreException.Code.ABORTED); + throw new FirebaseFirestoreException( + "abort", + FirebaseFirestoreException.Code.ABORTED + ); } // exit early if timeout from bridge - has to throw an exception otherwise will just keep trying ... if (transactionHandler.timeout) { - throw new FirebaseFirestoreException("timeout", FirebaseFirestoreException.Code.DEADLINE_EXCEEDED); + throw new FirebaseFirestoreException( + "timeout", + FirebaseFirestoreException.Code.DEADLINE_EXCEEDED + ); } // process any buffered commands from JS land @@ -357,15 +597,19 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { ReadableMap command = buffer.getMap(i); String path = command.getString("path"); String type = command.getString("type"); - RNFirebaseFirestoreDocumentReference documentReference = getDocumentForAppPath(appName, path); - + RNFirebaseFirestoreDocumentReference documentReference = getDocumentForAppPath( + appName, + path + ); switch (type) { case "set": data = command.getMap("data"); - ReadableMap options = command.getMap("options"); - Map setData = FirestoreSerialize.parseReadableMap(RNFirebaseFirestore.getFirestoreForApp(appName), data); + Map setData = FirestoreSerialize.parseReadableMap( + RNFirebaseFirestore.getFirestoreForApp(appName), + data + ); if (options != null && options.hasKey("merge") && options.getBoolean("merge")) { transaction.set(documentReference.getRef(), setData, SetOptions.merge()); @@ -376,7 +620,11 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { case "update": data = command.getMap("data"); - Map updateData = FirestoreSerialize.parseReadableMap(RNFirebaseFirestore.getFirestoreForApp(appName), data); + Map updateData = FirestoreSerialize.parseReadableMap( + RNFirebaseFirestore.getFirestoreForApp(appName), + data + ); + transaction.update(documentReference.getRef(), updateData); break; case "delete": @@ -396,7 +644,11 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { if (!transactionHandler.aborted) { Log.d(TAG, "Transaction onSuccess!"); WritableMap eventMap = transactionHandler.createEventMap(null, "complete"); - Utils.sendEvent(getReactApplicationContext(), "firestore_transaction_event", eventMap); + Utils.sendEvent( + getReactApplicationContext(), + "firestore_transaction_event", + eventMap + ); } } }) @@ -405,8 +657,15 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { public void onFailure(@NonNull Exception e) { if (!transactionHandler.aborted) { Log.w(TAG, "Transaction onFailure.", e); - WritableMap eventMap = transactionHandler.createEventMap((FirebaseFirestoreException) e, "error"); - Utils.sendEvent(getReactApplicationContext(), "firestore_transaction_event", eventMap); + WritableMap eventMap = transactionHandler.createEventMap( + (FirebaseFirestoreException) e, + "error" + ); + Utils.sendEvent( + getReactApplicationContext(), + "firestore_transaction_event", + eventMap + ); } } }); @@ -414,158 +673,44 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { }); } - - /* - * INTERNALS/UTILS - */ - - /** - * Generates a js-like error from an exception and rejects the provided promise with it. - * - * @param exception Exception Exception normally from a task result. - * @param promise Promise react native promise - */ - static void promiseRejectException(Promise promise, FirebaseFirestoreException exception) { - WritableMap jsError = getJSError(exception); - promise.reject( - jsError.getString("code"), - jsError.getString("message"), - exception - ); - } - - /** - * Get a database instance for a specific firebase app instance - * - * @param appName - * @return - */ - static FirebaseFirestore getFirestoreForApp(String appName) { - FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); - return FirebaseFirestore.getInstance(firebaseApp); - } - /** * Get a collection reference for a specific app and path * - * @param appName - * @param filters - * @param orders - * @param options + * @param appName appName + * @param filters filters + * @param orders orders + * @param options options * @param path @return */ - private RNFirebaseFirestoreCollectionReference getCollectionForAppPath(String appName, String path, - ReadableArray filters, - ReadableArray orders, - ReadableMap options) { - return new RNFirebaseFirestoreCollectionReference(this.getReactApplicationContext(), appName, path, filters, orders, options); + private RNFirebaseFirestoreCollectionReference getCollectionForAppPath( + String appName, String path, + ReadableArray filters, + ReadableArray orders, + ReadableMap options + ) { + return new RNFirebaseFirestoreCollectionReference( + this.getReactApplicationContext(), + appName, + path, + filters, + orders, + options + ); } /** * Get a document reference for a specific app and path * - * @param appName - * @param path - * @return + * @param appName appName + * @param path path + * @return RNFirebaseFirestoreDocumentReference */ private RNFirebaseFirestoreDocumentReference getDocumentForAppPath(String appName, String path) { - return new RNFirebaseFirestoreDocumentReference(this.getReactApplicationContext(), appName, path); - } - - /** - * Convert as firebase DatabaseError instance into a writable map - * with the correct web-like error codes. - * - * @param nativeException - * @return - */ - static WritableMap getJSError(FirebaseFirestoreException nativeException) { - WritableMap errorMap = Arguments.createMap(); - errorMap.putInt("nativeErrorCode", nativeException.getCode().value()); - errorMap.putString("nativeErrorMessage", nativeException.getMessage()); - - String code; - String message; - String service = "Firestore"; - - // TODO: Proper error mappings - switch (nativeException.getCode()) { - case OK: - code = ErrorUtils.getCodeWithService(service, "ok"); - message = ErrorUtils.getMessageWithService("Ok.", service, code); - break; - case CANCELLED: - code = ErrorUtils.getCodeWithService(service, "cancelled"); - message = ErrorUtils.getMessageWithService("The operation was cancelled.", service, code); - break; - case UNKNOWN: - code = ErrorUtils.getCodeWithService(service, "unknown"); - message = ErrorUtils.getMessageWithService("Unknown error or an error from a different error domain.", service, code); - break; - case INVALID_ARGUMENT: - code = ErrorUtils.getCodeWithService(service, "invalid-argument"); - message = ErrorUtils.getMessageWithService("Client specified an invalid argument.", service, code); - break; - case DEADLINE_EXCEEDED: - code = ErrorUtils.getCodeWithService(service, "deadline-exceeded"); - message = ErrorUtils.getMessageWithService("Deadline expired before operation could complete.", service, code); - break; - case NOT_FOUND: - code = ErrorUtils.getCodeWithService(service, "not-found"); - message = ErrorUtils.getMessageWithService("Some requested document was not found.", service, code); - break; - case ALREADY_EXISTS: - code = ErrorUtils.getCodeWithService(service, "already-exists"); - message = ErrorUtils.getMessageWithService("Some document that we attempted to create already exists.", service, code); - break; - case PERMISSION_DENIED: - code = ErrorUtils.getCodeWithService(service, "permission-denied"); - message = ErrorUtils.getMessageWithService("The caller does not have permission to execute the specified operation.", service, code); - break; - case RESOURCE_EXHAUSTED: - code = ErrorUtils.getCodeWithService(service, "resource-exhausted"); - message = ErrorUtils.getMessageWithService("Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.", service, code); - break; - case FAILED_PRECONDITION: - code = ErrorUtils.getCodeWithService(service, "failed-precondition"); - message = ErrorUtils.getMessageWithService("Operation was rejected because the system is not in a state required for the operation`s execution.", service, code); - break; - case ABORTED: - code = ErrorUtils.getCodeWithService(service, "aborted"); - message = ErrorUtils.getMessageWithService("The operation was aborted, typically due to a concurrency issue like transaction aborts, etc.", service, code); - break; - case OUT_OF_RANGE: - code = ErrorUtils.getCodeWithService(service, "out-of-range"); - message = ErrorUtils.getMessageWithService("Operation was attempted past the valid range.", service, code); - break; - case UNIMPLEMENTED: - code = ErrorUtils.getCodeWithService(service, "unimplemented"); - message = ErrorUtils.getMessageWithService("Operation is not implemented or not supported/enabled.", service, code); - break; - case INTERNAL: - code = ErrorUtils.getCodeWithService(service, "internal"); - message = ErrorUtils.getMessageWithService("Internal errors.", service, code); - break; - case UNAVAILABLE: - code = ErrorUtils.getCodeWithService(service, "unavailable"); - message = ErrorUtils.getMessageWithService("The service is currently unavailable.", service, code); - break; - case DATA_LOSS: - code = ErrorUtils.getCodeWithService(service, "data-loss"); - message = ErrorUtils.getMessageWithService("Unrecoverable data loss or corruption.", service, code); - break; - case UNAUTHENTICATED: - code = ErrorUtils.getCodeWithService(service, "unauthenticated"); - message = ErrorUtils.getMessageWithService("The request does not have valid authentication credentials for the operation.", service, code); - break; - default: - code = ErrorUtils.getCodeWithService(service, "unknown"); - message = ErrorUtils.getMessageWithService("An unknown error occurred.", service, code); - } - - errorMap.putString("code", code); - errorMap.putString("message", message); - return errorMap; + return new RNFirebaseFirestoreDocumentReference( + this.getReactApplicationContext(), + appName, + path + ); } /** diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java index 548f942f..b251ec5f 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreCollectionReference.java @@ -1,6 +1,7 @@ package io.invertase.firebase.firestore; +import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; @@ -22,28 +23,32 @@ import com.google.firebase.firestore.Query; import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.Source; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import io.invertase.firebase.Utils; -public class RNFirebaseFirestoreCollectionReference { +class RNFirebaseFirestoreCollectionReference { private static final String TAG = "RNFSCollectionReference"; private static Map collectionSnapshotListeners = new HashMap<>(); - private final String appName; private final String path; - private final ReadableArray filters; - private final ReadableArray orders; - private final ReadableMap options; private final Query query; + private final String appName; + private final ReadableMap options; + private final ReadableArray orders; + private final ReadableArray filters; private ReactContext reactContext; - RNFirebaseFirestoreCollectionReference(ReactContext reactContext, String appName, String path, - ReadableArray filters, ReadableArray orders, - ReadableMap options) { + RNFirebaseFirestoreCollectionReference( + ReactContext reactContext, + String appName, + String path, + ReadableArray filters, + ReadableArray orders, + ReadableMap options + ) { this.appName = appName; this.path = path; this.filters = filters; @@ -53,6 +58,13 @@ public class RNFirebaseFirestoreCollectionReference { this.reactContext = reactContext; } + static void offSnapshot(final String listenerId) { + ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove(listenerId); + if (listenerRegistration != null) { + listenerRegistration.remove(); + } + } + void get(ReadableMap getOptions, final Promise promise) { Source source; if (getOptions != null && getOptions.hasKey("source")) { @@ -67,29 +79,34 @@ public class RNFirebaseFirestoreCollectionReference { } else { source = Source.DEFAULT; } + + @SuppressLint("StaticFieldLeak") final QuerySnapshotSerializeAsyncTask serializeAsyncTask = new QuerySnapshotSerializeAsyncTask( + reactContext, this + ) { + @Override + protected void onPostExecute(WritableMap writableMap) { + promise.resolve(writableMap); + } + }; + query.get(source).addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { if (task.isSuccessful()) { Log.d(TAG, "get:onComplete:success"); - WritableMap data = FirestoreSerialize.snapshotToWritableMap(task.getResult()); - promise.resolve(data); + serializeAsyncTask.execute(task.getResult()); } else { Log.e(TAG, "get:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); + RNFirebaseFirestore.promiseRejectException( + promise, + (FirebaseFirestoreException) task.getException() + ); } } }); } - public static void offSnapshot(final String listenerId) { - ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove(listenerId); - if (listenerRegistration != null) { - listenerRegistration.remove(); - } - } - - public void onSnapshot(final String listenerId, final ReadableMap queryListenOptions) { + void onSnapshot(final String listenerId, final ReadableMap queryListenOptions) { if (!collectionSnapshotListeners.containsKey(listenerId)) { final EventListener listener = new EventListener() { @Override @@ -97,7 +114,8 @@ public class RNFirebaseFirestoreCollectionReference { if (exception == null) { handleQuerySnapshotEvent(listenerId, querySnapshot); } else { - ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove(listenerId); + ListenerRegistration listenerRegistration = collectionSnapshotListeners.remove( + listenerId); if (listenerRegistration != null) { listenerRegistration.remove(); } @@ -115,7 +133,10 @@ public class RNFirebaseFirestoreCollectionReference { metadataChanges = MetadataChanges.EXCLUDE; } - ListenerRegistration listenerRegistration = this.query.addSnapshotListener(metadataChanges, listener); + ListenerRegistration listenerRegistration = this.query.addSnapshotListener( + metadataChanges, + listener + ); collectionSnapshotListeners.put(listenerId, listenerRegistration); } } @@ -170,7 +191,7 @@ public class RNFirebaseFirestoreCollectionReference { } else { ReadableArray fieldPathElements = fieldPathMap.getArray("elements"); String[] fieldPathArray = new String[fieldPathElements.size()]; - for (int j=0; j order = (Map) o; String direction = (String) order.get("direction"); Map fieldPathMap = (Map) order.get("fieldPath"); - String fieldPathType = (String)fieldPathMap.get("type"); + String fieldPathType = (String) fieldPathMap.get("type"); if (fieldPathType.equals("string")) { - String fieldPath = (String)fieldPathMap.get("string"); + String fieldPath = (String) fieldPathMap.get("string"); query = query.orderBy(fieldPath, Query.Direction.valueOf(direction)); } else { - List fieldPathElements = (List)fieldPathMap.get("elements"); + List fieldPathElements = (List) fieldPathMap.get("elements"); FieldPath fieldPath = FieldPath.of(fieldPathElements.toArray(new String[fieldPathElements.size()])); query = query.orderBy(fieldPath, Query.Direction.valueOf(direction)); } @@ -218,57 +239,79 @@ public class RNFirebaseFirestoreCollectionReference { private Query applyOptions(FirebaseFirestore firestore, Query query) { if (options.hasKey("endAt")) { - List endAtList = FirestoreSerialize.parseReadableArray(firestore, options.getArray("endAt")); + List endAtList = FirestoreSerialize.parseReadableArray( + firestore, + options.getArray("endAt") + ); query = query.endAt(endAtList.toArray()); } + if (options.hasKey("endBefore")) { - List endBeforeList = FirestoreSerialize.parseReadableArray(firestore, options.getArray("endBefore")); + List endBeforeList = FirestoreSerialize.parseReadableArray( + firestore, + options.getArray("endBefore") + ); query = query.endBefore(endBeforeList.toArray()); } + if (options.hasKey("limit")) { int limit = options.getInt("limit"); query = query.limit(limit); } - if (options.hasKey("offset")) { - // Android doesn't support offset - } - if (options.hasKey("selectFields")) { - // Android doesn't support selectFields - } +// if (options.hasKey("offset")) { + // Android doesn't support offset +// } +// if (options.hasKey("selectFields")) { + // Android doesn't support selectFields +// } if (options.hasKey("startAfter")) { - List startAfterList= FirestoreSerialize.parseReadableArray(firestore, options.getArray("startAfter")); + List startAfterList = FirestoreSerialize.parseReadableArray( + firestore, + options.getArray("startAfter") + ); query = query.startAfter(startAfterList.toArray()); } + if (options.hasKey("startAt")) { - List startAtList= FirestoreSerialize.parseReadableArray(firestore, options.getArray("startAt")); + List startAtList = FirestoreSerialize.parseReadableArray( + firestore, + options.getArray("startAt") + ); query = query.startAt(startAtList.toArray()); } + return query; } /** * Handles documentSnapshot events. * - * @param listenerId - * @param querySnapshot + * @param listenerId id + * @param querySnapshot snapshot */ - private void handleQuerySnapshotEvent(String listenerId, QuerySnapshot querySnapshot) { - WritableMap event = Arguments.createMap(); - WritableMap data = FirestoreSerialize.snapshotToWritableMap(querySnapshot); + private void handleQuerySnapshotEvent(final String listenerId, QuerySnapshot querySnapshot) { + @SuppressLint("StaticFieldLeak") final QuerySnapshotSerializeAsyncTask serializeAsyncTask = new QuerySnapshotSerializeAsyncTask( + reactContext, this + ) { + @Override + protected void onPostExecute(WritableMap writableMap) { + WritableMap event = Arguments.createMap(); + event.putString("path", path); + event.putString("appName", appName); + event.putString("listenerId", listenerId); + event.putMap("querySnapshot", writableMap); + Utils.sendEvent(reactContext, "firestore_collection_sync_event", event); + } + }; - event.putString("appName", appName); - event.putString("path", path); - event.putString("listenerId", listenerId); - event.putMap("querySnapshot", data); - - Utils.sendEvent(reactContext, "firestore_collection_sync_event", event); + serializeAsyncTask.execute(querySnapshot); } /** * Handles a documentSnapshot error event * - * @param listenerId - * @param exception + * @param listenerId id + * @param exception exception */ private void handleQuerySnapshotError(String listenerId, FirebaseFirestoreException exception) { WritableMap event = Arguments.createMap(); diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java index ebb87a8d..2690afa1 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreDocumentReference.java @@ -1,5 +1,6 @@ package io.invertase.firebase.firestore; +import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; @@ -31,17 +32,24 @@ public class RNFirebaseFirestoreDocumentReference { private final String appName; private final String path; - private ReactContext reactContext; private final DocumentReference ref; + private ReactContext reactContext; RNFirebaseFirestoreDocumentReference(ReactContext reactContext, String appName, String path) { - this.appName = appName; this.path = path; + this.appName = appName; this.reactContext = reactContext; this.ref = RNFirebaseFirestore.getFirestoreForApp(appName).document(path); } - public void delete(final Promise promise) { + static void offSnapshot(final String listenerId) { + ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId); + if (listenerRegistration != null) { + listenerRegistration.remove(); + } + } + + void delete(final Promise promise) { this.ref.delete().addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { @@ -50,7 +58,10 @@ public class RNFirebaseFirestoreDocumentReference { promise.resolve(null); } else { Log.e(TAG, "delete:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); + RNFirebaseFirestore.promiseRejectException( + promise, + (FirebaseFirestoreException) task.getException() + ); } } }); @@ -58,6 +69,7 @@ public class RNFirebaseFirestoreDocumentReference { void get(final ReadableMap getOptions, final Promise promise) { Source source; + if (getOptions != null && getOptions.hasKey("source")) { String optionsSource = getOptions.getString("source"); if ("server".equals(optionsSource)) { @@ -70,45 +82,57 @@ public class RNFirebaseFirestoreDocumentReference { } else { source = Source.DEFAULT; } + + @SuppressLint("StaticFieldLeak") final DocumentSnapshotSerializeAsyncTask serializeAsyncTask = new DocumentSnapshotSerializeAsyncTask( + reactContext, this + ) { + @Override + protected void onPostExecute(WritableMap writableMap) { + promise.resolve(writableMap); + } + }; + this.ref.get(source).addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { if (task.isSuccessful()) { Log.d(TAG, "get:onComplete:success"); - WritableMap data = FirestoreSerialize.snapshotToWritableMap(task.getResult()); - promise.resolve(data); + serializeAsyncTask.execute(task.getResult()); } else { Log.e(TAG, "get:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); + RNFirebaseFirestore.promiseRejectException( + promise, + (FirebaseFirestoreException) task.getException() + ); } } }); } - public static void offSnapshot(final String listenerId) { - ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId); - if (listenerRegistration != null) { - listenerRegistration.remove(); - } - } - - public void onSnapshot(final String listenerId, final ReadableMap docListenOptions) { + void onSnapshot(final String listenerId, final ReadableMap docListenOptions) { if (!documentSnapshotListeners.containsKey(listenerId)) { final EventListener listener = new EventListener() { @Override - public void onEvent(DocumentSnapshot documentSnapshot, FirebaseFirestoreException exception) { + public void onEvent( + DocumentSnapshot documentSnapshot, + FirebaseFirestoreException exception + ) { if (exception == null) { handleDocumentSnapshotEvent(listenerId, documentSnapshot); } else { ListenerRegistration listenerRegistration = documentSnapshotListeners.remove(listenerId); + if (listenerRegistration != null) { listenerRegistration.remove(); } + handleDocumentSnapshotError(listenerId, exception); } } }; + MetadataChanges metadataChanges; + if (docListenOptions != null && docListenOptions.hasKey("includeMetadataChanges") && docListenOptions.getBoolean("includeMetadataChanges")) { @@ -116,19 +140,30 @@ public class RNFirebaseFirestoreDocumentReference { } else { metadataChanges = MetadataChanges.EXCLUDE; } - ListenerRegistration listenerRegistration = this.ref.addSnapshotListener(metadataChanges, listener); + + ListenerRegistration listenerRegistration = this.ref.addSnapshotListener( + metadataChanges, + listener + ); + documentSnapshotListeners.put(listenerId, listenerRegistration); } } public void set(final ReadableMap data, final ReadableMap options, final Promise promise) { - Map map = FirestoreSerialize.parseReadableMap(RNFirebaseFirestore.getFirestoreForApp(appName), data); Task task; + + Map map = FirestoreSerialize.parseReadableMap( + RNFirebaseFirestore.getFirestoreForApp(appName), + data + ); + if (options != null && options.hasKey("merge") && options.getBoolean("merge")) { task = this.ref.set(map, SetOptions.merge()); } else { task = this.ref.set(map); } + task.addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { @@ -137,14 +172,21 @@ public class RNFirebaseFirestoreDocumentReference { promise.resolve(null); } else { Log.e(TAG, "set:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); + RNFirebaseFirestore.promiseRejectException( + promise, + (FirebaseFirestoreException) task.getException() + ); } } }); } - public void update(final ReadableMap data, final Promise promise) { - Map map = FirestoreSerialize.parseReadableMap(RNFirebaseFirestore.getFirestoreForApp(appName), data); + void update(final ReadableMap data, final Promise promise) { + Map map = FirestoreSerialize.parseReadableMap( + RNFirebaseFirestore.getFirestoreForApp(appName), + data + ); + this.ref.update(map).addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { @@ -153,7 +195,10 @@ public class RNFirebaseFirestoreDocumentReference { promise.resolve(null); } else { Log.e(TAG, "update:onComplete:failure", task.getException()); - RNFirebaseFirestore.promiseRejectException(promise, (FirebaseFirestoreException)task.getException()); + RNFirebaseFirestore.promiseRejectException( + promise, + (FirebaseFirestoreException) task.getException() + ); } } }); @@ -163,7 +208,7 @@ public class RNFirebaseFirestoreDocumentReference { * INTERNALS/UTILS */ - public DocumentReference getRef() { + DocumentReference getRef() { return ref; } @@ -174,32 +219,44 @@ public class RNFirebaseFirestoreDocumentReference { /** * Handles documentSnapshot events. * - * @param listenerId - * @param documentSnapshot + * @param listenerId id + * @param documentSnapshot snapshot */ - private void handleDocumentSnapshotEvent(String listenerId, DocumentSnapshot documentSnapshot) { - WritableMap event = Arguments.createMap(); - WritableMap data = FirestoreSerialize.snapshotToWritableMap(documentSnapshot); + private void handleDocumentSnapshotEvent( + final String listenerId, + DocumentSnapshot documentSnapshot + ) { + @SuppressLint("StaticFieldLeak") final DocumentSnapshotSerializeAsyncTask serializeAsyncTask = new DocumentSnapshotSerializeAsyncTask( + reactContext, this + ) { + @Override + protected void onPostExecute(WritableMap writableMap) { + WritableMap event = Arguments.createMap(); + event.putString("path", path); + event.putString("appName", appName); + event.putString("listenerId", listenerId); + event.putMap("documentSnapshot", writableMap); + Utils.sendEvent(reactContext, "firestore_document_sync_event", event); + } + }; - event.putString("appName", appName); - event.putString("path", path); - event.putString("listenerId", listenerId); - event.putMap("documentSnapshot", data); - - Utils.sendEvent(reactContext, "firestore_document_sync_event", event); + serializeAsyncTask.execute(documentSnapshot); } /** * Handles a documentSnapshot error event * - * @param listenerId - * @param exception + * @param listenerId id + * @param exception exception */ - private void handleDocumentSnapshotError(String listenerId, FirebaseFirestoreException exception) { + private void handleDocumentSnapshotError( + String listenerId, + FirebaseFirestoreException exception + ) { WritableMap event = Arguments.createMap(); - event.putString("appName", appName); event.putString("path", path); + event.putString("appName", appName); event.putString("listenerId", listenerId); event.putMap("error", RNFirebaseFirestore.getJSError(exception)); diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java index f971daea..22629c35 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestorePackage.java @@ -1,7 +1,6 @@ package io.invertase.firebase.firestore; 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; @@ -29,7 +28,7 @@ public class RNFirebaseFirestorePackage implements ReactPackage { } /** - * @param reactContext + * @param reactContext reactContext * @return a list of view managers that should be registered with {@link UIManagerModule} */ @Override diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreTransactionHandler.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreTransactionHandler.java index ef136ed7..dd908eb8 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreTransactionHandler.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestoreTransactionHandler.java @@ -17,17 +17,16 @@ import javax.annotation.Nullable; class RNFirebaseFirestoreTransactionHandler { + private final ReentrantLock lock; + private final Condition condition; + boolean aborted = false; + boolean timeout = false; private String appName; private long timeoutAt; private int transactionId; - private final ReentrantLock lock; - private final Condition condition; private ReadableArray commandBuffer; private Transaction firestoreTransaction; - boolean aborted = false; - boolean timeout = false; - RNFirebaseFirestoreTransactionHandler(String app, int id) { appName = app; transactionId = id; diff --git a/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java b/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java index 00cf83f5..c1cebe80 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java +++ b/android/src/main/java/io/invertase/firebase/notifications/DisplayNotificationTask.java @@ -192,7 +192,7 @@ public class DisplayNotificationTask extends AsyncTask { nb = nb.setOngoing(android.getBoolean("ongoing")); } if (android.containsKey("onlyAlertOnce")) { - nb = nb.setOngoing(android.getBoolean("onlyAlertOnce")); + nb = nb.setOnlyAlertOnce(android.getBoolean("onlyAlertOnce")); } if (android.containsKey("people")) { List people = android.getStringArrayList("people"); @@ -279,13 +279,18 @@ public class DisplayNotificationTask extends AsyncTask { } } + String tag = null; + if (android.containsKey("tag")) { + tag = android.getString("tag"); + } + // Create the notification intent PendingIntent contentIntent = createIntent(intentClass, notification, android.getString("clickAction")); nb = nb.setContentIntent(contentIntent); // Build the notification and send it Notification builtNotification = nb.build(); - notificationManager.notify(notificationId.hashCode(), builtNotification); + notificationManager.notify(tag, notificationId.hashCode(), builtNotification); if (reactContext != null) { Utils.sendEvent(reactContext, "notifications_notification_displayed", Arguments.fromBundle(notification)); diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java index 185dc4d5..f347306d 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -17,6 +17,7 @@ import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.service.notification.StatusBarNotification; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; @@ -153,7 +154,7 @@ public class RNFirebaseNotificationManager { if (!notification.getBundle("schedule").containsKey("repeated") || !notification.getBundle("schedule").getBoolean("repeated")) { String notificationId = notification.getString("notificationId"); - preferences.edit().remove(notificationId).apply();; + preferences.edit().remove(notificationId).apply(); } if (Utils.isAppInForeground(context)) { @@ -195,6 +196,15 @@ public class RNFirebaseNotificationManager { promise.resolve(null); } + public void removeDeliveredNotificationsByTag(String tag, Promise promise) { + StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications(); + for (StatusBarNotification statusBarNotification : statusBarNotifications) { + if (statusBarNotification.getTag() == tag) { + notificationManager.cancel(statusBarNotification.getTag(), statusBarNotification.getId()); + } + } + promise.resolve(null); + } public void rescheduleNotifications() { ArrayList bundles = getScheduledNotifications(); @@ -320,15 +330,19 @@ public class RNFirebaseNotificationManager { String notificationId = notification.getString("notificationId"); Bundle schedule = notification.getBundle("schedule"); - // fireDate is stored in the Bundle as Long after notifications are rescheduled. - // This would lead to a fireDate of 0.0 when trying to extract a Double from the bundle. - // Instead always try extract a Long + // fireDate may be stored in the Bundle as 2 different types that we need to handle: + // 1. Double - when a call comes directly from React + // 2. Long - when notifications are rescheduled from boot service (Bundle is loaded from prefences). + // At the end we need Long value (timestamp) for the scheduler Long fireDate = -1L; - try { - fireDate = (long) schedule.getDouble("fireDate", -1); - } catch (ClassCastException e) { - fireDate = schedule.getLong("fireDate", -1); + Object fireDateObject = schedule.get("fireDate"); + if (fireDateObject instanceof Long) { + fireDate = (Long) fireDateObject; + } else if (fireDateObject instanceof Double) { + Double fireDateDouble = (Double) fireDateObject; + fireDate = fireDateDouble.longValue(); } + if (fireDate == -1) { if (promise == null) { Log.e(TAG, "Missing schedule information"); @@ -361,14 +375,33 @@ public class RNFirebaseNotificationManager { // If fireDate you specify is in the past, the alarm triggers immediately. // So we need to adjust the time for correct operation. if (fireDate < System.currentTimeMillis()) { + Log.w(TAG, "Scheduled notification date is in the past, will adjust it to be in future"); Calendar newFireDate = Calendar.getInstance(); - Calendar currentFireDate = Calendar.getInstance(); - currentFireDate.setTimeInMillis(fireDate); + Calendar pastFireDate = Calendar.getInstance(); + pastFireDate.setTimeInMillis(fireDate); - newFireDate.add(Calendar.DATE, 1); - newFireDate.set(Calendar.HOUR_OF_DAY, currentFireDate.get(Calendar.HOUR_OF_DAY)); - newFireDate.set(Calendar.MINUTE, currentFireDate.get(Calendar.MINUTE)); - newFireDate.set(Calendar.SECOND, currentFireDate.get(Calendar.SECOND)); + newFireDate.set(Calendar.SECOND, pastFireDate.get(Calendar.SECOND)); + + switch (schedule.getString("repeatInterval")) { + case "minute": + newFireDate.add(Calendar.MINUTE, 1); + break; + case "hour": + newFireDate.set(Calendar.MINUTE, pastFireDate.get(Calendar.MINUTE)); + newFireDate.add(Calendar.HOUR, 1); + break; + case "day": + newFireDate.set(Calendar.MINUTE, pastFireDate.get(Calendar.MINUTE)); + newFireDate.set(Calendar.HOUR_OF_DAY, pastFireDate.get(Calendar.HOUR_OF_DAY)); + newFireDate.add(Calendar.DATE, 1); + break; + case "week": + newFireDate.set(Calendar.MINUTE, pastFireDate.get(Calendar.MINUTE)); + newFireDate.set(Calendar.HOUR_OF_DAY, pastFireDate.get(Calendar.HOUR_OF_DAY)); + newFireDate.set(Calendar.DATE, pastFireDate.get(Calendar.DATE)); + newFireDate.add(Calendar.DATE, 7); + break; + } fireDate = newFireDate.getTimeInMillis(); } @@ -401,14 +434,14 @@ public class RNFirebaseNotificationManager { return; } - alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, fireDate.longValue(), interval, pendingIntent); + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent); } else { if (schedule.containsKey("exact") && schedule.getBoolean("exact") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate.longValue(), pendingIntent); + alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); } else { - alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate.longValue(), pendingIntent); + alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index d9ffd5c3..58f7a3de 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.RemoteInput; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; @@ -30,6 +31,8 @@ import io.invertase.firebase.Utils; import io.invertase.firebase.messaging.RNFirebaseMessagingService; import me.leolin.shortcutbadger.ShortcutBadger; +import static io.invertase.firebase.Utils.getResId; + public class RNFirebaseNotifications extends ReactContextBaseJavaModule implements ActivityEventListener { private static final String BADGE_FILE = "BadgeCountFile"; private static final String BADGE_KEY = "BadgeCount"; @@ -112,6 +115,11 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen notificationManager.removeDeliveredNotification(notificationId, promise); } + @ReactMethod + public void removeDeliveredNotificationsByTag(String tag, Promise promise) { + notificationManager.removeDeliveredNotificationsByTag(tag, promise); + } + @ReactMethod public void setBadge(int badge, Promise promise) { // Store the badge count for later retrieval @@ -264,7 +272,10 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen WritableMap dataMap = Arguments.createMap(); // Cross platform notification properties - notificationMap.putString("body", notification.getBody()); + String body = getNotificationBody(notification); + if (body != null) { + notificationMap.putString("body", body); + } if (message.getData() != null) { for (Map.Entry e : message.getData().entrySet()) { dataMap.putString(e.getKey(), e.getValue()); @@ -277,8 +288,9 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen if (notification.getSound() != null) { notificationMap.putString("sound", notification.getSound()); } - if (notification.getTitle() != null) { - notificationMap.putString("title", notification.getTitle()); + String title = getNotificationTitle(notification); + if (title != null) { + notificationMap.putString("title", title); } // Android specific notification properties @@ -296,12 +308,39 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen } if (notification.getTag() != null) { androidMap.putString("group", notification.getTag()); + androidMap.putString("tag", notification.getTag()); } notificationMap.putMap("android", androidMap); return notificationMap; } + private @Nullable String getNotificationBody(RemoteMessage.Notification notification) { + String body = notification.getBody(); + String bodyLocKey = notification.getBodyLocalizationKey(); + if (bodyLocKey != null) { + String[] bodyLocArgs = notification.getBodyLocalizationArgs(); + Context ctx = getReactApplicationContext(); + int resId = getResId(ctx, bodyLocKey); + return ctx.getResources().getString(resId, (Object[]) bodyLocArgs); + } else { + return body; + } + } + + private @Nullable String getNotificationTitle(RemoteMessage.Notification notification) { + String title = notification.getTitle(); + String titleLocKey = notification.getTitleLocalizationKey(); + if (titleLocKey != null) { + String[] titleLocArgs = notification.getTitleLocalizationArgs(); + Context ctx = getReactApplicationContext(); + int resId = getResId(ctx, titleLocKey); + return ctx.getResources().getString(resId, (Object[]) titleLocArgs); + } else { + return title; + } + } + private class RemoteNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { diff --git a/bridge/e2e/iid/iid.e2e.js b/bridge/e2e/iid/iid.e2e.js index 1c249e5a..18f3a07f 100644 --- a/bridge/e2e/iid/iid.e2e.js +++ b/bridge/e2e/iid/iid.e2e.js @@ -10,13 +10,56 @@ describe('iid()', () => { it('deletes the current instance id', async () => { const iidBefore = await firebase.iid().get(); iidBefore.should.be.a.String(); - await firebase.iid().delete(); const iidAfter = await firebase.iid().get(); iidAfter.should.be.a.String(); - iidBefore.should.not.equal(iidAfter); }); }); + + describe('getToken()', () => { + it('should return an FCM token from getToken with arguments', async () => { + const authorizedEntity = firebase.iid().app.options.messagingSenderId; + + await firebase.iid().delete(); + const token = await firebase.iid().getToken(authorizedEntity, '*'); + token.should.be.a.String(); + }); + + it('should return an FCM token from getToken without arguments', async () => { + await firebase.iid().delete(); + const token = await firebase.iid().getToken(); + token.should.be.a.String(); + }); + + it('should return an FCM token from getToken with 1 argument', async () => { + const authorizedEntity = firebase.iid().app.options.messagingSenderId; + + await firebase.iid().delete(); + const token = await firebase.iid().getToken(authorizedEntity); + token.should.be.a.String(); + }); + }); + + describe('deleteToken()', () => { + it('should return nil from deleteToken with arguments', async () => { + const authorizedEntity = firebase.iid().app.options.messagingSenderId; + + const token = await firebase.iid().deleteToken(authorizedEntity, '*'); + should.not.exist(token); + }); + + it('should return nil from deleteToken without arguments', async () => { + const token = await firebase.iid().deleteToken(); + should.not.exist(token); + }); + + it('should return nil from deleteToken with 1 argument', async () => { + const authorizedEntity = firebase.iid().app.options.messagingSenderId; + + const token = await firebase.iid().deleteToken(authorizedEntity); + should.not.exist(token); + }); + }); }); diff --git a/ios/RNFirebase/auth/RNFirebaseAuth.m b/ios/RNFirebase/auth/RNFirebaseAuth.m index 85b7c180..97e6e970 100644 --- a/ios/RNFirebase/auth/RNFirebaseAuth.m +++ b/ios/RNFirebase/auth/RNFirebaseAuth.m @@ -688,9 +688,23 @@ RCT_EXPORT_METHOD(checkActionCode: actionType = @"EMAIL_SIGNIN"; break; } + + NSMutableDictionary *data = [NSMutableDictionary dictionary]; - NSDictionary *result = @{@"data": @{@"email": [info dataForKey:FIRActionCodeEmailKey], @"fromEmail": [info dataForKey:FIRActionCodeFromEmailKey],}, @"actionType": actionType,}; + if ([info dataForKey:FIRActionCodeEmailKey] != nil) { + [data setValue:[info dataForKey:FIRActionCodeEmailKey] forKey:@"email"]; + } else { + [data setValue:[NSNull null] forKey:@"email"]; + } + if ([info dataForKey:FIRActionCodeFromEmailKey] != nil) { + [data setValue:[info dataForKey:FIRActionCodeFromEmailKey] forKey:@"fromEmail"]; + } else { + [data setValue:[NSNull null] forKey:@"fromEmail"]; + } + + NSDictionary *result = @{ @"data": data, @"actionType": actionType }; + resolve(result); } }]; diff --git a/ios/RNFirebase/config/RNFirebaseRemoteConfig.m b/ios/RNFirebase/config/RNFirebaseRemoteConfig.m index fe9c4061..59f8d17e 100644 --- a/ios/RNFirebase/config/RNFirebaseRemoteConfig.m +++ b/ios/RNFirebase/config/RNFirebaseRemoteConfig.m @@ -19,6 +19,16 @@ NSString *convertFIRRemoteConfigFetchStatusToNSString(FIRRemoteConfigFetchStatus } } +NSString *convertFIRRemoteConfigFetchStatusToNSStringDescription(FIRRemoteConfigFetchStatus value) { + switch (value) { + case FIRRemoteConfigFetchStatusThrottled: + return @"fetch() operation cannot be completed successfully, due to throttling."; + case FIRRemoteConfigFetchStatusNoFetchYet: + default: + return @"fetch() operation cannot be completed successfully."; + } +} + NSString *convertFIRRemoteConfigSourceToNSString(FIRRemoteConfigSource value) { switch (value) { case FIRRemoteConfigSourceDefault: @@ -49,9 +59,9 @@ RCT_EXPORT_METHOD(fetch: (RCTPromiseRejectBlock) reject) { [[FIRRemoteConfig remoteConfig] fetchWithCompletionHandler:^(FIRRemoteConfigFetchStatus status, NSError *__nullable error) { if (error) { - reject(convertFIRRemoteConfigFetchStatusToNSString(status), error.localizedDescription, error); + reject(convertFIRRemoteConfigFetchStatusToNSString(status), convertFIRRemoteConfigFetchStatusToNSStringDescription(status), error); } else { - resolve(convertFIRRemoteConfigFetchStatusToNSString(status)); + resolve([NSNull null]); } }]; } @@ -63,9 +73,9 @@ RCT_EXPORT_METHOD(fetchWithExpirationDuration: rejecter:(RCTPromiseRejectBlock)reject) { [[FIRRemoteConfig remoteConfig] fetchWithExpirationDuration:expirationDuration.doubleValue completionHandler:^(FIRRemoteConfigFetchStatus status, NSError *__nullable error) { if (error) { - reject(convertFIRRemoteConfigFetchStatusToNSString(status), error.localizedDescription, error); + reject(convertFIRRemoteConfigFetchStatusToNSString(status), convertFIRRemoteConfigFetchStatusToNSStringDescription(status), error); } else { - resolve(convertFIRRemoteConfigFetchStatusToNSString(status)); + resolve([NSNull null]); } }]; } diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index bc9caa57..704875a0 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -108,7 +108,22 @@ didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { // ** Start React Module methods ** RCT_EXPORT_METHOD(getToken:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - resolve([[FIRInstanceID instanceID] token]); + if (initialToken) { + resolve(initialToken); + } else if ([[FIRInstanceID instanceID] token]) { + resolve([[FIRInstanceID instanceID] token]); + } else { + NSString * senderId = [[FIRApp defaultApp] options].GCMSenderID; + [[FIRMessaging messaging] retrieveFCMTokenForSenderID:senderId completion:^(NSString * _Nullable FCMToken, NSError * _Nullable error) { + if (error) { + reject(@"messaging/fcm-token-error", @"Failed to retrieve FCM token.", error); + } else if (FCMToken) { + resolve(FCMToken); + } else { + resolve([NSNull null]); + } + }]; + } } RCT_EXPORT_METHOD(requestPermission:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 274f4d7d..2b8a0553 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -372,7 +372,7 @@ RCT_EXPORT_METHOD(setBadge:(NSInteger) number resolve(nil); }); } - + RCT_EXPORT_METHOD(jsInitialised:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { jsReady = TRUE; resolve(nil); @@ -487,11 +487,11 @@ RCT_EXPORT_METHOD(jsInitialised:(RCTPromiseResolveBlock)resolve rejecter:(RCTPro NSString *identifier = a[@"identifier"]; NSURL *url = [NSURL fileURLWithPath:a[@"url"]]; NSMutableDictionary *attachmentOptions = nil; - + if (a[@"options"]) { NSDictionary *options = a[@"options"]; attachmentOptions = [[NSMutableDictionary alloc] init]; - + for (id key in options) { if ([key isEqualToString:@"typeHint"]) { attachmentOptions[UNNotificationAttachmentOptionsTypeHintKey] = options[key]; diff --git a/lib/index.d.ts b/lib/index.d.ts index 45c5d54b..a2033c9d 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1089,8 +1089,8 @@ declare module 'react-native-firebase' { interface InstanceId { delete(): Promise; get(): Promise; - getToken(authorizedEntity: string, scope: string): Promise; - deleteToken(authorizedEntity: string, scope: string): Promise; + getToken(authorizedEntity?: string, scope?: string): Promise; + deleteToken(authorizedEntity?: string, scope?: string): Promise; } } @@ -1213,6 +1213,7 @@ declare module 'react-native-firebase' { showWhen?: boolean; smallIcon?: any; sortKey?: string; + tag?: string; ticker?: string; timeoutAfter?: number; usesChronometer?: boolean; @@ -1265,6 +1266,7 @@ declare module 'react-native-firebase' { setShowWhen(showWhen: boolean): Notification; setSmallIcon(icon: string, level?: number): Notification; setSortKey(sortKey: string): Notification; + setTag(tag: string): Notification; setTicker(ticker: string): Notification; setTimeoutAfter(timeoutAfter: number): Notification; setUsesChronometer(usesChronometer: boolean): Notification; diff --git a/lib/modules/iid/index.js b/lib/modules/iid/index.js index 4ede6748..773fb033 100644 --- a/lib/modules/iid/index.js +++ b/lib/modules/iid/index.js @@ -7,8 +7,8 @@ import { getNativeModule } from '../../utils/native'; import type App from '../core/app'; -export const MODULE_NAME = 'RNFirebaseInstanceId'; export const NAMESPACE = 'iid'; +export const MODULE_NAME = 'RNFirebaseInstanceId'; export default class InstanceId extends ModuleBase { constructor(app: App) { @@ -20,20 +20,51 @@ export default class InstanceId extends ModuleBase { }); } - delete(): Promise { - return getNativeModule(this).delete(); - } - + /** + * Get the current Instance ID. + * + * @returns {*} + */ get(): Promise { return getNativeModule(this).get(); } - getToken(authorizedEntity: string, scope: string): Promise { - return getNativeModule(this).getToken(authorizedEntity, scope); + /** + * Delete the current Instance ID. + * + * @returns {*} + */ + delete(): Promise { + return getNativeModule(this).delete(); } - deleteToken(authorizedEntity: string, scope: string): Promise { - return getNativeModule(this).deleteToken(authorizedEntity, scope); + /** + * Get a token that authorizes an Entity to perform an action on behalf + * of the application identified by Instance ID. + * + * @param authorizedEntity + * @param scope + * @returns {Promise} + */ + getToken(authorizedEntity?: string, scope?: string): Promise { + return getNativeModule(this).getToken( + authorizedEntity || this.app.options.messagingSenderId, + scope || '*' + ); + } + + /** + * Revokes access to a scope (action) for an entity previously authorized by getToken(). + * + * @param authorizedEntity + * @param scope + * @returns {Promise} + */ + deleteToken(authorizedEntity?: string, scope?: string): Promise { + return getNativeModule(this).deleteToken( + authorizedEntity || this.app.options.messagingSenderId, + scope || '*' + ); } } diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index e76facb7..279be7fe 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -53,6 +53,7 @@ export default class AndroidNotification { _smallIcon: SmallIcon; _sortKey: string | void; // TODO: style: Style; // Need to figure out if this can work + _tag: string | void; _ticker: string | void; _timeoutAfter: number | void; _usesChronometer: boolean | void; @@ -106,6 +107,7 @@ export default class AndroidNotification { this._showWhen = data.showWhen; this._smallIcon = data.smallIcon; this._sortKey = data.sortKey; + this._tag = data.tag; this._ticker = data.ticker; this._timeoutAfter = data.timeoutAfter; this._usesChronometer = data.usesChronometer; @@ -238,6 +240,10 @@ export default class AndroidNotification { return this._sortKey; } + get tag(): ?string { + return this._tag; + } + get ticker(): ?string { return this._ticker; } @@ -615,6 +621,16 @@ export default class AndroidNotification { return this._notification; } + /** + * + * @param tag + * @returns {Notification} + */ + setTag(tag: string): Notification { + this._tag = tag; + return this._notification; + } + /** * * @param ticker @@ -719,6 +735,7 @@ export default class AndroidNotification { smallIcon: this._smallIcon, sortKey: this._sortKey, // TODO: style: Style, + tag: this._tag, ticker: this._ticker, timeoutAfter: this._timeoutAfter, usesChronometer: this._usesChronometer, diff --git a/lib/modules/notifications/AndroidNotifications.js b/lib/modules/notifications/AndroidNotifications.js index 6873f052..c697d6fd 100644 --- a/lib/modules/notifications/AndroidNotifications.js +++ b/lib/modules/notifications/AndroidNotifications.js @@ -92,6 +92,20 @@ export default class AndroidNotifications { return Promise.resolve(); } + removeDeliveredNotificationsByTag(tag: string): Promise { + if (Platform.OS === 'android') { + if (typeof tag !== 'string') { + throw new Error( + `AndroidNotifications:removeDeliveredNotificationsByTag expects an 'string' but got type ${typeof tag}` + ); + } + return getNativeModule( + this._notifications + ).removeDeliveredNotificationsByTag(tag); + } + return Promise.resolve(); + } + deleteChannelGroup(groupId: string): Promise { if (Platform.OS === 'android') { if (typeof groupId !== 'string') { @@ -99,9 +113,7 @@ export default class AndroidNotifications { `AndroidNotifications:deleteChannelGroup expects an 'string' but got type ${typeof groupId}` ); } - return getNativeModule(this._notifications).deleteChannelGroup( - groupId - ); + return getNativeModule(this._notifications).deleteChannelGroup(groupId); } return Promise.resolve(); } @@ -113,9 +125,7 @@ export default class AndroidNotifications { `AndroidNotifications:deleteChannel expects an 'string' but got type ${typeof channelId}` ); } - return getNativeModule(this._notifications).deleteChannel( - channelId - ); + return getNativeModule(this._notifications).deleteChannel(channelId); } return Promise.resolve(); } diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index c43ceac3..f51e9da2 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -170,6 +170,7 @@ export type NativeAndroidNotification = {| smallIcon: SmallIcon, sortKey?: string, // TODO: style: Style, + tag?: string, ticker?: string, timeoutAfter?: number, usesChronometer?: boolean, diff --git a/package-lock.json b/package-lock.json index ec1b6a13..08fcdaa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "4.3.7", + "version": "4.3.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dd0aa9bc..0a5d5cf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-firebase", - "version": "4.3.7", + "version": "4.3.8", "author": "Invertase (http://invertase.io)", "description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Admob, Analytics, Auth, Crash Reporting, Cloud Firestore, Database, Dynamic Links, Functions, Messaging (FCM), Remote Config, Storage and more.", "main": "dist/index.js",