Flutter 3.44 Upgrade: build/ vom Analyzer ausgeschlossen (SwiftPM-Fremdpakete)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cschlaefke 2026-07-18 23:52:41 +02:00
parent f6dfb20b00
commit cda8bc4cc2
516 changed files with 105496 additions and 22 deletions

View file

@ -9,6 +9,12 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
# Der Build-Ordner enthält u. a. von Swift Package Manager abgelegte
# Fremd-Pakete (Firebase-Beispielcode), die nicht zum Projekt gehören.
- build/**
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`

1451
ios/Podfile.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,11 +10,13 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1AE0D843A80013A3DE955CB5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5A01F7941722D9D24C51BCF1 /* GoogleService-Info.plist */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
39FCA66956D66B4392ABB993 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B91F40C029091C1C3E88A3C3 /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C31C58142A353AEDF574B9A2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 112C112F9117329C8A0C16CE /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -41,12 +43,19 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0227F9101B0BDB72B82DAA0D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
041675BB1A44D5A4222F7755 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
112C112F9117329C8A0C16CE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
27AD176722468C79F044AE02 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
40BDA7DC6A2E53536C20AD8F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
5A01F7941722D9D24C51BCF1 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
5E4798F23DA181F60EE20F02 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
71826FE475D88ECFCE77AAA1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@ -57,19 +66,43 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B91F40C029091C1C3E88A3C3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
5EA876757110B2C0547C0DF3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C31C58142A353AEDF574B9A2 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
39FCA66956D66B4392ABB993 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1A729D5DAFA178A0397641ED /* Pods */ = {
isa = PBXGroup;
children = (
40BDA7DC6A2E53536C20AD8F /* Pods-Runner.debug.xcconfig */,
5E4798F23DA181F60EE20F02 /* Pods-Runner.release.xcconfig */,
0227F9101B0BDB72B82DAA0D /* Pods-Runner.profile.xcconfig */,
71826FE475D88ECFCE77AAA1 /* Pods-RunnerTests.debug.xcconfig */,
041675BB1A44D5A4222F7755 /* Pods-RunnerTests.release.xcconfig */,
27AD176722468C79F044AE02 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@ -97,6 +130,8 @@
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
5A01F7941722D9D24C51BCF1 /* GoogleService-Info.plist */,
1A729D5DAFA178A0397641ED /* Pods */,
C8BD523483A2B436EB74E60A /* Frameworks */,
);
sourceTree = "<group>";
};
@ -124,6 +159,15 @@
path = Runner;
sourceTree = "<group>";
};
C8BD523483A2B436EB74E60A /* Frameworks */ = {
isa = PBXGroup;
children = (
B91F40C029091C1C3E88A3C3 /* Pods_Runner.framework */,
112C112F9117329C8A0C16CE /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -131,8 +175,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
454D78F76B4578611B98BBDC /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
5EA876757110B2C0547C0DF3 /* Frameworks */,
);
buildRules = (
);
@ -148,12 +194,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
D881861011D3B69CC2AB38BC /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
2926D2B54AA48C072380AFB2 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -226,6 +274,23 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
2926D2B54AA48C072380AFB2 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -242,6 +307,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
454D78F76B4578611B98BBDC /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -257,6 +344,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
D881861011D3B69CC2AB38BC /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -383,6 +492,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 71826FE475D88ECFCE77AAA1 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -400,6 +510,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 041675BB1A44D5A4222F7755 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@ -415,6 +526,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 27AD176722468C79F044AE02 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;

View file

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

1
ios/build/.last_build_id Normal file
View file

@ -0,0 +1 @@
4f1fe94d1e0ea767694440acc2a152ac

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
Copyright 2017, the Chromium project authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,27 @@
[<img src="https://raw.githubusercontent.com/firebase/flutterfire/main/.github/images/flutter_favorite.png" width="200" />](https://flutter.dev/docs/development/packages-and-plugins/favorites)
# Cloud Firestore Plugin for Flutter
A Flutter plugin to use the [Cloud Firestore API](https://firebase.google.com/docs/firestore/).
To learn more about Firebase Cloud Firestore, please visit the [Firebase website](https://firebase.google.com/products/firestore)
[![pub package](https://img.shields.io/pub/v/cloud_firestore.svg)](https://pub.dev/packages/cloud_firestore)
## Getting Started
To get started with Cloud Firestore for Flutter, please [see the documentation](https://firebase.google.com/docs/firestore/quickstart).
## Usage
To use this plugin, please visit the [Firestore Usage documentation](https://firebase.google.com/docs/firestore/manage-data/add-data)
## Issues and feedback
Please file FlutterFire specific issues, bugs, or feature requests in our [issue tracker](https://github.com/firebase/flutterfire/issues/new).
Plugin issues that are not specific to FlutterFire can be filed in the [Flutter issue tracker](https://github.com/flutter/flutter/issues/new).
To contribute a change to this plugin,
please review our [contribution guide](https://github.com/firebase/flutterfire/blob/main/CONTRIBUTING.md)
and open a [pull request](https://github.com/firebase/flutterfire/pulls).

View file

@ -0,0 +1,71 @@
group 'io.flutter.plugins.firebase.cloudfirestore'
version '1.0-SNAPSHOT'
apply plugin: 'com.android.library'
apply from: file("local-config.gradle")
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.3.0'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
def firebaseCoreProject = findProject(':firebase_core')
if (firebaseCoreProject == null) {
throw new GradleException('Could not find the firebase_core FlutterFire plugin, have you added it as a dependency in your pubspec?')
} else if (!firebaseCoreProject.properties['FirebaseSDKVersion']) {
throw new GradleException('A newer version of the firebase_core FlutterFire plugin is required, please update your firebase_core pubspec dependency.')
}
def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) {
if (!rootProject.ext.has('FlutterFire')) return firebaseCoreProject.properties[name]
if (!rootProject.ext.get('FlutterFire')[name]) return firebaseCoreProject.properties[name]
return rootProject.ext.get('FlutterFire').get(name)
}
android {
// Conditional for compatibility with AGP <4.2.
if (project.android.hasProperty("namespace")) {
namespace 'io.flutter.plugins.firebase.firestore'
}
compileSdkVersion project.ext.compileSdk
defaultConfig {
minSdkVersion project.ext.minSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility project.ext.javaVersion
targetCompatibility project.ext.javaVersion
}
buildFeatures {
buildConfig true
}
lintOptions {
disable 'InvalidPackage'
}
dependencies {
api firebaseCoreProject
implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}")
implementation 'com.google.firebase:firebase-firestore'
}
}
apply from: file("./user-agent.gradle")

View file

@ -0,0 +1,7 @@
ext {
compileSdk=34
minSdk=23
targetSdk=34
javaVersion = JavaVersion.toVersion(17)
androidGradlePluginVersion = '8.3.0'
}

View file

@ -0,0 +1,10 @@
rootProject.name = 'cloud_firestore'
apply from: file("local-config.gradle")
pluginManagement {
plugins {
id "com.android.application" version project.ext.androidGradlePluginVersion
id "com.android.library" version project.ext.androidGradlePluginVersion
}
}

View file

@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.flutter.plugins.firebase.firestore">
<application>
<service android:name="com.google.firebase.components.ComponentDiscoveryService">
<meta-data android:name="com.google.firebase.components:io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestoreRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>
</application>
</manifest>

View file

@ -0,0 +1,240 @@
// 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.
package io.flutter.plugins.firebase.firestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FlutterFirebaseFirestoreException extends Exception {
private static final String ERROR_ABORTED =
"The operation was aborted, typically due to a concurrency issue like transaction aborts,"
+ " etc.";
private static final String ERROR_ALREADY_EXISTS =
"Some document that we attempted to create already exists.";
private static final String ERROR_CANCELLED =
"The operation was cancelled (typically by the caller).";
private static final String ERROR_DATA_LOSS = "Unrecoverable data loss or corruption.";
private static final String ERROR_DEADLINE_EXCEEDED =
"Deadline expired before operation could complete. For operations that change the state of"
+ " the system, this error may be returned even if the operation has completed"
+ " successfully. For example, a successful response from a server could have been"
+ " delayed long enough for the deadline to expire.";
private static final String ERROR_FAILED_PRECONDITION =
"Operation was rejected because the system is not in a state required for the operation's"
+ " execution. If performing a query, ensure it has been indexed via the Firebase"
+ " console.";
private static final String ERROR_INTERNAL =
"Internal errors. Means some invariants expected by underlying system has been broken. If you"
+ " see one of these errors, something is very broken.";
private static final String ERROR_INVALID_ARGUMENT =
"Client specified an invalid argument. Note that this differs from failed-precondition."
+ " invalid-argument indicates arguments that are problematic regardless of the state of"
+ " the system (e.g., an invalid field name).";
private static final String ERROR_NOT_FOUND = "Some requested document was not found.";
private static final String ERROR_OUT_OF_RANGE = "Operation was attempted past the valid range.";
private static final String ERROR_PERMISSION_DENIED =
"The caller does not have permission to execute the specified operation.";
private static final String ERROR_RESOURCE_EXHAUSTED =
"Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file"
+ " system is out of space.";
private static final String ERROR_UNAUTHENTICATED =
"The request does not have valid authentication credentials for the operation.";
private static final String ERROR_UNAVAILABLE =
"The service is currently unavailable. This is a most likely a transient condition and may be"
+ " corrected by retrying with a backoff.";
private static final String ERROR_UNIMPLEMENTED =
"Operation is not implemented or not supported/enabled.";
private static final String ERROR_UNKNOWN =
"Operation is not implemented or not supported/enabled.";
private final String code;
private final String message;
public FlutterFirebaseFirestoreException(
FirebaseFirestoreException nativeException, Throwable cause) {
super(nativeException != null ? nativeException.getMessage() : "", cause);
String code = null;
String message = null;
if (cause != null && cause.getMessage() != null && cause.getMessage().contains(":")) {
String causeMessage = cause.getMessage();
Matcher matcher = Pattern.compile("([A-Z_]{3,25}):\\s(.*)").matcher(causeMessage);
if (matcher.find()) {
String foundCode = matcher.group(1).trim();
String foundMessage = matcher.group(2).trim();
switch (foundCode) {
case "ABORTED":
code = "aborted";
message = ERROR_ABORTED;
break;
case "ALREADY_EXISTS":
code = "already-exists";
message = ERROR_ALREADY_EXISTS;
break;
case "CANCELLED":
code = "cancelled";
message = ERROR_CANCELLED;
break;
case "DATA_LOSS":
code = "data-loss";
message = ERROR_DATA_LOSS;
break;
case "DEADLINE_EXCEEDED":
code = "deadline-exceeded";
message = ERROR_DEADLINE_EXCEEDED;
break;
case "FAILED_PRECONDITION":
code = "failed-precondition";
if (foundMessage.contains("index")) {
message = foundMessage;
} else {
message = ERROR_FAILED_PRECONDITION;
}
break;
case "INTERNAL":
code = "internal";
message = ERROR_INTERNAL;
break;
case "INVALID_ARGUMENT":
code = "invalid-argument";
message = ERROR_INVALID_ARGUMENT;
break;
case "NOT_FOUND":
code = "not-found";
message = ERROR_NOT_FOUND;
break;
case "OUT_OF_RANGE":
code = "out-of-range";
message = ERROR_OUT_OF_RANGE;
break;
case "PERMISSION_DENIED":
code = "permission-denied";
message = ERROR_PERMISSION_DENIED;
break;
case "RESOURCE_EXHAUSTED":
code = "resource-exhausted";
message = ERROR_RESOURCE_EXHAUSTED;
break;
case "UNAUTHENTICATED":
code = "unauthenticated";
message = ERROR_UNAUTHENTICATED;
break;
case "UNAVAILABLE":
code = "unavailable";
message = ERROR_UNAVAILABLE;
break;
case "UNIMPLEMENTED":
code = "unimplemented";
message = ERROR_UNIMPLEMENTED;
break;
case "UNKNOWN":
code = "unknown";
message = ERROR_UNKNOWN;
break;
}
}
}
if (code == null && nativeException != null) {
switch (nativeException.getCode()) {
case ABORTED:
code = "aborted";
message = ERROR_ABORTED;
break;
case ALREADY_EXISTS:
code = "already-exists";
message = ERROR_ALREADY_EXISTS;
break;
case CANCELLED:
code = "cancelled";
message = ERROR_CANCELLED;
break;
case DATA_LOSS:
code = "data-loss";
message = ERROR_DATA_LOSS;
break;
case DEADLINE_EXCEEDED:
code = "deadline-exceeded";
message = ERROR_DEADLINE_EXCEEDED;
break;
case FAILED_PRECONDITION:
code = "failed-precondition";
if (nativeException.getMessage() != null
&& nativeException.getMessage().contains("query requires an index")
|| nativeException.getMessage().contains("ensure it has been indexed")) {
message = nativeException.getMessage();
} else {
message = ERROR_FAILED_PRECONDITION;
}
break;
case INTERNAL:
code = "internal";
message = ERROR_INTERNAL;
break;
case INVALID_ARGUMENT:
code = "invalid-argument";
message = ERROR_INVALID_ARGUMENT;
break;
case NOT_FOUND:
code = "not-found";
message = ERROR_NOT_FOUND;
break;
case OUT_OF_RANGE:
code = "out-of-range";
message = ERROR_OUT_OF_RANGE;
break;
case PERMISSION_DENIED:
code = "permission-denied";
message = ERROR_PERMISSION_DENIED;
break;
case RESOURCE_EXHAUSTED:
code = "resource-exhausted";
message = ERROR_RESOURCE_EXHAUSTED;
break;
case UNAUTHENTICATED:
code = "unauthenticated";
message = ERROR_UNAUTHENTICATED;
break;
case UNAVAILABLE:
code = "unavailable";
message = ERROR_UNAVAILABLE;
break;
case UNIMPLEMENTED:
code = "unimplemented";
message = ERROR_UNIMPLEMENTED;
break;
case UNKNOWN:
code = "unknown";
message = "Unknown error or an error from a different error domain.";
break;
default:
// Even though UNKNOWN exists, this is a fallback
code = "unknown";
message = "An unknown error occurred";
}
}
if (nativeException != null
&& nativeException.getMessage() != null
&& !nativeException.getMessage().isEmpty()) {
message = nativeException.getMessage();
}
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}

View file

@ -0,0 +1,25 @@
// Copyright 2023 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.
package io.flutter.plugins.firebase.firestore;
import com.google.firebase.firestore.FirebaseFirestore;
public class FlutterFirebaseFirestoreExtension {
private final FirebaseFirestore instance;
private final String databaseURL;
public FlutterFirebaseFirestoreExtension(FirebaseFirestore instance, String databaseURL) {
this.instance = instance;
this.databaseURL = databaseURL;
}
public FirebaseFirestore getInstance() {
return instance;
}
public String getDatabaseURL() {
return databaseURL;
}
}

View file

@ -0,0 +1,558 @@
// 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.
package io.flutter.plugins.firebase.firestore;
import android.util.Log;
import com.google.firebase.FirebaseApp;
import com.google.firebase.Timestamp;
import com.google.firebase.firestore.Blob;
import com.google.firebase.firestore.DocumentChange;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FieldPath;
import com.google.firebase.firestore.FieldValue;
import com.google.firebase.firestore.Filter;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreSettings;
import com.google.firebase.firestore.GeoPoint;
import com.google.firebase.firestore.LoadBundleTaskProgress;
import com.google.firebase.firestore.MemoryCacheSettings;
import com.google.firebase.firestore.PersistentCacheSettings;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QuerySnapshot;
import com.google.firebase.firestore.SnapshotMetadata;
import com.google.firebase.firestore.VectorValue;
import io.flutter.plugin.common.StandardMessageCodec;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
class FlutterFirebaseFirestoreMessageCodec extends StandardMessageCodec {
public static final FlutterFirebaseFirestoreMessageCodec INSTANCE =
new FlutterFirebaseFirestoreMessageCodec();
private static final byte DATA_TYPE_DATE_TIME = (byte) 180;
private static final byte DATA_TYPE_GEO_POINT = (byte) 181;
private static final byte DATA_TYPE_DOCUMENT_REFERENCE = (byte) 182;
private static final byte DATA_TYPE_BLOB = (byte) 183;
private static final byte DATA_TYPE_ARRAY_UNION = (byte) 184;
private static final byte DATA_TYPE_ARRAY_REMOVE = (byte) 185;
private static final byte DATA_TYPE_DELETE = (byte) 186;
private static final byte DATA_TYPE_SERVER_TIMESTAMP = (byte) 187;
private static final byte DATA_TYPE_TIMESTAMP = (byte) 188;
private static final byte DATA_TYPE_INCREMENT_DOUBLE = (byte) 189;
private static final byte DATA_TYPE_INCREMENT_INTEGER = (byte) 190;
private static final byte DATA_TYPE_DOCUMENT_ID = (byte) 191;
private static final byte DATA_TYPE_FIELD_PATH = (byte) 192;
private static final byte DATA_TYPE_NAN = (byte) 193;
private static final byte DATA_TYPE_INFINITY = (byte) 194;
private static final byte DATA_TYPE_NEGATIVE_INFINITY = (byte) 195;
private static final byte DATA_TYPE_FIRESTORE_INSTANCE = (byte) 196;
private static final byte DATA_TYPE_FIRESTORE_QUERY = (byte) 197;
private static final byte DATA_TYPE_FIRESTORE_SETTINGS = (byte) 198;
private static final byte DATA_TYPE_VECTOR_VALUE = (byte) 199;
@Override
protected void writeValue(ByteArrayOutputStream stream, Object value) {
if (value instanceof Date) {
stream.write(DATA_TYPE_DATE_TIME);
writeLong(stream, ((Date) value).getTime());
} else if (value instanceof Timestamp) {
stream.write(DATA_TYPE_TIMESTAMP);
writeLong(stream, ((Timestamp) value).getSeconds());
writeInt(stream, ((Timestamp) value).getNanoseconds());
} else if (value instanceof GeoPoint) {
stream.write(DATA_TYPE_GEO_POINT);
writeAlignment(stream, 8);
writeDouble(stream, ((GeoPoint) value).getLatitude());
writeDouble(stream, ((GeoPoint) value).getLongitude());
} else if (value instanceof VectorValue) {
stream.write(DATA_TYPE_VECTOR_VALUE);
writeValue(stream, ((VectorValue) value).toArray());
} else if (value instanceof DocumentReference) {
stream.write(DATA_TYPE_DOCUMENT_REFERENCE);
FirebaseFirestore firestore = ((DocumentReference) value).getFirestore();
String appName = firestore.getApp().getName();
writeValue(stream, appName);
writeValue(stream, ((DocumentReference) value).getPath());
String databaseURL;
// There is no way of getting database URL from Firebase android SDK API so we cache it
// ourselves
synchronized (FlutterFirebaseFirestorePlugin.firestoreInstanceCache) {
databaseURL =
FlutterFirebaseFirestorePlugin.getCachedFirebaseFirestoreInstanceForKey(firestore)
.getDatabaseURL();
}
writeValue(stream, databaseURL);
} else if (value instanceof DocumentSnapshot) {
writeDocumentSnapshot(stream, (DocumentSnapshot) value);
} else if (value instanceof QuerySnapshot) {
writeQuerySnapshot(stream, (QuerySnapshot) value);
} else if (value instanceof DocumentChange) {
writeDocumentChange(stream, (DocumentChange) value);
} else if (value instanceof LoadBundleTaskProgress) {
writeLoadBundleTaskProgress(stream, (LoadBundleTaskProgress) value);
} else if (value instanceof SnapshotMetadata) {
writeSnapshotMetadata(stream, (SnapshotMetadata) value);
} else if (value instanceof Blob) {
stream.write(DATA_TYPE_BLOB);
writeBytes(stream, ((Blob) value).toBytes());
} else if (value instanceof Double) {
Double doubleValue = (Double) value;
if (Double.isNaN(doubleValue)) {
stream.write(DATA_TYPE_NAN);
} else if (doubleValue.equals(Double.NEGATIVE_INFINITY)) {
stream.write(DATA_TYPE_NEGATIVE_INFINITY);
} else if (doubleValue.equals(Double.POSITIVE_INFINITY)) {
stream.write(DATA_TYPE_INFINITY);
} else {
super.writeValue(stream, value);
}
} else {
super.writeValue(stream, value);
}
}
private void writeSnapshotMetadata(ByteArrayOutputStream stream, SnapshotMetadata value) {
Map<String, Boolean> metadataMap = new HashMap<>();
metadataMap.put("hasPendingWrites", value.hasPendingWrites());
metadataMap.put("isFromCache", value.isFromCache());
writeValue(stream, metadataMap);
}
private void writeDocumentChange(ByteArrayOutputStream stream, DocumentChange value) {
Map<String, Object> changeMap = new HashMap<>();
String type = null;
switch (value.getType()) {
case ADDED:
type = "DocumentChangeType.added";
break;
case MODIFIED:
type = "DocumentChangeType.modified";
break;
case REMOVED:
type = "DocumentChangeType.removed";
break;
}
changeMap.put("type", type);
changeMap.put("data", value.getDocument().getData());
changeMap.put("path", value.getDocument().getReference().getPath());
changeMap.put("oldIndex", value.getOldIndex());
changeMap.put("newIndex", value.getNewIndex());
changeMap.put("metadata", value.getDocument().getMetadata());
writeValue(stream, changeMap);
}
private void writeQuerySnapshot(ByteArrayOutputStream stream, QuerySnapshot value) {
List<String> paths = new ArrayList<>();
Map<String, Object> querySnapshotMap = new HashMap<>();
List<Map<String, Object>> documents = new ArrayList<>();
List<SnapshotMetadata> metadatas = new ArrayList<>();
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior =
FlutterFirebaseFirestorePlugin.serverTimestampBehaviorHashMap.get(value.hashCode());
for (DocumentSnapshot document : value.getDocuments()) {
paths.add(document.getReference().getPath());
if (serverTimestampBehavior != null) {
documents.add(document.getData(serverTimestampBehavior));
} else {
documents.add(document.getData());
}
metadatas.add(document.getMetadata());
}
querySnapshotMap.put("paths", paths);
querySnapshotMap.put("documents", documents);
querySnapshotMap.put("metadatas", metadatas);
querySnapshotMap.put("documentChanges", value.getDocumentChanges());
querySnapshotMap.put("metadata", value.getMetadata());
FlutterFirebaseFirestorePlugin.serverTimestampBehaviorHashMap.remove(value.hashCode());
writeValue(stream, querySnapshotMap);
}
private void writeLoadBundleTaskProgress(
ByteArrayOutputStream stream, LoadBundleTaskProgress snapshot) {
Map<String, Object> snapshotMap = new HashMap<>();
snapshotMap.put("bytesLoaded", snapshot.getBytesLoaded());
snapshotMap.put("documentsLoaded", snapshot.getDocumentsLoaded());
snapshotMap.put("totalBytes", snapshot.getTotalBytes());
snapshotMap.put("totalDocuments", snapshot.getTotalDocuments());
LoadBundleTaskProgress.TaskState taskState = snapshot.getTaskState();
String convertedState = "running";
switch (taskState) {
case RUNNING:
convertedState = "running";
break;
case SUCCESS:
convertedState = "success";
break;
case ERROR:
convertedState = "error";
break;
}
snapshotMap.put("taskState", convertedState);
writeValue(stream, snapshotMap);
}
@SuppressWarnings("ConstantConditions")
private void writeDocumentSnapshot(ByteArrayOutputStream stream, DocumentSnapshot value) {
Map<String, Object> snapshotMap = new HashMap<>();
snapshotMap.put("path", value.getReference().getPath());
if (value.exists()) {
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);
}
@Override
protected Object readValueOfType(byte type, ByteBuffer buffer) {
switch (type) {
case DATA_TYPE_DATE_TIME:
return new Date(buffer.getLong());
case DATA_TYPE_TIMESTAMP:
return new Timestamp(buffer.getLong(), buffer.getInt());
case DATA_TYPE_GEO_POINT:
readAlignment(buffer, 8);
return new GeoPoint(buffer.getDouble(), buffer.getDouble());
case DATA_TYPE_VECTOR_VALUE:
@SuppressWarnings("unchecked")
final ArrayList<Double> arrayList = (ArrayList<Double>) readValue(buffer);
double[] doubleArray = new double[arrayList.size()];
for (int i = 0; i < arrayList.size(); i++) {
doubleArray[i] = Objects.requireNonNull(arrayList.get(i), "Null value at index " + i);
}
return FieldValue.vector(doubleArray);
case DATA_TYPE_DOCUMENT_REFERENCE:
FirebaseFirestore firestore = (FirebaseFirestore) readValue(buffer);
final String path = (String) readValue(buffer);
return firestore.document(path);
case DATA_TYPE_BLOB:
final byte[] bytes = readBytes(buffer);
return Blob.fromBytes(bytes);
case DATA_TYPE_ARRAY_UNION:
return FieldValue.arrayUnion(toArray(readValue(buffer)));
case DATA_TYPE_ARRAY_REMOVE:
return FieldValue.arrayRemove(toArray(readValue(buffer)));
case DATA_TYPE_DELETE:
return FieldValue.delete();
case DATA_TYPE_SERVER_TIMESTAMP:
return FieldValue.serverTimestamp();
case DATA_TYPE_INCREMENT_INTEGER:
final Number integerIncrementValue = (Number) readValue(buffer);
return FieldValue.increment(integerIncrementValue.intValue());
case DATA_TYPE_INCREMENT_DOUBLE:
final Number doubleIncrementValue = (Number) readValue(buffer);
return FieldValue.increment(doubleIncrementValue.doubleValue());
case DATA_TYPE_DOCUMENT_ID:
return FieldPath.documentId();
case DATA_TYPE_FIRESTORE_INSTANCE:
return readFirestoreInstance(buffer);
case DATA_TYPE_FIRESTORE_QUERY:
return readFirestoreQuery(buffer);
case DATA_TYPE_FIRESTORE_SETTINGS:
return readFirestoreSettings(buffer);
case DATA_TYPE_NAN:
return Double.NaN;
case DATA_TYPE_INFINITY:
return Double.POSITIVE_INFINITY;
case DATA_TYPE_NEGATIVE_INFINITY:
return Double.NEGATIVE_INFINITY;
case DATA_TYPE_FIELD_PATH:
final int size = readSize(buffer);
final List<Object> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(readValue(buffer));
}
return FieldPath.of((String[]) list.toArray(new String[0]));
default:
return super.readValueOfType(type, buffer);
}
}
private FirebaseFirestore readFirestoreInstance(ByteBuffer buffer) {
String appName = (String) readValue(buffer);
String databaseURL = (String) readValue(buffer);
FirebaseFirestoreSettings settings = (FirebaseFirestoreSettings) readValue(buffer);
synchronized (FlutterFirebaseFirestorePlugin.firestoreInstanceCache) {
FirebaseFirestore cachedFirestoreInstance =
FlutterFirebaseFirestorePlugin.getFirestoreInstanceByNameAndDatabaseUrl(
appName, databaseURL);
if (cachedFirestoreInstance != null) {
return cachedFirestoreInstance;
}
FirebaseApp app = FirebaseApp.getInstance(appName);
FirebaseFirestore firestore = FirebaseFirestore.getInstance(app, databaseURL);
firestore.setFirestoreSettings(settings);
FlutterFirebaseFirestorePlugin.setCachedFirebaseFirestoreInstanceForKey(
firestore, databaseURL);
return firestore;
}
}
private FirebaseFirestoreSettings readFirestoreSettings(ByteBuffer buffer) {
@SuppressWarnings("unchecked")
Map<String, Object> settingsMap = (Map<String, Object>) readValue(buffer);
FirebaseFirestoreSettings.Builder settingsBuilder = new FirebaseFirestoreSettings.Builder();
if (settingsMap.get("persistenceEnabled") != null) {
Boolean persistenceEnabled = (Boolean) settingsMap.get("persistenceEnabled");
if (Boolean.TRUE.equals(persistenceEnabled)) {
PersistentCacheSettings.Builder persistenceSettings = PersistentCacheSettings.newBuilder();
if (settingsMap.get("cacheSizeBytes") != null) {
Long cacheSizeBytes = 104857600L;
Object value = settingsMap.get("cacheSizeBytes");
if (value instanceof Long) {
cacheSizeBytes = (Long) value;
} else if (value instanceof Integer) {
cacheSizeBytes = Long.valueOf((Integer) value);
}
if (cacheSizeBytes == -1) {
persistenceSettings.setSizeBytes(FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED);
} else {
persistenceSettings.setSizeBytes(cacheSizeBytes);
}
}
settingsBuilder.setLocalCacheSettings(persistenceSettings.build());
} else {
settingsBuilder.setLocalCacheSettings(MemoryCacheSettings.newBuilder().build());
}
}
if (settingsMap.get("host") != null) {
settingsBuilder.setHost((String) Objects.requireNonNull(settingsMap.get("host")));
// Only allow changing ssl if host is also specified.
if (settingsMap.get("sslEnabled") != null) {
settingsBuilder.setSslEnabled(
(Boolean) Objects.requireNonNull(settingsMap.get("sslEnabled")));
}
}
return settingsBuilder.build();
}
private Filter filterFromJson(Map<String, Object> map) {
if (map.containsKey("fieldPath")) {
// Deserialize a FilterQuery
String op = (String) map.get("op");
FieldPath fieldPath = (FieldPath) map.get("fieldPath");
Object value = map.get("value");
// All the operators from Firebase
switch (op) {
case "==":
return Filter.equalTo(fieldPath, value);
case "!=":
return Filter.notEqualTo(fieldPath, value);
case "<":
return Filter.lessThan(fieldPath, value);
case "<=":
return Filter.lessThanOrEqualTo(fieldPath, value);
case ">":
return Filter.greaterThan(fieldPath, value);
case ">=":
return Filter.greaterThanOrEqualTo(fieldPath, value);
case "array-contains":
return Filter.arrayContains(fieldPath, value);
case "array-contains-any":
return Filter.arrayContainsAny(fieldPath, (List<? extends Object>) value);
case "in":
return Filter.inArray(fieldPath, (List<? extends Object>) value);
case "not-in":
return Filter.notInArray(fieldPath, (List<? extends Object>) value);
default:
throw new Error("Invalid operator");
}
}
// Deserialize a FilterOperator
String op = (String) map.get("op");
@SuppressWarnings("unchecked")
List<Map<String, Object>> queries = (List<Map<String, Object>>) map.get("queries");
// Map queries recursively
ArrayList<Filter> parsedFilters = new ArrayList<>();
for (Map<String, Object> query : queries) {
parsedFilters.add(filterFromJson(query));
}
if (op.equals("OR")) {
return Filter.or(parsedFilters.toArray(new Filter[0]));
} else if (op.equals("AND")) {
return Filter.and(parsedFilters.toArray(new Filter[0]));
}
throw new Error("Invalid operator");
}
private Query readFirestoreQuery(ByteBuffer buffer) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> values = (Map<String, Object>) readValue(buffer);
FirebaseFirestore firestore =
(FirebaseFirestore) Objects.requireNonNull(values.get("firestore"));
String path = (String) Objects.requireNonNull(values.get("path"));
boolean isCollectionGroup = (boolean) values.get("isCollectionGroup");
@SuppressWarnings("unchecked")
Map<String, Object> parameters = (Map<String, Object>) values.get("parameters");
Query query;
if (isCollectionGroup) {
query = firestore.collectionGroup(path);
} else {
query = firestore.collection(path);
}
if (parameters == null) return query;
boolean isFilterQuery = parameters.containsKey("filters");
if (isFilterQuery) {
@SuppressWarnings("unchecked")
Filter filter =
filterFromJson((Map<String, Object>) Objects.requireNonNull(parameters.get("filters")));
query = query.where(filter);
}
// "where" filters
@SuppressWarnings("unchecked")
List<List<Object>> filters =
(List<List<Object>>) Objects.requireNonNull(parameters.get("where"));
for (List<Object> condition : filters) {
FieldPath fieldPath = (FieldPath) condition.get(0);
String operator = (String) condition.get(1);
Object value = condition.get(2);
if ("==".equals(operator)) {
query = query.whereEqualTo(fieldPath, value);
} else if ("!=".equals(operator)) {
query = query.whereNotEqualTo(fieldPath, value);
} else if ("<".equals(operator)) {
query = query.whereLessThan(fieldPath, value);
} else if ("<=".equals(operator)) {
query = query.whereLessThanOrEqualTo(fieldPath, value);
} else if (">".equals(operator)) {
query = query.whereGreaterThan(fieldPath, value);
} else if (">=".equals(operator)) {
query = query.whereGreaterThanOrEqualTo(fieldPath, value);
} else if ("array-contains".equals(operator)) {
query = query.whereArrayContains(fieldPath, value);
} else if ("array-contains-any".equals(operator)) {
@SuppressWarnings("unchecked")
List<Object> listValues = (List<Object>) value;
query = query.whereArrayContainsAny(fieldPath, listValues);
} else if ("in".equals(operator)) {
@SuppressWarnings("unchecked")
List<Object> listValues = (List<Object>) value;
query = query.whereIn(fieldPath, listValues);
} else if ("not-in".equals(operator)) {
@SuppressWarnings("unchecked")
List<Object> listValues = (List<Object>) value;
query = query.whereNotIn(fieldPath, listValues);
} else {
Log.w(
"FLTFirestoreMsgCodec",
"An invalid query operator " + operator + " was received but not handled.");
}
}
// "limit" filters
Number limit = (Number) parameters.get("limit");
if (limit != null) query = query.limit(limit.longValue());
Number limitToLast = (Number) parameters.get("limitToLast");
if (limitToLast != null) query = query.limitToLast(limitToLast.longValue());
// "orderBy" filters
@SuppressWarnings("unchecked")
List<List<Object>> orderBy = (List<List<Object>>) parameters.get("orderBy");
if (orderBy == null) return query;
for (List<Object> order : orderBy) {
FieldPath fieldPath = (FieldPath) order.get(0);
boolean descending = (boolean) order.get(1);
Query.Direction direction =
descending ? Query.Direction.DESCENDING : Query.Direction.ASCENDING;
query = query.orderBy(fieldPath, direction);
}
// cursor queries
@SuppressWarnings("unchecked")
List<Object> startAt = (List<Object>) parameters.get("startAt");
if (startAt != null) query = query.startAt(Objects.requireNonNull(startAt.toArray()));
@SuppressWarnings("unchecked")
List<Object> startAfter = (List<Object>) parameters.get("startAfter");
if (startAfter != null)
query = query.startAfter(Objects.requireNonNull(startAfter.toArray()));
@SuppressWarnings("unchecked")
List<Object> endAt = (List<Object>) parameters.get("endAt");
if (endAt != null) query = query.endAt(Objects.requireNonNull(endAt.toArray()));
@SuppressWarnings("unchecked")
List<Object> endBefore = (List<Object>) parameters.get("endBefore");
if (endBefore != null) query = query.endBefore(Objects.requireNonNull(endBefore.toArray()));
return query;
} catch (Exception exception) {
Log.e(
"FLTFirestoreMsgCodec",
"An error occurred while parsing query arguments, this is most likely an error with this"
+ " SDK.",
exception);
return null;
}
}
private Object[] toArray(Object source) {
if (source instanceof List) {
return ((List<?>) source).toArray();
}
if (source == null) {
return new ArrayList<>().toArray();
}
String sourceType = source.getClass().getCanonicalName();
String message = "java.util.List was expected, unable to convert '%s' to an object array";
throw new IllegalArgumentException(String.format(message, sourceType));
}
}

View file

@ -0,0 +1,21 @@
// Copyright 2019 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.
package io.flutter.plugins.firebase.firestore;
import androidx.annotation.Keep;
import com.google.firebase.components.Component;
import com.google.firebase.components.ComponentRegistrar;
import com.google.firebase.platforminfo.LibraryVersionComponent;
import java.util.Collections;
import java.util.List;
@Keep
public class FlutterFirebaseFirestoreRegistrar implements ComponentRegistrar {
@Override
public List<Component<?>> getComponents() {
return Collections.singletonList(
LibraryVersionComponent.create(BuildConfig.LIBRARY_NAME, BuildConfig.LIBRARY_VERSION));
}
}

View file

@ -0,0 +1,29 @@
// 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.
package io.flutter.plugins.firebase.firestore;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class FlutterFirebaseFirestoreTransactionResult {
public final @Nullable Exception exception;
private FlutterFirebaseFirestoreTransactionResult(@NonNull Exception failureException) {
exception = failureException;
}
private FlutterFirebaseFirestoreTransactionResult() {
exception = null;
}
public static FlutterFirebaseFirestoreTransactionResult failed(@NonNull Exception exception) {
return new FlutterFirebaseFirestoreTransactionResult(exception);
}
public static FlutterFirebaseFirestoreTransactionResult complete() {
return new FlutterFirebaseFirestoreTransactionResult();
}
}

View file

@ -0,0 +1,83 @@
/*
* 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.streamhandler;
import static io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin.DEFAULT_ERROR_CODE;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.ListenSource;
import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.MetadataChanges;
import com.google.firebase.firestore.SnapshotListenOptions;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
import io.flutter.plugins.firebase.firestore.utils.PigeonParser;
import java.util.Map;
public class DocumentSnapshotsStreamHandler implements StreamHandler {
ListenerRegistration listenerRegistration;
FirebaseFirestore firestore;
DocumentReference documentReference;
MetadataChanges metadataChanges;
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior;
ListenSource source;
public DocumentSnapshotsStreamHandler(
FirebaseFirestore firestore,
DocumentReference documentReference,
Boolean includeMetadataChanges,
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior,
ListenSource source) {
this.firestore = firestore;
this.documentReference = documentReference;
this.metadataChanges =
includeMetadataChanges ? MetadataChanges.INCLUDE : MetadataChanges.EXCLUDE;
this.serverTimestampBehavior = serverTimestampBehavior;
this.source = source;
}
@Override
public void onListen(Object arguments, EventSink events) {
SnapshotListenOptions.Builder optionsBuilder = new SnapshotListenOptions.Builder();
optionsBuilder.setMetadataChanges(metadataChanges);
optionsBuilder.setSource(source);
listenerRegistration =
documentReference.addSnapshotListener(
optionsBuilder.build(),
(documentSnapshot, exception) -> {
if (exception != null) {
Map<String, String> exceptionDetails = ExceptionConverter.createDetails(exception);
events.error(DEFAULT_ERROR_CODE, exception.getMessage(), exceptionDetails);
events.endOfStream();
onCancel(null);
} else {
// Emit the Pigeon object directly; the Pigeon-aware codec on the
// MessageChannel serializes it end-to-end. Pigeon 26 no longer flattens
// nested types via `.toList()`, so calling `.toList()` here would send a
// raw list that the Dart side can no longer decode.
events.success(
PigeonParser.toPigeonDocumentSnapshot(
documentSnapshot, serverTimestampBehavior));
}
});
}
@Override
public void onCancel(Object arguments) {
if (listenerRegistration != null) {
listenerRegistration.remove();
listenerRegistration = null;
}
}
}

View file

@ -0,0 +1,49 @@
/*
* 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.streamhandler;
import static io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin.DEFAULT_ERROR_CODE;
import androidx.annotation.NonNull;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.LoadBundleTask;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
import java.util.Map;
public class LoadBundleStreamHandler implements EventChannel.StreamHandler {
public LoadBundleStreamHandler(FirebaseFirestore firestore, @NonNull byte[] bundle) {
this.firestore = firestore;
this.bundle = bundle;
}
private EventChannel.EventSink eventSink;
private final FirebaseFirestore firestore;
private final @NonNull byte[] bundle;
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
eventSink = events;
LoadBundleTask task = firestore.loadBundle(bundle);
task.addOnProgressListener(events::success);
task.addOnFailureListener(
exception -> {
Map<String, String> exceptionDetails = ExceptionConverter.createDetails(exception);
events.error(DEFAULT_ERROR_CODE, exception.getMessage(), exceptionDetails);
onCancel(null);
});
}
@Override
public void onCancel(Object arguments) {
eventSink.endOfStream();
}
}

View file

@ -0,0 +1,17 @@
/*
* 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.streamhandler;
import io.flutter.plugins.firebase.firestore.GeneratedAndroidFirebaseFirestore;
import java.util.List;
/** callback when a transaction result has been computed. */
public interface OnTransactionResultListener {
void receiveTransactionResponse(
GeneratedAndroidFirebaseFirestore.InternalTransactionResult resultType,
List<GeneratedAndroidFirebaseFirestore.InternalTransactionCommand> commands);
}

View file

@ -0,0 +1,91 @@
/*
* 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.streamhandler;
import static io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin.DEFAULT_ERROR_CODE;
import android.os.Handler;
import android.os.Looper;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.ListenSource;
import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.MetadataChanges;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.SnapshotListenOptions;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
import io.flutter.plugins.firebase.firestore.utils.PigeonParser;
import java.util.Map;
import java.util.concurrent.Executor;
public class QuerySnapshotsStreamHandler implements StreamHandler {
ListenerRegistration listenerRegistration;
private final Handler mainHandler = new Handler(Looper.getMainLooper());
Query query;
MetadataChanges metadataChanges;
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior;
ListenSource source;
Executor snapshotExecutor;
public QuerySnapshotsStreamHandler(
Query query,
Boolean includeMetadataChanges,
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior,
ListenSource source,
Executor snapshotExecutor) {
this.query = query;
this.metadataChanges =
includeMetadataChanges ? MetadataChanges.INCLUDE : MetadataChanges.EXCLUDE;
this.serverTimestampBehavior = serverTimestampBehavior;
this.source = source;
this.snapshotExecutor = snapshotExecutor;
}
@Override
public void onListen(Object arguments, EventSink events) {
SnapshotListenOptions.Builder optionsBuilder = new SnapshotListenOptions.Builder();
optionsBuilder.setMetadataChanges(metadataChanges);
optionsBuilder.setSource(source);
optionsBuilder.setExecutor(snapshotExecutor);
listenerRegistration =
query.addSnapshotListener(
optionsBuilder.build(),
(querySnapshot, exception) -> {
if (exception != null) {
Map<String, String> exceptionDetails = ExceptionConverter.createDetails(exception);
mainHandler.post(
() -> {
events.error(DEFAULT_ERROR_CODE, exception.getMessage(), exceptionDetails);
events.endOfStream();
});
onCancel(null);
} else {
// Emit the Pigeon object directly; the Pigeon-aware codec serializes
// nested `InternalDocumentSnapshot` / `InternalDocumentChange` /
// `InternalSnapshotMetadata` with their proper type codes. Pigeon 26
// no longer flattens nested types via `.toList()`.
Object pigeonSnapshot =
PigeonParser.toPigeonQuerySnapshot(querySnapshot, serverTimestampBehavior);
mainHandler.post(() -> events.success(pigeonSnapshot));
}
});
}
@Override
public void onCancel(Object arguments) {
if (listenerRegistration != null) {
listenerRegistration.remove();
listenerRegistration = null;
}
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.streamhandler;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.ListenerRegistration;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
public class SnapshotsInSyncStreamHandler implements StreamHandler {
ListenerRegistration listenerRegistration;
FirebaseFirestore firestore;
public SnapshotsInSyncStreamHandler(FirebaseFirestore firestore) {
this.firestore = firestore;
}
@Override
public void onListen(Object arguments, EventSink events) {
Runnable snapshotsInSyncRunnable = () -> events.success(null);
listenerRegistration = firestore.addSnapshotsInSyncListener(snapshotsInSyncRunnable);
}
@Override
public void onCancel(Object arguments) {
if (listenerRegistration != null) {
listenerRegistration.remove();
listenerRegistration = null;
}
}
}

View file

@ -0,0 +1,198 @@
/*
* 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.streamhandler;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.FieldPath;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.FirebaseFirestoreException.Code;
import com.google.firebase.firestore.SetOptions;
import com.google.firebase.firestore.Transaction;
import com.google.firebase.firestore.TransactionOptions;
import io.flutter.plugin.common.EventChannel.EventSink;
import io.flutter.plugin.common.EventChannel.StreamHandler;
import io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestoreTransactionResult;
import io.flutter.plugins.firebase.firestore.GeneratedAndroidFirebaseFirestore;
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
import io.flutter.plugins.firebase.firestore.utils.PigeonParser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class TransactionStreamHandler implements OnTransactionResultListener, StreamHandler {
/** Callback when the transaction has been started. */
public interface OnTransactionStartedListener {
void onStarted(Transaction transaction);
}
final OnTransactionStartedListener onTransactionStartedListener;
final FirebaseFirestore firestore;
final String transactionId;
final Long timeout;
final Long maxAttempts;
public TransactionStreamHandler(
OnTransactionStartedListener onTransactionStartedListener,
FirebaseFirestore firestore,
String transactionId,
Long timeout,
Long maxAttempts) {
this.onTransactionStartedListener = onTransactionStartedListener;
this.firestore = firestore;
this.transactionId = transactionId;
this.timeout = timeout;
this.maxAttempts = maxAttempts;
}
final Semaphore semaphore = new Semaphore(0);
private GeneratedAndroidFirebaseFirestore.InternalTransactionResult resultType;
private List<GeneratedAndroidFirebaseFirestore.InternalTransactionCommand> commands;
final Handler mainLooper = new Handler(Looper.getMainLooper());
@Override
public void onListen(Object arguments, EventSink events) {
firestore
.runTransaction(
new TransactionOptions.Builder().setMaxAttempts(maxAttempts.intValue()).build(),
transaction -> {
onTransactionStartedListener.onStarted(transaction);
Map<String, Object> attemptMap = new HashMap<>();
attemptMap.put("appName", firestore.getApp().getName());
mainLooper.post(() -> events.success(attemptMap));
try {
if (!semaphore.tryAcquire(timeout, TimeUnit.MILLISECONDS)) {
return FlutterFirebaseFirestoreTransactionResult.failed(
new FirebaseFirestoreException("timed out", Code.DEADLINE_EXCEEDED));
}
} catch (InterruptedException e) {
return FlutterFirebaseFirestoreTransactionResult.failed(
new FirebaseFirestoreException("interrupted", Code.DEADLINE_EXCEEDED));
}
if (commands.isEmpty()) {
return FlutterFirebaseFirestoreTransactionResult.complete();
}
if (resultType
== GeneratedAndroidFirebaseFirestore.InternalTransactionResult.FAILURE) {
return FlutterFirebaseFirestoreTransactionResult.complete();
}
for (GeneratedAndroidFirebaseFirestore.InternalTransactionCommand command :
commands) {
DocumentReference documentReference = firestore.document(command.getPath());
switch (command.getType()) {
case DELETE_TYPE:
transaction.delete(documentReference);
break;
case UPDATE:
{
Map<Object, Object> rawData = Objects.requireNonNull(command.getData());
Map<FieldPath, Object> updateData = new HashMap<>();
for (Object key : rawData.keySet()) {
if (key instanceof String) {
updateData.put(FieldPath.of((String) key), rawData.get(key));
} else if (key instanceof FieldPath) {
updateData.put((FieldPath) key, rawData.get(key));
}
}
FieldPath firstFieldPath = updateData.keySet().iterator().next();
Object firstObject = updateData.get(firstFieldPath);
ArrayList<Object> flattenData = new ArrayList<>();
for (FieldPath fieldPath : updateData.keySet()) {
if (fieldPath.equals(firstFieldPath)) {
continue;
}
flattenData.add(fieldPath);
flattenData.add(updateData.get(fieldPath));
}
transaction.update(
documentReference, firstFieldPath, firstObject, flattenData.toArray());
break;
}
case SET:
{
GeneratedAndroidFirebaseFirestore.InternalDocumentOption options =
Objects.requireNonNull(command.getOption());
SetOptions setOptions = null;
if (options.getMerge() != null && options.getMerge()) {
setOptions = SetOptions.merge();
} else if (options.getMergeFields() != null) {
List<List<String>> fieldList =
Objects.requireNonNull(options.getMergeFields());
List<FieldPath> fieldPathList = PigeonParser.parseFieldPath(fieldList);
setOptions = SetOptions.mergeFieldPaths(fieldPathList);
}
@SuppressWarnings("unchecked")
Map<String, Object> data =
(Map<String, Object>)
(Map<?, ?>) Objects.requireNonNull(command.getData());
if (setOptions == null) {
transaction.set(documentReference, data);
} else {
transaction.set(documentReference, data, setOptions);
}
break;
}
}
}
return FlutterFirebaseFirestoreTransactionResult.complete();
})
.addOnCompleteListener(
task -> {
final HashMap<String, Object> map = new HashMap<>();
if (task.getException() != null || task.getResult().exception != null) {
final @Nullable Exception exception =
task.getException() != null ? task.getException() : task.getResult().exception;
map.put("appName", firestore.getApp().getName());
map.put("error", ExceptionConverter.createDetails(exception));
} else if (task.getResult() != null) {
map.put("complete", true);
}
mainLooper.post(
() -> {
events.success(map);
events.endOfStream();
});
});
}
@Override
public void onCancel(Object arguments) {
semaphore.release();
}
@Override
public void receiveTransactionResponse(
GeneratedAndroidFirebaseFirestore.InternalTransactionResult resultType,
List<GeneratedAndroidFirebaseFirestore.InternalTransactionCommand> commands) {
this.resultType = resultType;
this.commands = commands;
semaphore.release();
}
}

View file

@ -0,0 +1,76 @@
/*
* 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 static io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin.DEFAULT_ERROR_CODE;
import android.util.Log;
import com.google.firebase.firestore.FirebaseFirestoreException;
import io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestoreException;
import io.flutter.plugins.firebase.firestore.GeneratedAndroidFirebaseFirestore;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ExceptionConverter {
public static Map<String, String> createDetails(Exception exception) {
Map<String, String> details = new HashMap<>();
if (exception == null) {
return details;
}
FlutterFirebaseFirestoreException firestoreException = null;
if (exception instanceof FirebaseFirestoreException) {
firestoreException =
new FlutterFirebaseFirestoreException(
(FirebaseFirestoreException) exception, exception.getCause());
} else if (exception.getCause() != null
&& exception.getCause() instanceof FirebaseFirestoreException) {
firestoreException =
new FlutterFirebaseFirestoreException(
(FirebaseFirestoreException) exception.getCause(),
exception.getCause().getCause() != null
? exception.getCause().getCause()
: exception.getCause());
}
if (firestoreException != null) {
details.put("code", firestoreException.getCode());
details.put("message", firestoreException.getMessage());
}
if (details.containsKey("code")
&& Objects.requireNonNull(details.get("code")).equals("unknown")) {
Log.e("FLTFirebaseFirestore", "An unknown error occurred", exception);
}
return details;
}
public static void sendErrorToFlutter(
GeneratedAndroidFirebaseFirestore.Result result, Exception exception) {
Map<String, String> exceptionDetails = ExceptionConverter.createDetails(exception);
result.error(
new GeneratedAndroidFirebaseFirestore.FlutterError(
DEFAULT_ERROR_CODE,
exception != null ? exception.getMessage() : null,
exceptionDetails));
}
public static void sendErrorToFlutter(
GeneratedAndroidFirebaseFirestore.VoidResult result, Exception exception) {
Map<String, String> exceptionDetails = ExceptionConverter.createDetails(exception);
result.error(
new GeneratedAndroidFirebaseFirestore.FlutterError(
DEFAULT_ERROR_CODE,
exception != null ? exception.getMessage() : null,
exceptionDetails));
}
}

View file

@ -0,0 +1,237 @@
/*
* Copyright 2026, 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.NonNull;
import com.google.firebase.Timestamp;
import com.google.firebase.firestore.Blob;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.GeoPoint;
import com.google.firebase.firestore.VectorValue;
import com.google.firebase.firestore.pipeline.BooleanExpression;
import com.google.firebase.firestore.pipeline.Expression;
import java.util.List;
import java.util.Map;
/** Helper utilities for parsing expressions and handling common patterns. */
class ExpressionHelpers {
/**
* Parses an "and" expression from a list of expression maps. Uses Expression.and() with varargs
* signature.
*
* @param exprMaps List of expression maps to combine with AND
* @param parser Reference to ExpressionParsers for recursive parsing
*/
@SuppressWarnings("unchecked")
static BooleanExpression parseAndExpression(
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
if (exprMaps == null || exprMaps.isEmpty()) {
throw new IllegalArgumentException("'and' requires at least one expression");
}
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
if (exprMaps.size() == 1) {
return first;
}
BooleanExpression[] rest = new BooleanExpression[exprMaps.size() - 1];
for (int i = 1; i < exprMaps.size(); i++) {
rest[i - 1] = parser.parseBooleanExpression(exprMaps.get(i));
}
return Expression.and(first, rest);
}
/**
* Parses an "or" expression from a list of expression maps. Uses Expression.or() with varargs
* signature.
*
* @param exprMaps List of expression maps to combine with OR
* @param parser Reference to ExpressionParsers for recursive parsing
*/
@SuppressWarnings("unchecked")
static BooleanExpression parseOrExpression(
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
if (exprMaps == null || exprMaps.isEmpty()) {
throw new IllegalArgumentException("'or' requires at least one expression");
}
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
if (exprMaps.size() == 1) {
return first;
}
BooleanExpression[] rest = new BooleanExpression[exprMaps.size() - 1];
for (int i = 1; i < exprMaps.size(); i++) {
rest[i - 1] = parser.parseBooleanExpression(exprMaps.get(i));
}
return Expression.or(first, rest);
}
/**
* Parses a "xor" expression from a list of expression maps.
*
* @param exprMaps List of expression maps to combine with XOR
* @param parser Reference to ExpressionParsers for recursive parsing
*/
@SuppressWarnings("unchecked")
static BooleanExpression parseXorExpression(
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
if (exprMaps == null || exprMaps.isEmpty()) {
throw new IllegalArgumentException("'xor' requires at least one expression");
}
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
if (exprMaps.size() == 1) {
return first;
}
BooleanExpression[] rest = new BooleanExpression[exprMaps.size() - 1];
for (int i = 1; i < exprMaps.size(); i++) {
rest[i - 1] = parser.parseBooleanExpression(exprMaps.get(i));
}
return Expression.xor(first, rest);
}
/**
* Parses a "nor" expression from a list of expression maps. Uses Expression.nor() with varargs
* signature.
*/
@SuppressWarnings("unchecked")
static BooleanExpression parseNorExpression(
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
if (exprMaps == null || exprMaps.isEmpty()) {
throw new IllegalArgumentException("'nor' requires at least one expression");
}
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
if (exprMaps.size() == 1) {
return first;
}
BooleanExpression[] rest = new BooleanExpression[exprMaps.size() - 1];
for (int i = 1; i < exprMaps.size(); i++) {
rest[i - 1] = parser.parseBooleanExpression(exprMaps.get(i));
}
return Expression.nor(first, rest);
}
/**
* Parses a "coalesce" expression from a list of expression maps. Uses Expression.coalesce() with
* varargs.
*/
@SuppressWarnings("unchecked")
static Expression parseCoalesceExpression(
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
if (exprMaps == null || exprMaps.size() < 2) {
throw new IllegalArgumentException("'coalesce' requires at least two expressions");
}
Expression first = parser.parseExpression(exprMaps.get(0));
Expression second = parser.parseExpression(exprMaps.get(1));
if (exprMaps.size() == 2) {
return Expression.coalesce(first, second);
}
Object[] rest = new Object[exprMaps.size() - 2];
for (int i = 2; i < exprMaps.size(); i++) {
rest[i - 2] = parser.parseExpression(exprMaps.get(i));
}
return Expression.coalesce(first, second, rest);
}
/**
* Parses a "switch_on" expression: alternating BooleanExpression condition and Expression result,
* with an optional trailing default Expression when the list length is odd.
*/
@SuppressWarnings("unchecked")
static Expression parseSwitchOnExpression(
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
int n = exprMaps.size();
if (n < 2) {
throw new IllegalArgumentException("'switch_on' requires at least two expressions");
}
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
Expression second = parser.parseExpression(exprMaps.get(1));
if (n == 2) {
return Expression.switchOn(first, second);
}
Object[] tail = new Object[n - 2];
for (int i = 2; i < n; i++) {
if (n % 2 == 1 && i == n - 1) {
tail[i - 2] = parser.parseExpression(exprMaps.get(i));
} else if (i % 2 == 0) {
tail[i - 2] = parser.parseBooleanExpression(exprMaps.get(i));
} else {
tail[i - 2] = parser.parseExpression(exprMaps.get(i));
}
}
return Expression.switchOn(first, second, tail);
}
/**
* Parses a constant value based on its type to match Android SDK constant() overloads. Valid
* types: String, Number, Boolean, Date, Timestamp, GeoPoint, byte[], Blob, DocumentReference,
* VectorValue
*/
static Expression parseConstantValue(Object value) {
if (value == null) {
return Expression.nullValue();
}
if (value instanceof String) {
return Expression.constant((String) value);
} else if (value instanceof Number) {
return Expression.constant((Number) value);
} else if (value instanceof Boolean) {
return Expression.constant((Boolean) value);
} else if (value instanceof java.util.Date) {
return Expression.constant((java.util.Date) value);
} else if (value instanceof Timestamp) {
return Expression.constant((Timestamp) value);
} else if (value instanceof GeoPoint) {
return Expression.constant((GeoPoint) value);
} else if (value instanceof byte[]) {
return Expression.constant((byte[]) value);
} else if (value instanceof List) {
// Handle List<int> from Dart which comes as List<Integer> or List<Number>
// This represents byte[] (byte array) for constant expressions
@SuppressWarnings("unchecked")
List<?> list = (List<?>) value;
// Check if all elements are numbers (for byte array)
boolean isByteArray = true;
for (Object item : list) {
if (!(item instanceof Number)) {
isByteArray = false;
break;
}
}
if (isByteArray && !list.isEmpty()) {
byte[] byteArray = new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
byteArray[i] = ((Number) list.get(i)).byteValue();
}
return Expression.constant(byteArray);
}
// If not a byte array, fall through to error
} else if (value instanceof Blob) {
return Expression.constant((Blob) value);
} else if (value instanceof DocumentReference) {
return Expression.constant((DocumentReference) value);
} else if (value instanceof VectorValue) {
return Expression.constant((VectorValue) value);
}
throw new IllegalArgumentException(
"Constant value must be one of: String, Number, Boolean, Date, Timestamp, "
+ "GeoPoint, byte[], Blob, DocumentReference, or VectorValue. Got: "
+ value.getClass().getName());
}
}

View file

@ -0,0 +1,808 @@
/*
* Copyright 2026, 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 android.util.Log;
import androidx.annotation.NonNull;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.pipeline.AggregateFunction;
import com.google.firebase.firestore.pipeline.AggregateOptions;
import com.google.firebase.firestore.pipeline.AggregateStage;
import com.google.firebase.firestore.pipeline.AliasedAggregate;
import com.google.firebase.firestore.pipeline.BooleanExpression;
import com.google.firebase.firestore.pipeline.Expression;
import com.google.firebase.firestore.pipeline.FindNearestStage;
import com.google.firebase.firestore.pipeline.Selectable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Parses Dart pipeline expression maps into Android {@link Expression} / {@link BooleanExpression}
* types. {@link #parseBooleanExpression}'s default delegates to {@link #parseExpression} when the
* name is a value expression that yields a boolean (e.g. aliased comparisons).
*/
class ExpressionParsers {
private static final String TAG = "ExpressionParsers";
private final FirebaseFirestore firestore;
ExpressionParsers(@NonNull FirebaseFirestore firestore) {
this.firestore = firestore;
}
/** Binary operation on two expressions. Used instead of BiFunction for API 23 compatibility. */
private interface BinaryExpressionOp<R> {
R apply(Expression left, Expression right);
}
@SuppressWarnings("unchecked")
private static Map<String, Object> argsOf(@NonNull Map<String, Object> expressionMap) {
Map<String, Object> args = (Map<String, Object>) expressionMap.get("args");
return args != null ? args : new HashMap<>();
}
@SuppressWarnings("unchecked")
private Expression parseChild(@NonNull Map<String, Object> args, @NonNull String key) {
return parseExpression((Map<String, Object>) args.get(key));
}
/** Parses a list of nested expression maps (e.g. {@code values}) to {@link Expression}s. */
private List<Expression> parseExpressionMaps(@NonNull List<Map<String, Object>> maps) {
Expression[] out = new Expression[maps.size()];
for (int i = 0; i < maps.size(); i++) {
out[i] = parseExpression(maps.get(i));
}
return Arrays.asList(out);
}
private BooleanExpression parseBinaryComparisonNamed(
@NonNull String name, @NonNull Map<String, Object> args) {
switch (name) {
case "equal":
return parseBinaryComparison(args, (left, right) -> left.equal(right));
case "not_equal":
return parseBinaryComparison(args, (left, right) -> left.notEqual(right));
case "greater_than":
return parseBinaryComparison(args, (left, right) -> left.greaterThan(right));
case "greater_than_or_equal":
return parseBinaryComparison(args, (left, right) -> left.greaterThanOrEqual(right));
case "less_than":
return parseBinaryComparison(args, (left, right) -> left.lessThan(right));
case "less_than_or_equal":
return parseBinaryComparison(args, (left, right) -> left.lessThanOrEqual(right));
default:
throw new IllegalArgumentException("Not a binary comparison expression: " + name);
}
}
@SuppressWarnings("unchecked")
private BooleanExpression parseEqualAny(@NonNull Map<String, Object> args) {
Map<String, Object> valueMap = (Map<String, Object>) args.get("value");
List<Map<String, Object>> valuesMaps = (List<Map<String, Object>>) args.get("values");
Expression value = parseExpression(valueMap);
return value.equalAny(parseExpressionMaps(valuesMaps));
}
@SuppressWarnings("unchecked")
private BooleanExpression parseNotEqualAny(@NonNull Map<String, Object> args) {
Map<String, Object> valueMap = (Map<String, Object>) args.get("value");
List<Map<String, Object>> valuesMaps = (List<Map<String, Object>>) args.get("values");
Expression value = parseExpression(valueMap);
return value.notEqualAny(parseExpressionMaps(valuesMaps));
}
@SuppressWarnings("unchecked")
private BooleanExpression parseArrayContainsElement(@NonNull Map<String, Object> args) {
Map<String, Object> arrayMap = (Map<String, Object>) args.get("array");
Map<String, Object> elementMap = (Map<String, Object>) args.get("element");
Expression array = parseExpression(arrayMap);
Expression element = parseExpression(elementMap);
return array.arrayContains(element);
}
/** Parses an expression from a map representation. */
@SuppressWarnings("unchecked")
Expression parseExpression(@NonNull Map<String, Object> expressionMap) {
String name = (String) expressionMap.get("name");
if (name == null) {
if (expressionMap.containsKey("field_name")) {
String fieldName = (String) expressionMap.get("field_name");
return Expression.field(fieldName);
}
Map<String, Object> argsCheck = (Map<String, Object>) expressionMap.get("args");
if (argsCheck != null && argsCheck.containsKey("field")) {
String fieldName = (String) argsCheck.get("field");
return Expression.field(fieldName);
}
throw new IllegalArgumentException("Expression must have a 'name' field");
}
Map<String, Object> args = argsOf(expressionMap);
switch (name) {
case "null":
return Expression.nullValue();
case "field":
{
String fieldName = (String) args.get("field");
if (fieldName == null) {
throw new IllegalArgumentException("Field expression must have a 'field' argument");
}
return Expression.field(fieldName);
}
case "constant":
{
Object value = args.get("value");
if (value instanceof Map) {
Map<String, Object> valueMap = (Map<String, Object>) value;
String path = (String) valueMap.get("path");
return Expression.constant(firestore.document(path));
}
return ExpressionHelpers.parseConstantValue(value);
}
case "alias":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
String alias = (String) args.get("alias");
Expression expr = parseExpression(exprMap);
return expr.alias(alias);
}
case "equal":
case "not_equal":
case "greater_than":
case "greater_than_or_equal":
case "less_than":
case "less_than_or_equal":
return parseBinaryComparisonNamed(name, args);
case "add":
return parseBinaryOperation(args, (left, right) -> left.add(right));
case "subtract":
return parseBinaryOperation(args, (left, right) -> left.subtract(right));
case "multiply":
return parseBinaryOperation(args, (left, right) -> left.multiply(right));
case "divide":
return parseBinaryOperation(args, (left, right) -> left.divide(right));
case "modulo":
return parseBinaryOperation(args, (left, right) -> left.mod(right));
case "and":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseAndExpression(exprMaps, this);
}
case "or":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseOrExpression(exprMaps, this);
}
case "xor":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseXorExpression(exprMaps, this);
}
case "nor":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseNorExpression(exprMaps, this);
}
case "not":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
BooleanExpression expr = parseBooleanExpression(exprMap);
return Expression.not(expr);
}
case "concat":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
if (exprMaps == null || exprMaps.size() < 2) {
throw new IllegalArgumentException("concat requires at least two expressions");
}
Expression first = parseExpression(exprMaps.get(0));
Expression second = parseExpression(exprMaps.get(1));
if (exprMaps.size() == 2) {
return Expression.concat(first, second);
}
Object[] others = new Object[exprMaps.size() - 2];
for (int i = 2; i < exprMaps.size(); i++) {
others[i - 2] = parseExpression(exprMaps.get(i));
}
return Expression.concat(first, second, others);
}
case "length":
return Expression.length(parseChild(args, "expression"));
case "to_lower_case":
return Expression.toLower(parseChild(args, "expression"));
case "to_upper_case":
return Expression.toUpper(parseChild(args, "expression"));
case "trim":
return Expression.trim(parseChild(args, "expression"));
case "substring":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
Map<String, Object> startMap = (Map<String, Object>) args.get("start");
Map<String, Object> endMap = (Map<String, Object>) args.get("end");
Expression stringExpr = parseExpression(exprMap);
Expression startExpr = parseExpression(startMap);
Expression endExpr = parseExpression(endMap);
Expression lengthExpr = Expression.subtract(endExpr, startExpr);
return Expression.substring(stringExpr, startExpr, lengthExpr);
}
case "split":
{
Map<String, Object> valueMap = (Map<String, Object>) args.get("expression");
Map<String, Object> delimiterMap = (Map<String, Object>) args.get("delimiter");
return Expression.split(parseExpression(valueMap), parseExpression(delimiterMap));
}
case "join":
{
Map<String, Object> arrayMap = (Map<String, Object>) args.get("expression");
Map<String, Object> delimiterMap = (Map<String, Object>) args.get("delimiter");
return Expression.join(parseExpression(arrayMap), parseExpression(delimiterMap));
}
case "abs":
return Expression.abs(parseChild(args, "expression"));
case "negate":
{
Expression expr = parseChild(args, "expression");
return Expression.subtract(Expression.constant(0), expr);
}
case "array_concat":
{
Map<String, Object> firstMap = (Map<String, Object>) args.get("first");
Map<String, Object> secondMap = (Map<String, Object>) args.get("second");
return Expression.arrayConcat(parseExpression(firstMap), parseExpression(secondMap));
}
case "array_concat_multiple":
{
List<Map<String, Object>> arrays = (List<Map<String, Object>>) args.get("arrays");
if (arrays == null || arrays.size() < 2) {
throw new IllegalArgumentException(
"array_concat_multiple requires at least two arrays");
}
Expression result =
Expression.arrayConcat(
parseExpression(arrays.get(0)), parseExpression(arrays.get(1)));
for (int i = 2; i < arrays.size(); i++) {
result = result.arrayConcat(parseExpression(arrays.get(i)));
}
return result;
}
case "array_length":
return Expression.arrayLength(parseChild(args, "expression"));
case "array_reverse":
return Expression.arrayReverse(parseChild(args, "expression"));
case "array_sum":
return Expression.arraySum(parseChild(args, "expression"));
case "array_slice":
{
Expression array = parseChild(args, "expression");
Expression offset = parseChild(args, "offset");
Map<String, Object> lengthMap = (Map<String, Object>) args.get("length");
if (lengthMap == null) {
return array.arraySliceToEnd(offset);
}
return array.arraySlice(offset, parseExpression(lengthMap));
}
case "array_filter":
{
Expression array = parseChild(args, "expression");
String alias = (String) args.get("alias");
Map<String, Object> filterMap = (Map<String, Object>) args.get("filter");
if (alias == null || filterMap == null) {
throw new IllegalArgumentException("array_filter requires alias and filter");
}
return array.arrayFilter(alias, parseBooleanExpression(filterMap));
}
case "array_transform":
{
Expression array = parseChild(args, "expression");
String elementAlias = (String) args.get("element_alias");
Map<String, Object> transformMap = (Map<String, Object>) args.get("transform");
if (elementAlias == null || transformMap == null) {
throw new IllegalArgumentException(
"array_transform requires element_alias and transform");
}
return array.arrayTransform(elementAlias, parseExpression(transformMap));
}
case "array_transform_with_index":
{
Expression array = parseChild(args, "expression");
String elementAlias = (String) args.get("element_alias");
String indexAlias = (String) args.get("index_alias");
Map<String, Object> transformMap = (Map<String, Object>) args.get("transform");
if (elementAlias == null || indexAlias == null || transformMap == null) {
throw new IllegalArgumentException(
"array_transform_with_index requires element_alias, index_alias, and transform");
}
return array.arrayTransformWithIndex(
elementAlias, indexAlias, parseExpression(transformMap));
}
case "if_absent":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
Map<String, Object> elseMap = (Map<String, Object>) args.get("else");
return Expression.ifAbsent(parseExpression(exprMap), parseExpression(elseMap));
}
case "if_error":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
Map<String, Object> catchMap = (Map<String, Object>) args.get("catch");
return Expression.ifError(parseExpression(exprMap), parseExpression(catchMap));
}
case "conditional":
{
Map<String, Object> conditionMap = (Map<String, Object>) args.get("condition");
Map<String, Object> thenMap = (Map<String, Object>) args.get("then");
Map<String, Object> elseMap = (Map<String, Object>) args.get("else");
BooleanExpression condition = parseBooleanExpression(conditionMap);
Expression thenExpr = parseExpression(thenMap);
Expression elseExpr = parseExpression(elseMap);
return Expression.conditional(condition, thenExpr, elseExpr);
}
case "document_id":
return Expression.documentId(parseChild(args, "expression"));
case "document_id_from_ref":
{
String path = (String) args.get("doc_ref");
if (path == null) {
throw new IllegalArgumentException("document_id_from_ref requires 'doc_ref' argument");
}
return Expression.documentId(firestore.document(path));
}
case "collection_id":
return Expression.collectionId(parseChild(args, "expression"));
case "map_get":
{
Map<String, Object> mapMap = (Map<String, Object>) args.get("map");
Map<String, Object> keyMap = (Map<String, Object>) args.get("key");
return Expression.mapGet(parseExpression(mapMap), parseExpression(keyMap));
}
case "current_timestamp":
return Expression.currentTimestamp();
case "timestamp_add":
{
Map<String, Object> timestampMap = (Map<String, Object>) args.get("timestamp");
String unit = (String) args.get("unit");
Map<String, Object> amountMap = (Map<String, Object>) args.get("amount");
if (unit == null || amountMap == null) {
throw new IllegalArgumentException("timestamp_add requires 'unit' and 'amount'");
}
Expression timestampExpr = parseExpression(timestampMap);
Expression amountExpr = parseExpression(amountMap);
return Expression.timestampAdd(timestampExpr, Expression.constant(unit), amountExpr);
}
case "timestamp_subtract":
{
Map<String, Object> timestampMap = (Map<String, Object>) args.get("timestamp");
String unit = (String) args.get("unit");
Map<String, Object> amountMap = (Map<String, Object>) args.get("amount");
if (unit == null || amountMap == null) {
throw new IllegalArgumentException("timestamp_subtract requires 'unit' and 'amount'");
}
Expression timestampExpr = parseExpression(timestampMap);
Expression amountExpr = parseExpression(amountMap);
return Expression.timestampSubtract(timestampExpr, Expression.constant(unit), amountExpr);
}
case "timestamp_truncate":
{
Map<String, Object> timestampMap = (Map<String, Object>) args.get("timestamp");
String unit = (String) args.get("unit");
if (unit == null) {
throw new IllegalArgumentException("timestamp_truncate requires 'unit'");
}
return Expression.timestampTruncate(parseExpression(timestampMap), unit);
}
case "timestamp_diff":
{
Map<String, Object> endMap = (Map<String, Object>) args.get("end");
Map<String, Object> startMap = (Map<String, Object>) args.get("start");
Object unitObj = args.get("unit");
Expression endExpr = parseExpression(endMap);
Expression startExpr = parseExpression(startMap);
if (unitObj instanceof String) {
return Expression.timestampDiff(endExpr, startExpr, (String) unitObj);
}
@SuppressWarnings("unchecked")
Map<String, Object> unitMap = (Map<String, Object>) unitObj;
return Expression.timestampDiff(endExpr, startExpr, parseExpression(unitMap));
}
case "timestamp_extract":
{
Map<String, Object> timestampMap = (Map<String, Object>) args.get("timestamp");
Map<String, Object> partMap = (Map<String, Object>) args.get("part");
Expression tsExpr = parseExpression(timestampMap);
Expression partExpr = parseExpression(partMap);
if (!args.containsKey("timezone") || args.get("timezone") == null) {
return Expression.timestampExtract(tsExpr, partExpr);
}
Object tzObj = args.get("timezone");
if (tzObj instanceof String) {
return Expression.timestampExtractWithTimezone(tsExpr, partExpr, (String) tzObj);
}
@SuppressWarnings("unchecked")
Map<String, Object> tzMap = (Map<String, Object>) tzObj;
return Expression.timestampExtractWithTimezone(tsExpr, partExpr, parseExpression(tzMap));
}
case "parent":
{
if (args.containsKey("doc_ref")) {
String path = (String) args.get("doc_ref");
if (path == null) {
throw new IllegalArgumentException("parent requires 'doc_ref' argument");
}
return Expression.parent(firestore.document(path));
}
return Expression.parent(parseChild(args, "expression"));
}
case "if_null":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
Map<String, Object> replacementMap = (Map<String, Object>) args.get("replacement");
return Expression.ifNull(parseExpression(exprMap), parseExpression(replacementMap));
}
case "coalesce":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseCoalesceExpression(exprMaps, this);
}
case "switch_on":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseSwitchOnExpression(exprMaps, this);
}
case "map_keys":
return Expression.mapKeys(parseChild(args, "expression"));
case "map_values":
return Expression.mapValues(parseChild(args, "expression"));
case "array":
{
List<?> elements = (List<?>) args.get("elements");
if (elements == null) {
throw new IllegalArgumentException("array requires 'elements'");
}
Object[] parsed = new Object[elements.size()];
for (int i = 0; i < elements.size(); i++) {
Object el = elements.get(i);
if (el instanceof Map) {
parsed[i] = parseExpression((Map<String, Object>) el);
} else {
parsed[i] = ExpressionHelpers.parseConstantValue(el);
}
}
return Expression.array(Arrays.asList(parsed));
}
case "map":
{
Map<String, Object> data = (Map<String, Object>) args.get("data");
if (data == null) {
throw new IllegalArgumentException("map requires 'data'");
}
Map<String, Object> parsed = new HashMap<>();
for (Map.Entry<String, Object> e : data.entrySet()) {
Object v = e.getValue();
if (v instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> nested = (Map<String, Object>) v;
if (nested.containsKey("name") && nested.containsKey("args")) {
parsed.put(e.getKey(), parseExpression(nested));
} else {
parsed.put(e.getKey(), v);
}
} else {
parsed.put(e.getKey(), ExpressionHelpers.parseConstantValue(v));
}
}
return Expression.map(parsed);
}
case "bit_and":
return parseBinaryOperation(args, (left, right) -> left.bitAnd(right));
case "bit_or":
return parseBinaryOperation(args, (left, right) -> left.bitOr(right));
case "bit_xor":
return parseBinaryOperation(args, (left, right) -> left.bitXor(right));
case "bit_not":
return parseChild(args, "expression").bitNot();
case "bit_left_shift":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
Map<String, Object> amountMap = (Map<String, Object>) args.get("amount");
return parseExpression(exprMap).bitLeftShift(parseExpression(amountMap));
}
case "bit_right_shift":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
Map<String, Object> amountMap = (Map<String, Object>) args.get("amount");
return parseExpression(exprMap).bitRightShift(parseExpression(amountMap));
}
case "is_absent":
return parseIsAbsent(args);
case "is_error":
return parseIsError(args);
case "exists":
return parseExists(args);
case "as_boolean":
return parseAsBoolean(args);
case "array_contains_all":
return parseArrayContainsAll(args);
case "array_contains_any":
return parseArrayContainsAny(args);
case "document_matches":
return parseDocumentMatches(args);
default:
Log.w(TAG, "Unsupported expression type: " + name);
throw new UnsupportedOperationException("Expression type not yet implemented: " + name);
}
}
@SuppressWarnings("unchecked")
private BooleanExpression parseBinaryComparison(
@NonNull Map<String, Object> args, @NonNull BinaryExpressionOp<BooleanExpression> operation) {
Map<String, Object> leftMap = (Map<String, Object>) args.get("left");
Map<String, Object> rightMap = (Map<String, Object>) args.get("right");
Expression left = parseExpression(leftMap);
Expression right = parseExpression(rightMap);
return operation.apply(left, right);
}
@SuppressWarnings("unchecked")
private Expression parseBinaryOperation(
@NonNull Map<String, Object> args, @NonNull BinaryExpressionOp<Expression> operation) {
Map<String, Object> leftMap = (Map<String, Object>) args.get("left");
Map<String, Object> rightMap = (Map<String, Object>) args.get("right");
Expression left = parseExpression(leftMap);
Expression right = parseExpression(rightMap);
return operation.apply(left, right);
}
private BooleanExpression parseIsAbsent(@NonNull Map<String, Object> args) {
return parseChild(args, "expression").isAbsent();
}
private BooleanExpression parseIsError(@NonNull Map<String, Object> args) {
return parseChild(args, "expression").isError();
}
private BooleanExpression parseExists(@NonNull Map<String, Object> args) {
return parseChild(args, "expression").exists();
}
private BooleanExpression parseAsBoolean(@NonNull Map<String, Object> args) {
return parseChild(args, "expression").asBoolean();
}
@SuppressWarnings("unchecked")
private BooleanExpression parseArrayContainsAll(@NonNull Map<String, Object> args) {
Map<String, Object> arrayMap = (Map<String, Object>) args.get("array");
Expression array = parseExpression(arrayMap);
if (args.get("values") != null) {
List<Map<String, Object>> valuesMaps = (List<Map<String, Object>>) args.get("values");
return array.arrayContainsAll(parseExpressionMaps(valuesMaps));
}
Map<String, Object> arrayExprMap = (Map<String, Object>) args.get("array_expression");
Expression arrayExpr = parseExpression(arrayExprMap);
return array.arrayContainsAll(arrayExpr);
}
@SuppressWarnings("unchecked")
private BooleanExpression parseArrayContainsAny(@NonNull Map<String, Object> args) {
Map<String, Object> arrayMap = (Map<String, Object>) args.get("array");
List<Map<String, Object>> valuesMaps = (List<Map<String, Object>>) args.get("values");
Expression array = parseExpression(arrayMap);
return array.arrayContainsAny(parseExpressionMaps(valuesMaps));
}
@SuppressWarnings("unchecked")
BooleanExpression parseBooleanExpression(@NonNull Map<String, Object> expressionMap) {
String name = (String) expressionMap.get("name");
if (name == null) {
throw new IllegalArgumentException("BooleanExpression must have a 'name' field");
}
Map<String, Object> args = argsOf(expressionMap);
switch (name) {
case "equal":
case "not_equal":
case "greater_than":
case "greater_than_or_equal":
case "less_than":
case "less_than_or_equal":
return parseBinaryComparisonNamed(name, args);
case "and":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseAndExpression(exprMaps, this);
}
case "or":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseOrExpression(exprMaps, this);
}
case "xor":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseXorExpression(exprMaps, this);
}
case "nor":
{
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
return ExpressionHelpers.parseNorExpression(exprMaps, this);
}
case "not":
{
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
BooleanExpression expr = parseBooleanExpression(exprMap);
return expr.not();
}
case "is_absent":
return parseIsAbsent(args);
case "is_error":
return parseIsError(args);
case "exists":
return parseExists(args);
case "array_contains":
return parseArrayContainsElement(args);
case "array_contains_all":
return parseArrayContainsAll(args);
case "array_contains_any":
return parseArrayContainsAny(args);
case "equal_any":
return parseEqualAny(args);
case "not_equal_any":
return parseNotEqualAny(args);
case "as_boolean":
return parseAsBoolean(args);
case "document_matches":
return parseDocumentMatches(args);
default:
Expression expr = parseExpression(expressionMap);
if (expr instanceof BooleanExpression) {
return (BooleanExpression) expr;
}
Log.w(TAG, "Expression type '" + name + "' is not a BooleanExpression, attempting cast");
throw new IllegalArgumentException(
"Expression type '" + name + "' cannot be used as a BooleanExpression");
}
}
@SuppressWarnings("unchecked")
Selectable parseSelectable(@NonNull Map<String, Object> expressionMap) {
Expression expr = parseExpression(expressionMap);
if (!(expr instanceof Selectable)) {
throw new IllegalArgumentException(
"Expression must be a Selectable (Field or AliasedExpression). Got: "
+ expressionMap.get("name"));
}
return (Selectable) expr;
}
private BooleanExpression parseDocumentMatches(@NonNull Map<String, Object> args) {
String query = (String) args.get("query");
if (query == null) {
throw new IllegalArgumentException("document_matches requires a 'query' argument");
}
return Expression.documentMatches(query);
}
@SuppressWarnings("unchecked")
AggregateFunction parseAggregateFunction(@NonNull Map<String, Object> aggregateMap) {
String functionName = (String) aggregateMap.get("function");
if (functionName == null) {
functionName = (String) aggregateMap.get("name");
}
Map<String, Object> args = (Map<String, Object>) aggregateMap.get("args");
Expression expr = null;
if (args != null) {
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
expr = parseExpression(exprMap);
}
switch (functionName) {
case "sum":
return AggregateFunction.sum(expr);
case "average":
return AggregateFunction.average(expr);
case "count":
return AggregateFunction.count(expr);
case "count_distinct":
return AggregateFunction.countDistinct(expr);
case "minimum":
return AggregateFunction.minimum(expr);
case "maximum":
return AggregateFunction.maximum(expr);
case "count_all":
return AggregateFunction.countAll();
default:
throw new IllegalArgumentException("Unknown aggregate function: " + functionName);
}
}
@SuppressWarnings("unchecked")
AliasedAggregate parseAliasedAggregate(@NonNull Map<String, Object> aggregateMap) {
String name = (String) aggregateMap.get("name");
if ("alias".equals(name)) {
Map<String, Object> args = (Map<String, Object>) aggregateMap.get("args");
String alias = (String) args.get("alias");
Map<String, Object> aggregateFunctionMap =
(Map<String, Object>) args.get("aggregate_function");
AggregateFunction function = parseAggregateFunction(aggregateFunctionMap);
return function.alias(alias);
}
String alias = (String) aggregateMap.get("alias");
if (alias != null) {
AggregateFunction function = parseAggregateFunction(aggregateMap);
return function.alias(alias);
}
throw new IllegalArgumentException(
"Aggregate function must have an alias. Expected AliasedAggregateFunction format.");
}
@SuppressWarnings("unchecked")
AggregateStage parseAggregateStage(@NonNull Map<String, Object> stageMap) {
List<Map<String, Object>> accumulatorMaps =
(List<Map<String, Object>>) stageMap.get("accumulators");
if (accumulatorMaps == null || accumulatorMaps.isEmpty()) {
throw new IllegalArgumentException("AggregateStage must have at least one accumulator");
}
AliasedAggregate[] accumulators = new AliasedAggregate[accumulatorMaps.size()];
for (int i = 0; i < accumulatorMaps.size(); i++) {
accumulators[i] = parseAliasedAggregate(accumulatorMaps.get(i));
}
AggregateStage aggregateStage;
if (accumulators.length == 1) {
aggregateStage = AggregateStage.withAccumulators(accumulators[0]);
} else {
AliasedAggregate[] rest = new AliasedAggregate[accumulators.length - 1];
System.arraycopy(accumulators, 1, rest, 0, rest.length);
aggregateStage = AggregateStage.withAccumulators(accumulators[0], rest);
}
List<Map<String, Object>> groupMaps = (List<Map<String, Object>>) stageMap.get("groups");
if (groupMaps != null && !groupMaps.isEmpty()) {
Selectable firstGroup = parseSelectable(groupMaps.get(0));
if (groupMaps.size() == 1) {
aggregateStage = aggregateStage.withGroups(firstGroup);
} else {
Object[] additionalGroups = new Object[groupMaps.size() - 1];
for (int i = 1; i < groupMaps.size(); i++) {
Expression groupExpr = parseExpression(groupMaps.get(i));
additionalGroups[i - 1] = groupExpr;
}
aggregateStage = aggregateStage.withGroups(firstGroup, additionalGroups);
}
}
return aggregateStage;
}
AggregateOptions parseAggregateOptions(@NonNull Map<String, Object> optionsMap) {
return new AggregateOptions();
}
FindNearestStage.DistanceMeasure parseDistanceMeasure(@NonNull String dartEnumName) {
switch (dartEnumName) {
case "cosine":
return FindNearestStage.DistanceMeasure.COSINE;
case "euclidean":
return FindNearestStage.DistanceMeasure.EUCLIDEAN;
case "dotProduct":
return FindNearestStage.DistanceMeasure.DOT_PRODUCT;
default:
throw new IllegalArgumentException(
"Unknown distance measure: "
+ dartEnumName
+ ". Expected: cosine, euclidean, or dotProduct");
}
}
}

