From d03ec0b8b073dfd799c006a7af3cd4e69f41d97f Mon Sep 17 00:00:00 2001 From: Robin Genz Date: Mon, 25 May 2026 10:58:18 +0200 Subject: [PATCH 1/2] feat(firestore): add serverTimestamps option to snapshot listeners Expose Firestore's pending server timestamp behavior ('estimate' | 'previous' | 'none') as an option on addDocumentSnapshotListener, addCollectionSnapshotListener, and addCollectionGroupSnapshotListener on Web, Android, and iOS. Defaults to 'none' (current behavior). Closes #985 --- .changeset/firestore-server-timestamps.md | 5 ++++ .../firebase/firestore/FirebaseFirestore.java | 16 ++++++++++--- .../firestore/FirebaseFirestoreHelper.java | 15 ++++++++++++ .../firestore/FirebaseFirestorePlugin.java | 6 +++++ ...ollectionGroupSnapshotListenerOptions.java | 10 ++++++++ .../AddCollectionSnapshotListenerOptions.java | 10 ++++++++ .../AddDocumentSnapshotListenerOptions.java | 17 +++++++++++++- .../classes/results/GetCollectionResult.java | 12 +++++++++- .../classes/results/GetDocumentResult.java | 12 +++++++++- ...llectionGroupSnapshotListenerOptions.swift | 8 ++++++- ...AddCollectionSnapshotListenerOptions.swift | 8 ++++++- .../AddDocumentSnapshotListenerOptions.swift | 8 ++++++- .../Results/GetCollectionGroupResult.swift | 6 +++-- .../Classes/Results/GetCollectionResult.swift | 6 +++-- .../Classes/Results/GetDocumentResult.swift | 6 +++-- .../ios/Plugin/FirebaseFirestore.swift | 9 +++++--- .../ios/Plugin/FirebaseFirestoreHelper.swift | 11 +++++++++ .../ios/Plugin/FirebaseFirestorePlugin.swift | 9 +++++--- packages/firestore/src/definitions.ts | 23 ++++++++++++++----- packages/firestore/src/web.ts | 16 ++++++++++--- 20 files changed, 183 insertions(+), 30 deletions(-) create mode 100644 .changeset/firestore-server-timestamps.md diff --git a/.changeset/firestore-server-timestamps.md b/.changeset/firestore-server-timestamps.md new file mode 100644 index 000000000..8f12b6406 --- /dev/null +++ b/.changeset/firestore-server-timestamps.md @@ -0,0 +1,5 @@ +--- +'@capacitor-firebase/firestore': minor +--- + +feat(firestore): support `serverTimestamps` behavior option for `addDocumentSnapshotListener(...)`, `addCollectionSnapshotListener(...)`, and `addCollectionGroupSnapshotListener(...)` diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestore.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestore.java index a16f8721c..eb8c780ac 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestore.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestore.java @@ -9,6 +9,7 @@ import com.google.firebase.firestore.AggregateSource; import com.google.firebase.firestore.CollectionReference; import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.Filter; import com.google.firebase.firestore.FirebaseFirestoreSettings; import com.google.firebase.firestore.ListenerRegistration; @@ -269,6 +270,9 @@ public void getCountFromServer(@NonNull GetCountFromServerOptions options, @NonN public void addDocumentSnapshotListener(@NonNull AddDocumentSnapshotListenerOptions options, @NonNull NonEmptyResultCallback callback) { String reference = options.getReference(); String callbackId = options.getCallbackId(); + DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior( + options.getServerTimestampBehavior() + ); ListenerRegistration listenerRegistration = getFirebaseFirestoreInstance() .document(reference) @@ -278,7 +282,7 @@ public void addDocumentSnapshotListener(@NonNull AddDocumentSnapshotListenerOpti if (exception != null) { callback.error(exception); } else { - GetDocumentResult result = new GetDocumentResult(documentSnapshot); + GetDocumentResult result = new GetDocumentResult(documentSnapshot, serverTimestampBehavior); callback.success(result); } } @@ -294,6 +298,9 @@ public void addCollectionSnapshotListener( QueryCompositeFilterConstraint compositeFilter = options.getCompositeFilter(); QueryNonFilterConstraint[] queryConstraints = options.getQueryConstraints(); String callbackId = options.getCallbackId(); + DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior( + options.getServerTimestampBehavior() + ); Query query = getFirebaseFirestoreInstance().collection(reference); if (compositeFilter != null) { @@ -312,7 +319,7 @@ public void addCollectionSnapshotListener( if (exception != null) { callback.error(exception); } else { - GetCollectionResult result = new GetCollectionResult(querySnapshot); + GetCollectionResult result = new GetCollectionResult(querySnapshot, serverTimestampBehavior); callback.success(result); } } @@ -328,6 +335,9 @@ public void addCollectionGroupSnapshotListener( QueryCompositeFilterConstraint compositeFilter = options.getCompositeFilter(); QueryNonFilterConstraint[] queryConstraints = options.getQueryConstraints(); String callbackId = options.getCallbackId(); + DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior( + options.getServerTimestampBehavior() + ); Query query = getFirebaseFirestoreInstance().collectionGroup(reference); if (compositeFilter != null) { @@ -346,7 +356,7 @@ public void addCollectionGroupSnapshotListener( if (exception != null) { callback.error(exception); } else { - GetCollectionResult result = new GetCollectionResult(querySnapshot); + GetCollectionResult result = new GetCollectionResult(querySnapshot, serverTimestampBehavior); callback.success(result); } } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java index e966d4599..09b1b22f7 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestoreHelper.java @@ -27,6 +27,21 @@ public class FirebaseFirestoreHelper { + @NonNull + public static DocumentSnapshot.ServerTimestampBehavior createServerTimestampBehavior(@Nullable String value) { + if (value == null) { + return DocumentSnapshot.ServerTimestampBehavior.NONE; + } + switch (value) { + case "estimate": + return DocumentSnapshot.ServerTimestampBehavior.ESTIMATE; + case "previous": + return DocumentSnapshot.ServerTimestampBehavior.PREVIOUS; + default: + return DocumentSnapshot.ServerTimestampBehavior.NONE; + } + } + public static HashMap createHashMapFromJSONObject(JSONObject object) throws JSONException { HashMap map = new HashMap<>(); Iterator keys = object.keys(); diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestorePlugin.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestorePlugin.java index f0dafbebf..87110ae87 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestorePlugin.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/FirebaseFirestorePlugin.java @@ -463,6 +463,7 @@ public void addDocumentSnapshotListener(PluginCall call) { return; } Boolean includeMetadataChanges = call.getBoolean("includeMetadataChanges"); + String serverTimestampBehavior = call.getString("serverTimestamps"); String callbackId = call.getCallbackId(); this.pluginCallMap.put(callbackId, call); @@ -470,6 +471,7 @@ public void addDocumentSnapshotListener(PluginCall call) { AddDocumentSnapshotListenerOptions options = new AddDocumentSnapshotListenerOptions( reference, includeMetadataChanges, + serverTimestampBehavior, callbackId ); NonEmptyResultCallback callback = new NonEmptyResultCallback() { @@ -505,6 +507,7 @@ public void addCollectionSnapshotListener(PluginCall call) { JSObject compositeFilter = call.getObject("compositeFilter"); JSArray queryConstraints = call.getArray("queryConstraints"); Boolean includeMetadataChanges = call.getBoolean("includeMetadataChanges"); + String serverTimestampBehavior = call.getString("serverTimestamps"); String callbackId = call.getCallbackId(); this.pluginCallMap.put(callbackId, call); @@ -514,6 +517,7 @@ public void addCollectionSnapshotListener(PluginCall call) { compositeFilter, queryConstraints, includeMetadataChanges, + serverTimestampBehavior, callbackId ); NonEmptyResultCallback callback = new NonEmptyResultCallback() { @@ -549,6 +553,7 @@ public void addCollectionGroupSnapshotListener(PluginCall call) { JSObject compositeFilter = call.getObject("compositeFilter"); JSArray queryConstraints = call.getArray("queryConstraints"); Boolean includeMetadataChanges = call.getBoolean("includeMetadataChanges"); + String serverTimestampBehavior = call.getString("serverTimestamps"); String callbackId = call.getCallbackId(); this.pluginCallMap.put(callbackId, call); @@ -558,6 +563,7 @@ public void addCollectionGroupSnapshotListener(PluginCall call) { compositeFilter, queryConstraints, includeMetadataChanges, + serverTimestampBehavior, callbackId ); NonEmptyResultCallback callback = new NonEmptyResultCallback() { diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionGroupSnapshotListenerOptions.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionGroupSnapshotListenerOptions.java index d446ad937..8f2f986a1 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionGroupSnapshotListenerOptions.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionGroupSnapshotListenerOptions.java @@ -24,17 +24,22 @@ public class AddCollectionGroupSnapshotListenerOptions { private boolean includeMetadataChanges; + @Nullable + private final String serverTimestampBehavior; + public AddCollectionGroupSnapshotListenerOptions( String reference, @Nullable JSObject compositeFilter, @Nullable JSArray queryConstraints, @Nullable Boolean includeMetadataChanges, + @Nullable String serverTimestampBehavior, String callbackId ) throws JSONException { this.reference = reference; this.compositeFilter = FirebaseFirestoreHelper.createQueryCompositeFilterConstraintFromJSObject(compositeFilter); this.queryConstraints = FirebaseFirestoreHelper.createQueryNonFilterConstraintArrayFromJSArray(queryConstraints); this.includeMetadataChanges = includeMetadataChanges == null ? false : includeMetadataChanges; + this.serverTimestampBehavior = serverTimestampBehavior; this.callbackId = callbackId; } @@ -56,6 +61,11 @@ public boolean isIncludeMetadataChanges() { return includeMetadataChanges; } + @Nullable + public String getServerTimestampBehavior() { + return serverTimestampBehavior; + } + public String getCallbackId() { return callbackId; } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionSnapshotListenerOptions.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionSnapshotListenerOptions.java index 7d6adae9b..7eba13aac 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionSnapshotListenerOptions.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddCollectionSnapshotListenerOptions.java @@ -25,17 +25,22 @@ public class AddCollectionSnapshotListenerOptions { private final boolean includeMetadataChanges; + @Nullable + private final String serverTimestampBehavior; + public AddCollectionSnapshotListenerOptions( String reference, @Nullable JSObject compositeFilter, @Nullable JSArray queryConstraints, @Nullable Boolean includeMetadataChanges, + @Nullable String serverTimestampBehavior, String callbackId ) throws JSONException { this.reference = reference; this.compositeFilter = FirebaseFirestoreHelper.createQueryCompositeFilterConstraintFromJSObject(compositeFilter); this.queryConstraints = FirebaseFirestoreHelper.createQueryNonFilterConstraintArrayFromJSArray(queryConstraints); this.includeMetadataChanges = includeMetadataChanges == null ? false : includeMetadataChanges; + this.serverTimestampBehavior = serverTimestampBehavior; this.callbackId = callbackId; } @@ -57,6 +62,11 @@ public boolean isIncludeMetadataChanges() { return includeMetadataChanges; } + @Nullable + public String getServerTimestampBehavior() { + return serverTimestampBehavior; + } + public String getCallbackId() { return callbackId; } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddDocumentSnapshotListenerOptions.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddDocumentSnapshotListenerOptions.java index ab0c2717a..34c4d86e7 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddDocumentSnapshotListenerOptions.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/options/AddDocumentSnapshotListenerOptions.java @@ -6,11 +6,21 @@ public class AddDocumentSnapshotListenerOptions { private String reference; private final boolean includeMetadataChanges; + + @Nullable + private final String serverTimestampBehavior; + private String callbackId; - public AddDocumentSnapshotListenerOptions(String reference, @Nullable Boolean includeMetadataChanges, String callbackId) { + public AddDocumentSnapshotListenerOptions( + String reference, + @Nullable Boolean includeMetadataChanges, + @Nullable String serverTimestampBehavior, + String callbackId + ) { this.reference = reference; this.includeMetadataChanges = includeMetadataChanges == null ? false : includeMetadataChanges; + this.serverTimestampBehavior = serverTimestampBehavior; this.callbackId = callbackId; } @@ -22,6 +32,11 @@ public boolean isIncludeMetadataChanges() { return includeMetadataChanges; } + @Nullable + public String getServerTimestampBehavior() { + return serverTimestampBehavior; + } + public String getCallbackId() { return callbackId; } diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetCollectionResult.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetCollectionResult.java index f5965aa9d..81e2e9a45 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetCollectionResult.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetCollectionResult.java @@ -1,7 +1,9 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.results; +import androidx.annotation.NonNull; import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; +import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.QueryDocumentSnapshot; import com.google.firebase.firestore.QuerySnapshot; import io.capawesome.capacitorjs.plugins.firebase.firestore.FirebaseFirestoreHelper; @@ -12,15 +14,23 @@ public class GetCollectionResult implements Result { private QuerySnapshot querySnapshot; + @NonNull + private DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior; + public GetCollectionResult(QuerySnapshot querySnapshot) { + this(querySnapshot, DocumentSnapshot.ServerTimestampBehavior.NONE); + } + + public GetCollectionResult(QuerySnapshot querySnapshot, @NonNull DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) { this.querySnapshot = querySnapshot; + this.serverTimestampBehavior = serverTimestampBehavior; } @Override public JSObject toJSObject() { JSArray snapshotsResult = new JSArray(); for (QueryDocumentSnapshot document : querySnapshot) { - JSObject snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromMap(document.getData()); + JSObject snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromMap(document.getData(serverTimestampBehavior)); JSObject snapshotResult = new JSObject(); snapshotResult.put("id", document.getId()); diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java index a75c5d2db..19191e5e9 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java @@ -1,5 +1,7 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.results; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.getcapacitor.JSObject; import com.google.firebase.firestore.DocumentSnapshot; import io.capawesome.capacitorjs.plugins.firebase.firestore.FirebaseFirestoreHelper; @@ -10,12 +12,20 @@ public class GetDocumentResult implements Result { private DocumentSnapshot documentSnapshot; + @NonNull + private DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior; + public GetDocumentResult(DocumentSnapshot documentSnapshot) { + this(documentSnapshot, DocumentSnapshot.ServerTimestampBehavior.NONE); + } + + public GetDocumentResult(DocumentSnapshot documentSnapshot, @NonNull DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) { this.documentSnapshot = documentSnapshot; + this.serverTimestampBehavior = serverTimestampBehavior; } public JSObject toJSObject() { - Object snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromMap(documentSnapshot.getData()); + Object snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromMap(documentSnapshot.getData(serverTimestampBehavior)); JSObject snapshotResult = new JSObject(); snapshotResult.put("id", documentSnapshot.getId()); diff --git a/packages/firestore/ios/Plugin/Classes/Options/AddCollectionGroupSnapshotListenerOptions.swift b/packages/firestore/ios/Plugin/Classes/Options/AddCollectionGroupSnapshotListenerOptions.swift index d104793cf..c3a7d666b 100644 --- a/packages/firestore/ios/Plugin/Classes/Options/AddCollectionGroupSnapshotListenerOptions.swift +++ b/packages/firestore/ios/Plugin/Classes/Options/AddCollectionGroupSnapshotListenerOptions.swift @@ -6,13 +6,15 @@ import Capacitor private var compositeFilter: QueryCompositeFilterConstraint? private var queryConstraints: [QueryNonFilterConstraint] private var includeMetadataChanges: Bool + private var serverTimestampBehavior: String? private var callbackId: String - init(reference: String, compositeFilter: JSObject?, queryConstraints: [JSObject]?, includeMetadataChanges: Bool, callbackId: String) { + init(reference: String, compositeFilter: JSObject?, queryConstraints: [JSObject]?, includeMetadataChanges: Bool, serverTimestampBehavior: String?, callbackId: String) { self.reference = reference self.compositeFilter = FirebaseFirestoreHelper.createQueryCompositeFilterConstraintFromJSObject(compositeFilter) self.queryConstraints = FirebaseFirestoreHelper.createQueryNonFilterConstraintArrayFromJSArray(queryConstraints) self.includeMetadataChanges = includeMetadataChanges + self.serverTimestampBehavior = serverTimestampBehavior self.callbackId = callbackId } @@ -32,6 +34,10 @@ import Capacitor return includeMetadataChanges } + func getServerTimestampBehavior() -> String? { + return serverTimestampBehavior + } + func getCallbackId() -> String { return callbackId } diff --git a/packages/firestore/ios/Plugin/Classes/Options/AddCollectionSnapshotListenerOptions.swift b/packages/firestore/ios/Plugin/Classes/Options/AddCollectionSnapshotListenerOptions.swift index 624b42020..b62e7452e 100644 --- a/packages/firestore/ios/Plugin/Classes/Options/AddCollectionSnapshotListenerOptions.swift +++ b/packages/firestore/ios/Plugin/Classes/Options/AddCollectionSnapshotListenerOptions.swift @@ -6,13 +6,15 @@ import Capacitor private var compositeFilter: QueryCompositeFilterConstraint? private var queryConstraints: [QueryNonFilterConstraint] private var includeMetadataChanges: Bool + private var serverTimestampBehavior: String? private var callbackId: String - init(reference: String, compositeFilter: JSObject?, queryConstraints: [JSObject]?, includeMetadataChanges: Bool, callbackId: String) { + init(reference: String, compositeFilter: JSObject?, queryConstraints: [JSObject]?, includeMetadataChanges: Bool, serverTimestampBehavior: String?, callbackId: String) { self.reference = reference self.compositeFilter = FirebaseFirestoreHelper.createQueryCompositeFilterConstraintFromJSObject(compositeFilter) self.queryConstraints = FirebaseFirestoreHelper.createQueryNonFilterConstraintArrayFromJSArray(queryConstraints) self.includeMetadataChanges = includeMetadataChanges + self.serverTimestampBehavior = serverTimestampBehavior self.callbackId = callbackId } @@ -32,6 +34,10 @@ import Capacitor return includeMetadataChanges } + func getServerTimestampBehavior() -> String? { + return serverTimestampBehavior + } + func getCallbackId() -> String { return callbackId } diff --git a/packages/firestore/ios/Plugin/Classes/Options/AddDocumentSnapshotListenerOptions.swift b/packages/firestore/ios/Plugin/Classes/Options/AddDocumentSnapshotListenerOptions.swift index 1157b1ade..393dd6092 100644 --- a/packages/firestore/ios/Plugin/Classes/Options/AddDocumentSnapshotListenerOptions.swift +++ b/packages/firestore/ios/Plugin/Classes/Options/AddDocumentSnapshotListenerOptions.swift @@ -3,11 +3,13 @@ import Foundation @objc public class AddDocumentSnapshotListenerOptions: NSObject { private var reference: String private var includeMetadataChanges: Bool + private var serverTimestampBehavior: String? private var callbackId: String - init(reference: String, includeMetadataChanges: Bool, callbackId: String) { + init(reference: String, includeMetadataChanges: Bool, serverTimestampBehavior: String?, callbackId: String) { self.reference = reference self.includeMetadataChanges = includeMetadataChanges + self.serverTimestampBehavior = serverTimestampBehavior self.callbackId = callbackId } @@ -19,6 +21,10 @@ import Foundation return includeMetadataChanges } + func getServerTimestampBehavior() -> String? { + return serverTimestampBehavior + } + func getCallbackId() -> String { return callbackId } diff --git a/packages/firestore/ios/Plugin/Classes/Results/GetCollectionGroupResult.swift b/packages/firestore/ios/Plugin/Classes/Results/GetCollectionGroupResult.swift index e220b52a6..6c4993553 100644 --- a/packages/firestore/ios/Plugin/Classes/Results/GetCollectionGroupResult.swift +++ b/packages/firestore/ios/Plugin/Classes/Results/GetCollectionGroupResult.swift @@ -4,15 +4,17 @@ import Capacitor @objc public class GetCollectionGroupResult: NSObject, Result { let querySnapshot: QuerySnapshot + let serverTimestampBehavior: ServerTimestampBehavior - init(_ querySnapshot: QuerySnapshot) { + init(_ querySnapshot: QuerySnapshot, _ serverTimestampBehavior: ServerTimestampBehavior = .none) { self.querySnapshot = querySnapshot + self.serverTimestampBehavior = serverTimestampBehavior } public func toJSObject() -> AnyObject { var snapshotsResult = JSArray() for documentSnapshot in querySnapshot.documents { - let snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromHashMap(documentSnapshot.data()) + let snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromHashMap(documentSnapshot.data(with: serverTimestampBehavior)) var snapshotResult = JSObject() snapshotResult["id"] = documentSnapshot.documentID diff --git a/packages/firestore/ios/Plugin/Classes/Results/GetCollectionResult.swift b/packages/firestore/ios/Plugin/Classes/Results/GetCollectionResult.swift index b259416ec..14f179d1f 100644 --- a/packages/firestore/ios/Plugin/Classes/Results/GetCollectionResult.swift +++ b/packages/firestore/ios/Plugin/Classes/Results/GetCollectionResult.swift @@ -4,15 +4,17 @@ import Capacitor @objc public class GetCollectionResult: NSObject, Result { let querySnapshot: QuerySnapshot + let serverTimestampBehavior: ServerTimestampBehavior - init(_ querySnapshot: QuerySnapshot) { + init(_ querySnapshot: QuerySnapshot, _ serverTimestampBehavior: ServerTimestampBehavior = .none) { self.querySnapshot = querySnapshot + self.serverTimestampBehavior = serverTimestampBehavior } public func toJSObject() -> AnyObject { var snapshotsResult = JSArray() for documentSnapshot in querySnapshot.documents { - let snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromHashMap(documentSnapshot.data()) + let snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromHashMap(documentSnapshot.data(with: serverTimestampBehavior)) var snapshotResult = JSObject() snapshotResult["id"] = documentSnapshot.documentID diff --git a/packages/firestore/ios/Plugin/Classes/Results/GetDocumentResult.swift b/packages/firestore/ios/Plugin/Classes/Results/GetDocumentResult.swift index fa8509a44..3168a849b 100644 --- a/packages/firestore/ios/Plugin/Classes/Results/GetDocumentResult.swift +++ b/packages/firestore/ios/Plugin/Classes/Results/GetDocumentResult.swift @@ -4,13 +4,15 @@ import Capacitor @objc public class GetDocumentResult: NSObject, Result { let documentSnapshot: DocumentSnapshot + let serverTimestampBehavior: ServerTimestampBehavior - init(_ documentSnapshot: DocumentSnapshot) { + init(_ documentSnapshot: DocumentSnapshot, _ serverTimestampBehavior: ServerTimestampBehavior = .none) { self.documentSnapshot = documentSnapshot + self.serverTimestampBehavior = serverTimestampBehavior } public func toJSObject() -> AnyObject { - let snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromHashMap(documentSnapshot.data()) + let snapshotDataResult = FirebaseFirestoreHelper.createJSObjectFromHashMap(documentSnapshot.data(with: serverTimestampBehavior)) var snapshotResult = JSObject() snapshotResult["id"] = documentSnapshot.documentID diff --git a/packages/firestore/ios/Plugin/FirebaseFirestore.swift b/packages/firestore/ios/Plugin/FirebaseFirestore.swift index 23815a8a2..b7a610bc4 100644 --- a/packages/firestore/ios/Plugin/FirebaseFirestore.swift +++ b/packages/firestore/ios/Plugin/FirebaseFirestore.swift @@ -265,13 +265,14 @@ private actor ListenerRegistrationMap { @objc public func addDocumentSnapshotListener(_ options: AddDocumentSnapshotListenerOptions, completion: @escaping (Result?, Error?) -> Void) { let reference = options.getReference() let includeMetadataChanges = options.getIncludeMetadataChanges() + let serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior(options.getServerTimestampBehavior()) let callbackId = options.getCallbackId() let listenerRegistration = getFirestoreInstance().document(reference).addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { documentSnapshot, error in if let error = error { completion(nil, error) } else { - let result = GetDocumentResult(documentSnapshot!) + let result = GetDocumentResult(documentSnapshot!, serverTimestampBehavior) completion(result, nil) } } @@ -285,6 +286,7 @@ private actor ListenerRegistrationMap { let compositeFilter = options.getCompositeFilter() let queryConstraints = options.getQueryConstraints() let includeMetadataChanges = options.getIncludeMetadataChanges() + let serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior(options.getServerTimestampBehavior()) let callbackId = options.getCallbackId() Task { @@ -306,7 +308,7 @@ private actor ListenerRegistrationMap { if let error = error { completion(nil, error) } else { - let result = GetCollectionResult(querySnapshot!) + let result = GetCollectionResult(querySnapshot!, serverTimestampBehavior) completion(result, nil) } } @@ -322,6 +324,7 @@ private actor ListenerRegistrationMap { let compositeFilter = options.getCompositeFilter() let queryConstraints = options.getQueryConstraints() let includeMetadataChanges = options.getIncludeMetadataChanges() + let serverTimestampBehavior = FirebaseFirestoreHelper.createServerTimestampBehavior(options.getServerTimestampBehavior()) let callbackId = options.getCallbackId() Task { @@ -343,7 +346,7 @@ private actor ListenerRegistrationMap { if let error = error { completion(nil, error) } else { - let result = GetCollectionGroupResult(querySnapshot!) + let result = GetCollectionGroupResult(querySnapshot!, serverTimestampBehavior) completion(result, nil) } } diff --git a/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift b/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift index 622f3fd1f..b0c780a1c 100644 --- a/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift +++ b/packages/firestore/ios/Plugin/FirebaseFirestoreHelper.swift @@ -3,6 +3,17 @@ import FirebaseFirestore import Capacitor public class FirebaseFirestoreHelper { + public static func createServerTimestampBehavior(_ value: String?) -> ServerTimestampBehavior { + switch value { + case "estimate": + return .estimate + case "previous": + return .previous + default: + return .none + } + } + public static func createHashMapFromJSObject(_ object: JSObject) -> [String: Any] { var map: [String: Any] = [:] for key in object.keys { diff --git a/packages/firestore/ios/Plugin/FirebaseFirestorePlugin.swift b/packages/firestore/ios/Plugin/FirebaseFirestorePlugin.swift index 15d0787aa..7eedfb261 100644 --- a/packages/firestore/ios/Plugin/FirebaseFirestorePlugin.swift +++ b/packages/firestore/ios/Plugin/FirebaseFirestorePlugin.swift @@ -295,6 +295,7 @@ public class FirebaseFirestorePlugin: CAPPlugin, CAPBridgedPlugin { return } let includeMetadataChanges = call.getBool("includeMetadataChanges", false) + let serverTimestampBehavior = call.getString("serverTimestamps") guard let callbackId = call.callbackId else { call.reject(errorCallbackIdMissing) return @@ -302,7 +303,7 @@ public class FirebaseFirestorePlugin: CAPPlugin, CAPBridgedPlugin { self.pluginCallMap[callbackId] = call - let options = AddDocumentSnapshotListenerOptions(reference: reference, includeMetadataChanges: includeMetadataChanges, callbackId: callbackId) + let options = AddDocumentSnapshotListenerOptions(reference: reference, includeMetadataChanges: includeMetadataChanges, serverTimestampBehavior: serverTimestampBehavior, callbackId: callbackId) implementation?.addDocumentSnapshotListener(options, completion: { result, error in if let error = error { @@ -326,6 +327,7 @@ public class FirebaseFirestorePlugin: CAPPlugin, CAPBridgedPlugin { let compositeFilter = call.getObject("compositeFilter") let queryConstraints = call.getArray("queryConstraints", JSObject.self) let includeMetadataChanges = call.getBool("includeMetadataChanges", false) + let serverTimestampBehavior = call.getString("serverTimestamps") guard let callbackId = call.callbackId else { call.reject(errorCallbackIdMissing) return @@ -333,7 +335,7 @@ public class FirebaseFirestorePlugin: CAPPlugin, CAPBridgedPlugin { self.pluginCallMap[callbackId] = call - let options = AddCollectionSnapshotListenerOptions(reference: reference, compositeFilter: compositeFilter, queryConstraints: queryConstraints, includeMetadataChanges: includeMetadataChanges, callbackId: callbackId) + let options = AddCollectionSnapshotListenerOptions(reference: reference, compositeFilter: compositeFilter, queryConstraints: queryConstraints, includeMetadataChanges: includeMetadataChanges, serverTimestampBehavior: serverTimestampBehavior, callbackId: callbackId) do { implementation?.addCollectionSnapshotListener(options, completion: { result, error in @@ -362,6 +364,7 @@ public class FirebaseFirestorePlugin: CAPPlugin, CAPBridgedPlugin { let compositeFilter = call.getObject("compositeFilter") let queryConstraints = call.getArray("queryConstraints", JSObject.self) let includeMetadataChanges = call.getBool("includeMetadataChanges", false) + let serverTimestampBehavior = call.getString("serverTimestamps") guard let callbackId = call.callbackId else { call.reject(errorCallbackIdMissing) return @@ -369,7 +372,7 @@ public class FirebaseFirestorePlugin: CAPPlugin, CAPBridgedPlugin { self.pluginCallMap[callbackId] = call - let options = AddCollectionGroupSnapshotListenerOptions(reference: reference, compositeFilter: compositeFilter, queryConstraints: queryConstraints, includeMetadataChanges: includeMetadataChanges, callbackId: callbackId) + let options = AddCollectionGroupSnapshotListenerOptions(reference: reference, compositeFilter: compositeFilter, queryConstraints: queryConstraints, includeMetadataChanges: includeMetadataChanges, serverTimestampBehavior: serverTimestampBehavior, callbackId: callbackId) do { implementation?.addCollectionGroupSnapshotListener(options, completion: { result, error in diff --git a/packages/firestore/src/definitions.ts b/packages/firestore/src/definitions.ts index cd1b1b783..46732e910 100644 --- a/packages/firestore/src/definitions.ts +++ b/packages/firestore/src/definitions.ts @@ -452,13 +452,26 @@ export interface SnapshotListenerOptions { * @default "default" */ readonly source?: 'default' | 'cache'; + /** + * Control how pending server timestamps are returned in snapshots + * that have not yet been acknowledged by the server. + * + * - `none`: pending server timestamps are returned as `null`. + * - `estimate`: pending server timestamps are replaced with the + * local client's time estimate. + * - `previous`: pending server timestamps are replaced with the + * previous value of the field (or `null` if there is no previous value). + * + * @since 8.3.0 + * @default "none" + */ + readonly serverTimestamps?: 'estimate' | 'previous' | 'none'; } /** * @since 5.2.0 */ -export interface AddDocumentSnapshotListenerOptions - extends SnapshotListenerOptions { +export interface AddDocumentSnapshotListenerOptions extends SnapshotListenerOptions { /** * The reference as a string, with path components separated by a forward slash (`/`). * @@ -483,8 +496,7 @@ export type AddDocumentSnapshotListenerCallbackEvent = GetDocumentResult; /** * @since 5.2.0 */ -export interface AddCollectionSnapshotListenerOptions - extends SnapshotListenerOptions { +export interface AddCollectionSnapshotListenerOptions extends SnapshotListenerOptions { /** * The reference as a string, with path components separated by a forward slash (`/`). * @@ -522,8 +534,7 @@ export type AddCollectionSnapshotListenerCallbackEvent = /** * @since 6.1.0 */ -export interface AddCollectionGroupSnapshotListenerOptions - extends SnapshotListenerOptions { +export interface AddCollectionGroupSnapshotListenerOptions extends SnapshotListenerOptions { /** * The reference as a string, with path components separated by a forward slash (`/`). * diff --git a/packages/firestore/src/web.ts b/packages/firestore/src/web.ts index 3319d6857..cb4dbedfd 100644 --- a/packages/firestore/src/web.ts +++ b/packages/firestore/src/web.ts @@ -118,7 +118,11 @@ export class FirebaseFirestoreWeb snapshots: snapshot.docs.map(documentSnapshot => ({ id: documentSnapshot.id, path: documentSnapshot.ref.path, - data: this.deserializeData(documentSnapshot.data()) as T, + data: this.deserializeData( + documentSnapshot.data({ + serverTimestamps: options.serverTimestamps, + }), + ) as T, metadata: { hasPendingWrites: documentSnapshot.metadata.hasPendingWrites, fromCache: documentSnapshot.metadata.fromCache, @@ -155,7 +159,11 @@ export class FirebaseFirestoreWeb snapshots: snapshot.docs.map(documentSnapshot => ({ id: documentSnapshot.id, path: documentSnapshot.ref.path, - data: this.deserializeData(documentSnapshot.data()) as T, + data: this.deserializeData( + documentSnapshot.data({ + serverTimestamps: options.serverTimestamps, + }), + ) as T, metadata: { hasPendingWrites: documentSnapshot.metadata.hasPendingWrites, fromCache: documentSnapshot.metadata.fromCache, @@ -202,7 +210,9 @@ export class FirebaseFirestoreWeb source: options.source, }, snapshot => { - const data = snapshot.data(); + const data = snapshot.data({ + serverTimestamps: options.serverTimestamps, + }); const event: AddDocumentSnapshotListenerCallbackEvent = { snapshot: { id: snapshot.id, From 8bee41b8f1a49aff603fb8e6c569b4a9bafb0346 Mon Sep 17 00:00:00 2001 From: Robin Genz Date: Mon, 25 May 2026 11:24:55 +0200 Subject: [PATCH 2/2] fix(firestore): address PR review feedback - Normalize serverTimestamps on Web to match native fall-back behavior - Remove unused Nullable import in GetDocumentResult.java - Reformat definitions.ts with prettier 3.4.2 (pinned CI version) --- .../classes/results/GetDocumentResult.java | 1 - packages/firestore/src/definitions.ts | 9 ++++++--- packages/firestore/src/web.ts | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java index 19191e5e9..2a9dc0385 100644 --- a/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java +++ b/packages/firestore/android/src/main/java/io/capawesome/capacitorjs/plugins/firebase/firestore/classes/results/GetDocumentResult.java @@ -1,7 +1,6 @@ package io.capawesome.capacitorjs.plugins.firebase.firestore.classes.results; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.getcapacitor.JSObject; import com.google.firebase.firestore.DocumentSnapshot; import io.capawesome.capacitorjs.plugins.firebase.firestore.FirebaseFirestoreHelper; diff --git a/packages/firestore/src/definitions.ts b/packages/firestore/src/definitions.ts index 46732e910..2539a7c0c 100644 --- a/packages/firestore/src/definitions.ts +++ b/packages/firestore/src/definitions.ts @@ -471,7 +471,8 @@ export interface SnapshotListenerOptions { /** * @since 5.2.0 */ -export interface AddDocumentSnapshotListenerOptions extends SnapshotListenerOptions { +export interface AddDocumentSnapshotListenerOptions + extends SnapshotListenerOptions { /** * The reference as a string, with path components separated by a forward slash (`/`). * @@ -496,7 +497,8 @@ export type AddDocumentSnapshotListenerCallbackEvent = GetDocumentResult; /** * @since 5.2.0 */ -export interface AddCollectionSnapshotListenerOptions extends SnapshotListenerOptions { +export interface AddCollectionSnapshotListenerOptions + extends SnapshotListenerOptions { /** * The reference as a string, with path components separated by a forward slash (`/`). * @@ -534,7 +536,8 @@ export type AddCollectionSnapshotListenerCallbackEvent = /** * @since 6.1.0 */ -export interface AddCollectionGroupSnapshotListenerOptions extends SnapshotListenerOptions { +export interface AddCollectionGroupSnapshotListenerOptions + extends SnapshotListenerOptions { /** * The reference as a string, with path components separated by a forward slash (`/`). * diff --git a/packages/firestore/src/web.ts b/packages/firestore/src/web.ts index cb4dbedfd..33c642225 100644 --- a/packages/firestore/src/web.ts +++ b/packages/firestore/src/web.ts @@ -91,6 +91,12 @@ import { FieldValue } from './field-value'; import { GeoPoint } from './geopoint'; import { Timestamp } from './timestamp'; +type ServerTimestamps = 'estimate' | 'previous' | 'none'; + +function normalizeServerTimestamps(value: unknown): ServerTimestamps { + return value === 'estimate' || value === 'previous' ? value : 'none'; +} + export class FirebaseFirestoreWeb extends WebPlugin implements FirebaseFirestorePlugin @@ -120,7 +126,9 @@ export class FirebaseFirestoreWeb path: documentSnapshot.ref.path, data: this.deserializeData( documentSnapshot.data({ - serverTimestamps: options.serverTimestamps, + serverTimestamps: normalizeServerTimestamps( + options.serverTimestamps, + ), }), ) as T, metadata: { @@ -161,7 +169,9 @@ export class FirebaseFirestoreWeb path: documentSnapshot.ref.path, data: this.deserializeData( documentSnapshot.data({ - serverTimestamps: options.serverTimestamps, + serverTimestamps: normalizeServerTimestamps( + options.serverTimestamps, + ), }), ) as T, metadata: { @@ -211,7 +221,7 @@ export class FirebaseFirestoreWeb }, snapshot => { const data = snapshot.data({ - serverTimestamps: options.serverTimestamps, + serverTimestamps: normalizeServerTimestamps(options.serverTimestamps), }); const event: AddDocumentSnapshotListenerCallbackEvent = { snapshot: {