diff --git a/packages/firestore/__tests__/firestore.test.ts b/packages/firestore/__tests__/firestore.test.ts index 49ff586a63..a21878b176 100644 --- a/packages/firestore/__tests__/firestore.test.ts +++ b/packages/firestore/__tests__/firestore.test.ts @@ -112,6 +112,18 @@ describe('Storage', function () { return expect(e.message).toContain("ignoreUndefinedProperties' must be a boolean value."); } }); + + it("throws if serverTimestampBehavior is not one of 'estimate', 'previous', 'none'", async function () { + try { + // @ts-ignore the type is incorrect *on purpose* to test type checking in javascript + await firestore().settings({ serverTimestampBehavior: 'bogus' }); + return Promise.reject(new Error('Should throw')); + } catch (e) { + return expect(e.message).toContain( + "serverTimestampBehavior' must be one of 'estimate', 'previous', 'none'", + ); + } + }); }); describe('runTransaction()', function () { diff --git a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java index aa611a745c..885926f8aa 100644 --- a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java +++ b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreModule.java @@ -75,6 +75,12 @@ Task settings(String appName, Map settings) { UniversalFirebaseFirestoreStatics.FIRESTORE_SSL + "_" + appName, (boolean) settings.get("ssl")); } + // settings.serverTimestampBehavior + if (settings.containsKey("serverTimestampBehavior")) { + UniversalFirebasePreferences.getSharedInstance().setStringValue( + UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + appName, (String) settings.get("serverTimestampBehavior")); + } + return null; }); } diff --git a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreStatics.java b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreStatics.java index 60b749ffeb..0d9682eb2a 100644 --- a/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreStatics.java +++ b/packages/firestore/android/src/main/java/io/invertase/firebase/firestore/UniversalFirebaseFirestoreStatics.java @@ -22,4 +22,5 @@ public class UniversalFirebaseFirestoreStatics { public static String FIRESTORE_HOST = "firebase_firestore_host"; public static String FIRESTORE_PERSISTENCE = "firebase_firestore_persistence"; public static String FIRESTORE_SSL = "firebase_firestore_ssl"; + public static String FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR = "firebase_firestore_server_timestamp_behavior"; } diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java index c55b8cb55c..c4098c15ae 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCollectionModule.java @@ -66,6 +66,7 @@ public void collectionOnSnapshot( FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); ReactNativeFirebaseFirestoreQuery firestoreQuery = new ReactNativeFirebaseFirestoreQuery( + appName, getQueryForFirestore(firebaseFirestore, path, type), filters, orders, @@ -128,6 +129,7 @@ public void collectionGet( ) { FirebaseFirestore firebaseFirestore = getFirestoreForApp(appName); ReactNativeFirebaseFirestoreQuery query = new ReactNativeFirebaseFirestoreQuery( + appName, getQueryForFirestore(firebaseFirestore, path, type), filters, orders, @@ -160,7 +162,7 @@ public void collectionGet( } private void sendOnSnapshotEvent(String appName, int listenerId, QuerySnapshot querySnapshot, MetadataChanges metadataChanges) { - Tasks.call(getTransactionalExecutor(Integer.toString(listenerId)), () -> snapshotToWritableMap("onSnapshot", querySnapshot, metadataChanges)).addOnCompleteListener(task -> { + Tasks.call(getTransactionalExecutor(Integer.toString(listenerId)), () -> snapshotToWritableMap(appName, "onSnapshot", querySnapshot, metadataChanges)).addOnCompleteListener(task -> { if (task.isSuccessful()) { WritableMap body = Arguments.createMap(); body.putMap("snapshot", task.getResult()); diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java index 3136da1660..141e60a942 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreCommon.java @@ -19,8 +19,11 @@ import com.facebook.react.bridge.Promise; +import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; +import io.invertase.firebase.common.UniversalFirebasePreferences; + import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithCodeAndMessage; import static io.invertase.firebase.common.ReactNativeFirebaseModule.rejectPromiseWithExceptionMap; @@ -39,4 +42,20 @@ static void rejectPromiseFirestoreException(Promise promise, Exception exception rejectPromiseWithExceptionMap(promise, exception); } } + + static DocumentSnapshot.ServerTimestampBehavior getServerTimestampBehavior(String appName) { + UniversalFirebasePreferences preferences = UniversalFirebasePreferences.getSharedInstance(); + String key = UniversalFirebaseFirestoreStatics.FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR + "_" + appName; + String behavior = preferences.getStringValue(key, "none"); + + if ("estimate".equals(behavior)) { + return DocumentSnapshot.ServerTimestampBehavior.ESTIMATE; + } + + if ("previous".equals(behavior)) { + return DocumentSnapshot.ServerTimestampBehavior.PREVIOUS; + } + + return DocumentSnapshot.ServerTimestampBehavior.NONE; + } } diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java index 58dff5ee6f..408941f209 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreDocumentModule.java @@ -131,7 +131,7 @@ public void documentGet(String appName, String path, ReadableMap getOptions, Pro Tasks.call(getExecutor(), () -> { DocumentSnapshot documentSnapshot = Tasks.await(documentReference.get(source)); - return snapshotToWritableMap(documentSnapshot); + return snapshotToWritableMap(appName, documentSnapshot); }).addOnCompleteListener(task -> { if (task.isSuccessful()) { promise.resolve(task.getResult()); @@ -261,7 +261,7 @@ public void documentBatch(String appName, ReadableArray writes, Promise promise) } private void sendOnSnapshotEvent(String appName, int listenerId, DocumentSnapshot documentSnapshot) { - Tasks.call(getExecutor(), () -> snapshotToWritableMap(documentSnapshot)).addOnCompleteListener(task -> { + Tasks.call(getExecutor(), () -> snapshotToWritableMap(appName, documentSnapshot)).addOnCompleteListener(task -> { if (task.isSuccessful()) { WritableMap body = Arguments.createMap(); body.putMap("snapshot", task.getResult()); diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java index b16a8ca3ed..1a3900cec6 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java @@ -37,14 +37,17 @@ import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreSerialize.*; public class ReactNativeFirebaseFirestoreQuery { + String appName; Query query; ReactNativeFirebaseFirestoreQuery( + String appName, Query query, ReadableArray filters, ReadableArray orders, ReadableMap options ) { + this.appName = appName; this.query = query; applyFilters(filters); applyOrders(orders); @@ -54,7 +57,7 @@ public class ReactNativeFirebaseFirestoreQuery { public Task get(Executor executor, Source source) { return Tasks.call(executor, () -> { QuerySnapshot querySnapshot = Tasks.await(query.get(source)); - return snapshotToWritableMap("get", querySnapshot, null); + return snapshotToWritableMap(this.appName, "get", querySnapshot, null); }); } diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java index 356ed1d43a..3d3515e30a 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreSerialize.java @@ -48,6 +48,7 @@ import javax.annotation.Nullable; +import static io.invertase.firebase.firestore.ReactNativeFirebaseFirestoreCommon.getServerTimestampBehavior; import static io.invertase.firebase.common.RCTConvertFirebase.toHashMap; // public access for native re-use in brownfield apps @@ -99,7 +100,7 @@ public class ReactNativeFirebaseFirestoreSerialize { * @param documentSnapshot DocumentSnapshot * @return WritableMap */ - static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) { + static WritableMap snapshotToWritableMap(String appName, DocumentSnapshot documentSnapshot) { WritableArray metadata = Arguments.createArray(); WritableMap documentMap = Arguments.createMap(); SnapshotMetadata snapshotMetadata = documentSnapshot.getMetadata(); @@ -112,9 +113,11 @@ static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) { documentMap.putString(KEY_PATH, documentSnapshot.getReference().getPath()); documentMap.putBoolean(KEY_EXISTS, documentSnapshot.exists()); + DocumentSnapshot.ServerTimestampBehavior timestampBehavior = getServerTimestampBehavior(appName); + if (documentSnapshot.exists()) { - if (documentSnapshot.getData() != null) { - documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); + if (documentSnapshot.getData(timestampBehavior) != null) { + documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData(timestampBehavior))); } } @@ -127,7 +130,7 @@ static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) { * @param querySnapshot QuerySnapshot * @return WritableMap */ - static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnapshot, @Nullable MetadataChanges metadataChanges) { + static WritableMap snapshotToWritableMap(String appName, String source, QuerySnapshot querySnapshot, @Nullable MetadataChanges metadataChanges) { WritableMap writableMap = Arguments.createMap(); writableMap.putString("source", source); @@ -140,14 +143,14 @@ static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnaps // If not listening to metadata changes, send the data back to JS land with a flag // indicating the data does not include these changes writableMap.putBoolean("excludesMetadataChanges", true); - writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentChangesList, null)); + writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(appName, documentChangesList, null)); } else { // If listening to metadata changes, get the changes list with document changes array. // To indicate whether a document change was because of metadata change, we check whether // its in the raw list by document key. writableMap.putBoolean("excludesMetadataChanges", false); List documentMetadataChangesList = querySnapshot.getDocumentChanges(MetadataChanges.INCLUDE); - writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentMetadataChangesList, documentChangesList)); + writableMap.putArray(KEY_CHANGES, documentChangesToWritableArray(appName, documentMetadataChangesList, documentChangesList)); } SnapshotMetadata snapshotMetadata = querySnapshot.getMetadata(); @@ -155,7 +158,7 @@ static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnaps // set documents for (DocumentSnapshot documentSnapshot : documentSnapshots) { - documents.pushMap(snapshotToWritableMap(documentSnapshot)); + documents.pushMap(snapshotToWritableMap(appName, documentSnapshot)); } writableMap.putArray(KEY_DOCUMENTS, documents); @@ -174,7 +177,7 @@ static WritableMap snapshotToWritableMap(String source, QuerySnapshot querySnaps * @param documentChanges List * @return WritableArray */ - private static WritableArray documentChangesToWritableArray(List documentChanges, @Nullable List comparableDocumentChanges) { + private static WritableArray documentChangesToWritableArray(String appName, List documentChanges, @Nullable List comparableDocumentChanges) { WritableArray documentChangesWritable = Arguments.createArray(); boolean checkIfMetadataChange = comparableDocumentChanges != null; @@ -191,7 +194,7 @@ private static WritableArray documentChangesToWritableArray(List } } - documentChangesWritable.pushMap(documentChangeToWritableMap(documentChange, isMetadataChange)); + documentChangesWritable.pushMap(documentChangeToWritableMap(appName, documentChange, isMetadataChange)); } return documentChangesWritable; @@ -203,7 +206,7 @@ private static WritableArray documentChangesToWritableArray(List * @param documentChange DocumentChange * @return WritableMap */ - private static WritableMap documentChangeToWritableMap(DocumentChange documentChange, boolean isMetadataChange) { + private static WritableMap documentChangeToWritableMap(String appName, DocumentChange documentChange, boolean isMetadataChange) { WritableMap documentChangeMap = Arguments.createMap(); documentChangeMap.putBoolean("isMetadataChange", isMetadataChange); @@ -221,7 +224,7 @@ private static WritableMap documentChangeToWritableMap(DocumentChange documentCh documentChangeMap.putMap( KEY_DOC_CHANGE_DOCUMENT, - snapshotToWritableMap(documentChange.getDocument()) + snapshotToWritableMap(appName, documentChange.getDocument()) ); documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex()); diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java index be91983718..6a1107164b 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreTransactionModule.java @@ -72,7 +72,7 @@ public void transactionGetDocument(String appName, int transactionId, String pat DocumentReference documentReference = getDocumentForFirestore(firebaseFirestore, path); Tasks - .call(getTransactionalExecutor(), () -> snapshotToWritableMap(transactionHandler.getDocument(documentReference))) + .call(getTransactionalExecutor(), () -> snapshotToWritableMap(appName, transactionHandler.getDocument(documentReference))) .addOnCompleteListener(task -> { if (task.isSuccessful()) { promise.resolve(task.getResult()); diff --git a/packages/firestore/e2e/firestore.e2e.js b/packages/firestore/e2e/firestore.e2e.js index 2584c8fec0..eacc4fb674 100644 --- a/packages/firestore/e2e/firestore.e2e.js +++ b/packages/firestore/e2e/firestore.e2e.js @@ -197,4 +197,76 @@ describe('firestore()', function () { should(timedOutWithNetworkEnabled).equal(false); }); }); + + describe('settings', function () { + describe('serverTimestampBehavior', function () { + it("handles 'estimate'", async function () { + firebase.firestore().settings({ serverTimestampBehavior: 'estimate' }); + const ref = firebase.firestore().doc(`${COLLECTION}/getData`); + + const promise = new Promise((resolve, reject) => { + const subscription = ref.onSnapshot(snapshot => { + should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); + subscription(); + resolve(); + }, reject); + }); + + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await ref.delete(); + }); + it("handles 'previous'", async function () { + firebase.firestore().settings({ serverTimestampBehavior: 'previous' }); + const ref = firebase.firestore().doc(`${COLLECTION}/getData`); + + const promise = new Promise((resolve, reject) => { + let counter = 0; + let previous = null; + const subscription = ref.onSnapshot(snapshot => { + switch (counter++) { + case 0: + break; + case 1: + should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); + break; + case 2: + should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); + should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(true); + break; + case 3: + should(snapshot.get('timestamp')).be.an.instanceOf(firebase.firestore.Timestamp); + should(snapshot.get('timestamp').isEqual(previous.get('timestamp'))).equal(false); + subscription(); + resolve(); + break; + } + previous = snapshot; + }, reject); + }); + + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await new Promise(resolve => setTimeout(resolve, 1)); + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await ref.delete(); + }); + it("handles 'none'", async function () { + firebase.firestore().settings({ serverTimestampBehavior: 'none' }); + const ref = firebase.firestore().doc(`${COLLECTION}/getData`); + + const promise = new Promise((resolve, reject) => { + const subscription = ref.onSnapshot(snapshot => { + should(snapshot.get('timestamp')).equal(null); + subscription(); + resolve(); + }, reject); + }); + + await ref.set({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }); + await promise; + await ref.delete(); + }); + }); + }); }); diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m index 5609db3df5..2424c6e3d8 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCollectionModule.m @@ -148,7 +148,8 @@ - (void)invalidate { if (error) { return [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; } else { - NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"get" snapshot:snapshot includeMetadataChanges:false]; + NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name]; + NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"get" snapshot:snapshot includeMetadataChanges:false appName:appName]; resolve(serialized); } }]; @@ -158,7 +159,8 @@ - (void)sendSnapshotEvent:(FIRApp *)firApp listenerId:(nonnull NSNumber *)listenerId snapshot:(FIRQuerySnapshot *)snapshot includeMetadataChanges:(BOOL)includeMetadataChanges { - NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"onSnapshot" snapshot:snapshot includeMetadataChanges:includeMetadataChanges]; + NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firApp.name]; + NSDictionary *serialized = [RNFBFirestoreSerialize querySnapshotToDictionary:@"onSnapshot" snapshot:snapshot includeMetadataChanges:includeMetadataChanges appName:appName]; [[RNFBRCTEventEmitter shared] sendEventWithName:RNFB_FIRESTORE_COLLECTION_SYNC body:@{ @"appName": [RNFBSharedUtils getAppJavaScriptName:firApp.name], @"listenerId": listenerId, diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h index c3190bc548..88ffee1d42 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.h @@ -41,4 +41,5 @@ extern NSString *const FIRESTORE_CACHE_SIZE; extern NSString *const FIRESTORE_HOST; extern NSString *const FIRESTORE_PERSISTENCE; extern NSString *const FIRESTORE_SSL; -extern NSMutableDictionary * instanceCache; +extern NSString *const FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR; +extern NSMutableDictionary *instanceCache; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m index f9c9bba6c3..f366fad03f 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreCommon.m @@ -23,6 +23,7 @@ NSString *const FIRESTORE_HOST = @"firebase_firestore_host"; NSString *const FIRESTORE_PERSISTENCE = @"firebase_firestore_persistence"; NSString *const FIRESTORE_SSL = @"firebase_firestore_ssl"; +NSString *const FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR = @"firebase_firestore_server_timestamp_behavior"; NSMutableDictionary * instanceCache; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m index 535c8f5763..3e1a1b19c8 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreDocumentModule.m @@ -136,7 +136,8 @@ - (void)invalidate { if (error) { return [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; } else { - NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot]; + NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name]; + NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot appName:appName]; resolve(serialized); } }]; @@ -259,7 +260,8 @@ - (void)invalidate { - (void)sendSnapshotEvent:(FIRApp *)firApp listenerId:(nonnull NSNumber *)listenerId snapshot:(FIRDocumentSnapshot *)snapshot { - NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot]; + NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firApp.name]; + NSDictionary *serialized = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot appName:appName]; [[RNFBRCTEventEmitter shared] sendEventWithName:RNFB_FIRESTORE_DOCUMENT_SYNC body:@{ @"appName": [RNFBSharedUtils getAppJavaScriptName:firApp.name], @"listenerId": listenerId, diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m index 1240c0a8e1..cd19d7e97f 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreModule.m @@ -100,6 +100,11 @@ + (BOOL)requiresMainQueueSetup { [[RNFBPreferences shared] setBooleanValue:sslKey boolValue:[settings[@"ssl"] boolValue]]; } + if (settings[@"serverTimestampBehavior"]) { + NSString *key = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR, appName]; + [[RNFBPreferences shared] setStringValue:key stringValue:settings[@"serverTimestampBehavior"]]; + } + resolve([NSNull null]); } diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h index 7712fc18c8..9fec22a607 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.h @@ -21,11 +21,11 @@ @interface RNFBFirestoreSerialize : NSObject -+ (NSDictionary *)querySnapshotToDictionary:(NSString *)source snapshot:(FIRQuerySnapshot *)snapshot includeMetadataChanges:(BOOL)includeMetadataChanges; ++ (NSDictionary *)querySnapshotToDictionary:(NSString *)source snapshot:(FIRQuerySnapshot *)snapshot includeMetadataChanges:(BOOL)includeMetadataChanges appName:(NSString *)appName; -+ (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange isMetadataChange:(BOOL)isMetadataChange; ++ (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange isMetadataChange:(BOOL)isMetadataChange appName:(NSString *)appName; -+ (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot; ++ (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot appName:(NSString *)appName; + (NSDictionary *)serializeDictionary:(NSDictionary *)dictionary; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m index 4705661998..99aaae98ae 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreSerialize.m @@ -17,6 +17,8 @@ */ #import "RNFBFirestoreSerialize.h" +#import "RNFBFirestoreCommon.h" +#import "RNFBPreferences.h" @implementation RNFBFirestoreSerialize @@ -61,7 +63,8 @@ @implementation RNFBFirestoreSerialize + (NSDictionary *)querySnapshotToDictionary :(NSString *)source snapshot:(FIRQuerySnapshot *)snapshot - includeMetadataChanges:(BOOL)includeMetadataChanges { + includeMetadataChanges:(BOOL)includeMetadataChanges + appName:(NSString *)appName { NSMutableArray *metadata = [[NSMutableArray alloc] init]; NSMutableDictionary *snapshotMap = [[NSMutableDictionary alloc] init]; @@ -78,7 +81,7 @@ + (NSDictionary *)querySnapshotToDictionary // indicating the data does not include these changes snapshotMap[@"excludesMetadataChanges"] = @(true); for (FIRDocumentChange *documentChange in documentChangesList) { - [changes addObject:[self documentChangeToDictionary:documentChange isMetadataChange:false]]; + [changes addObject:[self documentChangeToDictionary:documentChange isMetadataChange:false appName:appName]]; } } else { // If listening to metadata changes, get the changes list with document changes array. @@ -109,7 +112,7 @@ + (NSDictionary *)querySnapshotToDictionary isMetadataChange = YES; } - [changes addObject:[self documentChangeToDictionary:documentMetadataChange isMetadataChange:isMetadataChange]]; + [changes addObject:[self documentChangeToDictionary:documentMetadataChange isMetadataChange:isMetadataChange appName:appName]]; } } @@ -119,7 +122,7 @@ + (NSDictionary *)querySnapshotToDictionary //set documents NSMutableArray *documents = [[NSMutableArray alloc] init]; for (FIRDocumentSnapshot *documentSnapshot in documentSnapshots) { - [documents addObject:[self documentSnapshotToDictionary:documentSnapshot]]; + [documents addObject:[self documentSnapshotToDictionary:documentSnapshot appName:appName]]; } snapshotMap[KEY_DOCUMENTS] = documents; @@ -134,7 +137,8 @@ + (NSDictionary *)querySnapshotToDictionary + (NSDictionary *)documentChangeToDictionary :(FIRDocumentChange *)documentChange - isMetadataChange:(BOOL)isMetadataChange { + isMetadataChange:(BOOL)isMetadataChange + appName:(NSString *)appName { NSMutableDictionary *changeMap = [[NSMutableDictionary alloc] init]; changeMap[@"isMetadataChange"] = @(isMetadataChange); @@ -146,7 +150,7 @@ + (NSDictionary *)documentChangeToDictionary changeMap[KEY_DOC_CHANGE_TYPE] = CHANGE_REMOVED; } - changeMap[KEY_DOC_CHANGE_DOCUMENT] = [self documentSnapshotToDictionary:documentChange.document]; + changeMap[KEY_DOC_CHANGE_DOCUMENT] = [self documentSnapshotToDictionary:documentChange.document appName:appName]; // Note the Firestore C++ SDK here returns a maxed UInt that is != NSUIntegerMax, so we make one ourselves so we can // convert to -1 for JS land @@ -167,7 +171,7 @@ + (NSDictionary *)documentChangeToDictionary } // Native DocumentSnapshot -> NSDictionary (for JS) -+ (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot { ++ (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot appName:(NSString *)appName { NSMutableArray *metadata = [[NSMutableArray alloc] init]; NSMutableDictionary *documentMap = [[NSMutableDictionary alloc] init]; @@ -180,7 +184,21 @@ + (NSDictionary *)documentSnapshotToDictionary:(FIRDocumentSnapshot *)snapshot { documentMap[KEY_EXISTS] = @(snapshot.exists); if (snapshot.exists) { - documentMap[KEY_DATA] = [self serializeDictionary:snapshot.data]; + NSString *key = [NSString stringWithFormat:@"%@_%@", FIRESTORE_SERVER_TIMESTAMP_BEHAVIOR, appName]; + NSString *behavior = [[RNFBPreferences shared] getStringValue:key defaultValue:@"none"]; + + FIRServerTimestampBehavior serverTimestampBehavior; + + if ([behavior isEqualToString:@"estimate"]) { + serverTimestampBehavior = FIRServerTimestampBehaviorEstimate; + } else if ([behavior isEqualToString:@"previous"]) { + serverTimestampBehavior = FIRServerTimestampBehaviorPrevious; + } else { + serverTimestampBehavior = FIRServerTimestampBehaviorNone; + } + + NSDictionary *data = [snapshot dataWithServerTimestampBehavior:serverTimestampBehavior]; + documentMap[KEY_DATA] = [self serializeDictionary:data]; } return documentMap; diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m index 703a6a00d7..9d7deb2d82 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreTransactionModule.m @@ -84,7 +84,8 @@ - (void)invalidate { if (error != nil) { [RNFBFirestoreCommon promiseRejectFirestoreException:reject error:error]; } else { - NSDictionary *snapshotDict = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot]; + NSString *appName = [RNFBSharedUtils getAppJavaScriptName:firebaseApp.name]; + NSDictionary *snapshotDict = [RNFBFirestoreSerialize documentSnapshotToDictionary:snapshot appName:appName]; NSString *snapshotPath = snapshotDict[@"path"]; if (snapshotPath == nil) { diff --git a/packages/firestore/lib/index.d.ts b/packages/firestore/lib/index.d.ts index d83f8e7c79..69a90ec277 100644 --- a/packages/firestore/lib/index.d.ts +++ b/packages/firestore/lib/index.d.ts @@ -1426,6 +1426,19 @@ export namespace FirebaseFirestoreTypes { * If set to false or omitted, the SDK throws an exception when it encounters properties of type undefined. */ ignoreUndefinedProperties?: boolean; + + /** + * If set, controls the return value for server timestamps that have not yet been set to their final value. + * + * By specifying 'estimate', pending server timestamps return an estimate based on the local clock. + * This estimate will differ from the final value and cause these values to change once the server result becomes available. + * + * By specifying 'previous', pending timestamps will be ignored and return their previous value instead. + * + * If omitted or set to 'none', null will be returned by default until the server value becomes available. + * + */ + serverTimestampBehavior?: 'estimate' | 'previous' | 'none'; } /** diff --git a/packages/firestore/lib/index.js b/packages/firestore/lib/index.js index 629e726012..18fe8e12b4 100644 --- a/packages/firestore/lib/index.js +++ b/packages/firestore/lib/index.js @@ -191,7 +191,14 @@ class FirebaseFirestoreModule extends FirebaseModule { const keys = Object.keys(settings); - const opts = ['cacheSizeBytes', 'host', 'persistence', 'ssl', 'ignoreUndefinedProperties']; + const opts = [ + 'cacheSizeBytes', + 'host', + 'persistence', + 'ssl', + 'ignoreUndefinedProperties', + 'serverTimestampBehavior', + ]; for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -270,6 +277,17 @@ class FirebaseFirestoreModule extends FirebaseModule { throw new Error("firebase.firestore().settings(*) 'settings.ssl' must be a boolean value."); } + if ( + !isUndefined(settings.serverTimestampBehavior) && + !['estimate', 'previous', 'none'].includes(settings.serverTimestampBehavior) + ) { + return Promise.reject( + new Error( + "firebase.firestore().settings(*) 'settings.serverTimestampBehavior' must be one of 'estimate', 'previous', 'none'.", + ), + ); + } + if (!isUndefined(settings.ignoreUndefinedProperties)) { if (!isBoolean(settings.ignoreUndefinedProperties)) { return Promise.reject(