View file

@ -0,0 +1,336 @@
/*
* Copyright 2023, 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 android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.firestore.AggregateSource;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FieldPath;
import com.google.firebase.firestore.Filter;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.ListenSource;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.Source;
import io.flutter.plugins.firebase.firestore.GeneratedAndroidFirebaseFirestore;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class PigeonParser {
public static Source parsePigeonSource(GeneratedAndroidFirebaseFirestore.Source source) {
switch (source) {
case CACHE:
return Source.CACHE;
case SERVER_AND_CACHE:
return Source.DEFAULT;
case SERVER:
return Source.SERVER;
default:
throw new IllegalArgumentException("Unknown source: " + source);
}
}
public static DocumentSnapshot.ServerTimestampBehavior parsePigeonServerTimestampBehavior(
@Nullable GeneratedAndroidFirebaseFirestore.ServerTimestampBehavior serverTimestampBehavior) {
if (serverTimestampBehavior == null) {
return DocumentSnapshot.ServerTimestampBehavior.NONE;
}
switch (serverTimestampBehavior) {
case NONE:
return DocumentSnapshot.ServerTimestampBehavior.NONE;
case ESTIMATE:
return DocumentSnapshot.ServerTimestampBehavior.ESTIMATE;
case PREVIOUS:
return DocumentSnapshot.ServerTimestampBehavior.PREVIOUS;
default:
throw new IllegalArgumentException(
"Unknown server timestamp behavior: " + serverTimestampBehavior);
}
}
public static GeneratedAndroidFirebaseFirestore.InternalQuerySnapshot toPigeonQuerySnapshot(
com.google.firebase.firestore.QuerySnapshot querySnapshot,
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {
GeneratedAndroidFirebaseFirestore.InternalQuerySnapshot.Builder pigeonQuerySnapshot =
new GeneratedAndroidFirebaseFirestore.InternalQuerySnapshot.Builder();
pigeonQuerySnapshot.setMetadata(toPigeonSnapshotMetadata(querySnapshot.getMetadata()));
pigeonQuerySnapshot.setDocumentChanges(
toPigeonDocumentChanges(querySnapshot.getDocumentChanges(), serverTimestampBehavior));
pigeonQuerySnapshot.setDocuments(
toPigeonDocumentSnapshots(querySnapshot.getDocuments(), serverTimestampBehavior));
return pigeonQuerySnapshot.build();
}
public static GeneratedAndroidFirebaseFirestore.InternalSnapshotMetadata toPigeonSnapshotMetadata(
com.google.firebase.firestore.SnapshotMetadata snapshotMetadata) {
GeneratedAndroidFirebaseFirestore.InternalSnapshotMetadata.Builder pigeonSnapshotMetadata =
new GeneratedAndroidFirebaseFirestore.InternalSnapshotMetadata.Builder();
pigeonSnapshotMetadata.setHasPendingWrites(snapshotMetadata.hasPendingWrites());
pigeonSnapshotMetadata.setIsFromCache(snapshotMetadata.isFromCache());
return pigeonSnapshotMetadata.build();
}
public static List<GeneratedAndroidFirebaseFirestore.InternalDocumentChange>
toPigeonDocumentChanges(
List<com.google.firebase.firestore.DocumentChange> documentChanges,
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {
List<GeneratedAndroidFirebaseFirestore.InternalDocumentChange> pigeonDocumentChanges =
new ArrayList<>(documentChanges.size());
for (com.google.firebase.firestore.DocumentChange documentChange : documentChanges) {
pigeonDocumentChanges.add(toPigeonDocumentChange(documentChange, serverTimestampBehavior));
}
return pigeonDocumentChanges;
}
public static GeneratedAndroidFirebaseFirestore.InternalDocumentChange toPigeonDocumentChange(
com.google.firebase.firestore.DocumentChange documentChange,
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {
GeneratedAndroidFirebaseFirestore.InternalDocumentChange.Builder pigeonDocumentChange =
new GeneratedAndroidFirebaseFirestore.InternalDocumentChange.Builder();
pigeonDocumentChange.setType(toPigeonDocumentChangeType(documentChange.getType()));
pigeonDocumentChange.setOldIndex((long) documentChange.getOldIndex());
pigeonDocumentChange.setNewIndex((long) documentChange.getNewIndex());
pigeonDocumentChange.setDocument(
toPigeonDocumentSnapshot(documentChange.getDocument(), serverTimestampBehavior));
return pigeonDocumentChange.build();
}
public static GeneratedAndroidFirebaseFirestore.DocumentChangeType toPigeonDocumentChangeType(
com.google.firebase.firestore.DocumentChange.Type type) {
switch (type) {
case ADDED:
return GeneratedAndroidFirebaseFirestore.DocumentChangeType.ADDED;
case MODIFIED:
return GeneratedAndroidFirebaseFirestore.DocumentChangeType.MODIFIED;
case REMOVED:
return GeneratedAndroidFirebaseFirestore.DocumentChangeType.REMOVED;
default:
throw new IllegalArgumentException("Unknown change type: " + type);
}
}
public static ListenSource parseListenSource(
GeneratedAndroidFirebaseFirestore.ListenSource source) {
switch (source) {
case DEFAULT_SOURCE:
return ListenSource.DEFAULT;
case CACHE:
return ListenSource.CACHE;
default:
throw new IllegalArgumentException("Unknown ListenSource value: " + source);
}
}
public static GeneratedAndroidFirebaseFirestore.InternalDocumentSnapshot toPigeonDocumentSnapshot(
com.google.firebase.firestore.DocumentSnapshot documentSnapshot,
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {
GeneratedAndroidFirebaseFirestore.InternalDocumentSnapshot.Builder pigeonDocumentSnapshot =
new GeneratedAndroidFirebaseFirestore.InternalDocumentSnapshot.Builder();
pigeonDocumentSnapshot.setMetadata(toPigeonSnapshotMetadata(documentSnapshot.getMetadata()));
pigeonDocumentSnapshot.setData(documentSnapshot.getData(serverTimestampBehavior));
pigeonDocumentSnapshot.setPath(documentSnapshot.getReference().getPath());
return pigeonDocumentSnapshot.build();
}
public static List<GeneratedAndroidFirebaseFirestore.InternalDocumentSnapshot>
toPigeonDocumentSnapshots(
List<com.google.firebase.firestore.DocumentSnapshot> documentSnapshots,
DocumentSnapshot.ServerTimestampBehavior serverTimestampBehavior) {
List<GeneratedAndroidFirebaseFirestore.InternalDocumentSnapshot> pigeonDocumentSnapshots =
new ArrayList<>(documentSnapshots.size());
for (com.google.firebase.firestore.DocumentSnapshot documentSnapshot : documentSnapshots) {
pigeonDocumentSnapshots.add(
toPigeonDocumentSnapshot(documentSnapshot, serverTimestampBehavior));
}
return pigeonDocumentSnapshots;
}
public static List<FieldPath> parseFieldPath(List<List<String>> fieldPaths) {
List<FieldPath> paths = new ArrayList<>(fieldPaths.size());
for (List<String> fieldPath : fieldPaths) {
paths.add(FieldPath.of(fieldPath.toArray(new String[0])));
}
return paths;
}
public static Query parseQuery(
FirebaseFirestore firestore,
@NonNull String path,
boolean isCollectionGroup,
GeneratedAndroidFirebaseFirestore.InternalQueryParameters parameters) {
try {
Query query;
if (isCollectionGroup) {
query = firestore.collectionGroup(path);
} else {
query = firestore.collection(path);
}
if (parameters == null) return query;
boolean isFilterQuery = parameters.getFilters() != null;
if (isFilterQuery) {
Filter filter = filterFromJson(parameters.getFilters());
query = query.where(filter);
}
List<List<Object>> whereConditions = Objects.requireNonNull(parameters.getWhere());
for (List<Object> condition : whereConditions) {
FieldPath fieldPath = (FieldPath) condition.get(0);
String operator = (String) condition.get(1);
Object value = condition.get(2);
if ("==".equals(operator)) {
query = query.whereEqualTo(fieldPath, value);
} else if ("!=".equals(operator)) {
query = query.whereNotEqualTo(fieldPath, value);
} else if ("<".equals(operator)) {
query = query.whereLessThan(fieldPath, value);
} else if ("<=".equals(operator)) {
query = query.whereLessThanOrEqualTo(fieldPath, value);
} else if (">".equals(operator)) {
query = query.whereGreaterThan(fieldPath, value);
} else if (">=".equals(operator)) {
query = query.whereGreaterThanOrEqualTo(fieldPath, value);
} else if ("array-contains".equals(operator)) {
query = query.whereArrayContains(fieldPath, value);
} else if ("array-contains-any".equals(operator)) {
@SuppressWarnings("unchecked")
List<Object> listValues = (List<Object>) value;
query = query.whereArrayContainsAny(fieldPath, listValues);
} else if ("in".equals(operator)) {
@SuppressWarnings("unchecked")
List<Object> listValues = (List<Object>) value;
query = query.whereIn(fieldPath, listValues);
} else if ("not-in".equals(operator)) {
@SuppressWarnings("unchecked")
List<Object> listValues = (List<Object>) value;
query = query.whereNotIn(fieldPath, listValues);
} else {
Log.w(
"FLTFirestoreMsgCodec",
"An invalid query operator " + operator + " was received but not handled.");
}
}
// "limit" filters
Number limit = parameters.getLimit();
if (limit != null) query = query.limit(limit.longValue());
Number limitToLast = parameters.getLimitToLast();
if (limitToLast != null) query = query.limitToLast(limitToLast.longValue());
// "orderBy" filters
List<List<Object>> orderBy = parameters.getOrderBy();
if (orderBy == null) return query;
for (List<Object> order : orderBy) {
FieldPath fieldPath = (FieldPath) order.get(0);
boolean descending = (boolean) order.get(1);
Query.Direction direction =
descending ? Query.Direction.DESCENDING : Query.Direction.ASCENDING;
query = query.orderBy(fieldPath, direction);
}
// cursor queries
List<Object> startAt = parameters.getStartAt();
if (startAt != null) query = query.startAt(Objects.requireNonNull(startAt.toArray()));
List<Object> startAfter = parameters.getStartAfter();
if (startAfter != null)
query = query.startAfter(Objects.requireNonNull(startAfter.toArray()));
List<Object> endAt = parameters.getEndAt();
if (endAt != null) query = query.endAt(Objects.requireNonNull(endAt.toArray()));
List<Object> endBefore = parameters.getEndBefore();
if (endBefore != null) query = query.endBefore(Objects.requireNonNull(endBefore.toArray()));
return query;
} catch (Exception exception) {
Log.e(
"FLTFirestoreMsgCodec",
"An error occurred while parsing query arguments, this is most likely an error with this"
+ " SDK.",
exception);
return null;
}
}
private static Filter filterFromJson(Map<String, Object> map) {
if (map.containsKey("fieldPath")) {
// Deserialize a FilterQuery
String op = (String) map.get("op");
FieldPath fieldPath = (FieldPath) map.get("fieldPath");
Object value = map.get("value");
assert fieldPath != null;
assert op != null;
// All the operators from Firebase
switch (op) {
case "==":
return Filter.equalTo(fieldPath, value);
case "!=":
return Filter.notEqualTo(fieldPath, value);
case "<":
return Filter.lessThan(fieldPath, value);
case "<=":
return Filter.lessThanOrEqualTo(fieldPath, value);
case ">":
return Filter.greaterThan(fieldPath, value);
case ">=":
return Filter.greaterThanOrEqualTo(fieldPath, value);
case "array-contains":
return Filter.arrayContains(fieldPath, value);
case "array-contains-any":
return Filter.arrayContainsAny(fieldPath, (List<? extends Object>) value);
case "in":
return Filter.inArray(fieldPath, (List<? extends Object>) value);
case "not-in":
return Filter.notInArray(fieldPath, (List<? extends Object>) value);
default:
throw new Error("Invalid operator");
}
}
// Deserialize a FilterOperator
String op = (String) map.get("op");
@SuppressWarnings("unchecked")
List<Map<String, Object>> queries = (List<Map<String, Object>>) map.get("queries");
// Map queries recursively
ArrayList<Filter> parsedFilters = new ArrayList<>();
for (Map<String, Object> query : queries) {
parsedFilters.add(filterFromJson(query));
}
if (op.equals("OR")) {
return Filter.or(parsedFilters.toArray(new Filter[0]));
} else if (op.equals("AND")) {
return Filter.and(parsedFilters.toArray(new Filter[0]));
}
throw new Error("Invalid operator");
}
public static AggregateSource parseAggregateSource(
GeneratedAndroidFirebaseFirestore.AggregateSource source) {
switch (source) {
case SERVER:
return AggregateSource.SERVER;
default:
throw new IllegalArgumentException("Unknown AggregateSource value: " + source);
}
}
}

View file

@ -0,0 +1,140 @@
/*
* Copyright 2026, 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.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.Pipeline;
import com.google.firebase.firestore.Pipeline.Snapshot;
import com.google.firebase.firestore.PipelineSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class PipelineParser {
private static final String TAG = "PipelineParser";
/**
* Executes a pipeline from a list of stage maps.
*
* @param firestore The Firestore instance
* @param stages List of stage maps, each with 'stage' and 'args' fields
* @param options Optional execution options
* @return The pipeline snapshot result
*/
public static Snapshot executePipeline(
@NonNull FirebaseFirestore firestore,
@NonNull List<Map<String, Object>> stages,
@Nullable Map<String, Object> options)
throws Exception {
Pipeline pipeline = buildPipeline(firestore, stages);
Task<Snapshot> task;
if (options != null && !options.isEmpty()) {
Pipeline.ExecuteOptions executeOptions = parseExecuteOptions(options);
task = pipeline.execute(executeOptions);
} else {
task = pipeline.execute();
}
return Tasks.await(task);
}
private static Pipeline.ExecuteOptions parseExecuteOptions(@NonNull Map<String, Object> options) {
Pipeline.ExecuteOptions executeOptions = new Pipeline.ExecuteOptions();
Object indexModeObj = options.get("indexMode");
if (indexModeObj instanceof String) {
String indexModeStr = (String) indexModeObj;
if ("recommended".equalsIgnoreCase(indexModeStr)) {
executeOptions =
executeOptions.withIndexMode(Pipeline.ExecuteOptions.IndexMode.RECOMMENDED);
}
}
return executeOptions;
}
/**
* Builds a Pipeline from a list of stage maps without executing it. Used when a stage (e.g.
* union) requires another pipeline as an argument.
*/
@SuppressWarnings("unchecked")
public static Pipeline buildPipeline(
@NonNull FirebaseFirestore firestore, @NonNull List<Map<String, Object>> stages) {
if (stages.isEmpty()) {
throw new IllegalArgumentException("Pipeline must have at least one stage (source).");
}
ExpressionParsers expressionParsers = new ExpressionParsers(firestore);
PipelineStageHandlers stageHandlers = new PipelineStageHandlers(expressionParsers);
PipelineSource pipelineSource = firestore.pipeline();
Pipeline pipeline = null;
for (int i = 0; i < stages.size(); i++) {
Map<String, Object> stageMap = stages.get(i);
String stageName = (String) stageMap.get("stage");
if (stageName == null) {
throw new IllegalArgumentException("Stage must have a 'stage' field");
}
Map<String, Object> args = (Map<String, Object>) stageMap.get("args");
if (i == 0) {
pipeline = applySourceStage(pipelineSource, stageName, args, firestore);
} else {
pipeline = stageHandlers.applyStage(pipeline, stageName, args, firestore);
}
}
return pipeline;
}
/**
* Applies a source stage (collection, collection_group, documents, database) to PipelineSource.
* These are the only stages that can be the first stage and return a Pipeline instance.
*/
@SuppressWarnings("unchecked")
private static Pipeline applySourceStage(
@NonNull PipelineSource pipelineSource,
@NonNull String stageName,
@Nullable Map<String, Object> args,
@NonNull FirebaseFirestore firestore) {
if (args == null && !"database".equals(stageName)) {
throw new IllegalArgumentException("Stage args must not be null for stage: " + stageName);
}
switch (stageName) {
case "collection":
{
String path = (String) args.get("path");
return pipelineSource.collection(path);
}
case "collection_group":
{
String path = (String) args.get("path");
return pipelineSource.collectionGroup(path);
}
case "database":
{
return pipelineSource.database();
}
case "documents":
{
List<Map<String, Object>> docMaps = (List<Map<String, Object>>) args;
List<DocumentReference> docRefs = new ArrayList<>();
for (Map<String, Object> docMap : docMaps) {
String docPath = (String) docMap.get("path");
docRefs.add(firestore.document(docPath));
}
return pipelineSource.documents(docRefs.toArray(new DocumentReference[0]));
}
default:
throw new IllegalArgumentException(
"First stage must be one of: collection, collection_group, documents, database. Got: "
+ stageName);
}
}
}

