Foto-Flow mit KI-Erkennung: Kamera, Cloud Function identifyPlant, Storage für Fotos
- Cloud Function (europe-west3): PlantNet-Erkennung mit Claude-Fallback, deutsches Pflegeprofil von Claude, Keys als Secrets
- Pflanzen-Formular: Foto aufnehmen/wählen, Felder werden automatisch vorbefüllt
- Fotos in Storage unter households/{id}/plant-photos, Anzeige in Liste und Profil
- storage.rules mit Haushalts-Prinzip, iOS-Berechtigungstexte für Kamera/Fotos
- Doku Schritt 5: Storage aktivieren, PlantNet-Key, Secrets setzen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e999da5d48
commit
41e31bed88
539 changed files with 4159 additions and 105410 deletions
|
|
@ -1,2 +1,6 @@
|
|||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
|
|
|||
|
|
@ -89,11 +89,30 @@ Die Regeln in `firestore.rules` setzen das Haushalts-Prinzip durch:
|
|||
|
||||
Wichtig zu verstehen: Die App spricht direkt mit Firestore — die Regeln laufen **auf Googles Servern** und sind die eigentliche Zugriffskontrolle. Selbst eine manipulierte App könnte fremde Haushalte nicht lesen.
|
||||
|
||||
## Schritt 5 und folgende (kommen mit den nächsten Blöcken)
|
||||
## Schritt 5: Foto-Erkennung — Storage, Functions und API-Keys
|
||||
|
||||
- **Storage** — Pflanzen-Fotos speichern (kommt mit dem Foto-/KI-Block).
|
||||
- **Cloud Functions** — PlantNet-/Claude-Anbindung und die geplante Erinnerungs-Function (läuft alle 15 Minuten).
|
||||
- **FCM** — Push-Einrichtung inkl. APNs-Schlüssel für iOS (aus dem Apple Developer Account).
|
||||
Der Code ist fertig: Beim Anlegen einer Pflanze kannst du fotografieren, die Cloud Function `identifyPlant` bestimmt die Art (PlantNet, bei Unsicherheit Claude als Zweitmeinung) und Claude erstellt das deutsche Pflegeprofil samt Intervall-Vorschlägen. Das Foto landet in Firebase Storage im Haushalts-Ordner.
|
||||
|
||||
**Was du einmalig tun musst:**
|
||||
|
||||
1. **Storage aktivieren:** Firebase Console → *Build → Storage* → „Jetzt starten" → Standort **`eur3`** (falls gefragt) → Produktionsmodus. (Die Zugriffsregeln kommen danach per Deploy aus `storage.rules` — gleiches Haushalts-Prinzip wie bei Firestore.)
|
||||
2. **PlantNet-API-Key holen (kostenlos):** Auf <https://my.plantnet.org> registrieren → unter *Settings/API* den Key kopieren. Free-Tier: 500 Erkennungen/Tag — mehr als genug.
|
||||
3. **Beide API-Keys als Secrets hinterlegen** (im Terminal, jeweils Key einfügen, Enter):
|
||||
|
||||
```bash
|
||||
firebase functions:secrets:set PLANTNET_API_KEY --project leaf-it-to-me-app
|
||||
firebase functions:secrets:set ANTHROPIC_API_KEY --project leaf-it-to-me-app
|
||||
```
|
||||
|
||||
Die Keys liegen damit im **Google Secret Manager** — verschlüsselt, nur die Cloud Function kann sie lesen, sie tauchen nie im Code, im Git oder in der App auf.
|
||||
|
||||
**Danach (macht Claude):** `firebase deploy --only functions,storage` — deployt die Function nach `europe-west3` (Frankfurt) und die Storage-Regeln.
|
||||
|
||||
**Kosten:** PlantNet kostenlos; Claude ca. 1–3 Cent pro Foto (Erkennungs-Fallback + Pflegeprofil); Functions/Storage bei eurer Nutzung im Gratis-Kontingent. Der Budget-Alarm aus Schritt 2 überwacht alles.
|
||||
|
||||
## Schritt 6 und folgende (kommen mit den nächsten Blöcken)
|
||||
|
||||
- **FCM** — Push-Einrichtung inkl. APNs-Schlüssel für iOS (aus dem Apple Developer Account) und die geplante Erinnerungs-Function (läuft alle 15 Minuten).
|
||||
- **Google-/Apple-Login** — zusätzlich zu E-Mail/Passwort.
|
||||
|
||||
## Begriffe kurz erklärt
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@
|
|||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
},
|
||||
"storage": {
|
||||
"rules": "storage.rules"
|
||||
},
|
||||
"functions": {
|
||||
"source": "functions",
|
||||
"predeploy": ["npm --prefix functions run build"]
|
||||
},
|
||||
"flutter": {
|
||||
"platforms": {
|
||||
"android": {
|
||||
|
|
|
|||
3
functions/.gitignore
vendored
Normal file
3
functions/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
lib/
|
||||
*.log
|
||||
2976
functions/package-lock.json
generated
Normal file
2976
functions/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
functions/package.json
Normal file
20
functions/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "functions",
|
||||
"description": "Cloud Functions für LeafItToMe (KI-Aufrufe, später Push-Erinnerungen)",
|
||||
"engines": {
|
||||
"node": "22"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"firebase-admin": "^13.0.0",
|
||||
"firebase-functions": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
208
functions/src/index.ts
Normal file
208
functions/src/index.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import {onCall, HttpsError} from "firebase-functions/v2/https";
|
||||
import {defineSecret} from "firebase-functions/params";
|
||||
|
||||
// Secrets liegen im Google Secret Manager (firebase functions:secrets:set),
|
||||
// niemals im Code oder in der App.
|
||||
const plantNetApiKey = defineSecret("PLANTNET_API_KEY");
|
||||
const anthropicApiKey = defineSecret("ANTHROPIC_API_KEY");
|
||||
|
||||
const ANTHROPIC_MODEL = "claude-sonnet-5";
|
||||
|
||||
// Unterhalb dieser PlantNet-Konfidenz holen wir eine Zweitmeinung von Claude.
|
||||
const PLANTNET_CONFIDENCE_THRESHOLD = 0.3;
|
||||
|
||||
interface PlantNetResult {
|
||||
score: number;
|
||||
scientificName: string;
|
||||
commonNames: string[];
|
||||
}
|
||||
|
||||
interface CareProfile {
|
||||
germanName: string;
|
||||
description: string;
|
||||
careNotes: string;
|
||||
wateringIntervalDays: number;
|
||||
fertilizingIntervalDays: number;
|
||||
}
|
||||
|
||||
/** Identifikation über die kostenlose PlantNet-API. */
|
||||
async function identifyWithPlantNet(
|
||||
imageBase64: string,
|
||||
apiKey: string
|
||||
): Promise<PlantNetResult | null> {
|
||||
const form = new FormData();
|
||||
const bytes = Buffer.from(imageBase64, "base64");
|
||||
form.append("images", new Blob([bytes], {type: "image/jpeg"}), "plant.jpg");
|
||||
form.append("organs", "auto");
|
||||
|
||||
const response = await fetch(
|
||||
`https://my-api.plantnet.org/v2/identify/all?api-key=${apiKey}`,
|
||||
{method: "POST", body: form}
|
||||
);
|
||||
if (!response.ok) {
|
||||
console.error("PlantNet-Fehler", response.status, await response.text());
|
||||
return null;
|
||||
}
|
||||
const json = (await response.json()) as {
|
||||
results?: {
|
||||
score: number;
|
||||
species: {
|
||||
scientificNameWithoutAuthor: string;
|
||||
commonNames?: string[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
const best = json.results?.[0];
|
||||
if (!best) return null;
|
||||
return {
|
||||
score: best.score,
|
||||
scientificName: best.species.scientificNameWithoutAuthor,
|
||||
commonNames: best.species.commonNames ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/** Ein Aufruf der Claude-API mit Bild und Prompt, erwartet reines JSON zurück. */
|
||||
async function askClaude<T>(
|
||||
imageBase64: string,
|
||||
prompt: string,
|
||||
apiKey: string
|
||||
): Promise<T> {
|
||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: ANTHROPIC_MODEL,
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/jpeg",
|
||||
data: imageBase64,
|
||||
},
|
||||
},
|
||||
{type: "text", text: prompt},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("Anthropic-Fehler", response.status, await response.text());
|
||||
throw new HttpsError("internal", "KI-Anfrage fehlgeschlagen.");
|
||||
}
|
||||
const json = (await response.json()) as {
|
||||
content: {type: string; text?: string}[];
|
||||
};
|
||||
const text = json.content.find((c) => c.type === "text")?.text ?? "";
|
||||
// Eventuelle Markdown-Zäune entfernen, dann JSON parsen.
|
||||
const cleaned = text.replace(/```json|```/g, "").trim();
|
||||
try {
|
||||
return JSON.parse(cleaned) as T;
|
||||
} catch {
|
||||
console.error("Claude lieferte kein JSON:", text);
|
||||
throw new HttpsError("internal", "KI-Antwort war nicht lesbar.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pflanze per Foto erkennen und ein deutsches Pflegeprofil erstellen.
|
||||
*
|
||||
* Ablauf (Hybrid-Modell):
|
||||
* 1. PlantNet (kostenlos) bestimmt die Art.
|
||||
* 2. Bei zu geringer Konfidenz bestimmt Claude die Art als Fallback.
|
||||
* 3. Claude erstellt Beschreibung, Pflegehinweise und Intervall-Vorschläge.
|
||||
*/
|
||||
export const identifyPlant = onCall(
|
||||
{
|
||||
region: "europe-west3",
|
||||
secrets: [plantNetApiKey, anthropicApiKey],
|
||||
memory: "512MiB",
|
||||
timeoutSeconds: 120,
|
||||
maxInstances: 5,
|
||||
},
|
||||
async (request) => {
|
||||
if (!request.auth) {
|
||||
throw new HttpsError("unauthenticated", "Anmeldung erforderlich.");
|
||||
}
|
||||
const imageBase64 = request.data?.imageBase64;
|
||||
if (typeof imageBase64 !== "string" || imageBase64.length === 0) {
|
||||
throw new HttpsError("invalid-argument", "imageBase64 fehlt.");
|
||||
}
|
||||
if (imageBase64.length > 8_000_000) {
|
||||
throw new HttpsError("invalid-argument", "Bild ist zu groß.");
|
||||
}
|
||||
|
||||
// 1) Arterkennung: PlantNet zuerst, Claude als Fallback.
|
||||
let scientificName: string | null = null;
|
||||
let confidence = 0;
|
||||
let source = "plantnet";
|
||||
|
||||
const plantNet = await identifyWithPlantNet(
|
||||
imageBase64,
|
||||
plantNetApiKey.value()
|
||||
).catch((error) => {
|
||||
console.error("PlantNet nicht erreichbar:", error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (plantNet && plantNet.score >= PLANTNET_CONFIDENCE_THRESHOLD) {
|
||||
scientificName = plantNet.scientificName;
|
||||
confidence = plantNet.score;
|
||||
} else {
|
||||
source = "claude";
|
||||
const claudeId = await askClaude<{scientificName: string; confidence: number}>(
|
||||
imageBase64,
|
||||
"Bestimme die Pflanze auf dem Foto. Antworte NUR mit JSON im Format " +
|
||||
'{"scientificName": "<botanischer Name>", "confidence": <0..1>}. ' +
|
||||
"Wenn keine Pflanze erkennbar ist, setze scientificName auf leeren String.",
|
||||
anthropicApiKey.value()
|
||||
);
|
||||
scientificName = claudeId.scientificName || plantNet?.scientificName || null;
|
||||
confidence = claudeId.scientificName
|
||||
? claudeId.confidence
|
||||
: plantNet?.score ?? 0;
|
||||
}
|
||||
|
||||
if (!scientificName) {
|
||||
throw new HttpsError(
|
||||
"not-found",
|
||||
"Auf dem Foto wurde keine Pflanze erkannt."
|
||||
);
|
||||
}
|
||||
|
||||
// 2) Pflegeprofil auf Deutsch, mit dem Foto als Kontext (Zustand, Topfgröße).
|
||||
const profile = await askClaude<CareProfile>(
|
||||
imageBase64,
|
||||
`Die Pflanze auf dem Foto wurde als "${scientificName}" bestimmt. ` +
|
||||
"Erstelle ein Pflegeprofil auf Deutsch, in einfacher, freundlicher Sprache " +
|
||||
"(auch für ältere Menschen gut verständlich, keine Fachbegriffe ohne Erklärung). " +
|
||||
"Antworte NUR mit JSON im Format " +
|
||||
'{"germanName": "<gebräuchlicher deutscher Name>", ' +
|
||||
'"description": "<2-3 Sätze über die Pflanze>", ' +
|
||||
'"careNotes": "<2-3 Sätze konkrete Pflegetipps zu Gießen, Licht, Dünger>", ' +
|
||||
'"wateringIntervalDays": <Zahl>, "fertilizingIntervalDays": <Zahl>}. ' +
|
||||
"Die Intervalle sind Richtwerte für eine Zimmerpflanze in der aktuellen Jahreszeit.",
|
||||
anthropicApiKey.value()
|
||||
);
|
||||
|
||||
return {
|
||||
scientificName,
|
||||
confidence,
|
||||
source,
|
||||
germanName: profile.germanName,
|
||||
description: profile.description,
|
||||
careNotes: profile.careNotes,
|
||||
wateringIntervalDays: profile.wateringIntervalDays,
|
||||
fertilizingIntervalDays: profile.fertilizingIntervalDays,
|
||||
};
|
||||
}
|
||||
);
|
||||
14
functions/tsconfig.json
Normal file
14
functions/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2022",
|
||||
"lib": ["es2022"],
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -20,7 +20,5 @@
|
|||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
1435
ios/Podfile.lock
1435
ios/Podfile.lock
File diff suppressed because it is too large
Load diff
|
|
@ -13,6 +13,7 @@
|
|||
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 */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
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 */; };
|
||||
|
|
@ -58,6 +59,7 @@
|
|||
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>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.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>"; };
|
||||
|
|
@ -82,6 +84,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
39FCA66956D66B4392ABB993 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -114,6 +117,7 @@
|
|||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
|
|
@ -201,13 +205,15 @@
|
|||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
2926D2B54AA48C072380AFB2 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
|
|
@ -241,6 +247,9 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
|
@ -274,23 +283,6 @@
|
|||
/* 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;
|
||||
|
|
@ -730,6 +722,20 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "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 */;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
|
||||
"version" : "1.2024072200.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902",
|
||||
"version" : "11.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "firebase-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
|
||||
"version" : "12.15.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
|
||||
"version" : "3.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleappmeasurement",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
|
||||
"version" : "12.15.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googledatatransport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||
"version" : "10.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
|
||||
"version" : "8.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "grpc-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
|
||||
"version" : "1.69.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gtm-session-fetcher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||
"version" : "5.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||
"version" : "101.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "leveldb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/leveldb.git",
|
||||
"state" : {
|
||||
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||
"version" : "1.22.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nanopb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
|
||||
"version" : "2.30910.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
|
|
@ -5,6 +5,24 @@
|
|||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
|||
122
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
122
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
|
||||
"version" : "1.2024072200.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902",
|
||||
"version" : "11.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "firebase-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
|
||||
"version" : "12.15.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
|
||||
"version" : "3.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleappmeasurement",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
|
||||
"version" : "12.15.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googledatatransport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||
"version" : "10.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
|
||||
"version" : "8.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "grpc-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
|
||||
"version" : "1.69.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gtm-session-fetcher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||
"version" : "5.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||
"version" : "101.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "leveldb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/leveldb.git",
|
||||
"state" : {
|
||||
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||
"version" : "1.22.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nanopb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
|
||||
"version" : "2.30910.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
|
|
@ -2,12 +2,15 @@ import Flutter
|
|||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>LeafItToMe nutzt die Kamera, um Pflanzen zu fotografieren und automatisch zu erkennen.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>LeafItToMe greift auf deine Fotos zu, um vorhandene Pflanzenbilder auszuwählen.</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
|
|
@ -24,6 +30,29 @@
|
|||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
|
@ -41,9 +70,5 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
4f1fe94d1e0ea767694440acc2a152ac
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,26 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
[<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)
|
||||
|
||||
[](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).
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
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")
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
ext {
|
||||
compileSdk=34
|
||||
minSdk=23
|
||||
targetSdk=34
|
||||
javaVersion = JavaVersion.toVersion(17)
|
||||
androidGradlePluginVersion = '8.3.0'
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,558 +0,0 @@
|
|||
// 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));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,21 +0,0 @@
|
|||
// 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// 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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,808 +0,0 @@
|
|||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,336 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,420 +0,0 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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}\""
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
name: Cloud Firestore Example
|
||||
mode: flutter
|
||||
files:
|
||||
- name: lib/main.dart
|
||||
|
|
@ -1,431 +0,0 @@
|
|||
// 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',
|
||||
);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# firestore_example
|
||||
|
||||
Demonstrates how to use the firestore plugin.
|
||||
|
||||
## Getting Started
|
||||
|
||||
For help getting started with Flutter, view our online
|
||||
[documentation](https://flutter.dev/).
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
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 = "../.."
|
||||
}
|
||||
|
|
@ -1,615 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package io.flutter.plugins.firebase.firestore.example
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?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.
|
Before Width: | Height: | Size: 544 B |
Binary file not shown.
|
Before Width: | Height: | Size: 442 B |
Binary file not shown.
|
Before Width: | Height: | Size: 721 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,18 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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"
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"flutter":{"platforms":{"android":{"default":{"projectId":"flutterfire-e2e-tests","appId":"1:406099696497:android:175ea7a64b2faf5e3574d0","fileOutput":"android/app/google-services.json"}}}}}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
// 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
// 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,728 +0,0 @@
|
|||
// 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
// 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>());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
// 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',
|
||||
);
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// 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',
|
||||
);
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
// 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,383 +0,0 @@
|
|||
// 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
// 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()!;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,84 +0,0 @@
|
|||
// 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);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
// 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,558 +0,0 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
// 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]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
// 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
// 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,598 +0,0 @@
|
|||
// !$*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 */;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
<?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 "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<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>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue