diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestoreMessageCodec.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestoreMessageCodec.java index 1b2b2d1641e6..0286a06bbdc2 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestoreMessageCodec.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestoreMessageCodec.java @@ -139,9 +139,16 @@ private void writeQuerySnapshot(ByteArrayOutputStream stream, QuerySnapshot valu List> documents = new ArrayList<>(); List metadatas = new ArrayList<>(); + DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior = + FlutterFirebaseFirestorePlugin.serverTimestampBehaviorHashMap.get(value.hashCode()); + for (DocumentSnapshot document : value.getDocuments()) { paths.add(document.getReference().getPath()); - documents.add(document.getData()); + if (serverTimestampBehavior != null) { + documents.add(document.getData(serverTimestampBehavior)); + } else { + documents.add(document.getData()); + } metadatas.add(document.getMetadata()); } @@ -151,6 +158,7 @@ private void writeQuerySnapshot(ByteArrayOutputStream stream, QuerySnapshot valu querySnapshotMap.put("documentChanges", value.getDocumentChanges()); querySnapshotMap.put("metadata", value.getMetadata()); + FlutterFirebaseFirestorePlugin.serverTimestampBehaviorHashMap.remove(value.hashCode()); writeValue(stream, querySnapshotMap); } @@ -190,13 +198,20 @@ private void writeDocumentSnapshot(ByteArrayOutputStream stream, DocumentSnapsho snapshotMap.put("path", value.getReference().getPath()); if (value.exists()) { - snapshotMap.put("data", value.getData()); + DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior = + FlutterFirebaseFirestorePlugin.serverTimestampBehaviorHashMap.get(value.hashCode()); + if (serverTimestampBehavior != null) { + snapshotMap.put("data", value.getData(serverTimestampBehavior)); + } else { + snapshotMap.put("data", value.getData()); + } } else { snapshotMap.put("data", null); } snapshotMap.put("metadata", value.getMetadata()); + FlutterFirebaseFirestorePlugin.serverTimestampBehaviorHashMap.remove(value.hashCode()); writeValue(stream, snapshotMap); } diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java index 2a52eefa4848..e6d6ecd48865 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java @@ -42,6 +42,7 @@ import io.flutter.plugins.firebase.firestore.streamhandler.SnapshotsInSyncStreamHandler; import io.flutter.plugins.firebase.firestore.streamhandler.TransactionStreamHandler; import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter; +import io.flutter.plugins.firebase.firestore.utils.ServerTimestampBehaviorConverter; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -73,6 +74,10 @@ public class FlutterFirebaseFirestorePlugin private final Map streamHandlers = new HashMap<>(); private final Map transactionHandlers = new HashMap<>(); + // Used in the decoder to know which ServerTimestampBehavior to use + public static final Map + serverTimestampBehaviorHashMap = new HashMap<>(); + protected static FirebaseFirestore getCachedFirebaseFirestoreInstanceForKey(String key) { synchronized (firestoreInstanceCache) { return firestoreInstanceCache.get(key); @@ -294,8 +299,10 @@ private Task queryGet(Map arguments) { "An error occurred while parsing query arguments, see native logs for more information. Please report this issue.")); return; } + final QuerySnapshot querySnapshot = Tasks.await(query.get(source)); + saveTimestampBehavior(arguments, querySnapshot.hashCode()); - taskCompletionSource.setResult(Tasks.await(query.get(source))); + taskCompletionSource.setResult(querySnapshot); } catch (Exception e) { taskCompletionSource.setException(e); } @@ -314,7 +321,10 @@ private Task documentGet(Map arguments) { DocumentReference documentReference = (DocumentReference) Objects.requireNonNull(arguments.get("reference")); - taskCompletionSource.setResult(Tasks.await(documentReference.get(source))); + final DocumentSnapshot documentSnapshot = Tasks.await(documentReference.get(source)); + saveTimestampBehavior(arguments, documentSnapshot.hashCode()); + + taskCompletionSource.setResult(documentSnapshot); } catch (Exception e) { taskCompletionSource.setException(e); } @@ -343,7 +353,10 @@ private Task namedQueryGet(Map arguments) { return; } - taskCompletionSource.setResult(Tasks.await(query.get(source))); + final QuerySnapshot querySnapshot = Tasks.await(query.get(source)); + saveTimestampBehavior(arguments, querySnapshot.hashCode()); + + taskCompletionSource.setResult(querySnapshot); } catch (Exception e) { taskCompletionSource.setException(e); } @@ -352,6 +365,14 @@ private Task namedQueryGet(Map arguments) { return taskCompletionSource.getTask(); } + private void saveTimestampBehavior(Map arguments, int hashCode) { + String serverTimestampBehaviorString = (String) arguments.get("serverTimestampBehavior"); + DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior = + ServerTimestampBehaviorConverter.toServerTimestampBehavior(serverTimestampBehaviorString); + + serverTimestampBehaviorHashMap.put(hashCode, serverTimestampBehavior); + } + private Task documentSet(Map arguments) { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/QuerySnapshotsStreamHandler.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/QuerySnapshotsStreamHandler.java index 05c0ce1d9658..e9f217382a7d 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/QuerySnapshotsStreamHandler.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/streamhandler/QuerySnapshotsStreamHandler.java @@ -8,12 +8,15 @@ import static io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin.DEFAULT_ERROR_CODE; +import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.MetadataChanges; import com.google.firebase.firestore.Query; import io.flutter.plugin.common.EventChannel.EventSink; import io.flutter.plugin.common.EventChannel.StreamHandler; +import io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin; import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter; +import io.flutter.plugins.firebase.firestore.utils.ServerTimestampBehaviorConverter; import java.util.Map; import java.util.Objects; @@ -32,6 +35,9 @@ public void onListen(Object arguments, EventSink events) { : MetadataChanges.EXCLUDE; Query query = (Query) argumentsMap.get("query"); + String serverTimestampBehaviorString = (String) argumentsMap.get("serverTimestampBehavior"); + DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior = + ServerTimestampBehaviorConverter.toServerTimestampBehavior(serverTimestampBehaviorString); if (query == null) { throw new IllegalArgumentException( @@ -49,6 +55,10 @@ public void onListen(Object arguments, EventSink events) { onCancel(null); } else { + if (querySnapshot != null) { + FlutterFirebaseFirestorePlugin.serverTimestampBehaviorHashMap.put( + querySnapshot.hashCode(), serverTimestampBehavior); + } events.success(querySnapshot); } }); diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ServerTimestampBehaviorConverter.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ServerTimestampBehaviorConverter.java new file mode 100644 index 000000000000..dcbfab5c37a1 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ServerTimestampBehaviorConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022, the Chromium project authors. Please see the AUTHORS file + * for details. All rights reserved. Use of this source code is governed by a + * BSD-style license that can be found in the LICENSE file. + */ + +package io.flutter.plugins.firebase.firestore.utils; + +import androidx.annotation.Nullable; +import com.google.firebase.firestore.DocumentSnapshot; + +public class ServerTimestampBehaviorConverter { + public static DocumentSnapshot.ServerTimestampBehavior toServerTimestampBehavior( + @Nullable String serverTimestampBehavior) { + if (serverTimestampBehavior == null) { + return DocumentSnapshot.ServerTimestampBehavior.NONE; + } + switch (serverTimestampBehavior) { + case "estimate": + return DocumentSnapshot.ServerTimestampBehavior.ESTIMATE; + case "previous": + return DocumentSnapshot.ServerTimestampBehavior.PREVIOUS; + case "none": + default: + return DocumentSnapshot.ServerTimestampBehavior.NONE; + } + } +} diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart index d947e07eeb9f..5ae2b13b070e 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart @@ -145,6 +145,28 @@ void runQueryTests() { expect(qs.metadata.isFromCache, isFalse); }); + test('uses [GetOptions] serverTimestampBehavior previous', () async { + CollectionReference> collection = + await initializeTest('get'); + QuerySnapshot> qs = await collection.get( + const GetOptions( + serverTimestampBehavior: ServerTimestampBehavior.previous, + ), + ); + expect(qs, isA>>()); + }); + + test('uses [GetOptions] serverTimestampBehavior estimate', () async { + CollectionReference> collection = + await initializeTest('get'); + QuerySnapshot> qs = await collection.get( + const GetOptions( + serverTimestampBehavior: ServerTimestampBehavior.estimate, + ), + ); + expect(qs, isA>>()); + }); + test('throws a [FirebaseException]', () async { CollectionReference> collection = firestore.collection('not-allowed'); diff --git a/packages/cloud_firestore/cloud_firestore/example/lib/main.dart b/packages/cloud_firestore/cloud_firestore/example/lib/main.dart index a6060fb94da2..261e3cd26f1f 100755 --- a/packages/cloud_firestore/cloud_firestore/example/lib/main.dart +++ b/packages/cloud_firestore/cloud_firestore/example/lib/main.dart @@ -190,7 +190,12 @@ class _FilmListState extends State { } Future _resetLikes() async { - final movies = await moviesRef.get(); + final movies = await moviesRef.get( + const GetOptions( + serverTimestampBehavior: ServerTimestampBehavior.previous, + ), + ); + WriteBatch batch = FirebaseFirestore.instance.batch(); for (final movie in movies.docs) { diff --git a/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestorePlugin.m b/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestorePlugin.m index 2ef8576c6663..1703f40652d1 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestorePlugin.m +++ b/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestorePlugin.m @@ -60,6 +60,8 @@ - (NSString *)registerEventChannelWithPrefix:(NSString *)prefix streamHandler:(NSObject *)handler; @end +static NSMutableDictionary *_serverTimestampMap; + @implementation FLTFirebaseFirestorePlugin { NSMutableDictionary *_eventChannels; NSMutableDictionary *> *_streamHandlers; @@ -69,6 +71,10 @@ @implementation FLTFirebaseFirestorePlugin { FlutterStandardMethodCodec *_codec; ++ (NSMutableDictionary *)serverTimestampMap { + return _serverTimestampMap; +} + + (void)initialize { _codec = [FlutterStandardMethodCodec codecWithReaderWriter:[FLTFirebaseFirestoreReaderWriter new]]; @@ -98,6 +104,7 @@ - (instancetype)init:(NSObject *)messenger { _eventChannels = [NSMutableDictionary dictionary]; _streamHandlers = [NSMutableDictionary dictionary]; _transactionHandlers = [NSMutableDictionary dictionary]; + _serverTimestampMap = [NSMutableDictionary dictionary]; } return self; } @@ -457,10 +464,12 @@ - (void)documentDelete:(id)arguments withMethodCallResult:(FLTFirebaseMethodCall - (void)documentGet:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { FIRDocumentReference *document = arguments[@"reference"]; FIRFirestoreSource source = [FLTFirebaseFirestoreUtils FIRFirestoreSourceFromArguments:arguments]; + NSString *serverTimestampBehaviorString = arguments[@"serverTimestampBehavior"]; id completion = ^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error) { if (error != nil) { result.error(nil, nil, nil, error); } else { + [_serverTimestampMap setObject:serverTimestampBehaviorString forKey:@([snapshot hash])]; result.success(snapshot); } }; @@ -479,12 +488,16 @@ - (void)queryGet:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult return; } + NSString *serverTimestampBehaviorString = arguments[@"serverTimestampBehavior"]; + FIRFirestoreSource source = [FLTFirebaseFirestoreUtils FIRFirestoreSourceFromArguments:arguments]; [query getDocumentsWithSource:source completion:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { if (error != nil) { result.error(nil, nil, nil, error); } else { + [_serverTimestampMap setObject:serverTimestampBehaviorString + forKey:@([snapshot hash])]; result.success(snapshot); } }]; @@ -495,6 +508,7 @@ - (void)namedQueryGet:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallR NSString *name = arguments[@"name"]; FIRFirestoreSource source = [FLTFirebaseFirestoreUtils FIRFirestoreSourceFromArguments:arguments]; + NSString *serverTimestampBehaviorString = arguments[@"serverTimestampBehavior"]; [firestore getQueryNamed:name completion:^(FIRQuery *_Nullable query) { @@ -511,6 +525,9 @@ - (void)namedQueryGet:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallR if (error != nil) { result.error(nil, nil, nil, error); } else { + [_serverTimestampMap + setObject:serverTimestampBehaviorString + forKey:@([snapshot hash])]; result.success(snapshot); } }]; diff --git a/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestoreWriter.m b/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestoreWriter.m index ce7204a86a64..0a89f30b5e18 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestoreWriter.m +++ b/packages/cloud_firestore/cloud_firestore/ios/Classes/FLTFirebaseFirestoreWriter.m @@ -7,6 +7,7 @@ #import "Private/FLTFirebaseFirestoreUtils.h" #import "Private/FLTFirebaseFirestoreWriter.h" +#import "Public/FLTFirebaseFirestorePlugin.h" @implementation FLTFirebaseFirestoreWriter : FlutterStandardWriter - (void)writeValue:(id)value { @@ -133,10 +134,32 @@ - (NSDictionary *)FIRDocumentChange:(FIRDocumentChange *)documentChange { }; } +- (FIRServerTimestampBehavior)toServerTimestampBehavior:(NSString *)serverTimestampBehavior { + if (serverTimestampBehavior == nil) { + return FIRServerTimestampBehaviorNone; + } + + if ([serverTimestampBehavior isEqualToString:@"estimate"]) { + return FIRServerTimestampBehaviorEstimate; + } else if ([serverTimestampBehavior isEqualToString:@"previous"]) { + return FIRServerTimestampBehaviorPrevious; + } else { + return FIRServerTimestampBehaviorNone; + } +} + - (NSDictionary *)FIRDocumentSnapshot:(FIRDocumentSnapshot *)documentSnapshot { + FIRServerTimestampBehavior serverTimestampBehavior = + [self toServerTimestampBehavior:FLTFirebaseFirestorePlugin + .serverTimestampMap[@([documentSnapshot hash])]]; + + [FLTFirebaseFirestorePlugin.serverTimestampMap removeObjectForKey:@([documentSnapshot hash])]; + return @{ @"path" : documentSnapshot.reference.path, - @"data" : documentSnapshot.exists ? (id)documentSnapshot.data : [NSNull null], + @"data" : documentSnapshot.exists + ? (id)[documentSnapshot dataWithServerTimestampBehavior:serverTimestampBehavior] + : [NSNull null], @"metadata" : documentSnapshot.metadata, }; } @@ -168,10 +191,15 @@ - (NSDictionary *)FIRQuerySnapshot:(FIRQuerySnapshot *)querySnapshot { NSMutableArray *paths = [NSMutableArray array]; NSMutableArray *documents = [NSMutableArray array]; NSMutableArray *metadatas = [NSMutableArray array]; + FIRServerTimestampBehavior serverTimestampBehavior = + [self toServerTimestampBehavior:FLTFirebaseFirestorePlugin + .serverTimestampMap[@([querySnapshot hash])]]; + + [FLTFirebaseFirestorePlugin.serverTimestampMap removeObjectForKey:@([querySnapshot hash])]; for (FIRDocumentSnapshot *document in querySnapshot.documents) { [paths addObject:document.reference.path]; - [documents addObject:document.data]; + [documents addObject:[document dataWithServerTimestampBehavior:serverTimestampBehavior]]; [metadatas addObject:document.metadata]; } diff --git a/packages/cloud_firestore/cloud_firestore/ios/Classes/Public/FLTFirebaseFirestorePlugin.h b/packages/cloud_firestore/cloud_firestore/ios/Classes/Public/FLTFirebaseFirestorePlugin.h index 6658d5d013e9..bc783c9a79ef 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/Classes/Public/FLTFirebaseFirestorePlugin.h +++ b/packages/cloud_firestore/cloud_firestore/ios/Classes/Public/FLTFirebaseFirestorePlugin.h @@ -13,4 +13,5 @@ #import @interface FLTFirebaseFirestorePlugin : FLTFirebasePlugin ++ (NSMutableDictionary *)serverTimestampMap; @end diff --git a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart index 8383d7d3c6da..ee61f1b8bd31 100755 --- a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart @@ -25,6 +25,7 @@ export 'package:cloud_firestore_platform_interface/cloud_firestore_platform_inte Timestamp, Source, GetOptions, + ServerTimestampBehavior, SetOptions, DocumentChangeType, PersistenceSettings, diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index 3fc787e38770..d0580ca3500e 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -8,36 +8,40 @@ library cloud_firestore_platform_interface; import 'src/internal/pointer.dart'; export 'package:collection/collection.dart' show ListEquality; + +export 'src/aggregate_source.dart'; export 'src/blob.dart'; export 'src/field_path.dart'; export 'src/geo_point.dart'; -export 'src/platform_interface/platform_interface_firestore.dart'; +export 'src/get_options.dart'; +export 'src/load_bundle_task_state.dart'; +export 'src/load_bundle_task_state.dart'; +export 'src/persistence_settings.dart'; +export 'src/platform_interface/platform_interface_aggregate_query.dart'; +export 'src/platform_interface/platform_interface_aggregate_query_snapshot.dart'; export 'src/platform_interface/platform_interface_collection_reference.dart'; export 'src/platform_interface/platform_interface_document_change.dart'; export 'src/platform_interface/platform_interface_document_reference.dart'; export 'src/platform_interface/platform_interface_document_snapshot.dart'; export 'src/platform_interface/platform_interface_field_value.dart'; export 'src/platform_interface/platform_interface_field_value_factory.dart'; +export 'src/platform_interface/platform_interface_firestore.dart'; +export 'src/platform_interface/platform_interface_index_definitions.dart'; +export 'src/platform_interface/platform_interface_load_bundle_task.dart'; +export 'src/platform_interface/platform_interface_load_bundle_task.dart'; +export 'src/platform_interface/platform_interface_load_bundle_task_snapshot.dart'; +export 'src/platform_interface/platform_interface_load_bundle_task_snapshot.dart'; export 'src/platform_interface/platform_interface_query.dart'; export 'src/platform_interface/platform_interface_query_snapshot.dart'; export 'src/platform_interface/platform_interface_transaction.dart'; export 'src/platform_interface/platform_interface_write_batch.dart'; -export 'src/platform_interface/platform_interface_load_bundle_task.dart'; -export 'src/platform_interface/platform_interface_load_bundle_task_snapshot.dart'; -export 'src/platform_interface/platform_interface_aggregate_query.dart'; -export 'src/platform_interface/platform_interface_aggregate_query_snapshot.dart'; -export 'src/platform_interface/platform_interface_index_definitions.dart'; -export 'src/aggregate_source.dart'; +export 'src/platform_interface/utils/load_bundle_task_state.dart'; +export 'src/server_timestamp_behavior.dart'; +export 'src/set_options.dart'; +export 'src/settings.dart'; export 'src/snapshot_metadata.dart'; export 'src/source.dart'; -export 'src/load_bundle_task_state.dart'; export 'src/timestamp.dart'; -export 'src/settings.dart'; -export 'src/get_options.dart'; -export 'src/set_options.dart'; -export 'src/persistence_settings.dart'; -export 'src/load_bundle_task_state.dart'; -export 'src/platform_interface/utils/load_bundle_task_state.dart'; /// Helper method exposed to determine whether a given [collectionPath] points to /// a valid Firestore collection. diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/get_options.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/get_options.dart index 349f51b79aa9..ae4b63c8b9cd 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/get_options.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/get_options.dart @@ -3,6 +3,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'server_timestamp_behavior.dart'; import 'source.dart'; /// An options class that configures the behavior of get() calls on [DocumentReference] and [Query]. @@ -31,8 +32,19 @@ class GetOptions { /// QuerySnapshot with no documents. final Source source; + /// If set, controls the return value for server timestamps that have not yet been set to their final value. + /// + /// By specifying [ServerTimestampBehavior.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 [ServerTimestampBehavior.previous], pending timestamps will be ignored and return their previous value instead. + /// + /// If omitted or set to [ServerTimestampBehavior.none], null will be returned by default until the server value becomes available. + final ServerTimestampBehavior serverTimestampBehavior; + /// Creates a [GetOptions] instance. const GetOptions({ this.source = Source.serverAndCache, + this.serverTimestampBehavior = ServerTimestampBehavior.none, }); } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_document_reference.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_document_reference.dart index a24f3f22e744..2647cf223ec4 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_document_reference.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_document_reference.dart @@ -11,8 +11,8 @@ import 'package:cloud_firestore_platform_interface/src/internal/pointer.dart'; import 'package:flutter/services.dart'; import 'method_channel_firestore.dart'; -import 'utils/source.dart'; import 'utils/exception.dart'; +import 'utils/source.dart'; /// An implementation of [DocumentReferencePlatform] that uses [MethodChannel] to /// communicate with Firebase plugins. @@ -74,6 +74,9 @@ class MethodChannelDocumentReference extends DocumentReferencePlatform { 'firestore': firestore, 'reference': this, 'source': getSourceString(options.source), + 'serverTimestampBehavior': getServerTimestampBehaviorString( + options.serverTimestampBehavior, + ), }, ); diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart index d1cfcce7f731..89cfadef9ea8 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart @@ -103,6 +103,9 @@ class MethodChannelFirebaseFirestore extends FirebaseFirestorePlatform { 'name': name, 'firestore': FirebaseFirestorePlatform.instance, 'source': getSourceString(options.source), + 'serverTimestampBehavior': getServerTimestampBehaviorString( + options.serverTimestampBehavior, + ), }, ); diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_query.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_query.dart index e06842546b0a..8a9b13d28a0b 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_query.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_query.dart @@ -14,8 +14,8 @@ import 'package:flutter/services.dart'; import 'method_channel_aggregate_query.dart'; import 'method_channel_firestore.dart'; import 'method_channel_query_snapshot.dart'; -import 'utils/source.dart'; import 'utils/exception.dart'; +import 'utils/source.dart'; /// An implementation of [QueryPlatform] that uses [MethodChannel] to /// communicate with Firebase plugins. @@ -103,6 +103,9 @@ class MethodChannelQuery extends QueryPlatform { 'query': this, 'firestore': firestore, 'source': getSourceString(options.source), + 'serverTimestampBehavior': getServerTimestampBehaviorString( + options.serverTimestampBehavior, + ), }, ); @@ -131,6 +134,8 @@ class MethodChannelQuery extends QueryPlatform { @override Stream snapshots({ bool includeMetadataChanges = false, + ServerTimestampBehavior serverTimestampBehavior = + ServerTimestampBehavior.none, }) { // It's fine to let the StreamController be garbage collected once all the // subscribers have cancelled; this analyzer warning is safe to ignore. @@ -150,6 +155,9 @@ class MethodChannelQuery extends QueryPlatform { arguments: { 'query': this, 'includeMetadataChanges': includeMetadataChanges, + 'serverTimestampBehavior': getServerTimestampBehaviorString( + serverTimestampBehavior, + ), }, onError: convertPlatformException, ).listen( diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/server_timestamp_behavior.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/server_timestamp_behavior.dart new file mode 100644 index 000000000000..e6dd1fdd4633 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/server_timestamp_behavior.dart @@ -0,0 +1,22 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +enum ServerTimestampBehavior { + none, + estimate, + previous, +} + +String getServerTimestampBehaviorString( + ServerTimestampBehavior serverTimestampBehavior, +) { + switch (serverTimestampBehavior) { + case ServerTimestampBehavior.none: + return 'none'; + case ServerTimestampBehavior.estimate: + return 'estimate'; + case ServerTimestampBehavior.previous: + return 'previous'; + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_document_reference_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_document_reference_test.dart index d611096e7ff8..34c6a8315041 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_document_reference_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_document_reference_test.dart @@ -6,12 +6,11 @@ import 'dart:async'; import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:firebase_core/firebase_core.dart'; - import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_document_reference.dart'; import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_field_value_factory.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; import '../utils/test_common.dart'; diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_query_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_query_test.dart index df7c5ad7bebd..a1165e29c56a 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_query_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/method_channel_tests/method_channel_query_test.dart @@ -6,12 +6,11 @@ import 'dart:async'; import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; - -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/services.dart'; import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_firestore.dart'; import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_query.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; import '../utils/test_common.dart'; @@ -243,6 +242,7 @@ void main() { expect(log[1].arguments, { 'query': isInstanceOf(), 'includeMetadataChanges': false, + 'serverTimestampBehavior': 'none' }); //TODO(russellwheatley): NNBD failure. 'cancel' method not being recorded // expect(log[2].method, 'cancel'); diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart index 28d6c9c64a27..61bc4b249a48 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart @@ -192,7 +192,13 @@ class FirebaseFirestoreWeb extends FirebaseFirestorePlatform { firestore_interop.QuerySnapshot snapshot = await query.get(convertGetOptions(options)); - return convertWebQuerySnapshot(this, snapshot); + return convertWebQuerySnapshot( + this, + snapshot, + getServerTimestampBehaviorString( + options.serverTimestampBehavior, + ), + ); } @override diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/document_reference_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/document_reference_web.dart index e764bf2ebdb7..98d47175f15c 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/document_reference_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/document_reference_web.dart @@ -6,9 +6,9 @@ import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; import 'internals.dart'; -import 'utils/web_utils.dart'; -import 'utils/encode_utility.dart'; import 'interop/firestore.dart' as firestore_interop; +import 'utils/encode_utility.dart'; +import 'utils/web_utils.dart'; /// Web implementation for Firestore [DocumentReferencePlatform]. class DocumentReferenceWeb extends DocumentReferencePlatform { @@ -51,7 +51,11 @@ class DocumentReferenceWeb extends DocumentReferencePlatform { () => _delegate.get(convertGetOptions(options)), ); - return convertWebDocumentSnapshot(firestore, documentSnapshot); + return convertWebDocumentSnapshot( + firestore, + documentSnapshot, + getServerTimestampBehaviorString(options.serverTimestampBehavior), + ); } @override @@ -71,7 +75,11 @@ class DocumentReferenceWeb extends DocumentReferencePlatform { return convertWebExceptions( () => querySnapshots.map((webSnapshot) { - return convertWebDocumentSnapshot(firestore, webSnapshot); + return convertWebDocumentSnapshot( + firestore, + webSnapshot, + ServerTimestampBehavior.none.name, + ); }), ); } diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart index 17ea8867c9aa..538ac33852b7 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart @@ -575,7 +575,8 @@ class DocumentSnapshot firestore_interop.DocumentSnapshotJsImpl jsObject) : super.fromJsObject(jsObject); - Map? data() => dartify(jsObject.data()); + Map? data([firestore_interop.SnapshotOptions? options]) => + dartify(jsObject.data(options)); dynamic get(/*String|FieldPath*/ dynamic fieldPath) => dartify(jsObject.get(fieldPath)); diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart index 8d5778553368..ba5c16a3b4e7 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart @@ -390,7 +390,7 @@ abstract class DocumentSnapshotJsImpl { external SnapshotMetadata get metadata; external DocumentReferenceJsImpl get ref; - external dynamic data(); + external dynamic data([SnapshotOptions? options]); external bool exists(); external dynamic get(/*String|FieldPath*/ dynamic fieldPath); } diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart index 709e00961819..3275243881c0 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/query_web.dart @@ -150,6 +150,9 @@ class QueryWeb extends QueryPlatform { return convertWebQuerySnapshot( firestore, await _buildWebQueryWithParameters().get(convertGetOptions(options)), + getServerTimestampBehaviorString( + options.serverTimestampBehavior, + ), ); }); } @@ -183,7 +186,11 @@ class QueryWeb extends QueryPlatform { return convertWebExceptions( () => querySnapshots.map((webQuerySnapshot) { - return convertWebQuerySnapshot(firestore, webQuerySnapshot); + return convertWebQuerySnapshot( + firestore, + webQuerySnapshot, + ServerTimestampBehavior.none.name, + ); }), ); } diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/transaction_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/transaction_web.dart index f7b0b184ec05..25fa9eeec481 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/transaction_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/transaction_web.dart @@ -34,7 +34,11 @@ class TransactionWeb extends TransactionPlatform { () async { final webDocumentSnapshot = await _webTransactionDelegate .get(_webFirestoreDelegate.doc(documentPath)); - return convertWebDocumentSnapshot(_firestore, webDocumentSnapshot); + return convertWebDocumentSnapshot( + _firestore, + webDocumentSnapshot, + ServerTimestampBehavior.none.name, + ); }, ); } diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/web_utils.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/web_utils.dart index 711d7c5e0d16..e2bb7c4302df 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/web_utils.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/web_utils.dart @@ -5,8 +5,10 @@ import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; -import '../utils/decode_utility.dart'; import '../interop/firestore.dart' as firestore_interop; +import '../interop/firestore_interop.dart' + hide GetOptions, SetOptions, FieldPath; +import '../utils/decode_utility.dart'; const _kChangeTypeAdded = 'added'; const _kChangeTypeModified = 'modified'; @@ -15,16 +17,23 @@ const _kChangeTypeRemoved = 'removed'; /// Converts a [web.QuerySnapshot] to a [QuerySnapshotPlatform]. QuerySnapshotPlatform convertWebQuerySnapshot( FirebaseFirestorePlatform firestore, - firestore_interop.QuerySnapshot webQuerySnapshot) { + firestore_interop.QuerySnapshot webQuerySnapshot, + String serverTimestampBehavior) { return QuerySnapshotPlatform( webQuerySnapshot.docs - .map((webDocumentSnapshot) => - convertWebDocumentSnapshot(firestore, webDocumentSnapshot!)) + .map((webDocumentSnapshot) => convertWebDocumentSnapshot( + firestore, + webDocumentSnapshot!, + serverTimestampBehavior, + )) .toList(), webQuerySnapshot .docChanges() - .map((webDocumentChange) => - convertWebDocumentChange(firestore, webDocumentChange)) + .map((webDocumentChange) => convertWebDocumentChange( + firestore, + webDocumentChange, + serverTimestampBehavior, + )) .toList(), convertWebSnapshotMetadata(webQuerySnapshot.metadata), ); @@ -32,13 +41,17 @@ QuerySnapshotPlatform convertWebQuerySnapshot( /// Converts a [web.DocumentSnapshot] to a [DocumentSnapshotPlatform]. DocumentSnapshotPlatform convertWebDocumentSnapshot( - FirebaseFirestorePlatform firestore, - firestore_interop.DocumentSnapshot webSnapshot) { + FirebaseFirestorePlatform firestore, + firestore_interop.DocumentSnapshot webSnapshot, + String serverTimestampBehavior, +) { return DocumentSnapshotPlatform( firestore, webSnapshot.ref!.path, { - 'data': DecodeUtility.decodeMapData(webSnapshot.data()), + 'data': DecodeUtility.decodeMapData(webSnapshot.data(SnapshotOptions( + serverTimestamps: serverTimestampBehavior, + ))), 'metadata': { 'hasPendingWrites': webSnapshot.metadata.hasPendingWrites, 'isFromCache': webSnapshot.metadata.fromCache, @@ -49,13 +62,19 @@ DocumentSnapshotPlatform convertWebDocumentSnapshot( /// Converts a [web.DocumentChange] to a [DocumentChangePlatform]. DocumentChangePlatform convertWebDocumentChange( - FirebaseFirestorePlatform firestore, - firestore_interop.DocumentChange webDocumentChange) { + FirebaseFirestorePlatform firestore, + firestore_interop.DocumentChange webDocumentChange, + String serverTimestampBehavior, +) { return DocumentChangePlatform( convertWebDocumentChangeType(webDocumentChange.type), webDocumentChange.oldIndex as int, webDocumentChange.newIndex as int, - convertWebDocumentSnapshot(firestore, webDocumentChange.doc!)); + convertWebDocumentSnapshot( + firestore, + webDocumentChange.doc!, + serverTimestampBehavior, + )); } /// Converts a [web.DocumentChange] type into a [DocumentChangeType]. diff --git a/packages/firebase_messaging/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m b/packages/firebase_messaging/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m index 180496e0f421..5cec45179618 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m @@ -622,7 +622,7 @@ - (void)messagingGetNotificationSettings:(id)arguments - (void)messagingGetToken:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { FIRMessaging *messaging = [FIRMessaging messaging]; - // Keep behaviour consistent with android platform, newly retrieved tokens are streamed via + // Keep behavior consistent with android platform, newly retrieved tokens are streamed via // onTokenRefresh bool refreshToken = messaging.FCMToken == nil ? YES : NO; [messaging tokenWithCompletion:^(NSString *_Nullable token, NSError *_Nullable error) {