View file

@ -0,0 +1,420 @@
/*
* Copyright 2026, 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.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.Pipeline;
import com.google.firebase.firestore.pipeline.AggregateOptions;
import com.google.firebase.firestore.pipeline.AggregateStage;
import com.google.firebase.firestore.pipeline.AliasedAggregate;
import com.google.firebase.firestore.pipeline.BooleanExpression;
import com.google.firebase.firestore.pipeline.Expression;
import com.google.firebase.firestore.pipeline.Field;
import com.google.firebase.firestore.pipeline.FindNearestOptions;
import com.google.firebase.firestore.pipeline.FindNearestStage;
import com.google.firebase.firestore.pipeline.Ordering;
import com.google.firebase.firestore.pipeline.SampleStage;
import com.google.firebase.firestore.pipeline.SearchStage;
import com.google.firebase.firestore.pipeline.Selectable;
import com.google.firebase.firestore.pipeline.UnnestOptions;
import java.util.List;
import java.util.Map;
/** Handles parsing and applying pipeline stages to Pipeline instances. */
class PipelineStageHandlers {
private final ExpressionParsers parsers;
PipelineStageHandlers(@NonNull ExpressionParsers parsers) {
this.parsers = parsers;
}
/** Applies a pipeline stage to a Pipeline instance. */
@SuppressWarnings("unchecked")
Pipeline applyStage(
@NonNull Pipeline pipeline,
@NonNull String stageName,
@Nullable Map<String, Object> args,
@NonNull FirebaseFirestore firestore) {
switch (stageName) {
case "where":
return handleWhere(pipeline, args);
case "limit":
return handleLimit(pipeline, args);
case "offset":
return handleOffset(pipeline, args);
case "sort":
return handleSort(pipeline, args);
case "select":
return handleSelect(pipeline, args);
case "add_fields":
return handleAddFields(pipeline, args);
case "remove_fields":
return handleRemoveFields(pipeline, args);
case "distinct":
return handleDistinct(pipeline, args);
case "aggregate":
return handleAggregate(pipeline, args);
case "aggregate_with_options":
return handleAggregateWithOptions(pipeline, args);
case "unnest":
return handleUnnest(pipeline, args);
case "replace_with":
return handleReplaceWith(pipeline, args);
case "union":
return handleUnion(pipeline, args, firestore);
case "sample":
return handleSample(pipeline, args);
case "find_nearest":
return handleFindNearest(pipeline, args);
case "search":
return handleSearch(pipeline, args);
default:
throw new IllegalArgumentException("Unknown pipeline stage: " + stageName);
}
}
private Pipeline handleWhere(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
Map<String, Object> expressionMap = (Map<String, Object>) args.get("expression");
BooleanExpression booleanExpression = parsers.parseBooleanExpression(expressionMap);
return pipeline.where(booleanExpression);
}
private Pipeline handleLimit(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
Number limit = (Number) args.get("limit");
return pipeline.limit(limit.intValue());
}
private Pipeline handleOffset(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
Number offset = (Number) args.get("offset");
return pipeline.offset(offset.intValue());
}
@SuppressWarnings("unchecked")
private Pipeline handleSort(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
List<Map<String, Object>> orderingMaps = (List<Map<String, Object>>) args.get("orderings");
if (orderingMaps == null || orderingMaps.isEmpty()) {
throw new IllegalArgumentException("'sort' requires at least one ordering");
}
Map<String, Object> firstMap = orderingMaps.get(0);
Expression expression =
parsers.parseExpression((Map<String, Object>) firstMap.get("expression"));
String direction = (String) firstMap.get("order_direction");
Ordering firstOrdering =
"asc".equals(direction) ? expression.ascending() : expression.descending();
if (orderingMaps.size() == 1) {
return pipeline.sort(firstOrdering);
}
Ordering[] additionalOrderings = new Ordering[orderingMaps.size() - 1];
for (int i = 1; i < orderingMaps.size(); i++) {
Map<String, Object> map = orderingMaps.get(i);
expression = parsers.parseExpression((Map<String, Object>) map.get("expression"));
direction = (String) map.get("order_direction");
additionalOrderings[i - 1] =
"asc".equals(direction) ? expression.ascending() : expression.descending();
}
return pipeline.sort(firstOrdering, additionalOrderings);
}
private Pipeline handleSelect(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
List<Map<String, Object>> expressionMaps = (List<Map<String, Object>>) args.get("expressions");
if (expressionMaps == null || expressionMaps.isEmpty()) {
throw new IllegalArgumentException("'select' requires at least one expression");
}
// Parse first expression as Selectable
Selectable firstSelection = parsers.parseSelectable(expressionMaps.get(0));
// Parse remaining expressions as varargs
if (expressionMaps.size() == 1) {
return pipeline.select(firstSelection);
}
Object[] additionalSelections = new Object[expressionMaps.size() - 1];
for (int i = 1; i < expressionMaps.size(); i++) {
Expression expr = parsers.parseExpression(expressionMaps.get(i));
// Additional selections can be Selectable or any Object
additionalSelections[i - 1] = expr;
}
return pipeline.select(firstSelection, additionalSelections);
}
private Pipeline handleAddFields(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
List<Map<String, Object>> expressionMaps = (List<Map<String, Object>>) args.get("expressions");
if (expressionMaps == null || expressionMaps.isEmpty()) {
throw new IllegalArgumentException("'add_fields' requires at least one expression");
}
// Parse first expression as Selectable
Selectable firstField = parsers.parseSelectable(expressionMaps.get(0));
// Parse remaining expressions as Selectable varargs
if (expressionMaps.size() == 1) {
return pipeline.addFields(firstField);
}
Selectable[] additionalFields = new Selectable[expressionMaps.size() - 1];
for (int i = 1; i < expressionMaps.size(); i++) {
additionalFields[i - 1] = parsers.parseSelectable(expressionMaps.get(i));
}
return pipeline.addFields(firstField, additionalFields);
}
private Pipeline handleRemoveFields(
@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
List<String> fieldPaths = (List<String>) args.get("field_paths");
if (fieldPaths == null || fieldPaths.isEmpty()) {
throw new IllegalArgumentException("'remove_fields' requires at least one field path");
}
// Convert first field path string to Field
Field firstField = Expression.field(fieldPaths.get(0));
// Convert remaining field paths to Field varargs
if (fieldPaths.size() == 1) {
return pipeline.removeFields(firstField);
}
Field[] additionalFields = new Field[fieldPaths.size() - 1];
for (int i = 1; i < fieldPaths.size(); i++) {
additionalFields[i - 1] = Expression.field(fieldPaths.get(i));
}
return pipeline.removeFields(firstField, additionalFields);
}
private Pipeline handleDistinct(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
List<Map<String, Object>> expressionMaps = (List<Map<String, Object>>) args.get("expressions");
if (expressionMaps == null || expressionMaps.isEmpty()) {
throw new IllegalArgumentException("'distinct' requires at least one expression");
}
// Parse first expression as Selectable
Selectable firstGroup = parsers.parseSelectable(expressionMaps.get(0));
// Parse remaining expressions as varargs (can be Selectable or Any)
if (expressionMaps.size() == 1) {
return pipeline.distinct(firstGroup);
}
Object[] additionalGroups = new Object[expressionMaps.size() - 1];
for (int i = 1; i < expressionMaps.size(); i++) {
Expression expr = parsers.parseExpression(expressionMaps.get(i));
// Additional groups can be Selectable or any Object
additionalGroups[i - 1] = expr;
}
return pipeline.distinct(firstGroup, additionalGroups);
}
@SuppressWarnings("unchecked")
private Pipeline handleAggregate(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
List<Map<String, Object>> aggregateMaps =
(List<Map<String, Object>>) args.get("aggregate_functions");
if (aggregateMaps == null || aggregateMaps.isEmpty()) {
throw new IllegalArgumentException("'aggregate' requires at least one aggregate function");
}
AliasedAggregate firstAccumulator = parsers.parseAliasedAggregate(aggregateMaps.get(0));
if (aggregateMaps.size() == 1) {
return pipeline.aggregate(firstAccumulator);
}
AliasedAggregate[] additionalAccumulators = new AliasedAggregate[aggregateMaps.size() - 1];
for (int i = 1; i < aggregateMaps.size(); i++) {
additionalAccumulators[i - 1] = parsers.parseAliasedAggregate(aggregateMaps.get(i));
}
return pipeline.aggregate(firstAccumulator, additionalAccumulators);
}
@SuppressWarnings("unchecked")
private Pipeline handleAggregateWithOptions(
@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
Map<String, Object> aggregateStageMap = (Map<String, Object>) args.get("aggregate_stage");
AggregateStage aggregateStage = parsers.parseAggregateStage(aggregateStageMap);
Map<String, Object> optionsMap = (Map<String, Object>) args.get("options");
if (optionsMap != null && !optionsMap.isEmpty()) {
AggregateOptions options = parsers.parseAggregateOptions(optionsMap);
return pipeline.aggregate(aggregateStage, options);
}
return pipeline.aggregate(aggregateStage);
}
private Pipeline handleUnnest(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
Map<String, Object> expressionMap = (Map<String, Object>) args.get("expression");
Selectable expression = parsers.parseSelectable(expressionMap);
String indexField = (String) args.get("index_field");
if (indexField != null) {
return pipeline.unnest(expression, new UnnestOptions().withIndexField(indexField));
} else {
return pipeline.unnest(expression);
}
}
private Pipeline handleReplaceWith(
@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
Map<String, Object> expressionMap = (Map<String, Object>) args.get("expression");
Expression expression = parsers.parseExpression(expressionMap);
return pipeline.replaceWith(expression);
}
@SuppressWarnings("unchecked")
private Pipeline handleUnion(
@NonNull Pipeline pipeline,
@Nullable Map<String, Object> args,
@NonNull FirebaseFirestore firestore) {
List<Map<String, Object>> nestedStages = (List<Map<String, Object>>) args.get("pipeline");
if (nestedStages == null || nestedStages.isEmpty()) {
throw new IllegalArgumentException("'union' requires a non-empty 'pipeline' argument");
}
Pipeline otherPipeline = PipelineParser.buildPipeline(firestore, nestedStages);
return pipeline.union(otherPipeline);
}
private Pipeline handleSample(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
// Sample stage parsing
Map<String, Object> sampleMap = (Map<String, Object>) args;
// Parse sample configuration
String type = (String) sampleMap.get("type");
if ("percentage".equals(type)) {
double value = ((Number) sampleMap.get("value")).doubleValue();
return pipeline.sample(SampleStage.withPercentage(value));
} else {
int value = ((Number) sampleMap.get("value")).intValue();
return pipeline.sample(SampleStage.withDocLimit(value));
}
}
@SuppressWarnings("unchecked")
private Pipeline handleFindNearest(
@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
String vectorField = (String) args.get("vector_field");
List<Number> vectorValue = (List<Number>) args.get("vector_value");
String distanceMeasureStr = (String) args.get("distance_measure");
Number limitObj = (Number) args.get("limit");
if (distanceMeasureStr == null) {
throw new IllegalArgumentException("'find_nearest' requires a 'distance_measure' argument");
}
// Convert Dart enum name to Android enum value
FindNearestStage.DistanceMeasure distanceMeasure =
parsers.parseDistanceMeasure(distanceMeasureStr);
// Convert vector value to double array
double[] vectorArray = new double[vectorValue.size()];
for (int i = 0; i < vectorValue.size(); i++) {
vectorArray[i] = vectorValue.get(i).doubleValue();
}
Field fieldExpr = Expression.field(vectorField);
if (limitObj != null) {
return pipeline.findNearest(
vectorField,
Expression.vector(vectorArray),
distanceMeasure,
new FindNearestOptions().withLimit(limitObj.intValue()));
} else {
return pipeline.findNearest(fieldExpr, vectorArray, distanceMeasure);
}
}
@SuppressWarnings("unchecked")
private Pipeline handleSearch(@NonNull Pipeline pipeline, @Nullable Map<String, Object> args) {
if (args == null) {
throw new IllegalArgumentException("'search' requires arguments");
}
String queryType = (String) args.get("query_type");
Object query = args.get("query");
SearchStage searchStage;
if ("string".equals(queryType)) {
searchStage = SearchStage.withQuery((String) query);
} else if ("expression".equals(queryType)) {
BooleanExpression expressionQuery =
parsers.parseBooleanExpression((Map<String, Object>) query);
searchStage = SearchStage.withQuery(expressionQuery);
} else {
throw new IllegalArgumentException(
"'search' requires query_type to be either 'string' or 'expression'");
}
List<Map<String, Object>> sortMaps = (List<Map<String, Object>>) args.get("sort");
if (sortMaps != null && !sortMaps.isEmpty()) {
Ordering firstOrdering = parseOrdering(sortMaps.get(0));
if (sortMaps.size() == 1) {
searchStage = searchStage.withSort(firstOrdering);
} else {
Ordering[] additionalOrderings = new Ordering[sortMaps.size() - 1];
for (int i = 1; i < sortMaps.size(); i++) {
additionalOrderings[i - 1] = parseOrdering(sortMaps.get(i));
}
searchStage = searchStage.withSort(firstOrdering, additionalOrderings);
}
}
List<Map<String, Object>> addFieldMaps = (List<Map<String, Object>>) args.get("add_fields");
if (addFieldMaps != null && !addFieldMaps.isEmpty()) {
Selectable firstField = parsers.parseSelectable(addFieldMaps.get(0));
if (addFieldMaps.size() == 1) {
searchStage = searchStage.withAddFields(firstField);
} else {
Selectable[] additionalFields = new Selectable[addFieldMaps.size() - 1];
for (int i = 1; i < addFieldMaps.size(); i++) {
additionalFields[i - 1] = parsers.parseSelectable(addFieldMaps.get(i));
}
searchStage = searchStage.withAddFields(firstField, additionalFields);
}
}
String languageCode = (String) args.get("language_code");
if (languageCode != null) {
searchStage = searchStage.withLanguageCode(languageCode);
}
Number limit = (Number) args.get("limit");
if (limit != null) {
searchStage = searchStage.withLimit(limit.longValue());
}
Number offset = (Number) args.get("offset");
if (offset != null) {
searchStage = searchStage.withOffset(offset.longValue());
}
Number retrievalDepth = (Number) args.get("retrieval_depth");
if (retrievalDepth != null) {
searchStage = searchStage.withRetrievalDepth(retrievalDepth.longValue());
}
return pipeline.search(searchStage);
}
@SuppressWarnings("unchecked")
private Ordering parseOrdering(@NonNull Map<String, Object> orderingMap) {
Expression expression =
parsers.parseExpression((Map<String, Object>) orderingMap.get("expression"));
String direction = (String) orderingMap.get("order_direction");
return "asc".equals(direction) ? expression.ascending() : expression.descending();
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,22 @@
import java.util.regex.Matcher
import java.util.regex.Pattern
String libraryVersionName = "UNKNOWN"
String libraryName = "flutter-fire-fst"
File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml')
if (pubspec.exists()) {
String yaml = pubspec.text
// Using \s*['|"]?([^\n|'|"]*)['|"]? to extract version number.
Matcher versionMatcher = Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", Pattern.MULTILINE).matcher(yaml)
if (versionMatcher.find()) libraryVersionName = versionMatcher.group(1).replaceAll("\\+", "-")
}
android {
defaultConfig {
// BuildConfig.VERSION_NAME
buildConfigField 'String', 'LIBRARY_VERSION', "\"${libraryVersionName}\""
// BuildConfig.LIBRARY_NAME
buildConfigField 'String', 'LIBRARY_NAME', "\"${libraryName}\""
}
}

View file

@ -0,0 +1,4 @@
name: Cloud Firestore Example
mode: flutter
files:
- name: lib/main.dart

View file

@ -0,0 +1,431 @@
// ignore_for_file: prefer_const_constructors_in_immutables,unnecessary_const,library_private_types_in_public_api,avoid_print
// Copyright 2021, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: defaultFirebaseOptions);
runApp(FirestoreExampleApp());
}
/// A reference to the list of movies.
/// We are using `withConverter` to ensure that interactions with the collection
/// are type-safe.
final moviesRef = FirebaseFirestore.instance
.collection('firestore-example-app')
.withConverter<Movie>(
fromFirestore: (snapshots, _) => Movie.fromJson(snapshots.data()!),
toFirestore: (movie, _) => movie.toJson(),
);
/// The different ways that we can filter/sort movies.
enum MovieQuery {
year,
likesAsc,
likesDesc,
score,
sciFi,
fantasy,
}
extension on Query<Movie> {
/// Create a firebase query from a [MovieQuery]
Query<Movie> queryBy(MovieQuery query) {
return switch (query) {
MovieQuery.fantasy => where('genre', arrayContainsAny: ['Fantasy']),
MovieQuery.sciFi => where('genre', arrayContainsAny: ['Sci-Fi']),
MovieQuery.likesAsc ||
MovieQuery.likesDesc =>
orderBy('likes', descending: query == MovieQuery.likesDesc),
MovieQuery.year => orderBy('year', descending: true),
MovieQuery.score => orderBy('score', descending: true)
};
}
}
/// The entry point of the application.
///
/// Returns a [MaterialApp].
class FirestoreExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firestore Example App',
theme: ThemeData.dark(),
home: const Scaffold(
body: Center(child: FilmList()),
),
);
}
}
/// Holds all example app films
class FilmList extends StatefulWidget {
const FilmList({Key? key}) : super(key: key);
@override
_FilmListState createState() => _FilmListState();
}
class _FilmListState extends State<FilmList> {
MovieQuery query = MovieQuery.year;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Firestore Example: Movies'),
// This is a example use for 'snapshots in sync'.
// The view reflects the time of the last Firestore sync; which happens any time a field is updated.
StreamBuilder(
stream: FirebaseFirestore.instance.snapshotsInSync(),
builder: (context, _) {
return Text(
'Latest Snapshot: ${DateTime.now()}',
style: Theme.of(context).textTheme.bodySmall,
);
},
),
],
),
actions: <Widget>[
PopupMenuButton<MovieQuery>(
onSelected: (value) => setState(() => query = value),
icon: const Icon(Icons.sort),
itemBuilder: (BuildContext context) {
return [
const PopupMenuItem(
value: MovieQuery.year,
child: Text('Sort by Year'),
),
const PopupMenuItem(
value: MovieQuery.score,
child: Text('Sort by Score'),
),
const PopupMenuItem(
value: MovieQuery.likesAsc,
child: Text('Sort by Likes ascending'),
),
const PopupMenuItem(
value: MovieQuery.likesDesc,
child: Text('Sort by Likes descending'),
),
const PopupMenuItem(
value: MovieQuery.fantasy,
child: Text('Filter genre Fantasy'),
),
const PopupMenuItem(
value: MovieQuery.sciFi,
child: Text('Filter genre Sci-Fi'),
),
];
},
),
PopupMenuButton<String>(
onSelected: (_) => _resetLikes(),
itemBuilder: (BuildContext context) {
return [
const PopupMenuItem(
value: 'reset_likes',
child: Text('Reset like counts (WriteBatch)'),
),
];
},
),
],
),
body: StreamBuilder<QuerySnapshot<Movie>>(
stream: moviesRef.queryBy(query).snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final data = snapshot.requireData;
return ListView.builder(
itemCount: data.size,
itemBuilder: (context, index) {
return _MovieItem(
data.docs[index].data(),
data.docs[index].reference,
);
},
);
},
),
);
}
Future<void> _resetLikes() async {
final movies = await moviesRef.get();
WriteBatch batch = FirebaseFirestore.instance.batch();
for (final movie in movies.docs) {
batch.update(movie.reference, {'likes': 0});
}
await batch.commit();
}
}
/// A single movie row.
class _MovieItem extends StatelessWidget {
_MovieItem(this.movie, this.reference);
final Movie movie;
final DocumentReference<Movie> reference;
/// Returns the movie poster.
Widget get poster {
return SizedBox(
width: 100,
child: Image.network(movie.poster),
);
}
/// Returns movie details.
Widget get details {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
metadata,
genres,
Likes(
reference: reference,
currentLikes: movie.likes,
),
],
),
);
}
/// Return the movie title.
Widget get title {
return Text(
'${movie.title} (${movie.year})',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
);
}
/// Returns metadata about the movie.
Widget get metadata {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: Text('Rated: ${movie.rated}'),
),
Text('Runtime: ${movie.runtime}'),
],
),
);
}
/// Returns a list of genre movie tags.
List<Widget> get genreItems {
return [
for (final genre in movie.genre)
Padding(
padding: const EdgeInsets.only(right: 2),
child: Chip(
backgroundColor: Colors.lightBlue,
label: Text(
genre,
style: const TextStyle(color: Colors.white),
),
),
),
];
}
/// Returns all genres.
Widget get genres {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
children: genreItems,
),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 4, top: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
poster,
Flexible(child: details),
],
),
);
}
}
/// Displays and manages the movie 'like' count.
class Likes extends StatefulWidget {
/// Constructs a new [Likes] instance with a given [DocumentReference] and
/// current like count.
Likes({
Key? key,
required this.reference,
required this.currentLikes,
}) : super(key: key);
/// The reference relating to the counter.
final DocumentReference<Movie> reference;
/// The number of current likes (before manipulation).
final int currentLikes;
@override
_LikesState createState() => _LikesState();
}
class _LikesState extends State<Likes> {
/// A local cache of the current likes, used to immediately render the updated
/// likes count after an update, even while the request isn't completed yet.
late int _likes = widget.currentLikes;
Future<void> _onLike() async {
final currentLikes = _likes;
// Increment the 'like' count straight away to show feedback to the user.
setState(() {
_likes = currentLikes + 1;
});
try {
// Update the likes using a transaction.
// We use a transaction because multiple users could update the likes count
// simultaneously. As such, our likes count may be different from the likes
// count on the server.
int newLikes = await FirebaseFirestore.instance
.runTransaction<int>((transaction) async {
DocumentSnapshot<Movie> movie =
await transaction.get<Movie>(widget.reference);
if (!movie.exists) {
throw Exception('Document does not exist!');
}
int updatedLikes = movie.data()!.likes + 1;
transaction.update(widget.reference, {'likes': updatedLikes});
return updatedLikes;
});
// Update with the real count once the transaction has completed.
setState(() => _likes = newLikes);
} catch (e, s) {
print(s);
print('Failed to update likes for document! $e');
// If the transaction fails, revert back to the old count
setState(() => _likes = currentLikes);
}
}
@override
void didUpdateWidget(Likes oldWidget) {
super.didUpdateWidget(oldWidget);
// The likes on the server changed, so we need to update our local cache to
// keep things in sync. Otherwise if another user updates the likes,
// we won't see the update.
if (widget.currentLikes != oldWidget.currentLikes) {
_likes = widget.currentLikes;
}
}
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
iconSize: 20,
onPressed: _onLike,
icon: const Icon(Icons.favorite),
),
Text('$_likes likes'),
],
);
}
}
@immutable
class Movie {
Movie({
required this.genre,
required this.likes,
required this.poster,
required this.rated,
required this.runtime,
required this.title,
required this.year,
});
Movie.fromJson(Map<String, Object?> json)
: this(
genre: (json['genre']! as List).cast<String>(),
likes: json['likes']! as int,
poster: json['poster']! as String,
rated: json['rated']! as String,
runtime: json['runtime']! as String,
title: json['title']! as String,
year: json['year']! as int,
);
final String poster;
final int likes;
final String title;
final int year;
final String runtime;
final String rated;
final List<String> genre;
Map<String, Object?> toJson() {
return {
'genre': genre,
'likes': likes,
'poster': poster,
'rated': rated,
'runtime': runtime,
'title': title,
'year': year,
};
}
}
const defaultFirebaseOptions = const FirebaseOptions(
apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw',
appId: '1:406099696497:web:87e25e51afe982cd3574d0',
messagingSenderId: '406099696497',
projectId: 'flutterfire-e2e-tests',
authDomain: 'flutterfire-e2e-tests.firebaseapp.com',
databaseURL:
'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app',
storageBucket: 'flutterfire-e2e-tests.appspot.com',
measurementId: 'G-JN95N1JV2E',
);

View file

@ -0,0 +1,8 @@
# firestore_example
Demonstrates how to use the firestore plugin.
## Getting Started
For help getting started with Flutter, view our online
[documentation](https://flutter.dev/).

View file

@ -0,0 +1,10 @@
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# in the LICENSE file.
include: ../../../../analysis_options.yaml
linter:
rules:
avoid_print: false
depend_on_referenced_packages: false
library_private_types_in_public_api: false

View file

@ -0,0 +1,65 @@
plugins {
id "com.android.application"
// START: FlutterFire Configuration
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
apply from: file("../../../android/local-config.gradle")
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
android {
namespace = "io.flutter.plugins.firebase.firestore.example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = project.ext.javaVersion
targetCompatibility = project.ext.javaVersion
}
kotlinOptions {
jvmTarget = "17"
}
defaultConfig {
applicationId = "io.flutter.plugins.firebase.firestore.example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,615 @@
{
"project_info": {
"project_number": "406099696497",
"firebase_url": "https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app",
"project_id": "flutterfire-e2e-tests",
"storage_bucket": "flutterfire-e2e-tests.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:d86a91cc7b338b233574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.analytics.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:a241c4b471513a203574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.appcheck.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-7bvmqp0fffe24vm2arng0dtdeh2tvkgl.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase.appcheck.example",
"certificate_hash": "909ca1482ef022bbae45a2db6b6d05d807a4c4aa"
}
},
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:21d5142deea38dda3574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.auth.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-emmujnd7g2ammh5uu9ni6v04p4ateqac.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase.auth.example",
"certificate_hash": "5ad0d6d5cbe577ca185b8df246656bebc3957128"
}
},
{
"client_id": "406099696497-in8bfp0nali85oul1o98huoar6eo1vv1.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase.auth.example",
"certificate_hash": "909ca1482ef022bbae45a2db6b6d05d807a4c4aa"
}
},
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:3ef965ff044efc0b3574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.database.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:40da41183cb3d3ff3574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.dynamiclinksexample"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:175ea7a64b2faf5e3574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.firestore.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:7ca3394493cc601a3574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.functions.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase.functions.example",
"certificate_hash": "a4256c0612686b336af6d138a5479b7dc1ee1af6"
}
},
{
"client_id": "406099696497-tvtvuiqogct1gs1s6lh114jeps7hpjm5.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase.functions.example",
"certificate_hash": "909ca1482ef022bbae45a2db6b6d05d807a4c4aa"
}
},
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:6d1c1fbf4688f39c3574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.installations.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:74ebb073d7727cd43574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.messaging.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:f54b85cfa36a39f73574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.remoteconfig.example"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:0d4ed619c031c0ac3574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase.tests"
}
},
"oauth_client": [
{
"client_id": "406099696497-ib9hj9281l3343cm3nfvvdotaojrthdc.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase.tests",
"certificate_hash": "5ad0d6d5cbe577ca185b8df246656bebc3957128"
}
},
{
"client_id": "406099696497-lc54d5l8sp90k39r0bb39ovsgo1s9bek.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase.tests",
"certificate_hash": "909ca1482ef022bbae45a2db6b6d05d807a4c4aa"
}
},
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:899c6485cfce26c13574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebase_ui_example"
}
},
"oauth_client": [
{
"client_id": "406099696497-ltgvphphcckosvqhituel5km2k3aecg8.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "io.flutter.plugins.firebase_ui_example",
"certificate_hash": "a4256c0612686b336af6d138a5479b7dc1ee1af6"
}
},
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:bc0b12b0605df8633574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebasecoreexample"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:0f3f7bfe78b8b7103574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebasecrashlyticsexample"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:406099696497:android:2751af6868a69f073574d0",
"android_client_info": {
"package_name": "io.flutter.plugins.firebasestorageexample"
}
},
"oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "406099696497-0mofiof3ofcgmpmirb6q0fllvb372sme.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "io.flutter.plugins.firebase.example"
}
}
]
}
}
}
],
"configuration_version": "1"
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,5 @@
package io.flutter.plugins.firebase.firestore.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View file

@ -0,0 +1,8 @@
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
androidGradlePluginVersion=8.3.0
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip

View file

@ -0,0 +1,28 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "${androidGradlePluginVersion}" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}
include ":app"

View file

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"flutterfire-e2e-tests","appId":"1:406099696497:android:175ea7a64b2faf5e3574d0","fileOutput":"android/app/google-services.json"}}}}}

View file

@ -0,0 +1,211 @@
// Copyright 2020, 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.
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void runCollectionReferenceTests() {
group('$CollectionReference', () {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<CollectionReference<Map<String, dynamic>>> initializeTest(
String id,
) async {
CollectionReference<Map<String, dynamic>> collection =
firestore.collection('flutter-tests/$id/query-tests');
QuerySnapshot<Map<String, dynamic>> snapshot = await collection.get();
await Future.forEach(snapshot.docs,
(DocumentSnapshot<Map<String, dynamic>> documentSnapshot) {
return documentSnapshot.reference.delete();
});
return collection;
}
test('add() adds a document', () async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('collection-reference-add');
var rand = Random();
var randNum = rand.nextInt(999999);
DocumentReference<Map<String, dynamic>> doc = await collection.add({
'value': randNum,
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(randNum, equals(snapshot.data()!['value']));
});
test(
'snapshots() can be reused',
() async {
final foo = await initializeTest('foo');
final snapshot = foo.snapshots();
final snapshot2 = foo.snapshots();
expect(
await snapshot.first,
isA<QuerySnapshot<Map<String, dynamic>>>()
.having((e) => e.docs, 'docs', []),
);
expect(
await snapshot2.first,
isA<QuerySnapshot<Map<String, dynamic>>>()
.having((e) => e.docs, 'docs', []),
);
await foo.add({'value': 42});
expect(
await snapshot.first,
isA<QuerySnapshot<Map<String, dynamic>>>()
.having((e) => e.docs, 'docs', [
isA<QueryDocumentSnapshot>()
.having((e) => e.data(), 'data', {'value': 42}),
]),
);
expect(
await snapshot2.first,
isA<QuerySnapshot<Map<String, dynamic>>>()
.having((e) => e.docs, 'docs', [
isA<QueryDocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', {'value': 42}),
]),
);
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
group(
'withConverter',
() {
test(
'add/snapshot',
() async {
final foo = await initializeTest('foo');
final fooConverter = foo.withConverter<int>(
fromFirestore: (snapshots, _) =>
snapshots.data()!['value']! as int,
toFirestore: (value, _) => {'value': value},
);
final fooSnapshot = foo.snapshots();
final fooConverterSnapshot = fooConverter.snapshots();
await expectLater(
fooSnapshot,
emits(
isA<QuerySnapshot<Map<String, dynamic>>>()
.having((e) => e.docs, 'docs', []),
),
);
await expectLater(
fooConverterSnapshot,
emits(
isA<QuerySnapshot<int>>().having((e) => e.docs, 'docs', []),
),
);
final newDocument = await fooConverter.add(42);
await expectLater(
newDocument.get(),
completion(
isA<DocumentSnapshot<int>>()
.having((e) => e.data(), 'data', 42),
),
);
await expectLater(
fooSnapshot,
emits(
isA<QuerySnapshot>().having((e) => e.docs, 'docs', [
isA<QueryDocumentSnapshot>()
.having((e) => e.data(), 'data', {'value': 42}),
]),
),
);
await expectLater(
fooConverterSnapshot,
emits(
isA<QuerySnapshot<int>>().having((e) => e.docs, 'docs', [
isA<QueryDocumentSnapshot<int>>()
.having((e) => e.data(), 'data', 42),
]),
),
);
await foo.add({'value': 21});
await expectLater(
fooSnapshot,
emits(
isA<QuerySnapshot>().having(
(e) => e.docs,
'docs',
unorderedEquals([
isA<QueryDocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', {'value': 42}),
isA<QueryDocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', {'value': 21}),
]),
),
),
);
await expectLater(
fooConverterSnapshot,
emits(
isA<QuerySnapshot<int>>().having(
(e) => e.docs,
'docs',
unorderedEquals([
isA<QueryDocumentSnapshot<int>>()
.having((e) => e.data(), 'data', 42),
isA<QueryDocumentSnapshot<int>>()
.having((e) => e.data(), 'data', 21),
]),
),
),
);
},
timeout: const Timeout.factor(3),
);
test(
'returning null from `fromFirestore` should not throw a null check error',
() async {
final foo = await initializeTest('foo');
await foo.add({'value': 42});
final fooConverter = foo.withConverter(
fromFirestore: (_, __) => null,
toFirestore: (_, __) => {}, // unused
);
final fooConverterSnapshot = fooConverter.snapshots();
await expectLater(
fooConverterSnapshot,
emits(
// ignore: prefer_void_to_null
isA<QuerySnapshot<Null>>().having((e) => e.docs, 'docs', [
// ignore: prefer_void_to_null
isA<QueryDocumentSnapshot<Null>>()
.having((e) => e.data(), 'data', null),
]),
),
);
},
);
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
});
}

View file

@ -0,0 +1,196 @@
// Copyright 2020, 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.
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void runDocumentChangeTests() {
group('$DocumentChange', () {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<CollectionReference<Map<String, dynamic>>> initializeTest(
String id,
) async {
CollectionReference<Map<String, dynamic>> collection =
firestore.collection('flutter-tests/$id/query-tests');
QuerySnapshot<Map<String, dynamic>> snapshot = await collection.get();
await Future.forEach(snapshot.docs,
(DocumentSnapshot<Map<String, dynamic>> documentSnapshot) {
return documentSnapshot.reference.delete();
});
return collection;
}
test(
'can add/update values to null in the document',
() async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('null-test');
DocumentReference<Map<String, dynamic>> doc1 = collection.doc('doc1');
await expectLater(
doc1.snapshots(),
emits(
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((q) => q.exists, 'exists', false),
),
);
await doc1.set(<String, Object?>{
'key': null,
'key2': 42,
});
await expectLater(
doc1.snapshots(),
emits(
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((q) => q.exists, 'exists', true)
.having((q) => q.data(), 'data()', <String, Object?>{
'key': null,
'key2': 42,
}),
),
);
await doc1.set({
'key': null,
'key2': null,
});
await expectLater(
doc1.snapshots(),
emits(
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((q) => q.exists, 'exists', true)
.having((q) => q.data(), 'data()', <String, Object?>{
'key': null,
'key2': null,
}),
),
);
},
timeout: const Timeout.factor(8),
skip: defaultTargetPlatform == TargetPlatform.windows,
);
test(
'returns the correct metadata when adding and removing',
() async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('add-remove-document');
DocumentReference<Map<String, dynamic>> doc1 = collection.doc('doc1');
// Set something in the database
await doc1.set({'name': 'doc1'});
final snapshots = <QuerySnapshot<Map<String, dynamic>>>[];
final receivedAll = Completer<void>();
StreamSubscription subscription =
collection.snapshots().listen((snapshot) {
snapshots.add(snapshot);
if (snapshots.length >= 2 && !receivedAll.isCompleted) {
receivedAll.complete();
}
});
// Wait for the initial snapshot before modifying
await Future.delayed(const Duration(milliseconds: 500));
await doc1.delete();
await receivedAll.future.timeout(const Duration(seconds: 30));
await subscription.cancel();
// Verify first snapshot (added)
expect(snapshots[0].docs.length, equals(1));
expect(snapshots[0].docChanges.length, equals(1));
DocumentChange<Map<String, dynamic>> addChange =
snapshots[0].docChanges[0];
expect(addChange.newIndex, equals(0));
expect(addChange.oldIndex, equals(-1));
expect(addChange.type, equals(DocumentChangeType.added));
expect(addChange.doc.data()!['name'], equals('doc1'));
// Verify second snapshot (removed)
expect(snapshots[1].docs.length, equals(0));
expect(snapshots[1].docChanges.length, equals(1));
DocumentChange<Map<String, dynamic>> removeChange =
snapshots[1].docChanges[0];
expect(removeChange.newIndex, equals(-1));
expect(removeChange.oldIndex, equals(0));
expect(removeChange.type, equals(DocumentChangeType.removed));
expect(removeChange.doc.data()!['name'], equals('doc1'));
},
skip: defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.android,
);
test(
'returns the correct metadata when modifying',
() async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('add-modify-document');
DocumentReference<Map<String, dynamic>> doc1 = collection.doc('doc1');
DocumentReference<Map<String, dynamic>> doc2 = collection.doc('doc2');
DocumentReference<Map<String, dynamic>> doc3 = collection.doc('doc3');
await doc1.set({'value': 1});
await doc2.set({'value': 2});
await doc3.set({'value': 3});
final snapshots = <QuerySnapshot<Map<String, dynamic>>>[];
final receivedAll = Completer<void>();
StreamSubscription subscription =
collection.orderBy('value').snapshots().listen((snapshot) {
snapshots.add(snapshot);
if (snapshots.length >= 2 && !receivedAll.isCompleted) {
receivedAll.complete();
}
});
// Wait for the initial snapshot before modifying
await Future.delayed(const Duration(milliseconds: 500));
await doc1.update({'value': 4});
await receivedAll.future.timeout(const Duration(seconds: 30));
await subscription.cancel();
// Verify first snapshot (all 3 docs added)
expect(snapshots[0].docs.length, equals(3));
expect(snapshots[0].docChanges.length, equals(3));
snapshots[0]
.docChanges
.asMap()
.forEach((int index, DocumentChange<Map<String, dynamic>> change) {
expect(change.oldIndex, equals(-1));
expect(change.newIndex, equals(index));
expect(change.type, equals(DocumentChangeType.added));
expect(change.doc.data()!['value'], equals(index + 1));
});
// Verify second snapshot (doc1 modified, moved to end)
expect(snapshots[1].docs.length, equals(3));
expect(snapshots[1].docChanges.length, equals(1));
DocumentChange<Map<String, dynamic>> change =
snapshots[1].docChanges[0];
expect(change.oldIndex, equals(0));
expect(change.newIndex, equals(2));
expect(change.type, equals(DocumentChangeType.modified));
expect(change.doc.id, equals('doc1'));
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
});
}

View file

@ -0,0 +1,728 @@
// Copyright 2020, 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.
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void runDocumentReferenceTests() {
group('$DocumentReference', () {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<DocumentReference<Map<String, dynamic>>> initializeTest(
String path,
) async {
String prefixedPath = 'flutter-tests/$path';
await firestore.doc(prefixedPath).delete();
return firestore.doc(prefixedPath);
}
group(
'DocumentReference.snapshots()',
() {
test('returns a [Stream]', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-snapshot');
Stream<DocumentSnapshot<Map<String, dynamic>>> stream =
document.snapshots();
expect(stream, isA<Stream<DocumentSnapshot<Map<String, dynamic>>>>());
});
test('can be reused', () async {
final foo = await initializeTest('foo');
final snapshot = foo.snapshots();
final snapshot2 = foo.snapshots();
expect(
await snapshot.first,
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.exists, 'exists', false),
);
expect(
await snapshot2.first,
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.exists, 'exists', false),
);
await foo.set({'value': 42});
expect(
await snapshot.first,
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', {'value': 42}),
);
expect(
await snapshot2.first,
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', {'value': 42}),
);
});
test('listens to a single response', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-snapshot');
Stream<DocumentSnapshot<Map<String, dynamic>>> stream =
document.snapshots();
StreamSubscription<DocumentSnapshot<Map<String, dynamic>>>?
subscription;
subscription = stream.listen(
expectAsync1(
(DocumentSnapshot<Map<String, dynamic>> snapshot) {
expect(snapshot.exists, isFalse);
},
reason: 'Stream should only have been called once.',
),
);
addTearDown(() async {
await subscription?.cancel();
});
});
test(
'listens to a single response from cache',
() async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-snapshot');
Stream<DocumentSnapshot<Map<String, dynamic>>> stream =
document.snapshots(source: ListenSource.cache);
StreamSubscription<DocumentSnapshot<Map<String, dynamic>>>?
subscription;
subscription = stream.listen(
expectAsync1(
(DocumentSnapshot<Map<String, dynamic>> snapshot) {
expect(snapshot.exists, isFalse);
},
reason: 'Stream should only have been called once.',
),
);
addTearDown(() async {
await subscription?.cancel();
});
},
// Listening from cache is not supported on Windows (see
// DocumentReference.snapshots in cloud_firestore).
skip: defaultTargetPlatform == TargetPlatform.windows,
);
test(
'listens to a document from cache',
() async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-snapshot-cache');
await document.set({'foo': 'bar'});
Stream<DocumentSnapshot<Map<String, dynamic>>> stream =
document.snapshots(source: ListenSource.cache);
StreamSubscription<DocumentSnapshot<Map<String, dynamic>>>?
subscription;
subscription = stream.listen(
expectAsync1(
(DocumentSnapshot<Map<String, dynamic>> snapshot) {
expect(snapshot.exists, isTrue);
expect(snapshot.data(), equals({'foo': 'bar'}));
},
reason: 'Stream should only have been called once.',
),
);
addTearDown(() async {
await subscription?.cancel();
});
},
// Listening from cache is not supported on Windows.
skip: defaultTargetPlatform == TargetPlatform.windows,
);
test('listens to multiple documents', () async {
DocumentReference<Map<String, dynamic>> doc1 =
await initializeTest('document-snapshot-1');
DocumentReference<Map<String, dynamic>> doc2 =
await initializeTest('document-snapshot-2');
await doc1.set({'test': 'value1'});
await doc2.set({'test': 'value2'});
final value1 = doc1.snapshots().first.then((s) => s.data()!['test']);
final value2 = doc2.snapshots().first.then((s) => s.data()!['test']);
await expectLater(value1, completion('value1'));
await expectLater(value2, completion('value2'));
});
test('listens to a multiple changes response', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-snapshot-multiple');
Stream<DocumentSnapshot<Map<String, dynamic>>> stream =
document.snapshots();
int call = 0;
StreamSubscription subscription = stream.listen(
expectAsync1(
(DocumentSnapshot<Map<String, dynamic>> snapshot) {
call++;
if (call == 1) {
expect(snapshot.exists, isFalse);
} else if (call == 2) {
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['bar'], equals('baz'));
} else if (call == 3) {
expect(snapshot.exists, isFalse);
} else if (call == 4) {
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['foo'], equals('bar'));
} else if (call == 5) {
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['foo'], equals('baz'));
} else {
fail('Should not have been called');
}
},
count: 5,
reason: 'Stream should only have been called five times.',
),
);
await Future.delayed(
const Duration(seconds: 1),
); // allow stream to return a noop-doc
await document.set({'bar': 'baz'});
await document.delete();
await document.set({'foo': 'bar'});
await document.update({'foo': 'baz'});
await subscription.cancel();
await Future.delayed(
const Duration(seconds: 1),
);
});
test('listeners throws a [FirebaseException]', () async {
DocumentReference<Map<String, dynamic>> document =
firestore.doc('not-allowed/document');
Stream<DocumentSnapshot<Map<String, dynamic>>> stream =
document.snapshots();
try {
await stream.first;
} catch (error) {
expect(error, isA<FirebaseException>());
expect(
(error as FirebaseException).code,
equals('permission-denied'),
);
return;
}
fail('Should have thrown a [FirebaseException]');
});
},
);
group('DocumentReference.delete()', () {
test('delete() deletes a document', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-delete');
await document.set({
'foo': 'bar',
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(snapshot.exists, isTrue);
await document.delete();
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(snapshot2.exists, isFalse);
});
test(
'throws a [FirebaseException] on error',
() async {
DocumentReference<Map<String, dynamic>> document =
firestore.doc('not-allowed/document');
try {
await document.delete();
} catch (error) {
expect(error, isA<FirebaseException>());
expect(
(error as FirebaseException).code,
equals('permission-denied'),
);
return;
}
fail('Should have thrown a [FirebaseException]');
},
//This will fail on web until this is resolved: https://github.com/dart-lang/sdk/issues/52572
skip: kIsWeb,
);
});
group('DocumentReference.get()', () {
test('gets blob data', () async {
final document = await initializeTest('document-get-blob');
final blob = Blob(Uint8List.fromList(<int>[0, 127, 255]));
await document.set(<String, Object?>{'blob': blob});
final snapshot = await document.get();
expect(snapshot.get('blob'), blob);
});
test(
'preserves native error messages when offline',
() async {
final document =
await initializeTest('document-get-server-while-offline');
await firestore.disableNetwork();
addTearDown(firestore.enableNetwork);
await expectLater(
document.get(const GetOptions(source: Source.server)),
throwsA(
isA<FirebaseException>()
.having((error) => error.code, 'code', 'unavailable')
.having(
(error) => error.message,
'message',
contains('offline'),
),
),
);
},
skip: kIsWeb,
);
test('gets a document from server', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-get-server');
await document.set({'foo': 'bar'});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await document.get(const GetOptions(source: Source.server));
expect(snapshot.data(), {'foo': 'bar'});
expect(snapshot.metadata.isFromCache, isFalse);
});
test(
'gets a document from cache',
() async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-get-cache');
await document.set({'foo': 'bar'});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await document.get(const GetOptions(source: Source.cache));
expect(snapshot.data(), equals({'foo': 'bar'}));
expect(snapshot.metadata.isFromCache, isTrue);
},
skip: kIsWeb,
);
test(
'throws a [FirebaseException] on error',
() async {
DocumentReference<Map<String, dynamic>> document =
firestore.doc('not-allowed/document');
try {
await document.get();
} catch (error) {
expect(error, isA<FirebaseException>());
expect(
(error as FirebaseException).code,
equals('permission-denied'),
);
return;
}
fail('Should have thrown a [FirebaseException]');
},
);
});
group('DocumentReference.set()', () {
test('sets data', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-set');
await document.set({'foo': 'bar'});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(snapshot.data(), equals({'foo': 'bar'}));
await document.set({'bar': 'baz'});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(snapshot2.data(), equals({'bar': 'baz'}));
});
test('set() merges data', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-set-merge');
await document.set({'foo': 'bar'});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(snapshot.data(), equals({'foo': 'bar'}));
await document
.set({'foo': 'ben', 'bar': 'baz'}, SetOptions(merge: true));
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(snapshot2.data(), equals({'foo': 'ben', 'bar': 'baz'}));
});
test(
'set() merges fields',
() async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-set-merge-fields');
Map<String, dynamic> initialData = {
'foo': 'bar',
'bar': 123,
'baz': '456',
};
Map<String, dynamic> dataToSet = {
'foo': 'should-not-merge',
'bar': 456,
'baz': 'foo',
};
await document.set(initialData);
DocumentSnapshot<Map<String, dynamic>> snapshot =
await document.get();
expect(snapshot.data(), equals(initialData));
await document.set(
dataToSet,
SetOptions(
mergeFields: [
'bar',
FieldPath(const ['baz']),
],
),
);
DocumentSnapshot<Map<String, dynamic>> snapshot2 =
await document.get();
expect(
snapshot2.data(),
equals({'foo': 'bar', 'bar': 456, 'baz': 'foo'}),
);
},
);
test(
'throws a [FirebaseException] on error',
() async {
DocumentReference<Map<String, dynamic>> document =
firestore.doc('not-allowed/document');
try {
await document.set({'foo': 'bar'});
} catch (error) {
expect(error, isA<FirebaseException>());
expect(
(error as FirebaseException).code,
equals('permission-denied'),
);
return;
}
fail('Should have thrown a [FirebaseException]');
},
);
test('set and return all possible datatypes', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-types');
await document.set({
'string': 'foo bar',
'number_32': 123,
// Equivalent of `Number.MAX_SAFE_INTEGER` in JS, can't go higher than this.
'number_64': 9007199254740991,
'bool_true': true,
'bool_false': false,
'map': {
'foo': 'bar',
'bar': {'baz': 'ben'},
},
'list': [
1,
'2',
true,
false,
{'foo': 'bar'},
],
'null': null,
'timestamp': Timestamp.now(),
'geopoint': const GeoPoint(1, 2),
if (defaultTargetPlatform != TargetPlatform.windows)
'vectorValue': const VectorValue([1, 2, 3]),
'reference': firestore.doc('foo/bar'),
'nan': double.nan,
'infinity': double.infinity,
'negative_infinity': double.negativeInfinity,
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
Map<String, dynamic> data = snapshot.data()!;
expect(data['string'], equals('foo bar'));
expect(data['number_32'], equals(123));
expect(data['number_64'], equals(9007199254740991));
expect(data['bool_true'], isTrue);
expect(data['bool_false'], isFalse);
expect(
data['map'],
equals(<String, dynamic>{
'foo': 'bar',
'bar': {'baz': 'ben'},
}),
);
expect(
data['list'],
equals([
1,
'2',
true,
false,
{'foo': 'bar'},
]),
);
expect(data['null'], equals(null));
expect(data['timestamp'], isA<Timestamp>());
expect(data['geopoint'], isA<GeoPoint>());
expect((data['geopoint'] as GeoPoint).latitude, equals(1));
expect((data['geopoint'] as GeoPoint).longitude, equals(2));
if (defaultTargetPlatform != TargetPlatform.windows) {
expect(data['vectorValue'], isA<VectorValue>());
expect(
(data['vectorValue'] as VectorValue).toArray(),
equals([1, 2, 3]),
);
}
expect(data['reference'], isA<DocumentReference>());
expect((data['reference'] as DocumentReference).id, equals('bar'));
expect(data['nan'].isNaN, equals(true));
expect(data['infinity'], equals(double.infinity));
expect(data['negative_infinity'], equals(double.negativeInfinity));
});
test('sets data with DocumentReference as map key', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-set-ref-key');
DocumentReference<Map<String, dynamic>> refKey =
FirebaseFirestore.instance.doc('foo/bar');
await document.set({
'myMap': {refKey: 42.0},
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
final myMap = snapshot.data()!['myMap'] as Map<String, dynamic>;
expect(myMap[refKey.path], equals(42.0));
});
});
group('DocumentReference.update()', () {
test('updates data', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update');
await document.set({'foo': 'bar'});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(snapshot.data(), equals({'foo': 'bar'}));
await document.update({'bar': 'baz'});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(snapshot2.data(), equals({'foo': 'bar', 'bar': 'baz'}));
});
test('updates nested data using dots', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-field-path');
await document.set({
'foo': {'bar': 'baz'},
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(
snapshot.data(),
equals({
'foo': {'bar': 'baz'},
}),
);
await document.update({'foo.bar': 'toto'});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(
snapshot2.data(),
equals({
'foo': {'bar': 'toto'},
}),
);
});
test('updates nested data using FieldPath', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-field-path');
await document.set({
'foo': {'bar': 'baz'},
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(
snapshot.data(),
equals({
'foo': {'bar': 'baz'},
}),
);
await document.update({
FieldPath(const ['foo', 'bar']): 'toto',
});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(
snapshot2.data(),
equals({
'foo': {'bar': 'toto'},
}),
);
});
test('updates nested data containing a dot using FieldPath', () async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-field-path');
await document.set({'foo.bar': 'baz'});
DocumentSnapshot<Map<String, dynamic>> snapshot = await document.get();
expect(
snapshot.data(),
equals({'foo.bar': 'baz'}),
);
await document.update({
FieldPath(const ['foo.bar']): 'toto',
});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await document.get();
expect(
snapshot2.data(),
equals({'foo.bar': 'toto'}),
);
});
test(
'throws if document does not exist',
() async {
DocumentReference<Map<String, dynamic>> document =
await initializeTest('document-update-not-exists');
try {
await document.update({'foo': 'bar'});
fail('Should have thrown');
} catch (e) {
expect(
e,
isA<FirebaseException>()
.having((e) => e.code, 'code', 'not-found'),
);
}
},
);
});
group('withConverter', () {
test(
'set/snapshot/get',
() async {
final foo = await initializeTest('foo');
final fooConverter = foo.withConverter<int>(
fromFirestore: (snapshots, _) => snapshots.data()!['value']! as int,
toFirestore: (value, _) => {'value': value},
);
final fooSnapshot = foo.snapshots();
final fooConverterSnapshot = fooConverter.snapshots();
await expectLater(
fooSnapshot,
emits(
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', null),
),
);
await expectLater(
fooConverterSnapshot,
emits(
isA<DocumentSnapshot<int>>()
.having((e) => e.data(), 'data', null),
),
);
await fooConverter.set(42);
await expectLater(
fooSnapshot,
emits(
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', {'value': 42}),
),
);
await expectLater(
fooConverterSnapshot,
emits(
isA<DocumentSnapshot<int>>().having((e) => e.data(), 'data', 42),
),
);
await expectLater(
fooConverter.get(const GetOptions(source: Source.server)),
completion(
isA<DocumentSnapshot<int>>().having((e) => e.data(), 'data', 42),
),
);
await foo.set({'value': 21});
await expectLater(
fooSnapshot,
emits(
isA<DocumentSnapshot<Map<String, dynamic>>>()
.having((e) => e.data(), 'data', {'value': 21}),
),
);
await expectLater(
fooConverter.get(const GetOptions(source: Source.server)),
completion(
isA<DocumentSnapshot<int>>().having((e) => e.data(), 'data', 21),
),
);
},
timeout: const Timeout.factor(3),
);
});
group('DocumentReference as field value', () {
// Regression test for https://github.com/firebase/flutterfire/issues/18028
test('can store and read a DocumentReference as a field value', () async {
final doc = await initializeTest('doc-ref-field');
final targetDoc = firestore.doc('flutter-tests/target-doc');
await doc.set({'ref': targetDoc});
final snapshot = await doc.get();
final refValue = snapshot.data()!['ref'];
expect(refValue, isA<DocumentReference>());
expect((refValue as DocumentReference).path, targetDoc.path);
});
test('can query by DocumentReference value', () async {
final collection =
firestore.collection('flutter-tests/doc-ref-query/items');
final targetDoc = firestore.doc('flutter-tests/target-doc');
// Clean up
final existing = await collection.get();
for (final doc in existing.docs) {
await doc.reference.delete();
}
await collection.add({'ref': targetDoc, 'name': 'test'});
final querySnapshot =
await collection.where('ref', isEqualTo: targetDoc).get();
expect(querySnapshot.docs, hasLength(1));
expect(querySnapshot.docs.first.data()['name'], 'test');
});
});
});
}

View file

@ -0,0 +1,71 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'collection_reference_e2e.dart';
import 'document_change_e2e.dart';
import 'document_reference_e2e.dart';
import 'field_value_e2e.dart';
import 'firebase_options.dart';
import 'geo_point_e2e.dart';
import 'instance_e2e.dart';
import 'load_bundle_e2e.dart';
import 'query_e2e.dart';
import 'second_database.dart';
import 'settings_e2e.dart';
import 'snapshot_metadata_e2e.dart';
import 'timestamp_e2e.dart';
import 'transaction_e2e.dart';
import 'vector_value_e2e.dart';
import 'web_snapshot_listeners.dart';
import 'write_batch_e2e.dart';
bool kUseFirestoreEmulator = true;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('cloud_firestore', () {
setUpAll(() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// Web by default doesn't have persistence enabled
FirebaseFirestore.instance.settings = const Settings(
persistenceEnabled: true,
);
if (kUseFirestoreEmulator) {
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);
}
});
runInstanceTests();
runCollectionReferenceTests();
runDocumentChangeTests();
runDocumentReferenceTests();
runFieldValueTests();
runGeoPointTests();
runVectorValueTests();
runQueryTests();
runSnapshotMetadataTests();
runTimestampTests();
runTransactionTests();
runWriteBatchTests();
runLoadBundleTests();
runWebSnapshotListenersTests();
if (defaultTargetPlatform != TargetPlatform.windows) {
runSecondDatabaseTests();
}
if (kIsWeb) {
runSettingsTest();
}
});
}

View file

@ -0,0 +1,240 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
void runFieldValueTests() {
group('$FieldValue', () {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<DocumentReference<Map<String, dynamic>>> initializeTest(
String path,
) async {
String prefixedPath = 'flutter-tests/$path';
await firestore.doc(prefixedPath).delete();
return firestore.doc(prefixedPath);
}
group('FieldValue.increment()', () {
test('increments a number if it exists', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-increment-exists');
await doc.set({'foo': 2});
await doc.update({'foo': FieldValue.increment(1)});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals(3));
// Expect it to be a int
expect(snapshot.data()!['foo'], isA<int>());
});
test('increments a big number if it exists', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-increment-exists');
await doc.set({'foo': 0});
await doc.update({'foo': FieldValue.increment(2148000000)});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals(2148000000));
});
test('decrements a number', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-decrement-exists');
await doc.set({'foo': 2});
await doc.update({'foo': FieldValue.increment(-1)});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals(1));
});
test('sets an increment if it does not exist', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-increment-not-exists');
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.exists, isFalse);
await doc.set({'foo': FieldValue.increment(1)});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await doc.get();
expect(snapshot2.data()!['foo'], equals(1));
});
});
group('FieldValue.serverTimestamp()', () {
test('sets a new server time value', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-server-timestamp-new');
await doc.set({'foo': FieldValue.serverTimestamp()});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], isA<Timestamp>());
});
test('updates a server time value', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-server-timestamp-update');
await doc.set({'foo': FieldValue.serverTimestamp()});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
Timestamp serverTime1 = snapshot.data()!['foo'];
expect(serverTime1, isA<Timestamp>());
await Future.delayed(const Duration(milliseconds: 100));
await doc.update({'foo': FieldValue.serverTimestamp()});
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await doc.get();
Timestamp serverTime2 = snapshot2.data()!['foo'];
expect(serverTime2, isA<Timestamp>());
expect(
serverTime2.microsecondsSinceEpoch >
serverTime1.microsecondsSinceEpoch,
isTrue,
);
});
});
group('FieldValue.delete()', () {
test('removes a value', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-delete');
await doc.set({'foo': 'bar', 'bar': 'baz'});
await doc.update({'bar': FieldValue.delete()});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data(), equals(<String, dynamic>{'foo': 'bar'}));
});
});
group('FieldValue.arrayUnion()', () {
test('updates an existing array', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-array-union-update-array');
await doc.set({
'foo': [1, 2],
});
await doc.update({
'foo': FieldValue.arrayUnion([3, 4]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals([1, 2, 3, 4]));
});
test('updates an array if current value is not an array', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-array-union-replace');
await doc.set({'foo': 'bar'});
await doc.update({
'foo': FieldValue.arrayUnion([3, 4]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals([3, 4]));
});
test('sets an array if current value is not an array', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-array-union-replace');
await doc.set({'foo': 'bar'});
await doc.set({
'foo': FieldValue.arrayUnion([3, 4]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals([3, 4]));
});
});
group('FieldValue.arrayRemove()', () {
test('removes items in an array', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-array-remove-existing');
await doc.set({
'foo': [1, 2, 3, 4],
});
await doc.update({
'foo': FieldValue.arrayRemove([3, 4]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals([1, 2]));
});
test('removes & updates an array if existing item is not an array',
() async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-array-remove-replace');
await doc.set({'foo': 'bar'});
await doc.update({
'foo': FieldValue.arrayUnion([3, 4]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals([3, 4]));
});
test('removes & sets an array if existing item is not an array',
() async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-array-remove-replace');
await doc.set({'foo': 'bar'});
await doc.set({
'foo': FieldValue.arrayUnion([3, 4]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals([3, 4]));
});
// ignore: todo
// TODO(salakar): test is currently failing on CI but unable to reproduce locally
test(
'updates with nested types',
() async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('field-value-nested-types');
DocumentReference<Map<String, dynamic>> ref =
FirebaseFirestore.instance.doc('foo/bar');
await doc.set({
'foo': [1],
});
await doc.update({
'foo': FieldValue.arrayUnion([2, ref]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.data()!['foo'], equals([1, 2, ref]));
},
skip: true,
);
test('query should restore nested Timestamp', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('nested-timestamp');
await Future.wait([
doc.set({
'nested': {
'timestamp': Timestamp.fromDate(DateTime(2020)),
},
'timestamp': Timestamp.fromDate(DateTime(2020)),
}),
]);
final snapshot = await doc.get();
expect(snapshot.data()!['timestamp'], isA<Timestamp>());
expect(snapshot.data()!['nested']['timestamp'], isA<Timestamp>());
});
test('query should restore nested Timestamp in List', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('nested-timestamp');
await doc.set({
'timestamp': Timestamp.fromDate(DateTime.now()),
'logs': [
{
'createdAt': Timestamp.fromDate(DateTime.now()),
},
],
});
final snapshot = await doc.get();
expect(snapshot.data()!['timestamp'], isA<Timestamp>());
expect(snapshot.data()!['logs'][0]['createdAt'], isA<Timestamp>());
});
});
});
}

View file

@ -0,0 +1,92 @@
// Copyright 2021 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.
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
return switch (defaultTargetPlatform) {
TargetPlatform.android => android,
TargetPlatform.iOS => ios,
TargetPlatform.macOS => macos,
TargetPlatform.windows => android,
TargetPlatform.linux => throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
),
_ => throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
)
};
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw',
appId: '1:406099696497:web:87e25e51afe982cd3574d0',
messagingSenderId: '406099696497',
projectId: 'flutterfire-e2e-tests',
authDomain: 'flutterfire-e2e-tests.firebaseapp.com',
databaseURL:
'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app',
storageBucket: 'flutterfire-e2e-tests.appspot.com',
measurementId: 'G-JN95N1JV2E',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw',
appId: '1:406099696497:android:175ea7a64b2faf5e3574d0',
messagingSenderId: '406099696497',
projectId: 'flutterfire-e2e-tests',
databaseURL:
'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app',
storageBucket: 'flutterfire-e2e-tests.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c',
appId: '1:406099696497:ios:0670bc5fe8574a9c3574d0',
messagingSenderId: '406099696497',
projectId: 'flutterfire-e2e-tests',
databaseURL:
'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app',
storageBucket: 'flutterfire-e2e-tests.appspot.com',
androidClientId:
'406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com',
iosClientId:
'406099696497-l9gojfp6b3h1cgie1se28a9ol9fmsvvk.apps.googleusercontent.com',
iosBundleId: 'io.flutter.plugins.firebase.firestore.example',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c',
appId: '1:406099696497:ios:0670bc5fe8574a9c3574d0',
messagingSenderId: '406099696497',
projectId: 'flutterfire-e2e-tests',
databaseURL:
'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app',
storageBucket: 'flutterfire-e2e-tests.appspot.com',
androidClientId:
'406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com',
iosClientId:
'406099696497-l9gojfp6b3h1cgie1se28a9ol9fmsvvk.apps.googleusercontent.com',
iosBundleId: 'io.flutter.plugins.firebase.firestore.example',
);
}

View file

@ -0,0 +1,75 @@
// 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.
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options_secondary.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
return switch (defaultTargetPlatform) {
TargetPlatform.android => android,
TargetPlatform.iOS => ios,
TargetPlatform.macOS => macos,
TargetPlatform.windows => android,
_ => throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
)
};
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyAFh7c37C6MyAVwRJylz7EwvzZSECqmcus',
appId: '1:866672724757:web:3a0fad9cba4848ea19f71c',
messagingSenderId: '866672724757',
projectId: 'flutterfire-e2e-tests-2',
authDomain: 'flutterfire-e2e-tests-2.firebaseapp.com',
storageBucket: 'flutterfire-e2e-tests-2.appspot.com',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAMoRmAcD_NW0DVoO40ThJO1zDF2vDB7Rs',
appId: '1:866672724757:android:d069b410181b65cf19f71c',
messagingSenderId: '866672724757',
projectId: 'flutterfire-e2e-tests-2',
storageBucket: 'flutterfire-e2e-tests-2.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDfWh055gUcnS6_Gqd_Jjwy7boVm5_E8oI',
appId: '1:866672724757:ios:142139d84dda4ed419f71c',
messagingSenderId: '866672724757',
projectId: 'flutterfire-e2e-tests-2',
storageBucket: 'flutterfire-e2e-tests-2.appspot.com',
iosClientId:
'866672724757-rncbdu3qrn0j423e1tfk2jg7jdvrhd8i.apps.googleusercontent.com',
iosBundleId: 'io.flutter.plugins.firebase.firestoreExample',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyDfWh055gUcnS6_Gqd_Jjwy7boVm5_E8oI',
appId: '1:866672724757:ios:3e35357fc677cc5719f71c',
messagingSenderId: '866672724757',
projectId: 'flutterfire-e2e-tests-2',
storageBucket: 'flutterfire-e2e-tests-2.appspot.com',
iosClientId:
'866672724757-fup6o8riklmmc7mbo3b1jolbeadbb84m.apps.googleusercontent.com',
iosBundleId: 'io.flutter.plugins.firebase.firestore.example',
);
}

View file

@ -0,0 +1,54 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
void runGeoPointTests() {
group('$GeoPoint', () {
late FirebaseFirestore /*?*/ firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<DocumentReference<Map<String, dynamic>>> initializeTest(
String path,
) async {
String prefixedPath = 'flutter-tests/$path';
await firestore.doc(prefixedPath).delete();
return firestore.doc(prefixedPath);
}
test('sets a $GeoPoint & returns one', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('geo-point');
await doc.set({'foo': const GeoPoint(10, -10)});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
GeoPoint geopoint = snapshot.data()!['foo'];
expect(geopoint, isA<GeoPoint>());
expect(geopoint.latitude, equals(10));
expect(geopoint.longitude, equals(-10));
});
test('updates a $GeoPoint & returns', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('geo-point-update');
await doc.set({'foo': const GeoPoint(10, -10)});
await doc.update({'foo': const GeoPoint(-10, 10)});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
GeoPoint geopoint = snapshot.data()!['foo'];
expect(geopoint, isA<GeoPoint>());
expect(geopoint.latitude, equals(-10));
expect(geopoint.longitude, equals(10));
});
});
}

View file

@ -0,0 +1,383 @@
// Copyright 2020, 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.
import 'dart:async';
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void runInstanceTests() {
group(
'$FirebaseFirestore.instance',
() {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
test(
'snapshotsInSync()',
() async {
DocumentReference<Map<String, dynamic>> documentReference =
firestore.doc('flutter-tests/insync');
// Ensure deleted
await documentReference.delete();
StreamController controller = StreamController();
StreamSubscription insync;
StreamSubscription snapshots;
int inSyncCount = 0;
insync = firestore.snapshotsInSync().listen((_) {
controller.add('insync=$inSyncCount');
inSyncCount++;
});
snapshots = documentReference.snapshots().listen((ds) {
controller.add('snapshot-exists=${ds.exists}');
});
// Allow the snapshots to trigger...
await Future.delayed(const Duration(seconds: 1));
await documentReference.set({'foo': 'bar'});
await expectLater(
controller.stream,
emitsInOrder([
'insync=0', // No other snapshots
'snapshot-exists=false',
'insync=1',
'snapshot-exists=true',
'insync=2',
]),
);
await controller.close();
await insync.cancel();
await snapshots.cancel();
},
skip: kIsWeb,
);
test(
'enableNetwork()',
() async {
// Write some data while online
await firestore.enableNetwork();
DocumentReference<Map<String, dynamic>> documentReference =
firestore.doc('flutter-tests/enable-network');
await documentReference.set({'foo': 'bar'});
// Disable the network
await firestore.disableNetwork();
StreamController controller = StreamController();
// Set some data while offline
// ignore: unawaited_futures
documentReference.set({'foo': 'baz'}).then((_) async {
// Only when back online will this trigger
controller.add(true);
});
// Go back online
await firestore.enableNetwork();
await expectLater(controller.stream, emits(true));
await controller.close();
},
skip: kIsWeb,
);
test(
'disableNetwork()',
() async {
// Write some data while online
await firestore.enableNetwork();
DocumentReference<Map<String, dynamic>> documentReference =
firestore.doc('flutter-tests/disable-network');
await documentReference.set({'foo': 'bar'});
// Disable the network
await firestore.disableNetwork();
// Get data from cache
DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
await documentReference.get();
expect(documentSnapshot.metadata.isFromCache, isTrue);
expect(documentSnapshot.data()!['foo'], equals('bar'));
// Go back online once test complete
await firestore.enableNetwork();
},
skip: kIsWeb,
);
test(
'waitForPendingWrites()',
() async {
await firestore.waitForPendingWrites();
},
skip: kIsWeb,
);
test(
'terminate() / clearPersistence()',
() async {
// Since the firestore instance has already been used,
// calling `clearPersistence` will throw a native error.
// We first check it does throw as expected, then terminate
// the instance, and then check whether clearing succeeds.
try {
await firestore.clearPersistence();
fail('Should have thrown');
} on FirebaseException catch (e) {
expect(e.code, equals('failed-precondition'));
} catch (e) {
fail('$e');
}
await firestore.terminate();
await firestore.clearPersistence();
},
skip: kIsWeb,
);
test(
'terminate() then use Firestore again',
() async {
// Regression test for https://github.com/firebase/flutterfire/issues/17781
// On Windows, terminate() did not remove the instance from the native
// cache, so subsequent usage would crash with "The client has already
// been terminated".
final instance = FirebaseFirestore.instanceFor(
app: Firebase.app(),
databaseId: 'flutterfire-2',
);
instance.useFirestoreEmulator('localhost', 8080);
// Use Firestore so it is fully initialized
await instance.collection('flutterfire-2').doc('terminate-test').set(
{'foo': 'bar'},
);
await instance.terminate();
await instance.clearPersistence();
// After terminate + clearPersistence, we should be able to use
// Firestore again without crashing.
await instance
.collection('flutterfire-2')
.doc('terminate-test')
.get();
// Clean up: terminate so the native instance cache is cleared
// for subsequent tests that may use the same databaseId.
await instance.terminate();
},
skip: kIsWeb,
);
test(
'setIndexConfigurationFromJSON()',
() async {
final json = jsonEncode({
'indexes': [
{
'collectionGroup': 'posts',
'queryScope': 'COLLECTION',
'fields': [
{'fieldPath': 'author', 'arrayConfig': 'CONTAINS'},
{'fieldPath': 'timestamp', 'order': 'DESCENDING'},
],
}
],
'fieldOverrides': [
{
'collectionGroup': 'posts',
'fieldPath': 'myBigMapField',
'indexes': [],
}
],
});
// ignore: experimental_member_use
await firestore.setIndexConfigurationFromJSON(json);
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
test('setLoggingEnabled should resolve without issue', () async {
await FirebaseFirestore.setLoggingEnabled(true);
await FirebaseFirestore.setLoggingEnabled(false);
});
test(
'Settings() - `persistenceEnabled` & `cacheSizeBytes` with acceptable number',
() async {
FirebaseFirestore.instance.settings =
const Settings(persistenceEnabled: true, cacheSizeBytes: 10000000);
// Used to trigger settings
await FirebaseFirestore.instance
.collection('flutter-tests')
.doc('new-doc')
.set(
{'some': 'data'},
);
});
test(
'Settings() - `persistenceEnabled` & `cacheSizeBytes` with `Settings.CACHE_SIZE_UNLIMITED`',
() async {
FirebaseFirestore.instance.settings = const Settings(
persistenceEnabled: true,
cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);
// Used to trigger settings
await FirebaseFirestore.instance
.collection('flutter-tests')
.doc('new-doc')
.set(
{'some': 'data'},
);
});
test('Settings() - `persistenceEnabled` & without `cacheSizeBytes`',
() async {
FirebaseFirestore.instance.settings =
const Settings(persistenceEnabled: true);
// Used to trigger settings
await FirebaseFirestore.instance
.collection('flutter-tests')
.doc('new-doc')
.set(
{'some': 'data'},
);
});
test(
'`PersistenceCacheIndexManager` with default persistence settings for each platform',
() async {
if (defaultTargetPlatform == TargetPlatform.windows) {
try {
// Windows does not have `PersistenceCacheIndexManager` support
FirebaseFirestore.instance.persistentCacheIndexManager();
} catch (e) {
expect(e, isInstanceOf<UnimplementedError>());
}
} else {
if (kIsWeb) {
// persistence is disabled by default on web
final firestore = FirebaseFirestore.instanceFor(
app: Firebase.app(),
// Use different firestore instance to test behavior
databaseId: 'default-web',
);
PersistentCacheIndexManager? indexManager =
firestore.persistentCacheIndexManager();
expect(indexManager, isNull);
} else {
final firestore = FirebaseFirestore.instanceFor(
app: Firebase.app(),
// Use different firestore instance to test behavior
databaseId: 'default-other-platform-test',
);
// macOS, android, iOS have persistence enabled by default
PersistentCacheIndexManager? indexManager =
firestore.persistentCacheIndexManager();
await indexManager!.enableIndexAutoCreation();
await indexManager.disableIndexAutoCreation();
await indexManager.deleteAllIndexes();
}
}
},
);
test(
'`PersistenceCacheIndexManager` with persistence enabled for each platform',
() async {
if (kIsWeb) {
final firestore = FirebaseFirestore.instanceFor(
app: Firebase.app(),
databaseId: 'web-enabled',
);
// persistence is disabled by default so we enable it
firestore.settings = const Settings(persistenceEnabled: true);
PersistentCacheIndexManager? indexManager =
firestore.persistentCacheIndexManager();
await indexManager!.enableIndexAutoCreation();
await indexManager.disableIndexAutoCreation();
await indexManager.deleteAllIndexes();
final firestore2 = FirebaseFirestore.instanceFor(
app: Firebase.app(),
databaseId: 'web-disabled-2',
);
// Enable persistence using settings instead of deprecated enablePersistence()
firestore2.settings = const Settings(persistenceEnabled: true);
PersistentCacheIndexManager? indexManager2 =
firestore2.persistentCacheIndexManager();
await indexManager2!.enableIndexAutoCreation();
await indexManager2.disableIndexAutoCreation();
await indexManager2.deleteAllIndexes();
} else {
final firestore = FirebaseFirestore.instanceFor(
app: Firebase.app(),
databaseId: 'other-platform-enabled',
);
firestore.settings = const Settings(persistenceEnabled: true);
PersistentCacheIndexManager? indexManager =
firestore.persistentCacheIndexManager();
await indexManager!.enableIndexAutoCreation();
await indexManager.disableIndexAutoCreation();
await indexManager.deleteAllIndexes();
}
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
test(
'`PersistenceCacheIndexManager` with persistence disabled for each platform',
() async {
if (kIsWeb) {
final firestore = FirebaseFirestore.instanceFor(
app: Firebase.app(),
databaseId: 'web-disabled-1',
);
// persistence is disabled by default so we enable it
firestore.settings = const Settings(persistenceEnabled: false);
PersistentCacheIndexManager? indexManager =
firestore.persistentCacheIndexManager();
expect(indexManager, isNull);
} else {
final firestore = FirebaseFirestore.instanceFor(
app: Firebase.app(),
databaseId: 'other-platform-disabled',
);
// macOS, android, iOS have persistence enabled by default so we disable it
firestore.settings = const Settings(persistenceEnabled: false);
PersistentCacheIndexManager? indexManager =
firestore.persistentCacheIndexManager();
expect(indexManager, isNull);
}
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
},
);
}

View file

@ -0,0 +1,252 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
void runLoadBundleTests() {
group('$DocumentReference', () {
late FirebaseFirestore firestore;
Future<Uint8List> loadBundleSetup(int number) async {
// endpoint serves a bundle with 3 documents each containing
// a 'number' property that increments in value 1-3.
final url =
Uri.https('api.rnfirebase.io', '/firestore/e2e-tests/bundle-$number');
final response = await http.get(url);
String string = response.body;
return Uint8List.fromList(string.codeUnits);
}
setUp(() async {
firestore = FirebaseFirestore.instance;
});
group('FirebaseFirestore.loadBundle()', () {
test('loadBundle()', () async {
const int number = 1;
const String collection = 'firestore-bundle-tests-$number';
Uint8List buffer = await loadBundleSetup(number);
LoadBundleTask task = firestore.loadBundle(buffer);
// ensure the bundle has been completely cached
await task.stream.last;
QuerySnapshot<Map<String, Object?>> snapshot = await firestore
.collection(collection)
.get(const GetOptions(source: Source.cache));
expect(
snapshot.docs.map((document) => document['number']),
everyElement(anyOf(1, 2, 3)),
);
});
test(
'loadBundle(): LoadBundleTaskProgress stream snapshots',
() async {
Uint8List buffer = await loadBundleSetup(2);
LoadBundleTask task = firestore.loadBundle(buffer);
final list = await task.stream.toList();
expect(
list.map((e) => e.totalDocuments),
everyElement(isNonNegative),
);
expect(list.map((e) => e.bytesLoaded), everyElement(isNonNegative));
expect(
list.map((e) => e.documentsLoaded),
everyElement(isNonNegative),
);
expect(list.map((e) => e.totalBytes), everyElement(isNonNegative));
expect(list, everyElement(isInstanceOf<LoadBundleTaskSnapshot>()));
LoadBundleTaskSnapshot lastSnapshot = list.removeLast();
expect(lastSnapshot.taskState, LoadBundleTaskState.success);
expect(
list.map((e) => e.taskState),
everyElement(LoadBundleTaskState.running),
);
},
// Working locally but is failing on CI
skip: kIsWeb,
);
test(
'loadBundle(): error handling for malformed bundle',
() async {
final url = Uri.https(
'api.rnfirebase.io',
'/firestore/e2e-tests/malformed-bundle',
);
final response = await http.get(url);
String string = response.body;
Uint8List buffer = Uint8List.fromList(string.codeUnits);
LoadBundleTask task = firestore.loadBundle(buffer);
await expectLater(
task.stream.last,
throwsA(
isA<FirebaseException>()
.having((e) => e.code, 'code', 'load-bundle-error'),
),
);
},
);
test(
'loadBundle(): pause and resume stream',
() async {
Uint8List buffer = await loadBundleSetup(3);
LoadBundleTask task = firestore.loadBundle(buffer);
// Illustrates the pause() & resume() function.
// A single stream will stop sending events once the listener is unsubscribed
// Will listen & pause after first event received
await expectLater(
task.stream,
emits(
isA<LoadBundleTaskSnapshot>().having(
(ts) => ts.taskState,
'taskState',
LoadBundleTaskState.running,
),
),
);
await Future.delayed(const Duration(milliseconds: 1));
// Will resume & pause after second event received
await expectLater(
task.stream,
emits(
isA<LoadBundleTaskSnapshot>().having(
(ts) => ts.taskState,
'taskState',
anyOf(LoadBundleTaskState.running, LoadBundleTaskState.success),
),
),
);
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
});
group('FirebaseFirestore.namedQueryGet()', () {
test(
'namedQueryGet() successful',
() async {
const int number = 4;
Uint8List buffer = await loadBundleSetup(number);
LoadBundleTask task = firestore.loadBundle(buffer);
// ensure the bundle has been completely cached
await task.stream.last;
// namedQuery 'named-bundle-test' which returns a QuerySnaphot of the same 3 documents
// with 'number' property
QuerySnapshot<Map<String, Object?>> snapshot =
await firestore.namedQueryGet(
'named-bundle-test-$number',
options: const GetOptions(source: Source.cache),
);
expect(
snapshot.docs.map((document) => document['number']),
everyElement(anyOf(1, 2, 3)),
);
},
skip: kIsWeb,
);
test(
'namedQueryGet() error',
() async {
Uint8List buffer = await loadBundleSetup(4);
LoadBundleTask task = firestore.loadBundle(buffer);
// ensure the bundle has been completely cached
await task.stream.last;
await expectLater(
firestore.namedQueryGet(
'wrong-name',
options: const GetOptions(source: Source.cache),
),
throwsA(
isA<FirebaseException>()
.having((e) => e.code, 'code', 'non-existent-named-query'),
),
);
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
});
group('FirebaeFirestore.namedQueryWithConverterGet()', () {
test('namedQueryWithConverterGet() successful', () async {
const int number = 4;
Uint8List buffer = await loadBundleSetup(number);
LoadBundleTask task = firestore.loadBundle(buffer);
// ensure the bundle has been completely cached
await task.stream.last;
// namedQuery 'named-bundle-test' which returns a QuerySnaphot of the same 3 documents
// with 'number' property
QuerySnapshot<ConverterPlaceholder> snapshot =
await firestore.namedQueryWithConverterGet<ConverterPlaceholder>(
'named-bundle-test-$number',
options: const GetOptions(source: Source.cache),
fromFirestore: ConverterPlaceholder.new,
toFirestore: (value, options) => value.toFirestore(),
);
expect(
snapshot.docs.map((document) => document['number']),
everyElement(anyOf(1, 2, 3)),
);
});
test(
'namedQueryWithConverterGet() error',
() async {
Uint8List buffer = await loadBundleSetup(4);
LoadBundleTask task = firestore.loadBundle(buffer);
// ensure the bundle has been completely cached
await task.stream.last;
await expectLater(
firestore.namedQueryWithConverterGet<ConverterPlaceholder>(
'wrong-name',
options: const GetOptions(source: Source.cache),
fromFirestore: ConverterPlaceholder.new,
toFirestore: (value, options) => value.toFirestore(),
),
throwsA(
isA<FirebaseException>()
.having((e) => e.code, 'code', 'non-existent-named-query'),
),
);
},
skip: defaultTargetPlatform == TargetPlatform.windows,
);
});
});
}
class ConverterPlaceholder {
ConverterPlaceholder(this.firestore, this.getOptions);
final DocumentSnapshot<Map<String, Object?>> firestore;
final SnapshotOptions? getOptions;
Map<String, Object?> toFirestore() => firestore.data()!;
}

View file

@ -0,0 +1,84 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
void runSettingsTest() {
group(
'$Settings',
() {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<Settings> initializeTest() async {
Settings firestoreSettings = const Settings(
persistenceEnabled: false,
webExperimentalForceLongPolling: true,
webExperimentalAutoDetectLongPolling: true,
webExperimentalLongPollingOptions: WebExperimentalLongPollingOptions(
timeoutDuration: Duration(seconds: 15),
),
);
return firestore.settings = firestoreSettings;
}
test('checks if long polling settings were applied', () async {
Settings settings = await initializeTest();
expect(settings.webExperimentalForceLongPolling, true);
expect(settings.webExperimentalAutoDetectLongPolling, true);
expect(
settings.webExperimentalLongPollingOptions,
settings.webExperimentalLongPollingOptions,
);
});
test('can apply WebPersistentMultipleTabManager setting', () async {
const settings = Settings(
persistenceEnabled: true,
webPersistentTabManager: WebPersistentMultipleTabManager(),
);
firestore.settings = settings;
expect(
firestore.settings.webPersistentTabManager,
isA<WebPersistentMultipleTabManager>(),
);
});
test('can apply WebPersistentSingleTabManager setting', () async {
const settings = Settings(
persistenceEnabled: true,
webPersistentTabManager:
WebPersistentSingleTabManager(forceOwnership: true),
);
firestore.settings = settings;
final tabManager = firestore.settings.webPersistentTabManager;
expect(tabManager, isA<WebPersistentSingleTabManager>());
expect(
(tabManager! as WebPersistentSingleTabManager).forceOwnership,
true,
);
});
test('webPersistentTabManager defaults to null', () async {
const settings = Settings(
persistenceEnabled: true,
);
expect(settings.webPersistentTabManager, isNull);
});
},
);
}

View file

@ -0,0 +1,44 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void runSnapshotMetadataTests() {
group(
'$SnapshotMetadata',
() {
late FirebaseFirestore /*?*/ firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<CollectionReference> initializeTest(String id) async {
CollectionReference collection =
firestore.collection('flutter-tests/$id/query-tests');
QuerySnapshot snapshot = await collection.get();
await Future.forEach(snapshot.docs,
(DocumentSnapshot documentSnapshot) {
return documentSnapshot.reference.delete();
});
return collection;
}
test('a snapshot returns the correct [isFromCache] value', () async {
CollectionReference collection =
await initializeTest('snapshot-metadata-is-from-cache');
QuerySnapshot qs =
await collection.get(const GetOptions(source: Source.cache));
expect(qs.metadata.isFromCache, isTrue);
QuerySnapshot qs2 =
await collection.get(const GetOptions(source: Source.server));
expect(qs2.metadata.isFromCache, isFalse);
});
},
skip: kIsWeb,
);
}

View file

@ -0,0 +1,74 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
void runTimestampTests() {
group('$Timestamp', () {
late FirebaseFirestore /*?*/ firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<DocumentReference<Map<String, dynamic>>> initializeTest(
String path,
) async {
String prefixedPath = 'flutter-tests/$path';
await firestore.doc(prefixedPath).delete();
return firestore.doc(prefixedPath);
}
test('sets a $Timestamp & returns one', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('timestamp');
DateTime date = DateTime.utc(3000);
await doc.set({'foo': Timestamp.fromDate(date)});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
Timestamp timestamp = snapshot.data()!['foo'];
expect(timestamp, isA<Timestamp>());
expect(
timestamp.millisecondsSinceEpoch,
equals(date.millisecondsSinceEpoch),
);
});
test('updates a $Timestamp & returns', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('geo-point-update');
DateTime date = DateTime.utc(3000, 01, 02);
await doc.set({'foo': DateTime.utc(3000)});
await doc.update({'foo': date});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
Timestamp timestamp = snapshot.data()!['foo'];
expect(timestamp, isA<Timestamp>());
expect(
timestamp.millisecondsSinceEpoch,
equals(date.millisecondsSinceEpoch),
);
});
test('set pre-1970 $Timestamp and return', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('timestamp');
final date = DateTime(1969, 06, 22, 0, 0, 0, 123);
final localTimestamp = Timestamp.fromDate(date);
await doc.set({'foo': localTimestamp});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
Timestamp retievedTimestamp = snapshot.data()!['foo'];
expect(retievedTimestamp, isA<Timestamp>());
expect(
retievedTimestamp,
equals(localTimestamp),
);
});
});
}

View file

@ -0,0 +1,558 @@
// Copyright 2020, 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.
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void runTransactionTests() {
group(
'$Transaction',
() {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<DocumentReference<Map<String, dynamic>>> initializeTest(
String path,
) async {
String prefixedPath = 'flutter-tests/$path';
await firestore.doc(prefixedPath).delete();
return firestore.doc(prefixedPath);
}
test('works with withConverter', () async {
DocumentReference<Map<String, dynamic>> rawDoc =
await initializeTest('with-converter-batch');
DocumentReference<int> doc = rawDoc.withConverter(
fromFirestore: (snapshot, options) {
return snapshot.data()!['value'] as int;
},
toFirestore: (value, options) => {'value': value},
);
await doc.set(42);
expect(
await firestore.runTransaction<int?>((transaction) async {
final snapshot = await transaction.get<int>(doc);
return snapshot.data();
}),
42,
);
await firestore.runTransaction((transaction) async {
transaction.set(doc, 21);
});
expect(await doc.get().then((s) => s.data()), 21);
await firestore.runTransaction((transaction) async {
transaction.update(doc, {'value': 0});
});
expect(await doc.get().then((s) => s.data()), 0);
});
test('should resolve with user value', () async {
int randomValue = Random().nextInt(9999);
int response = await firestore
.runTransaction<int>((Transaction transaction) async {
return randomValue;
});
expect(response, equals(randomValue));
});
test('should abort if thrown and not continue', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-abort');
await documentReference.set({'foo': 'bar'});
try {
await firestore.runTransaction((Transaction transaction) async {
transaction.set(documentReference, {
'foo': 'baz',
});
throw 'Stop';
});
// ignore: dead_code
fail('Should have thrown');
} catch (e) {
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.data()!['foo'], equals('bar'));
}
});
test(
'should not collide if number of maxAttempts is enough',
() async {
DocumentReference<Map<String, dynamic>> doc1 =
await initializeTest('transaction-maxAttempts-1');
await doc1.set({'test': 0});
await Future.wait([
firestore.runTransaction(
(Transaction transaction) async {
final value = await transaction.get(doc1);
transaction.set(doc1, {
'test': value['test'] + 1,
});
},
maxAttempts: 2,
),
firestore.runTransaction(
(Transaction transaction) async {
final value = await transaction.get(doc1);
transaction.set(doc1, {
'test': value['test'] + 1,
});
},
maxAttempts: 2,
),
]);
DocumentSnapshot<Map<String, dynamic>> snapshot1 = await doc1.get();
expect(snapshot1.data()!['test'], equals(2));
},
retry: 2,
);
test(
'should collide if number of maxAttempts is too low',
() async {
DocumentReference<Map<String, dynamic>> doc1 =
await initializeTest('transaction-maxAttempts-2');
await doc1.set({'test': 0});
await expectLater(
Future.wait([
firestore.runTransaction(
(Transaction transaction) async {
final value = await transaction.get(doc1);
transaction.set(doc1, {
'test': value['test'] + 1,
});
},
maxAttempts: 1,
),
firestore.runTransaction(
(Transaction transaction) async {
final value = await transaction.get(doc1);
transaction.set(doc1, {
'test': value['test'] + 1,
});
},
maxAttempts: 1,
),
]),
throwsA(
isA<FirebaseException>()
.having((e) => e.code, 'code', 'failed-precondition'),
),
);
},
skip: kIsWeb || defaultTargetPlatform == TargetPlatform.windows,
);
test('runs multiple transactions in parallel', () async {
DocumentReference<Map<String, dynamic>> doc1 =
await initializeTest('transaction-multi-1');
DocumentReference<Map<String, dynamic>> doc2 =
await initializeTest('transaction-multi-2');
await doc1.set({'test': 'value1'});
await doc2.set({'test': 'value2'});
await Future.wait([
firestore.runTransaction((Transaction transaction) async {
transaction.set(doc1, {
'test': 'value3',
});
}),
firestore.runTransaction((Transaction transaction) async {
transaction.set(doc2, {
'test': 'value4',
});
}),
]);
DocumentSnapshot<Map<String, dynamic>> snapshot1 = await doc1.get();
expect(snapshot1.data()!['test'], equals('value3'));
DocumentSnapshot<Map<String, dynamic>> snapshot2 = await doc2.get();
expect(snapshot2.data()!['test'], equals('value4'));
});
test(
'should abort if timeout is exceeded',
() async {
await expectLater(
firestore.runTransaction(
(Transaction transaction) =>
Future.delayed(const Duration(seconds: 2)),
timeout: const Duration(seconds: 1),
),
throwsA(
isA<FirebaseException>()
.having((e) => e.code, 'code', 'deadline-exceeded'),
),
);
},
skip: kIsWeb || defaultTargetPlatform == TargetPlatform.windows,
);
test('should throw with exception', () async {
try {
await firestore.runTransaction((Transaction transaction) async {
throw StateError('foo');
});
// ignore: dead_code
fail('Transaction should not have resolved');
} on StateError catch (e) {
expect(e.message, equals('foo'));
return;
} catch (e) {
fail('Transaction threw invalid exeption');
}
});
test(
'should throw a native error, and convert to a [FirebaseException]',
() async {
DocumentReference<Map<String, dynamic>> documentReference =
firestore.doc('not-allowed/document');
try {
await firestore.runTransaction((Transaction transaction) async {
transaction.set(documentReference, {'foo': 'bar'});
});
fail('Transaction should not have resolved');
} on FirebaseException catch (e) {
expect(e.code, equals('permission-denied'));
return;
} catch (e) {
fail('Transaction threw invalid exception');
}
},
skip: kIsWeb || defaultTargetPlatform == TargetPlatform.windows,
);
group('Transaction.get()', () {
test('should throw if get is called after a command', () async {
DocumentReference<Map<String, dynamic>> documentReference =
firestore.doc('flutter-tests/foo');
expect(
() => firestore.runTransaction((Transaction transaction) async {
await transaction.get(documentReference);
transaction.set(documentReference, {'foo': 'bar'});
await transaction.get(documentReference);
}),
throwsAssertionError,
);
});
test(
'should throw a native error, and convert to a [FirebaseException]',
() async {
DocumentReference<Map<String, dynamic>> documentReference =
firestore.doc('not-allowed/document');
try {
await firestore.runTransaction((Transaction transaction) async {
await transaction.get(documentReference);
});
fail('Transaction should not have resolved');
} on FirebaseException catch (e) {
expect(e.code, equals('permission-denied'));
return;
} catch (e) {
fail('Transaction threw invalid exception');
}
},
skip: kIsWeb || defaultTargetPlatform == TargetPlatform.windows,
);
// ignore: todo
// TODO(Salakar): Test seems to fail sometimes. Will look at in a future PR.
// test('support returning any value, e.g. a [DocumentSnapshot]', () async {
// DocumentReference<Map<String, dynamic>> documentReference =
// await initializeTest('transaction-get');
// DocumentSnapshot<Map<String, dynamic>> snapshot =
// await firestore.runTransaction((Transaction transaction) async {
// DocumentSnapshot<Map<String, dynamic>> returned = await transaction.get(documentReference);
// // required:
// transaction.set(documentReference, {'foo': 'bar'});
// return returned;
// });
// expect(snapshot, isA<DocumentSnapshot>());
// expect(snapshot.reference.path, equals(documentReference.path));
// }, skip: kUseFirestoreEmulator);
});
group('Transaction.delete()', () {
test('should delete a document', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-delete');
await documentReference.set({'foo': 'bar'});
await firestore.runTransaction((Transaction transaction) async {
transaction.delete(documentReference);
});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.exists, isFalse);
});
});
group('Transaction.update()', () {
test('should update a document', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-update');
await documentReference.set({'foo': 'bar', 'bar': 1});
await firestore.runTransaction((Transaction transaction) async {
DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
await transaction.get(documentReference);
transaction.update(documentReference, {
'bar': documentSnapshot.data()!['bar'] + 1,
});
});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['bar'], equals(2));
expect(snapshot.data()!['foo'], equals('bar'));
});
test('should update a document using FieldPath keys', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-update-field-path');
await documentReference.set({
'nested': {'field': 'old_value'},
'top': 'value',
});
await firestore.runTransaction((Transaction transaction) async {
await transaction.get(documentReference);
transaction.update(documentReference, {
FieldPath(const ['nested', 'field']): 'new_value',
});
});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['nested']['field'], equals('new_value'));
expect(snapshot.data()!['top'], equals('value'));
});
});
group('Transaction.set()', () {
test('sets a document', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-set');
await documentReference.set({'foo': 'bar', 'bar': 1});
await firestore.runTransaction((Transaction transaction) async {
DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
await transaction.get(documentReference);
transaction.set(documentReference, {
'bar': documentSnapshot.data()!['bar'] + 1,
});
});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.exists, isTrue);
expect(
snapshot.data(),
equals(<String, dynamic>{'bar': 2}),
);
});
test('merges a document with set', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-set-merge');
await documentReference.set({'foo': 'bar', 'bar': 1});
await firestore.runTransaction((Transaction transaction) async {
DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
await transaction.get(documentReference);
transaction.set(
documentReference,
{'bar': documentSnapshot.data()!['bar'] + 1},
SetOptions(merge: true),
);
});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['bar'], equals(2));
expect(snapshot.data()!['foo'], equals('bar'));
});
test('merges fields a document with set', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-set-merge-fields');
await documentReference.set({'foo': 'bar', 'bar': 1, 'baz': 1});
await firestore.runTransaction((Transaction transaction) async {
DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
await transaction.get(documentReference);
transaction.set(
documentReference,
{
'bar': documentSnapshot.data()!['bar'] + 1,
'baz': 'ben',
},
SetOptions(mergeFields: ['bar']),
);
});
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.exists, isTrue);
expect(
snapshot.data(),
equals(<String, dynamic>{'foo': 'bar', 'bar': 2, 'baz': 1}),
);
});
});
test('runs all commands in a single transaction', () async {
DocumentReference<Map<String, dynamic>> documentReference =
await initializeTest('transaction-all');
DocumentReference<Map<String, dynamic>> documentReference2 =
firestore.doc('flutter-tests/delete');
await documentReference2.set({'foo': 'bar'});
await documentReference.set({'foo': 1});
String result = await firestore
.runTransaction<String>((Transaction transaction) async {
DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
await transaction.get(documentReference);
transaction.set(documentReference, {
'foo': documentSnapshot.data()!['foo'] + 1,
});
transaction.update(documentReference, {'bar': 'baz'});
transaction.delete(documentReference2);
return 'done';
});
expect(result, equals('done'));
DocumentSnapshot<Map<String, dynamic>> snapshot =
await documentReference.get();
expect(snapshot.exists, isTrue);
expect(
snapshot.data(),
equals(<String, dynamic>{'foo': 2, 'bar': 'baz'}),
);
DocumentSnapshot<Map<String, dynamic>> snapshot2 =
await documentReference2.get();
expect(snapshot2.exists, isFalse);
});
test(
'runs many transactions concurrently without corrupting native state',
() async {
// Regression test for
// https://github.com/firebase/flutterfire/issues/18417: concurrent
// transactions used to mutate the plugin's shared transaction map
// from multiple threads without synchronization, which could crash
// iOS with a heap-corruption SIGABRT.
const int count = 30;
final refs = [
for (var i = 0; i < count; i++)
firestore.doc('flutter-tests/transaction-concurrent-$i'),
];
await Future.wait([
for (final ref in refs)
firestore.runTransaction((Transaction transaction) async {
final snapshot = await transaction.get(ref);
transaction.set(ref, {
'value': ((snapshot.data()?['value'] as int?) ?? 0) + 1,
});
}),
]);
final snapshots = await Future.wait(refs.map((ref) => ref.get()));
for (final snapshot in snapshots) {
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['value'], isA<int>());
}
},
);
// TODO(Lyokone): adding auth make some tests fails in macOS
// test(
// 'should not fail to complete transaction if user is authenticated',
// () async {
// DocumentReference<Map<String, dynamic>> doc1 =
// await initializeTest('transaction-authentified-1');
// try {
// await FirebaseAuth.instance.createUserWithEmailAndPassword(
// email: 'firestore@mail.com',
// password: 'this-is-a-password',
// );
// } catch (e) {
// await FirebaseAuth.instance.signInWithEmailAndPassword(
// email: 'firestore@mail.com',
// password: 'this-is-a-password',
// );
// }
// await doc1.set({'test': 0});
// final value = await firestore.runTransaction(
// (Transaction transaction) async {
// final value = await transaction.get(doc1);
// final newValue = value['test'] + 1;
// transaction.set(doc1, {
// 'test': newValue,
// });
// return newValue;
// },
// maxAttempts: 1,
// );
// expect(value, equals(1));
// await FirebaseAuth.instance.signOut();
// });
},
skip: kIsWeb,
);
}

View file

@ -0,0 +1,184 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void runVectorValueTests() {
if (defaultTargetPlatform == TargetPlatform.windows) {
group('$VectorValue', () {
test(
'is not supported on Windows',
() {},
skip: 'The Firebase C++ SDK does not expose Firestore vector values.',
);
});
return;
}
group('$VectorValue', () {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<DocumentReference<Map<String, dynamic>>> initializeTest(
String path,
) async {
String prefixedPath = 'flutter-tests/$path';
await firestore.doc(prefixedPath).delete();
return firestore.doc(prefixedPath);
}
test('sets a $VectorValue & returns one', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value');
await doc.set({
'foo': const VectorValue([10.0, -10.0]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
VectorValue vectorValue = snapshot.data()!['foo'];
expect(vectorValue, isA<VectorValue>());
expect(vectorValue.toArray(), equals([10.0, -10.0]));
});
test('updates a $VectorValue & returns', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-update');
await doc.set({
'foo': const VectorValue([10.0, -10.0]),
});
await doc.update({
'foo': const VectorValue([-10.0, 10.0]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
VectorValue vectorValue = snapshot.data()!['foo'];
expect(vectorValue, isA<VectorValue>());
expect(vectorValue.toArray(), equals([-10.0, 10.0]));
});
test('handles empty vector', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-empty');
try {
await doc.set({
'foo': const VectorValue([]),
});
fail('Should have thrown an exception');
} catch (e) {
expect(e, isA<FirebaseException>());
expect(
(e as FirebaseException).code.contains('invalid-argument'),
isTrue,
);
}
});
test('handles single dimension vector', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-single');
await doc.set({
'foo': const VectorValue([42.0]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
VectorValue vectorValue = snapshot.data()!['foo'];
expect(vectorValue, isA<VectorValue>());
expect(vectorValue.toArray(), equals([42.0]));
});
test('handles maximum dimensions vector', () async {
List<double> maxDimensions = List.filled(2048, 1);
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-max-dimensions');
await doc.set({
'foo': VectorValue(maxDimensions),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
VectorValue vectorValue = snapshot.data()!['foo'];
expect(vectorValue, isA<VectorValue>());
expect(vectorValue.toArray(), equals(maxDimensions));
});
test('handles maximum dimensions + 1 vector', () async {
List<double> maxPlusOneDimensions = List.filled(2049, 1);
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-max-plus-one');
try {
await doc.set({
'foo': VectorValue(maxPlusOneDimensions),
});
fail('Should have thrown an exception');
} catch (e) {
expect(e, isA<FirebaseException>());
expect(
(e as FirebaseException).code.contains('invalid-argument'),
isTrue,
);
}
});
test('handles very large values in vector', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-large-values');
await doc.set({
'foo': const VectorValue([1e10, -1e10]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
VectorValue vectorValue = snapshot.data()!['foo'];
expect(vectorValue, isA<VectorValue>());
expect(vectorValue.toArray(), equals([1e10, -1e10]));
});
test('handles floats in vector', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-floats');
await doc.set({
'foo': const VectorValue([3.14, 2.718]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
VectorValue vectorValue = snapshot.data()!['foo'];
expect(vectorValue, isA<VectorValue>());
expect(vectorValue.toArray(), equals([3.14, 2.718]));
});
test('handles negative values in vector', () async {
DocumentReference<Map<String, dynamic>> doc =
await initializeTest('vector-value-negative');
await doc.set({
'foo': const VectorValue([-42.0, -100.0]),
});
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
VectorValue vectorValue = snapshot.data()!['foo'];
expect(vectorValue, isA<VectorValue>());
expect(vectorValue.toArray(), equals([-42.0, -100.0]));
});
});
}

View file

@ -0,0 +1,194 @@
// Copyright 2020, 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.
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
// Run only on web for demonstrating snapshot listener clean up in debug mode does not clean up the listeners incorrectly.
// See: https://github.com/firebase/flutterfire/issues/13019
void runWebSnapshotListenersTests() {
group('Web snapshot listeners', () {
late FirebaseFirestore firestore;
late CollectionReference<Map<String, dynamic>> collection;
late DocumentReference<Map<String, dynamic>> document;
late DocumentReference<Map<String, dynamic>> document2;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
collection = firestore
.collection('flutter-tests/web-snapshot-listeners/query-tests');
document = collection.doc('doc1');
document2 = collection.doc('doc1');
await Future.wait([
document.set({'foo': 1}),
collection.add({'foo': 2}),
collection.add({'foo': 3}),
]);
});
test(
'document snapshot listeners in debug',
() async {
Completer<bool> completer = Completer<bool>();
Completer<bool> completer2 = Completer<bool>();
Completer<bool> completer3 = Completer<bool>();
document.snapshots().listen((snapshot) {
if (completer.isCompleted) {
return;
}
completer.complete(true);
});
document.snapshots().listen((snapshot) {
if (completer2.isCompleted) {
return;
}
completer2.complete(true);
});
document.snapshots().listen((snapshot) {
if (completer3.isCompleted) {
return;
}
completer3.complete(true);
});
final one = await completer.future;
final two = await completer2.future;
final three = await completer3.future;
expect(one, true);
expect(two, true);
expect(three, true);
},
skip: !kIsWeb,
);
test(
'document snapshot listeners with different doc refs in debug',
() async {
Completer<bool> completer = Completer<bool>();
Completer<bool> completer2 = Completer<bool>();
Completer<bool> completer3 = Completer<bool>();
Completer<bool> completer4 = Completer<bool>();
document.snapshots().listen((snapshot) {
if (completer.isCompleted) {
return;
}
completer.complete(true);
});
document.snapshots().listen((snapshot) {
if (completer2.isCompleted) {
return;
}
completer2.complete(true);
});
document2.snapshots().listen((snapshot) {
if (completer3.isCompleted) {
return;
}
completer3.complete(true);
});
document2.snapshots().listen((snapshot) {
if (completer4.isCompleted) {
return;
}
completer4.complete(true);
});
final one = await completer.future;
final two = await completer2.future;
final three = await completer3.future;
final four = await completer4.future;
expect(one, true);
expect(two, true);
expect(three, true);
expect(four, true);
},
skip: !kIsWeb,
);
test(
'query snapshot listeners in debug',
() async {
Completer<bool> completer = Completer<bool>();
Completer<bool> completer2 = Completer<bool>();
Completer<bool> completer3 = Completer<bool>();
collection.snapshots().listen((snapshot) {
if (completer.isCompleted) {
return;
}
completer.complete(true);
});
collection.snapshots().listen((snapshot) {
if (completer2.isCompleted) {
return;
}
completer2.complete(true);
});
collection.snapshots().listen((snapshot) {
if (completer3.isCompleted) {
return;
}
completer3.complete(true);
});
final one = await completer.future;
final two = await completer2.future;
final three = await completer3.future;
expect(one, true);
expect(two, true);
expect(three, true);
},
skip: !kIsWeb,
);
test(
'snapshot in sync listeners in debug',
() async {
Completer<bool> completer = Completer<bool>();
Completer<bool> completer2 = Completer<bool>();
Completer<bool> completer3 = Completer<bool>();
firestore.snapshotsInSync().listen((snapshot) {
if (completer.isCompleted) {
return;
}
completer.complete(true);
});
firestore.snapshotsInSync().listen((snapshot) {
if (completer2.isCompleted) {
return;
}
completer2.complete(true);
});
firestore.snapshotsInSync().listen((snapshot) {
if (completer3.isCompleted) {
return;
}
completer3.complete(true);
});
final one = await completer.future;
final two = await completer2.future;
final three = await completer3.future;
expect(one, true);
expect(two, true);
expect(three, true);
},
skip: !kIsWeb,
);
});
}

View file

@ -0,0 +1,309 @@
// Copyright 2020, 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.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
void runWriteBatchTests() {
group('$WriteBatch', () {
late FirebaseFirestore firestore;
setUpAll(() async {
firestore = FirebaseFirestore.instance;
});
Future<CollectionReference<Map<String, dynamic>>> initializeTest(
String id,
) async {
CollectionReference<Map<String, dynamic>> collection =
firestore.collection('flutter-tests/$id/query-tests');
QuerySnapshot<Map<String, dynamic>> snapshot = await collection.get();
await Future.forEach(snapshot.docs, (
DocumentSnapshot<Map<String, dynamic>> documentSnapshot,
) {
return documentSnapshot.reference.delete();
});
return collection;
}
test('works with withConverter', () async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('with-converter-batch');
WriteBatch batch = firestore.batch();
DocumentReference<int> doc = collection.doc('doc1').withConverter(
fromFirestore: (snapshot, options) {
return snapshot.data()!['value'] as int;
},
toFirestore: (value, options) => {'value': value},
);
var snapshot = await doc.get();
expect(snapshot.exists, false);
batch.set<int>(doc, 42);
await batch.commit();
snapshot = await doc.get();
expect(snapshot.exists, true);
expect(snapshot.data(), 42);
batch = firestore.batch();
batch.update(doc, {'value': 21});
await batch.commit();
snapshot = await doc.get();
expect(snapshot.exists, true);
expect(snapshot.data(), 21);
batch = firestore.batch();
batch.delete(doc);
await batch.commit();
snapshot = await doc.get();
expect(snapshot.exists, false);
});
test('updates with typed data through withConverter', () async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('with-converter-batch-update');
WriteBatch batch = firestore.batch();
DocumentReference<int> doc = collection.doc('doc1').withConverter(
fromFirestore: (snapshot, options) {
return snapshot.data()!['value'] as int;
},
toFirestore: (value, options) => {'value': value},
);
await doc.set(42);
batch.update<int>(doc, 21);
await batch.commit();
DocumentSnapshot<int> snapshot = await doc.get();
expect(snapshot.exists, isTrue);
expect(snapshot.data(), 21);
});
test('updates complex typed data through withConverter', () async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('with-converter-complex-batch-update');
DocumentReference<Map<String, dynamic>> rawDoc = collection.doc('doc1');
DocumentReference<_WriteBatchProfile> doc = rawDoc.withConverter(
fromFirestore: (snapshot, options) {
return _WriteBatchProfile.fromFirestore(snapshot.data()!);
},
toFirestore: (value, options) => value.toFirestore(),
);
await rawDoc.set({
'existing': 'preserved',
'name': 'before',
});
WriteBatch batch = firestore.batch();
batch.update<_WriteBatchProfile>(
doc,
_WriteBatchProfile(
name: 'Ada',
score: 42,
address: _WriteBatchAddress(city: 'London', postcode: 'NW1'),
tags: ['admin', 'tester'],
preferences: {
'email': true,
'theme': 'dark',
},
nickname: null,
),
);
await batch.commit();
DocumentSnapshot<Map<String, dynamic>> rawSnapshot = await rawDoc.get();
expect(rawSnapshot.data(), {
'existing': 'preserved',
'name': 'Ada',
'score': 42,
'address': {
'city': 'London',
'postcode': 'NW1',
},
'tags': ['admin', 'tester'],
'preferences': {
'email': true,
'theme': 'dark',
},
'nickname': null,
});
DocumentSnapshot<_WriteBatchProfile> snapshot = await doc.get();
_WriteBatchProfile profile = snapshot.data()!;
expect(profile.name, 'Ada');
expect(profile.score, 42);
expect(profile.address.city, 'London');
expect(profile.address.postcode, 'NW1');
expect(profile.tags, ['admin', 'tester']);
expect(profile.preferences, {
'email': true,
'theme': 'dark',
});
expect(profile.nickname, isNull);
});
test('should update a document using FieldPath keys', () async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('write-batch-field-path');
DocumentReference<Map<String, dynamic>> doc = collection.doc('doc1');
await doc.set({
'nested': {'field': 'old_value'},
'top': 'value',
});
WriteBatch batch = firestore.batch();
batch.update(doc, {
FieldPath(const ['nested', 'field']): 'new_value',
});
await batch.commit();
DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get();
expect(snapshot.exists, isTrue);
expect(snapshot.data()!['nested']['field'], equals('new_value'));
expect(snapshot.data()!['top'], equals('value'));
});
test('performs batch operations', () async {
CollectionReference<Map<String, dynamic>> collection =
await initializeTest('write-batch-ops');
WriteBatch batch = firestore.batch();
DocumentReference<Map<String, dynamic>> doc1 =
collection.doc('doc1'); // delete
DocumentReference<Map<String, dynamic>> doc2 =
collection.doc('doc2'); // set
DocumentReference<Map<String, dynamic>> doc3 =
collection.doc('doc3'); // update
DocumentReference<Map<String, dynamic>> doc4 =
collection.doc('doc4'); // update w/ merge
DocumentReference<Map<String, dynamic>> doc5 =
collection.doc('doc5'); // update w/ mergeFields
await Future.wait([
doc1.set({'foo': 'bar'}),
doc2.set({'foo': 'bar'}),
doc3.set({'foo': 'bar', 'bar': 'baz'}),
doc4.set({'foo': 'bar'}),
doc5.set({'foo': 'bar', 'bar': 'baz'}),
]);
batch.delete(doc1);
batch.set(doc2, <String, dynamic>{'bar': 'baz'});
batch.update(doc3, <String, dynamic>{'bar': 'ben'});
batch.set(doc4, <String, dynamic>{'bar': 'ben'}, SetOptions(merge: true));
batch.set(
doc5,
<String, dynamic>{'bar': 'ben'},
SetOptions(mergeFields: ['bar']),
);
await batch.commit();
QuerySnapshot<Map<String, dynamic>> snapshot = await collection.get();
expect(snapshot.docs.length, equals(4));
expect(snapshot.docs.where((doc) => doc.id == 'doc1').isEmpty, isTrue);
expect(
snapshot.docs.firstWhere((doc) => doc.id == 'doc2').data(),
equals(<String, dynamic>{'bar': 'baz'}),
);
expect(
snapshot.docs.firstWhere((doc) => doc.id == 'doc3').data(),
equals(<String, dynamic>{'foo': 'bar', 'bar': 'ben'}),
);
expect(
snapshot.docs.firstWhere((doc) => doc.id == 'doc4').data(),
equals(<String, dynamic>{'foo': 'bar', 'bar': 'ben'}),
);
expect(
snapshot.docs.firstWhere((doc) => doc.id == 'doc5').data(),
equals(<String, dynamic>{'foo': 'bar', 'bar': 'ben'}),
);
});
});
}
class _WriteBatchProfile {
_WriteBatchProfile({
required this.name,
required this.score,
required this.address,
required this.tags,
required this.preferences,
required this.nickname,
});
factory _WriteBatchProfile.fromFirestore(Map<String, dynamic> data) {
return _WriteBatchProfile(
name: data['name'] as String,
score: data['score'] as int,
address: _WriteBatchAddress.fromFirestore(
data['address'] as Map<String, dynamic>,
),
tags: (data['tags'] as List<dynamic>).cast<String>(),
preferences: Map<String, Object?>.from(data['preferences'] as Map),
nickname: data['nickname'] as String?,
);
}
final String name;
final int score;
final _WriteBatchAddress address;
final List<String> tags;
final Map<String, Object?> preferences;
final String? nickname;
Map<String, Object?> toFirestore() {
return {
'name': name,
'score': score,
'address': address.toFirestore(),
'tags': tags,
'preferences': preferences,
'nickname': nickname,
};
}
}
class _WriteBatchAddress {
_WriteBatchAddress({
required this.city,
required this.postcode,
});
factory _WriteBatchAddress.fromFirestore(Map<String, dynamic> data) {
return _WriteBatchAddress(
city: data['city'] as String,
postcode: data['postcode'] as String,
);
}
final String city;
final String postcode;
Map<String, Object?> toFirestore() {
return {
'city': city,
'postcode': postcode,
};
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View file

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View file

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View file

@ -0,0 +1,64 @@
# Uncomment this line to define a global platform for your project
platform :ios, '15.0'
require 'yaml'
pubspec = YAML.load_file(File.join('..', File.join('..', 'pubspec.yaml')))
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
if defined?($FirebaseSDKVersion)
Pod::UI.puts "#{pubspec['name']}: Using user specified Firebase SDK version for FirebaseFirestore framework: '#{$FirebaseSDKVersion}'"
firebase_sdk_version = $FirebaseSDKVersion
else
firebase_core_script = File.join(File.expand_path('..', File.expand_path('..', File.expand_path('..', File.expand_path('..', File.dirname(__FILE__))))), 'firebase_core/firebase_core/ios/firebase_sdk_version.rb')
if File.exist?(firebase_core_script)
require firebase_core_script
firebase_sdk_version = firebase_sdk_version!
Pod::UI.puts "#{pubspec['name']}: Using Firebase SDK version '#{firebase_sdk_version}' defined in 'firebase_core for FirebaseFirestore framework'"
else
raise "Error - unable to locate firebase_ios_sdk.rb script in firebase_core, and no FirebaseSDKVersion specified"
end
end
pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => "#{firebase_sdk_version}"
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
end
flutter_additional_ios_build_settings(target)
end
end

View file

@ -0,0 +1,598 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
697953CD5D47B316E75ECD82 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A2D71CA981BE46B8B7F14013 /* GoogleService-Info.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A83F699FC746D5736A6B5780 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8EC7192C60686BF1D599360 /* Pods_Runner.framework */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
00AB97D99E5ECF11DF47B0E1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
80F1DA47CF78E61FED2F1DC2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
82EEF9818494FDE3E5DA52EA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A2D71CA981BE46B8B7F14013 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
E8EC7192C60686BF1D599360 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
A83F699FC746D5736A6B5780 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
CAD6F5F5D2CB16B87B6F4AE4 /* Pods */,
D113A4D5E6EE00522247FBF7 /* Frameworks */,
A2D71CA981BE46B8B7F14013 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
CAD6F5F5D2CB16B87B6F4AE4 /* Pods */ = {
isa = PBXGroup;
children = (
00AB97D99E5ECF11DF47B0E1 /* Pods-Runner.debug.xcconfig */,
80F1DA47CF78E61FED2F1DC2 /* Pods-Runner.release.xcconfig */,
82EEF9818494FDE3E5DA52EA /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
D113A4D5E6EE00522247FBF7 /* Frameworks */ = {
isa = PBXGroup;
children = (
E8EC7192C60686BF1D599360 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
C2BA042A3BD6EB9AE6CA3D9C /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
279EA8199A12C4F77765546D /* [CP] Embed Pods Frameworks */,
11D71EC060D78E89744DA92E /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
697953CD5D47B316E75ECD82 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
11D71EC060D78E89744DA92E /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
279EA8199A12C4F77765546D /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
C2BA042A3BD6EB9AE6CA3D9C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = YYX2P3XVJ7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.firestore.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = YYX2P3XVJ7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.firestore.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = YYX2P3XVJ7;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.firestore.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Some files were not shown because too many files have changed in this diff Show more