diff --git a/.github/workflows/all_plugins.yaml b/.github/workflows/all_plugins.yaml index 3dc43e4eb790..ac5acce22e61 100644 --- a/.github/workflows/all_plugins.yaml +++ b/.github/workflows/all_plugins.yaml @@ -193,7 +193,7 @@ jobs: # check-license-header) - run: go install github.com/google/addlicense@latest - name: Install Dart - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c - name: Install Melos uses: bluefireteam/melos-action@c7dcb921b23cc520cace360b95d02b37bf09cdaa with: diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index 02dca1cc1de2..54bb34693732 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -110,7 +110,7 @@ jobs: emulator-build: 14214601 working-directory: ${{ matrix.working_directory }} script: | - flutter test integration_test/e2e_test.dart --ignore-timeouts --dart-define=CI=true -d emulator-5554 + flutter test integration_test/e2e_test.dart --timeout 10x --dart-define=CI=true -d emulator-5554 - name: Ensure Appium is shut down # Required because of below issue where emulator failing to shut down properly causes tests to fail # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 diff --git a/.github/workflows/e2e_tests_fdc.yaml b/.github/workflows/e2e_tests_fdc.yaml index 2d29c3512e3a..998ae091362d 100644 --- a/.github/workflows/e2e_tests_fdc.yaml +++ b/.github/workflows/e2e_tests_fdc.yaml @@ -21,6 +21,9 @@ on: - '**/example/**' - '**.md' +permissions: + contents: read + jobs: android: runs-on: ubuntu-latest @@ -102,7 +105,7 @@ jobs: ~/.android/adb* key: avd-${{ runner.os }}-${{ env.AVD_API_LEVEL }}-${{ env.AVD_TARGET }}-${{ env.AVD_ARCH }} - name: Start AVD then run E2E tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b with: api-level: ${{ env.AVD_API_LEVEL }} target: ${{ env.AVD_TARGET }} @@ -110,7 +113,7 @@ jobs: emulator-build: 14214601 working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' script: | - flutter test integration_test/e2e_test.dart --dart-define=CI=true -d emulator-5554 + flutter test integration_test/e2e_test.dart --dart-define=CI=true --timeout 10x -d emulator-5554 - name: Save Android Emulator Cache # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. if: github.ref == 'refs/heads/main' @@ -153,7 +156,7 @@ jobs: java-version: '21' - name: Setup PostgreSQL for Linux/macOS/Windows uses: ikalnytskyi/action-setup-postgres@v7 - - uses: hendrikmuhs/ccache-action@c92f40bee50034e84c763e33b317c77adaa81c92 + - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 name: Xcode Compile Cache with: key: xcode-cache-${{ runner.os }} @@ -213,19 +216,27 @@ jobs: unset PGSERVICEFILE firebase experiments:enable dataconnect ./start-firebase-emulator.sh + - uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 + id: simulator + with: + model: "iPhone 16" - name: 'E2E Tests' working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + env: + SIMULATOR: ${{ steps.simulator.outputs.udid }} run: | - # Boot simulator and wait for System app to be ready. - # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md#installed-simulators - SIMULATOR="iPhone 16" - xcrun simctl bootstatus "$SIMULATOR" -b - xcrun simctl logverbose "$SIMULATOR" enable - # Sleep to allow simulator to settle. - sleep 15 # Uncomment following line to have simulator logs printed out for debugging purposes. # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & - flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true + # The iOS simulator sometimes fails to connect the VM Service, hanging for + # 12 minutes before timing out. Use a 6-minute limit and retry once with + # a simulator reboot. Normal connection takes 30s-5min. + perl -e 'alarm 360; exec @ARGV' -- flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true --timeout 10x || { + echo "First attempt failed or timed out. Rebooting simulator and retrying..." + xcrun simctl shutdown "$SIMULATOR" || true + xcrun simctl boot "$SIMULATOR" + xcrun simctl bootstatus "$SIMULATOR" -b + flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true --timeout 10x + } - name: Save Firestore Emulator Cache # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. if: github.ref == 'refs/heads/main' @@ -325,3 +336,82 @@ jobs: key: ${{ steps.firebase-emulator-cache.outputs.cache-primary-key }} # Must match the restore path exactly path: ~/.cache/firebase/emulators + + web-wasm: + runs-on: macos-latest + timeout-minutes: 15 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a + name: Install Node.js 20 + with: + node-version: '20' + - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b + with: + distribution: 'temurin' + java-version: '21' + - name: Setup PostgreSQL for Linux/macOS/Windows + uses: ikalnytskyi/action-setup-postgres@v7 + - uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff + with: + channel: 'stable' + cache: true + cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" + pub-cache-key: "flutter-pub-:os:-:channel:-:version:-:arch:-:hash:" + - uses: bluefireteam/melos-action@c7dcb921b23cc520cace360b95d02b37bf09cdaa + with: + run-bootstrap: false + melos-version: '5.3.0' + - name: 'Bootstrap package' + run: melos bootstrap --scope "firebase_data_connect*" + - name: 'Install Tools' + run: | + sudo npm i -g firebase-tools + echo "FIREBASE_TOOLS_VERSION=$(npm firebase --version)" >> $GITHUB_ENV + - name: Firebase Emulator Cache + id: firebase-emulator-cache + uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 + continue-on-error: true + with: + # The firebase emulators are pure javascript and java, OS-independent + enableCrossOsArchive: true + # Must match the save path exactly + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-${{ env.FIREBASE_TOOLS_VERSION }} + restore-keys: firebase-emulators-v3 + - name: Start Firebase Emulator + run: | + cd ./packages/firebase_data_connect/firebase_data_connect/example + unset PGSERVICEFILE + firebase experiments:enable dataconnect + ./start-firebase-emulator.sh + - name: 'E2E Tests' + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + # Web devices are not supported for the `flutter test` command yet. As a + # workaround we can use the `flutter drive` command. Tracking issue: + # https://github.com/flutter/flutter/issues/66264 + run: | + chromedriver --port=4444 --trace-buffer-size=100000 & + mv ./web/wasm_index.html ./web/index.html + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --wasm --dart-define=CI=true | tee output.log + # We have to check the output for failed tests matching the string "[E]" + output=$(&1) + EXIT_CODE=$? + echo "$OUTPUT" + if [ $EXIT_CODE -ne 0 ]; then + if echo "$OUTPUT" | grep -q "Some tests failed" || echo "$OUTPUT" | grep -q "test failed"; then + exit 1 + fi + if echo "$OUTPUT" | grep -q "tests passed"; then + echo "All tests passed but flutter test exited with $EXIT_CODE (likely 'Failed to foreground app'). Treating as success." + exit 0 + fi + exit $EXIT_CODE + fi - name: Save Firestore Emulator Cache continue-on-error: true # Branches can read main cache but main cannot read branch cache. Avoid LRU eviction with main-only cache. diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 6b12485276b7..95e86678e12e 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 with: sarif_file: results.sarif diff --git a/.github/workflows/pr_title.yaml b/.github/workflows/pr_title.yaml index 317338e169a3..453064704526 100644 --- a/.github/workflows/pr_title.yaml +++ b/.github/workflows/pr_title.yaml @@ -9,8 +9,10 @@ on: jobs: validate: + permissions: + pull-requests: read runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scripts/functions/package-lock.json b/.github/workflows/scripts/functions/package-lock.json index e48eda2f586d..b0e9cf375ef2 100644 --- a/.github/workflows/scripts/functions/package-lock.json +++ b/.github/workflows/scripts/functions/package-lock.json @@ -163,9 +163,9 @@ } }, "node_modules/@google-cloud/storage": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.16.0.tgz", - "integrity": "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", "optional": true, "dependencies": { "@google-cloud/paginator": "^5.0.0", @@ -174,7 +174,7 @@ "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", @@ -596,34 +596,31 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/cliui": { @@ -730,22 +727,6 @@ } } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -907,38 +888,38 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -972,6 +953,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/express/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -991,10 +986,22 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "optional": true }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "optional": true + }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz", + "integrity": "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==", "funding": [ { "type": "github", @@ -1003,7 +1010,8 @@ ], "optional": true, "dependencies": { - "strnum": "^1.1.1" + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -1121,14 +1129,15 @@ } }, "node_modules/form-data": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", - "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "optional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -1333,17 +1342,6 @@ "node": ">=14.0.0" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1531,30 +1529,30 @@ } }, "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -1576,11 +1574,11 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -1590,9 +1588,9 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -1726,9 +1724,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "engines": { "node": ">= 6.13.0" } @@ -1751,9 +1749,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "engines": { "node": ">= 0.4" }, @@ -2031,36 +2029,71 @@ "node": ">= 0.8" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -2128,9 +2161,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", @@ -2519,9 +2552,9 @@ "optional": true }, "@google-cloud/storage": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.16.0.tgz", - "integrity": "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", "optional": true, "requires": { "@google-cloud/paginator": "^5.0.0", @@ -2530,7 +2563,7 @@ "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", @@ -2886,18 +2919,6 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, - "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - } - }, "call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2907,6 +2928,15 @@ "function-bind": "^1.1.2" } }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2982,16 +3012,6 @@ "ms": "^2.1.3" } }, - "define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3116,38 +3136,38 @@ "optional": true }, "express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -3170,6 +3190,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "requires": { + "side-channel": "^1.1.0" + } } } }, @@ -3189,13 +3217,20 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "optional": true }, + "fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "optional": true + }, "fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz", + "integrity": "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==", "optional": true, "requires": { - "strnum": "^1.1.1" + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" } }, "faye-websocket": { @@ -3282,14 +3317,15 @@ } }, "form-data": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", - "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "optional": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } @@ -3436,14 +3472,6 @@ "jws": "^4.0.0" } }, - "has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "requires": { - "es-define-property": "^1.0.0" - } - }, "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3574,32 +3602,32 @@ }, "dependencies": { "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "requires": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "requires": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } } } }, "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "requires": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -3618,11 +3646,11 @@ } }, "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "requires": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -3632,9 +3660,9 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "lodash.camelcase": { "version": "4.3.0", @@ -3727,9 +3755,9 @@ } }, "node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==" }, "object-assign": { "version": "4.1.1", @@ -3743,9 +3771,9 @@ "optional": true }, "object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "on-finished": { "version": "2.4.1", @@ -3956,33 +3984,53 @@ } } }, - "set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "requires": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - } - }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "requires": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "statuses": { @@ -4035,9 +4083,9 @@ } }, "strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "optional": true }, "stubs": { diff --git a/.github/workflows/scripts/swift-integration.dart b/.github/workflows/scripts/swift-integration.dart index e64390df2bf4..1c4e3f75cdd2 100644 --- a/.github/workflows/scripts/swift-integration.dart +++ b/.github/workflows/scripts/swift-integration.dart @@ -214,7 +214,10 @@ Future updatePackageSwiftForPackage( } // handles forked repositories - final repoSlug = headRepo != baseRepo ? headRepo : baseRepo; + final repoSlug = + (headRepo != null && headRepo.isNotEmpty && headRepo != baseRepo) + ? headRepo + : baseRepo; print('repoSlug: $repoSlug'); print('branch: $branch'); diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 025ae1cdfe5f..1783aa967913 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -53,8 +53,10 @@ jobs: - name: "Install Tools" run: | npm install -g firebase-tools + - name: "Build Windows (Release)" + run: cd tests && flutter build windows --release - name: Start Firebase Emulator and run tests - run: cd ./.github/workflows/scripts && firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../tests && flutter test .\integration_test\e2e_test.dart -d windows" + run: cd ./.github/workflows/scripts && firebase emulators:exec --project flutterfire-e2e-tests "cd ../../../tests && flutter test .\integration_test\e2e_test.dart -d windows --verbose" # We cannot run the tests but we can still try to build the app because of https://github.com/flutter/flutter/issues/79213 windows-firestore: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ff549d5057e..943eacc0fc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,156 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2026-03-02 - [BoM 4.10.0](https://github.com/firebase/flutterfire/blob/main/VERSIONS.md#flutter-bom-4100-2026-03-02) + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`_flutterfire_internals` - `v1.3.67`](#_flutterfire_internals---v1367) + - [`cloud_firestore` - `v6.1.3`](#cloud_firestore---v613) + - [`firebase_ai` - `v3.9.0`](#firebase_ai---v390) + - [`firebase_analytics` - `v12.1.3`](#firebase_analytics---v1213) + - [`firebase_auth` - `v6.2.0`](#firebase_auth---v620) + - [`firebase_core` - `v4.5.0`](#firebase_core---v450) + - [`firebase_core_web` - `v3.5.0`](#firebase_core_web---v350) + - [`firebase_data_connect` - `v0.2.3`](#firebase_data_connect---v023) + - [`firebase_remote_config` - `v6.2.0`](#firebase_remote_config---v620) + - [`firebase_remote_config_platform_interface` - `v2.1.0`](#firebase_remote_config_platform_interface---v210) + - [`firebase_storage` - `v13.1.0`](#firebase_storage---v1310) + - [`firebase_in_app_messaging_platform_interface` - `v0.2.5+18`](#firebase_in_app_messaging_platform_interface---v02518) + - [`firebase_crashlytics_platform_interface` - `v3.8.18`](#firebase_crashlytics_platform_interface---v3818) + - [`firebase_remote_config_web` - `v1.10.4`](#firebase_remote_config_web---v1104) + - [`firebase_database_platform_interface` - `v0.3.0+3`](#firebase_database_platform_interface---v0303) + - [`cloud_firestore_web` - `v5.1.3`](#cloud_firestore_web---v513) + - [`firebase_app_installations_platform_interface` - `v0.1.4+66`](#firebase_app_installations_platform_interface---v01466) + - [`firebase_messaging_web` - `v4.1.3`](#firebase_messaging_web---v413) + - [`firebase_app_installations_web` - `v0.1.7+3`](#firebase_app_installations_web---v0173) + - [`firebase_auth_platform_interface` - `v8.1.7`](#firebase_auth_platform_interface---v817) + - [`firebase_messaging_platform_interface` - `v4.7.7`](#firebase_messaging_platform_interface---v477) + - [`cloud_firestore_platform_interface` - `v7.0.7`](#cloud_firestore_platform_interface---v707) + - [`firebase_analytics_web` - `v0.6.1+3`](#firebase_analytics_web---v0613) + - [`firebase_app_check_platform_interface` - `v0.2.1+5`](#firebase_app_check_platform_interface---v0215) + - [`firebase_app_check_web` - `v0.2.2+3`](#firebase_app_check_web---v0223) + - [`firebase_analytics_platform_interface` - `v5.0.7`](#firebase_analytics_platform_interface---v507) + - [`firebase_storage_web` - `v3.11.3`](#firebase_storage_web---v3113) + - [`firebase_performance_platform_interface` - `v0.1.6+5`](#firebase_performance_platform_interface---v0165) + - [`firebase_storage_platform_interface` - `v5.2.18`](#firebase_storage_platform_interface---v5218) + - [`firebase_performance_web` - `v0.1.8+3`](#firebase_performance_web---v0183) + - [`firebase_in_app_messaging` - `v0.9.0+7`](#firebase_in_app_messaging---v0907) + - [`firebase_crashlytics` - `v5.0.8`](#firebase_crashlytics---v508) + - [`firebase_database_web` - `v0.2.7+4`](#firebase_database_web---v0274) + - [`firebase_database` - `v12.1.4`](#firebase_database---v1214) + - [`firebase_app_installations` - `v0.4.0+7`](#firebase_app_installations---v0407) + - [`firebase_messaging` - `v16.1.2`](#firebase_messaging---v1612) + - [`firebase_auth_web` - `v6.1.3`](#firebase_auth_web---v613) + - [`firebase_app_check` - `v0.4.1+5`](#firebase_app_check---v0415) + - [`firebase_performance` - `v0.11.1+5`](#firebase_performance---v01115) + - [`firebase_ml_model_downloader` - `v0.4.0+7`](#firebase_ml_model_downloader---v0407) + - [`firebase_ml_model_downloader_platform_interface` - `v0.1.5+18`](#firebase_ml_model_downloader_platform_interface---v01518) + - [`cloud_functions_web` - `v5.1.3`](#cloud_functions_web---v513) + - [`cloud_functions` - `v6.0.7`](#cloud_functions---v607) + - [`cloud_functions_platform_interface` - `v5.8.10`](#cloud_functions_platform_interface---v5810) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `firebase_in_app_messaging_platform_interface` - `v0.2.5+18` + - `firebase_crashlytics_platform_interface` - `v3.8.18` + - `firebase_remote_config_web` - `v1.10.4` + - `firebase_database_platform_interface` - `v0.3.0+3` + - `cloud_firestore_web` - `v5.1.3` + - `firebase_app_installations_platform_interface` - `v0.1.4+66` + - `firebase_messaging_web` - `v4.1.3` + - `firebase_app_installations_web` - `v0.1.7+3` + - `firebase_auth_platform_interface` - `v8.1.7` + - `firebase_messaging_platform_interface` - `v4.7.7` + - `cloud_firestore_platform_interface` - `v7.0.7` + - `firebase_analytics_web` - `v0.6.1+3` + - `firebase_app_check_platform_interface` - `v0.2.1+5` + - `firebase_app_check_web` - `v0.2.2+3` + - `firebase_analytics_platform_interface` - `v5.0.7` + - `firebase_storage_web` - `v3.11.3` + - `firebase_performance_platform_interface` - `v0.1.6+5` + - `firebase_storage_platform_interface` - `v5.2.18` + - `firebase_performance_web` - `v0.1.8+3` + - `firebase_in_app_messaging` - `v0.9.0+7` + - `firebase_crashlytics` - `v5.0.8` + - `firebase_database_web` - `v0.2.7+4` + - `firebase_database` - `v12.1.4` + - `firebase_app_installations` - `v0.4.0+7` + - `firebase_messaging` - `v16.1.2` + - `firebase_auth_web` - `v6.1.3` + - `firebase_app_check` - `v0.4.1+5` + - `firebase_performance` - `v0.11.1+5` + - `firebase_ml_model_downloader` - `v0.4.0+7` + - `firebase_ml_model_downloader_platform_interface` - `v0.1.5+18` + - `cloud_functions_web` - `v5.1.3` + - `cloud_functions` - `v6.0.7` + - `cloud_functions_platform_interface` - `v5.8.10` + +--- + +#### `_flutterfire_internals` - `v1.3.67` + + - **FIX**(database): improve error handling in `platformExceptionToFirebaseException` ([#18007](https://github.com/firebase/flutterfire/issues/18007)). ([25f92046](https://github.com/firebase/flutterfire/commit/25f92046985b7b7105bb88f31d35d0d793744b23)) + +#### `cloud_firestore` - `v6.1.3` + + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + +#### `firebase_ai` - `v3.9.0` + + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + - **FEAT**(firebaseai): update Live API sample to add video support. ([#18018](https://github.com/firebase/flutterfire/issues/18018)). ([f91df750](https://github.com/firebase/flutterfire/commit/f91df7503bc4506c66cbebcfa562d65de1ae0e5b)) + +#### `firebase_analytics` - `v12.1.3` + + - **FIX**(analytics,iOS): Update hashedPhoneNumber handling to use hex string conversion ([#17807](https://github.com/firebase/flutterfire/issues/17807)). ([407c2490](https://github.com/firebase/flutterfire/commit/407c2490602484499d1ab5b2ce6860af00a218c8)) + +#### `firebase_auth` - `v6.2.0` + + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + +#### `firebase_core` - `v4.5.0` + + - **FEAT**(core,windows): update C++ Desktop SDK to 13.4.0. This may require updating your Visual Studio version and C++ build tools. ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + - **FEAT**: bump Firebase iOS SDK to 12.9.0 ([#18034](https://github.com/firebase/flutterfire/issues/18034)). ([c45894e2](https://github.com/firebase/flutterfire/commit/c45894e23895f9add8c152d13324920babe9b708)) + - **FEAT**: bump Firebase android SDK to 34.9.0 ([#18016](https://github.com/firebase/flutterfire/issues/18016)). ([b218dbff](https://github.com/firebase/flutterfire/commit/b218dbffd72d0bf666ff94f79a3de1e24d038df0)) + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + +#### `firebase_core_web` - `v3.5.0` + + - **FEAT**: bump Firebase JS SDK to 12.9.0 ([#18043](https://github.com/firebase/flutterfire/issues/18043)). ([1b29c4d4](https://github.com/firebase/flutterfire/commit/1b29c4d432597d12e08990825647f0ac9467a8f3)) + +#### `firebase_data_connect` - `v0.2.3` + + - **REFACTOR**(fdc): Support for entityId path extensions and hardening ([#17988](https://github.com/firebase/flutterfire/issues/17988)). ([fed585f5](https://github.com/firebase/flutterfire/commit/fed585f5a9b65d683cefdc7fa97ed2692e4ec817)) + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + - **FEAT**(fdc): Data Connect client sdk caching ([#17890](https://github.com/firebase/flutterfire/issues/17890)). ([02a019bc](https://github.com/firebase/flutterfire/commit/02a019bc25bb4a49d62c1079ed15e0c3aec8a5ec)) + +#### `firebase_remote_config` - `v6.2.0` + + - **FIX**(remote_config): correct `lastFetchTime` calculation ([#18004](https://github.com/firebase/flutterfire/issues/18004)). ([92f03e08](https://github.com/firebase/flutterfire/commit/92f03e08e9b5362c180da16d60d869568daf2c55)) + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + +#### `firebase_remote_config_platform_interface` - `v2.1.0` + + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + +#### `firebase_storage` - `v13.1.0` + + - **FEAT**(storage,windows): add emulator support ([#18030](https://github.com/firebase/flutterfire/issues/18030)). ([461dfa43](https://github.com/firebase/flutterfire/commit/461dfa43764469b518984052cb7bbc0a2a2675eb)) + + ## 2026-02-09 - [BoM 4.9.0](https://github.com/firebase/flutterfire/blob/main/VERSIONS.md#flutter-bom-490-2026-02-09) ### Changes diff --git a/Package.swift b/Package.swift index 139ffc0574ee..53cd2b38a81a 100644 --- a/Package.swift +++ b/Package.swift @@ -9,8 +9,8 @@ import Foundation import PackageDescription // auto-generated by melos post commit hook script -let firebase_core_version: String = "4.4.0" -let firebase_ios_sdk_version: String = "12.8.0" +let firebase_core_version: String = "4.5.0" +let firebase_ios_sdk_version: String = "12.9.0" /// Shared Swift package manager code for firebase core let package = Package( diff --git a/VERSIONS.md b/VERSIONS.md index e25ad0f1bc35..2616b73c2a36 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -4,6 +4,44 @@ This document is listing all the compatible versions of the FlutterFire plugins. # Versions +## [Flutter BoM 4.10.0 (2026-03-02)](https://github.com/firebase/flutterfire/blob/main/CHANGELOG.md#2026-03-02) + +Install this version using FlutterFire CLI + +```bash +flutterfire install 4.10.0 +``` + +### Included Native Firebase SDK Versions +| Firebase SDK | Version | Link | +|--------------|---------|------| +| Android SDK | 34.9.0 | [Release Notes](https://firebase.google.com/support/release-notes/android) | +| iOS SDK | 12.9.0 | [Release Notes](https://firebase.google.com/support/release-notes/ios) | +| Web SDK | 12.9.0 | [Release Notes](https://firebase.google.com/support/release-notes/js) | +| Windows SDK | 13.4.0 | [Release Notes](https://firebase.google.com/support/release-notes/cpp-relnotes) | + +### FlutterFire Plugin Versions +| Plugin | Version | Dart Version | Flutter Version | +|--------|---------|--------------|-----------------| +| [cloud_firestore](https://pub.dev/packages/cloud_firestore/versions/6.1.3) | 6.1.3 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [cloud_functions](https://pub.dev/packages/cloud_functions/versions/6.0.7) | 6.0.7 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_ai](https://pub.dev/packages/firebase_ai/versions/3.9.0) | 3.9.0 | >=3.2.0 <4.0.0 | >=3.16.0 | +| [firebase_analytics](https://pub.dev/packages/firebase_analytics/versions/12.1.3) | 12.1.3 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_app_check](https://pub.dev/packages/firebase_app_check/versions/0.4.1+5) | 0.4.1+5 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_app_installations](https://pub.dev/packages/firebase_app_installations/versions/0.4.0+7) | 0.4.0+7 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_auth](https://pub.dev/packages/firebase_auth/versions/6.2.0) | 6.2.0 | >=3.2.0 <4.0.0 | >=3.16.0 | +| [firebase_core](https://pub.dev/packages/firebase_core/versions/4.5.0) | 4.5.0 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_crashlytics](https://pub.dev/packages/firebase_crashlytics/versions/5.0.8) | 5.0.8 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_data_connect](https://pub.dev/packages/firebase_data_connect/versions/0.2.3) | 0.2.3 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_database](https://pub.dev/packages/firebase_database/versions/12.1.4) | 12.1.4 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_in_app_messaging](https://pub.dev/packages/firebase_in_app_messaging/versions/0.9.0+7) | 0.9.0+7 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_messaging](https://pub.dev/packages/firebase_messaging/versions/16.1.2) | 16.1.2 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_ml_model_downloader](https://pub.dev/packages/firebase_ml_model_downloader/versions/0.4.0+7) | 0.4.0+7 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_performance](https://pub.dev/packages/firebase_performance/versions/0.11.1+5) | 0.11.1+5 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_remote_config](https://pub.dev/packages/firebase_remote_config/versions/6.2.0) | 6.2.0 | >=3.2.0 <4.0.0 | >=3.3.0 | +| [firebase_storage](https://pub.dev/packages/firebase_storage/versions/13.1.0) | 13.1.0 | >=3.2.0 <4.0.0 | >=3.3.0 | + + ## [Flutter BoM 4.9.0 (2026-02-09)](https://github.com/firebase/flutterfire/blob/main/CHANGELOG.md#2026-02-09) Install this version using FlutterFire CLI diff --git a/docs/analytics/_get-started.md b/docs/analytics/_get-started.md index b4460dfd9bf1..78bbe531e1e4 100644 --- a/docs/analytics/_get-started.md +++ b/docs/analytics/_get-started.md @@ -83,6 +83,32 @@ await FirebaseAnalytics.instance ); ``` +## Using Analytics without Ad ID support (iOS) {:#without-ad-id} + +If your app doesn't use IDFA, you can use `FirebaseAnalyticsWithoutAdIdSupport` +instead of the default `FirebaseAnalytics` iOS dependency to avoid App Store +review questions about advertising identifiers. + +### Swift Package Manager + +Set the `FIREBASE_ANALYTICS_WITHOUT_ADID` environment variable when building: + +```bash +FIREBASE_ANALYTICS_WITHOUT_ADID=true flutter build ios +``` + +You can also add this variable to your Xcode scheme's environment variables +for persistent configuration. + +### CocoaPods + +Add this to your app's `Podfile`: + +```ruby +pod 'FirebaseAnalytics', :modular_headers => true +pod 'FirebaseAnalyticsWithoutAdIdSupport', :modular_headers => true +``` + ## Next steps * Use the [DebugView](/docs/analytics/debugview) to verify your events. diff --git a/docs/auth/federated-auth.md b/docs/auth/federated-auth.md index 943505bdc4ce..3811c94f348a 100644 --- a/docs/auth/federated-auth.md +++ b/docs/auth/federated-auth.md @@ -12,6 +12,13 @@ Both native platforms and web support creating a credential which can then be pa or `linkWithCredential` methods. Alternatively on web platforms, you can trigger the authentication process via a popup or redirect. +Note: On Android, `signInWithProvider` opens a Chrome Custom Tab for the OAuth flow. If your +`AndroidManifest.xml` contains `android:taskAffinity=""` (Flutter's default template), the Custom Tab +will close when the user switches apps (e.g. to open a password manager), and returning will give a +`web-context-already-presented` error. To fix this, remove `android:taskAffinity=""` from your +`AndroidManifest.xml`. +{: .callout .callout-warning} + ## Google Most configuration is already setup when using Google Sign-In with Firebase, however you need to ensure your machine's @@ -208,11 +215,18 @@ For further information, see this [issue](https://github.com/firebase/flutterfir and [enable Apple as a sign-in provider](/docs/auth/web/apple#enable-apple-as-a-sign-in-provider). +To have Apple present the full first-time sign-in UI (including the "Share/Hide email" option), +you must request the `email` and `name` scopes: +{: .callout .callout-info} + ```dart import 'package:firebase_auth/firebase_auth.dart'; Future signInWithApple() async { final appleProvider = AppleAuthProvider(); + appleProvider.addScope('email'); + appleProvider.addScope('name'); + if (kIsWeb) { await FirebaseAuth.instance.signInWithPopup(appleProvider); } else { @@ -259,6 +273,8 @@ import 'package:firebase_auth/firebase_auth.dart'; Future signInWithApple() async { final appleProvider = AppleAuthProvider(); + appleProvider.addScope('email'); + appleProvider.addScope('name'); UserCredential userCredential = await FirebaseAuth.instance.signInWithPopup(appleProvider); // Keep the authorization code returned from Apple platforms diff --git a/docs/auth/password-auth.md b/docs/auth/password-auth.md index ce1acecd2185..adbea74e3686 100644 --- a/docs/auth/password-auth.md +++ b/docs/auth/password-auth.md @@ -73,14 +73,28 @@ try { password: password ); } on FirebaseAuthException catch (e) { - if (e.code == 'user-not-found') { + if (e.code == 'invalid-credential') { + // Email or password is incorrect. Projects with email enumeration + // protection enabled (the default since September 2023) return this + // code instead of 'user-not-found' or 'wrong-password'. + print('Invalid email or password.'); + } else if (e.code == 'user-not-found') { + // Only returned when email enumeration protection is disabled. print('No user found for that email.'); } else if (e.code == 'wrong-password') { + // Only returned when email enumeration protection is disabled. print('Wrong password provided for that user.'); } } ``` +Note: Since September 2023, Firebase enables +[email enumeration protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) +by default on new projects. With this feature enabled, `user-not-found` and +`wrong-password` error codes are replaced by `invalid-credential` to prevent +revealing whether an email address is registered. You can manage this setting in +the Firebase console under **Authentication > Settings**. + Caution: When a user uninstalls your app on iOS or macOS, the user's authentication state can persist between app re-installs, as the Firebase iOS SDK persists authentication state to the system keychain. diff --git a/docs/cloud-messaging/client.md b/docs/cloud-messaging/client.md index d5c8861d127b..7988b30cca16 100644 --- a/docs/cloud-messaging/client.md +++ b/docs/cloud-messaging/client.md @@ -1,25 +1,12 @@ -Project: /docs/cloud-messaging/_project.yaml -Book: /docs/_book.yaml -page_type: guide - -{% include "_shared/apis/console/_local_variables.html" %} -{% include "_local_variables.html" %} -{% include "docs/cloud-messaging/_local_variables.html" %} -{% include "docs/android/_local_variables.html" %} - # Set up a Firebase Cloud Messaging client app on Flutter -Follow these steps to set up an FCM client on Flutter. - -## Platform-specific setup and requirements +Depending on the platform you're targeting, there are some additional required setup steps that you'll need to take. -Some of the required steps depend on the platform you're targeting. +## iOS+ -### iOS+ - -#### Enable app capabilities in Xcode +### Enable app capabilities in Xcode Before your application can start to receive messages, you must enable push notifications and background modes in your Xcode project. @@ -31,27 +18,25 @@ notifications and background modes in your Xcode project. #### Upload your APNs authentication key -Before you use FCM, upload your APNs certificate to Firebase. If you don't -already have an APNs certificate, create one in the +Before you use FCM, upload your APNs authentication key to Firebase. If you don't +already have an APNs authentication key, create one in the [Apple Developer Member Center](https://developer.apple.com/membercenter/index.action). 1. Inside your project in the Firebase console, select the gear icon, select **Project Settings**, and then select the **Cloud Messaging** tab. -1. Select the **Upload Certificate** button for your development certificate, - your production certificate, or both. At least one is required. -1. For each certificate, select the .p12 file, and provide the password, if - any. Make sure the bundle ID for this certificate matches the bundle ID of - your app. Select **Save**. +1. Select the **Upload** button for your development authentication key, + your production authentication key, or both. At least one is required. +1. For each authentication key, select the .p8 file, and provide the key ID and your Apple team ID. Select **Save**. #### Method swizzling -To use the FCM Flutter plugin on Apple devices, you must not disable method -swizzling. Swizzling is required, and without it, key Firebase features such as -FCM token handling do not function properly. +To use the FCM Flutter plugin on Apple devices, method +swizzling is required. Without it, key Firebase features such as +FCM token handling won't function properly. -### Android +## Android -#### Google Play services +### Google Play services FCM clients require devices running Android 4.4 or higher that also have Google Play services installed, or an emulator running Android 4.4 with Google APIs. @@ -69,12 +54,12 @@ other means, such as through the back button, the check is still performed. If the device doesn't have a compatible version of Google Play services, your app can call [`GoogleApiAvailability.makeGooglePlayServicesAvailable()`](//developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability.html#public-methods) to allow users to download Google Play services from the Play Store. -### Web +## Web -#### Configure Web Credentials with FCM +### Configure Web Credentials with FCM -The FCM Web interface uses Web credentials called "Voluntary Application Server -Identification," or "VAPID" keys, to authorize send requests to supported web +The FCM Web interface uses Web credentials called Voluntary Application Server +Identification, or "VAPID" keys, to authorize send requests to supported web push services. To subscribe your app to push notifications, you need to associate a pair of keys with your Firebase project. You can either generate a new key pair or import your existing key pair through the Firebase console. @@ -131,20 +116,15 @@ see [Application server keys](https://developers.google.com/web/fundamentals/pus flutter run ``` - ## Access the registration token -To send a message to a specific device, you need to know that device's -registration token. Because you'll need to enter the token in a field in the -Notifications console to complete this tutorial, make sure to copy the token -or securely store it after you retrieve it. - -To retrieve the current registration token for an app instance, call +To send a message to a specific device, you need to know the device +registration token. To retrieve the current registration token for an app instance, call `getToken()`. If notification permission has not been granted, this method will ask the user for notification permissions. Otherwise, it returns a token or rejects the future due to an error. -Warning: In iOS SDK 10.4.0 and higher, it is a requirement that the APNs token +Warning: In iOS SDK 10.4.0 and higher, it is required that the APNs token is available before making API requests. The APNs token is not guaranteed to have been received before making FCM plugin API requests. @@ -182,14 +162,13 @@ FirebaseMessaging.instance.onTokenRefresh }); ``` - ## Prevent auto initialization {:#prevent-auto-init} When an FCM registration token is generated, the library uploads the identifier and configuration data to Firebase. If you prefer to prevent token autogeneration, disable auto-initialization at build time. -#### iOS +### iOS On iOS, add a metadata value to your `Info.plist`: @@ -197,8 +176,7 @@ On iOS, add a metadata value to your `Info.plist`: FirebaseMessagingAutoInitEnabled = NO ``` - -#### Android +### Android On Android, disable Analytics collection and FCM auto initialization (you must disable both) by adding these metadata values to your `AndroidManifest.xml`: @@ -222,17 +200,49 @@ await FirebaseMessaging.instance.setAutoInitEnabled(true); This value persists across app restarts once set. -## Next steps +## Send a test notification message + +1. Install and run the app on the target device. On Apple devices, you'll need to accept the request for permission to receive remote notifications. +2. Make sure the app is in the background on the device. +3. In the Firebase console, open the Messaging page. +4. If this is your first message, select **Create your first campaign**. Select **Firebase Notification messages** and select **Create**. +5. Otherwise, on the **Campaign** tab, select **New campaign** and then **Notifications**. +6. Enter the message text. +7. Select **Send test message** from the right pane. +8. In the field labeled **Add an FCM registration token**, enter your registration token. +9. Select **Test**. + +After you select **Test**, the targeted client device, with the app in the background, should receive the notification. + +For insight into message delivery to your app, see the FCM reporting dashboard, which records the number of messages sent and opened on Apple and Android devices, along with impression data for Android apps. + +## Handling interaction -After the client app is set up, you are ready to start sending downstream -messages with the -[Notifications composer](//console.firebase.google.com/project/_/notification). -See [Send a test message to a backgrounded app](first-message). +When users tap a notification, the default behavior on both Android and iOS is +to open the application. If the application is terminated, it will be started, +and if it is in the background, it will be brought to the foreground. -To add other, more advanced behavior to your app, you'll need a -[server implementation](/docs/cloud-messaging/server). +Depending on the content of a notification, you may want to handle the user's +interaction when the application opens. For example, if a new chat message is +sent using a notification and the user selects it, you may want to open the +specific conversation when the application opens. -Then, in your app client: +The `firebase-messaging` package provides two ways to handle this interaction: + 1. `getInitialMessage():` If the application is opened from a terminated + state, this method returns a `Future` containing a `RemoteMessage`. Once + consumed, the `RemoteMessage` will be removed. + 1. `onMessageOpenedApp`: A`Stream` which posts a `RemoteMessage` when the + application is opened from a background state. + +To make sure your users have a smooth experience, you should handle both +scenarios. The following code example outlines how this can be achieved: + +How you handle interactions depends on your application setup. The previously +shown example is a basic example of using a `StatefulWidget`. + +## Next steps +After the client app is set up, you can start receiving messages or sending them to your users: +- [Send a test message to a backgrounded app](first-message) - [Receive messages](/docs/cloud-messaging/flutter/receive) -- [Subscribe to message topics](/docs/cloud-messaging/flutter/topic-messaging) +- [Notification composer](///console.firebase.google.com/project/_/notification) diff --git a/docs/storage/start.md b/docs/storage/start.md index 4e7f846f8f77..4b7e503f931a 100644 --- a/docs/storage/start.md +++ b/docs/storage/start.md @@ -148,7 +148,9 @@ have to grant Firebase the ability to access these files using the [Google Cloud SDK](//cloud.google.com/sdk/docs/): ```bash -gsutil -m acl ch -r -u service-PROJECT_NUMBER@gcp-sa-firebasestorage.iam.gserviceaccount.com gs://YOUR-CLOUD-STORAGE-BUCKET +gcloud storage objects add-iam-policy-binding gs://YOUR-CLOUD-STORAGE-BUCKET \ + --member="serviceAccount:service-PROJECT_NUMBER@gcp-sa-firebasestorage.iam.gserviceaccount.com" \ + --role="roles/storage.objectViewer" ``` You can find your project number as described in the [introduction to diff --git a/packages/_flutterfire_internals/CHANGELOG.md b/packages/_flutterfire_internals/CHANGELOG.md index dfda9b346106..94ea80f71c75 100644 --- a/packages/_flutterfire_internals/CHANGELOG.md +++ b/packages/_flutterfire_internals/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.3.67 + + - **FIX**(database): improve error handling in `platformExceptionToFirebaseException` ([#18007](https://github.com/firebase/flutterfire/issues/18007)). ([25f92046](https://github.com/firebase/flutterfire/commit/25f92046985b7b7105bb88f31d35d0d793744b23)) + ## 1.3.66 - Update a dependency to the latest release. diff --git a/packages/_flutterfire_internals/pubspec.yaml b/packages/_flutterfire_internals/pubspec.yaml index 46dfeda68820..435a4ac3a807 100755 --- a/packages/_flutterfire_internals/pubspec.yaml +++ b/packages/_flutterfire_internals/pubspec.yaml @@ -2,7 +2,7 @@ name: _flutterfire_internals description: A package hosting Dart code shared between FlutterFire plugins. homepage: https://firebase.google.com/docs/firestore repository: https://github.com/firebase/flutterfire/tree/main/packages/_flutterfire_internals -version: 1.3.66 +version: 1.3.67 environment: sdk: '>=3.2.0 <4.0.0' @@ -10,7 +10,7 @@ environment: dependencies: collection: ^1.0.0 - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/cloud_firestore/cloud_firestore/CHANGELOG.md b/packages/cloud_firestore/cloud_firestore/CHANGELOG.md index 209e9b322228..3f5751eeb1be 100644 --- a/packages/cloud_firestore/cloud_firestore/CHANGELOG.md +++ b/packages/cloud_firestore/cloud_firestore/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.3 + + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + ## 6.1.2 - **FIX**(firestore,android): avoid ConcurrentModificationException by collecting Firestore instances before termination ([#17956](https://github.com/firebase/flutterfire/issues/17956)). ([f94bbd68](https://github.com/firebase/flutterfire/commit/f94bbd688c3c0aaa62ba9117b23902c10297ea84)) diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java index 4b65130f62a4..4a17da9d532f 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java @@ -28,6 +28,8 @@ import com.google.firebase.firestore.MemoryCacheSettings; import com.google.firebase.firestore.PersistentCacheIndexManager; import com.google.firebase.firestore.PersistentCacheSettings; +import com.google.firebase.firestore.Pipeline; +import com.google.firebase.firestore.PipelineResult; import com.google.firebase.firestore.Query; import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.SetOptions; @@ -52,6 +54,7 @@ import io.flutter.plugins.firebase.firestore.streamhandler.TransactionStreamHandler; import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter; import io.flutter.plugins.firebase.firestore.utils.PigeonParser; +import io.flutter.plugins.firebase.firestore.utils.PipelineParser; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -983,4 +986,73 @@ public void documentReferenceSnapshot( parameters.getServerTimestampBehavior()), PigeonParser.parseListenSource(source)))); } + + @Override + public void executePipeline( + @NonNull GeneratedAndroidFirebaseFirestore.FirestorePigeonFirebaseApp app, + @NonNull List> stages, + @Nullable Map options, + @NonNull + GeneratedAndroidFirebaseFirestore.Result< + GeneratedAndroidFirebaseFirestore.PigeonPipelineSnapshot> + result) { + cachedThreadPool.execute( + () -> { + try { + FirebaseFirestore firestore = getFirestoreFromPigeon(app); + + // Execute pipeline using Android Firestore SDK + Pipeline.Snapshot snapshot = PipelineParser.executePipeline(firestore, stages, options); + + // Convert Pipeline.Snapshot to PigeonPipelineSnapshot + List pipelineResults = + new ArrayList<>(); + + // Iterate through snapshot results + for (PipelineResult pipelineResult : snapshot.getResults()) { + GeneratedAndroidFirebaseFirestore.PigeonPipelineResult.Builder resultBuilder = + new GeneratedAndroidFirebaseFirestore.PigeonPipelineResult.Builder(); + if (pipelineResult.getRef() != null) { + resultBuilder.setDocumentPath(pipelineResult.getRef().getPath()); + } + + // Convert timestamps (assuming they're in milliseconds) + if (pipelineResult.getCreateTime() != null) { + resultBuilder.setCreateTime(pipelineResult.getCreateTime().toDate().getTime()); + } else { + resultBuilder.setCreateTime(0L); + } + + if (pipelineResult.getUpdateTime() != null) { + resultBuilder.setUpdateTime(pipelineResult.getUpdateTime().toDate().getTime()); + } else { + resultBuilder.setUpdateTime(0L); + } + + Map data = pipelineResult.getData(); + if (data != null) { + resultBuilder.setData(data); + } + + pipelineResults.add(resultBuilder.build()); + } + + // Build the snapshot + GeneratedAndroidFirebaseFirestore.PigeonPipelineSnapshot.Builder snapshotBuilder = + new GeneratedAndroidFirebaseFirestore.PigeonPipelineSnapshot.Builder(); + snapshotBuilder.setResults(pipelineResults); + + // Set execution time (use current time if not available from snapshot) + if (snapshot.getExecutionTime() != null) { + snapshotBuilder.setExecutionTime(snapshot.getExecutionTime().toDate().getTime()); + } else { + snapshotBuilder.setExecutionTime(System.currentTimeMillis()); + } + + result.success(snapshotBuilder.build()); + } catch (Exception e) { + ExceptionConverter.sendErrorToFlutter(result, e); + } + }); + } } diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/GeneratedAndroidFirebaseFirestore.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/GeneratedAndroidFirebaseFirestore.java index f7d24bc7c7ec..4e6e315d71fe 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/GeneratedAndroidFirebaseFirestore.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/GeneratedAndroidFirebaseFirestore.java @@ -856,6 +856,197 @@ public ArrayList toList() { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class PigeonPipelineResult { + private @Nullable String documentPath; + + public @Nullable String getDocumentPath() { + return documentPath; + } + + public void setDocumentPath(@Nullable String setterArg) { + this.documentPath = setterArg; + } + + private @Nullable Long createTime; + + public @Nullable Long getCreateTime() { + return createTime; + } + + public void setCreateTime(@Nullable Long setterArg) { + this.createTime = setterArg; + } + + private @Nullable Long updateTime; + + public @Nullable Long getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(@Nullable Long setterArg) { + this.updateTime = setterArg; + } + + /** All fields in the result (from PipelineResult.data() on Android). */ + private @Nullable Map data; + + public @Nullable Map getData() { + return data; + } + + public void setData(@Nullable Map setterArg) { + this.data = setterArg; + } + + public static final class Builder { + + private @Nullable String documentPath; + + public @NonNull Builder setDocumentPath(@Nullable String setterArg) { + this.documentPath = setterArg; + return this; + } + + private @Nullable Long createTime; + + public @NonNull Builder setCreateTime(@Nullable Long setterArg) { + this.createTime = setterArg; + return this; + } + + private @Nullable Long updateTime; + + public @NonNull Builder setUpdateTime(@Nullable Long setterArg) { + this.updateTime = setterArg; + return this; + } + + private @Nullable Map data; + + public @NonNull Builder setData(@Nullable Map setterArg) { + this.data = setterArg; + return this; + } + + public @NonNull PigeonPipelineResult build() { + PigeonPipelineResult pigeonReturn = new PigeonPipelineResult(); + pigeonReturn.setDocumentPath(documentPath); + pigeonReturn.setCreateTime(createTime); + pigeonReturn.setUpdateTime(updateTime); + pigeonReturn.setData(data); + return pigeonReturn; + } + } + + @NonNull + public ArrayList toList() { + ArrayList toListResult = new ArrayList(4); + toListResult.add(documentPath); + toListResult.add(createTime); + toListResult.add(updateTime); + toListResult.add(data); + return toListResult; + } + + static @NonNull PigeonPipelineResult fromList(@NonNull ArrayList list) { + PigeonPipelineResult pigeonResult = new PigeonPipelineResult(); + Object documentPath = list.get(0); + pigeonResult.setDocumentPath((String) documentPath); + Object createTime = list.get(1); + pigeonResult.setCreateTime( + (createTime == null) + ? null + : ((createTime instanceof Integer) ? (Integer) createTime : (Long) createTime)); + Object updateTime = list.get(2); + pigeonResult.setUpdateTime( + (updateTime == null) + ? null + : ((updateTime instanceof Integer) ? (Integer) updateTime : (Long) updateTime)); + Object data = list.get(3); + pigeonResult.setData((Map) data); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class PigeonPipelineSnapshot { + private @NonNull List results; + + public @NonNull List getResults() { + return results; + } + + public void setResults(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"results\" is null."); + } + this.results = setterArg; + } + + private @NonNull Long executionTime; + + public @NonNull Long getExecutionTime() { + return executionTime; + } + + public void setExecutionTime(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"executionTime\" is null."); + } + this.executionTime = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PigeonPipelineSnapshot() {} + + public static final class Builder { + + private @Nullable List results; + + public @NonNull Builder setResults(@NonNull List setterArg) { + this.results = setterArg; + return this; + } + + private @Nullable Long executionTime; + + public @NonNull Builder setExecutionTime(@NonNull Long setterArg) { + this.executionTime = setterArg; + return this; + } + + public @NonNull PigeonPipelineSnapshot build() { + PigeonPipelineSnapshot pigeonReturn = new PigeonPipelineSnapshot(); + pigeonReturn.setResults(results); + pigeonReturn.setExecutionTime(executionTime); + return pigeonReturn; + } + } + + @NonNull + public ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(results); + toListResult.add(executionTime); + return toListResult; + } + + static @NonNull PigeonPipelineSnapshot fromList(@NonNull ArrayList list) { + PigeonPipelineSnapshot pigeonResult = new PigeonPipelineSnapshot(); + Object results = list.get(0); + pigeonResult.setResults((List) results); + Object executionTime = list.get(1); + pigeonResult.setExecutionTime( + (executionTime == null) + ? null + : ((executionTime instanceof Integer) + ? (Integer) executionTime + : (Long) executionTime)); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class PigeonGetOptions { private @NonNull Source source; @@ -1660,12 +1851,16 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 136: return PigeonGetOptions.fromList((ArrayList) readValue(buffer)); case (byte) 137: - return PigeonQueryParameters.fromList((ArrayList) readValue(buffer)); + return PigeonPipelineResult.fromList((ArrayList) readValue(buffer)); case (byte) 138: - return PigeonQuerySnapshot.fromList((ArrayList) readValue(buffer)); + return PigeonPipelineSnapshot.fromList((ArrayList) readValue(buffer)); case (byte) 139: - return PigeonSnapshotMetadata.fromList((ArrayList) readValue(buffer)); + return PigeonQueryParameters.fromList((ArrayList) readValue(buffer)); case (byte) 140: + return PigeonQuerySnapshot.fromList((ArrayList) readValue(buffer)); + case (byte) 141: + return PigeonSnapshotMetadata.fromList((ArrayList) readValue(buffer)); + case (byte) 142: return PigeonTransactionCommand.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -1701,17 +1896,23 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PigeonGetOptions) { stream.write(136); writeValue(stream, ((PigeonGetOptions) value).toList()); - } else if (value instanceof PigeonQueryParameters) { + } else if (value instanceof PigeonPipelineResult) { stream.write(137); + writeValue(stream, ((PigeonPipelineResult) value).toList()); + } else if (value instanceof PigeonPipelineSnapshot) { + stream.write(138); + writeValue(stream, ((PigeonPipelineSnapshot) value).toList()); + } else if (value instanceof PigeonQueryParameters) { + stream.write(139); writeValue(stream, ((PigeonQueryParameters) value).toList()); } else if (value instanceof PigeonQuerySnapshot) { - stream.write(138); + stream.write(140); writeValue(stream, ((PigeonQuerySnapshot) value).toList()); } else if (value instanceof PigeonSnapshotMetadata) { - stream.write(139); + stream.write(141); writeValue(stream, ((PigeonSnapshotMetadata) value).toList()); } else if (value instanceof PigeonTransactionCommand) { - stream.write(140); + stream.write(142); writeValue(stream, ((PigeonTransactionCommand) value).toList()); } else { super.writeValue(stream, value); @@ -1836,6 +2037,12 @@ void persistenceCacheIndexManagerRequest( @NonNull PersistenceCacheIndexManagerRequest request, @NonNull Result result); + void executePipeline( + @NonNull FirestorePigeonFirebaseApp app, + @NonNull List> stages, + @Nullable Map options, + @NonNull Result result); + /** The codec used by FirebaseFirestoreHostApi. */ static @NonNull MessageCodec getCodec() { return FirebaseFirestoreHostApiCodec.INSTANCE; @@ -2624,6 +2831,39 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.cloud_firestore_platform_interface.FirebaseFirestoreHostApi.executePipeline", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + FirestorePigeonFirebaseApp appArg = (FirestorePigeonFirebaseApp) args.get(0); + List> stagesArg = (List>) args.get(1); + Map optionsArg = (Map) args.get(2); + Result resultCallback = + new Result() { + public void success(PigeonPipelineSnapshot result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.executePipeline(appArg, stagesArg, optionsArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionHelpers.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionHelpers.java new file mode 100644 index 000000000000..86c0877a902c --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionHelpers.java @@ -0,0 +1,159 @@ +/* + * 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> 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> 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> 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 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 from Dart which comes as List or List + // 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()); + } +} diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java new file mode 100644 index 000000000000..c937008dc3e2 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java @@ -0,0 +1,559 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; + +/** Handles parsing of all expression types from Dart map representations to Android SDK objects. */ +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 apply(Expression left, Expression right); + } + + /** Parses an expression from a map representation. */ + @SuppressWarnings("unchecked") + Expression parseExpression(@NonNull Map expressionMap) { + String name = (String) expressionMap.get("name"); + if (name == null) { + // Might be a field reference directly (legacy format) + if (expressionMap.containsKey("field_name")) { + String fieldName = (String) expressionMap.get("field_name"); + return Expression.field(fieldName); + } + // Check for field in args (current format) + Map argsCheck = (Map) 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 args = (Map) expressionMap.get("args"); + if (args == null) { + args = new HashMap<>(); + } + + switch (name) { + 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) { + @SuppressWarnings("unchecked") + Map valueMap = (Map) value; + String path = (String) valueMap.get("path"); + return Expression.constant(firestore.document(path)); + } + return ExpressionHelpers.parseConstantValue(value); + } + case "alias": + { + Map exprMap = (Map) args.get("expression"); + String alias = (String) args.get("alias"); + Expression expr = parseExpression(exprMap); + return expr.alias(alias); + } + // Comparison operations + 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)); + // Arithmetic operations + 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)); + // Logic operations + case "and": + { + List> exprMaps = (List>) args.get("expressions"); + return ExpressionHelpers.parseAndExpression(exprMaps, this); + } + case "or": + { + List> exprMaps = (List>) args.get("expressions"); + return ExpressionHelpers.parseOrExpression(exprMaps, this); + } + case "xor": + { + List> exprMaps = (List>) args.get("expressions"); + return ExpressionHelpers.parseXorExpression(exprMaps, this); + } + case "not": + { + Map exprMap = (Map) args.get("expression"); + BooleanExpression expr = parseBooleanExpression(exprMap); + return Expression.not(expr); + } + default: + Log.w(TAG, "Unsupported expression type: " + name); + throw new UnsupportedOperationException("Expression type not yet implemented: " + name); + } + } + + /** Helper to parse binary comparison operations (equal, not_equal, greater_than, etc.). */ + @SuppressWarnings("unchecked") + private BooleanExpression parseBinaryComparison( + @NonNull Map args, @NonNull BinaryExpressionOp operation) { + Map leftMap = (Map) args.get("left"); + Map rightMap = (Map) args.get("right"); + Expression left = parseExpression(leftMap); + Expression right = parseExpression(rightMap); + return operation.apply(left, right); + } + + /** Helper to parse binary arithmetic operations (add, subtract, multiply, etc.). */ + @SuppressWarnings("unchecked") + private Expression parseBinaryOperation( + @NonNull Map args, @NonNull BinaryExpressionOp operation) { + Map leftMap = (Map) args.get("left"); + Map rightMap = (Map) args.get("right"); + Expression left = parseExpression(leftMap); + Expression right = parseExpression(rightMap); + return operation.apply(left, right); + } + + /** + * Parses a boolean expression from a map representation. Boolean expressions are used in where + * clauses and return BooleanExpression. + */ + @SuppressWarnings("unchecked") + BooleanExpression parseBooleanExpression(@NonNull Map expressionMap) { + String name = (String) expressionMap.get("name"); + if (name == null) { + throw new IllegalArgumentException("BooleanExpression must have a 'name' field"); + } + + Map args = (Map) expressionMap.get("args"); + if (args == null) { + args = new HashMap<>(); + } + + switch (name) { + // Comparison operations - these return BooleanExpression + 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)); + // Logical operations - these return BooleanExpression + case "and": + { + List> exprMaps = (List>) args.get("expressions"); + return ExpressionHelpers.parseAndExpression(exprMaps, this); + } + case "or": + { + List> exprMaps = (List>) args.get("expressions"); + if (exprMaps == null || exprMaps.isEmpty()) { + throw new IllegalArgumentException("'or' requires at least one expression"); + } + if (exprMaps.size() == 1) { + return parseBooleanExpression(exprMaps.get(0)); + } + // BooleanExpression.or() takes exactly 2 parameters, so we chain them + BooleanExpression result = parseBooleanExpression(exprMaps.get(0)); + for (int i = 1; i < exprMaps.size(); i++) { + BooleanExpression next = parseBooleanExpression(exprMaps.get(i)); + result = BooleanExpression.or(result, next); + } + return result; + } + case "xor": + { + List> exprMaps = (List>) args.get("expressions"); + return ExpressionHelpers.parseXorExpression(exprMaps, this); + } + case "not": + { + Map exprMap = (Map) args.get("expression"); + BooleanExpression expr = parseBooleanExpression(exprMap); + return expr.not(); + } + // Boolean-specific expressions + case "is_absent": + { + Map exprMap = (Map) args.get("expression"); + Expression expr = parseExpression(exprMap); + return expr.isAbsent(); + } + case "is_error": + { + Map exprMap = (Map) args.get("expression"); + Expression expr = parseExpression(exprMap); + return expr.isError(); + } + case "exists": + { + Map exprMap = (Map) args.get("expression"); + Expression expr = parseExpression(exprMap); + return expr.exists(); + } + case "array_contains": + { + Map arrayMap = (Map) args.get("array"); + Map elementMap = (Map) args.get("element"); + Expression array = parseExpression(arrayMap); + Expression element = parseExpression(elementMap); + return array.arrayContains(element); + } + case "array_contains_all": + { + Map arrayMap = (Map) args.get("array"); + Map arrayExprMap = (Map) args.get("array_expression"); + Expression array = parseExpression(arrayMap); + Expression arrayExpr = parseExpression(arrayExprMap); + return array.arrayContainsAll(arrayExpr); + } + case "array_contains_any": + { + Map arrayMap = (Map) args.get("array"); + Map arrayExprMap = (Map) args.get("array_expression"); + Expression array = parseExpression(arrayMap); + Expression arrayExpr = parseExpression(arrayExprMap); + return array.arrayContainsAny(arrayExpr); + } + case "equal_any": + { + Map valueMap = (Map) args.get("value"); + List> valuesMaps = (List>) args.get("values"); + Expression value = parseExpression(valueMap); + Expression[] values = new Expression[valuesMaps.size()]; + for (int i = 0; i < valuesMaps.size(); i++) { + values[i] = parseExpression(valuesMaps.get(i)); + } + return value.equalAny(List.of(values)); + } + case "not_equal_any": + { + Map valueMap = (Map) args.get("value"); + List> valuesMaps = (List>) args.get("values"); + Expression value = parseExpression(valueMap); + Expression[] values = new Expression[valuesMaps.size()]; + for (int i = 0; i < valuesMaps.size(); i++) { + values[i] = parseExpression(valuesMaps.get(i)); + } + return value.notEqualAny(List.of(values)); + } + case "as_boolean": + { + Map exprMap = (Map) args.get("expression"); + Expression expr = parseExpression(exprMap); + return expr.asBoolean(); + } + // Handle filter expressions (PipelineFilter) + case "filter": + return parseFilterExpression(args); + default: + // Try parsing as a regular expression first, then cast to BooleanExpression if possible + 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"); + } + } + + /** + * Parses a filter expression (PipelineFilter) which can have operator-based or field-based forms. + */ + @SuppressWarnings("unchecked") + private BooleanExpression parseFilterExpression(@NonNull Map args) { + // PipelineFilter can have various forms - check for operator-based or field-based + if (args.containsKey("operator")) { + String operator = (String) args.get("operator"); + List> exprMaps = (List>) args.get("expressions"); + if ("and".equals(operator)) { + return ExpressionHelpers.parseAndExpression(exprMaps, this); + } else if ("or".equals(operator)) { + if (exprMaps == null || exprMaps.isEmpty()) { + throw new IllegalArgumentException("'or' requires at least one expression"); + } + if (exprMaps.size() == 1) { + return parseBooleanExpression(exprMaps.get(0)); + } + // BooleanExpression.or() takes exactly 2 parameters, so we chain them + BooleanExpression result = parseBooleanExpression(exprMaps.get(0)); + for (int i = 1; i < exprMaps.size(); i++) { + BooleanExpression next = parseBooleanExpression(exprMaps.get(i)); + result = BooleanExpression.or(result, next); + } + return result; + } + } + // Field-based filter - parse field and create appropriate comparison + String fieldName = (String) args.get("field"); + Expression fieldExpr = Expression.field(fieldName); + + return parseFieldBasedFilter(fieldExpr, args); + } + + /** Parses field-based filter comparisons (isEqualTo, isGreaterThan, etc.). */ + @SuppressWarnings("unchecked") + private BooleanExpression parseFieldBasedFilter( + @NonNull Expression fieldExpr, @NonNull Map args) { + if (args.containsKey("isEqualTo")) { + Object value = args.get("isEqualTo"); + return value instanceof Map + ? fieldExpr.equal(parseExpression((Map) value)) + : fieldExpr.equal(value); + } + if (args.containsKey("isNotEqualTo")) { + Object value = args.get("isNotEqualTo"); + return value instanceof Map + ? fieldExpr.notEqual(parseExpression((Map) value)) + : fieldExpr.notEqual(value); + } + if (args.containsKey("isGreaterThan")) { + Object value = args.get("isGreaterThan"); + return value instanceof Map + ? fieldExpr.greaterThan(parseExpression((Map) value)) + : fieldExpr.greaterThan(value); + } + if (args.containsKey("isGreaterThanOrEqualTo")) { + Object value = args.get("isGreaterThanOrEqualTo"); + return value instanceof Map + ? fieldExpr.greaterThanOrEqual(parseExpression((Map) value)) + : fieldExpr.greaterThanOrEqual(value); + } + if (args.containsKey("isLessThan")) { + Object value = args.get("isLessThan"); + return value instanceof Map + ? fieldExpr.lessThan(parseExpression((Map) value)) + : fieldExpr.lessThan(value); + } + if (args.containsKey("isLessThanOrEqualTo")) { + Object value = args.get("isLessThanOrEqualTo"); + return value instanceof Map + ? fieldExpr.lessThanOrEqual(parseExpression((Map) value)) + : fieldExpr.lessThanOrEqual(value); + } + if (args.containsKey("arrayContains")) { + Object value = args.get("arrayContains"); + return value instanceof Map + ? fieldExpr.arrayContains(parseExpression((Map) value)) + : fieldExpr.arrayContains(value); + } + throw new IllegalArgumentException("Unsupported filter expression format"); + } + + /** + * Parses a Selectable from a map representation. Selectables are Field or AliasedExpression + * types. + */ + @SuppressWarnings("unchecked") + Selectable parseSelectable(@NonNull Map 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; + } + + /** Parses an aggregate function from a map representation. */ + @SuppressWarnings("unchecked") + AggregateFunction parseAggregateFunction(@NonNull Map aggregateMap) { + String functionName = (String) aggregateMap.get("function"); + if (functionName == null) { + // Try "name" as fallback + functionName = (String) aggregateMap.get("name"); + } + Map args = (Map) aggregateMap.get("args"); + Map exprMap; + Expression expr = null; + if (args != null) { + exprMap = (Map) 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); + } + } + + /** + * Parses an AliasedAggregate from a Dart AliasedAggregateFunction map representation. Since Dart + * API only accepts AliasedAggregateFunction, we can directly construct AliasedAggregate. + */ + @SuppressWarnings("unchecked") + AliasedAggregate parseAliasedAggregate(@NonNull Map aggregateMap) { + // Check if this is an aliased aggregate function (Dart AliasedAggregateFunction format) + String name = (String) aggregateMap.get("name"); + if ("alias".equals(name)) { + Map args = (Map) aggregateMap.get("args"); + String alias = (String) args.get("alias"); + Map aggregateFunctionMap = + (Map) args.get("aggregate_function"); + + // Parse the underlying aggregate function + AggregateFunction function = parseAggregateFunction(aggregateFunctionMap); + + // Apply the alias to get AliasedAggregate + return function.alias(alias); + } + + // If not in alias format, it might be a direct aggregate function with alias field + // This shouldn't happen with the new Dart API, but handle for backward compatibility + 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."); + } + + /** Parses an AggregateStage from a map representation. */ + @SuppressWarnings("unchecked") + AggregateStage parseAggregateStage(@NonNull Map stageMap) { + // Parse accumulators (required) + List> accumulatorMaps = + (List>) stageMap.get("accumulators"); + if (accumulatorMaps == null || accumulatorMaps.isEmpty()) { + throw new IllegalArgumentException("AggregateStage must have at least one accumulator"); + } + + // Parse accumulators as AliasedAggregate + AliasedAggregate[] accumulators = new AliasedAggregate[accumulatorMaps.size()]; + for (int i = 0; i < accumulatorMaps.size(); i++) { + accumulators[i] = parseAliasedAggregate(accumulatorMaps.get(i)); + } + + // Build AggregateStage with accumulators + 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); + } + + // Parse optional groups and add them using withGroups() + // withGroups(group: Selectable, vararg additionalGroups: Any) + List> groupMaps = (List>) stageMap.get("groups"); + if (groupMaps != null && !groupMaps.isEmpty()) { + // Parse first group as Selectable (required) + Selectable firstGroup = parseSelectable(groupMaps.get(0)); + + if (groupMaps.size() == 1) { + // Only one group + aggregateStage = aggregateStage.withGroups(firstGroup); + } else { + // Multiple groups - parse remaining as Any[] (varargs) + Object[] additionalGroups = new Object[groupMaps.size() - 1]; + for (int i = 1; i < groupMaps.size(); i++) { + // Parse as Expression first, then convert to Object (can be Selectable or Any) + Expression expr = parseExpression(groupMaps.get(i)); + additionalGroups[i - 1] = expr; + } + aggregateStage = aggregateStage.withGroups(firstGroup, additionalGroups); + } + } + + return aggregateStage; + } + + /** Parses AggregateOptions from a map representation. */ + @SuppressWarnings("unchecked") + AggregateOptions parseAggregateOptions(@NonNull Map optionsMap) { + // For now, AggregateOptions is empty, but this method is ready for future options + return new AggregateOptions(); + } + + /** + * Converts a Dart DistanceMeasure enum name to Android FindNearestStage.DistanceMeasure enum. + * Dart enum values: cosine, euclidean, dotProduct Android enum values: COSINE, EUCLIDEAN, + * DOT_PRODUCT + */ + 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"); + } + } +} diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/PipelineParser.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/PipelineParser.java new file mode 100644 index 000000000000..9528a5ad8eb6 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/PipelineParser.java @@ -0,0 +1,115 @@ +/* + * 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> stages, + @Nullable Map options) + throws Exception { + Pipeline pipeline = buildPipeline(firestore, stages); + Task task = pipeline.execute(); + return Tasks.await(task); + } + + /** + * 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> stages) { + 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 stageMap = stages.get(i); + String stageName = (String) stageMap.get("stage"); + if (stageName == null) { + throw new IllegalArgumentException("Stage must have a 'stage' field"); + } + + Map args = (Map) 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 args, + @NonNull FirebaseFirestore firestore) { + 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> docMaps = (List>) args; + List docRefs = new ArrayList<>(); + for (Map 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); + } + } +} diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/PipelineStageHandlers.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/PipelineStageHandlers.java new file mode 100644 index 000000000000..a1e5525813cb --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/PipelineStageHandlers.java @@ -0,0 +1,338 @@ +/* + * 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.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 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); + default: + throw new IllegalArgumentException("Unknown pipeline stage: " + stageName); + } + } + + private Pipeline handleWhere(@NonNull Pipeline pipeline, @Nullable Map args) { + Map expressionMap = (Map) args.get("expression"); + BooleanExpression booleanExpression = parsers.parseBooleanExpression(expressionMap); + return pipeline.where(booleanExpression); + } + + private Pipeline handleLimit(@NonNull Pipeline pipeline, @Nullable Map args) { + Number limit = (Number) args.get("limit"); + return pipeline.limit(limit.intValue()); + } + + private Pipeline handleOffset(@NonNull Pipeline pipeline, @Nullable Map args) { + Number offset = (Number) args.get("offset"); + return pipeline.offset(offset.intValue()); + } + + @SuppressWarnings("unchecked") + private Pipeline handleSort(@NonNull Pipeline pipeline, @Nullable Map args) { + List> orderingMaps = (List>) args.get("orderings"); + if (orderingMaps == null || orderingMaps.isEmpty()) { + throw new IllegalArgumentException("'sort' requires at least one ordering"); + } + + Map firstMap = orderingMaps.get(0); + Expression expression = + parsers.parseExpression((Map) 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 map = orderingMaps.get(i); + expression = parsers.parseExpression((Map) 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 args) { + List> expressionMaps = (List>) 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 args) { + List> expressionMaps = (List>) 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 args) { + List fieldPaths = (List) 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 args) { + List> expressionMaps = (List>) 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 args) { + List> aggregateMaps = + (List>) 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 args) { + Map aggregateStageMap = (Map) args.get("aggregate_stage"); + + AggregateStage aggregateStage = parsers.parseAggregateStage(aggregateStageMap); + + Map optionsMap = (Map) 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 args) { + Map expressionMap = (Map) 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 args) { + Map expressionMap = (Map) args.get("expression"); + Expression expression = parsers.parseExpression(expressionMap); + return pipeline.replaceWith(expression); + } + + @SuppressWarnings("unchecked") + private Pipeline handleUnion( + @NonNull Pipeline pipeline, + @Nullable Map args, + @NonNull FirebaseFirestore firestore) { + List> nestedStages = (List>) 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 args) { + // Sample stage parsing + Map sampleMap = (Map) args; + // Parse sample configuration + String type = (String) sampleMap.get("type"); + if (type == "percentage") { + double value = (double) sampleMap.get("value"); + return pipeline.sample(SampleStage.withPercentage(value)); + } else { + int value = (int) sampleMap.get("value"); + return pipeline.sample(SampleStage.withDocLimit(value)); + } + } + + @SuppressWarnings("unchecked") + private Pipeline handleFindNearest( + @NonNull Pipeline pipeline, @Nullable Map args) { + String vectorField = (String) args.get("vector_field"); + List vectorValue = (List) 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); + } + } +} diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/document_change_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/document_change_e2e.dart index bc4eb570638d..74e5f9f37ab5 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/document_change_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/document_change_e2e.dart @@ -94,49 +94,43 @@ void runDocumentChangeTests() { // Set something in the database await doc1.set({'name': 'doc1'}); - Stream>> stream = - collection.snapshots(); - int call = 0; - - StreamSubscription subscription = stream.listen( - expectAsync1( - (QuerySnapshot> snapshot) { - call++; - if (call == 1) { - expect(snapshot.docs.length, equals(1)); - expect(snapshot.docChanges.length, equals(1)); - expect(snapshot.docChanges[0], isA()); - DocumentChange> change = - snapshot.docChanges[0]; - expect(change.newIndex, equals(0)); - expect(change.oldIndex, equals(-1)); - expect(change.type, equals(DocumentChangeType.added)); - expect(change.doc.data()!['name'], equals('doc1')); - } else if (call == 2) { - expect(snapshot.docs.length, equals(0)); - expect(snapshot.docChanges.length, equals(1)); - expect(snapshot.docChanges[0], isA()); - DocumentChange> change = - snapshot.docChanges[0]; - expect(change.newIndex, equals(-1)); - expect(change.oldIndex, equals(0)); - expect(change.type, equals(DocumentChangeType.removed)); - expect(change.doc.data()!['name'], equals('doc1')); - } else { - fail('Should not have been called'); - } - }, - count: 2, - reason: 'Stream should only have been called twice.', - ), - ); + final snapshots = >>[]; + final receivedAll = Completer(); + + StreamSubscription subscription = + collection.snapshots().listen((snapshot) { + snapshots.add(snapshot); + if (snapshots.length >= 2 && !receivedAll.isCompleted) { + receivedAll.complete(); + } + }); - await Future.delayed( - const Duration(seconds: 1), - ); // Ensure listener fires + // 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> 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> 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, @@ -154,48 +148,47 @@ void runDocumentChangeTests() { await doc1.set({'value': 1}); await doc2.set({'value': 2}); await doc3.set({'value': 3}); - Stream>> stream = - collection.orderBy('value').snapshots(); - - int call = 0; - StreamSubscription subscription = stream.listen( - expectAsync1( - (QuerySnapshot> snapshot) { - call++; - if (call == 1) { - expect(snapshot.docs.length, equals(3)); - expect(snapshot.docChanges.length, equals(3)); - snapshot.docChanges.asMap().forEach( - (int index, DocumentChange> 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)); - }); - } else if (call == 2) { - expect(snapshot.docs.length, equals(3)); - expect(snapshot.docChanges.length, equals(1)); - DocumentChange> change = - snapshot.docChanges[0]; - expect(change.oldIndex, equals(0)); - expect(change.newIndex, equals(2)); - expect(change.type, equals(DocumentChangeType.modified)); - expect(change.doc.id, equals('doc1')); - } else { - fail('Should not have been called'); - } - }, - count: 2, - reason: 'Stream should only have been called twice.', - ), - ); - await Future.delayed( - const Duration(seconds: 1), - ); // Ensure listener fires + final snapshots = >>[]; + final receivedAll = Completer(); + + 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> 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> 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, ); diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart index c668ea8f6e38..5e570f983331 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/document_reference_e2e.dart @@ -456,6 +456,19 @@ void runDocumentReferenceTests() { expect(data['infinity'], equals(double.infinity)); expect(data['negative_infinity'], equals(double.negativeInfinity)); }); + + test('sets data with DocumentReference as map key', () async { + DocumentReference> document = + await initializeTest('document-set-ref-key'); + DocumentReference> refKey = + FirebaseFirestore.instance.doc('foo/bar'); + await document.set({ + 'myMap': {refKey: 42.0}, + }); + DocumentSnapshot> snapshot = await document.get(); + final myMap = snapshot.data()!['myMap'] as Map; + expect(myMap[refKey.path], equals(42.0)); + }); }); group('DocumentReference.update()', () { @@ -629,5 +642,39 @@ void runDocumentReferenceTests() { 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()); + 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'); + }); + }); }); } diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/instance_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/instance_e2e.dart index fdde6a261eff..108cd2565cfe 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/instance_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/instance_e2e.dart @@ -148,7 +148,43 @@ void runInstanceTests() { await firestore.terminate(); await firestore.clearPersistence(); }, - skip: kIsWeb || defaultTargetPlatform == TargetPlatform.windows, + 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( diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart index d3e663b01ab2..49b7bb0c932a 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/query_e2e.dart @@ -328,53 +328,48 @@ void runQueryTests() { Stream>> stream = collection.snapshots(); - int call = 0; + final snapshots = >>[]; + final receivedAll = Completer(); - StreamSubscription subscription = stream.listen( - expectAsync1( - (QuerySnapshot> snapshot) { - call++; - if (call == 1) { - expect(snapshot.docs.length, equals(1)); - QueryDocumentSnapshot> documentSnapshot = - snapshot.docs[0]; - expect(documentSnapshot.data()['foo'], equals('bar')); - } else if (call == 2) { - expect(snapshot.docs.length, equals(2)); - QueryDocumentSnapshot> documentSnapshot = - snapshot.docs.firstWhere((doc) => doc.id == 'doc1'); - expect(documentSnapshot.data()['bar'], equals('baz')); - } else if (call == 3) { - expect(snapshot.docs.length, equals(1)); - expect( - snapshot.docs.where((doc) => doc.id == 'doc1').isEmpty, - isTrue, - ); - } else if (call == 4) { - expect(snapshot.docs.length, equals(2)); - QueryDocumentSnapshot> documentSnapshot = - snapshot.docs.firstWhere((doc) => doc.id == 'doc2'); - expect(documentSnapshot.data()['foo'], equals('bar')); - } else if (call == 5) { - expect(snapshot.docs.length, equals(2)); - QueryDocumentSnapshot> documentSnapshot = - snapshot.docs.firstWhere((doc) => doc.id == 'doc2'); - expect(documentSnapshot.data()['foo'], equals('baz')); - } else { - fail('Should not have been called'); - } - }, - count: 5, - reason: 'Stream should only have been called five times.', - ), - ); + StreamSubscription subscription = stream.listen((snapshot) { + snapshots.add(snapshot); + if (snapshots.length >= 5 && !receivedAll.isCompleted) { + receivedAll.complete(); + } + }); + // Wait for initial snapshot before making changes await Future.delayed(const Duration(milliseconds: 500)); await collection.doc('doc1').set({'bar': 'baz'}); await collection.doc('doc1').delete(); await collection.doc('doc2').set({'foo': 'bar'}); await collection.doc('doc2').update({'foo': 'baz'}); + // Wait for all 5 snapshots with a timeout instead of hanging forever + await receivedAll.future.timeout(const Duration(seconds: 30)); + + expect(snapshots[0].docs.length, equals(1)); + expect(snapshots[0].docs[0].data()['foo'], equals('bar')); + + expect(snapshots[1].docs.length, equals(2)); + final doc1 = snapshots[1].docs.firstWhere((doc) => doc.id == 'doc1'); + expect(doc1.data()['bar'], equals('baz')); + + expect(snapshots[2].docs.length, equals(1)); + expect( + snapshots[2].docs.where((doc) => doc.id == 'doc1').isEmpty, + isTrue, + ); + + expect(snapshots[3].docs.length, equals(2)); + final doc2set = snapshots[3].docs.firstWhere((doc) => doc.id == 'doc2'); + expect(doc2set.data()['foo'], equals('bar')); + + expect(snapshots[4].docs.length, equals(2)); + final doc2update = + snapshots[4].docs.firstWhere((doc) => doc.id == 'doc2'); + expect(doc2update.data()['foo'], equals('baz')); + await subscription.cancel(); }); diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/settings_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/settings_e2e.dart index c6d0014d0270..10de2f3bef55 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/settings_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/settings_e2e.dart @@ -40,6 +40,45 @@ void runSettingsTest() { settings.webExperimentalLongPollingOptions, ); }); + + test('can apply WebPersistentMultipleTabManager setting', () async { + const settings = Settings( + persistenceEnabled: true, + webPersistentTabManager: WebPersistentMultipleTabManager(), + ); + + firestore.settings = settings; + + expect( + firestore.settings.webPersistentTabManager, + isA(), + ); + }); + + 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()); + expect( + (tabManager! as WebPersistentSingleTabManager).forceOwnership, + true, + ); + }); + + test('webPersistentTabManager defaults to null', () async { + const settings = Settings( + persistenceEnabled: true, + ); + + expect(settings.webPersistentTabManager, isNull); + }); }, ); } diff --git a/packages/cloud_firestore/cloud_firestore/example/ios/Runner/Info.plist b/packages/cloud_firestore/cloud_firestore/example/ios/Runner/Info.plist index 5131b325e791..4326ea3865a3 100644 --- a/packages/cloud_firestore/cloud_firestore/example/ios/Runner/Info.plist +++ b/packages/cloud_firestore/cloud_firestore/example/ios/Runner/Info.plist @@ -47,5 +47,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml b/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml index 8825aa440aea..3895aab98525 100755 --- a/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml +++ b/packages/cloud_firestore/cloud_firestore/example/pubspec.yaml @@ -5,8 +5,8 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - cloud_firestore: ^6.1.2 - firebase_core: ^4.4.0 + cloud_firestore: ^6.1.3 + firebase_core: ^4.5.0 flutter: sdk: flutter http: ^1.0.0 diff --git a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTFirebaseFirestorePlugin.m b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTFirebaseFirestorePlugin.m index b7fa740fe68c..ab826056264f 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTFirebaseFirestorePlugin.m +++ b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTFirebaseFirestorePlugin.m @@ -15,6 +15,7 @@ #import "include/cloud_firestore/Private/FLTFirebaseFirestoreReader.h" #import "include/cloud_firestore/Private/FLTFirebaseFirestoreUtils.h" #import "include/cloud_firestore/Private/FLTLoadBundleStreamHandler.h" +#import "include/cloud_firestore/Private/FLTPipelineParser.h" #import "include/cloud_firestore/Private/FLTQuerySnapshotStreamHandler.h" #import "include/cloud_firestore/Private/FLTSnapshotsInSyncStreamHandler.h" #import "include/cloud_firestore/Private/FLTTransactionStreamHandler.h" @@ -73,6 +74,20 @@ - (NSString *)registerEventChannelWithPrefix:(NSString *)prefix static NSCache *_serverTimestampMap; +static id _Nullable FLTPipelineNullSafe(id value) { + return (value == nil || [value isKindOfClass:[NSNull class]]) ? nil : value; +} + +static NSNumber *_Nullable FLTPipelineTimestampToMs(id value) { + if (!value) return nil; + if ([value isKindOfClass:[NSNumber class]]) return value; + if ([value isKindOfClass:[FIRTimestamp class]]) { + FIRTimestamp *ts = value; + return @((int64_t)ts.seconds * 1000 + (int64_t)ts.nanoseconds / 1000000); + } + return nil; +} + @implementation FLTFirebaseFirestorePlugin { NSMutableDictionary *_eventChannels; NSMutableDictionary *> *_streamHandlers; @@ -883,4 +898,66 @@ - (void)aggregateQueryApp:(nonnull FirestorePigeonFirebaseApp *)app }]; } +- (void)executePipelineApp:(nonnull FirestorePigeonFirebaseApp *)app + stages:(nonnull NSArray *> *)stages + options:(nullable NSDictionary *)options + completion:(nonnull void (^)(PigeonPipelineSnapshot *_Nullable, + FlutterError *_Nullable))completion { + FIRFirestore *firestore = [self getFIRFirestoreFromAppNameFromPigeon:app]; + + [FLTPipelineParser + executePipelineWithFirestore:firestore + stages:stages + options:options + completion:^(id _Nullable snapshot, NSError *_Nullable error) { + if (error) { + completion(nil, [self convertToFlutterError:error]); + return; + } + if (snapshot == nil) { + completion( + nil, + [FlutterError errorWithCode:@"error" + message:@"Pipeline execution returned no result" + details:nil]); + return; + } + + NSMutableArray *pigeonResults = + [NSMutableArray array]; + NSArray *results = [snapshot results]; + if ([results isKindOfClass:[NSArray class]]) { + for (id result in results) { + id ref = [result reference]; + NSString *path = (ref && [ref respondsToSelector:@selector(path)]) + ? [ref path] + : FLTPipelineNullSafe([result documentID]); + NSNumber *createTime = + FLTPipelineTimestampToMs([result valueForKey:@"create_time"]); + NSNumber *updateTime = + FLTPipelineTimestampToMs([result valueForKey:@"update_time"]); + NSDictionary *data = FLTPipelineNullSafe([result data]); + PigeonPipelineResult *pigeonResult = + [PigeonPipelineResult makeWithDocumentPath:path + createTime:createTime + updateTime:updateTime + data:data]; + [pigeonResults addObject:pigeonResult]; + } + } + + NSNumber *executionTime = + FLTPipelineTimestampToMs([snapshot execution_time]); + if (executionTime == nil) { + executionTime = + @((int64_t)([[NSDate date] timeIntervalSince1970] * 1000)); + } + + PigeonPipelineSnapshot *pigeonSnapshot = + [PigeonPipelineSnapshot makeWithResults:pigeonResults + executionTime:executionTime]; + completion(pigeonSnapshot, nil); + }]; +} + @end diff --git a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m new file mode 100644 index 000000000000..4dd518d7929a --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m @@ -0,0 +1,905 @@ +/* + * 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. + */ + +#import "include/cloud_firestore/Private/FLTPipelineParser.h" + +#if TARGET_OS_OSX +#import +#else +@import FirebaseFirestore; +#import "FIRPipelineBridge.h" +#endif + +#import + +static NSString *const kPipelineNotAvailable = + @"Pipeline API is not available. Firestore Pipelines require Firebase iOS SDK with pipeline " + "support."; + +static NSError *pipelineUnavailableError(void) { + return [NSError errorWithDomain:@"FLTFirebaseFirestore" + code:-1 + userInfo:@{NSLocalizedDescriptionKey : kPipelineNotAvailable}]; +} + +#if __has_include("FIRPipelineBridge.h") +#define FLT_PIPELINE_AVAILABLE 1 + +static NSError *parseError(NSString *message) { + return [NSError errorWithDomain:@"FLTFirebaseFirestore" + code:-1 + userInfo:@{NSLocalizedDescriptionKey : message}]; +} + +@interface FLTPipelineExpressionParser : NSObject +@property(nonatomic, strong) FIRFirestore *firestore; +- (instancetype)initWithFirestore:(FIRFirestore *)firestore; +- (FIRExprBridge *)parseExpression:(NSDictionary *)map error:(NSError **)error; +- (FIRExprBridge *)parseBooleanExpression:(NSDictionary *)map + error:(NSError **)error; +- (FIRExprBridge *)rightExprFromValue:(id)value error:(NSError **)error; +@end + +@implementation FLTPipelineExpressionParser + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _firestore = firestore; + } + return self; +} + +- (FIRExprBridge *)parseExpression:(NSDictionary *)map error:(NSError **)error { + NSString *name = map[@"name"]; + if (!name) { + NSDictionary *args = map[@"args"]; + if ([args isKindOfClass:[NSDictionary class]] && args[@"field"]) { + return [[FIRFieldBridge alloc] initWithName:args[@"field"]]; + } + if (error) *error = parseError(@"Expression must have a 'name' field"); + return nil; + } + + NSDictionary *args = map[@"args"]; + if (![args isKindOfClass:[NSDictionary class]]) args = @{}; + + if ([name isEqualToString:@"field"]) { + NSString *field = args[@"field"]; + if (!field) { + if (error) *error = parseError(@"Field expression requires 'field' argument"); + return nil; + } + return [[FIRFieldBridge alloc] initWithName:field]; + } + + if ([name isEqualToString:@"constant"]) { + id value = args[@"value"]; + if (value == nil) { + if (error) *error = parseError(@"Constant requires 'value' argument"); + return nil; + } + if ([value isKindOfClass:[NSDictionary class]]) { + NSString *path = ((NSDictionary *)value)[@"path"]; + if ([path isKindOfClass:[NSString class]] && self.firestore) { + FIRDocumentReference *docRef = [self.firestore documentWithPath:path]; + return [[FIRConstantBridge alloc] init:docRef]; + } + } + return [[FIRConstantBridge alloc] init:value]; + } + + if ([name isEqualToString:@"alias"]) { + id exprMap = args[@"expression"]; + if (![exprMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"Alias requires 'expression'"); + return nil; + } + // No explicit AliasedExpression type in ObjC; aliases are dict keys when building stages. + // Parse and return the inner expression; the caller uses args[@"alias"] as the dict key. + return [self parseExpression:exprMap error:error]; + } + + // Map Dart names to iOS SDK names where they differ + NSString *sdkName = name; + if ([name isEqualToString:@"bit_xor"]) sdkName = @"xor"; + + // ------------------------------------------------------------------------- + // Binary expressions (left + right): comparisons, arithmetic, xor + // ------------------------------------------------------------------------- + static NSArray *binaryNames = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + binaryNames = @[ + @"equal", @"not_equal", @"greater_than", @"greater_than_or_equal", @"less_than", + @"less_than_or_equal", @"add", @"subtract", @"multiply", @"divide", @"modulo" + ]; + }); + if ([binaryNames containsObject:sdkName] || [name isEqualToString:@"bit_xor"]) { + id leftMap = args[@"left"]; + id rightMap = args[@"right"]; + if (![leftMap isKindOfClass:[NSDictionary class]] || + ![rightMap isKindOfClass:[NSDictionary class]]) { + if (error) + *error = + parseError([NSString stringWithFormat:@"%@ requires left and right expressions", name]); + return nil; + } + FIRExprBridge *left = [self parseExpression:leftMap error:error]; + FIRExprBridge *right = [self parseExpression:rightMap error:error]; + if (!left || !right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:sdkName Args:@[ left, right ]]; + } + + // ------------------------------------------------------------------------- + // Unary expressions (single expression): exists, is_error, is_absent, not + // ------------------------------------------------------------------------- + NSArray *unaryNames = @[ @"exists", @"is_error", @"is_absent", @"not" ]; + if ([unaryNames containsObject:name]) { + id exprMap = args[@"expression"]; + if (![exprMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError([NSString stringWithFormat:@"%@ requires expression", name]); + return nil; + } + FIRExprBridge *expr = [name isEqualToString:@"not"] + ? [self parseBooleanExpression:exprMap error:error] + : [self parseExpression:exprMap error:error]; + if (!expr) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:name Args:@[ expr ]]; + } + + // ------------------------------------------------------------------------- + // N-ary logical (expressions array): and, or, xor + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"and"] || [name isEqualToString:@"or"] || + [name isEqualToString:@"xor"]) { + NSArray *exprMaps = args[@"expressions"]; + if (![exprMaps isKindOfClass:[NSArray class]] || exprMaps.count == 0) { + if (error) + *error = + parseError([NSString stringWithFormat:@"%@ requires at least one expression", name]); + return nil; + } + NSMutableArray *all = [NSMutableArray array]; + for (id em in exprMaps) { + if (![em isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *e = [self parseBooleanExpression:em error:error]; + if (!e) return nil; + [all addObject:e]; + } + if (all.count == 0) { + if (error) + *error = + parseError([NSString stringWithFormat:@"%@ requires at least one expression", name]); + return nil; + } + return [[FIRFunctionExprBridge alloc] initWithName:name Args:all]; + } + + // ------------------------------------------------------------------------- + // value + values[]: equal_any, not_equal_any + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"equal_any"] || [name isEqualToString:@"not_equal_any"]) { + id valueMap = args[@"value"]; + NSArray *valuesMaps = args[@"values"]; + if (![valueMap isKindOfClass:[NSDictionary class]] || + ![valuesMaps isKindOfClass:[NSArray class]] || valuesMaps.count == 0) { + if (error) + *error = + parseError([NSString stringWithFormat:@"%@ requires value and non-empty values", name]); + return nil; + } + FIRExprBridge *valueExpr = [self parseExpression:valueMap error:error]; + if (!valueExpr) return nil; + NSMutableArray *valueExprs = [NSMutableArray array]; + for (id vm in valuesMaps) { + if (![vm isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *ve = [self parseExpression:vm error:error]; + if (!ve) return nil; + [valueExprs addObject:ve]; + } + if (valueExprs.count == 0) { + if (error) + *error = parseError([NSString stringWithFormat:@"%@ requires at least one value", name]); + return nil; + } + NSMutableArray *argsArray = [NSMutableArray arrayWithObject:valueExpr]; + [argsArray addObjectsFromArray:valueExprs]; + return [[FIRFunctionExprBridge alloc] initWithName:name Args:argsArray]; + } + + // ------------------------------------------------------------------------- + // array + element: array_contains + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"array_contains"]) { + id arrayMap = args[@"array"]; + id elementMap = args[@"element"]; + if (![arrayMap isKindOfClass:[NSDictionary class]] || + ![elementMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"array_contains requires array and element"); + return nil; + } + FIRExprBridge *arrayExpr = [self parseExpression:arrayMap error:error]; + FIRExprBridge *elementExpr = [self parseExpression:elementMap error:error]; + if (!arrayExpr || !elementExpr) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:name Args:@[ arrayExpr, elementExpr ]]; + } + + // ------------------------------------------------------------------------- + // array + values[]: array_contains_all, array_contains_any + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"array_contains_all"] || + [name isEqualToString:@"array_contains_any"]) { + id arrayMap = args[@"array"]; + NSArray *valuesMaps = args[@"values"]; + if (![valuesMaps isKindOfClass:[NSArray class]]) valuesMaps = args[@"elements"]; + if (![arrayMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError([NSString stringWithFormat:@"%@ requires array", name]); + return nil; + } + FIRExprBridge *arrayExpr = [self parseExpression:arrayMap error:error]; + if (!arrayExpr) return nil; + NSMutableArray *argsArray = [NSMutableArray arrayWithObject:arrayExpr]; + if ([valuesMaps isKindOfClass:[NSArray class]]) { + for (id vm in valuesMaps) { + if (![vm isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *ve = [self parseExpression:vm error:error]; + if (!ve) return nil; + [argsArray addObject:ve]; + } + } + if (argsArray.count < 2) { + if (error) + *error = parseError( + [NSString stringWithFormat:@"%@ requires array and at least one value", name]); + return nil; + } + return [[FIRFunctionExprBridge alloc] initWithName:name Args:argsArray]; + } + + // ------------------------------------------------------------------------- + // PipelineFilter (name "filter"): operator-based (and/or) or field-based + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"filter"]) { + return [self parseFilterExpressionWithArgs:args error:error]; + } + + if (error) *error = parseError([NSString stringWithFormat:@"Unsupported expression: %@", name]); + return nil; +} + +- (FIRExprBridge *)rightExprFromValue:(id)value error:(NSError **)error { + if ([value isKindOfClass:[NSDictionary class]]) { + return [self parseExpression:(NSDictionary *)value error:error]; + } + return [[FIRConstantBridge alloc] init:value]; +} + +- (FIRExprBridge *)parseFilterExpressionWithArgs:(NSDictionary *)args error:(NSError **)error { + // Operator-based: and/or with expressions array (from PipelineFilter.and / .or) + NSString *operator= args[@"operator"]; + NSArray *exprMaps = args[@"expressions"]; + if ([operator isKindOfClass:[NSString class]] && [exprMaps isKindOfClass:[NSArray class]]) { + if (exprMaps.count == 0) { + if (error) *error = parseError(@"filter with operator requires at least one expression"); + return nil; + } + if (exprMaps.count == 1) { + id em = exprMaps[0]; + if (![em isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"filter expressions must be maps"); + return nil; + } + return [self parseBooleanExpression:(NSDictionary *)em error:error]; + } + NSMutableArray *all = [NSMutableArray array]; + for (id em in exprMaps) { + if (![em isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *e = [self parseBooleanExpression:(NSDictionary *)em error:error]; + if (!e) return nil; + [all addObject:e]; + } + if (all.count == 0) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:operator Args:all]; + } + + // Field-based: field + isEqualTo, isGreaterThan, etc. + NSString *fieldName = args[@"field"]; + if (![fieldName isKindOfClass:[NSString class]]) { + if (error) *error = parseError(@"filter requires operator+expressions or field"); + return nil; + } + FIRExprBridge *fieldExpr = [[FIRFieldBridge alloc] initWithName:fieldName]; + + static NSArray *filterComparisonKeys = nil; + static dispatch_once_t filterOnce; + dispatch_once(&filterOnce, ^{ + filterComparisonKeys = @[ + @"isEqualTo", @"isNotEqualTo", @"isGreaterThan", @"isGreaterThanOrEqualTo", @"isLessThan", + @"isLessThanOrEqualTo", @"arrayContains", @"arrayContainsAny", @"whereIn", @"whereNotIn", + @"isNull", @"isNotNull" + ]; + }); + for (NSString *key in filterComparisonKeys) { + id value = args[key]; + if (value == nil) continue; + + if ([key isEqualToString:@"isEqualTo"]) { + FIRExprBridge *right = [self rightExprFromValue:value error:error]; + if (!right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:@"equal" Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"isNotEqualTo"]) { + FIRExprBridge *right = [self rightExprFromValue:value error:error]; + if (!right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:@"not_equal" Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"isGreaterThan"]) { + FIRExprBridge *right = [self rightExprFromValue:value error:error]; + if (!right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:@"greater_than" + Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"isGreaterThanOrEqualTo"]) { + FIRExprBridge *right = [self rightExprFromValue:value error:error]; + if (!right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:@"greater_than_or_equal" + Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"isLessThan"]) { + FIRExprBridge *right = [self rightExprFromValue:value error:error]; + if (!right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:@"less_than" Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"isLessThanOrEqualTo"]) { + FIRExprBridge *right = [self rightExprFromValue:value error:error]; + if (!right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:@"less_than_or_equal" + Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"arrayContains"]) { + FIRExprBridge *right = [self rightExprFromValue:value error:error]; + if (!right) return nil; + return [[FIRFunctionExprBridge alloc] initWithName:@"array_contains" + Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"arrayContainsAny"] || [key isEqualToString:@"whereIn"]) { + NSArray *valuesList = [value isKindOfClass:[NSArray class]] ? value : @[]; + NSMutableArray *valueExprs = [NSMutableArray array]; + for (id v in valuesList) { + FIRExprBridge *ve = [self rightExprFromValue:v error:error]; + if (!ve) return nil; + [valueExprs addObject:ve]; + } + if (valueExprs.count == 0) { + if (error) *error = parseError(@"arrayContainsAny/whereIn requires non-empty list"); + return nil; + } + NSMutableArray *argsArray = [NSMutableArray arrayWithObject:fieldExpr]; + [argsArray addObjectsFromArray:valueExprs]; + return [[FIRFunctionExprBridge alloc] initWithName:@"equal_any" Args:argsArray]; + } + if ([key isEqualToString:@"whereNotIn"]) { + NSArray *valuesList = [value isKindOfClass:[NSArray class]] ? value : @[]; + NSMutableArray *valueExprs = [NSMutableArray array]; + for (id v in valuesList) { + FIRExprBridge *ve = [self rightExprFromValue:v error:error]; + if (!ve) return nil; + [valueExprs addObject:ve]; + } + if (valueExprs.count == 0) { + if (error) *error = parseError(@"whereNotIn requires non-empty list"); + return nil; + } + NSMutableArray *argsArray = [NSMutableArray arrayWithObject:fieldExpr]; + [argsArray addObjectsFromArray:valueExprs]; + return [[FIRFunctionExprBridge alloc] initWithName:@"not_equal_any" Args:argsArray]; + } + if ([key isEqualToString:@"isNull"]) { + FIRExprBridge *right = [[FIRConstantBridge alloc] init:[NSNull null]]; + return [[FIRFunctionExprBridge alloc] initWithName:@"equal" Args:@[ fieldExpr, right ]]; + } + if ([key isEqualToString:@"isNotNull"]) { + FIRExprBridge *right = [[FIRConstantBridge alloc] init:[NSNull null]]; + return [[FIRFunctionExprBridge alloc] initWithName:@"not_equal" Args:@[ fieldExpr, right ]]; + } + } + + if (error) + *error = + parseError(@"filter requires at least one comparison (isEqualTo, isGreaterThan, etc.)"); + return nil; +} + +- (FIRExprBridge *)parseBooleanExpression:(NSDictionary *)map + error:(NSError **)error { + return [self parseExpression:map error:error]; +} + +@end + +@implementation FLTPipelineParser + +/// Returns the key (alias or field name) for an expression map in select/distinct stages. +/// Uses args.alias if present; otherwise for "field" expressions uses args.field. Returns nil if +/// no key can be determined (caller should error). ++ (NSString *)keyForExpressionMap:(NSDictionary *)em error:(NSError **)error { + NSString *alias = [em valueForKeyPath:@"args.alias"]; + if ([alias isKindOfClass:[NSString class]] && alias.length > 0) { + return alias; + } + if ([em[@"name"] isEqualToString:@"field"]) { + NSString *field = [em valueForKeyPath:@"args.field"]; + if ([field isKindOfClass:[NSString class]]) return field; + if (error) *error = parseError(@"field expression must have args.field"); + return nil; + } + if (error) + *error = parseError(@"select/distinct expression must have alias or be a field reference"); + return nil; +} + ++ (NSArray *) + parseStagesWithFirestore:(FIRFirestore *)firestore + stages:(NSArray *> *)stages + error:(NSError **)error { + FLTPipelineExpressionParser *exprParser = + [[FLTPipelineExpressionParser alloc] initWithFirestore:firestore]; + NSMutableArray *stageBridges = [NSMutableArray array]; + NSError *parseErr = nil; + + for (NSUInteger i = 0; i < stages.count; i++) { + NSDictionary *stageMap = stages[i]; + if (![stageMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"Stage must be a map"); + return nil; + } + NSString *stageName = stageMap[@"stage"]; + if (![stageName isKindOfClass:[NSString class]]) { + if (error) *error = parseError(@"Stage must have a 'stage' field"); + return nil; + } + id argsObj = stageMap[@"args"]; + NSDictionary *args = [argsObj isKindOfClass:[NSDictionary class]] ? argsObj : @{}; + NSArray *argsArray = [argsObj isKindOfClass:[NSArray class]] ? argsObj : nil; + + FIRStageBridge *stage = nil; + + if (i == 0) { + if ([stageName isEqualToString:@"collection"]) { + NSString *path = args[@"path"]; + if (!path) { + if (error) *error = parseError(@"collection requires 'path'"); + return nil; + } + FIRCollectionReference *ref = [firestore collectionWithPath:path]; + stage = [[FIRCollectionSourceStageBridge alloc] initWithRef:ref firestore:firestore]; + } else if ([stageName isEqualToString:@"collection_group"]) { + NSString *path = args[@"path"]; + if (!path) { + if (error) *error = parseError(@"collection_group requires 'path'"); + return nil; + } + stage = [[FIRCollectionGroupSourceStageBridge alloc] initWithCollectionId:path]; + } else if ([stageName isEqualToString:@"database"]) { + stage = [[FIRDatabaseSourceStageBridge alloc] init]; + } else if ([stageName isEqualToString:@"documents"]) { + NSArray *docMaps = argsArray; + if (!docMaps || docMaps.count == 0) { + if (error) *error = parseError(@"documents requires array of document refs"); + return nil; + } + NSMutableArray *refs = [NSMutableArray array]; + for (id docMap in docMaps) { + if (![docMap isKindOfClass:[NSDictionary class]]) continue; + NSString *path = ((NSDictionary *)docMap)[@"path"]; + if (path) [refs addObject:[firestore documentWithPath:path]]; + } + stage = [[FIRDocumentsSourceStageBridge alloc] initWithDocuments:refs firestore:firestore]; + } else { + if (error) + *error = parseError( + [NSString stringWithFormat:@"First stage must be collection, collection_group, " + @"documents, or database. Got: %@", + stageName]); + return nil; + } + } else { + if ([stageName isEqualToString:@"where"]) { + id exprMap = args[@"expression"]; + if (![exprMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"where requires expression"); + return nil; + } + FIRExprBridge *expr = [exprParser parseBooleanExpression:exprMap error:&parseErr]; + if (!expr) { + if (error) *error = parseErr; + return nil; + } + stage = [[FIRWhereStageBridge alloc] initWithExpr:expr]; + } else if ([stageName isEqualToString:@"limit"]) { + NSNumber *limit = args[@"limit"]; + if (![limit isKindOfClass:[NSNumber class]]) { + if (error) *error = parseError(@"limit requires numeric limit"); + return nil; + } + stage = [[FIRLimitStageBridge alloc] initWithLimit:limit.intValue]; + } else if ([stageName isEqualToString:@"offset"]) { + NSNumber *offset = args[@"offset"]; + if (![offset isKindOfClass:[NSNumber class]]) { + if (error) *error = parseError(@"offset requires numeric offset"); + return nil; + } + stage = [[FIROffsetStageBridge alloc] initWithOffset:offset.intValue]; + } else if ([stageName isEqualToString:@"sort"]) { + NSArray *orderingMaps = args[@"orderings"]; + if (![orderingMaps isKindOfClass:[NSArray class]] || orderingMaps.count == 0) { + if (error) *error = parseError(@"sort requires at least one ordering"); + return nil; + } + NSMutableArray *orderings = [NSMutableArray array]; + for (id om in orderingMaps) { + if (![om isKindOfClass:[NSDictionary class]]) continue; + id exprMap = ((NSDictionary *)om)[@"expression"]; + NSString *dir = ((NSDictionary *)om)[@"order_direction"]; + if (![exprMap isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *expr = [exprParser parseExpression:exprMap error:&parseErr]; + if (!expr) { + if (error) *error = parseErr; + return nil; + } + NSString *direction = [dir isEqualToString:@"asc"] ? @"ascending" : @"descending"; + FIROrderingBridge *ordering = [[FIROrderingBridge alloc] initWithExpr:expr + Direction:direction]; + [orderings addObject:ordering]; + } + if (orderings.count == 0) { + if (error) *error = parseError(@"sort requires at least one ordering"); + return nil; + } + stage = [[FIRSorStageBridge alloc] initWithOrderings:orderings]; + } else if ([stageName isEqualToString:@"select"]) { + NSArray *exprMaps = args[@"expressions"]; + if (![exprMaps isKindOfClass:[NSArray class]] || exprMaps.count == 0) { + if (error) *error = parseError(@"select requires at least one expression"); + return nil; + } + NSMutableDictionary *fields = [NSMutableDictionary dictionary]; + for (id em in exprMaps) { + if (![em isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *expr = [exprParser parseExpression:em error:&parseErr]; + if (!expr) { + if (error) *error = parseErr; + return nil; + } + NSString *key = [self keyForExpressionMap:em error:error]; + if (!key) return nil; + fields[key] = expr; + } + stage = [[FIRSelectStageBridge alloc] initWithSelections:fields]; + } else if ([stageName isEqualToString:@"add_fields"]) { + NSArray *exprMaps = args[@"expressions"]; + if (![exprMaps isKindOfClass:[NSArray class]] || exprMaps.count == 0) { + if (error) *error = parseError(@"add_fields requires at least one expression"); + return nil; + } + NSMutableDictionary *fields = [NSMutableDictionary dictionary]; + for (id em in exprMaps) { + if (![em isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *expr = [exprParser parseExpression:em error:&parseErr]; + if (!expr) { + if (error) *error = parseErr; + return nil; + } + NSString *alias = [em valueForKeyPath:@"args.alias"]; + if (!alias) { + if (error) *error = parseError(@"add_fields expressions must have alias"); + return nil; + } + fields[alias] = expr; + } + stage = [[FIRAddFieldsStageBridge alloc] initWithFields:fields]; + } else if ([stageName isEqualToString:@"remove_fields"]) { + NSArray *paths = args[@"field_paths"]; + if (![paths isKindOfClass:[NSArray class]] || paths.count == 0) { + if (error) *error = parseError(@"remove_fields requires field_paths"); + return nil; + } + stage = [[FIRRemoveFieldsStageBridge alloc] initWithFields:paths]; + } else if ([stageName isEqualToString:@"distinct"]) { + NSArray *exprMaps = args[@"expressions"]; + if (![exprMaps isKindOfClass:[NSArray class]] || exprMaps.count == 0) { + if (error) *error = parseError(@"distinct requires at least one expression"); + return nil; + } + NSMutableDictionary *fields = [NSMutableDictionary dictionary]; + for (id em in exprMaps) { + if (![em isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *expr = [exprParser parseExpression:em error:&parseErr]; + if (!expr) { + if (error) *error = parseErr; + return nil; + } + NSString *key = [self keyForExpressionMap:em error:error]; + if (!key) return nil; + fields[key] = expr; + } + stage = [[FIRDistinctStageBridge alloc] initWithGroups:fields]; + } else if ([stageName isEqualToString:@"replace_with"]) { + id exprMap = args[@"expression"]; + if (![exprMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"replace_with requires expression"); + return nil; + } + FIRExprBridge *expr = [exprParser parseExpression:exprMap error:&parseErr]; + if (!expr) { + if (error) *error = parseErr; + return nil; + } + stage = [[FIRReplaceWithStageBridge alloc] initWithExpr:expr]; + } else if ([stageName isEqualToString:@"union"]) { + NSArray *nestedStages = args[@"pipeline"]; + if (![nestedStages isKindOfClass:[NSArray class]] || nestedStages.count == 0) { + if (error) *error = parseError(@"union requires non-empty pipeline"); + return nil; + } + id otherPipeline = [self buildPipelineWithFirestore:firestore + stages:nestedStages + error:&parseErr]; + if (!otherPipeline) { + if (error) *error = parseErr; + return nil; + } + stage = [[FIRUnionStageBridge alloc] initWithOther:otherPipeline]; + } else if ([stageName isEqualToString:@"sample"]) { + NSString *type = args[@"type"]; + id val = args[@"value"]; + if ([type isEqualToString:@"percentage"]) { + double v = [val isKindOfClass:[NSNumber class]] ? [(NSNumber *)val doubleValue] : 0; + stage = [[FIRSampleStageBridge alloc] initWithPercentage:v]; + } else { + int v = [val isKindOfClass:[NSNumber class]] ? [(NSNumber *)val intValue] : 0; + stage = [[FIRSampleStageBridge alloc] initWithCount:v]; + } + } else if ([stageName isEqualToString:@"aggregate"]) { + stage = [self parseAggregateStageWithArgs:args exprParser:exprParser error:error]; + } else if ([stageName isEqualToString:@"aggregate_with_options"]) { + stage = [self parseAggregateStageWithOptionsArgs:args exprParser:exprParser error:error]; + } else if ([stageName isEqualToString:@"unnest"]) { + id exprMap = args[@"expression"]; + if (![exprMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"unnest requires expression"); + return nil; + } + FIRExprBridge *fieldExpr = nil; + FIRExprBridge *aliasExpr = nil; + NSDictionary *exprDict = (NSDictionary *)exprMap; + NSString *aliasStr = nil; + if ([exprDict[@"name"] isEqualToString:@"alias"]) { + NSDictionary *aliasArgs = exprDict[@"args"]; + if ([aliasArgs isKindOfClass:[NSDictionary class]] && aliasArgs[@"expression"]) { + fieldExpr = [exprParser parseExpression:aliasArgs[@"expression"] error:&parseErr]; + if (!fieldExpr) { + if (error) *error = parseErr; + return nil; + } + aliasStr = + [aliasArgs[@"alias"] isKindOfClass:[NSString class]] ? aliasArgs[@"alias"] : nil; + } + } + if (!fieldExpr) { + fieldExpr = [exprParser parseExpression:exprMap error:&parseErr]; + if (!fieldExpr) { + if (error) *error = parseErr; + return nil; + } + if (!aliasStr && [exprDict[@"name"] isEqualToString:@"field"]) { + NSDictionary *fieldArgs = exprDict[@"args"]; + aliasStr = + [fieldArgs[@"field"] isKindOfClass:[NSString class]] ? fieldArgs[@"field"] : @"_"; + } + } + if (!aliasStr) aliasStr = @"_"; + aliasExpr = [[FIRFieldBridge alloc] initWithName:aliasStr]; + NSString *indexFieldStr = + [args[@"index_field"] isKindOfClass:[NSString class]] ? args[@"index_field"] : nil; + FIRExprBridge *indexFieldExpr = + (indexFieldStr.length > 0) ? [[FIRFieldBridge alloc] initWithName:indexFieldStr] : nil; + stage = [[FIRUnnestStageBridge alloc] initWithField:fieldExpr + alias:aliasExpr + indexField:indexFieldExpr]; + } else { + if (error) + *error = parseError([NSString stringWithFormat:@"Unknown pipeline stage: %@", stageName]); + return nil; + } + } + + if (stage) [stageBridges addObject:stage]; + } + + if (stageBridges.count == 0) { + if (error && !*error) *error = parseError(@"No valid stages"); + return nil; + } + + return stageBridges; +} + ++ (FIRAggregateFunctionBridge *)aggregateFunctionFromMap:(NSDictionary *)funcMap + exprParser:(FLTPipelineExpressionParser *)exprParser + error:(NSError **)error { + NSString *name = funcMap[@"name"]; + if (![name isKindOfClass:[NSString class]]) { + if (error) *error = parseError(@"Aggregate function must have a 'name'"); + return nil; + } + // Map Dart aggregate function names to iOS SDK names (count_all -> count with no args; minimum -> + // min; maximum -> max) + NSString *iosName = name; + if ([name isEqualToString:@"count_all"]) { + iosName = @"count"; + } else if ([name isEqualToString:@"minimum"]) { + iosName = @"min"; + } else if ([name isEqualToString:@"maximum"]) { + iosName = @"max"; + } + NSDictionary *argsDict = funcMap[@"args"]; + NSMutableArray *argsArray = [NSMutableArray array]; + if ([argsDict isKindOfClass:[NSDictionary class]]) { + id exprMap = argsDict[@"expression"]; + if ([exprMap isKindOfClass:[NSDictionary class]]) { + FIRExprBridge *expr = [exprParser parseExpression:exprMap error:error]; + if (!expr) return nil; + [argsArray addObject:expr]; + } + } + return [[FIRAggregateFunctionBridge alloc] initWithName:iosName Args:argsArray]; +} + ++ (FIRStageBridge *)parseAggregateStageWithArgs:(NSDictionary *)args + exprParser:(FLTPipelineExpressionParser *)exprParser + error:(NSError **)error { + NSArray *accumulatorMaps = args[@"aggregate_functions"]; + if (![accumulatorMaps isKindOfClass:[NSArray class]] || accumulatorMaps.count == 0) { + if (error) *error = parseError(@"aggregate requires aggregate_functions"); + return nil; + } + return [self parseAggregateStageWithAccumulatorMaps:accumulatorMaps + groupMaps:nil + exprParser:exprParser + error:error]; +} + ++ (FIRStageBridge *)parseAggregateStageWithOptionsArgs:(NSDictionary *)args + exprParser:(FLTPipelineExpressionParser *)exprParser + error:(NSError **)error { + NSDictionary *stageMap = args[@"aggregate_stage"]; + if (![stageMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"aggregate_with_options requires aggregate_stage"); + return nil; + } + NSArray *accumulatorMaps = stageMap[@"accumulators"]; + if (![accumulatorMaps isKindOfClass:[NSArray class]] || accumulatorMaps.count == 0) { + accumulatorMaps = stageMap[@"aggregate_functions"]; + } + if (![accumulatorMaps isKindOfClass:[NSArray class]] || accumulatorMaps.count == 0) { + if (error) *error = parseError(@"aggregate_stage requires accumulators or aggregate_functions"); + return nil; + } + NSArray *groupMaps = stageMap[@"groups"]; + return [self parseAggregateStageWithAccumulatorMaps:accumulatorMaps + groupMaps:groupMaps + exprParser:exprParser + error:error]; +} + ++ (FIRStageBridge *)parseAggregateStageWithAccumulatorMaps:(NSArray *)accumulatorMaps + groupMaps:(nullable NSArray *)groupMaps + exprParser:(FLTPipelineExpressionParser *)exprParser + error:(NSError **)error { + NSError *parseErr = nil; + NSMutableDictionary *accumulators = + [NSMutableDictionary dictionary]; + for (id accMap in accumulatorMaps) { + if (![accMap isKindOfClass:[NSDictionary class]]) continue; + NSString *alias = nil; + NSDictionary *funcMap = nil; + if ([accMap[@"name"] isEqualToString:@"alias"]) { + NSDictionary *accArgs = accMap[@"args"]; + if (![accArgs isKindOfClass:[NSDictionary class]]) continue; + alias = accArgs[@"alias"]; + funcMap = accArgs[@"aggregate_function"]; + } + if (![alias isKindOfClass:[NSString class]] || ![funcMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"Each accumulator must have alias and aggregate_function"); + return nil; + } + FIRAggregateFunctionBridge *func = [self aggregateFunctionFromMap:funcMap + exprParser:exprParser + error:&parseErr]; + if (!func) { + if (error) *error = parseErr; + return nil; + } + accumulators[alias] = func; + } + if (accumulators.count == 0) { + if (error) *error = parseError(@"aggregate requires at least one valid accumulator"); + return nil; + } + + NSMutableDictionary *groups = [NSMutableDictionary dictionary]; + if ([groupMaps isKindOfClass:[NSArray class]] && groupMaps.count > 0) { + for (NSUInteger g = 0; g < groupMaps.count; g++) { + id gm = groupMaps[g]; + if (![gm isKindOfClass:[NSDictionary class]]) continue; + FIRExprBridge *expr = [exprParser parseExpression:gm error:&parseErr]; + if (!expr) continue; + groups[[NSString stringWithFormat:@"_%lu", (unsigned long)g]] = expr; + } + } + + return [[FIRAggregateStageBridge alloc] initWithAccumulators:accumulators groups:groups]; +} + ++ (void)executePipelineWithFirestore:(FIRFirestore *)firestore + stages:(NSArray *> *)stages + options:(nullable NSDictionary *)options + completion:(void (^)(id _Nullable snapshot, + NSError *_Nullable error))completion { + if (!stages || stages.count == 0) { + completion(nil, parseError(@"Pipeline requires at least one stage")); + return; + } + + NSError *parseErr = nil; + NSArray *stageBridges = [self parseStagesWithFirestore:firestore + stages:stages + error:&parseErr]; + if (!stageBridges) { + completion(nil, parseErr); + return; + } + + FIRPipelineBridge *pipeline = [[FIRPipelineBridge alloc] initWithStages:stageBridges + db:firestore]; + [pipeline executeWithCompletion:^(id snapshot, NSError *execError) { + if (execError) { + completion(nil, execError); + return; + } + completion(snapshot, nil); + }]; +} + ++ (id)buildPipelineWithFirestore:(FIRFirestore *)firestore + stages:(NSArray *> *)stages + error:(NSError **)error { + NSArray *stageBridges = [self parseStagesWithFirestore:firestore + stages:stages + error:error]; + if (!stageBridges) return nil; + return [[FIRPipelineBridge alloc] initWithStages:stageBridges db:firestore]; +} + +@end + +#else + +@implementation FLTPipelineParser + ++ (void)executePipelineWithFirestore:(FIRFirestore *)firestore + stages:(NSArray *> *)stages + options:(nullable NSDictionary *)options + completion:(void (^)(id _Nullable snapshot, + NSError *_Nullable error))completion { + completion(nil, pipelineUnavailableError()); +} + +@end + +#endif diff --git a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FirestoreMessages.g.m b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FirestoreMessages.g.m index 34e717b694a7..3a6b44eaad00 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FirestoreMessages.g.m +++ b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FirestoreMessages.g.m @@ -168,6 +168,18 @@ + (nullable PigeonQuerySnapshot *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface PigeonPipelineResult () ++ (PigeonPipelineResult *)fromList:(NSArray *)list; ++ (nullable PigeonPipelineResult *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface PigeonPipelineSnapshot () ++ (PigeonPipelineSnapshot *)fromList:(NSArray *)list; ++ (nullable PigeonPipelineSnapshot *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface PigeonGetOptions () + (PigeonGetOptions *)fromList:(NSArray *)list; + (nullable PigeonGetOptions *)nullableFromList:(NSArray *)list; @@ -410,6 +422,66 @@ - (NSArray *)toList { } @end +@implementation PigeonPipelineResult ++ (instancetype)makeWithDocumentPath:(nullable NSString *)documentPath + createTime:(nullable NSNumber *)createTime + updateTime:(nullable NSNumber *)updateTime + data:(nullable NSDictionary *)data { + PigeonPipelineResult *pigeonResult = [[PigeonPipelineResult alloc] init]; + pigeonResult.documentPath = documentPath; + pigeonResult.createTime = createTime; + pigeonResult.updateTime = updateTime; + pigeonResult.data = data; + return pigeonResult; +} ++ (PigeonPipelineResult *)fromList:(NSArray *)list { + PigeonPipelineResult *pigeonResult = [[PigeonPipelineResult alloc] init]; + pigeonResult.documentPath = GetNullableObjectAtIndex(list, 0); + pigeonResult.createTime = GetNullableObjectAtIndex(list, 1); + pigeonResult.updateTime = GetNullableObjectAtIndex(list, 2); + pigeonResult.data = GetNullableObjectAtIndex(list, 3); + return pigeonResult; +} ++ (nullable PigeonPipelineResult *)nullableFromList:(NSArray *)list { + return (list) ? [PigeonPipelineResult fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.documentPath ?: [NSNull null]), + (self.createTime ?: [NSNull null]), + (self.updateTime ?: [NSNull null]), + (self.data ?: [NSNull null]), + ]; +} +@end + +@implementation PigeonPipelineSnapshot ++ (instancetype)makeWithResults:(NSArray *)results + executionTime:(NSNumber *)executionTime { + PigeonPipelineSnapshot *pigeonResult = [[PigeonPipelineSnapshot alloc] init]; + pigeonResult.results = results; + pigeonResult.executionTime = executionTime; + return pigeonResult; +} ++ (PigeonPipelineSnapshot *)fromList:(NSArray *)list { + PigeonPipelineSnapshot *pigeonResult = [[PigeonPipelineSnapshot alloc] init]; + pigeonResult.results = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.results != nil, @""); + pigeonResult.executionTime = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.executionTime != nil, @""); + return pigeonResult; +} ++ (nullable PigeonPipelineSnapshot *)nullableFromList:(NSArray *)list { + return (list) ? [PigeonPipelineSnapshot fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.results ?: [NSNull null]), + (self.executionTime ?: [NSNull null]), + ]; +} +@end + @implementation PigeonGetOptions + (instancetype)makeWithSource:(Source)source serverTimestampBehavior:(ServerTimestampBehavior)serverTimestampBehavior { @@ -673,12 +745,16 @@ - (nullable id)readValueOfType:(UInt8)type { case 136: return [PigeonGetOptions fromList:[self readValue]]; case 137: - return [PigeonQueryParameters fromList:[self readValue]]; + return [PigeonPipelineResult fromList:[self readValue]]; case 138: - return [PigeonQuerySnapshot fromList:[self readValue]]; + return [PigeonPipelineSnapshot fromList:[self readValue]]; case 139: - return [PigeonSnapshotMetadata fromList:[self readValue]]; + return [PigeonQueryParameters fromList:[self readValue]]; case 140: + return [PigeonQuerySnapshot fromList:[self readValue]]; + case 141: + return [PigeonSnapshotMetadata fromList:[self readValue]]; + case 142: return [PigeonTransactionCommand fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -717,18 +793,24 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[PigeonGetOptions class]]) { [self writeByte:136]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[PigeonQueryParameters class]]) { + } else if ([value isKindOfClass:[PigeonPipelineResult class]]) { [self writeByte:137]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[PigeonQuerySnapshot class]]) { + } else if ([value isKindOfClass:[PigeonPipelineSnapshot class]]) { [self writeByte:138]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[PigeonSnapshotMetadata class]]) { + } else if ([value isKindOfClass:[PigeonQueryParameters class]]) { [self writeByte:139]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[PigeonTransactionCommand class]]) { + } else if ([value isKindOfClass:[PigeonQuerySnapshot class]]) { [self writeByte:140]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[PigeonSnapshotMetadata class]]) { + [self writeByte:141]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[PigeonTransactionCommand class]]) { + [self writeByte:142]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -934,11 +1016,12 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setIndexConfigurationApp: - indexConfiguration:completion:)], - @"FirebaseFirestoreHostApi api (%@) doesn't respond to " - @"@selector(setIndexConfigurationApp:indexConfiguration:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector( + setIndexConfigurationApp:indexConfiguration:completion:)], + @"FirebaseFirestoreHostApi api (%@) doesn't respond to " + @"@selector(setIndexConfigurationApp:indexConfiguration:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FirestorePigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1006,11 +1089,11 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(transactionCreateApp: - timeout:maxAttempts:completion:)], - @"FirebaseFirestoreHostApi api (%@) doesn't respond to " - @"@selector(transactionCreateApp:timeout:maxAttempts:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(transactionCreateApp:timeout:maxAttempts:completion:)], + @"FirebaseFirestoreHostApi api (%@) doesn't respond to " + @"@selector(transactionCreateApp:timeout:maxAttempts:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FirestorePigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1034,8 +1117,8 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (transactionStoreResultTransactionId:resultType:commands:completion:)], + NSCAssert([api respondsToSelector:@selector(transactionStoreResultTransactionId:resultType: + commands:completion:)], @"FirebaseFirestoreHostApi api (%@) doesn't respond to " @"@selector(transactionStoreResultTransactionId:resultType:commands:completion:)", api); @@ -1062,11 +1145,11 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(transactionGetApp: - transactionId:path:completion:)], - @"FirebaseFirestoreHostApi api (%@) doesn't respond to " - @"@selector(transactionGetApp:transactionId:path:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(transactionGetApp:transactionId:path:completion:)], + @"FirebaseFirestoreHostApi api (%@) doesn't respond to " + @"@selector(transactionGetApp:transactionId:path:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FirestorePigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1192,8 +1275,8 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (queryGetApp:path:isCollectionGroup:parameters:options:completion:)], + NSCAssert([api respondsToSelector:@selector(queryGetApp:path:isCollectionGroup:parameters: + options:completion:)], @"FirebaseFirestoreHostApi api (%@) doesn't respond to " @"@selector(queryGetApp:path:isCollectionGroup:parameters:options:completion:)", api); @@ -1225,9 +1308,8 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (aggregateQueryApp: - path:parameters:source:queries:isCollectionGroup:completion:)], + NSCAssert([api respondsToSelector:@selector(aggregateQueryApp:path:parameters:source:queries: + isCollectionGroup:completion:)], @"FirebaseFirestoreHostApi api (%@) doesn't respond to " @"@selector(aggregateQueryApp:path:parameters:source:queries:isCollectionGroup:" @"completion:)", @@ -1287,14 +1369,13 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (querySnapshotApp: - path:isCollectionGroup:parameters:options:includeMetadataChanges - :source:completion:)], - @"FirebaseFirestoreHostApi api (%@) doesn't respond to " - @"@selector(querySnapshotApp:path:isCollectionGroup:parameters:options:" - @"includeMetadataChanges:source:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(querySnapshotApp:path:isCollectionGroup:parameters: + options:includeMetadataChanges:source:completion:)], + @"FirebaseFirestoreHostApi api (%@) doesn't respond to " + @"@selector(querySnapshotApp:path:isCollectionGroup:parameters:options:" + @"includeMetadataChanges:source:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FirestorePigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1326,9 +1407,8 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (documentReferenceSnapshotApp: - parameters:includeMetadataChanges:source:completion:)], + NSCAssert([api respondsToSelector:@selector(documentReferenceSnapshotApp:parameters: + includeMetadataChanges:source:completion:)], @"FirebaseFirestoreHostApi api (%@) doesn't respond to " @"@selector(documentReferenceSnapshotApp:parameters:includeMetadataChanges:source:" @"completion:)", @@ -1359,11 +1439,12 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, binaryMessenger:binaryMessenger codec:FirebaseFirestoreHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (persistenceCacheIndexManagerRequestApp:request:completion:)], - @"FirebaseFirestoreHostApi api (%@) doesn't respond to " - @"@selector(persistenceCacheIndexManagerRequestApp:request:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector( + persistenceCacheIndexManagerRequestApp:request:completion:)], + @"FirebaseFirestoreHostApi api (%@) doesn't respond to " + @"@selector(persistenceCacheIndexManagerRequestApp:request:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FirestorePigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1379,4 +1460,32 @@ void FirebaseFirestoreHostApiSetup(id binaryMessenger, [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.cloud_firestore_platform_interface." + @"FirebaseFirestoreHostApi.executePipeline" + binaryMessenger:binaryMessenger + codec:FirebaseFirestoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(executePipelineApp:stages:options:completion:)], + @"FirebaseFirestoreHostApi api (%@) doesn't respond to " + @"@selector(executePipelineApp:stages:options:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FirestorePigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); + NSArray *> *arg_stages = GetNullableObjectAtIndex(args, 1); + NSDictionary *arg_options = GetNullableObjectAtIndex(args, 2); + [api executePipelineApp:arg_app + stages:arg_stages + options:arg_options + completion:^(PigeonPipelineSnapshot *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Private/FLTPipelineParser.h b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Private/FLTPipelineParser.h new file mode 100644 index 000000000000..97c77f0e2a88 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Private/FLTPipelineParser.h @@ -0,0 +1,23 @@ +/* + * 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. + */ + +#import + +@class FIRFirestore; + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTPipelineParser : NSObject + ++ (void)executePipelineWithFirestore:(FIRFirestore *)firestore + stages:(NSArray *> *)stages + options:(nullable NSDictionary *)options + completion: + (void (^)(id _Nullable snapshot, NSError *_Nullable error))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Public/FirestoreMessages.g.h b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Public/FirestoreMessages.g.h index 2eabaeaef25f..a435fda97bbd 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Public/FirestoreMessages.g.h +++ b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Public/FirestoreMessages.g.h @@ -165,6 +165,8 @@ typedef NS_ENUM(NSUInteger, AggregateType) { @class PigeonDocumentSnapshot; @class PigeonDocumentChange; @class PigeonQuerySnapshot; +@class PigeonPipelineResult; +@class PigeonPipelineSnapshot; @class PigeonGetOptions; @class PigeonDocumentOption; @class PigeonTransactionCommand; @@ -243,6 +245,27 @@ typedef NS_ENUM(NSUInteger, AggregateType) { @property(nonatomic, strong) PigeonSnapshotMetadata *metadata; @end +@interface PigeonPipelineResult : NSObject ++ (instancetype)makeWithDocumentPath:(nullable NSString *)documentPath + createTime:(nullable NSNumber *)createTime + updateTime:(nullable NSNumber *)updateTime + data:(nullable NSDictionary *)data; +@property(nonatomic, copy, nullable) NSString *documentPath; +@property(nonatomic, strong, nullable) NSNumber *createTime; +@property(nonatomic, strong, nullable) NSNumber *updateTime; +/// All fields in the result (from PipelineResult.data() on Android). +@property(nonatomic, strong, nullable) NSDictionary *data; +@end + +@interface PigeonPipelineSnapshot : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithResults:(NSArray *)results + executionTime:(NSNumber *)executionTime; +@property(nonatomic, strong) NSArray *results; +@property(nonatomic, strong) NSNumber *executionTime; +@end + @interface PigeonGetOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -416,6 +439,11 @@ NSObject *FirebaseFirestoreHostApiGetCodec(void); - (void)persistenceCacheIndexManagerRequestApp:(FirestorePigeonFirebaseApp *)app request:(PersistenceCacheIndexManagerRequest)request completion:(void (^)(FlutterError *_Nullable))completion; +- (void)executePipelineApp:(FirestorePigeonFirebaseApp *)app + stages:(NSArray *> *)stages + options:(nullable NSDictionary *)options + completion:(void (^)(PigeonPipelineSnapshot *_Nullable, + FlutterError *_Nullable))completion; @end extern void FirebaseFirestoreHostApiSetup(id binaryMessenger, diff --git a/packages/cloud_firestore/cloud_firestore/ios/generated_firebase_sdk_version.txt b/packages/cloud_firestore/cloud_firestore/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/generated_firebase_sdk_version.txt +++ b/packages/cloud_firestore/cloud_firestore/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart index 6efbb19345ce..32937ed915c1 100755 --- a/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/cloud_firestore.dart @@ -28,6 +28,9 @@ export 'package:cloud_firestore_platform_interface/cloud_firestore_platform_inte PersistenceSettings, Settings, WebExperimentalLongPollingOptions, + WebPersistentTabManager, + WebPersistentMultipleTabManager, + WebPersistentSingleTabManager, IndexField, Index, FieldOverrides, @@ -53,7 +56,17 @@ part 'src/filters.dart'; part 'src/firestore.dart'; part 'src/load_bundle_task.dart'; part 'src/load_bundle_task_snapshot.dart'; +part 'pipeline_snapshot.dart'; part 'src/persistent_cache_index_manager.dart'; +part 'src/pipeline.dart'; +part 'src/pipeline_aggregate.dart'; +part 'src/pipeline_distance.dart'; +part 'src/pipeline_execute_options.dart'; +part 'src/pipeline_expression.dart'; +part 'src/pipeline_ordering.dart'; +part 'src/pipeline_sample.dart'; +part 'src/pipeline_source.dart'; +part 'src/pipeline_stage.dart'; part 'src/query.dart'; part 'src/query_document_snapshot.dart'; part 'src/query_snapshot.dart'; diff --git a/packages/cloud_firestore/cloud_firestore/lib/pipeline_snapshot.dart b/packages/cloud_firestore/cloud_firestore/lib/pipeline_snapshot.dart new file mode 100644 index 000000000000..f2adc19a19cb --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/pipeline_snapshot.dart @@ -0,0 +1,33 @@ +// 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. + +part of 'cloud_firestore.dart'; + +/// Result of executing a pipeline +class PipelineResult { + /// The document reference, or null for aggregate-only results (no document row). + final DocumentReference>? document; + final DateTime? createTime; + final DateTime? updateTime; + final Map? _data; + + PipelineResult({ + this.document, + this.createTime, + this.updateTime, + Map? data, + }) : _data = data; + + /// Retrieves all fields in the result as a map. + /// Returns null if the result has no data. + Map? data() => _data; +} + +/// Snapshot containing pipeline execution results +class PipelineSnapshot { + final List result; + final DateTime executionTime; + + PipelineSnapshot._(this.result, this.executionTime); +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart index 4f735629aee1..975717d523b2 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/firestore.dart @@ -282,6 +282,7 @@ class FirebaseFirestore extends FirebasePluginPlatform { settings.webExperimentalAutoDetectLongPolling, webExperimentalLongPollingOptions: settings.webExperimentalLongPollingOptions, + webPersistentTabManager: settings.webPersistentTabManager, ); } @@ -339,6 +340,26 @@ class FirebaseFirestore extends FirebasePluginPlatform { return null; } + /// Returns a [PipelineSource] for creating and executing pipelines. + /// + /// Pipelines allow you to perform complex queries and transformations on + /// Firestore data using a fluent API. + /// + /// Example: + /// ```dart + /// final snapshot = await FirebaseFirestore.instance + /// .pipeline() + /// .collection('users') + /// .where(PipelineFilter(Field('age'), isGreaterThan: 18)) + /// .sort(Field('name').ascending(), Field('age').descending()) + /// .limit(10) + /// .execute(); + /// ``` + // ignore: use_to_and_as_if_applicable + PipelineSource pipeline() { + return PipelineSource._(this); + } + /// Configures indexing for local query execution. Any previous index configuration is overridden. /// /// The index entries themselves are created asynchronously. You can continue to use queries that diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline.dart new file mode 100644 index 000000000000..dd8ee5148ea4 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline.dart @@ -0,0 +1,602 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// A pipeline for querying and transforming Firestore data +class Pipeline { + final FirebaseFirestore _firestore; + final PipelinePlatform _delegate; + + Pipeline._(this._firestore, this._delegate) { + PipelinePlatform.verify(_delegate); + } + + /// Exposes the [stages] on the pipeline delegate. + /// + /// This should only be used for testing to ensure that all + /// pipeline stages are correctly set on the underlying delegate + /// when being tested from a different package. + @visibleForTesting + List> get stages { + return _delegate.stages; + } + + /// Executes the pipeline and returns a snapshot of the results + Future execute({ExecuteOptions? options}) async { + final optionsMap = options != null + ? { + 'indexMode': options.indexMode.name, + } + : null; + final platformSnapshot = await _delegate.execute(options: optionsMap); + return _convertPlatformSnapshot(platformSnapshot); + } + + /// Converts platform snapshot to public snapshot + PipelineSnapshot _convertPlatformSnapshot( + PipelineSnapshotPlatform platformSnapshot, + ) { + final results = platformSnapshot.results.map((platformResult) { + return PipelineResult( + document: platformResult.document != null + ? _JsonDocumentReference(_firestore, platformResult.document!) + : null, + createTime: platformResult.createTime, + updateTime: platformResult.updateTime, + data: platformResult.data, + ); + }).toList(); + + return PipelineSnapshot._(results, platformSnapshot.executionTime); + } + + // Pipeline Actions + + /// Adds fields to documents using expressions + Pipeline addFields( + Selectable selectable1, [ + Selectable? selectable2, + Selectable? selectable3, + Selectable? selectable4, + Selectable? selectable5, + Selectable? selectable6, + Selectable? selectable7, + Selectable? selectable8, + Selectable? selectable9, + Selectable? selectable10, + Selectable? selectable11, + Selectable? selectable12, + Selectable? selectable13, + Selectable? selectable14, + Selectable? selectable15, + Selectable? selectable16, + Selectable? selectable17, + Selectable? selectable18, + Selectable? selectable19, + Selectable? selectable20, + Selectable? selectable21, + Selectable? selectable22, + Selectable? selectable23, + Selectable? selectable24, + Selectable? selectable25, + Selectable? selectable26, + Selectable? selectable27, + Selectable? selectable28, + Selectable? selectable29, + Selectable? selectable30, + ]) { + final selectables = [selectable1]; + if (selectable2 != null) selectables.add(selectable2); + if (selectable3 != null) selectables.add(selectable3); + if (selectable4 != null) selectables.add(selectable4); + if (selectable5 != null) selectables.add(selectable5); + if (selectable6 != null) selectables.add(selectable6); + if (selectable7 != null) selectables.add(selectable7); + if (selectable8 != null) selectables.add(selectable8); + if (selectable9 != null) selectables.add(selectable9); + if (selectable10 != null) selectables.add(selectable10); + if (selectable21 != null) selectables.add(selectable21); + if (selectable22 != null) selectables.add(selectable22); + if (selectable23 != null) selectables.add(selectable23); + if (selectable24 != null) selectables.add(selectable24); + if (selectable25 != null) selectables.add(selectable25); + if (selectable26 != null) selectables.add(selectable26); + if (selectable27 != null) selectables.add(selectable27); + if (selectable28 != null) selectables.add(selectable28); + if (selectable29 != null) selectables.add(selectable29); + if (selectable30 != null) selectables.add(selectable30); + final stage = _AddFieldsStage(selectables); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Aggregates data using aggregate functions + Pipeline aggregate( + AliasedAggregateFunction aggregateFunction1, [ + AliasedAggregateFunction? aggregateFunction2, + AliasedAggregateFunction? aggregateFunction3, + AliasedAggregateFunction? aggregateFunction4, + AliasedAggregateFunction? aggregateFunction5, + AliasedAggregateFunction? aggregateFunction6, + AliasedAggregateFunction? aggregateFunction7, + AliasedAggregateFunction? aggregateFunction8, + AliasedAggregateFunction? aggregateFunction9, + AliasedAggregateFunction? aggregateFunction10, + AliasedAggregateFunction? aggregateFunction11, + AliasedAggregateFunction? aggregateFunction12, + AliasedAggregateFunction? aggregateFunction13, + AliasedAggregateFunction? aggregateFunction14, + AliasedAggregateFunction? aggregateFunction15, + AliasedAggregateFunction? aggregateFunction16, + AliasedAggregateFunction? aggregateFunction17, + AliasedAggregateFunction? aggregateFunction18, + AliasedAggregateFunction? aggregateFunction19, + AliasedAggregateFunction? aggregateFunction20, + AliasedAggregateFunction? aggregateFunction21, + AliasedAggregateFunction? aggregateFunction22, + AliasedAggregateFunction? aggregateFunction23, + AliasedAggregateFunction? aggregateFunction24, + AliasedAggregateFunction? aggregateFunction25, + AliasedAggregateFunction? aggregateFunction26, + AliasedAggregateFunction? aggregateFunction27, + AliasedAggregateFunction? aggregateFunction28, + AliasedAggregateFunction? aggregateFunction29, + AliasedAggregateFunction? aggregateFunction30, + ]) { + final functions = [aggregateFunction1]; + if (aggregateFunction2 != null) functions.add(aggregateFunction2); + if (aggregateFunction3 != null) functions.add(aggregateFunction3); + if (aggregateFunction4 != null) functions.add(aggregateFunction4); + if (aggregateFunction5 != null) functions.add(aggregateFunction5); + if (aggregateFunction6 != null) functions.add(aggregateFunction6); + if (aggregateFunction7 != null) functions.add(aggregateFunction7); + if (aggregateFunction8 != null) functions.add(aggregateFunction8); + if (aggregateFunction9 != null) functions.add(aggregateFunction9); + if (aggregateFunction10 != null) functions.add(aggregateFunction10); + if (aggregateFunction11 != null) functions.add(aggregateFunction11); + if (aggregateFunction12 != null) functions.add(aggregateFunction12); + if (aggregateFunction13 != null) functions.add(aggregateFunction13); + if (aggregateFunction14 != null) functions.add(aggregateFunction14); + if (aggregateFunction15 != null) functions.add(aggregateFunction15); + if (aggregateFunction16 != null) functions.add(aggregateFunction16); + if (aggregateFunction17 != null) functions.add(aggregateFunction17); + if (aggregateFunction18 != null) functions.add(aggregateFunction18); + if (aggregateFunction19 != null) functions.add(aggregateFunction19); + if (aggregateFunction20 != null) functions.add(aggregateFunction20); + if (aggregateFunction21 != null) functions.add(aggregateFunction21); + if (aggregateFunction22 != null) functions.add(aggregateFunction22); + if (aggregateFunction23 != null) functions.add(aggregateFunction23); + if (aggregateFunction24 != null) functions.add(aggregateFunction24); + if (aggregateFunction25 != null) functions.add(aggregateFunction25); + if (aggregateFunction26 != null) functions.add(aggregateFunction26); + if (aggregateFunction27 != null) functions.add(aggregateFunction27); + if (aggregateFunction28 != null) functions.add(aggregateFunction28); + if (aggregateFunction29 != null) functions.add(aggregateFunction29); + if (aggregateFunction30 != null) functions.add(aggregateFunction30); + + final stage = _AggregateStage(functions); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Performs optionally grouped aggregation operations on the documents from previous stages. + /// + /// This method allows you to calculate aggregate values over a set of documents, optionally + /// grouped by one or more fields or expressions. You can specify: + /// + /// - **Grouping Fields or Expressions**: One or more fields or functions to group the documents by. + /// For each distinct combination of values in these fields, a separate group is created. + /// If no grouping fields are provided, a single group containing all documents is used. + /// + /// - **Aggregate Functions**: One or more accumulation operations to perform within each group. + /// These are defined using [AliasedAggregateFunction] expressions, which are typically created + /// by calling `.as('alias')` on [PipelineAggregateFunction] instances. Each aggregation calculates + /// a value (e.g., sum, average, count) based on the documents within its group. + /// + /// Example: + /// ```dart + /// pipeline.aggregateStage( + /// AggregateStage( + /// accumulators: [ + /// Expression.field('likes').sum().as('total_likes'), + /// Expression.field('likes').average().as('avg_likes'), + /// ], + /// groups: [Expression.field('category')], + /// ), + /// ); + /// ``` + /// + /// With options: + /// ```dart + /// pipeline.aggregateStage( + /// AggregateStage( + /// accumulators: [ + /// Expression.field('likes').sum().as('total_likes'), + /// ], + /// ), + /// options: AggregateOptions(), + /// ); + /// ``` + Pipeline aggregateWithOptions( + AggregateStageOptions aggregateStage, { + AggregateOptions? options, + }) { + final stage = _AggregateStageWithOptions( + aggregateStage, + options ?? AggregateOptions(), + ); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Gets distinct values based on expressions + Pipeline distinct( + Selectable expression1, [ + Selectable? expression2, + Selectable? expression3, + Selectable? expression4, + Selectable? expression5, + Selectable? expression6, + Selectable? expression7, + Selectable? expression8, + Selectable? expression9, + Selectable? expression10, + Selectable? expression11, + Selectable? expression12, + Selectable? expression13, + Selectable? expression14, + Selectable? expression15, + Selectable? expression16, + Selectable? expression17, + Selectable? expression18, + Selectable? expression19, + Selectable? expression20, + Selectable? expression21, + Selectable? expression22, + Selectable? expression23, + Selectable? expression24, + Selectable? expression25, + Selectable? expression26, + Selectable? expression27, + Selectable? expression28, + Selectable? expression29, + Selectable? expression30, + ]) { + final expressions = [expression1]; + if (expression2 != null) expressions.add(expression2); + if (expression3 != null) expressions.add(expression3); + if (expression4 != null) expressions.add(expression4); + if (expression5 != null) expressions.add(expression5); + if (expression6 != null) expressions.add(expression6); + if (expression7 != null) expressions.add(expression7); + if (expression8 != null) expressions.add(expression8); + if (expression9 != null) expressions.add(expression9); + if (expression10 != null) expressions.add(expression10); + if (expression11 != null) expressions.add(expression11); + if (expression12 != null) expressions.add(expression12); + if (expression13 != null) expressions.add(expression13); + if (expression14 != null) expressions.add(expression14); + if (expression15 != null) expressions.add(expression15); + if (expression16 != null) expressions.add(expression16); + if (expression17 != null) expressions.add(expression17); + if (expression18 != null) expressions.add(expression18); + if (expression19 != null) expressions.add(expression19); + if (expression20 != null) expressions.add(expression20); + if (expression21 != null) expressions.add(expression21); + if (expression22 != null) expressions.add(expression22); + if (expression23 != null) expressions.add(expression23); + if (expression24 != null) expressions.add(expression24); + if (expression25 != null) expressions.add(expression25); + if (expression26 != null) expressions.add(expression26); + if (expression27 != null) expressions.add(expression27); + if (expression28 != null) expressions.add(expression28); + if (expression29 != null) expressions.add(expression29); + if (expression30 != null) expressions.add(expression30); + + final stage = _DistinctStage(expressions); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Finds nearest vectors using vector similarity search + Pipeline findNearest( + Field vectorField, + List vectorValue, + DistanceMeasure distanceMeasure, { + int? limit, + }) { + final stage = _FindNearestStage(vectorField, vectorValue, distanceMeasure, + limit: limit); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Limits the number of results + Pipeline limit(int limit) { + final stage = _LimitStage(limit); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Offsets the results + Pipeline offset(int offset) { + final stage = _OffsetStage(offset); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Removes specified fields from documents + Pipeline removeFields( + String fieldPath1, [ + String? fieldPath2, + String? fieldPath3, + String? fieldPath4, + String? fieldPath5, + String? fieldPath6, + String? fieldPath7, + String? fieldPath8, + String? fieldPath9, + String? fieldPath10, + String? fieldPath11, + String? fieldPath12, + String? fieldPath13, + String? fieldPath14, + String? fieldPath15, + String? fieldPath16, + String? fieldPath17, + String? fieldPath18, + String? fieldPath19, + String? fieldPath20, + String? fieldPath21, + String? fieldPath22, + String? fieldPath23, + String? fieldPath24, + String? fieldPath25, + String? fieldPath26, + String? fieldPath27, + String? fieldPath28, + String? fieldPath29, + String? fieldPath30, + ]) { + final fieldPaths = [fieldPath1]; + if (fieldPath2 != null) fieldPaths.add(fieldPath2); + if (fieldPath3 != null) fieldPaths.add(fieldPath3); + if (fieldPath4 != null) fieldPaths.add(fieldPath4); + if (fieldPath5 != null) fieldPaths.add(fieldPath5); + if (fieldPath6 != null) fieldPaths.add(fieldPath6); + if (fieldPath7 != null) fieldPaths.add(fieldPath7); + if (fieldPath8 != null) fieldPaths.add(fieldPath8); + if (fieldPath9 != null) fieldPaths.add(fieldPath9); + if (fieldPath10 != null) fieldPaths.add(fieldPath10); + if (fieldPath11 != null) fieldPaths.add(fieldPath11); + if (fieldPath12 != null) fieldPaths.add(fieldPath12); + if (fieldPath13 != null) fieldPaths.add(fieldPath13); + if (fieldPath14 != null) fieldPaths.add(fieldPath14); + if (fieldPath15 != null) fieldPaths.add(fieldPath15); + if (fieldPath16 != null) fieldPaths.add(fieldPath16); + if (fieldPath17 != null) fieldPaths.add(fieldPath17); + if (fieldPath18 != null) fieldPaths.add(fieldPath18); + if (fieldPath19 != null) fieldPaths.add(fieldPath19); + if (fieldPath20 != null) fieldPaths.add(fieldPath20); + if (fieldPath21 != null) fieldPaths.add(fieldPath21); + if (fieldPath22 != null) fieldPaths.add(fieldPath22); + if (fieldPath23 != null) fieldPaths.add(fieldPath23); + if (fieldPath24 != null) fieldPaths.add(fieldPath24); + if (fieldPath25 != null) fieldPaths.add(fieldPath25); + if (fieldPath26 != null) fieldPaths.add(fieldPath26); + if (fieldPath27 != null) fieldPaths.add(fieldPath27); + if (fieldPath28 != null) fieldPaths.add(fieldPath28); + if (fieldPath29 != null) fieldPaths.add(fieldPath29); + if (fieldPath30 != null) fieldPaths.add(fieldPath30); + + final stage = _RemoveFieldsStage(fieldPaths); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Replaces documents with the result of an expression + Pipeline replaceWith(Expression expression) { + final stage = _ReplaceWithStage(expression); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Samples documents using a sampling strategy + Pipeline sample(PipelineSample sample) { + final stage = _SampleStage(sample); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Selects specific fields using selectable expressions + Pipeline select( + Selectable expression1, [ + Selectable? expression2, + Selectable? expression3, + Selectable? expression4, + Selectable? expression5, + Selectable? expression6, + Selectable? expression7, + Selectable? expression8, + Selectable? expression9, + Selectable? expression10, + Selectable? expression11, + Selectable? expression12, + Selectable? expression13, + Selectable? expression14, + Selectable? expression15, + Selectable? expression16, + Selectable? expression17, + Selectable? expression18, + Selectable? expression19, + Selectable? expression20, + Selectable? expression21, + Selectable? expression22, + Selectable? expression23, + Selectable? expression24, + Selectable? expression25, + Selectable? expression26, + Selectable? expression27, + Selectable? expression28, + Selectable? expression29, + Selectable? expression30, + ]) { + final expressions = [expression1]; + if (expression2 != null) expressions.add(expression2); + if (expression3 != null) expressions.add(expression3); + if (expression4 != null) expressions.add(expression4); + if (expression5 != null) expressions.add(expression5); + if (expression6 != null) expressions.add(expression6); + if (expression7 != null) expressions.add(expression7); + if (expression8 != null) expressions.add(expression8); + if (expression9 != null) expressions.add(expression9); + if (expression10 != null) expressions.add(expression10); + if (expression11 != null) expressions.add(expression11); + if (expression12 != null) expressions.add(expression12); + if (expression13 != null) expressions.add(expression13); + if (expression14 != null) expressions.add(expression14); + if (expression15 != null) expressions.add(expression15); + if (expression16 != null) expressions.add(expression16); + if (expression17 != null) expressions.add(expression17); + if (expression18 != null) expressions.add(expression18); + if (expression19 != null) expressions.add(expression19); + if (expression20 != null) expressions.add(expression20); + if (expression21 != null) expressions.add(expression21); + if (expression22 != null) expressions.add(expression22); + if (expression23 != null) expressions.add(expression23); + if (expression24 != null) expressions.add(expression24); + if (expression25 != null) expressions.add(expression25); + if (expression26 != null) expressions.add(expression26); + if (expression27 != null) expressions.add(expression27); + if (expression28 != null) expressions.add(expression28); + if (expression29 != null) expressions.add(expression29); + if (expression30 != null) expressions.add(expression30); + + final stage = _SelectStage(expressions); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Sorts results using one or more ordering specifications. + /// Orderings are applied in sequence (e.g. primary sort by first, then by second, etc.). + Pipeline sort( + Ordering order, [ + Ordering? order2, + Ordering? order3, + Ordering? order4, + Ordering? order5, + Ordering? order6, + Ordering? order7, + Ordering? order8, + Ordering? order9, + Ordering? order10, + Ordering? order11, + Ordering? order12, + Ordering? order13, + Ordering? order14, + Ordering? order15, + Ordering? order16, + Ordering? order17, + Ordering? order18, + Ordering? order19, + Ordering? order20, + Ordering? order21, + Ordering? order22, + Ordering? order23, + Ordering? order24, + Ordering? order25, + Ordering? order26, + Ordering? order27, + Ordering? order28, + Ordering? order29, + Ordering? order30, + ]) { + final orderings = [order]; + if (order2 != null) orderings.add(order2); + if (order3 != null) orderings.add(order3); + if (order4 != null) orderings.add(order4); + if (order5 != null) orderings.add(order5); + if (order6 != null) orderings.add(order6); + if (order7 != null) orderings.add(order7); + if (order8 != null) orderings.add(order8); + if (order9 != null) orderings.add(order9); + if (order10 != null) orderings.add(order10); + if (order11 != null) orderings.add(order11); + if (order12 != null) orderings.add(order12); + if (order13 != null) orderings.add(order13); + if (order14 != null) orderings.add(order14); + if (order15 != null) orderings.add(order15); + if (order16 != null) orderings.add(order16); + if (order17 != null) orderings.add(order17); + if (order18 != null) orderings.add(order18); + if (order19 != null) orderings.add(order19); + if (order20 != null) orderings.add(order20); + if (order21 != null) orderings.add(order21); + if (order22 != null) orderings.add(order22); + if (order23 != null) orderings.add(order23); + if (order24 != null) orderings.add(order24); + if (order25 != null) orderings.add(order25); + if (order26 != null) orderings.add(order26); + if (order27 != null) orderings.add(order27); + if (order28 != null) orderings.add(order28); + if (order29 != null) orderings.add(order29); + if (order30 != null) orderings.add(order30); + + final stage = _SortStage(orderings); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Unnests arrays into separate documents + Pipeline unnest(Selectable expression, [String? indexField]) { + final stage = _UnnestStage(expression, indexField); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Unions results with another pipeline + Pipeline union(Pipeline pipeline) { + final stage = _UnionStage(pipeline); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } + + /// Filters documents using a boolean expression + Pipeline where(BooleanExpression expression) { + final stage = _WhereStage(expression); + return Pipeline._( + _firestore, + _delegate.addStage(stage.toMap()), + ); + } +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_aggregate.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_aggregate.dart new file mode 100644 index 000000000000..175195dc7f08 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_aggregate.dart @@ -0,0 +1,206 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Base class for aggregate functions used in pipelines +abstract class PipelineAggregateFunction implements PipelineSerializable { + /// Assigns an alias to this aggregate function + AliasedAggregateFunction as(String alias) { + return AliasedAggregateFunction( + alias: alias, + aggregateFunction: this, + ); + } + + String get name; + + @override + Map toMap() { + return { + 'name': name, + }; + } +} + +/// Represents an aggregate function with an alias +class AliasedAggregateFunction implements PipelineSerializable { + final String _alias; + final PipelineAggregateFunction aggregateFunction; + + AliasedAggregateFunction({ + required String alias, + required this.aggregateFunction, + }) : _alias = alias; + + String get alias => _alias; + + @override + Map toMap() { + return { + 'name': 'alias', + 'args': { + 'alias': _alias, + 'aggregate_function': aggregateFunction.toMap(), + }, + }; + } +} + +/// Counts all documents in the pipeline result +class CountAll extends PipelineAggregateFunction { + CountAll(); + + @override + String get name => 'count_all'; +} + +/// Counts non-null values of the specified expression +class Count extends PipelineAggregateFunction { + final Expression expression; + + Count(this.expression); + + @override + String get name => 'count'; + + @override + Map toMap() { + final map = super.toMap(); + map['args'] = { + 'expression': expression.toMap(), + }; + return map; + } +} + +/// Sums numeric values of the specified expression +class Sum extends PipelineAggregateFunction { + final Expression expression; + + Sum(this.expression); + + @override + String get name => 'sum'; + + @override + Map toMap() { + final map = super.toMap(); + map['args'] = { + 'expression': expression.toMap(), + }; + return map; + } +} + +/// Calculates average of numeric values of the specified expression +class Average extends PipelineAggregateFunction { + final Expression expression; + + Average(this.expression); + + @override + String get name => 'average'; + + @override + Map toMap() { + final map = super.toMap(); + map['args'] = { + 'expression': expression.toMap(), + }; + return map; + } +} + +/// Counts distinct values of the specified expression +class CountDistinct extends PipelineAggregateFunction { + final Expression expression; + + CountDistinct(this.expression); + + @override + String get name => 'count_distinct'; + + @override + Map toMap() { + final map = super.toMap(); + map['args'] = { + 'expression': expression.toMap(), + }; + return map; + } +} + +/// Finds minimum value of the specified expression +class Minimum extends PipelineAggregateFunction { + final Expression expression; + + Minimum(this.expression); + + @override + String get name => 'minimum'; + + @override + Map toMap() { + final map = super.toMap(); + map['args'] = { + 'expression': expression.toMap(), + }; + return map; + } +} + +/// Finds maximum value of the specified expression +class Maximum extends PipelineAggregateFunction { + final Expression expression; + + Maximum(this.expression); + + @override + String get name => 'maximum'; + + @override + Map toMap() { + final map = super.toMap(); + map['args'] = { + 'expression': expression.toMap(), + }; + return map; + } +} + +/// Represents an aggregate stage with functions and optional grouping +class AggregateStageOptions implements PipelineSerializable { + final List accumulators; + final List? groups; + + AggregateStageOptions({ + required this.accumulators, + this.groups, + }); + + @override + Map toMap() { + final map = { + 'accumulators': accumulators.map((acc) => acc.toMap()).toList(), + }; + if (groups != null && groups!.isNotEmpty) { + map['groups'] = groups!.map((group) => group.toMap()).toList(); + } + return map; + } +} + +/// Options for aggregate operations +class AggregateOptions implements PipelineSerializable { + // Add any aggregate-specific options here as needed + // For now, this is a placeholder for future options + + AggregateOptions(); + + @override + Map toMap() { + return {}; + } +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_distance.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_distance.dart new file mode 100644 index 000000000000..8f01b16de52e --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_distance.dart @@ -0,0 +1,17 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Distance measure algorithms for vector similarity search +enum DistanceMeasure { + /// Cosine similarity + cosine, + + /// Euclidean distance + euclidean, + + /// Dot product + dotProduct, +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_execute_options.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_execute_options.dart new file mode 100644 index 000000000000..ee8b8e2eb42d --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_execute_options.dart @@ -0,0 +1,20 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Index mode for pipeline execution +enum IndexMode { + /// Use recommended index mode + recommended, +} + +/// Options for executing a pipeline +class ExecuteOptions { + final IndexMode indexMode; + + const ExecuteOptions({ + this.indexMode = IndexMode.recommended, + }); +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart new file mode 100644 index 000000000000..ee0b7be60c61 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart @@ -0,0 +1,3197 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Base interface for pipeline serialization +mixin PipelineSerializable { + Map toMap(); +} + +/// Helper function to convert values to Expression (wraps in Constant if needed) +Expression _toExpression(Object? value) { + if (value == null) return Constant(null); + if (value is Expression) return value; + if (value is List) return Expression.array(value.cast()); + if (value is Map) { + return Expression.map(value.cast()); + } + return Constant(value); +} + +/// Base class for all pipeline expressions +abstract class Expression implements PipelineSerializable { + /// Creates an aliased expression + AliasedExpression as(String alias) { + return AliasedExpression( + alias: alias, + expression: this, + ); + } + + /// Creates a descending ordering for this expression + Ordering descending() { + return Ordering(this, OrderDirection.desc); + } + + /// Creates an ascending ordering for this expression + Ordering ascending() { + return Ordering(this, OrderDirection.asc); + } + + // ============================================================================ + // CONDITIONAL / LOGIC OPERATIONS + // ============================================================================ + + /// Returns an alternative expression if this expression is absent + Expression ifAbsent(Expression elseExpr) { + return _IfAbsentExpression(this, elseExpr); + } + + /// Returns an alternative value if this expression is absent + Expression ifAbsentValue(Object? elseValue) { + return _IfAbsentExpression(this, _toExpression(elseValue)); + } + + /// Returns an alternative expression if this expression errors + Expression ifError(Expression catchExpr) { + return _IfErrorExpression(this, catchExpr); + } + + /// Returns an alternative value if this expression errors + Expression ifErrorValue(Object? catchValue) { + return _IfErrorExpression(this, _toExpression(catchValue)); + } + + /// Checks if this expression is absent (null/undefined) + // ignore: use_to_and_as_if_applicable + BooleanExpression isAbsent() { + return _IsAbsentExpression(this); + } + + /// Checks if this expression produces an error + // ignore: use_to_and_as_if_applicable + BooleanExpression isError() { + return _IsErrorExpression(this); + } + + /// Checks if this field expression exists in the document + // ignore: use_to_and_as_if_applicable + BooleanExpression exists() { + return _ExistsExpression(this); + } + + // ============================================================================ + // TYPE CONVERSION + // ============================================================================ + + /// Casts this expression to a boolean expression + BooleanExpression asBoolean() { + return _AsBooleanExpression(this); + } + + /// Converts this expression to a string with a format + Expression toStringWithFormat(Expression format) { + return _ToStringWithFormatExpression(this, format); + } + + /// Converts this expression to a string with a literal format + Expression toStringWithFormatLiteral(String format) { + return _ToStringWithFormatExpression(this, Constant(format)); + } + + // ============================================================================ + // BITWISE OPERATIONS + // ============================================================================ + + /// Performs bitwise AND with another expression + Expression bitAnd(Expression bitsOther) { + return _BitAndExpression(this, bitsOther); + } + + /// Performs bitwise AND with byte array + Expression bitAndBytes(List bitsOther) { + return _BitAndExpression(this, Constant(bitsOther)); + } + + /// Performs bitwise OR with another expression + Expression bitOr(Expression bitsOther) { + return _BitOrExpression(this, bitsOther); + } + + /// Performs bitwise OR with byte array + Expression bitOrBytes(List bitsOther) { + return _BitOrExpression(this, Constant(bitsOther)); + } + + /// Performs bitwise XOR with another expression + Expression bitXor(Expression bitsOther) { + return _BitXorExpression(this, bitsOther); + } + + /// Performs bitwise XOR with byte array + Expression bitXorBytes(List bitsOther) { + return _BitXorExpression(this, Constant(bitsOther)); + } + + /// Performs bitwise NOT on this expression + // ignore: use_to_and_as_if_applicable + Expression bitNot() { + return _BitNotExpression(this); + } + + /// Shifts bits left by an expression amount + Expression bitLeftShift(Expression numberExpr) { + return _BitLeftShiftExpression(this, numberExpr); + } + + /// Shifts bits left by a literal amount + Expression bitLeftShiftLiteral(int number) { + return _BitLeftShiftExpression(this, Constant(number)); + } + + /// Shifts bits right by an expression amount + Expression bitRightShift(Expression numberExpr) { + return _BitRightShiftExpression(this, numberExpr); + } + + /// Shifts bits right by a literal amount + Expression bitRightShiftLiteral(int number) { + return _BitRightShiftExpression(this, Constant(number)); + } + + // ============================================================================ + // DOCUMENT / PATH OPERATIONS + // ============================================================================ + + /// Returns the document ID from this path expression + // ignore: use_to_and_as_if_applicable + Expression documentId() { + return _DocumentIdExpression(this); + } + + /// Returns the collection ID from this path expression + // ignore: use_to_and_as_if_applicable + Expression collectionId() { + return _CollectionIdExpression(this); + } + + // ============================================================================ + // MAP OPERATIONS + // ============================================================================ + + /// Gets a value from this map expression by key expression + Expression mapGet(Expression key) { + return _MapGetExpression(this, key); + } + + /// Gets a value from this map expression by literal key + Expression mapGetLiteral(String key) { + return _MapGetExpression(this, Constant(key)); + } + + /// Returns the keys from this map expression + // ignore: use_to_and_as_if_applicable + Expression mapKeys() { + return _MapKeysExpression(this); + } + + /// Returns the values from this map expression + // ignore: use_to_and_as_if_applicable + Expression mapValues() { + return _MapValuesExpression(this); + } + + // ============================================================================ + // ALIASING + // ============================================================================ + + /// Assigns an alias to this expression for use in output + Selectable alias(String alias) { + return AliasedExpression(alias: alias, expression: this); + } + + // ============================================================================ + // ARITHMETIC OPERATIONS + // ============================================================================ + + /// Adds this expression to another expression + Expression add(Expression other) { + return _AddExpression(this, other); + } + + /// Adds a number to this expression + Expression addNumber(num other) { + return _AddExpression(this, Constant(other)); + } + + /// Subtracts another expression from this expression + Expression subtract(Expression other) { + return _SubtractExpression(this, other); + } + + /// Subtracts a number from this expression + Expression subtractNumber(num other) { + return _SubtractExpression(this, Constant(other)); + } + + /// Multiplies this expression by another expression + Expression multiply(Expression other) { + return _MultiplyExpression(this, other); + } + + /// Multiplies this expression by a number + Expression multiplyNumber(num other) { + return _MultiplyExpression(this, Constant(other)); + } + + /// Divides this expression by another expression + Expression divide(Expression other) { + return _DivideExpression(this, other); + } + + /// Divides this expression by a number + Expression divideNumber(num other) { + return _DivideExpression(this, Constant(other)); + } + + /// Returns the remainder of dividing this expression by another + Expression modulo(Expression other) { + return _ModuloExpression(this, other); + } + + /// Returns the remainder of dividing this expression by a number + Expression moduloNumber(num other) { + return _ModuloExpression(this, Constant(other)); + } + + /// Returns the absolute value of this expression + // ignore: use_to_and_as_if_applicable + Expression abs() { + return _AbsExpression(this); + } + + /// Returns the negation of this expression + // ignore: use_to_and_as_if_applicable + Expression negate() { + return _NegateExpression(this); + } + + // ============================================================================ + // COMPARISON OPERATIONS (return BooleanExpression) + // ============================================================================ + + /// Checks if this expression equals another expression + BooleanExpression equal(Expression other) { + return _EqualExpression(this, other); + } + + /// Checks if this expression equals a value + BooleanExpression equalValue(Object? value) { + return _EqualExpression(this, _toExpression(value)); + } + + /// Checks if this expression does not equal another expression + BooleanExpression notEqual(Expression other) { + return _NotEqualExpression(this, other); + } + + /// Checks if this expression does not equal a value + BooleanExpression notEqualValue(Object? value) { + return _NotEqualExpression(this, _toExpression(value)); + } + + /// Checks if this expression is greater than another expression + BooleanExpression greaterThan(Expression other) { + return _GreaterThanExpression(this, other); + } + + /// Checks if this expression is greater than a value + BooleanExpression greaterThanValue(Object? value) { + return _GreaterThanExpression(this, _toExpression(value)); + } + + /// Checks if this expression is greater than or equal to another expression + BooleanExpression greaterThanOrEqual(Expression other) { + return _GreaterThanOrEqualExpression(this, other); + } + + /// Checks if this expression is greater than or equal to a value + BooleanExpression greaterThanOrEqualValue(Object? value) { + return _GreaterThanOrEqualExpression(this, _toExpression(value)); + } + + /// Checks if this expression is less than another expression + BooleanExpression lessThan(Expression other) { + return _LessThanExpression(this, other); + } + + /// Checks if this expression is less than a value + BooleanExpression lessThanValue(Object? value) { + return _LessThanExpression(this, _toExpression(value)); + } + + /// Checks if this expression is less than or equal to another expression + BooleanExpression lessThanOrEqual(Expression other) { + return _LessThanOrEqualExpression(this, other); + } + + /// Checks if this expression is less than or equal to a value + BooleanExpression lessThanOrEqualValue(Object? value) { + return _LessThanOrEqualExpression(this, _toExpression(value)); + } + + // ============================================================================ + // STRING OPERATIONS + // ============================================================================ + + /// Returns the length of this string expression + // ignore: use_to_and_as_if_applicable + Expression length() { + return _LengthExpression(this); + } + + /// Concatenates this expression with other expressions/values + Expression concat(List others) { + final expressions = [this]; + for (final other in others) { + expressions.add(_toExpression(other)); + } + return _ConcatExpression(expressions); + } + + /// Converts this string expression to lowercase + Expression toLowerCase() { + return _ToLowerCaseExpression(this); + } + + /// Converts this string expression to uppercase + Expression toUpperCase() { + return _ToUpperCaseExpression(this); + } + + /// Extracts a substring from this string expression + Expression substring(Expression start, Expression end) { + return _SubstringExpression(this, start, end); + } + + /// Extracts a substring using literal indices + Expression substringLiteral(int start, int end) { + return _SubstringExpression(this, Constant(start), Constant(end)); + } + + /// Replaces occurrences of a pattern in this string + Expression replace(Expression find, Expression replacement) { + return _ReplaceExpression(this, find, replacement); + } + + /// Replaces occurrences of a string literal + Expression replaceLiteral(String find, String replacement) { + return _ReplaceExpression(this, Constant(find), Constant(replacement)); + } + + /// Splits this string expression by a delimiter + Expression split(Expression delimiter) { + return _SplitExpression(this, delimiter); + } + + /// Splits this string by a literal delimiter + Expression splitLiteral(String delimiter) { + return _SplitExpression(this, Constant(delimiter)); + } + + /// Joins array elements with a delimiter + Expression join(Expression delimiter) { + return _JoinExpression(this, delimiter); + } + + /// Joins array elements with a literal delimiter + Expression joinLiteral(String delimiter) { + return _JoinExpression(this, Constant(delimiter)); + } + + /// Trims whitespace from this string expression + // ignore: use_to_and_as_if_applicable + Expression trim() { + return _TrimExpression(this); + } + + // ============================================================================ + // ARRAY OPERATIONS + // ============================================================================ + + /// Concatenates this array with another array expression + Expression arrayConcat(Expression secondArray) { + return _ArrayConcatExpression(this, secondArray); + } + + /// Concatenates this array with multiple arrays/values + Expression arrayConcatMultiple(List otherArrays) { + final expressions = [this]; + for (final other in otherArrays) { + expressions.add(_toExpression(other)); + } + return _ArrayConcatMultipleExpression(expressions); + } + + /// Checks if this array contains an element expression + BooleanExpression arrayContainsElement(Expression element) { + return _ArrayContainsExpression(this, element); + } + + /// Checks if this array contains a value + BooleanExpression arrayContainsValue(Object? element) { + return _ArrayContainsExpression(this, _toExpression(element)); + } + + /// Checks if this array contains any of the given values or expressions + BooleanExpression arrayContainsAny(List values) { + return _ArrayContainsAnyExpression( + this, values.map(_toExpression).toList()); + } + + /// Returns the length of this array expression + // ignore: use_to_and_as_if_applicable + Expression arrayLength() { + return _ArrayLengthExpression(this); + } + + /// Reverses this array expression + // ignore: use_to_and_as_if_applicable + Expression arrayReverse() { + return _ArrayReverseExpression(this); + } + + /// Returns the sum of numeric elements in this array + // ignore: use_to_and_as_if_applicable + Expression arraySum() { + return _ArraySumExpression(this); + } + + /// Extracts a slice from this array + Expression arraySlice(Expression start, Expression end) { + return _ArraySliceExpression(this, start, end); + } + + /// Extracts a slice using literal indices + Expression arraySliceLiteral(int start, int end) { + return _ArraySliceExpression(this, Constant(start), Constant(end)); + } + + // ============================================================================ + // AGGREGATE FUNCTIONS + // ============================================================================ + + /// Creates a sum aggregation function from this expression + // ignore: use_to_and_as_if_applicable + PipelineAggregateFunction sum() { + return Sum(this); + } + + /// Creates an average aggregation function from this expression + // ignore: use_to_and_as_if_applicable + PipelineAggregateFunction average() { + return Average(this); + } + + /// Creates a count aggregation function from this expression + // ignore: use_to_and_as_if_applicable + PipelineAggregateFunction count() { + return Count(this); + } + + /// Creates a count distinct aggregation function from this expression + // ignore: use_to_and_as_if_applicable + PipelineAggregateFunction countDistinct() { + return CountDistinct(this); + } + + /// Creates a minimum aggregation function from this expression + // ignore: use_to_and_as_if_applicable + PipelineAggregateFunction minimum() { + return Minimum(this); + } + + /// Creates a maximum aggregation function from this expression + // ignore: use_to_and_as_if_applicable + PipelineAggregateFunction maximum() { + return Maximum(this); + } + + String get name; + + @override + Map toMap() { + return { + 'name': name, + }; + } + + // ============================================================================ + // STATIC FACTORY METHODS + // ============================================================================ + + /// Creates a constant expression from a string value + static Expression constantString(String value) => Constant(value); + + /// Creates a constant expression from a number value + static Expression constantNumber(num value) => Constant(value); + + /// Creates a constant expression from a boolean value + static Expression constantBoolean(bool value) => Constant(value); + + /// Creates a constant expression from a DateTime value + static Expression constantDateTime(DateTime value) => Constant(value); + + /// Creates a constant expression from a Timestamp value + static Expression constantTimestamp(Timestamp value) => Constant(value); + + /// Creates a constant expression from a GeoPoint value + static Expression constantGeoPoint(GeoPoint value) => Constant(value); + + /// Creates a constant expression from a Blob value + static Expression constantBlob(Blob value) => Constant(value); + + /// Creates a constant expression from a DocumentReference value + static Expression constantDocumentReference(DocumentReference value) => + Constant(value); + + /// Creates a constant expression from a byte array value + static Expression constantBytes(List value) => Constant(value); + + /// Creates a constant expression from a VectorValue value + static Expression constantVector(VectorValue value) => Constant(value); + + /// Creates a constant expression from any value (convenience) + /// + /// Valid types: String, num, bool, DateTime, Timestamp, GeoPoint, List (byte[]), + /// Blob, DocumentReference, VectorValue + static Expression constant(Object? value) { + if (value == null) { + return Constant(null); + } + // Validate that the value is one of the accepted types + if (value is! String && + value is! num && + value is! bool && + value is! DateTime && + value is! Timestamp && + value is! GeoPoint && + value is! List && + value is! Blob && + value is! DocumentReference && + value is! VectorValue) { + throw ArgumentError( + 'Constant value must be one of: String, num, bool, DateTime, Timestamp, ' + 'GeoPoint, List (byte[]), Blob, DocumentReference, or VectorValue. ' + 'Got: ${value.runtimeType}', + ); + } + return Constant(value); + } + + /// Creates a field reference expression from a field path string + static Field field(String fieldPath) => Field(fieldPath); + + /// Creates a field reference expression from a FieldPath object + static Field fieldPath(FieldPath fieldPath) => Field(fieldPath.toString()); + + /// Creates a null value expression + static Expression nullValue() => _NullExpression(); + + /// Creates a conditional (ternary) expression + static Expression conditional( + BooleanExpression condition, + Expression thenExpr, + Expression elseExpr, + ) { + return _ConditionalExpression(condition, thenExpr, elseExpr); + } + + /// Creates a conditional expression with literal values + static Expression conditionalValues( + BooleanExpression condition, + Object? thenValue, + Object? elseValue, + ) { + return _ConditionalExpression( + condition, + _toExpression(thenValue), + _toExpression(elseValue), + ); + } + + /// Creates an array expression from elements + static Expression array(List elements) { + return _ArrayExpression( + elements.map(_toExpression).toList(), + ); + } + + /// Creates a map expression from key-value pairs + static Expression map(Map data) { + return _MapExpression(data.map((k, v) => MapEntry(k, _toExpression(v)))); + } + + /// Creates a map expression from alternating key-value expressions + static Expression mapFromPairs(List keyValuePairs) { + return _MapFromPairsExpression(keyValuePairs); + } + + /// Returns the current timestamp + static Expression currentTimestamp() { + return _CurrentTimestampExpression(); + } + + /// Adds time to a timestamp expression + static Expression timestampAdd( + Expression timestamp, + String unit, + Expression amount, + ) { + return _TimestampAddExpression(timestamp, unit, amount); + } + + /// Adds time to a timestamp with a literal amount + static Expression timestampAddLiteral( + Expression timestamp, + String unit, + int amount, + ) { + return _TimestampAddExpression(timestamp, unit, Constant(amount)); + } + + /// Subtracts time from a timestamp expression + static Expression timestampSubtract( + Expression timestamp, + String unit, + Expression amount, + ) { + return _TimestampSubtractExpression(timestamp, unit, amount); + } + + /// Subtracts time from a timestamp with a literal amount + static Expression timestampSubtractLiteral( + Expression timestamp, + String unit, + int amount, + ) { + return _TimestampSubtractExpression(timestamp, unit, Constant(amount)); + } + + /// Calculates the difference between two timestamps + static Expression timestampDiff( + Expression timestamp1, + Expression timestamp2, + String unit, + ) { + return _TimestampDiffExpression(timestamp1, timestamp2, unit); + } + + /// Truncates a timestamp to a specific unit + static Expression timestampTruncate( + Expression timestamp, + String unit, + ) { + return _TimestampTruncateExpression(timestamp, unit); + } + + /// Calculates the distance between two GeoPoint expressions + static Expression distance( + Expression geoPoint1, + Expression geoPoint2, + ) { + return _DistanceExpression(geoPoint1, geoPoint2); + } + + /// Creates a document ID expression from a DocumentReference + static Expression documentIdFromRef(DocumentReference docRef) { + return _DocumentIdFromRefExpression(docRef); + } + + /// Checks if a value is in a list (IN operator) + static BooleanExpression equalAny( + Expression value, + List values, + ) { + return _EqualAnyExpression(value, values.map(_toExpression).toList()); + } + + /// Checks if a value is not in a list (NOT IN operator) + static BooleanExpression notEqualAny( + Expression value, + List values, + ) { + return _NotEqualAnyExpression(value, values.map(_toExpression).toList()); + } + + /// Checks if a field exists in the document + static BooleanExpression existsField(String fieldName) { + return _ExistsExpression(Field(fieldName)); + } + + /// Returns an expression if another is absent + static Expression ifAbsentStatic( + Expression ifExpr, + Expression elseExpr, + ) { + return _IfAbsentExpression(ifExpr, elseExpr); + } + + /// Returns a value if an expression is absent + static Expression ifAbsentValueStatic( + Expression ifExpr, + Object? elseValue, + ) { + return _IfAbsentExpression(ifExpr, _toExpression(elseValue)); + } + + /// Checks if an expression is absent + static BooleanExpression isAbsentStatic(Expression value) { + return _IsAbsentExpression(value); + } + + /// Checks if a field is absent + static BooleanExpression isAbsentField(String fieldName) { + return _IsAbsentExpression(Field(fieldName)); + } + + /// Returns an expression if another errors + static BooleanExpression ifErrorStatic( + BooleanExpression tryExpr, + BooleanExpression catchExpr, + ) { + return _IfErrorExpression(tryExpr, catchExpr) as BooleanExpression; + } + + /// Checks if an expression produces an error + static BooleanExpression isErrorStatic(Expression expr) { + return _IsErrorExpression(expr); + } + + /// Negates a boolean expression + static BooleanExpression not(BooleanExpression expression) { + return _NotExpression(expression); + } + + /// Combines boolean expressions with a logical XOR + static BooleanExpression xor( + BooleanExpression expression1, [ + BooleanExpression? expression2, + BooleanExpression? expression3, + BooleanExpression? expression4, + BooleanExpression? expression5, + BooleanExpression? expression6, + BooleanExpression? expression7, + BooleanExpression? expression8, + BooleanExpression? expression9, + BooleanExpression? expression10, + BooleanExpression? expression11, + BooleanExpression? expression12, + BooleanExpression? expression13, + BooleanExpression? expression14, + BooleanExpression? expression15, + BooleanExpression? expression16, + BooleanExpression? expression17, + BooleanExpression? expression18, + BooleanExpression? expression19, + BooleanExpression? expression20, + BooleanExpression? expression21, + BooleanExpression? expression22, + BooleanExpression? expression23, + BooleanExpression? expression24, + BooleanExpression? expression25, + BooleanExpression? expression26, + BooleanExpression? expression27, + BooleanExpression? expression28, + BooleanExpression? expression29, + BooleanExpression? expression30, + ]) { + final expressions = [expression1]; + if (expression2 != null) expressions.add(expression2); + if (expression3 != null) expressions.add(expression3); + if (expression4 != null) expressions.add(expression4); + if (expression5 != null) expressions.add(expression5); + if (expression6 != null) expressions.add(expression6); + if (expression7 != null) expressions.add(expression7); + if (expression8 != null) expressions.add(expression8); + if (expression9 != null) expressions.add(expression9); + if (expression10 != null) expressions.add(expression10); + if (expression11 != null) expressions.add(expression11); + if (expression12 != null) expressions.add(expression12); + if (expression13 != null) expressions.add(expression13); + if (expression14 != null) expressions.add(expression14); + if (expression15 != null) expressions.add(expression15); + if (expression16 != null) expressions.add(expression16); + if (expression17 != null) expressions.add(expression17); + if (expression18 != null) expressions.add(expression18); + if (expression19 != null) expressions.add(expression19); + if (expression20 != null) expressions.add(expression20); + if (expression21 != null) expressions.add(expression21); + if (expression22 != null) expressions.add(expression22); + if (expression23 != null) expressions.add(expression23); + if (expression24 != null) expressions.add(expression24); + if (expression25 != null) expressions.add(expression25); + if (expression26 != null) expressions.add(expression26); + if (expression27 != null) expressions.add(expression27); + if (expression28 != null) expressions.add(expression28); + if (expression29 != null) expressions.add(expression29); + if (expression30 != null) expressions.add(expression30); + return _XorExpression(expressions); + } + + /// Combines boolean expressions with a logical AND + static BooleanExpression and( + BooleanExpression expression1, [ + BooleanExpression? expression2, + BooleanExpression? expression3, + BooleanExpression? expression4, + BooleanExpression? expression5, + BooleanExpression? expression6, + BooleanExpression? expression7, + BooleanExpression? expression8, + BooleanExpression? expression9, + BooleanExpression? expression10, + BooleanExpression? expression11, + BooleanExpression? expression12, + BooleanExpression? expression13, + BooleanExpression? expression14, + BooleanExpression? expression15, + BooleanExpression? expression16, + BooleanExpression? expression17, + BooleanExpression? expression18, + BooleanExpression? expression19, + BooleanExpression? expression20, + BooleanExpression? expression21, + BooleanExpression? expression22, + BooleanExpression? expression23, + BooleanExpression? expression24, + BooleanExpression? expression25, + BooleanExpression? expression26, + BooleanExpression? expression27, + BooleanExpression? expression28, + BooleanExpression? expression29, + BooleanExpression? expression30, + ]) { + final expressions = [expression1]; + if (expression2 != null) expressions.add(expression2); + if (expression3 != null) expressions.add(expression3); + if (expression4 != null) expressions.add(expression4); + if (expression5 != null) expressions.add(expression5); + if (expression6 != null) expressions.add(expression6); + if (expression7 != null) expressions.add(expression7); + if (expression8 != null) expressions.add(expression8); + if (expression9 != null) expressions.add(expression9); + if (expression10 != null) expressions.add(expression10); + if (expression11 != null) expressions.add(expression11); + if (expression12 != null) expressions.add(expression12); + if (expression13 != null) expressions.add(expression13); + if (expression14 != null) expressions.add(expression14); + if (expression15 != null) expressions.add(expression15); + if (expression16 != null) expressions.add(expression16); + if (expression17 != null) expressions.add(expression17); + if (expression18 != null) expressions.add(expression18); + if (expression19 != null) expressions.add(expression19); + if (expression20 != null) expressions.add(expression20); + if (expression21 != null) expressions.add(expression21); + if (expression22 != null) expressions.add(expression22); + if (expression23 != null) expressions.add(expression23); + if (expression24 != null) expressions.add(expression24); + if (expression25 != null) expressions.add(expression25); + if (expression26 != null) expressions.add(expression26); + if (expression27 != null) expressions.add(expression27); + if (expression28 != null) expressions.add(expression28); + if (expression29 != null) expressions.add(expression29); + if (expression30 != null) expressions.add(expression30); + return _AndExpression(expressions); + } + + /// Combines boolean expressions with a logical OR + static BooleanExpression or( + BooleanExpression expression1, [ + BooleanExpression? expression2, + BooleanExpression? expression3, + BooleanExpression? expression4, + BooleanExpression? expression5, + BooleanExpression? expression6, + BooleanExpression? expression7, + BooleanExpression? expression8, + BooleanExpression? expression9, + BooleanExpression? expression10, + BooleanExpression? expression11, + BooleanExpression? expression12, + BooleanExpression? expression13, + BooleanExpression? expression14, + BooleanExpression? expression15, + BooleanExpression? expression16, + BooleanExpression? expression17, + BooleanExpression? expression18, + BooleanExpression? expression19, + BooleanExpression? expression20, + BooleanExpression? expression21, + BooleanExpression? expression22, + BooleanExpression? expression23, + BooleanExpression? expression24, + BooleanExpression? expression25, + BooleanExpression? expression26, + BooleanExpression? expression27, + BooleanExpression? expression28, + BooleanExpression? expression29, + BooleanExpression? expression30, + ]) { + final expressions = [expression1]; + if (expression2 != null) expressions.add(expression2); + if (expression3 != null) expressions.add(expression3); + if (expression4 != null) expressions.add(expression4); + if (expression5 != null) expressions.add(expression5); + if (expression6 != null) expressions.add(expression6); + if (expression7 != null) expressions.add(expression7); + if (expression8 != null) expressions.add(expression8); + if (expression9 != null) expressions.add(expression9); + if (expression10 != null) expressions.add(expression10); + if (expression11 != null) expressions.add(expression11); + if (expression12 != null) expressions.add(expression12); + if (expression13 != null) expressions.add(expression13); + if (expression14 != null) expressions.add(expression14); + if (expression15 != null) expressions.add(expression15); + if (expression16 != null) expressions.add(expression16); + if (expression17 != null) expressions.add(expression17); + if (expression18 != null) expressions.add(expression18); + if (expression19 != null) expressions.add(expression19); + if (expression20 != null) expressions.add(expression20); + if (expression21 != null) expressions.add(expression21); + if (expression22 != null) expressions.add(expression22); + if (expression23 != null) expressions.add(expression23); + if (expression24 != null) expressions.add(expression24); + if (expression25 != null) expressions.add(expression25); + if (expression26 != null) expressions.add(expression26); + if (expression27 != null) expressions.add(expression27); + if (expression28 != null) expressions.add(expression28); + if (expression29 != null) expressions.add(expression29); + if (expression30 != null) expressions.add(expression30); + return _OrExpression(expressions); + } + + /// Joins array elements with a delimiter + static Expression joinStatic( + Expression arrayExpression, + Expression delimiterExpression, + ) { + return _JoinExpression(arrayExpression, delimiterExpression); + } + + /// Joins array elements with a literal delimiter + static Expression joinStaticLiteral( + Expression arrayExpression, + String delimiter, + ) { + return _JoinExpression(arrayExpression, Constant(delimiter)); + } + + /// Joins a field's array with a delimiter + static Expression joinField( + String arrayFieldName, + String delimiter, + ) { + return _JoinExpression(Field(arrayFieldName), Constant(delimiter)); + } + + /// Concatenates arrays + static Expression arrayConcatStatic( + Expression firstArray, + Expression secondArray, + List? otherArrays, + ) { + final expressions = [firstArray, secondArray]; + if (otherArrays != null) { + for (final other in otherArrays) { + expressions.add(_toExpression(other)); + } + } + return _ArrayConcatMultipleExpression(expressions); + } + + /// Returns the length of an expression + static Expression lengthStatic(Expression expr) { + return _LengthExpression(expr); + } + + /// Returns the length of a field + static Expression lengthField(String fieldName) { + return _LengthExpression(Field(fieldName)); + } + + /// Returns the absolute value of an expression + static Expression absStatic(Expression numericExpr) { + return _AbsExpression(numericExpr); + } + + /// Returns the absolute value of a field + static Expression absField(String numericField) { + return _AbsExpression(Field(numericField)); + } + + /// Negates an expression + static Expression negateStatic(Expression numericExpr) { + return _NegateExpression(numericExpr); + } + + /// Negates a field + static Expression negateField(String numericField) { + return _NegateExpression(Field(numericField)); + } + + /// Adds two expressions + static Expression addStatic( + Expression first, + Expression second, + ) { + return _AddExpression(first, second); + } + + /// Adds an expression and a number + static Expression addStaticNumber( + Expression first, + num second, + ) { + return _AddExpression(first, Constant(second)); + } + + /// Adds a field and an expression + static Expression addField( + String numericFieldName, + Expression second, + ) { + return _AddExpression(Field(numericFieldName), second); + } + + /// Adds a field and a number + static Expression addFieldNumber( + String numericFieldName, + num second, + ) { + return _AddExpression(Field(numericFieldName), Constant(second)); + } + + /// Subtracts two expressions + static Expression subtractStatic( + Expression minuend, + Expression subtrahend, + ) { + return _SubtractExpression(minuend, subtrahend); + } + + /// Multiplies two expressions + static Expression multiplyStatic( + Expression multiplicand, + Expression multiplier, + ) { + return _MultiplyExpression(multiplicand, multiplier); + } + + /// Divides two expressions + static Expression divideStatic( + Expression dividend, + Expression divisor, + ) { + return _DivideExpression(dividend, divisor); + } + + /// Returns modulo of two expressions + static Expression moduloStatic( + Expression dividend, + Expression divisor, + ) { + return _ModuloExpression(dividend, divisor); + } + + /// Compares two expressions for equality + static BooleanExpression equalStatic( + Expression left, + Expression right, + ) { + return _EqualExpression(left, right); + } + + /// Compares expression with value for equality + static BooleanExpression equalStaticValue( + Expression left, + Object? right, + ) { + return _EqualExpression(left, _toExpression(right)); + } + + /// Compares field with value for equality + static BooleanExpression equalField( + String fieldName, + Object? value, + ) { + return _EqualExpression(Field(fieldName), _toExpression(value)); + } + + /// Compares two expressions for inequality + static BooleanExpression notEqualStatic( + Expression left, + Expression right, + ) { + return _NotEqualExpression(left, right); + } + + /// Compares expression with value for inequality + static BooleanExpression notEqualStaticValue( + Expression left, + Object? right, + ) { + return _NotEqualExpression(left, _toExpression(right)); + } + + /// Greater than comparison + static BooleanExpression greaterThanStatic( + Expression left, + Expression right, + ) { + return _GreaterThanExpression(left, right); + } + + /// Greater than comparison with value + static BooleanExpression greaterThanStaticValue( + Expression left, + Object? right, + ) { + return _GreaterThanExpression(left, _toExpression(right)); + } + + /// Greater than comparison for field + static BooleanExpression greaterThanField( + String fieldName, + Object? value, + ) { + return _GreaterThanExpression(Field(fieldName), _toExpression(value)); + } + + /// Greater than or equal comparison + static BooleanExpression greaterThanOrEqualStatic( + Expression left, + Expression right, + ) { + return _GreaterThanOrEqualExpression(left, right); + } + + /// Less than comparison + static BooleanExpression lessThanStatic( + Expression left, + Expression right, + ) { + return _LessThanExpression(left, right); + } + + /// Less than comparison with value + static BooleanExpression lessThanStaticValue( + Expression left, + Object? right, + ) { + return _LessThanExpression(left, _toExpression(right)); + } + + /// Less than comparison for field + static BooleanExpression lessThanField( + String fieldName, + Object? value, + ) { + return _LessThanExpression(Field(fieldName), _toExpression(value)); + } + + /// Less than or equal comparison + static BooleanExpression lessThanOrEqualStatic( + Expression left, + Expression right, + ) { + return _LessThanOrEqualExpression(left, right); + } + + /// Concatenates expressions + static Expression concatStatic( + Expression first, + Expression second, + List? others, + ) { + final expressions = [first, second]; + if (others != null) { + for (final other in others) { + expressions.add(_toExpression(other)); + } + } + return _ConcatExpression(expressions); + } + + /// Converts to lowercase + static Expression toLowerCaseStatic(Expression stringExpr) { + return _ToLowerCaseExpression(stringExpr); + } + + /// Converts field to lowercase + static Expression toLowerCaseField(String stringField) { + return _ToLowerCaseExpression(Field(stringField)); + } + + /// Converts to uppercase + static Expression toUpperCaseStatic(Expression stringExpr) { + return _ToUpperCaseExpression(stringExpr); + } + + /// Converts field to uppercase + static Expression toUpperCaseField(String stringField) { + return _ToUpperCaseExpression(Field(stringField)); + } + + /// Trims whitespace + static Expression trimStatic(Expression stringExpr) { + return _TrimExpression(stringExpr); + } + + /// Trims field whitespace + static Expression trimField(String stringField) { + return _TrimExpression(Field(stringField)); + } + + /// Extracts substring + static Expression substringStatic( + Expression stringExpr, + Expression start, + Expression end, + ) { + return _SubstringExpression(stringExpr, start, end); + } + + /// Replaces in string + static Expression replaceStatic( + Expression stringExpr, + Expression find, + Expression replacement, + ) { + return _ReplaceExpression(stringExpr, find, replacement); + } + + /// Splits string + static Expression splitStatic( + Expression stringExpr, + Expression delimiter, + ) { + return _SplitExpression(stringExpr, delimiter); + } + + /// Reverses array + static Expression arrayReverseStatic(Expression array) { + return _ArrayReverseExpression(array); + } + + /// Reverses field array + static Expression arrayReverseField(String arrayFieldName) { + return _ArrayReverseExpression(Field(arrayFieldName)); + } + + /// Sums array + static Expression arraySumStatic(Expression array) { + return _ArraySumExpression(array); + } + + /// Sums field array + static Expression arraySumField(String arrayFieldName) { + return _ArraySumExpression(Field(arrayFieldName)); + } + + /// Gets array length + static Expression arrayLengthStatic(Expression array) { + return _ArrayLengthExpression(array); + } + + /// Gets field array length + static Expression arrayLengthField(String arrayFieldName) { + return _ArrayLengthExpression(Field(arrayFieldName)); + } + + /// Slices array + static Expression arraySliceStatic( + Expression array, + Expression start, + Expression end, + ) { + return _ArraySliceExpression(array, start, end); + } + + /// Checks array contains + static BooleanExpression arrayContainsElementStatic( + Expression array, + Expression element, + ) { + return _ArrayContainsExpression(array, element); + } + + /// Checks field array contains + static BooleanExpression arrayContainsField( + String arrayFieldName, + Object? element, + ) { + return _ArrayContainsExpression( + Field(arrayFieldName), _toExpression(element)); + } + + /// Creates a raw/custom function expression + static Expression rawFunction( + String name, + List args, + ) { + return _RawFunctionExpression(name, args); + } +} + +/// Base class for function expressions +abstract class FunctionExpression extends Expression {} + +/// Base class for selectable expressions (can be used in select stage) +abstract class Selectable extends Expression { + String get aliasName; + Expression get expression; +} + +/// Represents an aliased expression wrapper +class AliasedExpression extends Selectable { + final String _alias; + + @override + String get aliasName => _alias; + + @override + final Expression expression; + + AliasedExpression({ + required String alias, + required this.expression, + }) : _alias = alias; + + @override + String get name => 'alias'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'alias': _alias, + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a field reference in a pipeline expression +class Field extends Selectable { + final String fieldName; + + Field(this.fieldName); + + @override + String get name => 'field'; + + @override + String get aliasName => fieldName; + + @override + Expression get expression => this; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'field': fieldName, + }, + }; + } +} + +/// Represents a null value expression +class _NullExpression extends Expression { + _NullExpression(); + + @override + String get name => 'null'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'value': null, + }, + }; + } +} + +/// Represents a constant value in a pipeline expression +/// +/// Valid types: String, num, bool, DateTime, Timestamp, GeoPoint, List (byte[]), +/// Blob, DocumentReference, VectorValue, or null +class Constant extends Expression { + final Object? value; + + Constant(this.value) { + if (value != null) { + // Validate that the value is one of the accepted types + if (value is! String && + value is! num && + value is! bool && + value is! DateTime && + value is! Timestamp && + value is! GeoPoint && + value is! List && + value is! Blob && + value is! DocumentReference && + value is! VectorValue) { + throw ArgumentError( + 'Constant value must be one of: String, num, bool, DateTime, Timestamp, ' + 'GeoPoint, List (byte[]), Blob, DocumentReference, or VectorValue. ' + 'Got: ${value.runtimeType}', + ); + } + } + } + + @override + String get name => 'constant'; + + @override + Map toMap() { + Object? serializedValue = value; + if (value is DocumentReference) { + serializedValue = {'path': (value! as DocumentReference).path}; + } + return { + 'name': name, + 'args': {'value': serializedValue}, + }; + } +} + +/// Represents a concatenation function expression +class Concat extends FunctionExpression { + final List expressions; + + Concat(this.expressions); + + @override + String get name => 'concat'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expressions': expressions.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Represents a concat function expression (internal) +class _ConcatExpression extends FunctionExpression { + final List expressions; + + _ConcatExpression(this.expressions); + + @override + String get name => 'concat'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expressions': expressions.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Represents a length function expression +class _LengthExpression extends FunctionExpression { + final Expression expression; + + _LengthExpression(this.expression); + + @override + String get name => 'length'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a toLowerCase function expression +class _ToLowerCaseExpression extends FunctionExpression { + final Expression expression; + + _ToLowerCaseExpression(this.expression); + + @override + String get name => 'to_lower_case'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a toUpperCase function expression +class _ToUpperCaseExpression extends FunctionExpression { + final Expression expression; + + _ToUpperCaseExpression(this.expression); + + @override + String get name => 'to_upper_case'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a substring function expression +class _SubstringExpression extends FunctionExpression { + final Expression expression; + final Expression start; + final Expression end; + + _SubstringExpression(this.expression, this.start, this.end); + + @override + String get name => 'substring'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'start': start.toMap(), + 'end': end.toMap(), + }, + }; + } +} + +/// Represents a replace function expression +class _ReplaceExpression extends FunctionExpression { + final Expression expression; + final Expression find; + final Expression replacement; + + _ReplaceExpression(this.expression, this.find, this.replacement); + + @override + String get name => 'replace'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'find': find.toMap(), + 'replacement': replacement.toMap(), + }, + }; + } +} + +/// Represents a split function expression +class _SplitExpression extends FunctionExpression { + final Expression expression; + final Expression delimiter; + + _SplitExpression(this.expression, this.delimiter); + + @override + String get name => 'split'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'delimiter': delimiter.toMap(), + }, + }; + } +} + +/// Represents a join function expression +class _JoinExpression extends FunctionExpression { + final Expression expression; + final Expression delimiter; + + _JoinExpression(this.expression, this.delimiter); + + @override + String get name => 'join'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'delimiter': delimiter.toMap(), + }, + }; + } +} + +/// Represents a trim function expression +class _TrimExpression extends FunctionExpression { + final Expression expression; + + _TrimExpression(this.expression); + + @override + String get name => 'trim'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Base class for boolean expressions used in filtering +abstract class BooleanExpression extends Expression {} + +/// Represents a filter expression for pipeline where clauses +class PipelineFilter extends BooleanExpression { + final Object field; + final Object? isEqualTo; + final Object? isNotEqualTo; + final Object? isLessThan; + final Object? isLessThanOrEqualTo; + final Object? isGreaterThan; + final Object? isGreaterThanOrEqualTo; + final Object? arrayContains; + final List? filterArrayContainsAny; + final List? whereIn; + final List? whereNotIn; + final bool? isNull; + final bool? isNotNull; + final BooleanExpression? _andExpression; + final BooleanExpression? _orExpression; + + PipelineFilter( + this.field, { + this.isEqualTo, + this.isNotEqualTo, + this.isLessThan, + this.isLessThanOrEqualTo, + this.isGreaterThan, + this.isGreaterThanOrEqualTo, + this.arrayContains, + this.filterArrayContainsAny, + this.whereIn, + this.whereNotIn, + this.isNull, + this.isNotNull, + }) : _andExpression = null, + _orExpression = null; + + PipelineFilter._internal({ + required BooleanExpression? andExpression, + required BooleanExpression? orExpression, + }) : field = '', + isEqualTo = null, + isNotEqualTo = null, + isLessThan = null, + isLessThanOrEqualTo = null, + isGreaterThan = null, + isGreaterThanOrEqualTo = null, + arrayContains = null, + filterArrayContainsAny = null, + whereIn = null, + whereNotIn = null, + isNull = null, + isNotNull = null, + _andExpression = andExpression, + _orExpression = orExpression; + + /// Creates an OR filter combining multiple boolean expressions + static PipelineFilter or( + BooleanExpression expression1, [ + BooleanExpression? expression2, + BooleanExpression? expression3, + BooleanExpression? expression4, + BooleanExpression? expression5, + BooleanExpression? expression6, + BooleanExpression? expression7, + BooleanExpression? expression8, + BooleanExpression? expression9, + BooleanExpression? expression10, + BooleanExpression? expression11, + BooleanExpression? expression12, + BooleanExpression? expression13, + BooleanExpression? expression14, + BooleanExpression? expression15, + BooleanExpression? expression16, + BooleanExpression? expression17, + BooleanExpression? expression18, + BooleanExpression? expression19, + BooleanExpression? expression20, + BooleanExpression? expression21, + BooleanExpression? expression22, + BooleanExpression? expression23, + BooleanExpression? expression24, + BooleanExpression? expression25, + BooleanExpression? expression26, + BooleanExpression? expression27, + BooleanExpression? expression28, + BooleanExpression? expression29, + BooleanExpression? expression30, + ]) { + final expressions = [expression1]; + if (expression2 != null) expressions.add(expression2); + if (expression3 != null) expressions.add(expression3); + if (expression4 != null) expressions.add(expression4); + if (expression5 != null) expressions.add(expression5); + if (expression6 != null) expressions.add(expression6); + if (expression7 != null) expressions.add(expression7); + if (expression8 != null) expressions.add(expression8); + if (expression9 != null) expressions.add(expression9); + if (expression10 != null) expressions.add(expression10); + if (expression11 != null) expressions.add(expression11); + if (expression12 != null) expressions.add(expression12); + if (expression13 != null) expressions.add(expression13); + if (expression14 != null) expressions.add(expression14); + if (expression15 != null) expressions.add(expression15); + if (expression16 != null) expressions.add(expression16); + if (expression17 != null) expressions.add(expression17); + if (expression18 != null) expressions.add(expression18); + if (expression19 != null) expressions.add(expression19); + if (expression20 != null) expressions.add(expression20); + if (expression21 != null) expressions.add(expression21); + if (expression22 != null) expressions.add(expression22); + if (expression23 != null) expressions.add(expression23); + if (expression24 != null) expressions.add(expression24); + if (expression25 != null) expressions.add(expression25); + if (expression26 != null) expressions.add(expression26); + if (expression27 != null) expressions.add(expression27); + if (expression28 != null) expressions.add(expression28); + if (expression29 != null) expressions.add(expression29); + if (expression30 != null) expressions.add(expression30); + + return PipelineFilter._internal( + andExpression: null, + orExpression: _combineExpressions(expressions, 'or'), + ); + } + + /// Creates an AND filter combining multiple boolean expressions + static PipelineFilter and( + BooleanExpression expression1, [ + BooleanExpression? expression2, + BooleanExpression? expression3, + BooleanExpression? expression4, + BooleanExpression? expression5, + BooleanExpression? expression6, + BooleanExpression? expression7, + BooleanExpression? expression8, + BooleanExpression? expression9, + BooleanExpression? expression10, + BooleanExpression? expression11, + BooleanExpression? expression12, + BooleanExpression? expression13, + BooleanExpression? expression14, + BooleanExpression? expression15, + BooleanExpression? expression16, + BooleanExpression? expression17, + BooleanExpression? expression18, + BooleanExpression? expression19, + BooleanExpression? expression20, + BooleanExpression? expression21, + BooleanExpression? expression22, + BooleanExpression? expression23, + BooleanExpression? expression24, + BooleanExpression? expression25, + BooleanExpression? expression26, + BooleanExpression? expression27, + BooleanExpression? expression28, + BooleanExpression? expression29, + BooleanExpression? expression30, + ]) { + final expressions = [expression1]; + if (expression2 != null) expressions.add(expression2); + if (expression3 != null) expressions.add(expression3); + if (expression4 != null) expressions.add(expression4); + if (expression5 != null) expressions.add(expression5); + if (expression6 != null) expressions.add(expression6); + if (expression7 != null) expressions.add(expression7); + if (expression8 != null) expressions.add(expression8); + if (expression9 != null) expressions.add(expression9); + if (expression10 != null) expressions.add(expression10); + if (expression11 != null) expressions.add(expression11); + if (expression12 != null) expressions.add(expression12); + if (expression13 != null) expressions.add(expression13); + if (expression14 != null) expressions.add(expression14); + if (expression15 != null) expressions.add(expression15); + if (expression16 != null) expressions.add(expression16); + if (expression17 != null) expressions.add(expression17); + if (expression18 != null) expressions.add(expression18); + if (expression19 != null) expressions.add(expression19); + if (expression20 != null) expressions.add(expression20); + if (expression21 != null) expressions.add(expression21); + if (expression22 != null) expressions.add(expression22); + if (expression23 != null) expressions.add(expression23); + if (expression24 != null) expressions.add(expression24); + if (expression25 != null) expressions.add(expression25); + if (expression26 != null) expressions.add(expression26); + if (expression27 != null) expressions.add(expression27); + if (expression28 != null) expressions.add(expression28); + if (expression29 != null) expressions.add(expression29); + if (expression30 != null) expressions.add(expression30); + + return PipelineFilter._internal( + andExpression: _combineExpressions(expressions, 'and'), + orExpression: null, + ); + } + + static BooleanExpression _combineExpressions( + List expressions, + String operator, + ) { + if (expressions.length == 1) return expressions.first; + + // Create a nested structure for multiple expressions + BooleanExpression result = expressions.first; + for (int i = 1; i < expressions.length; i++) { + if (operator == 'and') { + result = PipelineFilter.and(result, expressions[i]); + } else { + result = PipelineFilter.or(result, expressions[i]); + } + } + return result; + } + + @override + String get name => 'filter'; + + @override + Map toMap() { + final map = super.toMap(); + + if (_andExpression != null) { + map['args'] = { + 'operator': 'and', + 'expressions': [_andExpression.toMap()], + }; + return map; + } + + if (_orExpression != null) { + map['args'] = { + 'operator': 'or', + 'expressions': [_orExpression.toMap()], + }; + return map; + } + + final args = {}; + if (field is String) { + args['field'] = field; + } else if (field is Field) { + args['field'] = (field as Field).fieldName; + } + + if (isEqualTo != null) args['isEqualTo'] = isEqualTo; + if (isNotEqualTo != null) args['isNotEqualTo'] = isNotEqualTo; + if (isLessThan != null) args['isLessThan'] = isLessThan; + if (isLessThanOrEqualTo != null) { + args['isLessThanOrEqualTo'] = isLessThanOrEqualTo; + } + if (isGreaterThan != null) args['isGreaterThan'] = isGreaterThan; + if (isGreaterThanOrEqualTo != null) { + args['isGreaterThanOrEqualTo'] = isGreaterThanOrEqualTo; + } + if (arrayContains != null) args['arrayContains'] = arrayContains; + if (filterArrayContainsAny != null) { + args['arrayContainsAny'] = filterArrayContainsAny; + } + if (whereIn != null) args['whereIn'] = whereIn; + if (whereNotIn != null) args['whereNotIn'] = whereNotIn; + if (isNull != null) args['isNull'] = isNull; + if (isNotNull != null) args['isNotNull'] = isNotNull; + + map['args'] = args; + return map; + } +} + +// ============================================================================ +// PATTERN DEMONSTRATION - Concrete Function Expression Classes +// ============================================================================ + +/// Represents an addition function expression +class _AddExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _AddExpression(this.left, this.right); + + @override + String get name => 'add'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a subtraction function expression +class _SubtractExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _SubtractExpression(this.left, this.right); + + @override + String get name => 'subtract'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents an equality comparison function expression +class _EqualExpression extends BooleanExpression { + final Expression left; + final Expression right; + + _EqualExpression(this.left, this.right); + + @override + String get name => 'equal'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a greater-than comparison function expression +class _GreaterThanExpression extends BooleanExpression { + final Expression left; + final Expression right; + + _GreaterThanExpression(this.left, this.right); + + @override + String get name => 'greater_than'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a multiply function expression +class _MultiplyExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _MultiplyExpression(this.left, this.right); + + @override + String get name => 'multiply'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a divide function expression +class _DivideExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _DivideExpression(this.left, this.right); + + @override + String get name => 'divide'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a modulo function expression +class _ModuloExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _ModuloExpression(this.left, this.right); + + @override + String get name => 'modulo'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents an absolute value function expression +class _AbsExpression extends FunctionExpression { + final Expression expression; + + _AbsExpression(this.expression); + + @override + String get name => 'abs'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a negation function expression +class _NegateExpression extends FunctionExpression { + final Expression expression; + + _NegateExpression(this.expression); + + @override + String get name => 'negate'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a not-equal comparison function expression +class _NotEqualExpression extends BooleanExpression { + final Expression left; + final Expression right; + + _NotEqualExpression(this.left, this.right); + + @override + String get name => 'not_equal'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a greater-than-or-equal comparison function expression +class _GreaterThanOrEqualExpression extends BooleanExpression { + final Expression left; + final Expression right; + + _GreaterThanOrEqualExpression(this.left, this.right); + + @override + String get name => 'greater_than_or_equal'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a less-than comparison function expression +class _LessThanExpression extends BooleanExpression { + final Expression left; + final Expression right; + + _LessThanExpression(this.left, this.right); + + @override + String get name => 'less_than'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a less-than-or-equal comparison function expression +class _LessThanOrEqualExpression extends BooleanExpression { + final Expression left; + final Expression right; + + _LessThanOrEqualExpression(this.left, this.right); + + @override + String get name => 'less_than_or_equal'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +// ============================================================================ +// ARRAY OPERATION EXPRESSION CLASSES +// ============================================================================ + +/// Represents an array concat function expression +class _ArrayConcatExpression extends FunctionExpression { + final Expression firstArray; + final Expression secondArray; + + _ArrayConcatExpression(this.firstArray, this.secondArray); + + @override + String get name => 'array_concat'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'first': firstArray.toMap(), + 'second': secondArray.toMap(), + }, + }; + } +} + +/// Represents an array concat multiple function expression +class _ArrayConcatMultipleExpression extends FunctionExpression { + final List arrays; + + _ArrayConcatMultipleExpression(this.arrays); + + @override + String get name => 'array_concat_multiple'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'arrays': arrays.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Represents an array contains function expression +class _ArrayContainsExpression extends BooleanExpression { + final Expression array; + final Expression element; + + _ArrayContainsExpression(this.array, this.element); + + @override + String get name => 'array_contains'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'array': array.toMap(), + 'element': element.toMap(), + }, + }; + } +} + +/// Represents an arrayContainsAny function expression +class _ArrayContainsAnyExpression extends BooleanExpression { + final Expression array; + final List values; + + _ArrayContainsAnyExpression(this.array, this.values); + + @override + String get name => 'array_contains_any'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'array': array.toMap(), + 'values': values.map((v) => v.toMap()).toList(), + }, + }; + } +} + +/// Represents an array length function expression +class _ArrayLengthExpression extends FunctionExpression { + final Expression expression; + + _ArrayLengthExpression(this.expression); + + @override + String get name => 'array_length'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents an array reverse function expression +class _ArrayReverseExpression extends FunctionExpression { + final Expression expression; + + _ArrayReverseExpression(this.expression); + + @override + String get name => 'array_reverse'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents an array sum function expression +class _ArraySumExpression extends FunctionExpression { + final Expression expression; + + _ArraySumExpression(this.expression); + + @override + String get name => 'array_sum'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents an array slice function expression +class _ArraySliceExpression extends FunctionExpression { + final Expression array; + final Expression start; + final Expression end; + + _ArraySliceExpression(this.array, this.start, this.end); + + @override + String get name => 'array_slice'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'array': array.toMap(), + 'start': start.toMap(), + 'end': end.toMap(), + }, + }; + } +} + +// ============================================================================ +// CONDITIONAL / LOGIC OPERATION EXPRESSION CLASSES +// ============================================================================ + +/// Represents an ifAbsent function expression +class _IfAbsentExpression extends FunctionExpression { + final Expression expression; + final Expression elseExpr; + + _IfAbsentExpression(this.expression, this.elseExpr); + + @override + String get name => 'if_absent'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'else': elseExpr.toMap(), + }, + }; + } +} + +/// Represents an ifError function expression +class _IfErrorExpression extends FunctionExpression { + final Expression expression; + final Expression catchExpr; + + _IfErrorExpression(this.expression, this.catchExpr); + + @override + String get name => 'if_error'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'catch': catchExpr.toMap(), + }, + }; + } +} + +/// Represents an isAbsent function expression +class _IsAbsentExpression extends BooleanExpression { + final Expression expression; + + _IsAbsentExpression(this.expression); + + @override + String get name => 'is_absent'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents an isError function expression +class _IsErrorExpression extends BooleanExpression { + final Expression expression; + + _IsErrorExpression(this.expression); + + @override + String get name => 'is_error'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents an exists function expression +class _ExistsExpression extends BooleanExpression { + final Expression expression; + + _ExistsExpression(this.expression); + + @override + String get name => 'exists'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a not (negation) function expression +class _NotExpression extends BooleanExpression { + final BooleanExpression expression; + + _NotExpression(this.expression); + + @override + String get name => 'not'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +class _XorExpression extends BooleanExpression { + final List expressions; + + _XorExpression(this.expressions); + + @override + String get name => 'xor'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expressions': expressions.map((e) => e.toMap()).toList(), + }, + }; + } +} + +class _AndExpression extends BooleanExpression { + final List expressions; + + _AndExpression(this.expressions); + + @override + String get name => 'and'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expressions': expressions.map((e) => e.toMap()).toList(), + }, + }; + } +} + +class _OrExpression extends BooleanExpression { + final List expressions; + + _OrExpression(this.expressions); + + @override + String get name => 'or'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expressions': expressions.map((e) => e.toMap()).toList(), + }, + }; + } +} + +/// Represents a conditional (ternary) function expression +class _ConditionalExpression extends FunctionExpression { + final BooleanExpression condition; + final Expression thenExpr; + final Expression elseExpr; + + _ConditionalExpression(this.condition, this.thenExpr, this.elseExpr); + + @override + String get name => 'conditional'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'condition': condition.toMap(), + 'then': thenExpr.toMap(), + 'else': elseExpr.toMap(), + }, + }; + } +} + +// ============================================================================ +// TYPE CONVERSION EXPRESSION CLASSES +// ============================================================================ + +/// Represents an asBoolean function expression +class _AsBooleanExpression extends BooleanExpression { + final Expression expression; + + _AsBooleanExpression(this.expression); + + @override + String get name => 'as_boolean'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a toStringWithFormat function expression +class _ToStringWithFormatExpression extends FunctionExpression { + final Expression expression; + final Expression format; + + _ToStringWithFormatExpression(this.expression, this.format); + + @override + String get name => 'to_string_with_format'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'format': format.toMap(), + }, + }; + } +} + +// ============================================================================ +// BITWISE OPERATION EXPRESSION CLASSES +// ============================================================================ + +/// Represents a bitAnd function expression +class _BitAndExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _BitAndExpression(this.left, this.right); + + @override + String get name => 'bit_and'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a bitOr function expression +class _BitOrExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _BitOrExpression(this.left, this.right); + + @override + String get name => 'bit_or'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a bitXor function expression +class _BitXorExpression extends FunctionExpression { + final Expression left; + final Expression right; + + _BitXorExpression(this.left, this.right); + + @override + String get name => 'bit_xor'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'left': left.toMap(), + 'right': right.toMap(), + }, + }; + } +} + +/// Represents a bitNot function expression +class _BitNotExpression extends FunctionExpression { + final Expression expression; + + _BitNotExpression(this.expression); + + @override + String get name => 'bit_not'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a bitLeftShift function expression +class _BitLeftShiftExpression extends FunctionExpression { + final Expression expression; + final Expression amount; + + _BitLeftShiftExpression(this.expression, this.amount); + + @override + String get name => 'bit_left_shift'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'amount': amount.toMap(), + }, + }; + } +} + +/// Represents a bitRightShift function expression +class _BitRightShiftExpression extends FunctionExpression { + final Expression expression; + final Expression amount; + + _BitRightShiftExpression(this.expression, this.amount); + + @override + String get name => 'bit_right_shift'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'amount': amount.toMap(), + }, + }; + } +} + +// ============================================================================ +// DOCUMENT / PATH OPERATION EXPRESSION CLASSES +// ============================================================================ + +/// Represents a documentId function expression +class _DocumentIdExpression extends FunctionExpression { + final Expression expression; + + _DocumentIdExpression(this.expression); + + @override + String get name => 'document_id'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a collectionId function expression +class _CollectionIdExpression extends FunctionExpression { + final Expression expression; + + _CollectionIdExpression(this.expression); + + @override + String get name => 'collection_id'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a documentIdFromRef function expression +class _DocumentIdFromRefExpression extends FunctionExpression { + final DocumentReference docRef; + + _DocumentIdFromRefExpression(this.docRef); + + @override + String get name => 'document_id_from_ref'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'doc_ref': docRef.path, + }, + }; + } +} + +// ============================================================================ +// MAP OPERATION EXPRESSION CLASSES +// ============================================================================ + +/// Represents a mapGet function expression +class _MapGetExpression extends FunctionExpression { + final Expression map; + final Expression key; + + _MapGetExpression(this.map, this.key); + + @override + String get name => 'map_get'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'map': map.toMap(), + 'key': key.toMap(), + }, + }; + } +} + +/// Represents a mapKeys function expression +class _MapKeysExpression extends FunctionExpression { + final Expression expression; + + _MapKeysExpression(this.expression); + + @override + String get name => 'map_keys'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Represents a mapValues function expression +class _MapValuesExpression extends FunctionExpression { + final Expression expression; + + _MapValuesExpression(this.expression); + + @override + String get name => 'map_values'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +// ============================================================================ +// TIMESTAMP OPERATION EXPRESSION CLASSES +// ============================================================================ + +/// Represents a currentTimestamp function expression +class _CurrentTimestampExpression extends FunctionExpression { + _CurrentTimestampExpression(); + + @override + String get name => 'current_timestamp'; + + @override + Map toMap() { + return { + 'name': name, + }; + } +} + +/// Represents a timestampAdd function expression +class _TimestampAddExpression extends FunctionExpression { + final Expression timestamp; + final String unit; + final Expression amount; + + _TimestampAddExpression(this.timestamp, this.unit, this.amount); + + @override + String get name => 'timestamp_add'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'timestamp': timestamp.toMap(), + 'unit': unit, + 'amount': amount.toMap(), + }, + }; + } +} + +/// Represents a timestampSubtract function expression +class _TimestampSubtractExpression extends FunctionExpression { + final Expression timestamp; + final String unit; + final Expression amount; + + _TimestampSubtractExpression(this.timestamp, this.unit, this.amount); + + @override + String get name => 'timestamp_subtract'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'timestamp': timestamp.toMap(), + 'unit': unit, + 'amount': amount.toMap(), + }, + }; + } +} + +/// Represents a timestampDiff function expression +class _TimestampDiffExpression extends FunctionExpression { + final Expression timestamp1; + final Expression timestamp2; + final String unit; + + _TimestampDiffExpression(this.timestamp1, this.timestamp2, this.unit); + + @override + String get name => 'timestamp_diff'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'timestamp1': timestamp1.toMap(), + 'timestamp2': timestamp2.toMap(), + 'unit': unit, + }, + }; + } +} + +/// Represents a timestampTruncate function expression +class _TimestampTruncateExpression extends FunctionExpression { + final Expression timestamp; + final String unit; + + _TimestampTruncateExpression(this.timestamp, this.unit); + + @override + String get name => 'timestamp_truncate'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'timestamp': timestamp.toMap(), + 'unit': unit, + }, + }; + } +} + +/// Represents a distance function expression +class _DistanceExpression extends FunctionExpression { + final Expression geoPoint1; + final Expression geoPoint2; + + _DistanceExpression(this.geoPoint1, this.geoPoint2); + + @override + String get name => 'distance'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'geo_point1': geoPoint1.toMap(), + 'geo_point2': geoPoint2.toMap(), + }, + }; + } +} + +// ============================================================================ +// SPECIAL OPERATION EXPRESSION CLASSES +// ============================================================================ + +/// Represents an equalAny (IN) function expression +class _EqualAnyExpression extends BooleanExpression { + final Expression value; + final List values; + + _EqualAnyExpression(this.value, this.values); + + @override + String get name => 'equal_any'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'value': value.toMap(), + 'values': values.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Represents a notEqualAny (NOT IN) function expression +class _NotEqualAnyExpression extends BooleanExpression { + final Expression value; + final List values; + + _NotEqualAnyExpression(this.value, this.values); + + @override + String get name => 'not_equal_any'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'value': value.toMap(), + 'values': values.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Represents an array expression +class _ArrayExpression extends FunctionExpression { + final List elements; + + _ArrayExpression(this.elements); + + @override + String get name => 'array'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'elements': elements.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Represents a map expression +class _MapExpression extends FunctionExpression { + final Map data; + + _MapExpression(this.data); + + @override + String get name => 'map'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'data': data.map((k, v) => MapEntry(k, v.toMap())), + }, + }; + } +} + +/// Represents a mapFromPairs expression +class _MapFromPairsExpression extends FunctionExpression { + final List keyValuePairs; + + _MapFromPairsExpression(this.keyValuePairs); + + @override + String get name => 'map_from_pairs'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'pairs': keyValuePairs.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Represents a raw function expression +class _RawFunctionExpression extends FunctionExpression { + final String functionName; + final List args; + + _RawFunctionExpression(this.functionName, this.args); + + @override + String get name => functionName; + + @override + Map toMap() { + return { + 'name': name, + 'args': args.map((expr) => expr.toMap()).toList(), + }; + } +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_ordering.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_ordering.dart new file mode 100644 index 000000000000..b55687fa5d8b --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_ordering.dart @@ -0,0 +1,30 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Direction for ordering results +enum OrderDirection { + /// Ascending order + asc, + + /// Descending order + desc, +} + +/// Represents an ordering specification for pipeline sorting +class Ordering implements PipelineSerializable { + final Expression expression; + final OrderDirection direction; + + Ordering(this.expression, this.direction); + + @override + Map toMap() { + return { + 'expression': expression.toMap(), + 'order_direction': direction == OrderDirection.asc ? 'asc' : 'desc', + }; + } +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_sample.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_sample.dart new file mode 100644 index 000000000000..4136c9922872 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_sample.dart @@ -0,0 +1,43 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Base class for pipeline sampling strategies +abstract class PipelineSample implements PipelineSerializable { + const PipelineSample(); + + /// Creates a sample with a fixed size + factory PipelineSample.withSize(int size) = _PipelineSampleSize; + + /// Creates a sample with a percentage + factory PipelineSample.withPercentage(double percentage) = + _PipelineSamplePercentage; +} + +/// Sample stage with a fixed size +class _PipelineSampleSize extends PipelineSample { + final int size; + + const _PipelineSampleSize(this.size); + + @override + Map toMap() => { + 'type': 'size', + 'value': size, + }; +} + +/// Sample stage with a percentage +class _PipelineSamplePercentage extends PipelineSample { + final double percentage; + + const _PipelineSamplePercentage(this.percentage); + + @override + Map toMap() => { + 'type': 'percentage', + 'value': percentage, + }; +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_source.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_source.dart new file mode 100644 index 000000000000..c981d53cae0d --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_source.dart @@ -0,0 +1,66 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Provides methods to create pipelines from different sources +class PipelineSource { + final FirebaseFirestore _firestore; + + PipelineSource._(this._firestore); + + /// Creates a pipeline from a collection path + Pipeline collection(String collectionPath) { + if (collectionPath.isEmpty) { + throw ArgumentError('A collection path must be a non-empty string.'); + } else if (collectionPath.contains('//')) { + throw ArgumentError('A collection path must not contain "//".'); + } + + final stage = _CollectionPipelineStage(collectionPath); + final delegate = _firestore._delegate.pipeline([stage.toMap()]); + return Pipeline._(_firestore, delegate); + } + + /// Creates a pipeline from a collection reference + Pipeline collectionReference( + CollectionReference> collectionReference) { + final stage = _CollectionPipelineStage(collectionReference.path); + final delegate = _firestore._delegate.pipeline([stage.toMap()]); + return Pipeline._(_firestore, delegate); + } + + /// Creates a pipeline from a collection group + Pipeline collectionGroup(String collectionId) { + if (collectionId.isEmpty) { + throw ArgumentError('A collection ID must be a non-empty string.'); + } else if (collectionId.contains('/')) { + throw ArgumentError( + 'A collection ID passed to collectionGroup() cannot contain "/".', + ); + } + + final stage = _CollectionGroupPipelineStage(collectionId); + final delegate = _firestore._delegate.pipeline([stage.toMap()]); + return Pipeline._(_firestore, delegate); + } + + /// Creates a pipeline from a list of document references + Pipeline documents(List>> documents) { + if (documents.isEmpty) { + throw ArgumentError('Documents list must not be empty.'); + } + + final stage = _DocumentsPipelineStage(documents); + final delegate = _firestore._delegate.pipeline([stage.toMap()]); + return Pipeline._(_firestore, delegate); + } + + /// Creates a pipeline from the entire database + Pipeline database() { + final stage = _DatabasePipelineStage(); + final delegate = _firestore._delegate.pipeline([stage.toMap()]); + return Pipeline._(_firestore, delegate); + } +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_stage.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_stage.dart new file mode 100644 index 000000000000..07060acdefca --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_stage.dart @@ -0,0 +1,415 @@ +// 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. + +part of '../cloud_firestore.dart'; + +/// Base sealed class for all pipeline stages +sealed class PipelineStage implements PipelineSerializable { + String get name; +} + +/// Stage representing a collection source +final class _CollectionPipelineStage extends PipelineStage { + final String collectionPath; + + _CollectionPipelineStage(this.collectionPath); + + @override + String get name => 'collection'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'path': collectionPath, + }, + }; + } +} + +/// Stage representing a documents source +final class _DocumentsPipelineStage extends PipelineStage { + final List>> documents; + + _DocumentsPipelineStage(this.documents); + + @override + String get name => 'documents'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': documents + .map((doc) => { + 'path': doc.path, + }) + .toList(), + }; + } +} + +/// Stage representing a database source +final class _DatabasePipelineStage extends PipelineStage { + _DatabasePipelineStage(); + + @override + String get name => 'database'; + + @override + Map toMap() { + return { + 'stage': name, + }; + } +} + +/// Stage representing a collection group source +final class _CollectionGroupPipelineStage extends PipelineStage { + final String collectionPath; + + _CollectionGroupPipelineStage(this.collectionPath); + + @override + String get name => 'collection_group'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'path': collectionPath, + }, + }; + } +} + +/// Stage for adding fields to documents +final class _AddFieldsStage extends PipelineStage { + final List expressions; + + _AddFieldsStage(this.expressions); + + @override + String get name => 'add_fields'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'expressions': expressions.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Stage for aggregating data +final class _AggregateStage extends PipelineStage { + final List aggregateFunctions; + + _AggregateStage(this.aggregateFunctions); + + @override + String get name => 'aggregate'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'aggregate_functions': + aggregateFunctions.map((func) => func.toMap()).toList(), + }, + }; + } +} + +/// Stage for aggregating data with options and grouping +final class _AggregateStageWithOptions extends PipelineStage { + final AggregateStageOptions aggregateStage; + final AggregateOptions? options; + + _AggregateStageWithOptions(this.aggregateStage, this.options); + + @override + String get name => 'aggregate_with_options'; + + @override + Map toMap() { + final map = aggregateStage.toMap(); + final optionsMap = options?.toMap(); + return { + 'stage': name, + 'args': { + 'aggregate_stage': map, + 'options': optionsMap, + }, + }; + } +} + +/// Stage for getting distinct values +final class _DistinctStage extends PipelineStage { + final List expressions; + + _DistinctStage(this.expressions); + + @override + String get name => 'distinct'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'expressions': expressions.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Stage for finding nearest vectors +final class _FindNearestStage extends PipelineStage { + final Field vectorField; + final List vectorValue; + final DistanceMeasure distanceMeasure; + final int? limit; + + _FindNearestStage( + this.vectorField, + this.vectorValue, + this.distanceMeasure, { + this.limit, + }); + + @override + String get name => 'find_nearest'; + + @override + Map toMap() { + final map = { + 'stage': name, + 'args': { + 'vector_field': vectorField.fieldName, + 'vector_value': vectorValue, + 'distance_measure': distanceMeasure.name, + }, + }; + if (limit != null) { + map['args']['limit'] = limit; + } + return map; + } +} + +/// Stage for limiting results +final class _LimitStage extends PipelineStage { + final int limit; + + _LimitStage(this.limit); + + @override + String get name => 'limit'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'limit': limit, + }, + }; + } +} + +/// Stage for offsetting results +final class _OffsetStage extends PipelineStage { + final int offset; + + _OffsetStage(this.offset); + + @override + String get name => 'offset'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'offset': offset, + }, + }; + } +} + +/// Stage for removing fields +final class _RemoveFieldsStage extends PipelineStage { + final List fieldPaths; + + _RemoveFieldsStage(this.fieldPaths); + + @override + String get name => 'remove_fields'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'field_paths': fieldPaths, + }, + }; + } +} + +/// Stage for replacing documents +final class _ReplaceWithStage extends PipelineStage { + final Expression expression; + + _ReplaceWithStage(this.expression); + + @override + String get name => 'replace_with'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} + +/// Stage for sampling documents +final class _SampleStage extends PipelineStage { + final PipelineSample sample; + + _SampleStage(this.sample); + + @override + String get name => 'sample'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': sample.toMap(), + }; + } +} + +/// Stage for selecting specific fields +final class _SelectStage extends PipelineStage { + final List expressions; + + _SelectStage(this.expressions); + + @override + String get name => 'select'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'expressions': expressions.map((expr) => expr.toMap()).toList(), + }, + }; + } +} + +/// Stage for sorting results +final class _SortStage extends PipelineStage { + final List orderings; + + _SortStage(this.orderings); + + @override + String get name => 'sort'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'orderings': orderings + .map((o) => { + 'expression': o.expression.toMap(), + 'order_direction': + o.direction == OrderDirection.asc ? 'asc' : 'desc', + }) + .toList(), + }, + }; + } +} + +/// Stage for unnesting arrays +final class _UnnestStage extends PipelineStage { + final Selectable expression; + final String? indexField; + + _UnnestStage(this.expression, this.indexField); + + @override + String get name => 'unnest'; + + @override + Map toMap() { + final map = { + 'stage': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + if (indexField != null) { + map['args']['index_field'] = indexField; + } + return map; + } +} + +/// Stage for union with another pipeline +final class _UnionStage extends PipelineStage { + final Pipeline pipeline; + + _UnionStage(this.pipeline); + + @override + String get name => 'union'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'pipeline': pipeline.stages, + }, + }; + } +} + +/// Stage for filtering documents +final class _WhereStage extends PipelineStage { + final BooleanExpression expression; + + _WhereStage(this.expression); + + @override + String get name => 'where'; + + @override + Map toMap() { + return { + 'stage': name, + 'args': { + 'expression': expression.toMap(), + }, + }; + } +} diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/utils/codec_utility.dart b/packages/cloud_firestore/cloud_firestore/lib/src/utils/codec_utility.dart index 2c704fe861c6..2e305ce16393 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/utils/codec_utility.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/utils/codec_utility.dart @@ -17,8 +17,11 @@ class _CodecUtility { if (data == null) { return null; } - Map output = Map.from(data); - output.updateAll((_, value) => valueEncode(value)); + final output = {}; + data.forEach((key, value) { + final stringKey = key is DocumentReference ? key.path : key as String; + output[stringKey] = valueEncode(value); + }); return output; } diff --git a/packages/cloud_firestore/cloud_firestore/pubspec.yaml b/packages/cloud_firestore/cloud_firestore/pubspec.yaml index 206d51178b6d..ca58a8f63461 100755 --- a/packages/cloud_firestore/cloud_firestore/pubspec.yaml +++ b/packages/cloud_firestore/cloud_firestore/pubspec.yaml @@ -4,7 +4,7 @@ description: live synchronization and offline support on Android and iOS. homepage: https://firebase.google.com/docs/firestore repository: https://github.com/firebase/flutterfire/tree/main/packages/cloud_firestore/cloud_firestore -version: 6.1.2 +version: 6.1.3 topics: - firebase - firestore @@ -20,10 +20,10 @@ environment: flutter: '>=3.3.0' dependencies: - cloud_firestore_platform_interface: ^7.0.6 - cloud_firestore_web: ^5.1.2 + cloud_firestore_platform_interface: ^7.0.7 + cloud_firestore_web: ^5.1.3 collection: ^1.0.0 - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/cloud_firestore/cloud_firestore/windows/cloud_firestore_plugin.cpp b/packages/cloud_firestore/cloud_firestore/windows/cloud_firestore_plugin.cpp index 2e08cb71ef3e..342103df811a 100644 --- a/packages/cloud_firestore/cloud_firestore/windows/cloud_firestore_plugin.cpp +++ b/packages/cloud_firestore/cloud_firestore/windows/cloud_firestore_plugin.cpp @@ -706,10 +706,12 @@ void CloudFirestorePlugin::EnableNetwork( void CloudFirestorePlugin::Terminate( const FirestorePigeonFirebaseApp& app, std::function reply)> result) { + std::string cacheKey = app.app_name() + "-" + app.database_u_r_l(); Firestore* firestore = GetFirestoreFromPigeon(app); firestore->Terminate().OnCompletion( - [result](const Future& completed_future) { + [result, cacheKey](const Future& completed_future) { if (completed_future.error() == firebase::firestore::kErrorOk) { + CloudFirestorePlugin::firestoreInstances_.erase(cacheKey); result(std::nullopt); } else { result(CloudFirestorePlugin::ParseError(completed_future)); diff --git a/packages/cloud_firestore/cloud_firestore/windows/firestore_codec.cpp b/packages/cloud_firestore/cloud_firestore/windows/firestore_codec.cpp index 573636581ae4..14be5a1776b0 100644 --- a/packages/cloud_firestore/cloud_firestore/windows/firestore_codec.cpp +++ b/packages/cloud_firestore/cloud_firestore/windows/firestore_codec.cpp @@ -212,18 +212,23 @@ cloud_firestore_windows::FirestoreCodec::ReadValueOfType( std::get( FirestoreCodec::ReadValue(stream))); - if (CloudFirestorePlugin::firestoreInstances_.find(appName) != + // Use composite key matching GetFirestoreFromPigeon to avoid + // creating a duplicate unique_ptr for the same Firestore instance. + // See https://github.com/firebase/flutterfire/issues/18028 + std::string cacheKey = appName + "-" + databaseUrl; + + if (CloudFirestorePlugin::firestoreInstances_.find(cacheKey) != CloudFirestorePlugin::firestoreInstances_.end()) { return CustomEncodableValue( - CloudFirestorePlugin::firestoreInstances_[appName].get()); + CloudFirestorePlugin::firestoreInstances_[cacheKey].get()); } firebase::App* app = firebase::App::GetInstance(appName.c_str()); - Firestore* firestore = Firestore::GetInstance(app); + Firestore* firestore = Firestore::GetInstance(app, databaseUrl.c_str()); firestore->set_settings(settings); - CloudFirestorePlugin::firestoreInstances_[appName] = + CloudFirestorePlugin::firestoreInstances_[cacheKey] = std::unique_ptr(firestore); return CustomEncodableValue(firestore); diff --git a/packages/cloud_firestore/cloud_firestore/windows/messages.g.cpp b/packages/cloud_firestore/cloud_firestore/windows/messages.g.cpp index bb9f58d5775d..10175dffd191 100644 --- a/packages/cloud_firestore/cloud_firestore/windows/messages.g.cpp +++ b/packages/cloud_firestore/cloud_firestore/windows/messages.g.cpp @@ -397,6 +397,144 @@ PigeonQuerySnapshot PigeonQuerySnapshot::FromEncodableList( return decoded; } +// PigeonPipelineResult + +PigeonPipelineResult::PigeonPipelineResult() {} + +PigeonPipelineResult::PigeonPipelineResult(const std::string* document_path, + const int64_t* create_time, + const int64_t* update_time, + const EncodableMap* data) + : document_path_(document_path ? std::optional(*document_path) + : std::nullopt), + create_time_(create_time ? std::optional(*create_time) + : std::nullopt), + update_time_(update_time ? std::optional(*update_time) + : std::nullopt), + data_(data ? std::optional(*data) : std::nullopt) {} + +const std::string* PigeonPipelineResult::document_path() const { + return document_path_ ? &(*document_path_) : nullptr; +} + +void PigeonPipelineResult::set_document_path( + const std::string_view* value_arg) { + document_path_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void PigeonPipelineResult::set_document_path(std::string_view value_arg) { + document_path_ = value_arg; +} + +const int64_t* PigeonPipelineResult::create_time() const { + return create_time_ ? &(*create_time_) : nullptr; +} + +void PigeonPipelineResult::set_create_time(const int64_t* value_arg) { + create_time_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void PigeonPipelineResult::set_create_time(int64_t value_arg) { + create_time_ = value_arg; +} + +const int64_t* PigeonPipelineResult::update_time() const { + return update_time_ ? &(*update_time_) : nullptr; +} + +void PigeonPipelineResult::set_update_time(const int64_t* value_arg) { + update_time_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void PigeonPipelineResult::set_update_time(int64_t value_arg) { + update_time_ = value_arg; +} + +const EncodableMap* PigeonPipelineResult::data() const { + return data_ ? &(*data_) : nullptr; +} + +void PigeonPipelineResult::set_data(const EncodableMap* value_arg) { + data_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void PigeonPipelineResult::set_data(const EncodableMap& value_arg) { + data_ = value_arg; +} + +EncodableList PigeonPipelineResult::ToEncodableList() const { + EncodableList list; + list.reserve(4); + list.push_back(document_path_ ? EncodableValue(*document_path_) + : EncodableValue()); + list.push_back(create_time_ ? EncodableValue(*create_time_) + : EncodableValue()); + list.push_back(update_time_ ? EncodableValue(*update_time_) + : EncodableValue()); + list.push_back(data_ ? EncodableValue(*data_) : EncodableValue()); + return list; +} + +PigeonPipelineResult PigeonPipelineResult::FromEncodableList( + const EncodableList& list) { + PigeonPipelineResult decoded; + auto& encodable_document_path = list[0]; + if (!encodable_document_path.IsNull()) { + decoded.set_document_path(std::get(encodable_document_path)); + } + auto& encodable_create_time = list[1]; + if (!encodable_create_time.IsNull()) { + decoded.set_create_time(encodable_create_time.LongValue()); + } + auto& encodable_update_time = list[2]; + if (!encodable_update_time.IsNull()) { + decoded.set_update_time(encodable_update_time.LongValue()); + } + auto& encodable_data = list[3]; + if (!encodable_data.IsNull()) { + decoded.set_data(std::get(encodable_data)); + } + return decoded; +} + +// PigeonPipelineSnapshot + +PigeonPipelineSnapshot::PigeonPipelineSnapshot(const EncodableList& results, + int64_t execution_time) + : results_(results), execution_time_(execution_time) {} + +const EncodableList& PigeonPipelineSnapshot::results() const { + return results_; +} + +void PigeonPipelineSnapshot::set_results(const EncodableList& value_arg) { + results_ = value_arg; +} + +int64_t PigeonPipelineSnapshot::execution_time() const { + return execution_time_; +} + +void PigeonPipelineSnapshot::set_execution_time(int64_t value_arg) { + execution_time_ = value_arg; +} + +EncodableList PigeonPipelineSnapshot::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(results_)); + list.push_back(EncodableValue(execution_time_)); + return list; +} + +PigeonPipelineSnapshot PigeonPipelineSnapshot::FromEncodableList( + const EncodableList& list) { + PigeonPipelineSnapshot decoded(std::get(list[0]), + list[1].LongValue()); + return decoded; +} + // PigeonGetOptions PigeonGetOptions::PigeonGetOptions( @@ -1034,15 +1172,21 @@ EncodableValue FirebaseFirestoreHostApiCodecSerializer::ReadValueOfType( return CustomEncodableValue(PigeonGetOptions::FromEncodableList( std::get(ReadValue(stream)))); case 137: - return CustomEncodableValue(PigeonQueryParameters::FromEncodableList( + return CustomEncodableValue(PigeonPipelineResult::FromEncodableList( std::get(ReadValue(stream)))); case 138: - return CustomEncodableValue(PigeonQuerySnapshot::FromEncodableList( + return CustomEncodableValue(PigeonPipelineSnapshot::FromEncodableList( std::get(ReadValue(stream)))); case 139: - return CustomEncodableValue(PigeonSnapshotMetadata::FromEncodableList( + return CustomEncodableValue(PigeonQueryParameters::FromEncodableList( std::get(ReadValue(stream)))); case 140: + return CustomEncodableValue(PigeonQuerySnapshot::FromEncodableList( + std::get(ReadValue(stream)))); + case 141: + return CustomEncodableValue(PigeonSnapshotMetadata::FromEncodableList( + std::get(ReadValue(stream)))); + case 142: return CustomEncodableValue(PigeonTransactionCommand::FromEncodableList( std::get(ReadValue(stream)))); default: @@ -1127,8 +1271,24 @@ void FirebaseFirestoreHostApiCodecSerializer::WriteValue( stream); return; } - if (custom_value->type() == typeid(PigeonQueryParameters)) { + if (custom_value->type() == typeid(PigeonPipelineResult)) { stream->WriteByte(137); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PigeonPipelineSnapshot)) { + stream->WriteByte(138); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(PigeonQueryParameters)) { + stream->WriteByte(139); WriteValue( EncodableValue(std::any_cast(*custom_value) .ToEncodableList()), @@ -1136,7 +1296,7 @@ void FirebaseFirestoreHostApiCodecSerializer::WriteValue( return; } if (custom_value->type() == typeid(PigeonQuerySnapshot)) { - stream->WriteByte(138); + stream->WriteByte(140); WriteValue( EncodableValue(std::any_cast(*custom_value) .ToEncodableList()), @@ -1144,7 +1304,7 @@ void FirebaseFirestoreHostApiCodecSerializer::WriteValue( return; } if (custom_value->type() == typeid(PigeonSnapshotMetadata)) { - stream->WriteByte(139); + stream->WriteByte(141); WriteValue( EncodableValue(std::any_cast(*custom_value) .ToEncodableList()), @@ -1152,7 +1312,7 @@ void FirebaseFirestoreHostApiCodecSerializer::WriteValue( return; } if (custom_value->type() == typeid(PigeonTransactionCommand)) { - stream->WriteByte(140); + stream->WriteByte(142); WriteValue( EncodableValue(std::any_cast(*custom_value) .ToEncodableList()), @@ -2290,8 +2450,8 @@ void FirebaseFirestoreHostApi::SetUp(flutter::BinaryMessenger* binary_messenger, reply(WrapError("request_arg unexpectedly null.")); return; } - const PersistenceCacheIndexManagerRequestEnum& request_arg = - (PersistenceCacheIndexManagerRequestEnum) + const PersistenceCacheIndexManagerRequest& request_arg = + (PersistenceCacheIndexManagerRequest) encodable_request_arg.LongValue(); api->PersistenceCacheIndexManagerRequest( app_arg, request_arg, @@ -2312,6 +2472,56 @@ void FirebaseFirestoreHostApi::SetUp(flutter::BinaryMessenger* binary_messenger, channel->SetMessageHandler(nullptr); } } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.cloud_firestore_platform_interface." + "FirebaseFirestoreHostApi.executePipeline", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_stages_arg = args.at(1); + if (encodable_stages_arg.IsNull()) { + reply(WrapError("stages_arg unexpectedly null.")); + return; + } + const auto& stages_arg = + std::get(encodable_stages_arg); + const auto& encodable_options_arg = args.at(2); + const auto* options_arg = + std::get_if(&encodable_options_arg); + api->ExecutePipeline( + app_arg, stages_arg, options_arg, + [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } } EncodableValue FirebaseFirestoreHostApi::WrapError( diff --git a/packages/cloud_firestore/cloud_firestore/windows/messages.g.h b/packages/cloud_firestore/cloud_firestore/windows/messages.g.h index 8aecb887facc..739c8b2486ae 100644 --- a/packages/cloud_firestore/cloud_firestore/windows/messages.g.h +++ b/packages/cloud_firestore/cloud_firestore/windows/messages.g.h @@ -136,7 +136,7 @@ enum class AggregateSource { // [PersistenceCacheIndexManagerRequest] represents the request types for the // persistence cache index manager. -enum class PersistenceCacheIndexManagerRequestEnum { +enum class PersistenceCacheIndexManagerRequest { enableIndexAutoCreation = 0, disableIndexAutoCreation = 1, deleteAllIndexes = 2 @@ -348,6 +348,70 @@ class PigeonQuerySnapshot { PigeonSnapshotMetadata metadata_; }; +// Generated class from Pigeon that represents data sent in messages. +class PigeonPipelineResult { + public: + // Constructs an object setting all non-nullable fields. + PigeonPipelineResult(); + + // Constructs an object setting all fields. + explicit PigeonPipelineResult(const std::string* document_path, + const int64_t* create_time, + const int64_t* update_time, + const flutter::EncodableMap* data); + + const std::string* document_path() const; + void set_document_path(const std::string_view* value_arg); + void set_document_path(std::string_view value_arg); + + const int64_t* create_time() const; + void set_create_time(const int64_t* value_arg); + void set_create_time(int64_t value_arg); + + const int64_t* update_time() const; + void set_update_time(const int64_t* value_arg); + void set_update_time(int64_t value_arg); + + // All fields in the result (from PipelineResult.data() on Android). + const flutter::EncodableMap* data() const; + void set_data(const flutter::EncodableMap* value_arg); + void set_data(const flutter::EncodableMap& value_arg); + + private: + static PigeonPipelineResult FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseFirestoreHostApi; + friend class FirebaseFirestoreHostApiCodecSerializer; + std::optional document_path_; + std::optional create_time_; + std::optional update_time_; + std::optional data_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class PigeonPipelineSnapshot { + public: + // Constructs an object setting all fields. + explicit PigeonPipelineSnapshot(const flutter::EncodableList& results, + int64_t execution_time); + + const flutter::EncodableList& results() const; + void set_results(const flutter::EncodableList& value_arg); + + int64_t execution_time() const; + void set_execution_time(int64_t value_arg); + + private: + static PigeonPipelineSnapshot FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseFirestoreHostApi; + friend class FirebaseFirestoreHostApiCodecSerializer; + flutter::EncodableList results_; + int64_t execution_time_; +}; + // Generated class from Pigeon that represents data sent in messages. class PigeonGetOptions { public: @@ -724,8 +788,13 @@ class FirebaseFirestoreHostApi { std::function reply)> result) = 0; virtual void PersistenceCacheIndexManagerRequest( const FirestorePigeonFirebaseApp& app, - const PersistenceCacheIndexManagerRequestEnum& request, + const PersistenceCacheIndexManagerRequest& request, std::function reply)> result) = 0; + virtual void ExecutePipeline( + const FirestorePigeonFirebaseApp& app, + const flutter::EncodableList& stages, + const flutter::EncodableMap* options, + std::function reply)> result) = 0; // The codec used by FirebaseFirestoreHostApi. static const flutter::StandardMessageCodec& GetCodec(); diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md b/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md index 58596721bb42..36ba9c30451c 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.0.7 + + - Update a dependency to the latest release. + ## 7.0.6 - Update a dependency to the latest release. diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart index 9f9b8e7866e0..2e7e97bec179 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/cloud_firestore_platform_interface.dart @@ -29,6 +29,8 @@ export 'src/platform_interface/platform_interface_index_definitions.dart'; export 'src/platform_interface/platform_interface_load_bundle_task.dart'; export 'src/platform_interface/platform_interface_load_bundle_task_snapshot.dart'; export 'src/platform_interface/platform_interface_persistent_cache_index_manager.dart'; +export 'src/platform_interface/platform_interface_pipeline.dart'; +export 'src/platform_interface/platform_interface_pipeline_snapshot.dart'; export 'src/platform_interface/platform_interface_query.dart'; export 'src/platform_interface/platform_interface_query_snapshot.dart'; export 'src/platform_interface/platform_interface_transaction.dart'; diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart index 573e5a3c91b9..889ea4c5351b 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart @@ -15,6 +15,8 @@ import 'package:flutter/services.dart'; import 'method_channel_collection_reference.dart'; import 'method_channel_document_reference.dart'; +import 'method_channel_pipeline.dart'; +import 'method_channel_pipeline_snapshot.dart'; import 'method_channel_query.dart'; import 'method_channel_transaction.dart'; import 'method_channel_write_batch.dart'; @@ -350,4 +352,37 @@ class MethodChannelFirebaseFirestore extends FirebaseFirestorePlatform { convertPlatformException(e, stack); } } + + @override + PipelinePlatform pipeline(List> initialStages) { + return MethodChannelPipeline(this, pigeonApp, stages: initialStages); + } + + @override + Future executePipeline( + List> stages, { + Map? options, + }) async { + try { + // Convert stages to Pigeon format (List?>) + final List?> pigeonStages = stages.map((stage) { + return stage.map(MapEntry.new); + }).toList(); + + // Convert options to Pigeon format (Map?) + final Map? pigeonOptions = options?.map( + MapEntry.new, + ); + + final PigeonPipelineSnapshot result = await pigeonChannel.executePipeline( + pigeonApp, + pigeonStages, + pigeonOptions, + ); + + return MethodChannelPipelineSnapshot(this, pigeonApp, result); + } catch (e, stack) { + convertPlatformException(e, stack); + } + } } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_pipeline.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_pipeline.dart new file mode 100644 index 000000000000..7e0b65e43c7d --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_pipeline.dart @@ -0,0 +1,51 @@ +// ignore_for_file: require_trailing_commas, unnecessary_lambdas +// 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. + +import 'dart:async'; + +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; +import 'package:cloud_firestore_platform_interface/src/platform_interface/platform_interface_pipeline.dart' + as pipeline; + +/// An implementation of [PipelinePlatform] that uses [MethodChannel] to +/// communicate with Firebase plugins. +class MethodChannelPipeline extends pipeline.PipelinePlatform { + /// Create a [MethodChannelPipeline] from [stages] + MethodChannelPipeline( + FirebaseFirestorePlatform _firestore, + this.pigeonApp, { + List>? stages, + }) : super(_firestore, stages); + + final FirestorePigeonFirebaseApp pigeonApp; + + /// Creates a new instance of [MethodChannelPipeline], however overrides + /// any existing [stages]. + /// + /// This is in place to ensure that changes to a pipeline don't mutate + /// other pipelines. + MethodChannelPipeline _copyWithStages(List> newStages) { + return MethodChannelPipeline( + firestore, + pigeonApp, + stages: List.unmodifiable([ + ...stages, + ...newStages, + ]), + ); + } + + @override + pipeline.PipelinePlatform addStage(Map serializedStage) { + return _copyWithStages([serializedStage]); + } + + @override + Future execute({ + Map? options, + }) async { + return firestore.executePipeline(stages, options: options); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_pipeline_snapshot.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_pipeline_snapshot.dart new file mode 100644 index 000000000000..c5f351ef88ed --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_pipeline_snapshot.dart @@ -0,0 +1,83 @@ +// ignore_for_file: require_trailing_commas +// 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. + +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; +import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_document_reference.dart'; + +/// An implementation of [PipelineSnapshotPlatform] that uses [MethodChannel] to +/// communicate with Firebase plugins. +class MethodChannelPipelineSnapshot extends PipelineSnapshotPlatform { + final List _results; + final DateTime _executionTime; + + /// Creates a [MethodChannelPipelineSnapshot] from the given [pigeonSnapshot] + MethodChannelPipelineSnapshot( + FirebaseFirestorePlatform firestore, + FirestorePigeonFirebaseApp pigeonApp, + PigeonPipelineSnapshot pigeonSnapshot, + ) : _results = pigeonSnapshot.results + .whereType() + .map((result) => MethodChannelPipelineResult( + firestore, + pigeonApp, + result.documentPath, + result.createTime != null + ? DateTime.fromMillisecondsSinceEpoch(result.createTime!) + : null, + result.updateTime != null + ? DateTime.fromMillisecondsSinceEpoch(result.updateTime!) + : null, + result.data?.cast(), + )) + .toList(), + _executionTime = DateTime.fromMillisecondsSinceEpoch( + pigeonSnapshot.executionTime, + ), + super(); + + @override + List get results => _results; + + @override + DateTime get executionTime => _executionTime; +} + +/// An implementation of [PipelineResultPlatform] that uses [MethodChannel] to +/// communicate with Firebase plugins. +class MethodChannelPipelineResult extends PipelineResultPlatform { + final DocumentReferencePlatform? _document; + final DateTime? _createTime; + final DateTime? _updateTime; + final Map? _data; + + MethodChannelPipelineResult( + FirebaseFirestorePlatform firestore, + FirestorePigeonFirebaseApp pigeonApp, + String? documentPath, + this._createTime, + this._updateTime, + Map? data, + ) : _document = (documentPath != null && documentPath.isNotEmpty) + ? MethodChannelDocumentReference( + firestore, + documentPath, + pigeonApp, + ) + : null, + _data = data, + super(); + + @override + DocumentReferencePlatform? get document => _document; + + @override + DateTime? get createTime => _createTime; + + @override + DateTime? get updateTime => _updateTime; + + @override + Map? get data => _data; +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/pigeon/messages.pigeon.dart index 85af6edc5260..224ded3d6bf6 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/pigeon/messages.pigeon.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -301,6 +301,69 @@ class PigeonQuerySnapshot { } } +class PigeonPipelineResult { + PigeonPipelineResult({ + this.documentPath, + this.createTime, + this.updateTime, + this.data, + }); + + String? documentPath; + + int? createTime; + + int? updateTime; + + /// All fields in the result (from PipelineResult.data() on Android). + Map? data; + + Object encode() { + return [ + documentPath, + createTime, + updateTime, + data, + ]; + } + + static PigeonPipelineResult decode(Object result) { + result as List; + return PigeonPipelineResult( + documentPath: result[0] as String?, + createTime: result[1] as int?, + updateTime: result[2] as int?, + data: (result[3] as Map?)?.cast(), + ); + } +} + +class PigeonPipelineSnapshot { + PigeonPipelineSnapshot({ + required this.results, + required this.executionTime, + }); + + List results; + + int executionTime; + + Object encode() { + return [ + results, + executionTime, + ]; + } + + static PigeonPipelineSnapshot decode(Object result) { + result as List; + return PigeonPipelineSnapshot( + results: (result[0] as List?)!.cast(), + executionTime: result[1]! as int, + ); + } +} + class PigeonGetOptions { PigeonGetOptions({ required this.source, @@ -586,18 +649,24 @@ class _FirebaseFirestoreHostApiCodec extends FirestoreMessageCodec { } else if (value is PigeonGetOptions) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is PigeonQueryParameters) { + } else if (value is PigeonPipelineResult) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is PigeonQuerySnapshot) { + } else if (value is PigeonPipelineSnapshot) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is PigeonSnapshotMetadata) { + } else if (value is PigeonQueryParameters) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is PigeonTransactionCommand) { + } else if (value is PigeonQuerySnapshot) { buffer.putUint8(140); writeValue(buffer, value.encode()); + } else if (value is PigeonSnapshotMetadata) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else if (value is PigeonTransactionCommand) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -625,12 +694,16 @@ class _FirebaseFirestoreHostApiCodec extends FirestoreMessageCodec { case 136: return PigeonGetOptions.decode(readValue(buffer)!); case 137: - return PigeonQueryParameters.decode(readValue(buffer)!); + return PigeonPipelineResult.decode(readValue(buffer)!); case 138: - return PigeonQuerySnapshot.decode(readValue(buffer)!); + return PigeonPipelineSnapshot.decode(readValue(buffer)!); case 139: - return PigeonSnapshotMetadata.decode(readValue(buffer)!); + return PigeonQueryParameters.decode(readValue(buffer)!); case 140: + return PigeonQuerySnapshot.decode(readValue(buffer)!); + case 141: + return PigeonSnapshotMetadata.decode(readValue(buffer)!); + case 142: return PigeonTransactionCommand.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1342,4 +1415,37 @@ class FirebaseFirestoreHostApi { return; } } + + Future executePipeline( + FirestorePigeonFirebaseApp arg_app, + List?> arg_stages, + Map? arg_options, + ) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.cloud_firestore_platform_interface.FirebaseFirestoreHostApi.executePipeline', + codec, + binaryMessenger: _binaryMessenger, + ); + final List? replyList = await channel + .send([arg_app, arg_stages, arg_options]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as PigeonPipelineSnapshot?)!; + } + } } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_firestore.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_firestore.dart index 3faffc0a3778..ee9cd3a32478 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_firestore.dart @@ -243,6 +243,21 @@ abstract class FirebaseFirestorePlatform extends PlatformInterface { throw UnimplementedError('setLoggingEnabled() is not implemented'); } + /// Creates a pipeline platform instance with initial stages. + PipelinePlatform pipeline(List> initialStages) { + throw UnimplementedError('pipeline() is not implemented'); + } + + /// Executes a pipeline and returns the results. + /// + /// The [stages] parameter contains the serialized pipeline stages. + Future executePipeline( + List> stages, { + Map? options, + }) { + throw UnimplementedError('executePipeline() is not implemented'); + } + @override //ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) => diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_pipeline.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_pipeline.dart new file mode 100644 index 000000000000..2d0449ee4aef --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_pipeline.dart @@ -0,0 +1,54 @@ +// ignore_for_file: require_trailing_commas +// 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. + +import 'dart:async'; + +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; +import 'package:meta/meta.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Represents a pipeline for querying and transforming Firestore data. +@immutable +abstract class PipelinePlatform extends PlatformInterface { + /// Create a [PipelinePlatform] instance + PipelinePlatform(this.firestore, List>? stages) + : _stages = stages ?? [], + super(token: _token); + + static final Object _token = Object(); + + /// Throws an [AssertionError] if [instance] does not extend + /// [PipelinePlatform]. + /// + /// This is used by the app-facing [Pipeline] to ensure that + /// the object in which it's going to delegate calls has been + /// constructed properly. + static void verify(PipelinePlatform instance) { + PlatformInterface.verify(instance, _token); + } + + /// The [FirebaseFirestorePlatform] interface for this current pipeline. + final FirebaseFirestorePlatform firestore; + + /// Stores the pipeline stages. + final List> _stages; + + /// Exposes the [stages] on the pipeline delegate. + /// + /// This should only be used for testing to ensure that all + /// pipeline stages are correctly set on the underlying delegate + /// when being tested from a different package. + List> get stages { + return List.unmodifiable(_stages); + } + + /// Adds a serialized stage to the pipeline + PipelinePlatform addStage(Map serializedStage); + + /// Executes the pipeline and returns a snapshot of the results + Future execute({ + Map? options, + }); +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_pipeline_snapshot.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_pipeline_snapshot.dart new file mode 100644 index 000000000000..1946ce740712 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/platform_interface/platform_interface_pipeline_snapshot.dart @@ -0,0 +1,44 @@ +// ignore_for_file: require_trailing_commas +// 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. + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../../cloud_firestore_platform_interface.dart'; +import 'platform_interface_document_reference.dart'; + +/// Platform interface for [PipelineSnapshot]. +abstract class PipelineSnapshotPlatform extends PlatformInterface { + /// Create an instance of [PipelineSnapshotPlatform]. + PipelineSnapshotPlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The results of the pipeline execution + List get results; + + /// The execution time of the pipeline + DateTime get executionTime; +} + +/// Platform interface for [PipelineResult]. +abstract class PipelineResultPlatform extends PlatformInterface { + /// Create an instance of [PipelineResultPlatform]. + PipelineResultPlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The document reference. Null for aggregate-only results (no document row). + DocumentReferencePlatform? get document; + + /// The creation time of the document + DateTime? get createTime; + + /// The update time of the document + DateTime? get updateTime; + + /// All fields in the result (from PipelineResult.data() on the native SDK). + /// Returns null if the result has no data. + Map? get data; +} diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/settings.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/settings.dart index ab5d3c993272..eba93af2ebea 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/settings.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/settings.dart @@ -20,6 +20,7 @@ class Settings { this.webExperimentalAutoDetectLongPolling, this.webExperimentalLongPollingOptions, this.ignoreUndefinedProperties = false, + this.webPersistentTabManager, }); /// Constant used to indicate the LRU garbage collection should be disabled. @@ -75,6 +76,18 @@ class Settings { /// Otherwise, these options have no effect. final WebExperimentalLongPollingOptions? webExperimentalLongPollingOptions; + /// Configures how multiple browser tabs are managed when using persistent + /// cache on web. + /// + /// When `null` (the default), the SDK uses single-tab mode. Set to + /// [WebPersistentMultipleTabManager] for multi-tab synchronization, or + /// [WebPersistentSingleTabManager] with [WebPersistentSingleTabManager.forceOwnership] + /// for Web Worker support. + /// + /// This setting only applies to Flutter Web with [persistenceEnabled] set + /// to `true`. It is ignored on other platforms. + final WebPersistentTabManager? webPersistentTabManager; + /// Returns the settings as a [Map] Map get asMap { return { @@ -88,6 +101,7 @@ class Settings { 'webExperimentalLongPollingOptions': webExperimentalLongPollingOptions?.asMap, if (kIsWeb) 'ignoreUndefinedProperties': ignoreUndefinedProperties, + if (kIsWeb) 'webPersistentTabManager': webPersistentTabManager, }; } @@ -100,6 +114,7 @@ class Settings { bool? webExperimentalAutoDetectLongPolling, bool? ignoreUndefinedProperties, WebExperimentalLongPollingOptions? webExperimentalLongPollingOptions, + WebPersistentTabManager? webPersistentTabManager, }) { assert( cacheSizeBytes == null || @@ -122,6 +137,8 @@ class Settings { this.webExperimentalLongPollingOptions, ignoreUndefinedProperties: ignoreUndefinedProperties ?? this.ignoreUndefinedProperties, + webPersistentTabManager: + webPersistentTabManager ?? this.webPersistentTabManager, ); } @@ -139,7 +156,8 @@ class Settings { webExperimentalAutoDetectLongPolling && other.webExperimentalLongPollingOptions == webExperimentalLongPollingOptions && - other.ignoreUndefinedProperties == ignoreUndefinedProperties; + other.ignoreUndefinedProperties == ignoreUndefinedProperties && + other.webPersistentTabManager == webPersistentTabManager; @override int get hashCode => Object.hash( @@ -152,12 +170,86 @@ class Settings { webExperimentalAutoDetectLongPolling, webExperimentalLongPollingOptions, ignoreUndefinedProperties, + webPersistentTabManager, ); @override String toString() => 'Settings($asMap)'; } +/// Configures how multiple browser tabs are managed by the Firestore SDK +/// when using persistent cache on web. +/// +/// This setting only applies to Flutter Web with [Settings.persistenceEnabled] +/// set to `true`. It is ignored on other platforms. +/// +/// See also: +/// - [WebPersistentMultipleTabManager] for multi-tab synchronization +/// - [WebPersistentSingleTabManager] for single-tab mode with optional +/// force ownership (Web Workers) +sealed class WebPersistentTabManager { + const WebPersistentTabManager(); +} + +/// Enables multi-tab synchronization for Firestore’s persistent cache. +/// +/// The SDK will synchronize queries and mutations across all open browser +/// tabs that use the same Firestore instance. +/// +/// Example: +/// ```dart +/// FirebaseFirestore.instance.settings = const Settings( +/// persistenceEnabled: true, +/// webPersistentTabManager: WebPersistentMultipleTabManager(), +/// ); +/// ``` +@immutable +class WebPersistentMultipleTabManager extends WebPersistentTabManager { + const WebPersistentMultipleTabManager(); + + @override + bool operator ==(Object other) => + other is WebPersistentMultipleTabManager && + other.runtimeType == runtimeType; + + @override + int get hashCode => runtimeType.hashCode; +} + +/// Configures the Firestore SDK to operate in single-tab mode. +/// +/// When [forceOwnership] is `true`, this tab forcibly acquires the +/// IndexedDB lock, which is useful for Web Workers but will cause other +/// tabs using persistence to fail. +/// +/// Example: +/// ```dart +/// FirebaseFirestore.instance.settings = const Settings( +/// persistenceEnabled: true, +/// webPersistentTabManager: WebPersistentSingleTabManager(forceOwnership: true), +/// ); +/// ``` +@immutable +class WebPersistentSingleTabManager extends WebPersistentTabManager { + const WebPersistentSingleTabManager({this.forceOwnership = false}); + + /// Whether to force-enable persistent (IndexedDB) cache for this tab. + /// + /// This cannot be used with multi-tab synchronization and is primarily + /// intended for use with Web Workers. Setting this to `true` will enable + /// IndexedDB, but cause other tabs using IndexedDB cache to fail. + final bool forceOwnership; + + @override + bool operator ==(Object other) => + other is WebPersistentSingleTabManager && + other.runtimeType == runtimeType && + other.forceOwnership == forceOwnership; + + @override + int get hashCode => Object.hash(runtimeType, forceOwnership); +} + /// Options that configure the SDK’s underlying network transport (WebChannel) when long-polling is used. @immutable class WebExperimentalLongPollingOptions { diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/generate_pigeon.sh b/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/generate_pigeon.sh index f6f0a2d4aef5..e8950e42ef53 100755 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/generate_pigeon.sh +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/generate_pigeon.sh @@ -15,7 +15,7 @@ sed -i '' 's/private static class FirebaseFirestoreHostApiCodec extends Standard echo "Android modification complete." # Fix iOS files -FILE_NAME="../../cloud_firestore/ios/Classes/FirestoreMessages.g.m" +FILE_NAME="../../cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FirestoreMessages.g.m" sed -i '' '/#import "FirestoreMessages.g.h"/a\ #import "FLTFirebaseFirestoreReader.h"\ #import "FLTFirebaseFirestoreWriter.h" @@ -26,7 +26,7 @@ sed -i '' 's/(self\.newIndex \?: \[NSNull null\]),/(self.index ?: [NSNull null]) sed -i '' 's/@interface FirebaseFirestoreHostApiCodecReader : FlutterStandardReader/@interface FirebaseFirestoreHostApiCodecReader : FLTFirebaseFirestoreReader/' $FILE_NAME sed -i '' 's/@interface FirebaseFirestoreHostApiCodecWriter : FlutterStandardWriter/@interface FirebaseFirestoreHostApiCodecWriter : FLTFirebaseFirestoreWriter/' $FILE_NAME -FILE_NAME="../../cloud_firestore/ios/Classes/Public/FirestoreMessages.g.h" +FILE_NAME="../../cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/include/cloud_firestore/Public/FirestoreMessages.g.h" sed -i '' 's/@property(nonatomic, strong) NSNumber \*newIndex;/@property(nonatomic, strong) NSNumber \*index;/' $FILE_NAME echo "iOS modification complete." diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/messages.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/messages.dart index 19a5ddfdc4ee..a71ecdec270c 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/messages.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/pigeons/messages.dart @@ -118,6 +118,31 @@ class PigeonQuerySnapshot { final PigeonSnapshotMetadata metadata; } +class PigeonPipelineResult { + const PigeonPipelineResult({ + this.documentPath, + this.createTime, + this.updateTime, + this.data, + }); + + final String? documentPath; + final int? createTime; // Timestamp in milliseconds since epoch + final int? updateTime; // Timestamp in milliseconds since epoch + /// All fields in the result (from PipelineResult.data() on Android). + final Map? data; +} + +class PigeonPipelineSnapshot { + const PigeonPipelineSnapshot({ + required this.results, + required this.executionTime, + }); + + final List results; + final int executionTime; // Timestamp in milliseconds since epoch +} + /// An enumeration of firestore source types. enum Source { /// Causes Firestore to try to retrieve an up-to-date (server-retrieved) snapshot, but fall back to @@ -442,4 +467,11 @@ abstract class FirebaseFirestoreHostApi { FirestorePigeonFirebaseApp app, PersistenceCacheIndexManagerRequest request, ); + + @async + PigeonPipelineSnapshot executePipeline( + FirestorePigeonFirebaseApp app, + List?> stages, + Map? options, + ); } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml b/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml index 632f5dd354c3..7753c04b0dce 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: cloud_firestore_platform_interface description: A common platform interface for the cloud_firestore plugin. -version: 7.0.6 +version: 7.0.7 homepage: https://github.com/firebase/flutterfire/tree/main/packages/cloud_firestore/cloud_firestore_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/cloud_firestore/cloud_firestore_platform_interface @@ -9,9 +9,9 @@ environment: flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 + _flutterfire_internals: ^1.3.67 collection: ^1.15.0 - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/pigeon/test_api.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/pigeon/test_api.dart index 32d57ebdcb48..e9ed58eafc5c 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/pigeon/test_api.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/pigeon/test_api.dart @@ -7,12 +7,12 @@ // ignore_for_file: avoid_relative_lib_imports import 'dart:async'; import 'dart:typed_data' show Uint8List; - -import 'package:cloud_firestore_platform_interface/src/pigeon/messages.pigeon.dart'; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:cloud_firestore_platform_interface/src/pigeon/messages.pigeon.dart'; + class _TestFirebaseFirestoreHostApiCodec extends StandardMessageCodec { const _TestFirebaseFirestoreHostApiCodec(); @override @@ -44,18 +44,24 @@ class _TestFirebaseFirestoreHostApiCodec extends StandardMessageCodec { } else if (value is PigeonGetOptions) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is PigeonQueryParameters) { + } else if (value is PigeonPipelineResult) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is PigeonQuerySnapshot) { + } else if (value is PigeonPipelineSnapshot) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is PigeonSnapshotMetadata) { + } else if (value is PigeonQueryParameters) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is PigeonTransactionCommand) { + } else if (value is PigeonQuerySnapshot) { buffer.putUint8(140); writeValue(buffer, value.encode()); + } else if (value is PigeonSnapshotMetadata) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else if (value is PigeonTransactionCommand) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -63,22 +69,40 @@ class _TestFirebaseFirestoreHostApiCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { - return switch (type) { - 128 => AggregateQuery.decode(readValue(buffer)!), - 129 => AggregateQueryResponse.decode(readValue(buffer)!), - 130 => DocumentReferenceRequest.decode(readValue(buffer)!), - 131 => FirestorePigeonFirebaseApp.decode(readValue(buffer)!), - 132 => PigeonDocumentChange.decode(readValue(buffer)!), - 133 => PigeonDocumentOption.decode(readValue(buffer)!), - 134 => PigeonDocumentSnapshot.decode(readValue(buffer)!), - 135 => PigeonFirebaseSettings.decode(readValue(buffer)!), - 136 => PigeonGetOptions.decode(readValue(buffer)!), - 137 => PigeonQueryParameters.decode(readValue(buffer)!), - 138 => PigeonQuerySnapshot.decode(readValue(buffer)!), - 139 => PigeonSnapshotMetadata.decode(readValue(buffer)!), - 140 => PigeonTransactionCommand.decode(readValue(buffer)!), - _ => super.readValueOfType(type, buffer) - }; + switch (type) { + case 128: + return AggregateQuery.decode(readValue(buffer)!); + case 129: + return AggregateQueryResponse.decode(readValue(buffer)!); + case 130: + return DocumentReferenceRequest.decode(readValue(buffer)!); + case 131: + return FirestorePigeonFirebaseApp.decode(readValue(buffer)!); + case 132: + return PigeonDocumentChange.decode(readValue(buffer)!); + case 133: + return PigeonDocumentOption.decode(readValue(buffer)!); + case 134: + return PigeonDocumentSnapshot.decode(readValue(buffer)!); + case 135: + return PigeonFirebaseSettings.decode(readValue(buffer)!); + case 136: + return PigeonGetOptions.decode(readValue(buffer)!); + case 137: + return PigeonPipelineResult.decode(readValue(buffer)!); + case 138: + return PigeonPipelineSnapshot.decode(readValue(buffer)!); + case 139: + return PigeonQueryParameters.decode(readValue(buffer)!); + case 140: + return PigeonQuerySnapshot.decode(readValue(buffer)!); + case 141: + return PigeonSnapshotMetadata.decode(readValue(buffer)!); + case 142: + return PigeonTransactionCommand.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } } } @@ -197,6 +221,12 @@ abstract class TestFirebaseFirestoreHostApi { PersistenceCacheIndexManagerRequest request, ); + Future executePipeline( + FirestorePigeonFirebaseApp app, + List?> stages, + Map? options, + ); + static void setup( TestFirebaseFirestoreHostApi? api, { BinaryMessenger? binaryMessenger, @@ -1088,5 +1118,43 @@ abstract class TestFirebaseFirestoreHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.cloud_firestore_platform_interface.FirebaseFirestoreHostApi.executePipeline', + codec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert( + message != null, + 'Argument for dev.flutter.pigeon.cloud_firestore_platform_interface.FirebaseFirestoreHostApi.executePipeline was null.', + ); + final List args = (message as List?)!; + final FirestorePigeonFirebaseApp? arg_app = + (args[0] as FirestorePigeonFirebaseApp?); + assert( + arg_app != null, + 'Argument for dev.flutter.pigeon.cloud_firestore_platform_interface.FirebaseFirestoreHostApi.executePipeline was null, expected non-null FirestorePigeonFirebaseApp.', + ); + final List?>? arg_stages = + (args[1] as List?)?.cast?>(); + assert( + arg_stages != null, + 'Argument for dev.flutter.pigeon.cloud_firestore_platform_interface.FirebaseFirestoreHostApi.executePipeline was null, expected non-null List?>.', + ); + final Map? arg_options = + (args[2] as Map?)?.cast(); + final PigeonPipelineSnapshot output = + await api.executePipeline(arg_app!, arg_stages!, arg_options); + return [output]; + }); + } + } } } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/settings_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/settings_test.dart index dfecd20fae20..a035780479f5 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/settings_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/settings_test.dart @@ -20,6 +20,7 @@ void main() { webExperimentalLongPollingOptions: WebExperimentalLongPollingOptions( timeoutDuration: Duration(seconds: 4), ), + webPersistentTabManager: WebPersistentMultipleTabManager(), ), equals( const Settings( @@ -33,6 +34,7 @@ void main() { WebExperimentalLongPollingOptions( timeoutDuration: Duration(seconds: 4), ), + webPersistentTabManager: WebPersistentMultipleTabManager(), ), ), ); @@ -111,6 +113,62 @@ void main() { test('CACHE_SIZE_UNLIMITED returns -1', () { expect(Settings.CACHE_SIZE_UNLIMITED, equals(-1)); }); + + test('WebPersistentTabManager equality', () { + expect( + const WebPersistentMultipleTabManager(), + equals(const WebPersistentMultipleTabManager()), + ); + + expect( + const WebPersistentSingleTabManager(), + equals(const WebPersistentSingleTabManager()), + ); + + expect( + const WebPersistentSingleTabManager(forceOwnership: true), + equals(const WebPersistentSingleTabManager(forceOwnership: true)), + ); + + expect( + const WebPersistentSingleTabManager(forceOwnership: true), + isNot(equals(const WebPersistentSingleTabManager())), + ); + + expect( + const WebPersistentMultipleTabManager(), + isNot(equals(const WebPersistentSingleTabManager())), + ); + }); + + test('Settings with different webPersistentTabManager are not equal', () { + expect( + const Settings( + persistenceEnabled: true, + webPersistentTabManager: WebPersistentMultipleTabManager(), + ), + isNot(equals( + const Settings( + persistenceEnabled: true, + webPersistentTabManager: WebPersistentSingleTabManager(), + ), + )), + ); + }); + + test('copyWith preserves webPersistentTabManager', () { + const settings = Settings( + persistenceEnabled: true, + webPersistentTabManager: WebPersistentMultipleTabManager(), + ); + + final copied = settings.copyWith(host: 'localhost'); + + expect(copied.webPersistentTabManager, + isA()); + expect(copied.host, 'localhost'); + expect(copied.persistenceEnabled, true); + }); }); } diff --git a/packages/cloud_firestore/cloud_firestore_web/CHANGELOG.md b/packages/cloud_firestore/cloud_firestore_web/CHANGELOG.md index 2adcf57d5b41..21917752a172 100644 --- a/packages/cloud_firestore/cloud_firestore_web/CHANGELOG.md +++ b/packages/cloud_firestore/cloud_firestore_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.1.3 + + - Update a dependency to the latest release. + ## 5.1.2 - Update a dependency to the latest release. diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart index bb7ddaa47d04..f4a46f69c482 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/cloud_firestore_web.dart @@ -21,6 +21,9 @@ import 'src/collection_reference_web.dart'; import 'src/document_reference_web.dart'; import 'src/field_value_factory_web.dart'; import 'src/interop/firestore.dart' as firestore_interop; +import 'src/interop/firestore_interop.dart' as firestore_interop_js; +import 'src/pipeline_builder_web.dart'; +import 'src/pipeline_web.dart'; import 'src/query_web.dart'; import 'src/transaction_web.dart'; import 'src/write_batch_web.dart'; @@ -145,6 +148,7 @@ class FirebaseFirestoreWeb extends FirebaseFirestorePlatform { sslEnabled: firestoreSettings.sslEnabled, cacheSizeBytes: firestoreSettings.cacheSizeBytes, ignoreUndefinedProperties: firestoreSettings.ignoreUndefinedProperties, + webPersistentTabManager: firestoreSettings.webPersistentTabManager, ); // Union type MemoryLocalCache | PersistentLocalCache dynamic localCache; @@ -152,10 +156,28 @@ class FirebaseFirestoreWeb extends FirebaseFirestorePlatform { if (persistenceEnabled == null || persistenceEnabled == false) { localCache = firestore_interop.memoryLocalCache(null); } else { - localCache = firestore_interop - .persistentLocalCache(firestore_interop.PersistentCacheSettings( - cacheSizeBytes: firestoreSettings.cacheSizeBytes?.toJS, - )); + final tabManagerSetting = firestoreSettings.webPersistentTabManager; + final firestore_interop.PersistentCacheSettings cacheSettings; + if (tabManagerSetting is WebPersistentMultipleTabManager) { + cacheSettings = firestore_interop.PersistentCacheSettings( + cacheSizeBytes: firestoreSettings.cacheSizeBytes?.toJS, + tabManager: firestore_interop.persistentMultipleTabManager(), + ); + } else if (tabManagerSetting is WebPersistentSingleTabManager) { + cacheSettings = firestore_interop.PersistentCacheSettings( + cacheSizeBytes: firestoreSettings.cacheSizeBytes?.toJS, + tabManager: firestore_interop.persistentSingleTabManager( + firestore_interop.PersistentSingleTabManagerSettings( + forceOwnership: tabManagerSetting.forceOwnership.toJS, + ), + ), + ); + } else { + cacheSettings = firestore_interop.PersistentCacheSettings( + cacheSizeBytes: firestoreSettings.cacheSizeBytes?.toJS, + ); + } + localCache = firestore_interop.persistentLocalCache(cacheSettings); } if (firestoreSettings.host != null && firestoreSettings.sslEnabled != null) { @@ -252,4 +274,30 @@ class FirebaseFirestoreWeb extends FirebaseFirestorePlatform { } _delegate.setLoggingEnabled(value); } + + @override + PipelinePlatform pipeline(List> initialStages) { + return PipelineWeb(this, _delegate, initialStages); + } + + @override + Future executePipeline( + List> stages, { + Map? options, + }) async { + return convertWebExceptions(() async { + final jsFirestore = _delegate.jsObject; + final jsPipeline = buildPipelineFromStages(jsFirestore, stages); + final dartPipeline = firestore_interop.Pipeline.getInstance(jsPipeline); + final snapshot = await dartPipeline.execute(); + + final results = snapshot.results + .map((r) => PipelineResultWeb(this, _delegate, r.jsObject)) + .toList(); + + final executionTime = snapshot.executionTime ?? DateTime.now(); + + return PipelineSnapshotWeb(results, executionTime); + }); + } } diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/cloud_firestore_version.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/cloud_firestore_version.dart index 5c0a9eb18eef..59c18f82c5ec 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/cloud_firestore_version.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/cloud_firestore_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '6.1.2'; +const packageVersion = '6.1.3'; diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart index d64c5f98cfeb..b7e448750a50 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore.dart @@ -1075,3 +1075,120 @@ class AggregateQuerySnapshot } } } + +// ============================================================================= +// Pipeline (global execute + snapshot/result wrappers) +// ============================================================================= + +/// Single result in a pipeline snapshot (document + data). +class PipelineResult + extends JsObjectWrapper { + static final _expando = Expando(); + + late final DocumentReference? _ref; + late final Map? _data; + late final DateTime? _createTime; + late final DateTime? _updateTime; + + static PipelineResult getInstance( + firestore_interop.PipelineResultJsImpl jsObject) { + return _expando[jsObject] ??= PipelineResult._fromJsObject(jsObject); + } + + PipelineResult._fromJsObject(firestore_interop.PipelineResultJsImpl jsObject) + : _ref = jsObject.ref != null + ? DocumentReference.getInstance(jsObject.ref!) + : null, + _data = _dataFromResult(jsObject), + _createTime = _timestampToDateTime(jsObject.createTime), + _updateTime = _timestampToDateTime(jsObject.updateTime), + super.fromJsObject(jsObject); + + static Map? _dataFromResult( + firestore_interop.PipelineResultJsImpl jsResult) { + final d = jsResult.data(); + if (d == null) return null; + final parsed = dartify(d); + return parsed != null + ? Map.from(parsed as Map) + : null; + } + + static DateTime? _timestampToDateTime(dynamic value) { + if (value == null) return null; + final d = dartify(value); + if (d == null) return null; + if (d is DateTime) return d; + if (d is Timestamp) return d.toDate(); + if (d is int) return DateTime.fromMillisecondsSinceEpoch(d); + return null; + } + + DocumentReference? get ref => _ref; + Map? get data => _data; + DateTime? get createTime => _createTime; + DateTime? get updateTime => _updateTime; +} + +/// Snapshot of pipeline execution results. +class PipelineSnapshot + extends JsObjectWrapper { + static final _expando = Expando(); + + late final List _results; + late final DateTime? _executionTime; + + static PipelineSnapshot getInstance( + firestore_interop.PipelineSnapshotJsImpl jsObject) { + // Bypass Expando to test if key type causes the error: + // return PipelineSnapshot._fromJsObject(jsObject); + return _expando[jsObject] ??= PipelineSnapshot._fromJsObject(jsObject); + } + + static List _buildResults( + firestore_interop.PipelineSnapshotJsImpl jsObject) { + final rawResults = jsObject.results.toDart; + return rawResults + .cast() + .map(PipelineResult.getInstance) + .toList(); + } + + PipelineSnapshot._fromJsObject( + firestore_interop.PipelineSnapshotJsImpl jsObject) + : _results = _buildResults(jsObject), + _executionTime = _executionTimeFromJs(jsObject.executionTime), + super.fromJsObject(jsObject); + + static DateTime? _executionTimeFromJs(dynamic value) { + if (value == null) return null; + final d = dartify(value); + if (d == null) return null; + if (d is DateTime) return d; + if (d is int) return DateTime.fromMillisecondsSinceEpoch(d); + return null; + } + + List get results => _results; + DateTime? get executionTime => _executionTime; +} + +/// Wraps a JS pipeline; use [execute] to run it via the global execute function. +class Pipeline extends JsObjectWrapper { + static final _expando = Expando(); + + static Pipeline getInstance(firestore_interop.PipelineJsImpl jsObject) { + return _expando[jsObject] ??= Pipeline._fromJsObject(jsObject); + } + + Pipeline._fromJsObject(firestore_interop.PipelineJsImpl jsObject) + : super.fromJsObject(jsObject); + + /// Runs this pipeline using the global JS SDK execute function. + Future execute() async { + final snapshot = + await firestore_interop.pipelines.execute(jsObject as JSAny).toDart; + return PipelineSnapshot.getInstance( + snapshot! as firestore_interop.PipelineSnapshotJsImpl); + } +} diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart index 9d9fa0fdadf4..9ca1b21f4150 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart @@ -234,9 +234,7 @@ external PersistentSingleTabManager persistentSingleTabManager( @JS() @staticInterop -external PersistentMultipleTabManager persistentMultipleTabManager( - PersistentSingleTabManagerSettings? settings, -); +external PersistentMultipleTabManager persistentMultipleTabManager(); @JS() @staticInterop @@ -335,13 +333,129 @@ external JSObject get and; @staticInterop external WriteBatchJsImpl writeBatch(FirestoreJsImpl firestore); -@JS('Firestore') -@staticInterop -abstract class FirestoreJsImpl {} - -extension FirestoreJsImplExtension on FirestoreJsImpl { +extension type FirestoreJsImpl._(JSObject _) implements JSObject { external AppJsImpl get app; external JSString get type; + + /// Returns the pipeline source for building and executing pipelines. + external JSAny pipeline(); +} + +@JS() +@staticInterop +external PipelinesJsImpl get pipelines; + +/// Pipeline expression API — mirrors the Firebase JS SDK pipelines module. +/// Use these to build expressions for where(), sort(), addFields(), aggregate(), etc. +extension type PipelinesJsImpl._(JSObject _) implements JSObject { + external JSPromise execute(JSAny pipeline); + + // --- Expression builders --- + external ExpressionJsImpl field(JSString path); + external ExpressionJsImpl constant(JSAny? value); + + // --- Boolean / comparison --- + external JSAny equal(JSAny left, JSAny right); + external JSAny notEqual(JSAny left, JSAny right); + external JSAny greaterThan(JSAny left, JSAny right); + external JSAny greaterThanOrEqual(JSAny left, JSAny right); + external JSAny lessThan(JSAny left, JSAny right); + external JSAny lessThanOrEqual(JSAny left, JSAny right); + external JSAny and(JSAny a, JSAny b); + external JSAny or(JSAny a, JSAny b); + external JSAny xor(JSAny a, JSAny b); + external JSAny not(JSAny expr); + + // --- Existence / type checks --- + external JSAny exists(JSAny expr); + external JSAny isAbsent(JSAny expr); + external JSAny isError(JSAny expr); + + // --- Array --- + external JSAny arrayContains(JSAny array, JSAny element); + external JSAny arrayContainsAny(JSAny array, JSArray values); + + // --- Ordering (for sort stage) --- + external JSAny ascending(JSAny expr); + external JSAny descending(JSAny expr); + + // --- Aggregates --- + external AggregateFunctionJsImpl sum(JSAny expr); + external AggregateFunctionJsImpl average(JSAny expr); + external AggregateFunctionJsImpl count(JSAny expr); + external AggregateFunctionJsImpl countDistinct(JSAny expr); + external AggregateFunctionJsImpl minimum(JSAny expr); + external AggregateFunctionJsImpl maximum(JSAny expr); + external AggregateFunctionJsImpl countAll(); + + // --- Aliased (for select/addFields/aggregate output names) --- + external JSAny aliased(JSAny expr, JSString alias); +} + +/// Aggregate function (result of sum(), average(), count(), etc. on pipelines). +/// Has .as(alias) to create an aliased aggregate for accumulators. +extension type AggregateFunctionJsImpl._(JSObject _) implements JSObject { + @JS('as') + external JSAny asAlias(JSString alias); +} + +extension type ExpressionJsImpl._(JSObject _) implements JSObject { + @JS('as') + external JSAny asAlias(JSString alias); + + external ExpressionJsImpl add(JSAny right); + external ExpressionJsImpl subtract(JSAny right); + external ExpressionJsImpl multiply(JSAny right); + external ExpressionJsImpl divide(JSAny right); + @JS('mod') + external ExpressionJsImpl modulo(JSAny right); + external ExpressionJsImpl length(); + external ExpressionJsImpl concat(JSAny right); + external ExpressionJsImpl toLowerCase(); + external ExpressionJsImpl toUpperCase(); + external ExpressionJsImpl trim(); +} + +extension type SelectableJsImpl._(JSObject _) implements JSObject { + @JS('as') + external JSAny asAlias(JSString alias); +} + +/// Aliased aggregate for use in aggregate() stage accumulators. +/// Mirrors Firebase JS SDK: constructor(aggregate, alias, _methodName?). +@JS('AliasedAggregate') +@staticInterop +abstract class AliasedAggregateJsImpl { + external factory AliasedAggregateJsImpl( + JSAny aggregate, + JSString alias, [ + JSString? methodName, + ]); +} + +/// Options for the aggregate() pipeline stage. +/// Mirrors Firebase JS SDK AggregateStageOptions: { accumulators, groups? }. +extension type AggregateStageOptionsJsImpl._(JSObject _) implements JSObject { + AggregateStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set accumulators(JSAny value); + // ignore: avoid_setters_without_getters + external set groups(JSAny value); +} + +extension type SelectStageOptionsJsImpl._(JSObject _) implements JSObject { + SelectStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set selections(JSArray value); +} + +extension type AddFieldsOptionsJsImpl._(JSObject _) implements JSObject { + AddFieldsOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set fields(JSAny value); } extension type WriteBatchJsImpl._(JSObject _) implements JSObject { @@ -825,11 +939,20 @@ extension PersistentCacheSettingsExtension on PersistentCacheSettings { external set tabManager(JSObject v); } -/// An settings object to configure an PersistentLocalCache instance. +/// Settings to configure a PersistentSingleTabManager instance. /// /// See: . -extension type PersistentSingleTabManagerSettings._(JSObject _) - implements JSObject { +@anonymous +@JS() +@staticInterop +abstract class PersistentSingleTabManagerSettings { + external factory PersistentSingleTabManagerSettings({ + JSBoolean? forceOwnership, + }); +} + +extension PersistentSingleTabManagerSettingsExtension + on PersistentSingleTabManagerSettings { /// Whether to force-enable persistent (IndexedDB) cache for the client. /// This cannot be used with multi-tab synchronization and is primarily /// intended for use with Web Workers. @@ -1007,3 +1130,134 @@ extension type AggregateQuerySnapshotJsImpl._(JSObject _) implements JSObject { @JS() @staticInterop abstract class PersistentCacheIndexManager {} + +/// Entry point for defining the data source of a Firestore Pipeline. +/// Use .collection(), .collectionGroup(), .database(), or .documents(). +extension type PipelineSourceJsImpl._(JSObject _) implements JSObject { + /// Returns all documents from the entire collection (can be nested). + external PipelineJsImpl collection(JSString collectionPath); + + /// Returns all documents from a collection ID regardless of parent. + external PipelineJsImpl collectionGroup(JSString collectionId); + + /// Returns all documents from the entire database. + external PipelineJsImpl database(); + + /// Sets the pipeline source to the given document paths or references. + external PipelineJsImpl documents(JSArray docs); +} + +/// Pipeline returned by PipelineSource methods; chain stages and call execute(). +/// See: https://firebase.google.com/docs/reference/js/firestore_pipelines.pipeline +extension type PipelineJsImpl._(JSObject _) implements JSObject { + external JSPromise execute( + [PipelineExecuteOptions? options]); + external PipelineJsImpl limit(JSNumber limit); + external PipelineJsImpl offset(JSNumber offset); + external PipelineJsImpl where(JSAny condition); + external PipelineJsImpl sort(JSAny orderingOrOptions); + external PipelineJsImpl addFields(JSAny fieldOrOptions); + external PipelineJsImpl select(JSAny selectionOrOptions); + external PipelineJsImpl distinct(JSAny groupOrOptions); + external PipelineJsImpl aggregate(AggregateStageOptionsJsImpl options); + external PipelineJsImpl sample(JSAny documentsOrOptions); + external PipelineJsImpl unnest(JSAny selectableOrOptions); + external PipelineJsImpl removeFields(JSAny fieldOrOptions); + external PipelineJsImpl replaceWith(JSAny fieldNameOrOptions); + external PipelineJsImpl findNearest(JSAny options); + external PipelineJsImpl union(JSAny otherOrOptions); + external PipelineJsImpl rawStage(JSString name, JSArray params, + [JSAny? options]); +} + +/// Options for pipeline execution (e.g. index mode). +@anonymous +@JS() +@staticInterop +abstract class PipelineExecuteOptions { + external factory PipelineExecuteOptions({JSString? indexMode}); +} + +extension PipelineExecuteOptionsExtension on PipelineExecuteOptions { + external JSString? get indexMode; + external set indexMode(JSString? v); +} + +/// Snapshot of pipeline execution results. +extension type PipelineSnapshotJsImpl._(JSObject _) implements JSObject { + /// Array of [PipelineResultJsImpl]. + external JSArray get results; + + /// Execution time (if provided by SDK). + external JSAny? get executionTime; +} + +/// Single result in a pipeline snapshot (document + data). +extension type PipelineResultJsImpl._(JSObject _) implements JSObject { + external DocumentReferenceJsImpl? get ref; + external JSObject? data(); + external JSAny? get createTime; + external JSAny? get updateTime; +} + +extension type SampleStageOptionsJsImpl._(JSObject _) implements JSObject { + SampleStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set documents(JSAny value); + // ignore: avoid_setters_without_getters + external set percentage(JSAny value); +} + +extension type SortStageOptionsJsImpl._(JSObject _) implements JSObject { + SortStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set orderings(JSAny value); +} + +extension type DistinctStageOptionsJsImpl._(JSObject _) implements JSObject { + DistinctStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set groups(JSAny value); +} + +extension type UnnestStageOptionsJsImpl._(JSObject _) implements JSObject { + UnnestStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set selectable(JSAny value); + // ignore: avoid_setters_without_getters + external set indexField(JSString? value); +} + +extension type RemoveFieldsStageOptionsJsImpl._(JSObject _) + implements JSObject { + RemoveFieldsStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set fields(JSArray value); +} + +extension type ReplaceWithStageOptionsJsImpl._(JSObject _) implements JSObject { + ReplaceWithStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set map(JSAny value); +} + +extension type FindNearestStageOptionsJsImpl._(JSObject _) implements JSObject { + FindNearestStageOptionsJsImpl() : this._(JSObject.new()); + + // ignore: avoid_setters_without_getters + external set field(JSAny value); + // ignore: avoid_setters_without_getters + external set vectorValue(JSAny value); + // ignore: avoid_setters_without_getters + external set distanceMeasure(JSString value); + // ignore: avoid_setters_without_getters + external set limit(JSNumber value); + // ignore: avoid_setters_without_getters + external set distanceField(JSString value); +} diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_builder_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_builder_web.dart new file mode 100644 index 000000000000..8faede5c1c24 --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_builder_web.dart @@ -0,0 +1,150 @@ +// ignore_for_file: require_trailing_commas +// 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. + +import 'dart:js_interop'; + +import 'package:cloud_firestore_web/src/interop/firestore_interop.dart' + as interop; +import 'package:cloud_firestore_web/src/pipeline_expression_parser_web.dart'; + +/// Builds a JS Pipeline from serialized [stages] and returns it ready to execute. +/// Keeps [executePipeline] thin: build → execute → convert. +interop.PipelineJsImpl buildPipelineFromStages( + interop.FirestoreJsImpl jsFirestore, + List> stages, +) { + if (stages.isEmpty) { + throw ArgumentError('Pipeline must have at least one stage (source).'); + } + final source = jsFirestore.pipeline(); + final first = stages.first; + final stageName = first['stage'] as String?; + + // Build source stage + interop.PipelineJsImpl pipeline = _applySourceStage( + source as interop.PipelineSourceJsImpl, jsFirestore, stageName, first); + + final converter = PipelineExpressionParserWeb(interop.pipelines, jsFirestore); + + // Apply remaining stages + for (var i = 1; i < stages.length; i++) { + pipeline = _applyStage(pipeline, stages[i], converter, jsFirestore); + } + return pipeline; +} + +interop.PipelineJsImpl _applySourceStage( + interop.PipelineSourceJsImpl source, + interop.FirestoreJsImpl jsFirestore, + String? stageName, + Map first, +) { + final args = first['args']; + switch (stageName) { + case 'collection': + final path = (args is Map ? args['path'] as String? : null) ?? ''; + return source.collection(path.toJS); + case 'collection_group': + final path = (args is Map ? args['path'] as String? : null) ?? ''; + return source.collectionGroup(path.toJS); + case 'database': + return source.database(); + case 'documents': + final docsRaw = first['args']; + final docs = docsRaw is List + ? docsRaw + : (args is Map + ? args['documents'] as List? ?? + args['paths'] as List? + : null) ?? + []; + final paths = docs + .map((e) => (e is Map ? e['path'] as String? : e?.toString()) ?? '') + .where((s) => s.isNotEmpty) + .toList(); + final refs = + paths.map((p) => interop.doc(jsFirestore as JSAny, p.toJS)).toList(); + return source.documents(refs.toJS); + default: + throw UnsupportedError( + 'Pipeline source stage "$stageName" is not supported on web.', + ); + } +} + +interop.PipelineJsImpl _applyStage( + interop.PipelineJsImpl pipeline, + Map stage, + PipelineExpressionParserWeb converter, + interop.FirestoreJsImpl jsFirestore, +) { + final name = stage['stage'] as String?; + final args = stage['args']; + final map = args is Map ? args : {}; + + switch (name) { + case 'limit': + final limit = map['limit'] as int; + return pipeline.limit(limit.toJS); + case 'offset': + final offset = map['offset'] as int; + return pipeline.offset(offset.toJS); + case 'where': + final expression = map['expression']; + if (expression == null) return pipeline; + final condition = + converter.toBooleanExpression(expression as Map); + if (condition == null) { + throw UnsupportedError( + 'Pipeline where() on web: could not parse the condition expression.', + ); + } + return pipeline.where(condition); + case 'sort': + final orderings = map['orderings'] as List?; + if (orderings == null || orderings.isEmpty) return pipeline; + return pipeline.sort(converter.toSortOptions(orderings)); + case 'add_fields': + final expressions = map['expressions'] as List?; + if (expressions == null || expressions.isEmpty) return pipeline; + return pipeline.addFields(converter.toAddFieldsOptions(expressions)); + case 'select': + final expressions = map['expressions'] as List?; + if (expressions == null || expressions.isEmpty) return pipeline; + return pipeline.select(converter.toSelectOptions(expressions)); + case 'distinct': + final expressions = map['expressions'] as List?; + if (expressions == null || expressions.isEmpty) return pipeline; + return pipeline.distinct(converter.toDistinctOptions(expressions)); + case 'aggregate': + return pipeline.aggregate(converter.toAggregateOptionsFromFunctions(map)); + case 'aggregate_with_options': + return pipeline + .aggregate(converter.toAggregateOptionsFromStageAndOptions(map)); + case 'sample': + return pipeline.sample(converter.toSampleOptions(args)); + case 'unnest': + return pipeline.unnest(converter.toUnnestOptions(map)); + case 'remove_fields': + final fieldPaths = map['field_paths'] as List?; + if (fieldPaths == null || fieldPaths.isEmpty) return pipeline; + return pipeline.removeFields(converter.toRemoveFieldsOptions(fieldPaths)); + case 'replace_with': + final expression = map['expression']; + if (expression == null) return pipeline; + return pipeline.replaceWith( + converter.toReplaceWithOptions(expression as Map)); + case 'find_nearest': + return pipeline.findNearest(converter.toFindNearestOptions(map)); + case 'union': + final pipelineStages = map['pipeline'] as List>; + final otherPipeline = + buildPipelineFromStages(jsFirestore, pipelineStages); + return pipeline.union(otherPipeline); + default: + // Ignore unknown stages + return pipeline; + } +} diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart new file mode 100644 index 000000000000..d7ff79df173f --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart @@ -0,0 +1,436 @@ +// ignore_for_file: require_trailing_commas +// 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. + +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart' + show Blob, GeoPoint, Timestamp, VectorValue; +import 'package:cloud_firestore_web/src/interop/firestore_interop.dart' + as interop; +import 'package:cloud_firestore_web/src/interop/utils/utils.dart'; + +/// Converts Dart serialized pipeline expressions/stage args into JS pipeline +/// types by calling the pipelines interop API (field, constant, equal, and, +/// ascending, etc.) that mirrors the Firebase JS SDK. +class PipelineExpressionParserWeb { + PipelineExpressionParserWeb(this._pipelines, this._jsFirestore); + + final interop.PipelinesJsImpl _pipelines; + final interop.FirestoreJsImpl _jsFirestore; + + static const _kName = 'name'; + static const _kArgs = 'args'; + static const _kLeft = 'left'; + static const _kRight = 'right'; + static const _kExpression = 'expression'; + static const _kField = 'field'; + static const _kAlias = 'alias'; + static const _kValue = 'value'; + + // ── Value expressions ───────────────────────────────────────────────────── + + /// Converts a serialized value expression to a JS Expression. + interop.ExpressionJsImpl toExpression(Map map) { + final name = map[_kName] as String?; + final argsMap = _argsOf(map); + switch (name) { + case 'field': + return _pipelines.field(((argsMap[_kField] as String?) ?? '').toJS); + case 'add': + return _binaryArithmetic(argsMap, (l, r) => l.add(r)); + case 'subtract': + return _binaryArithmetic(argsMap, (l, r) => l.subtract(r)); + case 'multiply': + return _binaryArithmetic(argsMap, (l, r) => l.multiply(r)); + case 'divide': + return _binaryArithmetic(argsMap, (l, r) => l.divide(r)); + case 'modulo': + return _binaryArithmetic(argsMap, (l, r) => l.modulo(r)); + case 'constant': + case 'null': + return _pipelines.constant(_constantValueToJs(argsMap[_kValue])); + default: + throw UnsupportedError('Unsupported expression: $name'); + } + } + + // ── Boolean expressions ─────────────────────────────────────────────────── + + /// Converts a serialized boolean expression to a JS BooleanExpression. + /// + /// Returns null if [map] is not a recognized boolean expression. + JSAny? toBooleanExpression(Map map) { + final name = map[_kName] as String?; + final argsMap = _argsOf(map); + switch (name) { + case 'equal': + return _pipelines.equal( + _expr(argsMap, _kLeft), _expr(argsMap, _kRight)); + case 'not_equal': + return _pipelines.notEqual( + _expr(argsMap, _kLeft), _expr(argsMap, _kRight)); + case 'greater_than': + return _pipelines.greaterThan( + _expr(argsMap, _kLeft), _expr(argsMap, _kRight)); + case 'greater_than_or_equal': + return _pipelines.greaterThanOrEqual( + _expr(argsMap, _kLeft), _expr(argsMap, _kRight)); + case 'less_than': + return _pipelines.lessThan( + _expr(argsMap, _kLeft), _expr(argsMap, _kRight)); + case 'less_than_or_equal': + return _pipelines.lessThanOrEqual( + _expr(argsMap, _kLeft), _expr(argsMap, _kRight)); + case 'and': + case 'or': + case 'xor': + final exprMaps = argsMap['expressions'] as List?; + if (exprMaps == null || exprMaps.isEmpty) return null; + final exprs = exprMaps + .map((e) => toBooleanExpression(e as Map)) + .whereType() + .toList(); + if (exprs.isEmpty) return null; + var result = exprs.first; + for (var i = 1; i < exprs.length; i++) { + if (name == 'and') { + result = _pipelines.and(result, exprs[i]); + } else if (name == 'or') { + result = _pipelines.or(result, exprs[i]); + } else { + result = _pipelines.xor(result, exprs[i]); + } + } + return result; + case 'not': + return _pipelines.not(_expr(argsMap, _kExpression)); + case 'exists': + return _pipelines.exists(_expr(argsMap, _kExpression)); + case 'is_absent': + return _pipelines.isAbsent(_expr(argsMap, _kExpression)); + case 'is_error': + return _pipelines.isError(_expr(argsMap, _kExpression)); + case 'array_contains': + return _pipelines.arrayContains( + _expr(argsMap, 'array'), _expr(argsMap, 'element')); + case 'array_contains_any': + final valuesMaps = argsMap['values'] as List?; + if (valuesMaps == null || valuesMaps.isEmpty) return null; + final valuesJs = valuesMaps + .map((v) => toExpression(v as Map)) + .toList() + .toJS; + return _pipelines.arrayContainsAny(_expr(argsMap, 'array'), valuesJs); + case 'filter': + return _buildFilterExpression(argsMap); + default: + return null; + } + } + + // ── Stage options ───────────────────────────────────────────────────────── + + /// Converts orderings list to JS SortStageOptions. + /// + /// Each item shape: `{ expression: Map, order_direction: 'asc' | 'desc' }`. + JSAny toSortOptions(List orderings) { + final list = []; + for (final o in orderings) { + final m = o is Map ? o : {}; + final expr = m[_kExpression]; + if (expr == null) continue; + final exprJs = toExpression(expr as Map); + final dir = m['order_direction'] as String?; + list.add(dir == 'desc' + ? _pipelines.descending(exprJs) + : _pipelines.ascending(exprJs)); + } + if (list.isEmpty) { + throw UnsupportedError( + 'Pipeline sort() on web requires the Firebase JS pipeline expression API ' + '(ascending, descending). Ensure the pipelines module is loaded.', + ); + } + return interop.SortStageOptionsJsImpl()..orderings = list.toJS; + } + + /// Converts a single expression map to a JS Selectable (field or aliased). + JSAny toSelectable(Map map) { + final name = map[_kName] as String?; + final argsMap = _argsOf(map); + if (name == 'field') { + return _pipelines.field(((argsMap[_kField] as String?) ?? '').toJS); + } + if (name == _kAlias) { + final alias = argsMap[_kAlias] as String; + final expression = argsMap[_kExpression]; + return toExpression(expression as Map) + .asAlias(alias.toJS); + } + return toExpression(map); + } + + /// Converts add_fields expressions to JS AddFieldsStageOptions. + JSAny toAddFieldsOptions(List expressions) => + interop.AddFieldsOptionsJsImpl() + ..fields = _toSelectableList(expressions).toJS; + + /// Converts select stage expressions to JS SelectStageOptions. + JSAny toSelectOptions(List expressions) => + interop.SelectStageOptionsJsImpl() + ..selections = _toSelectableList(expressions).toJS; + + /// Converts distinct stage groups to JS DistinctStageOptions. + JSAny toDistinctOptions(List expressions) { + final list = _toSelectableList(expressions); + if (list.isEmpty) { + throw UnsupportedError( + 'Pipeline distinct() on web requires the Firebase JS pipeline expression API.', + ); + } + return interop.DistinctStageOptionsJsImpl()..groups = list.toJS; + } + + // ── Aggregate ───────────────────────────────────────────────────────────── + + /// Converts args for the 'aggregate' stage to JS AggregateStageOptions. + /// + /// Expects [map] to contain an [aggregate_functions] list. + interop.AggregateStageOptionsJsImpl toAggregateOptionsFromFunctions( + Map map) { + final list = map['aggregate_functions'] as List; + return _buildAccumulators(list); + } + + /// Converts args for the 'aggregate_with_options' stage to JS AggregateStageOptions. + /// + /// Expects [map] to contain an [aggregate_stage] map with [accumulators] + /// and optionally [groups]. + interop.AggregateStageOptionsJsImpl toAggregateOptionsFromStageAndOptions( + Map map) { + final stage = map['aggregate_stage'] as Map; + final list = stage['accumulators'] as List; + final groups = stage['groups'] as List?; + return _buildAccumulators(list, groups: groups); + } + + // ── Other stage options ─────────────────────────────────────────────────── + + /// Converts sample stage args to JS (integer count or SampleStageOptions). + /// + /// Dart serializes as `{ type: 'size', value: n }` or a raw number. + JSAny toSampleOptions(Map map) { + final args = map['type'] as String; + if (args == 'size') { + final value = map['value'] as num; + return interop.SampleStageOptionsJsImpl()..documents = value.toInt().toJS; + } else { + final value = map['value'] as num; + return interop.SampleStageOptionsJsImpl() + ..percentage = value.toDouble().toJS; + } + } + + /// Converts unnest stage args to JS UnnestStageOptions. + JSAny toUnnestOptions(Map map) { + final expression = map[_kExpression] as Map; + final indexField = map['index_field'] as String?; + final sel = toSelectable(expression); + return interop.UnnestStageOptionsJsImpl() + ..selectable = sel + ..indexField = indexField?.toJS; + } + + /// Converts remove_fields field paths to JS RemoveFieldsStageOptions. + JSAny toRemoveFieldsOptions(List fieldPaths) { + final paths = []; + for (final e in fieldPaths) { + final s = e is String + ? e + : (e is Map ? e[_kField] ?? e['path'] : null)?.toString(); + if (s != null) paths.add(s.toJS); + } + return interop.RemoveFieldsStageOptionsJsImpl()..fields = paths.toJS; + } + + /// Converts replace_with expression to JS ReplaceWithStageOptions. + JSAny toReplaceWithOptions(Map expression) { + return interop.ReplaceWithStageOptionsJsImpl() + ..map = toExpression(expression); + } + + /// Converts find_nearest args to JS FindNearestStageOptions. + interop.FindNearestStageOptionsJsImpl toFindNearestOptions( + Map map) { + final vectorField = + (map['vector_field'] as String?) ?? (map[_kField] as String?); + final vectorValue = map['vector_value'] as List?; + final distanceMeasure = (map['distance_measure'] as String?) ?? 'cosine'; + final limit = map['limit'] as int?; + final distanceField = map['distance_field'] as String?; + if (vectorField == null || vectorValue == null) { + throw UnsupportedError( + 'Pipeline findNearest() on web requires vector_field and vector_value.', + ); + } + final doubles = vectorValue.map((e) => (e as num).toDouble()).toList(); + final opts = interop.FindNearestStageOptionsJsImpl() + ..field = _pipelines.field(vectorField.toJS) + ..vectorValue = interop.vector(doubles.jsify()! as JSArray) + ..distanceMeasure = distanceMeasure.toJS; + if (limit != null) opts.limit = limit.toJS; + if (distanceField != null) opts.distanceField = distanceField.toJS; + return opts; + } + + // ── Private helpers ─────────────────────────────────────────────────────── + + /// Converts a [Constant] value to the correct JS type for the pipelines API. + /// + /// Each Dart type accepted by [Constant] is mapped to the corresponding + /// Firestore JS SDK interop type so that the JS SDK receives a properly typed + /// value (e.g. a JS `Timestamp`, `GeoPoint`, or `Bytes` object) rather than + /// a plain JS primitive or an unrecognised object. + JSAny? _constantValueToJs(Object? value) { + if (value == null) return null; + if (value is String) return value.toJS; + if (value is bool) return value.toJS; + if (value is int) return value.toJS; + if (value is double) return value.toJS; + if (value is DateTime) { + return interop.TimestampJsImpl.fromMillis( + value.millisecondsSinceEpoch.toJS) as JSAny; + } + + if (value is Timestamp) { + // Use seconds + nanoseconds directly to preserve sub-millisecond precision. + return interop.TimestampJsImpl(value.seconds.toJS, value.nanoseconds.toJS) + as JSAny; + } + if (value is GeoPoint) { + return interop.GeoPointJsImpl(value.latitude.toJS, value.longitude.toJS) + as JSAny; + } + if (value is Blob) { + return interop.BytesJsImpl.fromUint8Array(value.bytes.toJS) as JSAny; + } + if (value is List) { + return interop.BytesJsImpl.fromUint8Array(Uint8List.fromList(value).toJS) + as JSAny; + } + if (value is VectorValue) { + return interop.vector(value.toArray().jsify()! as JSArray) as JSAny; + } + if (value is Map) { + final path = value['path'] as String; + return interop.doc(_jsFirestore as JSAny, path.toJS) as JSAny; + } + return jsify(value); + } + + /// Extracts and safe-casts the 'args' sub-map from an expression map. + static Map _argsOf(Map map) { + final a = map[_kArgs]; + return a is Map ? a : const {}; + } + + /// Resolves [key] from [argsMap] as a value expression. + JSAny _expr(Map argsMap, String key) => + toExpression(argsMap[key] as Map); + + interop.ExpressionJsImpl _binaryArithmetic( + Map argsMap, + interop.ExpressionJsImpl Function( + interop.ExpressionJsImpl left, interop.ExpressionJsImpl right) + op, + ) => + op( + toExpression(argsMap[_kLeft] as Map), + toExpression(argsMap[_kRight] as Map), + ); + + JSAny? _buildFilterExpression(Map argsMap) { + final operator = argsMap['operator'] as String?; + final expressions = argsMap['expressions'] as List?; + if (expressions == null || expressions.isEmpty) return null; + final jsList = expressions + .map((e) => toExpression(e as Map)) + .toList(); + if (jsList.length == 1) return jsList.single; + JSAny result = jsList[0]; + for (var i = 1; i < jsList.length; i++) { + result = operator == 'and' + ? _pipelines.and(result, jsList[i]) + : _pipelines.or(result, jsList[i]); + } + return result; + } + + List _toSelectableList(List expressions) => expressions + .map((e) => + toSelectable(e is Map ? e : {})) + .whereType() + .toList(); + + interop.AggregateStageOptionsJsImpl _buildAccumulators( + List items, { + List? groups, + }) { + final accumulators = items + .map((item) => _parseAccumulator(item as Map)) + .whereType() + .toList(); + + if (accumulators.isEmpty) { + throw UnsupportedError( + 'Pipeline aggregate() on web requires at least one valid accumulator.', + ); + } + + final opts = interop.AggregateStageOptionsJsImpl() + ..accumulators = accumulators.toJS; + if (groups != null && groups.isNotEmpty) { + opts.groups = _toSelectableList(groups).toJS; + } + return opts; + } + + JSAny? _parseAccumulator(Map item) { + final args = item[_kArgs] as Map; + final alias = args[_kAlias] as String?; + final aggregateFn = args['aggregate_function'] as Map?; + if (alias == null || aggregateFn == null) return null; + final fnName = aggregateFn[_kName] as String?; + if (fnName == null) return null; + final expressionMap = (aggregateFn[_kArgs] + as Map?)?[_kExpression] as Map?; + final exprJs = expressionMap != null ? toExpression(expressionMap) : null; + return _buildAggregateFunction(fnName, exprJs)?.asAlias(alias.toJS); + } + + /// Builds one JS aggregate function from a serialized [name] and optional [exprJs]. + interop.AggregateFunctionJsImpl? _buildAggregateFunction( + String name, JSAny? exprJs) { + switch (name) { + case 'count_all': + return _pipelines.countAll(); + case 'sum': + return exprJs != null ? _pipelines.sum(exprJs) : null; + case 'average': + return exprJs != null ? _pipelines.average(exprJs) : null; + case 'count': + return exprJs != null ? _pipelines.count(exprJs) : null; + case 'count_distinct': + return exprJs != null ? _pipelines.countDistinct(exprJs) : null; + case 'minimum': + return exprJs != null ? _pipelines.minimum(exprJs) : null; + case 'maximum': + return exprJs != null ? _pipelines.maximum(exprJs) : null; + default: + return null; + } + } +} diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_web.dart new file mode 100644 index 000000000000..4361b186befc --- /dev/null +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_web.dart @@ -0,0 +1,108 @@ +// ignore_for_file: require_trailing_commas +// 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. + +import 'dart:js_interop'; + +import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart'; +import 'package:cloud_firestore_web/src/interop/utils/utils.dart'; + +import 'document_reference_web.dart'; +import 'interop/firestore.dart' as firestore_interop; +import 'interop/firestore_interop.dart' as interop; + +/// Web implementation of [PipelinePlatform]. +class PipelineWeb extends PipelinePlatform { + final firestore_interop.Firestore _firestoreWeb; + + PipelineWeb( + FirebaseFirestorePlatform firestore, + this._firestoreWeb, + List>? stages, + ) : super(firestore, stages); + + @override + PipelinePlatform addStage(Map serializedStage) { + return PipelineWeb( + firestore, + _firestoreWeb, + [...stages, serializedStage], + ); + } + + @override + Future execute({ + Map? options, + }) async { + return firestore.executePipeline(stages, options: options); + } +} + +/// Web implementation of [PipelineSnapshotPlatform]. +class PipelineSnapshotWeb extends PipelineSnapshotPlatform { + PipelineSnapshotWeb(this._results, this._executionTime) : super(); + + final List _results; + final DateTime _executionTime; + + @override + List get results => _results; + + @override + DateTime get executionTime => _executionTime; +} + +/// Web implementation of [PipelineResultPlatform]. +class PipelineResultWeb extends PipelineResultPlatform { + PipelineResultWeb( + FirebaseFirestorePlatform firestore, + firestore_interop.Firestore firestoreWeb, + interop.PipelineResultJsImpl jsResult, + ) : _document = jsResult.ref != null + ? DocumentReferenceWeb( + firestore, + firestoreWeb, + jsResult.ref!.path.toDart, + ) + : null, + _createTime = _timestampToDateTime(jsResult.createTime), + _updateTime = _timestampToDateTime(jsResult.updateTime), + _data = _dataFromResult(jsResult), + super(); + + final DocumentReferencePlatform? _document; + final DateTime? _createTime; + final DateTime? _updateTime; + final Map? _data; + + static Map? _dataFromResult( + interop.PipelineResultJsImpl jsResult) { + final d = jsResult.data(); + return d != null + ? Map.from(dartify(d) as Map) + : null; + } + + static DateTime? _timestampToDateTime(JSAny? value) { + if (value == null) return null; + final d = dartify(value); + if (d == null) return null; + if (d is DateTime) return d; + if (d is Timestamp) return d.toDate(); + if (d is int) return DateTime.fromMillisecondsSinceEpoch(d); + return null; + } + + @override + DocumentReferencePlatform? get document => _document; + + @override + DateTime? get createTime => _createTime; + + @override + DateTime? get updateTime => _updateTime; + + @override + Map? get data => _data; +} diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/encode_utility.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/encode_utility.dart index defa2f1fb702..ca5f5bf5ea4b 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/encode_utility.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/utils/encode_utility.dart @@ -18,8 +18,12 @@ class EncodeUtility { if (data == null) { return null; } - Map output = Map.from(data); - output.updateAll((key, value) => valueEncode(value)); + final output = {}; + data.forEach((key, value) { + final stringKey = + key is DocumentReferencePlatform ? key.path : key as String; + output[stringKey] = valueEncode(value); + }); return output; } @@ -126,8 +130,8 @@ class EncodeUtility { return firestore_interop.BytesJsImpl.fromUint8Array(value.bytes.toJS); } else if (value is DocumentReferenceWeb) { return value.firestoreWeb.doc(value.path); - } else if (value is Map) { - return encodeMapData(value); + } else if (value is Map) { + return encodeMapData(value.cast()); } else if (value is List) { return encodeArrayData(value); } else if (value is Iterable) { diff --git a/packages/cloud_firestore/cloud_firestore_web/pubspec.yaml b/packages/cloud_firestore/cloud_firestore_web/pubspec.yaml index 6e1623e063f9..e26d2b243d57 100644 --- a/packages/cloud_firestore/cloud_firestore_web/pubspec.yaml +++ b/packages/cloud_firestore/cloud_firestore_web/pubspec.yaml @@ -3,18 +3,18 @@ description: The web implementation of cloud_firestore homepage: https://github.com/firebase/flutterfire/tree/main/packages/cloud_firestore/cloud_firestore_web repository: https://github.com/firebase/flutterfire/tree/main/packages/cloud_firestore/cloud_firestore_web -version: 5.1.2 +version: 5.1.3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 - cloud_firestore_platform_interface: ^7.0.6 + _flutterfire_internals: ^1.3.67 + cloud_firestore_platform_interface: ^7.0.7 collection: ^1.0.0 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/cloud_functions/cloud_functions/CHANGELOG.md b/packages/cloud_functions/cloud_functions/CHANGELOG.md index 9468ff7808bc..cbdfce5d99fb 100644 --- a/packages/cloud_functions/cloud_functions/CHANGELOG.md +++ b/packages/cloud_functions/cloud_functions/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.7 + + - Update a dependency to the latest release. + ## 6.0.6 - **FIX**(cloud_functions): enhance stream response types for better type safety ([#17938](https://github.com/firebase/flutterfire/issues/17938)). ([b89e5890](https://github.com/firebase/flutterfire/commit/b89e5890dfe7ce725022c9e470ee34ff64eb7a99)) diff --git a/packages/cloud_functions/cloud_functions/android/build.gradle b/packages/cloud_functions/cloud_functions/android/build.gradle index 763d75805fda..9d1295a9567c 100644 --- a/packages/cloud_functions/cloud_functions/android/build.gradle +++ b/packages/cloud_functions/cloud_functions/android/build.gradle @@ -3,7 +3,12 @@ version '1.0-SNAPSHOT' apply plugin: 'com.android.library' apply from: file("local-config.gradle") -apply plugin: 'kotlin-android' + +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly. +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +if (agpMajor < 9) { + apply plugin: 'kotlin-android' +} buildscript { ext.kotlin_version = "1.8.22" @@ -56,6 +61,12 @@ android { targetCompatibility project.ext.javaVersion } + if (agpMajor < 9) { + kotlinOptions { + jvmTarget = project.ext.javaVersion + } + } + sourceSets { main.java.srcDirs += "src/main/kotlin" test.java.srcDirs += "src/test/kotlin" @@ -77,11 +88,6 @@ android { implementation 'org.reactivestreams:reactive-streams:1.0.4' } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17 - } - } -apply from: file("./user-agent.gradle") -apply plugin: 'org.jetbrains.kotlin.android' \ No newline at end of file +apply from: file("./user-agent.gradle") \ No newline at end of file diff --git a/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.h b/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.h +++ b/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.m b/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.m index 59a72e90be12..9c45e766f906 100644 --- a/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.m +++ b/packages/cloud_functions/cloud_functions/example/ios/Runner/AppDelegate.m @@ -5,9 +5,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/cloud_functions/cloud_functions/example/ios/Runner/Info.plist b/packages/cloud_functions/cloud_functions/example/ios/Runner/Info.plist index 73f87a4eb1fa..319a574f5ffe 100644 --- a/packages/cloud_functions/cloud_functions/example/ios/Runner/Info.plist +++ b/packages/cloud_functions/cloud_functions/example/ios/Runner/Info.plist @@ -50,5 +50,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/cloud_functions/cloud_functions/example/pubspec.yaml b/packages/cloud_functions/cloud_functions/example/pubspec.yaml index 7f8ba3d5313e..56991739d9f4 100644 --- a/packages/cloud_functions/cloud_functions/example/pubspec.yaml +++ b/packages/cloud_functions/cloud_functions/example/pubspec.yaml @@ -6,8 +6,8 @@ environment: flutter: '>=3.3.0' dependencies: - cloud_functions: ^6.0.6 - firebase_core: ^4.4.0 + cloud_functions: ^6.0.7 + firebase_core: ^4.5.0 flutter: sdk: flutter diff --git a/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/Constants.swift b/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/Constants.swift index 7fc9b631b2a4..e39f14a01c32 100644 --- a/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/Constants.swift +++ b/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/Constants.swift @@ -3,4 +3,4 @@ // found in the LICENSE file. /// Auto-generated file. Do not edit. -public let versionNumber = "6.0.6" +public let versionNumber = "6.0.7" diff --git a/packages/cloud_functions/cloud_functions/ios/generated_firebase_sdk_version.txt b/packages/cloud_functions/cloud_functions/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/cloud_functions/cloud_functions/ios/generated_firebase_sdk_version.txt +++ b/packages/cloud_functions/cloud_functions/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/cloud_functions/cloud_functions/lib/src/https_callable.dart b/packages/cloud_functions/cloud_functions/lib/src/https_callable.dart index 264e6b93e7bb..cac96c0c3374 100644 --- a/packages/cloud_functions/cloud_functions/lib/src/https_callable.dart +++ b/packages/cloud_functions/cloud_functions/lib/src/https_callable.dart @@ -79,7 +79,8 @@ class HttpsCallable { dynamic _updateRawDataToList(dynamic value) { if (value is Uint8List || value is Int32List || - value is Int64List || + // Int64List is not supported by dart2js, skip the check on web. + (!kIsWeb && value is Int64List) || value is Float32List || value is Float64List) { return value.toList(); diff --git a/packages/cloud_functions/cloud_functions/pubspec.yaml b/packages/cloud_functions/cloud_functions/pubspec.yaml index 937a884d77e7..1fd4bfc19b24 100644 --- a/packages/cloud_functions/cloud_functions/pubspec.yaml +++ b/packages/cloud_functions/cloud_functions/pubspec.yaml @@ -1,6 +1,6 @@ name: cloud_functions description: A Flutter plugin allowing you to use Firebase Cloud Functions. -version: 6.0.6 +version: 6.0.7 homepage: https://firebase.google.com/docs/functions repository: https://github.com/firebase/flutterfire/tree/main/packages/cloud_functions/cloud_functions topics: @@ -17,9 +17,9 @@ environment: flutter: '>=3.3.0' dependencies: - cloud_functions_platform_interface: ^5.8.9 - cloud_functions_web: ^5.1.2 - firebase_core: ^4.4.0 + cloud_functions_platform_interface: ^5.8.10 + cloud_functions_web: ^5.1.3 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/cloud_functions/cloud_functions/test/https_callable_test.dart b/packages/cloud_functions/cloud_functions/test/https_callable_test.dart index 87a21f8a61c6..8a9224ad470a 100644 --- a/packages/cloud_functions/cloud_functions/test/https_callable_test.dart +++ b/packages/cloud_functions/cloud_functions/test/https_callable_test.dart @@ -3,6 +3,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; + import 'package:cloud_functions/cloud_functions.dart'; import 'package:cloud_functions_platform_interface/cloud_functions_platform_interface.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -95,6 +97,45 @@ void main() { ); }); + test('converts typed data lists in map values to regular lists', + () async { + final result = await httpsCallable!.call({ + 'bytes': Uint8List.fromList([1, 2, 3]), + 'ints': Int32List.fromList([4, 5, 6]), + 'floats': Float32List.fromList([1.0, 2.0]), + 'doubles': Float64List.fromList([3.0, 4.0]), + }); + final data = result.data as Map; + expect(data['bytes'], isA>()); + expect(data['bytes'], isNot(isA())); + expect(data['bytes'], equals([1, 2, 3])); + expect(data['ints'], isA>()); + expect(data['ints'], isNot(isA())); + expect(data['floats'], isA>()); + expect(data['floats'], isNot(isA())); + expect(data['doubles'], isA>()); + expect(data['doubles'], isNot(isA())); + }); + + test('converts typed data lists passed as direct parameters', () async { + final result = await httpsCallable!.call(Uint8List.fromList([7, 8, 9])); + expect(result.data, isA()); + expect(result.data, isNot(isA())); + expect(result.data, equals([7, 8, 9])); + }); + + test('converts typed data lists inside list parameters', () async { + final result = await httpsCallable!.call([ + Uint8List.fromList([1, 2]), + Int32List.fromList([3, 4]), + ]); + final data = result.data as List; + expect(data[0], isA>()); + expect(data[0], isNot(isA())); + expect(data[1], isA>()); + expect(data[1], isNot(isA())); + }); + test('parameter validation throws if any other type of data is passed', () async { expect(() { diff --git a/packages/cloud_functions/cloud_functions_platform_interface/CHANGELOG.md b/packages/cloud_functions/cloud_functions_platform_interface/CHANGELOG.md index 7fc2450de856..1ef9ffbdce83 100644 --- a/packages/cloud_functions/cloud_functions_platform_interface/CHANGELOG.md +++ b/packages/cloud_functions/cloud_functions_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.8.10 + + - Update a dependency to the latest release. + ## 5.8.9 - Update a dependency to the latest release. diff --git a/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart b/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart index e74e76988714..d17ea0bb612b 100644 --- a/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart +++ b/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart @@ -16,16 +16,12 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform { /// Creates a new [MethodChannelHttpsCallable] instance. MethodChannelHttpsCallable(FirebaseFunctionsPlatform functions, String? origin, String? name, HttpsCallableOptions options, Uri? uri) - : _transformedUri = uri?.pathSegments.join('_').replaceAll('.', '_'), - super(functions, origin, name, options, uri) { - _eventChannelId = name ?? _transformedUri ?? ''; - _channel = - EventChannel('plugins.flutter.io/firebase_functions/$_eventChannelId'); - } + : _baseEventChannelId = + name ?? uri?.pathSegments.join('_').replaceAll('.', '_') ?? '', + super(functions, origin, name, options, uri); - late final EventChannel _channel; - final String? _transformedUri; - late String _eventChannelId; + static int _streamIdCounter = 0; + final String _baseEventChannelId; @override Future call([Object? parameters]) async { @@ -54,10 +50,15 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform { @override Stream stream(Object? parameters) async* { + // Each stream() call gets a unique channel ID to prevent collisions + // when invoking the same function concurrently. See #18036. + final eventChannelId = '${_baseEventChannelId}_${_streamIdCounter++}'; + final channel = + EventChannel('plugins.flutter.io/firebase_functions/$eventChannelId'); try { await MethodChannelFirebaseFunctions.pigeonChannel .registerEventChannel({ - 'eventChannelId': _eventChannelId, + 'eventChannelId': eventChannelId, 'appName': functions.app!.name, 'region': functions.region, }); @@ -69,7 +70,7 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform { 'limitedUseAppCheckToken': options.limitedUseAppCheckToken, 'timeout': options.timeout.inMilliseconds, }; - yield* _channel.receiveBroadcastStream(eventData).map((message) { + yield* channel.receiveBroadcastStream(eventData).map((message) { if (message is Map) { return Map.from(message); } diff --git a/packages/cloud_functions/cloud_functions_platform_interface/pubspec.yaml b/packages/cloud_functions/cloud_functions_platform_interface/pubspec.yaml index e5b7e0e91ee6..5075b75e3d0e 100644 --- a/packages/cloud_functions/cloud_functions_platform_interface/pubspec.yaml +++ b/packages/cloud_functions/cloud_functions_platform_interface/pubspec.yaml @@ -5,14 +5,14 @@ repository: https://github.com/firebase/flutterfire/tree/main/packages/cloud_fun # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 5.8.9 +version: 5.8.10 environment: sdk: '>=3.2.0 <4.0.0' flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/cloud_functions/cloud_functions_web/CHANGELOG.md b/packages/cloud_functions/cloud_functions_web/CHANGELOG.md index 8fa9f0090b77..bd0a93ec763a 100644 --- a/packages/cloud_functions/cloud_functions_web/CHANGELOG.md +++ b/packages/cloud_functions/cloud_functions_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.1.3 + + - Update a dependency to the latest release. + ## 5.1.2 - Update a dependency to the latest release. diff --git a/packages/cloud_functions/cloud_functions_web/lib/src/cloud_functions_version.dart b/packages/cloud_functions/cloud_functions_web/lib/src/cloud_functions_version.dart index f3f8436dafd7..a698ecff621f 100644 --- a/packages/cloud_functions/cloud_functions_web/lib/src/cloud_functions_version.dart +++ b/packages/cloud_functions/cloud_functions_web/lib/src/cloud_functions_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '6.0.6'; +const packageVersion = '6.0.7'; diff --git a/packages/cloud_functions/cloud_functions_web/pubspec.yaml b/packages/cloud_functions/cloud_functions_web/pubspec.yaml index 97b4ffcf8da4..7dd3ecd561fa 100644 --- a/packages/cloud_functions/cloud_functions_web/pubspec.yaml +++ b/packages/cloud_functions/cloud_functions_web/pubspec.yaml @@ -3,16 +3,16 @@ description: The web implementation of cloud_functions homepage: https://github.com/firebase/flutterfire/tree/main/packages/cloud_functions/cloud_functions_web repository: https://github.com/firebase/flutterfire/tree/main/packages/cloud_functions/cloud_functions_web -version: 5.1.2 +version: 5.1.3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - cloud_functions_platform_interface: ^5.8.9 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 + cloud_functions_platform_interface: ^5.8.10 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_ai/firebase_ai/CHANGELOG.md b/packages/firebase_ai/firebase_ai/CHANGELOG.md index 953d937fcd92..71aa4901e610 100644 --- a/packages/firebase_ai/firebase_ai/CHANGELOG.md +++ b/packages/firebase_ai/firebase_ai/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.9.0 + + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + - **FEAT**(firebaseai): update Live API sample to add video support. ([#18018](https://github.com/firebase/flutterfire/issues/18018)). ([f91df750](https://github.com/firebase/flutterfire/commit/f91df7503bc4506c66cbebcfa562d65de1ae0e5b)) + ## 3.8.0 - **FIX**(firebase_ai): Rename `groundingSupport` to `groundingSupports` ([#17961](https://github.com/firebase/flutterfire/issues/17961)). ([cfb90989](https://github.com/firebase/flutterfire/commit/cfb909896d8ae9edc49b10f1def5b64dcc3dfb35)) diff --git a/packages/firebase_ai/firebase_ai/example/ios/Runner/Info.plist b/packages/firebase_ai/firebase_ai/example/ios/Runner/Info.plist index babcfb712863..32b85a81f400 100644 --- a/packages/firebase_ai/firebase_ai/example/ios/Runner/Info.plist +++ b/packages/firebase_ai/firebase_ai/example/ios/Runner/Info.plist @@ -49,5 +49,26 @@ We need camera access to take pictures and record video. NSMicrophoneUsageDescription We need access to the microphone to record audio. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_ai/firebase_ai/example/pubspec.yaml b/packages/firebase_ai/firebase_ai/example/pubspec.yaml index e65649e5db79..6150596fcb89 100644 --- a/packages/firebase_ai/firebase_ai/example/pubspec.yaml +++ b/packages/firebase_ai/firebase_ai/example/pubspec.yaml @@ -22,9 +22,9 @@ dependencies: camera: ^0.11.2+1 camera_macos: ^0.0.9 cupertino_icons: ^1.0.6 - firebase_ai: ^3.8.0 - firebase_core: ^4.4.0 - firebase_storage: ^13.0.6 + firebase_ai: ^3.9.0 + firebase_core: ^4.5.0 + firebase_storage: ^13.1.0 flutter: sdk: flutter flutter_animate: ^4.5.2 diff --git a/packages/firebase_ai/firebase_ai/lib/src/firebaseai_version.dart b/packages/firebase_ai/firebase_ai/lib/src/firebaseai_version.dart index 800106e2d40f..1fa94cbdf38b 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/firebaseai_version.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/firebaseai_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '0.2.2+2'; +const packageVersion = '0.2.3'; diff --git a/packages/firebase_ai/firebase_ai/pubspec.yaml b/packages/firebase_ai/firebase_ai/pubspec.yaml index 13145cb7ec2d..e545f84b43b2 100644 --- a/packages/firebase_ai/firebase_ai/pubspec.yaml +++ b/packages/firebase_ai/firebase_ai/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_ai description: Firebase AI Logic SDK. -version: 3.8.0 +version: 3.9.0 homepage: https://firebase.google.com/docs/vertex-ai/get-started?platform=flutter topics: - firebase @@ -20,9 +20,9 @@ environment: flutter: ">=3.16.0" dependencies: - firebase_app_check: ^0.4.1+4 - firebase_auth: ^6.1.4 - firebase_core: ^4.4.0 + firebase_app_check: ^0.4.1+5 + firebase_auth: ^6.2.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/firebase_analytics/firebase_analytics/CHANGELOG.md b/packages/firebase_analytics/firebase_analytics/CHANGELOG.md index 70938372ff1c..721bd08cfafe 100644 --- a/packages/firebase_analytics/firebase_analytics/CHANGELOG.md +++ b/packages/firebase_analytics/firebase_analytics/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.1.3 + + - **FIX**(analytics,iOS): Update hashedPhoneNumber handling to use hex string conversion ([#17807](https://github.com/firebase/flutterfire/issues/17807)). ([407c2490](https://github.com/firebase/flutterfire/commit/407c2490602484499d1ab5b2ce6860af00a218c8)) + ## 12.1.2 - **FIX**(firebase_analytics): update logInAppPurchase documentation to specify iOS support only ([#17968](https://github.com/firebase/flutterfire/issues/17968)). ([b3caa545](https://github.com/firebase/flutterfire/commit/b3caa54592d431a1ac1b7007a154cdf739b0e406)) diff --git a/packages/firebase_analytics/firebase_analytics/android/build.gradle b/packages/firebase_analytics/firebase_analytics/android/build.gradle index 41533c10483d..183ea4605c04 100755 --- a/packages/firebase_analytics/firebase_analytics/android/build.gradle +++ b/packages/firebase_analytics/firebase_analytics/android/build.gradle @@ -25,7 +25,12 @@ rootProject.allprojects { } apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' + +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly. +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +if (agpMajor < 9) { + apply plugin: 'kotlin-android' +} def firebaseCoreProject = findProject(':firebase_core') if (firebaseCoreProject == null) { @@ -53,8 +58,10 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17 + if (agpMajor < 9) { + kotlinOptions { + jvmTarget = project.ext.javaVersion + } } compileOptions { diff --git a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt index aca07f356d51..3c366eebf7e4 100644 --- a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt +++ b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/FlutterFirebaseAnalyticsPlugin.kt @@ -443,4 +443,16 @@ class FlutterFirebaseAnalyticsPlugin : FlutterFirebasePlugin, ) ) } + + override fun logTransaction(transactionId: String, callback: (Result) -> Unit) { + callback( + Result.failure( + FlutterError( + "unimplemented", + "logTransaction is only available on iOS.", + null + ) + ) + ) + } } diff --git a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt index 0a8351824439..fdfc88dc8f42 100644 --- a/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt +++ b/packages/firebase_analytics/firebase_analytics/android/src/main/kotlin/io/flutter/plugins/firebase/analytics/GeneratedAndroidFirebaseAnalytics.g.kt @@ -147,6 +147,7 @@ interface FirebaseAnalyticsHostApi { fun getAppInstanceId(callback: (Result) -> Unit) fun getSessionId(callback: (Result) -> Unit) fun initiateOnDeviceConversionMeasurement(arguments: Map, callback: (Result) -> Unit) + fun logTransaction(transactionId: String, callback: (Result) -> Unit) companion object { /** The codec used by FirebaseAnalyticsHostApi. */ @@ -363,6 +364,25 @@ interface FirebaseAnalyticsHostApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val transactionIdArg = args[0] as String + api.logTransaction(transactionIdArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeneratedAndroidFirebaseAnalyticsPigeonUtils.wrapError(error)) + } else { + reply.reply(GeneratedAndroidFirebaseAnalyticsPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.h b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.h index d9e18e990f2e..6020ddf58053 100644 --- a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.h @@ -5,6 +5,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.m b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.m index a4b51c88eb60..b915a48d031c 100644 --- a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/AppDelegate.m @@ -9,8 +9,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist index 76eb7953efc4..248bf3883db1 100755 --- a/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist +++ b/packages/firebase_analytics/firebase_analytics/example/ios/Runner/Info.plist @@ -49,5 +49,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_analytics/firebase_analytics/example/lib/main.dart b/packages/firebase_analytics/firebase_analytics/example/lib/main.dart index 2b885ed94b1f..37c7dd8eec76 100755 --- a/packages/firebase_analytics/firebase_analytics/example/lib/main.dart +++ b/packages/firebase_analytics/firebase_analytics/example/lib/main.dart @@ -8,6 +8,7 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; import 'firebase_options.dart'; import 'tabs_page.dart'; @@ -62,6 +63,44 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String _message = ''; + StreamSubscription>? _purchaseSubscription; + + static const String _testProductId = '123456'; + + @override + void initState() { + super.initState(); + _purchaseSubscription = + InAppPurchase.instance.purchaseStream.listen(_onPurchaseUpdate); + } + + @override + void dispose() { + _purchaseSubscription?.cancel(); + super.dispose(); + } + + void _onPurchaseUpdate(List purchases) { + for (final purchase in purchases) { + if (purchase.pendingCompletePurchase) { + InAppPurchase.instance.completePurchase(purchase); + } + if (purchase.status == PurchaseStatus.purchased || + purchase.status == PurchaseStatus.restored) { + final transactionId = purchase.purchaseID; + print('transactionId: $transactionId'); + if (transactionId != null) { + widget.analytics.logTransaction(transactionId).then((_) { + setMessage('logTransaction succeeded with ID: $transactionId'); + }).catchError((e) { + setMessage('logTransaction failed: $e'); + }); + } + } else if (purchase.status == PurchaseStatus.error) { + setMessage('Purchase error: ${purchase.error?.message}'); + } + } + } void setMessage(String message) { setState(() { @@ -158,6 +197,40 @@ class _MyHomePageState extends State { setMessage('initiateOnDeviceConversionMeasurement succeeded'); } + Future _testLogTransaction() async { + if (kIsWeb || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS)) { + setMessage('logTransaction() is only supported on iOS and macOS'); + return; + } + + setMessage('Loading product $_testProductId...'); + + final response = + await InAppPurchase.instance.queryProductDetails({_testProductId}); + + if (response.error != null) { + setMessage('Failed to load product: ${response.error!.message}'); + return; + } + + if (response.productDetails.isEmpty) { + setMessage( + 'Product "$_testProductId" not found. ' + 'Make sure your StoreKit config file is set up correctly.', + ); + return; + } + + final product = response.productDetails.first; + setMessage('Initiating purchase for "${product.id}"...'); + + await InAppPurchase.instance.buyNonConsumable( + purchaseParam: PurchaseParam(productDetails: product), + ); + } + AnalyticsEventItem itemCreator() { return AnalyticsEventItem( affiliation: 'affil', @@ -365,6 +438,13 @@ class _MyHomePageState extends State { onPressed: _testInitiateOnDeviceConversionMeasurement, child: const Text('Test initiateOnDeviceConversionMeasurement'), ), + if (!kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS)) + MaterialButton( + onPressed: _testLogTransaction, + child: const Text('Test logTransaction (product: 123456)'), + ), Text( _message, style: const TextStyle(color: Color.fromARGB(255, 0, 155, 0)), diff --git a/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml b/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml index 85d39997dda7..d94b9a167ced 100755 --- a/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml +++ b/packages/firebase_analytics/firebase_analytics/example/pubspec.yaml @@ -6,10 +6,11 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_analytics: ^12.1.2 - firebase_core: ^4.4.0 + firebase_analytics: ^12.1.3 + firebase_core: ^4.5.0 flutter: sdk: flutter + in_app_purchase: ^3.2.3 flutter: uses-material-design: true diff --git a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Package.swift b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Package.swift index 0e717ea4144b..6483af79396a 100644 --- a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Package.swift +++ b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Package.swift @@ -79,6 +79,11 @@ guard let shared_spm_version = Version("\(firebase_core_version_string)\(shared_ fatalError("Invalid firebase_core version: \(firebase_core_version_string)\(shared_spm_tag)") } +// Set FIREBASE_ANALYTICS_WITHOUT_ADID=true to use FirebaseAnalyticsWithoutAdIdSupport +// e.g. FIREBASE_ANALYTICS_WITHOUT_ADID=true flutter build ios +let useWithoutAdId = ProcessInfo.processInfo.environment["FIREBASE_ANALYTICS_WITHOUT_ADID"] != nil +let analyticsProduct = useWithoutAdId ? "FirebaseAnalyticsWithoutAdIdSupport" : "FirebaseAnalytics" + let package = Package( name: "firebase_analytics", platforms: [ @@ -95,7 +100,7 @@ let package = Package( .target( name: "firebase_analytics", dependencies: [ - .product(name: "FirebaseAnalytics", package: "firebase-ios-sdk"), + .product(name: analyticsProduct, package: "firebase-ios-sdk"), // Wrapper dependency .product(name: "firebase-core-shared", package: "flutterfire"), ], diff --git a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift index f8124ea1dc43..d8b175ba6e20 100644 --- a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift +++ b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsMessages.g.swift @@ -220,6 +220,7 @@ protocol FirebaseAnalyticsHostApi { func getSessionId(completion: @escaping (Result) -> Void) func initiateOnDeviceConversionMeasurement(arguments: [String: String?], completion: @escaping (Result) -> Void) + func logTransaction(transactionId: String, completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -459,5 +460,26 @@ class FirebaseAnalyticsHostApiSetup { } else { initiateOnDeviceConversionMeasurementChannel.setMessageHandler(nil) } + let logTransactionChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec + ) + if let api { + logTransactionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let transactionIdArg = args[0] as! String + api.logTransaction(transactionId: transactionIdArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case let .failure(error): + reply(wrapError(error)) + } + } + } + } else { + logTransactionChannel.setMessageHandler(nil) + } } } diff --git a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift index fcd6bda21819..41651b2ace45 100644 --- a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift +++ b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/Sources/firebase_analytics/FirebaseAnalyticsPlugin.swift @@ -14,6 +14,7 @@ import firebase_core_shared #endif import FirebaseAnalytics +import StoreKit let kFLTFirebaseAnalyticsName = "name" let kFLTFirebaseAnalyticsValue = "value" @@ -28,6 +29,8 @@ let kFLTFirebaseAnalyticsUserId = "userId" let FLTFirebaseAnalyticsChannelName = "plugins.flutter.io/firebase_analytics" +extension FlutterError: Error {} + public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, FlutterPlugin, FirebaseAnalyticsHostApi { public static func register(with registrar: any FlutterPluginRegistrar) { @@ -132,16 +135,108 @@ public class FirebaseAnalyticsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt Analytics.initiateOnDeviceConversionMeasurement(phoneNumber: phoneNumber) } if let hashedEmailAddress = arguments["hashedEmailAddress"] as? String, - let data = hashedEmailAddress.data(using: .utf8) { + let data = hexStringToData(hashedEmailAddress) { Analytics.initiateOnDeviceConversionMeasurement(hashedEmailAddress: data) } if let hashedPhoneNumber = arguments["hashedPhoneNumber"] as? String, - let data = hashedPhoneNumber.data(using: .utf8) { + let data = hexStringToData(hashedPhoneNumber) { Analytics.initiateOnDeviceConversionMeasurement(hashedPhoneNumber: data) } completion(.success(())) } + func logTransaction(transactionId: String, + completion: @escaping (Result) -> Void) { + #if os(macOS) + if #available(macOS 12.0, *) { + logTransactionWithStoreKit(transactionId: transactionId, completion: completion) + } else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "logTransaction() is only supported on macOS 12.0 or newer", + details: nil + ))) + } + #else + if #available(iOS 15.0, *) { + logTransactionWithStoreKit(transactionId: transactionId, completion: completion) + } else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "logTransaction() is only supported on iOS 15.0 or newer", + details: nil + ))) + } + #endif + } + + #if os(macOS) + @available(macOS 12.0, *) + #else + @available(iOS 15.0, *) + #endif + private func logTransactionWithStoreKit(transactionId: String, + completion: @escaping (Result) -> Void) { + Task { + do { + guard let id = UInt64(transactionId) else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "Invalid transactionId", + details: nil + ))) + return + } + + var foundTransaction: Transaction? + for await result in Transaction.all { + switch result { + case let .verified(transaction): + if transaction.id == id { + foundTransaction = transaction + break + } + case .unverified: + continue + } + } + + guard let transaction = foundTransaction else { + completion(.failure(FlutterError( + code: "firebase_analytics", + message: "Transaction not found", + details: nil + ))) + return + } + + Analytics.logTransaction(transaction) + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + } + + private func hexStringToData(_ hexString: String) -> Data? { + let length = hexString.count + guard length % 2 == 0 else { return nil } + + var data = Data(capacity: length / 2) + var index = hexString.startIndex + + for _ in 0 ..< (length / 2) { + let nextIndex = hexString.index(index, offsetBy: 2) + guard let byte = UInt8(hexString[index ..< nextIndex], radix: 16) else { + return nil + } + data.append(byte) + index = nextIndex + } + + return data + } + public func didReinitializeFirebaseCore(_ completion: @escaping () -> Void) { completion() } diff --git a/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/analytics_storekit_config.storekit b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/analytics_storekit_config.storekit new file mode 100644 index 000000000000..092f5c7b86c4 --- /dev/null +++ b/packages/firebase_analytics/firebase_analytics/ios/firebase_analytics/analytics_storekit_config.storekit @@ -0,0 +1,54 @@ +{ + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, + "identifier" : "D50F15B4", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "FAAD0643", + "localizations" : [ + { + "description" : "", + "displayName" : "", + "locale" : "en_US" + } + ], + "productID" : "123456", + "referenceName" : "premium_upgrade", + "type" : "NonConsumable" + } + ], + "settings" : { + "_askToBuyEnabled" : false, + "_billingGracePeriodEnabled" : false, + "_billingIssuesEnabled" : false, + "_disableDialogs" : false, + "_failTransactionsEnabled" : false, + "_locale" : "en_US", + "_renewalBillingIssuesEnabled" : false, + "_storefront" : "USA", + "_storeKitErrors" : [ + + ], + "_timeRate" : 0 + }, + "subscriptionGroups" : [ + + ], + "version" : { + "major" : 4, + "minor" : 0 + } +} diff --git a/packages/firebase_analytics/firebase_analytics/ios/generated_firebase_sdk_version.txt b/packages/firebase_analytics/firebase_analytics/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_analytics/firebase_analytics/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_analytics/firebase_analytics/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart index 237a972b19c7..38c5a9077977 100755 --- a/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics/lib/src/firebase_analytics.dart @@ -1242,6 +1242,23 @@ class FirebaseAnalytics extends FirebasePluginPlatform { ); } + /// Logs verified in-app purchase events in Google Analytics for Firebase + /// after a purchase is successful. + /// + /// Only available on iOS. + /// + /// You can obtain the [transactionId] from the + /// [in_app_purchase](https://pub.dev/packages/in_app_purchase) package. + Future logTransaction(String transactionId) async { + if (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS) { + throw UnimplementedError( + 'logTransaction() is only supported on iOS and macOS.', + ); + } + return _delegate.logTransaction(transactionId: transactionId); + } + /// Sets the duration of inactivity that terminates the current session. /// /// The default value is 1800000 milliseconds (30 minutes). diff --git a/packages/firebase_analytics/firebase_analytics/pubspec.yaml b/packages/firebase_analytics/firebase_analytics/pubspec.yaml index fba973a0cd69..ef1dbd2277d1 100755 --- a/packages/firebase_analytics/firebase_analytics/pubspec.yaml +++ b/packages/firebase_analytics/firebase_analytics/pubspec.yaml @@ -4,7 +4,7 @@ description: solution that provides insight on app usage and user engagement on Android and iOS. homepage: https://firebase.google.com/docs/analytics repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_analytics/firebase_analytics -version: 12.1.2 +version: 12.1.3 topics: - firebase - analytics @@ -19,9 +19,9 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_analytics_platform_interface: ^5.0.6 - firebase_analytics_web: ^0.6.1+2 - firebase_core: ^4.4.0 + firebase_analytics_platform_interface: ^5.0.7 + firebase_analytics_web: ^0.6.1+3 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp b/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp index 2402f854a82a..14a92edf1452 100644 --- a/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp +++ b/packages/firebase_analytics/firebase_analytics/windows/messages.g.cpp @@ -525,6 +525,45 @@ void FirebaseAnalyticsHostApi::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_analytics_platform_interface." + "FirebaseAnalyticsHostApi.logTransaction" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_transaction_id_arg = args.at(0); + if (encodable_transaction_id_arg.IsNull()) { + reply(WrapError("transaction_id_arg unexpectedly null.")); + return; + } + const auto& transaction_id_arg = + std::get(encodable_transaction_id_arg); + api->LogTransaction( + transaction_id_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } } EncodableValue FirebaseAnalyticsHostApi::WrapError( diff --git a/packages/firebase_analytics/firebase_analytics/windows/messages.g.h b/packages/firebase_analytics/firebase_analytics/windows/messages.g.h index 69c859c6e515..b755d9ef6045 100644 --- a/packages/firebase_analytics/firebase_analytics/windows/messages.g.h +++ b/packages/firebase_analytics/firebase_analytics/windows/messages.g.h @@ -138,6 +138,9 @@ class FirebaseAnalyticsHostApi { virtual void InitiateOnDeviceConversionMeasurement( const flutter::EncodableMap& arguments, std::function reply)> result) = 0; + virtual void LogTransaction( + const std::string& transaction_id, + std::function reply)> result) = 0; // The codec used by FirebaseAnalyticsHostApi. static const flutter::StandardMessageCodec& GetCodec(); diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/CHANGELOG.md b/packages/firebase_analytics/firebase_analytics_platform_interface/CHANGELOG.md index 1942237f8d1a..ad86b4e3fa8c 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/CHANGELOG.md +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.7 + + - Update a dependency to the latest release. + ## 5.0.6 - Update a dependency to the latest release. diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart index 0ad8119fe21c..e3c5d41eaa42 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/method_channel/method_channel_firebase_analytics.dart @@ -195,4 +195,15 @@ class MethodChannelFirebaseAnalytics extends FirebaseAnalyticsPlatform { convertPlatformException(e, s); } } + + @override + Future logTransaction({ + required String transactionId, + }) { + try { + return _api.logTransaction(transactionId); + } catch (e, s) { + convertPlatformException(e, s); + } + } } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart index 6b74cc466a3d..003dd773639e 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -416,4 +416,30 @@ class FirebaseAnalyticsHostApi { return; } } + + Future logTransaction(String transactionId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([transactionId]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart index 8fbe90066d5b..f69c816e707d 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/lib/src/platform_interface/platform_interface_firebase_analytics.dart @@ -209,4 +209,10 @@ abstract class FirebaseAnalyticsPlatform extends PlatformInterface { 'initiateOnDeviceConversionMeasurement() is not implemented', ); } + + Future logTransaction({ + required String transactionId, + }) { + throw UnimplementedError('logTransaction() is not implemented'); + } } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart index 4c1ada746e85..b7008b4d3871 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/pigeons/messages.dart @@ -66,4 +66,7 @@ abstract class FirebaseAnalyticsHostApi { @async void initiateOnDeviceConversionMeasurement(Map arguments); + + @async + void logTransaction(String transactionId); } diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/pubspec.yaml b/packages/firebase_analytics/firebase_analytics_platform_interface/pubspec.yaml index 12f2a2117f5c..24fd16722d61 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/pubspec.yaml +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/pubspec.yaml @@ -2,15 +2,15 @@ name: firebase_analytics_platform_interface description: A common platform interface for the firebase_analytics plugin. homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_analytics/firebase_analytics_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_analytics/firebase_analytics_platform_interface -version: 5.0.6 +version: 5.0.7 environment: sdk: '>=3.2.0 <4.0.0' flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart b/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart index cbef0d67a9eb..591cce9f19e2 100644 --- a/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart +++ b/packages/firebase_analytics/firebase_analytics_platform_interface/test/pigeon/test_api.dart @@ -67,6 +67,8 @@ abstract class TestFirebaseAnalyticsHostApi { Future initiateOnDeviceConversionMeasurement( Map arguments); + Future logTransaction(String transactionId); + static void setUp( TestFirebaseAnalyticsHostApi? api, { BinaryMessenger? binaryMessenger, @@ -409,5 +411,37 @@ abstract class TestFirebaseAnalyticsHostApi { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction was null.'); + final List args = (message as List?)!; + final String? arg_transactionId = (args[0] as String?); + assert(arg_transactionId != null, + 'Argument for dev.flutter.pigeon.firebase_analytics_platform_interface.FirebaseAnalyticsHostApi.logTransaction was null, expected non-null String.'); + try { + await api.logTransaction(arg_transactionId!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } diff --git a/packages/firebase_analytics/firebase_analytics_web/CHANGELOG.md b/packages/firebase_analytics/firebase_analytics_web/CHANGELOG.md index 6e835a6c0cbc..ee6be702e5f0 100644 --- a/packages/firebase_analytics/firebase_analytics_web/CHANGELOG.md +++ b/packages/firebase_analytics/firebase_analytics_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.1+3 + + - Update a dependency to the latest release. + ## 0.6.1+2 - Update a dependency to the latest release. diff --git a/packages/firebase_analytics/firebase_analytics_web/lib/src/firebase_analytics_version.dart b/packages/firebase_analytics/firebase_analytics_web/lib/src/firebase_analytics_version.dart index 51e1d43bf8bd..6162ddc5882f 100644 --- a/packages/firebase_analytics/firebase_analytics_web/lib/src/firebase_analytics_version.dart +++ b/packages/firebase_analytics/firebase_analytics_web/lib/src/firebase_analytics_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '12.1.2'; +const packageVersion = '12.1.3'; diff --git a/packages/firebase_analytics/firebase_analytics_web/pubspec.yaml b/packages/firebase_analytics/firebase_analytics_web/pubspec.yaml index 75d2bff5206f..8e04d196c61f 100644 --- a/packages/firebase_analytics/firebase_analytics_web/pubspec.yaml +++ b/packages/firebase_analytics/firebase_analytics_web/pubspec.yaml @@ -2,17 +2,17 @@ name: firebase_analytics_web description: The web implementation of firebase_analytics homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_analytics/firebase_analytics_web repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_analytics/firebase_analytics_web -version: 0.6.1+2 +version: 0.6.1+3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_analytics_platform_interface: ^5.0.6 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 + _flutterfire_internals: ^1.3.67 + firebase_analytics_platform_interface: ^5.0.7 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_app_check/firebase_app_check/CHANGELOG.md b/packages/firebase_app_check/firebase_app_check/CHANGELOG.md index 21f0c8c9bbc6..97de1656084c 100644 --- a/packages/firebase_app_check/firebase_app_check/CHANGELOG.md +++ b/packages/firebase_app_check/firebase_app_check/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.1+5 + + - Update a dependency to the latest release. + ## 0.4.1+4 - Update a dependency to the latest release. diff --git a/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.h b/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.m b/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.m index b6e4f92f4e00..7171162c055c 100644 --- a/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_app_check/firebase_app_check/example/ios/Runner/AppDelegate.m @@ -6,9 +6,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_app_check/firebase_app_check/example/ios/Runner/Info.plist b/packages/firebase_app_check/firebase_app_check/example/ios/Runner/Info.plist index 4c88b5e5d4c8..3a8a7a2f7159 100644 --- a/packages/firebase_app_check/firebase_app_check/example/ios/Runner/Info.plist +++ b/packages/firebase_app_check/firebase_app_check/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_app_check/firebase_app_check/example/lib/main.dart b/packages/firebase_app_check/firebase_app_check/example/lib/main.dart index 1fbb5c9b7745..7a1e778d35a0 100644 --- a/packages/firebase_app_check/firebase_app_check/example/lib/main.dart +++ b/packages/firebase_app_check/firebase_app_check/example/lib/main.dart @@ -23,7 +23,9 @@ Future main() async { await FirebaseAppCheck.instance // Your personal reCaptcha public key goes here: .activate( - providerWeb: ReCaptchaV3Provider(kWebRecaptchaSiteKey), + providerWeb: kDebugMode + ? WebDebugProvider() + : ReCaptchaV3Provider(kWebRecaptchaSiteKey), providerAndroid: const AndroidDebugProvider(), providerApple: const AppleDebugProvider(), ); diff --git a/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml b/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml index d320cc700dc5..6362bc129a1c 100644 --- a/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml +++ b/packages/firebase_app_check/firebase_app_check/example/pubspec.yaml @@ -9,9 +9,9 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - cloud_firestore: ^6.1.2 - firebase_app_check: ^0.4.1+4 - firebase_core: ^4.4.0 + cloud_firestore: ^6.1.3 + firebase_app_check: ^0.4.1+5 + firebase_core: ^4.5.0 flutter: sdk: flutter diff --git a/packages/firebase_app_check/firebase_app_check/ios/generated_firebase_sdk_version.txt b/packages/firebase_app_check/firebase_app_check/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_app_check/firebase_app_check/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_app_check/firebase_app_check/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart index df0e9641fe25..c5f5ae1ad633 100644 --- a/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart @@ -20,7 +20,8 @@ export 'package:firebase_app_check_platform_interface/firebase_app_check_platfor AppleAppAttestProvider, AppleAppAttestWithDeviceCheckFallbackProvider, ReCaptchaEnterpriseProvider, - ReCaptchaV3Provider; + ReCaptchaV3Provider, + WebDebugProvider; export 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' show FirebaseException; diff --git a/packages/firebase_app_check/firebase_app_check/pubspec.yaml b/packages/firebase_app_check/firebase_app_check/pubspec.yaml index 59d7ad260051..41bad99db192 100644 --- a/packages/firebase_app_check/firebase_app_check/pubspec.yaml +++ b/packages/firebase_app_check/firebase_app_check/pubspec.yaml @@ -2,7 +2,7 @@ name: firebase_app_check description: App Check works alongside other Firebase services to help protect your backend resources from abuse, such as billing fraud or phishing. homepage: https://firebase.google.com/docs/app-check repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_check/firebase_app_check -version: 0.4.1+4 +version: 0.4.1+5 topics: - firebase - app-check @@ -17,9 +17,9 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_app_check_platform_interface: ^0.2.1+4 - firebase_app_check_web: ^0.2.2+2 - firebase_core: ^4.4.0 + firebase_app_check_platform_interface: ^0.2.1+5 + firebase_app_check_web: ^0.2.2+3 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/CHANGELOG.md b/packages/firebase_app_check/firebase_app_check_platform_interface/CHANGELOG.md index 5a0d6664d005..f13b7d53885c 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/CHANGELOG.md +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1+5 + + - Update a dependency to the latest release. + ## 0.2.1+4 - Update a dependency to the latest release. diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/web_providers.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/web_providers.dart index 8715a4248f78..55d5ab576c7e 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/web_providers.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/web_providers.dart @@ -15,3 +15,19 @@ class ReCaptchaV3Provider extends WebProvider { class ReCaptchaEnterpriseProvider extends WebProvider { ReCaptchaEnterpriseProvider(String siteKey) : super(siteKey); } + +/// Debug provider for Web. +/// +/// Sets `self.FIREBASE_APPCHECK_DEBUG_TOKEN` before initializing App Check. +/// If [debugToken] is provided, that token is used. Otherwise the Firebase JS +/// SDK auto-generates one and prints it to the browser console — you then +/// register that token in the Firebase Console. +/// +/// See documentation: https://firebase.google.com/docs/app-check/web/debug-provider +class WebDebugProvider extends WebProvider { + /// Creates a web debug provider with an optional debug token. + WebDebugProvider({this.debugToken}) : super(''); + + /// The debug token for this provider. + final String? debugToken; +} diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/pubspec.yaml b/packages/firebase_app_check/firebase_app_check_platform_interface/pubspec.yaml index fb944c5fb2e9..fd1ef215d65d 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/pubspec.yaml +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/pubspec.yaml @@ -1,15 +1,15 @@ name: firebase_app_check_platform_interface description: A common platform interface for the firebase_app_check plugin. homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_check/firebase_app_check_platform_interface -version: 0.2.1+4 +version: 0.2.1+5 environment: sdk: '>=3.2.0 <4.0.0' flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_app_check/firebase_app_check_web/CHANGELOG.md b/packages/firebase_app_check/firebase_app_check_web/CHANGELOG.md index 9dd60b35623f..ddb611320113 100644 --- a/packages/firebase_app_check/firebase_app_check_web/CHANGELOG.md +++ b/packages/firebase_app_check/firebase_app_check_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.2+3 + + - Update a dependency to the latest release. + ## 0.2.2+2 - Update a dependency to the latest release. diff --git a/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart b/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart index b9011976f765..6393edc01587 100644 --- a/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart +++ b/packages/firebase_app_check/firebase_app_check_web/lib/firebase_app_check_web.dart @@ -22,6 +22,7 @@ class FirebaseAppCheckWeb extends FirebaseAppCheckPlatform { static const String _libraryName = 'flutter-fire-app-check'; static const recaptchaTypeV3 = 'recaptcha-v3'; static const recaptchaTypeEnterprise = 'enterprise'; + static const recaptchaTypeDebug = 'debug'; static Map> _tokenChangesListeners = {}; /// Stub initializer to allow the [registerWith] to create an instance without @@ -56,14 +57,22 @@ class FirebaseAppCheckWeb extends FirebaseAppCheckPlatform { .getItem(_sessionKeyRecaptchaSiteKey(firebaseApp.name)); } - if (recaptchaType != null && recaptchaSiteKey != null) { + if (recaptchaType != null) { final WebProvider provider; - if (recaptchaType == recaptchaTypeV3) { - provider = ReCaptchaV3Provider(recaptchaSiteKey); - } else if (recaptchaType == recaptchaTypeEnterprise) { - provider = ReCaptchaEnterpriseProvider(recaptchaSiteKey); + if (recaptchaType == recaptchaTypeDebug) { + final debugToken = + recaptchaSiteKey?.isNotEmpty ?? false ? recaptchaSiteKey : null; + provider = WebDebugProvider(debugToken: debugToken); + } else if (recaptchaSiteKey != null) { + if (recaptchaType == recaptchaTypeV3) { + provider = ReCaptchaV3Provider(recaptchaSiteKey); + } else if (recaptchaType == recaptchaTypeEnterprise) { + provider = ReCaptchaEnterpriseProvider(recaptchaSiteKey); + } else { + throw Exception('Invalid recaptcha type: $recaptchaType'); + } } else { - throw Exception('Invalid recaptcha type: $recaptchaType'); + return; } await instance.activate(webProvider: provider); } @@ -127,7 +136,9 @@ class FirebaseAppCheckWeb extends FirebaseAppCheckPlatform { // save the recaptcha type and site key for future startups if (webProvider != null) { final String recaptchaType; - if (webProvider is ReCaptchaV3Provider) { + if (webProvider is WebDebugProvider) { + recaptchaType = recaptchaTypeDebug; + } else if (webProvider is ReCaptchaV3Provider) { recaptchaType = recaptchaTypeV3; } else if (webProvider is ReCaptchaEnterpriseProvider) { recaptchaType = recaptchaTypeEnterprise; @@ -136,8 +147,11 @@ class FirebaseAppCheckWeb extends FirebaseAppCheckPlatform { } web.window.localStorage .setItem(_sessionKeyRecaptchaType(app.name), recaptchaType); - web.window.localStorage - .setItem(_sessionKeyRecaptchaSiteKey(app.name), webProvider.siteKey); + web.window.localStorage.setItem( + _sessionKeyRecaptchaSiteKey(app.name), + webProvider is WebDebugProvider + ? webProvider.debugToken ?? '' + : webProvider.siteKey); } // activate API no longer exists, recaptcha key has to be passed on initialization of app-check instance. diff --git a/packages/firebase_app_check/firebase_app_check_web/lib/src/firebase_app_check_version.dart b/packages/firebase_app_check/firebase_app_check_web/lib/src/firebase_app_check_version.dart index 70f0585c6815..1e16bda3acdf 100644 --- a/packages/firebase_app_check/firebase_app_check_web/lib/src/firebase_app_check_version.dart +++ b/packages/firebase_app_check/firebase_app_check_web/lib/src/firebase_app_check_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '0.4.1+4'; +const packageVersion = '0.4.1+5'; diff --git a/packages/firebase_app_check/firebase_app_check_web/lib/src/interop/app_check.dart b/packages/firebase_app_check/firebase_app_check_web/lib/src/interop/app_check.dart index 63f261ca8fa6..c35a5d1d8d16 100644 --- a/packages/firebase_app_check/firebase_app_check_web/lib/src/interop/app_check.dart +++ b/packages/firebase_app_check/firebase_app_check_web/lib/src/interop/app_check.dart @@ -18,7 +18,24 @@ export 'app_check_interop.dart'; AppCheck? getAppCheckInstance([App? app, WebProvider? provider]) { late app_check_interop.ReCaptchaProvider jsProvider; - if (provider is ReCaptchaV3Provider) { + if (provider is WebDebugProvider) { + // Set the debug token global before initializing App Check. + // The Firebase JS SDK reads this and creates a DebugProvider internally. + if (provider.debugToken != null) { + globalContext.setProperty( + 'FIREBASE_APPCHECK_DEBUG_TOKEN'.toJS, + provider.debugToken!.toJS, + ); + } else { + globalContext.setProperty( + 'FIREBASE_APPCHECK_DEBUG_TOKEN'.toJS, + true.toJS, + ); + } + // A provider is still required by initializeAppCheck, but the debug + // token global overrides it. + jsProvider = app_check_interop.ReCaptchaV3Provider('debug'.toJS); + } else if (provider is ReCaptchaV3Provider) { jsProvider = app_check_interop.ReCaptchaV3Provider(provider.siteKey.toJS); } else if (provider is ReCaptchaEnterpriseProvider) { jsProvider = diff --git a/packages/firebase_app_check/firebase_app_check_web/pubspec.yaml b/packages/firebase_app_check/firebase_app_check_web/pubspec.yaml index 1d81257e93b3..ab8e2cc2c50a 100644 --- a/packages/firebase_app_check/firebase_app_check_web/pubspec.yaml +++ b/packages/firebase_app_check/firebase_app_check_web/pubspec.yaml @@ -1,17 +1,17 @@ name: firebase_app_check_web description: The web implementation of firebase_app_check homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_check/firebase_app_check_web -version: 0.2.2+2 +version: 0.2.2+3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_app_check_platform_interface: ^0.2.1+4 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 + _flutterfire_internals: ^1.3.67 + firebase_app_check_platform_interface: ^0.2.1+5 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_app_installations/firebase_app_installations/CHANGELOG.md b/packages/firebase_app_installations/firebase_app_installations/CHANGELOG.md index 03ce1ca89db8..f978f0c8bd1d 100644 --- a/packages/firebase_app_installations/firebase_app_installations/CHANGELOG.md +++ b/packages/firebase_app_installations/firebase_app_installations/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0+7 + + - Update a dependency to the latest release. + ## 0.4.0+6 - Update a dependency to the latest release. diff --git a/packages/firebase_app_installations/firebase_app_installations/example/ios/Runner/Info.plist b/packages/firebase_app_installations/firebase_app_installations/example/ios/Runner/Info.plist index 53f00185bf94..5080e4ae257c 100644 --- a/packages/firebase_app_installations/firebase_app_installations/example/ios/Runner/Info.plist +++ b/packages/firebase_app_installations/firebase_app_installations/example/ios/Runner/Info.plist @@ -47,5 +47,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml b/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml index 9a46153e57fb..90f31154854b 100644 --- a/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml +++ b/packages/firebase_app_installations/firebase_app_installations/example/pubspec.yaml @@ -9,8 +9,8 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - firebase_core: ^4.4.0 - firebase_app_installations: ^0.4.0+6 + firebase_core: ^4.5.0 + firebase_app_installations: ^0.4.0+7 flutter: sdk: flutter diff --git a/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/Constants.swift b/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/Constants.swift index bbd958ad5897..6d856d7c77e6 100644 --- a/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/Constants.swift +++ b/packages/firebase_app_installations/firebase_app_installations/ios/firebase_app_installations/Sources/firebase_app_installations/Constants.swift @@ -3,4 +3,4 @@ // found in the LICENSE file. /// Auto-generated file. Do not edit. -public let versionNumber = "0.4.0+6" +public let versionNumber = "0.4.0+7" diff --git a/packages/firebase_app_installations/firebase_app_installations/ios/generated_firebase_sdk_version.txt b/packages/firebase_app_installations/firebase_app_installations/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_app_installations/firebase_app_installations/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_app_installations/firebase_app_installations/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_app_installations/firebase_app_installations/pubspec.yaml b/packages/firebase_app_installations/firebase_app_installations/pubspec.yaml index 583ad6606bbe..8a5fed240709 100644 --- a/packages/firebase_app_installations/firebase_app_installations/pubspec.yaml +++ b/packages/firebase_app_installations/firebase_app_installations/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_app_installations description: A Flutter plugin allowing you to use Firebase Installations. -version: 0.4.0+6 +version: 0.4.0+7 homepage: https://firebase.google.com/docs/projects/manage-installations#flutter repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_installations/firebase_app_installations topics: @@ -17,9 +17,9 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_app_installations_platform_interface: ^0.1.4+65 - firebase_app_installations_web: ^0.1.7+2 - firebase_core: ^4.4.0 + firebase_app_installations_platform_interface: ^0.1.4+66 + firebase_app_installations_web: ^0.1.7+3 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/CHANGELOG.md b/packages/firebase_app_installations/firebase_app_installations_platform_interface/CHANGELOG.md index e7a47ed3ca13..870f130fd5db 100644 --- a/packages/firebase_app_installations/firebase_app_installations_platform_interface/CHANGELOG.md +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4+66 + + - Update a dependency to the latest release. + ## 0.1.4+65 - Update a dependency to the latest release. diff --git a/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml index 3be0580b713b..568ee1323bc4 100644 --- a/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml +++ b/packages/firebase_app_installations/firebase_app_installations_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_app_installations_platform_interface description: A common platform interface for the firebase_app_installations plugin. -version: 0.1.4+65 +version: 0.1.4+66 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_installations/firebase_app_installations_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_installations/firebase_app_installations_platform_interface @@ -9,8 +9,8 @@ environment: flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_app_installations/firebase_app_installations_web/CHANGELOG.md b/packages/firebase_app_installations/firebase_app_installations_web/CHANGELOG.md index 072d593b01c3..0f9b5525a432 100644 --- a/packages/firebase_app_installations/firebase_app_installations_web/CHANGELOG.md +++ b/packages/firebase_app_installations/firebase_app_installations_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.7+3 + + - Update a dependency to the latest release. + ## 0.1.7+2 - Update a dependency to the latest release. diff --git a/packages/firebase_app_installations/firebase_app_installations_web/lib/src/firebase_app_installations_version.dart b/packages/firebase_app_installations/firebase_app_installations_web/lib/src/firebase_app_installations_version.dart index 70a2f9ad6b25..4995f1941d53 100644 --- a/packages/firebase_app_installations/firebase_app_installations_web/lib/src/firebase_app_installations_version.dart +++ b/packages/firebase_app_installations/firebase_app_installations_web/lib/src/firebase_app_installations_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '0.4.0+6'; +const packageVersion = '0.4.0+7'; diff --git a/packages/firebase_app_installations/firebase_app_installations_web/pubspec.yaml b/packages/firebase_app_installations/firebase_app_installations_web/pubspec.yaml index aa616877d08a..ceab46ce6456 100644 --- a/packages/firebase_app_installations/firebase_app_installations_web/pubspec.yaml +++ b/packages/firebase_app_installations/firebase_app_installations_web/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_app_installations_web description: The web implementation of firebase_app_installations. -version: 0.1.7+2 +version: 0.1.7+3 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_installations/firebase_app_installations_web repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_app_installations/firebase_app_installations_web @@ -9,10 +9,10 @@ environment: flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_app_installations_platform_interface: ^0.1.4+65 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 + _flutterfire_internals: ^1.3.67 + firebase_app_installations_platform_interface: ^0.1.4+66 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_auth/firebase_auth/CHANGELOG.md b/packages/firebase_auth/firebase_auth/CHANGELOG.md index fe3401976991..872fc395b59c 100644 --- a/packages/firebase_auth/firebase_auth/CHANGELOG.md +++ b/packages/firebase_auth/firebase_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.2.0 + + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + ## 6.1.4 - Update a dependency to the latest release. diff --git a/packages/firebase_auth/firebase_auth/example/ios/Runner/Info.plist b/packages/firebase_auth/firebase_auth/example/ios/Runner/Info.plist index 1cc8cb984344..e1ffa4fee645 100644 --- a/packages/firebase_auth/firebase_auth/example/ios/Runner/Info.plist +++ b/packages/firebase_auth/firebase_auth/example/ios/Runner/Info.plist @@ -73,5 +73,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_auth/firebase_auth/example/pubspec.yaml b/packages/firebase_auth/firebase_auth/example/pubspec.yaml index c894e45410e2..9857751e4eb9 100644 --- a/packages/firebase_auth/firebase_auth/example/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth/example/pubspec.yaml @@ -6,12 +6,12 @@ environment: dependencies: barcode_widget: ^2.0.4 - firebase_auth: ^6.1.4 - firebase_core: ^4.4.0 - firebase_messaging: ^16.1.1 + firebase_auth: ^6.2.0 + firebase_core: ^4.5.0 + firebase_messaging: ^16.1.2 flutter: sdk: flutter - flutter_facebook_auth: ^7.0.1 + flutter_facebook_auth: ^7.1.5 flutter_signin_button: ^2.0.0 google_sign_in: ^6.1.0 google_sign_in_dartio: ^0.3.0 diff --git a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m index c55ea4144e03..7edbadb78910 100644 --- a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m +++ b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m @@ -113,6 +113,9 @@ @implementation FLTFirebaseAuthPlugin { // Map an id to a MultiFactorResolver object. NSMutableDictionary *_multiFactorTotpSecretMap; + // Emulator host/port per app, used to build REST URLs for workarounds. + NSMutableDictionary *_emulatorConfigs; + NSObject *_binaryMessenger; NSMutableDictionary *_eventChannels; NSMutableDictionary *> *_streamHandlers; @@ -134,6 +137,7 @@ - (instancetype)init:(NSObject *)messenger { _multiFactorResolverMap = [NSMutableDictionary dictionary]; _multiFactorAssertionMap = [NSMutableDictionary dictionary]; _multiFactorTotpSecretMap = [NSMutableDictionary dictionary]; + _emulatorConfigs = [NSMutableDictionary dictionary]; } return self; } @@ -148,6 +152,13 @@ + (void)registerWithRegistrar:(NSObject *)registrar { [registrar publish:instance]; [registrar addApplicationDelegate:instance]; +#if !TARGET_OS_OSX + if (@available(iOS 13.0, *)) { + if ([registrar respondsToSelector:@selector(addSceneDelegate:)]) { + [registrar performSelector:@selector(addSceneDelegate:) withObject:instance]; + } + } +#endif SetUpFirebaseAuthHostApi(registrar.messenger, instance); SetUpFirebaseAuthUserHostApi(registrar.messenger, instance); SetUpMultiFactorUserHostApi(registrar.messenger, instance); @@ -274,6 +285,18 @@ - (void)application:(UIApplication *)application - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { return [[FIRAuth auth] canHandleURL:url]; } + +#pragma mark - SceneDelegate + +- (BOOL)scene:(UIScene *)scene + openURLContexts:(NSSet *)URLContexts API_AVAILABLE(ios(13.0)) { + for (UIOpenURLContext *urlContext in URLContexts) { + if ([[FIRAuth auth] canHandleURL:urlContext.URL]) { + return YES; + } + } + return NO; +} #endif #pragma mark - FLTFirebasePlugin @@ -831,6 +854,31 @@ - (nonnull ASPresentationAnchor)presentationAnchorForAuthorizationController: #if TARGET_OS_OSX return [[NSApplication sharedApplication] keyWindow]; #else + // UIApplication.keyWindow is deprecated in iOS 13+ with UIScene lifecycle. + // Walk the connected scenes to find the foreground active window. + if (@available(iOS 15.0, *)) { + for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive && + [scene isKindOfClass:[UIWindowScene class]]) { + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (windowScene.keyWindow) { + return windowScene.keyWindow; + } + } + } + } else if (@available(iOS 13.0, *)) { + for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive && + [scene isKindOfClass:[UIWindowScene class]]) { + UIWindowScene *windowScene = (UIWindowScene *)scene; + for (UIWindow *window in windowScene.windows) { + if (window.isKeyWindow) { + return window; + } + } + } + } + } return [[UIApplication sharedApplication] keyWindow]; #endif } @@ -1093,7 +1141,20 @@ - (void)checkActionCodeApp:(nonnull AuthPigeonFirebaseApp *)app if (error != nil) { completion(nil, [FLTFirebaseAuthPlugin convertToFlutterError:error]); } else { - completion([self parseActionCode:info], nil); + PigeonActionCodeInfo *result = [self parseActionCode:info]; + if (result.operation == ActionCodeInfoOperationUnknown) { + // Workaround: Firebase iOS SDK >=11.12.0 returns .unknown because + // actionCodeOperation(forRequestType:) only matches camelCase but the + // REST API returns SCREAMING_SNAKE_CASE (e.g. "VERIFY_EMAIL"). + // Re-fetch the raw requestType via REST to resolve the operation. + // See: https://github.com/firebase/flutterfire/issues/17452 + [self resolveActionCodeOperationForApp:app + code:code + fallbackInfo:result + completion:completion]; + } else { + completion(result, nil); + } } }]; } @@ -1123,6 +1184,91 @@ - (PigeonActionCodeInfo *_Nullable)parseActionCode:(nonnull FIRActionCodeInfo *) return [PigeonActionCodeInfo makeWithOperation:operation data:data]; } +/// Maps a raw requestType string (either camelCase or SCREAMING_SNAKE_CASE) to +/// the corresponding Pigeon enum value. ++ (ActionCodeInfoOperation)operationFromRequestType:(nullable NSString *)requestType { + static NSDictionary *mapping; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + mapping = @{ + @"PASSWORD_RESET" : @(ActionCodeInfoOperationPasswordReset), + @"resetPassword" : @(ActionCodeInfoOperationPasswordReset), + @"VERIFY_EMAIL" : @(ActionCodeInfoOperationVerifyEmail), + @"verifyEmail" : @(ActionCodeInfoOperationVerifyEmail), + @"RECOVER_EMAIL" : @(ActionCodeInfoOperationRecoverEmail), + @"recoverEmail" : @(ActionCodeInfoOperationRecoverEmail), + @"EMAIL_SIGNIN" : @(ActionCodeInfoOperationEmailSignIn), + @"signIn" : @(ActionCodeInfoOperationEmailSignIn), + @"VERIFY_AND_CHANGE_EMAIL" : @(ActionCodeInfoOperationVerifyAndChangeEmail), + @"verifyAndChangeEmail" : @(ActionCodeInfoOperationVerifyAndChangeEmail), + @"REVERT_SECOND_FACTOR_ADDITION" : @(ActionCodeInfoOperationRevertSecondFactorAddition), + @"revertSecondFactorAddition" : @(ActionCodeInfoOperationRevertSecondFactorAddition), + }; + }); + + NSNumber *value = mapping[requestType]; + return value ? (ActionCodeInfoOperation)value.integerValue : ActionCodeInfoOperationUnknown; +} + +/// Calls the Identity Toolkit REST API directly to retrieve the raw requestType +/// string, which the iOS SDK fails to parse correctly. Falls back to the original +/// result if the REST call fails for any reason. +- (void)resolveActionCodeOperationForApp:(nonnull AuthPigeonFirebaseApp *)app + code:(nonnull NSString *)code + fallbackInfo:(nonnull PigeonActionCodeInfo *)fallbackInfo + completion:(nonnull void (^)(PigeonActionCodeInfo *_Nullable, + FlutterError *_Nullable))completion { + FIRApp *firebaseApp = [FLTFirebasePlugin firebaseAppNamed:app.appName]; + NSString *apiKey = firebaseApp.options.APIKey; + + NSString *baseURL; + NSDictionary *emulatorConfig = _emulatorConfigs[app.appName]; + if (emulatorConfig) { + baseURL = [NSString stringWithFormat:@"http://%@:%@/identitytoolkit.googleapis.com", + emulatorConfig[@"host"], emulatorConfig[@"port"]]; + } else { + baseURL = @"https://identitytoolkit.googleapis.com"; + } + + NSString *urlString = + [NSString stringWithFormat:@"%@/v1/accounts:resetPassword?key=%@", baseURL, apiKey]; + NSURL *url = [NSURL URLWithString:urlString]; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.HTTPMethod = @"POST"; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + request.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{@"oobCode" : code} + options:0 + error:nil]; + + NSURLSessionDataTask *task = [[NSURLSession sharedSession] + dataTaskWithRequest:request + completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error || !data) { + completion(fallbackInfo, nil); + return; + } + + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (!json || json[@"error"]) { + completion(fallbackInfo, nil); + return; + } + + ActionCodeInfoOperation operation = + [FLTFirebaseAuthPlugin operationFromRequestType:json[@"requestType"]]; + + if (operation != ActionCodeInfoOperationUnknown) { + completion([PigeonActionCodeInfo makeWithOperation:operation data:fallbackInfo.data], + nil); + } else { + completion(fallbackInfo, nil); + } + }]; + [task resume]; +} + - (void)confirmPasswordResetApp:(nonnull AuthPigeonFirebaseApp *)app code:(nonnull NSString *)code newPassword:(nonnull NSString *)newPassword @@ -1556,6 +1702,7 @@ - (void)useEmulatorApp:(nonnull AuthPigeonFirebaseApp *)app completion:(nonnull void (^)(FlutterError *_Nullable))completion { FIRAuth *auth = [self getFIRAuthFromAppNameFromPigeon:app]; [auth useEmulatorWithHost:host port:port]; + _emulatorConfigs[app.appName] = @{@"host" : host, @"port" : @(port)}; completion(nil); } @@ -1903,7 +2050,7 @@ - (void)reloadApp:(nonnull AuthPigeonFirebaseApp *)app if (error != nil) { completion(nil, [FLTFirebaseAuthPlugin convertToFlutterError:error]); } else { - completion([PigeonParser getPigeonDetails:auth.currentUser], nil); + completion([PigeonParser getPigeonDetails:currentUser], nil); } }]; } @@ -1979,7 +2126,7 @@ - (void)updateEmailApp:(nonnull AuthPigeonFirebaseApp *)app if (reloadError != nil) { completion(nil, [FLTFirebaseAuthPlugin convertToFlutterError:reloadError]); } else { - completion([PigeonParser getPigeonDetails:auth.currentUser], nil); + completion([PigeonParser getPigeonDetails:currentUser], nil); } }]; } @@ -2009,7 +2156,7 @@ - (void)updatePasswordApp:(nonnull AuthPigeonFirebaseApp *)app if (reloadError != nil) { completion(nil, [FLTFirebaseAuthPlugin convertToFlutterError:reloadError]); } else { - completion([PigeonParser getPigeonDetails:auth.currentUser], nil); + completion([PigeonParser getPigeonDetails:currentUser], nil); } }]; } @@ -2065,9 +2212,8 @@ - (void)updatePhoneNumberApp:(nonnull AuthPigeonFirebaseApp *)app reloadError]); } else { completion( - [PigeonParser - getPigeonDetails: - auth.currentUser], + [PigeonParser getPigeonDetails: + currentUser], nil); } }]; @@ -2120,7 +2266,7 @@ - (void)updateProfileApp:(nonnull AuthPigeonFirebaseApp *)app if (reloadError != nil) { completion(nil, [FLTFirebaseAuthPlugin convertToFlutterError:reloadError]); } else { - completion([PigeonParser getPigeonDetails:auth.currentUser], nil); + completion([PigeonParser getPigeonDetails:currentUser], nil); } }]; } diff --git a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/PigeonParser.m b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/PigeonParser.m index 3386570909a4..f8ef1b77493c 100644 --- a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/PigeonParser.m +++ b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/PigeonParser.m @@ -32,12 +32,12 @@ + (PigeonUserDetails *)getPigeonDetails:(nonnull FIRUser *)user { } + (PigeonUserInfo *)getPigeonUserInfo:(nonnull FIRUser *)user { + NSString *photoUrlString = user.photoURL.absoluteString; return [PigeonUserInfo makeWithUid:user.uid email:user.email displayName:user.displayName - photoUrl:(user.photoURL.absoluteString.length > 0) ? user.photoURL.absoluteString - : nil + photoUrl:(photoUrlString.length > 0) ? photoUrlString : nil phoneNumber:user.phoneNumber isAnonymous:user.isAnonymous isEmailVerified:user.emailVerified @@ -54,6 +54,7 @@ + (PigeonUserInfo *)getPigeonUserInfo:(nonnull FIRUser *)user { [NSMutableArray arrayWithCapacity:providerData.count]; for (id userInfo in providerData) { + NSString *photoUrlStr = userInfo.photoURL.absoluteString; NSDictionary *dataDict = @{ @"providerId" : userInfo.providerID, // Can be null on emulator @@ -61,7 +62,7 @@ + (PigeonUserInfo *)getPigeonUserInfo:(nonnull FIRUser *)user { @"displayName" : userInfo.displayName ?: [NSNull null], @"email" : userInfo.email ?: [NSNull null], @"phoneNumber" : userInfo.phoneNumber ?: [NSNull null], - @"photoURL" : userInfo.photoURL.absoluteString ?: [NSNull null], + @"photoURL" : photoUrlStr ?: [NSNull null], // isAnonymous is always false on in a providerData object (the user is not anonymous) @"isAnonymous" : @NO, // isEmailVerified is always true on in a providerData object (the email is verified by the diff --git a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m index 8a32f1224ae6..365ff70d690c 100644 --- a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m +++ b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m @@ -1065,11 +1065,11 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:FirebaseAuthHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(confirmPasswordResetApp: - code:newPassword:completion:)], - @"FirebaseAuthHostApi api (%@) doesn't respond to " - @"@selector(confirmPasswordResetApp:code:newPassword:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(confirmPasswordResetApp:code:newPassword:completion:)], + @"FirebaseAuthHostApi api (%@) doesn't respond to " + @"@selector(confirmPasswordResetApp:code:newPassword:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1096,11 +1096,13 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:FirebaseAuthHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (createUserWithEmailAndPasswordApp:email:password:completion:)], - @"FirebaseAuthHostApi api (%@) doesn't respond to " - @"@selector(createUserWithEmailAndPasswordApp:email:password:completion:)", - api); + NSCAssert( + [api + respondsToSelector:@selector( + createUserWithEmailAndPasswordApp:email:password:completion:)], + @"FirebaseAuthHostApi api (%@) doesn't respond to " + @"@selector(createUserWithEmailAndPasswordApp:email:password:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1213,11 +1215,12 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:FirebaseAuthHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (signInWithEmailAndPasswordApp:email:password:completion:)], - @"FirebaseAuthHostApi api (%@) doesn't respond to " - @"@selector(signInWithEmailAndPasswordApp:email:password:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector( + signInWithEmailAndPasswordApp:email:password:completion:)], + @"FirebaseAuthHostApi api (%@) doesn't respond to " + @"@selector(signInWithEmailAndPasswordApp:email:password:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1245,11 +1248,11 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:FirebaseAuthHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(signInWithEmailLinkApp: - email:emailLink:completion:)], - @"FirebaseAuthHostApi api (%@) doesn't respond to " - @"@selector(signInWithEmailLinkApp:email:emailLink:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(signInWithEmailLinkApp:email:emailLink:completion:)], + @"FirebaseAuthHostApi api (%@) doesn't respond to " + @"@selector(signInWithEmailLinkApp:email:emailLink:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1277,11 +1280,11 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:FirebaseAuthHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(signInWithProviderApp: - signInProvider:completion:)], - @"FirebaseAuthHostApi api (%@) doesn't respond to " - @"@selector(signInWithProviderApp:signInProvider:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(signInWithProviderApp:signInProvider:completion:)], + @"FirebaseAuthHostApi api (%@) doesn't respond to " + @"@selector(signInWithProviderApp:signInProvider:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1361,8 +1364,8 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:FirebaseAuthHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (sendPasswordResetEmailApp:email:actionCodeSettings:completion:)], + NSCAssert([api respondsToSelector: + @selector(sendPasswordResetEmailApp:email:actionCodeSettings:completion:)], @"FirebaseAuthHostApi api (%@) doesn't respond to " @"@selector(sendPasswordResetEmailApp:email:actionCodeSettings:completion:)", api); @@ -1392,8 +1395,8 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:FirebaseAuthHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (sendSignInLinkToEmailApp:email:actionCodeSettings:completion:)], + NSCAssert([api respondsToSelector: + @selector(sendSignInLinkToEmailApp:email:actionCodeSettings:completion:)], @"FirebaseAuthHostApi api (%@) doesn't respond to " @"@selector(sendSignInLinkToEmailApp:email:actionCodeSettings:completion:)", api); @@ -1535,7 +1538,7 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng codec:FirebaseAuthHostApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(revokeTokenWithAuthorizationCodeApp: - authorizationCode:completion:)], + authorizationCode:completion:)], @"FirebaseAuthHostApi api (%@) doesn't respond to " @"@selector(revokeTokenWithAuthorizationCodeApp:authorizationCode:completion:)", api); @@ -1844,11 +1847,11 @@ void SetUpFirebaseAuthUserHostApiWithSuffix(id binaryMes binaryMessenger:binaryMessenger codec:FirebaseAuthUserHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(reauthenticateWithCredentialApp: - input:completion:)], - @"FirebaseAuthUserHostApi api (%@) doesn't respond to " - @"@selector(reauthenticateWithCredentialApp:input:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(reauthenticateWithCredentialApp:input:completion:)], + @"FirebaseAuthUserHostApi api (%@) doesn't respond to " + @"@selector(reauthenticateWithCredentialApp:input:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1874,11 +1877,12 @@ void SetUpFirebaseAuthUserHostApiWithSuffix(id binaryMes binaryMessenger:binaryMessenger codec:FirebaseAuthUserHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(reauthenticateWithProviderApp: - signInProvider:completion:)], - @"FirebaseAuthUserHostApi api (%@) doesn't respond to " - @"@selector(reauthenticateWithProviderApp:signInProvider:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector( + reauthenticateWithProviderApp:signInProvider:completion:)], + @"FirebaseAuthUserHostApi api (%@) doesn't respond to " + @"@selector(reauthenticateWithProviderApp:signInProvider:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -1929,11 +1933,12 @@ void SetUpFirebaseAuthUserHostApiWithSuffix(id binaryMes binaryMessenger:binaryMessenger codec:FirebaseAuthUserHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(sendEmailVerificationApp: - actionCodeSettings:completion:)], - @"FirebaseAuthUserHostApi api (%@) doesn't respond to " - @"@selector(sendEmailVerificationApp:actionCodeSettings:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector( + sendEmailVerificationApp:actionCodeSettings:completion:)], + @"FirebaseAuthUserHostApi api (%@) doesn't respond to " + @"@selector(sendEmailVerificationApp:actionCodeSettings:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -2099,8 +2104,8 @@ void SetUpFirebaseAuthUserHostApiWithSuffix(id binaryMes binaryMessenger:binaryMessenger codec:FirebaseAuthUserHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (verifyBeforeUpdateEmailApp:newEmail:actionCodeSettings:completion:)], + NSCAssert([api respondsToSelector:@selector(verifyBeforeUpdateEmailApp:newEmail: + actionCodeSettings:completion:)], @"FirebaseAuthUserHostApi api (%@) doesn't respond to " @"@selector(verifyBeforeUpdateEmailApp:newEmail:actionCodeSettings:completion:)", api); @@ -2204,11 +2209,11 @@ void SetUpMultiFactorUserHostApiWithSuffix(id binaryMess binaryMessenger:binaryMessenger codec:MultiFactorUserHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(enrollPhoneApp: - assertion:displayName:completion:)], - @"MultiFactorUserHostApi api (%@) doesn't respond to " - @"@selector(enrollPhoneApp:assertion:displayName:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(enrollPhoneApp:assertion:displayName:completion:)], + @"MultiFactorUserHostApi api (%@) doesn't respond to " + @"@selector(enrollPhoneApp:assertion:displayName:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -2234,11 +2239,11 @@ void SetUpMultiFactorUserHostApiWithSuffix(id binaryMess binaryMessenger:binaryMessenger codec:MultiFactorUserHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(enrollTotpApp: - assertionId:displayName:completion:)], - @"MultiFactorUserHostApi api (%@) doesn't respond to " - @"@selector(enrollTotpApp:assertionId:displayName:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(enrollTotpApp:assertionId:displayName:completion:)], + @"MultiFactorUserHostApi api (%@) doesn't respond to " + @"@selector(enrollTotpApp:assertionId:displayName:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); @@ -2430,8 +2435,8 @@ void SetUpMultiFactoResolverHostApiWithSuffix(id binaryM binaryMessenger:binaryMessenger codec:MultiFactoResolverHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (resolveSignInResolverId:assertion:totpAssertionId:completion:)], + NSCAssert([api respondsToSelector: + @selector(resolveSignInResolverId:assertion:totpAssertionId:completion:)], @"MultiFactoResolverHostApi api (%@) doesn't respond to " @"@selector(resolveSignInResolverId:assertion:totpAssertionId:completion:)", api); @@ -2549,8 +2554,8 @@ void SetUpMultiFactorTotpHostApiWithSuffix(id binaryMess binaryMessenger:binaryMessenger codec:MultiFactorTotpHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getAssertionForEnrollmentSecretKey: - oneTimePassword:completion:)], + NSCAssert([api respondsToSelector: + @selector(getAssertionForEnrollmentSecretKey:oneTimePassword:completion:)], @"MultiFactorTotpHostApi api (%@) doesn't respond to " @"@selector(getAssertionForEnrollmentSecretKey:oneTimePassword:completion:)", api); @@ -2579,8 +2584,8 @@ void SetUpMultiFactorTotpHostApiWithSuffix(id binaryMess binaryMessenger:binaryMessenger codec:MultiFactorTotpHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getAssertionForSignInEnrollmentId: - oneTimePassword:completion:)], + NSCAssert([api respondsToSelector: + @selector(getAssertionForSignInEnrollmentId:oneTimePassword:completion:)], @"MultiFactorTotpHostApi api (%@) doesn't respond to " @"@selector(getAssertionForSignInEnrollmentId:oneTimePassword:completion:)", api); @@ -2627,11 +2632,12 @@ void SetUpMultiFactorTotpSecretHostApiWithSuffix(id bina binaryMessenger:binaryMessenger codec:MultiFactorTotpSecretHostApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(generateQrCodeUrlSecretKey: - accountName:issuer:completion:)], - @"MultiFactorTotpSecretHostApi api (%@) doesn't respond to " - @"@selector(generateQrCodeUrlSecretKey:accountName:issuer:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector( + generateQrCodeUrlSecretKey:accountName:issuer:completion:)], + @"MultiFactorTotpSecretHostApi api (%@) doesn't respond to " + @"@selector(generateQrCodeUrlSecretKey:accountName:issuer:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_secretKey = GetNullableObjectAtIndex(args, 0); diff --git a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/FLTFirebaseAuthPlugin.h b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/FLTFirebaseAuthPlugin.h index ea4a0168a18d..552728aab958 100644 --- a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/FLTFirebaseAuthPlugin.h +++ b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/FLTFirebaseAuthPlugin.h @@ -19,6 +19,10 @@ #endif #import "firebase_auth_messages.g.h" +#if !TARGET_OS_OSX +@protocol FlutterSceneLifeCycleDelegate; +#endif + @interface FLTFirebaseAuthPlugin : FLTFirebasePlugin + ASAuthorizationControllerPresentationContextProviding +#if !TARGET_OS_OSX +#if __has_include() || \ + defined(FlutterSceneLifeCycleDelegate) + , + FlutterSceneLifeCycleDelegate +#endif +#endif + > + (FlutterError *)convertToFlutterError:(NSError *)error; @end diff --git a/packages/firebase_auth/firebase_auth/ios/generated_firebase_sdk_version.txt b/packages/firebase_auth/firebase_auth/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_auth/firebase_auth/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_auth/firebase_auth/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index bdef18cbd7e1..027892a8c8d9 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -535,11 +535,19 @@ class FirebaseAuth extends FirebasePluginPlatform { /// - Thrown if the email address is not valid. /// - **user-disabled**: /// - Thrown if the user corresponding to the given email has been disabled. - /// - **user-not-found**: + /// - **user-not-found** _(deprecated)_: /// - Thrown if there is no user corresponding to the given email. - /// - **wrong-password**: + /// **Note:** This code is no longer returned on projects that have + /// [email enumeration protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// enabled (the default for new projects since September 2023). + /// Use **invalid-credential** instead. + /// - **wrong-password** _(deprecated)_: /// - Thrown if the password is invalid for the given email, or the account /// corresponding to the email does not have a password set. + /// **Note:** This code is no longer returned on projects that have + /// [email enumeration protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// enabled (the default for new projects since September 2023). + /// Use **invalid-credential** instead. /// - **too-many-requests**: /// - Thrown if the user sent too many requests at the same time, for security /// the api will not allow too many attempts at the same time, user will have @@ -550,11 +558,13 @@ class FirebaseAuth extends FirebasePluginPlatform { /// - **network-request-failed**: /// - Thrown if there was a network request error, for example the user /// doesn't have internet connection - /// - **INVALID_LOGIN_CREDENTIALS** or **invalid-credential**: - /// - Thrown if the password is invalid for the given email, or the account - /// corresponding to the email does not have a password set. - /// Depending on if you are using firebase emulator or not the code is - /// different + /// - **invalid-credential**: + /// - Thrown if the email or password is incorrect. On projects with + /// [email enumeration protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// enabled (the default since September 2023), this replaces + /// **user-not-found** and **wrong-password** to prevent revealing + /// whether an account exists. On the Firebase emulator, the code may + /// appear as **INVALID_LOGIN_CREDENTIALS**. /// - **operation-not-allowed**: /// - Thrown if email/password accounts are not enabled. Enable /// email/password accounts in the Firebase Console, under the Auth tab. diff --git a/packages/firebase_auth/firebase_auth/pubspec.yaml b/packages/firebase_auth/firebase_auth/pubspec.yaml index af71825278dd..960bb0260d0a 100755 --- a/packages/firebase_auth/firebase_auth/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for Firebase Auth, enabling like Google, Facebook and Twitter. homepage: https://firebase.google.com/docs/auth repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_auth/firebase_auth -version: 6.1.4 +version: 6.2.0 topics: - firebase - authentication @@ -20,9 +20,9 @@ environment: flutter: '>=3.16.0' dependencies: - firebase_auth_platform_interface: ^8.1.6 - firebase_auth_web: ^6.1.2 - firebase_core: ^4.4.0 + firebase_auth_platform_interface: ^8.1.7 + firebase_auth_web: ^6.1.3 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 flutter: sdk: flutter diff --git a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp index 8986fb26e613..5a4f1bf27faf 100644 --- a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp +++ b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp @@ -53,6 +53,9 @@ void FirebaseAuthPlugin::RegisterWithRegistrar( FirebaseAuthHostApi::SetUp(registrar->messenger(), plugin.get()); FirebaseAuthUserHostApi::SetUp(registrar->messenger(), plugin.get()); + RegisterFlutterFirebasePlugin("plugins.flutter.io/firebase_auth", + plugin.get()); + registrar->AddPlugin(std::move(plugin)); binaryMessenger = registrar->messenger(); @@ -1283,4 +1286,35 @@ void FirebaseAuthPlugin::RevokeTokenWithAuthorizationCode( nullptr)); } +flutter::EncodableMap FirebaseAuthPlugin::GetPluginConstantsForFirebaseApp( + const firebase::App& app) { + flutter::EncodableMap constants; + + Auth* auth = Auth::GetAuth(const_cast(&app)); + firebase::auth::User user = auth->current_user(); + + if (user.is_valid()) { + PigeonUserDetails userDetails = ParseUserDetails(user); + flutter::EncodableList userDetailsList; + userDetailsList.push_back( + flutter::EncodableValue(userDetails.user_info().ToEncodableList())); + userDetailsList.push_back( + flutter::EncodableValue(userDetails.provider_data())); + constants[flutter::EncodableValue("APP_CURRENT_USER")] = + flutter::EncodableValue(userDetailsList); + } + + std::string lang = auth->language_code(); + if (!lang.empty()) { + constants[flutter::EncodableValue("APP_LANGUAGE_CODE")] = + flutter::EncodableValue(lang); + } + + return constants; +} + +void FirebaseAuthPlugin::DidReinitializeFirebaseCore() { + // No-op for now. Could be used to reset cached auth instances. +} + } // namespace firebase_auth_windows diff --git a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h index 419c83236fb6..a75ff33e6cf5 100644 --- a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h +++ b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h @@ -16,6 +16,7 @@ #include "firebase/auth.h" #include "firebase/auth/types.h" #include "firebase/future.h" +#include "firebase_core/flutter_firebase_plugin.h" #include "messages.g.h" using firebase::auth::AuthError; @@ -24,7 +25,8 @@ namespace firebase_auth_windows { class FirebaseAuthPlugin : public flutter::Plugin, public FirebaseAuthHostApi, - public FirebaseAuthUserHostApi { + public FirebaseAuthUserHostApi, + public FlutterFirebasePlugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); @@ -180,6 +182,11 @@ class FirebaseAuthPlugin : public flutter::Plugin, const AuthPigeonFirebaseApp& app, const std::string& authorization_code, std::function reply)> result) override; + // FlutterFirebasePlugin methods. + flutter::EncodableMap GetPluginConstantsForFirebaseApp( + const firebase::App& app) override; + void DidReinitializeFirebaseCore() override; + private: static flutter::BinaryMessenger* binaryMessenger; }; diff --git a/packages/firebase_auth/firebase_auth_platform_interface/CHANGELOG.md b/packages/firebase_auth/firebase_auth_platform_interface/CHANGELOG.md index 752214a4a9e9..a1a1c74b1d54 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/CHANGELOG.md +++ b/packages/firebase_auth/firebase_auth_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 8.1.7 + + - Update a dependency to the latest release. + ## 8.1.6 - Update a dependency to the latest release. diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart index 583a43f273dc..f78a4e99669d 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/utils/exception.dart @@ -154,7 +154,7 @@ String? _getCustomCode(Map? additionalData, String? message) { for (final recognizedCode in listOfRecognizedCode) { if (additionalData?['message'] == recognizedCode || (message?.contains(recognizedCode) ?? false)) { - return recognizedCode; + return recognizedCode.toLowerCase().replaceAll('_', '-'); } } diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart index 9a4fcb1c0308..a57a98532592 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart @@ -514,11 +514,19 @@ abstract class FirebaseAuthPlatform extends PlatformInterface { /// - Thrown if the email address is not valid. /// - **user-disabled**: /// - Thrown if the user corresponding to the given email has been disabled. - /// - **user-not-found**: + /// - **user-not-found** _(deprecated)_: /// - Thrown if there is no user corresponding to the given email. - /// - **wrong-password**: + /// **Note:** This code is no longer returned on projects that have + /// [email enumeration protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// enabled (the default for new projects since September 2023). + /// Use **invalid-credential** instead. + /// - **wrong-password** _(deprecated)_: /// - Thrown if the password is invalid for the given email, or the account /// corresponding to the email does not have a password set. + /// **Note:** This code is no longer returned on projects that have + /// [email enumeration protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// enabled (the default for new projects since September 2023). + /// Use **invalid-credential** instead. /// - **too-many-requests**: /// - Thrown if the user sent too many requests at the same time, for security /// the api will not allow too many attempts at the same time, user will have @@ -529,11 +537,13 @@ abstract class FirebaseAuthPlatform extends PlatformInterface { /// - **network-request-failed**: /// - Thrown if there was a network request error, for example the user /// doesn't have internet connection - /// - **INVALID_LOGIN_CREDENTIALS** or **invalid-credential**: - /// - Thrown if the password is invalid for the given email, or the account - /// corresponding to the email does not have a password set. - /// Depending on if you are using firebase emulator or not the code is - /// different + /// - **invalid-credential**: + /// - Thrown if the email or password is incorrect. On projects with + /// [email enumeration protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// enabled (the default since September 2023), this replaces + /// **user-not-found** and **wrong-password** to prevent revealing + /// whether an account exists. On the Firebase emulator, the code may + /// appear as **INVALID_LOGIN_CREDENTIALS**. /// - **operation-not-allowed**: /// - Thrown if email/password accounts are not enabled. Enable /// email/password accounts in the Firebase Console, under the Auth tab. diff --git a/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml b/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml index 656b8e2061d2..d747cd4dfccc 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth_platform_interface/pubspec.yaml @@ -4,16 +4,16 @@ homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_au repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_auth/firebase_auth_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 8.1.6 +version: 8.1.7 environment: sdk: '>=3.2.0 <4.0.0' flutter: '>=3.16.0' dependencies: - _flutterfire_internals: ^1.3.66 + _flutterfire_internals: ^1.3.67 collection: ^1.16.0 - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter http: ^1.1.0 diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/utils_tests/exception_test.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/utils_tests/exception_test.dart index 43c00f88eff8..2fdfb6df6271 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/utils_tests/exception_test.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/utils_tests/exception_test.dart @@ -44,7 +44,7 @@ void main() { () => convertPlatformException(platformException, StackTrace.empty), throwsA( isA() - .having((e) => e.code, 'code', 'BLOCKING_FUNCTION_ERROR_RESPONSE') + .having((e) => e.code, 'code', 'blocking-function-error-response') .having((e) => e.message, 'message', '{"error":{"details":"The user is not allowed to log in","message":"","status":"PERMISSION_DENIED"}}'), ), diff --git a/packages/firebase_auth/firebase_auth_web/CHANGELOG.md b/packages/firebase_auth/firebase_auth_web/CHANGELOG.md index 90e7cc3f3c2b..34ff02fc17d3 100644 --- a/packages/firebase_auth/firebase_auth_web/CHANGELOG.md +++ b/packages/firebase_auth/firebase_auth_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.3 + + - Update a dependency to the latest release. + ## 6.1.2 - Update a dependency to the latest release. diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_version.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_version.dart index 9623ca56f4d7..305156baf450 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_version.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '6.1.4'; +const packageVersion = '6.2.0'; diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart index 67ce4631d71c..7d66865f0d35 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart @@ -575,6 +575,7 @@ extension type ConfirmationResultJsImpl._(JSObject _) implements JSObject { /// See: . extension type ActionCodeInfo._(JSObject _) implements JSObject { external ActionCodeData get data; + external JSString get operation; } /// Interface representing a user's metadata. diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart b/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart index b1a1e865c249..7e215a3299b4 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart @@ -172,7 +172,8 @@ ActionCodeInfo? convertWebActionCodeInfo( } return ActionCodeInfo( - operation: ActionCodeInfoOperation.passwordReset, + operation: + _convertWebActionCodeOperation(webActionCodeInfo.operation.toDart), data: ActionCodeInfoData( email: webActionCodeInfo.data.email?.toDart, previousEmail: webActionCodeInfo.data.previousEmail?.toDart, @@ -180,6 +181,25 @@ ActionCodeInfo? convertWebActionCodeInfo( ); } +ActionCodeInfoOperation _convertWebActionCodeOperation(String operation) { + switch (operation) { + case 'EMAIL_SIGNIN': + return ActionCodeInfoOperation.emailSignIn; + case 'PASSWORD_RESET': + return ActionCodeInfoOperation.passwordReset; + case 'RECOVER_EMAIL': + return ActionCodeInfoOperation.recoverEmail; + case 'REVERT_SECOND_FACTOR_ADDITION': + return ActionCodeInfoOperation.revertSecondFactorAddition; + case 'VERIFY_AND_CHANGE_EMAIL': + return ActionCodeInfoOperation.verifyAndChangeEmail; + case 'VERIFY_EMAIL': + return ActionCodeInfoOperation.verifyEmail; + default: + return ActionCodeInfoOperation.unknown; + } +} + /// Converts a [auth_interop.AdditionalUserInfo] into a [AdditionalUserInfo]. AdditionalUserInfo? convertWebAdditionalUserInfo( auth_interop.AdditionalUserInfo? webAdditionalUserInfo, diff --git a/packages/firebase_auth/firebase_auth_web/pubspec.yaml b/packages/firebase_auth/firebase_auth_web/pubspec.yaml index 5c61b6f1da6c..07b25884a54b 100644 --- a/packages/firebase_auth/firebase_auth_web/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth_web/pubspec.yaml @@ -2,16 +2,16 @@ name: firebase_auth_web description: The web implementation of firebase_auth homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_auth/firebase_auth_web repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_auth/firebase_auth_web -version: 6.1.2 +version: 6.1.3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - firebase_auth_platform_interface: ^8.1.6 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 + firebase_auth_platform_interface: ^8.1.7 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_core/firebase_core/CHANGELOG.md b/packages/firebase_core/firebase_core/CHANGELOG.md index 0b4c3f34e47f..fbba8a7b29a8 100644 --- a/packages/firebase_core/firebase_core/CHANGELOG.md +++ b/packages/firebase_core/firebase_core/CHANGELOG.md @@ -1,3 +1,11 @@ +## 4.5.0 + + - **FEAT**(core,windows): update C++ Desktop SDK to 13.4.0. This may require updating your Visual Studio version and C++ build tools. ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + - **FEAT**: bump Firebase iOS SDK to 12.9.0 ([#18034](https://github.com/firebase/flutterfire/issues/18034)). ([c45894e2](https://github.com/firebase/flutterfire/commit/c45894e23895f9add8c152d13324920babe9b708)) + - **FEAT**: bump Firebase android SDK to 34.9.0 ([#18016](https://github.com/firebase/flutterfire/issues/18016)). ([b218dbff](https://github.com/firebase/flutterfire/commit/b218dbffd72d0bf666ff94f79a3de1e24d038df0)) + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + ## 4.4.0 - **FEAT**: bump Firebase iOS SDK to 12.8.0 ([#17947](https://github.com/firebase/flutterfire/issues/17947)). ([4eb249ec](https://github.com/firebase/flutterfire/commit/4eb249ec5d870a960d3834e40fd0f3c3b871430c)) diff --git a/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.h b/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.m b/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.m index 70e83933db14..90e3db78c8bb 100644 --- a/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_core/firebase_core/example/ios/Runner/AppDelegate.m @@ -5,9 +5,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_core/firebase_core/example/ios/Runner/Info.plist b/packages/firebase_core/firebase_core/example/ios/Runner/Info.plist index b66b3b91d998..e88ddaa2946e 100644 --- a/packages/firebase_core/firebase_core/example/ios/Runner/Info.plist +++ b/packages/firebase_core/firebase_core/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_core/firebase_core/example/pubspec.yaml b/packages/firebase_core/firebase_core/example/pubspec.yaml index d898a878519e..c1495f17a811 100644 --- a/packages/firebase_core/firebase_core/example/pubspec.yaml +++ b/packages/firebase_core/firebase_core/example/pubspec.yaml @@ -5,7 +5,7 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter diff --git a/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m b/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m index ff9ac9933bc0..cf3e439b92ac 100644 --- a/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m +++ b/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m @@ -224,11 +224,11 @@ void SetUpFirebaseCoreHostApiWithSuffix(id binaryMesseng binaryMessenger:binaryMessenger codec:nullGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initializeAppAppName: - initializeAppRequest:completion:)], - @"FirebaseCoreHostApi api (%@) doesn't respond to " - @"@selector(initializeAppAppName:initializeAppRequest:completion:)", - api); + NSCAssert( + [api respondsToSelector:@selector(initializeAppAppName:initializeAppRequest:completion:)], + @"FirebaseCoreHostApi api (%@) doesn't respond to " + @"@selector(initializeAppAppName:initializeAppRequest:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_appName = GetNullableObjectAtIndex(args, 0); @@ -313,11 +313,13 @@ void SetUpFirebaseAppHostApiWithSuffix(id binaryMessenge binaryMessenger:binaryMessenger codec:nullGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (setAutomaticDataCollectionEnabledAppName:enabled:completion:)], - @"FirebaseAppHostApi api (%@) doesn't respond to " - @"@selector(setAutomaticDataCollectionEnabledAppName:enabled:completion:)", - api); + NSCAssert( + [api + respondsToSelector:@selector( + setAutomaticDataCollectionEnabledAppName:enabled:completion:)], + @"FirebaseAppHostApi api (%@) doesn't respond to " + @"@selector(setAutomaticDataCollectionEnabledAppName:enabled:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_appName = GetNullableObjectAtIndex(args, 0); @@ -342,8 +344,8 @@ void SetUpFirebaseAppHostApiWithSuffix(id binaryMessenge binaryMessenger:binaryMessenger codec:nullGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector - (setAutomaticResourceManagementEnabledAppName:enabled:completion:)], + NSCAssert([api respondsToSelector:@selector(setAutomaticResourceManagementEnabledAppName: + enabled:completion:)], @"FirebaseAppHostApi api (%@) doesn't respond to " @"@selector(setAutomaticResourceManagementEnabledAppName:enabled:completion:)", api); diff --git a/packages/firebase_core/firebase_core/pubspec.yaml b/packages/firebase_core/firebase_core/pubspec.yaml index 89f700a80ce6..30f18802c1dc 100644 --- a/packages/firebase_core/firebase_core/pubspec.yaml +++ b/packages/firebase_core/firebase_core/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Core, enabling connecting to multiple Firebase apps. homepage: https://firebase.google.com/docs/flutter/setup repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_core/firebase_core -version: 4.4.0 +version: 4.5.0 topics: - firebase - core @@ -17,7 +17,7 @@ environment: dependencies: firebase_core_platform_interface: ^6.0.2 - firebase_core_web: ^3.4.0 + firebase_core_web: ^3.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_core/firebase_core/windows/CMakeLists.txt b/packages/firebase_core/firebase_core/windows/CMakeLists.txt index 40a880ba426b..d944944c8c8a 100644 --- a/packages/firebase_core/firebase_core/windows/CMakeLists.txt +++ b/packages/firebase_core/firebase_core/windows/CMakeLists.txt @@ -4,7 +4,7 @@ # customers of the plugin. cmake_minimum_required(VERSION 3.14) -set(FIREBASE_SDK_VERSION "13.4.0") +set(FIREBASE_SDK_VERSION "13.5.0") if (EXISTS $ENV{FIREBASE_CPP_SDK_DIR}/include/firebase/version.h) file(READ "$ENV{FIREBASE_CPP_SDK_DIR}/include/firebase/version.h" existing_version) @@ -65,6 +65,8 @@ list(APPEND PLUGIN_SOURCES "firebase_core_plugin.h" "messages.g.cpp" "messages.g.h" + "flutter_firebase_plugin_registry.h" + "flutter_firebase_plugin_registry.cpp" ) # Read version from pubspec.yaml @@ -120,7 +122,7 @@ add_subdirectory(${FIREBASE_CPP_SDK_DIR} bin/ EXCLUDE_FROM_ALL) target_include_directories(${PLUGIN_NAME} INTERFACE "${FIREBASE_CPP_SDK_DIR}/include") -set(FIREBASE_RELEASE_PATH_LIBS firebase_app firebase_auth firebase_storage firebase_firestore) +set(FIREBASE_RELEASE_PATH_LIBS firebase_app firebase_auth firebase_remote_config firebase_storage firebase_firestore firebase_database) foreach(firebase_lib IN ITEMS ${FIREBASE_RELEASE_PATH_LIBS}) get_target_property(firebase_lib_path ${firebase_lib} IMPORTED_LOCATION) string(REPLACE "Debug" "Release" firebase_lib_release_path ${firebase_lib_path}) diff --git a/packages/firebase_core/firebase_core/windows/firebase_core_plugin.cpp b/packages/firebase_core/firebase_core/windows/firebase_core_plugin.cpp index b544e85ecb36..0644dc8cf7ea 100644 --- a/packages/firebase_core/firebase_core/windows/firebase_core_plugin.cpp +++ b/packages/firebase_core/firebase_core/windows/firebase_core_plugin.cpp @@ -9,6 +9,7 @@ #include "firebase/app.h" #include "firebase_core/plugin_version.h" +#include "flutter_firebase_plugin_registry.h" #include "messages.g.h" // For getPlatformVersion; remove unless needed for your plugin implementation. @@ -95,7 +96,8 @@ CoreFirebaseOptions optionsFromFIROptions(const firebase::AppOptions& options) { // Convert a firebase::App to CoreInitializeResponse CoreInitializeResponse AppToCoreInitializeResponse(const App& app) { - flutter::EncodableMap plugin_constants; + flutter::EncodableMap plugin_constants = + FlutterFirebasePluginRegistry::GetPluginConstantsForFirebaseApp(app); CoreInitializeResponse response = CoreInitializeResponse( app.name(), optionsFromFIROptions(app.options()), plugin_constants); return response; @@ -116,7 +118,11 @@ void FirebaseCorePlugin::InitializeApp( void FirebaseCorePlugin::InitializeCore( std::function reply)> result) { - // TODO: Missing function to get the list of currently initialized apps + if (coreInitialized) { + FlutterFirebasePluginRegistry::DidReinitializeFirebaseCore(); + } + coreInitialized = true; + std::vector initializedApps; std::vector all_apps = App::GetApps(); for (const App* app : all_apps) { diff --git a/packages/firebase_core/firebase_core/windows/firebase_core_plugin_c_api.cpp b/packages/firebase_core/firebase_core/windows/firebase_core_plugin_c_api.cpp index d8215be27a40..3615a59064b6 100644 --- a/packages/firebase_core/firebase_core/windows/firebase_core_plugin_c_api.cpp +++ b/packages/firebase_core/firebase_core/windows/firebase_core_plugin_c_api.cpp @@ -10,6 +10,7 @@ #include #include "firebase_core_plugin.h" +#include "flutter_firebase_plugin_registry.h" void FirebaseCorePluginCApiRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { @@ -17,3 +18,9 @@ void FirebaseCorePluginCApiRegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); } + +void RegisterFlutterFirebasePlugin(const std::string& channel_name, + FlutterFirebasePlugin* plugin) { + firebase_core_windows::FlutterFirebasePluginRegistry::RegisterPlugin( + channel_name, plugin); +} diff --git a/packages/firebase_core/firebase_core/windows/flutter_firebase_plugin_registry.cpp b/packages/firebase_core/firebase_core/windows/flutter_firebase_plugin_registry.cpp new file mode 100644 index 000000000000..3d4c1d80fe21 --- /dev/null +++ b/packages/firebase_core/firebase_core/windows/flutter_firebase_plugin_registry.cpp @@ -0,0 +1,39 @@ +// 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. + +#include "flutter_firebase_plugin_registry.h" + +namespace firebase_core_windows { + +std::unordered_map& +FlutterFirebasePluginRegistry::GetRegisteredPlugins() { + static std::unordered_map plugins; + return plugins; +} + +void FlutterFirebasePluginRegistry::RegisterPlugin( + const std::string& channel_name, FlutterFirebasePlugin* plugin) { + GetRegisteredPlugins()[channel_name] = plugin; +} + +flutter::EncodableMap +FlutterFirebasePluginRegistry::GetPluginConstantsForFirebaseApp( + const firebase::App& app) { + flutter::EncodableMap all_constants; + for (const auto& entry : GetRegisteredPlugins()) { + flutter::EncodableMap plugin_constants = + entry.second->GetPluginConstantsForFirebaseApp(app); + all_constants[flutter::EncodableValue(entry.first)] = + flutter::EncodableValue(plugin_constants); + } + return all_constants; +} + +void FlutterFirebasePluginRegistry::DidReinitializeFirebaseCore() { + for (const auto& entry : GetRegisteredPlugins()) { + entry.second->DidReinitializeFirebaseCore(); + } +} + +} // namespace firebase_core_windows diff --git a/packages/firebase_core/firebase_core/windows/flutter_firebase_plugin_registry.h b/packages/firebase_core/firebase_core/windows/flutter_firebase_plugin_registry.h new file mode 100644 index 000000000000..a6c5ddb3a068 --- /dev/null +++ b/packages/firebase_core/firebase_core/windows/flutter_firebase_plugin_registry.h @@ -0,0 +1,43 @@ +// 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. + +#ifndef FLUTTER_FIREBASE_PLUGIN_REGISTRY_H_ +#define FLUTTER_FIREBASE_PLUGIN_REGISTRY_H_ + +#include + +#include +#include + +#include "firebase/app.h" +#include "include/firebase_core/flutter_firebase_plugin.h" + +namespace firebase_core_windows { + +// Static registry that collects plugin constants from all registered Firebase +// plugins during initializeCore, mirroring Android's +// FlutterFirebasePluginRegistry. +class FlutterFirebasePluginRegistry { + public: + // Registers a plugin with the given channel name. + static void RegisterPlugin(const std::string& channel_name, + FlutterFirebasePlugin* plugin); + + // Collects constants from all registered plugins for the given app. + // Returns a map keyed by channel name, with each value being the plugin's + // constants map. + static flutter::EncodableMap GetPluginConstantsForFirebaseApp( + const firebase::App& app); + + // Notifies all registered plugins that Firebase core was re-initialized. + static void DidReinitializeFirebaseCore(); + + private: + static std::unordered_map& + GetRegisteredPlugins(); +}; + +} // namespace firebase_core_windows + +#endif // FLUTTER_FIREBASE_PLUGIN_REGISTRY_H_ diff --git a/packages/firebase_core/firebase_core/windows/include/firebase_core/firebase_core_plugin_c_api.h b/packages/firebase_core/firebase_core/windows/include/firebase_core/firebase_core_plugin_c_api.h index 68f3d1d314d6..93023e96a4c3 100644 --- a/packages/firebase_core/firebase_core/windows/include/firebase_core/firebase_core_plugin_c_api.h +++ b/packages/firebase_core/firebase_core/windows/include/firebase_core/firebase_core_plugin_c_api.h @@ -12,6 +12,8 @@ #include #include +#include "flutter_firebase_plugin.h" + #ifdef FLUTTER_PLUGIN_IMPL #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) #else @@ -21,4 +23,10 @@ FLUTTER_PLUGIN_EXPORT void FirebaseCorePluginCApiRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); +// Registers a FlutterFirebasePlugin so that its constants are collected during +// Firebase.initializeApp(). The channel_name should match the Dart +// MethodChannel name (e.g. "plugins.flutter.io/firebase_auth"). +FLUTTER_PLUGIN_EXPORT void RegisterFlutterFirebasePlugin( + const std::string& channel_name, FlutterFirebasePlugin* plugin); + #endif // FLUTTER_PLUGIN_FIREBASE_CORE_PLUGIN_C_API_H_ diff --git a/packages/firebase_core/firebase_core/windows/include/firebase_core/flutter_firebase_plugin.h b/packages/firebase_core/firebase_core/windows/include/firebase_core/flutter_firebase_plugin.h new file mode 100644 index 000000000000..ade655151cda --- /dev/null +++ b/packages/firebase_core/firebase_core/windows/include/firebase_core/flutter_firebase_plugin.h @@ -0,0 +1,29 @@ +// 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. + +#ifndef FLUTTER_FIREBASE_PLUGIN_H_ +#define FLUTTER_FIREBASE_PLUGIN_H_ + +#include + +#include "firebase/app.h" + +// Abstract interface mirroring Android's FlutterFirebasePlugin.java and iOS's +// FLTFirebasePlugin.h. Each Firebase plugin implements this to provide initial +// constants (e.g. current user) during Firebase.initializeApp(). +class FlutterFirebasePlugin { + public: + virtual ~FlutterFirebasePlugin() {} + + // Returns a map of plugin-specific constants for the given Firebase app. + // Called synchronously during initializeCore to populate pluginConstants. + virtual flutter::EncodableMap GetPluginConstantsForFirebaseApp( + const firebase::App& app) = 0; + + // Called when Firebase core is re-initialized, allowing plugins to reset + // their state. + virtual void DidReinitializeFirebaseCore() = 0; +}; + +#endif // FLUTTER_FIREBASE_PLUGIN_H_ diff --git a/packages/firebase_core/firebase_core_web/CHANGELOG.md b/packages/firebase_core/firebase_core_web/CHANGELOG.md index 5a1d2761e7ae..4f0c85a42b77 100644 --- a/packages/firebase_core/firebase_core_web/CHANGELOG.md +++ b/packages/firebase_core/firebase_core_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.5.0 + + - **FEAT**: bump Firebase JS SDK to 12.9.0 ([#18043](https://github.com/firebase/flutterfire/issues/18043)). ([1b29c4d4](https://github.com/firebase/flutterfire/commit/1b29c4d432597d12e08990825647f0ac9467a8f3)) + ## 3.4.0 - **FIX**(firebase_core,web): return empty list from apps getter in WASM mode ([#17919](https://github.com/firebase/flutterfire/issues/17919)). ([0eea9f81](https://github.com/firebase/flutterfire/commit/0eea9f814e7f8bace50e8c1e5973c231cf9a4e3a)) diff --git a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_version.dart b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_version.dart index c7f26e8f49c6..ab81ec074242 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_version.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '4.4.0'; +const packageVersion = '4.5.0'; diff --git a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart index b70f503d9ff3..5ec94d43b010 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart @@ -213,6 +213,14 @@ class FirebaseCoreWeb extends FirebasePlatform { if (ignored.contains(service.override ?? service.name)) { return Future.value(); } + final firestoreServiceName = 'firestore'; + + if (service.name == firestoreServiceName) { + return injectSrcScript( + 'https://www.gstatic.com/firebasejs/$version/firebase-firestore-pipelines.js', + 'firebase_$firestoreServiceName', + ); + } return injectSrcScript( 'https://www.gstatic.com/firebasejs/$version/firebase-${service.name}.js', diff --git a/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart b/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart index 6a43bfd5f478..3e04c6928134 100644 --- a/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart +++ b/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart @@ -6,4 +6,4 @@ part of '../firebase_core_web.dart'; /// The currently supported Firebase JS SDK version. -const String supportedFirebaseJsSdkVersion = '12.7.0'; +const String supportedFirebaseJsSdkVersion = '12.9.0'; diff --git a/packages/firebase_core/firebase_core_web/pubspec.yaml b/packages/firebase_core/firebase_core_web/pubspec.yaml index 147a38d9d9f4..e91ee92d82f1 100644 --- a/packages/firebase_core/firebase_core_web/pubspec.yaml +++ b/packages/firebase_core/firebase_core_web/pubspec.yaml @@ -2,7 +2,7 @@ name: firebase_core_web description: The web implementation of firebase_core homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_core/firebase_core_web repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_core/firebase_core_web -version: 3.4.0 +version: 3.5.0 environment: sdk: '>=3.4.0 <4.0.0' diff --git a/packages/firebase_crashlytics/firebase_crashlytics/CHANGELOG.md b/packages/firebase_crashlytics/firebase_crashlytics/CHANGELOG.md index e125985721f3..00c1620d1b3d 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/CHANGELOG.md +++ b/packages/firebase_crashlytics/firebase_crashlytics/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.8 + + - Update a dependency to the latest release. + ## 5.0.7 - Update a dependency to the latest release. diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.h b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.m b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.m index 59a72e90be12..9c45e766f906 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/AppDelegate.m @@ -5,9 +5,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/Info.plist b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/Info.plist index bb85eb3fe72b..8efef89a5b8a 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/Info.plist +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml b/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml index 6ca0411eb57a..7702022198d1 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml +++ b/packages/firebase_crashlytics/firebase_crashlytics/example/pubspec.yaml @@ -6,9 +6,9 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_analytics: ^12.1.2 - firebase_core: ^4.4.0 - firebase_crashlytics: ^5.0.7 + firebase_analytics: ^12.1.3 + firebase_core: ^4.5.0 + firebase_crashlytics: ^5.0.8 flutter: sdk: flutter diff --git a/packages/firebase_crashlytics/firebase_crashlytics/ios/generated_firebase_sdk_version.txt b/packages/firebase_crashlytics/firebase_crashlytics/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_crashlytics/firebase_crashlytics/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_crashlytics/firebase_crashlytics/pubspec.yaml b/packages/firebase_crashlytics/firebase_crashlytics/pubspec.yaml index 83df717b3776..beb36c017df5 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/pubspec.yaml +++ b/packages/firebase_crashlytics/firebase_crashlytics/pubspec.yaml @@ -2,7 +2,7 @@ name: firebase_crashlytics description: Flutter plugin for Firebase Crashlytics. It reports uncaught errors to the Firebase console. -version: 5.0.7 +version: 5.0.8 homepage: https://firebase.google.com/docs/crashlytics repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_crashlytics/firebase_crashlytics topics: @@ -19,9 +19,9 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_crashlytics_platform_interface: ^3.8.17 + firebase_crashlytics_platform_interface: ^3.8.18 flutter: sdk: flutter stack_trace: ^1.10.0 diff --git a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/CHANGELOG.md b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/CHANGELOG.md index 8c0910d5acb7..b2aa9a7d2c6a 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/CHANGELOG.md +++ b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.8.18 + + - Update a dependency to the latest release. + ## 3.8.17 - Update a dependency to the latest release. diff --git a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/pubspec.yaml b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/pubspec.yaml index c2d14b66ac80..ba32b6af7f28 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/pubspec.yaml +++ b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_crashlytics_platform_interface description: A common platform interface for the firebase_crashlytics plugin. -version: 3.8.17 +version: 3.8.18 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_crashlytics/firebase_crashlytics_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_crashlytics/firebase_crashlytics_platform_interface @@ -9,9 +9,9 @@ environment: flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 + _flutterfire_internals: ^1.3.67 collection: ^1.15.0 - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md b/packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md index fd908f0b3c95..6b2db45fb457 100644 --- a/packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md +++ b/packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.2.3 + + - **REFACTOR**(fdc): Support for entityId path extensions and hardening ([#17988](https://github.com/firebase/flutterfire/issues/17988)). ([fed585f5](https://github.com/firebase/flutterfire/commit/fed585f5a9b65d683cefdc7fa97ed2692e4ec817)) + - **FIX**: resolve lint issues ([#18017](https://github.com/firebase/flutterfire/issues/18017)). ([e8e85397](https://github.com/firebase/flutterfire/commit/e8e85397ccdcab6c8b84348884b4673f86b79d1c)) + - **FEAT**(fdc): Data Connect client sdk caching ([#17890](https://github.com/firebase/flutterfire/issues/17890)). ([02a019bc](https://github.com/firebase/flutterfire/commit/02a019bc25bb4a49d62c1079ed15e0c3aec8a5ec)) + ## 0.2.2+2 - Update a dependency to the latest release. diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist index fcfd18a5b8a3..f7b4b42c7db6 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist @@ -58,5 +58,26 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml index bb84d06a7c5a..dc796e7ec424 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml @@ -11,16 +11,16 @@ environment: dependencies: flutter: sdk: flutter - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 google_sign_in: ^6.1.0 - firebase_auth: ^6.1.4 + firebase_auth: ^6.2.0 firebase_data_connect: path: ../ cupertino_icons: ^1.0.6 flutter_rating_bar: ^4.0.1 protobuf: ^3.1.0 - firebase_app_check: ^0.4.1+4 + firebase_app_check: ^0.4.1+5 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/wasm_index.html b/packages/firebase_data_connect/firebase_data_connect/example/web/wasm_index.html new file mode 100644 index 000000000000..fd4dcfa432a6 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/web/wasm_index.html @@ -0,0 +1,14 @@ + + + + + Flutter web app + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/generate_proto.sh b/packages/firebase_data_connect/firebase_data_connect/generate_proto.sh index b8d3232eef71..5b3fc45dcaed 100755 --- a/packages/firebase_data_connect/firebase_data_connect/generate_proto.sh +++ b/packages/firebase_data_connect/firebase_data_connect/generate_proto.sh @@ -1,4 +1,9 @@ #!/bin/bash + +# Uses dart protoc_plugin version 21.1.2. There are compilation issues with newer plugin versions. +# https://github.com/google/protobuf.dart/releases/tag/protoc_plugin-v21.1.2 +# Run `pub global activate protoc_plugin 21.1.2` + rm -rf lib/src/generated mkdir lib/src/generated -protoc --dart_out=grpc:lib/src/generated -I./protos/firebase -I./protos/google connector_service.proto google/protobuf/struct.proto graphql_error.proto --proto_path=./protos +protoc --dart_out=grpc:lib/src/generated -I./protos/firebase -I./protos/google connector_service.proto google/protobuf/struct.proto google/protobuf/duration.proto graphql_error.proto graphql_response_extensions.proto --proto_path=./protos diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_manager.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache.dart similarity index 80% rename from packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_manager.dart rename to packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache.dart index b2cc1506161f..3983df6c5842 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_manager.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache.dart @@ -50,9 +50,13 @@ class Cache { Stream> get impactedQueries => _impactedQueryController.stream; String _constructCacheIdentifier() { - final rawIdentifier = - '${_settings.storage}-${dataConnect.app.options.projectId}-${dataConnect.app.name}-${dataConnect.connectorConfig.serviceId}-${dataConnect.connectorConfig.connector}-${dataConnect.connectorConfig.location}-${dataConnect.auth?.currentUser?.uid ?? 'anon'}-${dataConnect.transport.transportOptions.host}'; - return convertToSha256(rawIdentifier); + final rawPrefix = + '${_settings.storage}-${dataConnect.app.options.projectId}-${dataConnect.app.name}-${dataConnect.connectorConfig.serviceId}-${dataConnect.connectorConfig.connector}-${dataConnect.connectorConfig.location}-${dataConnect.transport.transportOptions.host}'; + final prefixSha = convertToSha256(rawPrefix); + final rawSuffix = dataConnect.auth?.currentUser?.uid ?? 'anon'; + final suffixSha = convertToSha256(rawSuffix); + + return '$prefixSha-$suffixSha'; } void _initializeProvider() { @@ -92,23 +96,32 @@ class Cache { return; } - final dehydrationResult = await _resultTreeProcessor.dehydrate( - queryId, serverResponse.data, _cacheProvider!); + final Map paths = + serverResponse.extensions != null + ? ExtensionResponse.fromJson(serverResponse.extensions!) + .flattenPathMetadata() + : {}; + + final dehydrationResult = await _resultTreeProcessor.dehydrateResults( + queryId, serverResponse.data, _cacheProvider!, paths); EntityNode rootNode = dehydrationResult.dehydratedTree; Map dehydratedMap = rootNode.toJson(mode: EncodingMode.dehydrated); // if we have server ttl, that overrides maxAge from cacheSettings - Duration ttl = - serverResponse.ttl != null ? serverResponse.ttl! : _settings.maxAge; + Duration ttl = serverResponse.extensions != null && + serverResponse.extensions!['ttl'] != null + ? Duration(seconds: serverResponse.extensions!['ttl'] as int) + : (serverResponse.ttl ?? _settings.maxAge); + final resultTree = ResultTree( data: dehydratedMap, ttl: ttl, cachedAt: DateTime.now(), lastAccessed: DateTime.now()); - _cacheProvider!.saveResultTree(queryId, resultTree); + _cacheProvider!.setResultTree(queryId, resultTree); Set impactedQueryIds = dehydrationResult.impactedQueryIds; impactedQueryIds.remove(queryId); // remove query being cached @@ -116,7 +129,8 @@ class Cache { } /// Fetches a cached result. - Future?> get(String queryId, bool allowStale) async { + Future?> resultTree( + String queryId, bool allowStale) async { if (_cacheProvider == null) { return null; } @@ -137,23 +151,20 @@ class Cache { } resultTree.lastAccessed = DateTime.now(); - _cacheProvider!.saveResultTree(queryId, resultTree); + _cacheProvider!.setResultTree(queryId, resultTree); EntityNode rootNode = EntityNode.fromJson(resultTree.data, _cacheProvider!); + Map hydratedJson = - rootNode.toJson(); //default mode for toJson is hydrate + await _resultTreeProcessor.hydrateResults(rootNode, _cacheProvider!); + return hydratedJson; } return null; } - /// Invalidates the cache. - Future invalidate() async { - _cacheProvider?.clear(); - } - void dispose() { _impactedQueryController.close(); } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart index ae80c237817a..0768da15232c 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart @@ -15,41 +15,144 @@ import 'dart:convert'; import 'package:firebase_data_connect/src/cache/cache_provider.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:firebase_data_connect/src/common/common_library.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart' show kIsWeb, listEquals; /// Type of storage to use for the cache enum CacheStorage { persistent, memory } -const String kGlobalIDKey = 'cacheId'; +const String kGlobalIDKey = 'guid'; + +@immutable +class DataConnectPath { + final List components; + + DataConnectPath([List? components]) + : components = components ?? []; + + DataConnectPath appending(DataConnectPathSegment segment) { + return DataConnectPath([...components, segment]); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DataConnectPath && + runtimeType == other.runtimeType && + listEquals(components, other.components); + + @override + int get hashCode => Object.hashAll(components); + + @override + String toString() => 'DataConnectPath($components)'; +} + +/// Additional information about object / field identified by a path +class PathMetadata { + final DataConnectPath path; + final String? entityId; + + PathMetadata({required this.path, this.entityId}); + + @override + String toString() { + return '$path : ${entityId ?? "null"}'; + } +} + +/// Represents the server response contained within the extension response +class PathMetadataResponse { + final List path; + final String? entityId; + final List? entityIds; + + PathMetadataResponse({required this.path, this.entityId, this.entityIds}); + + factory PathMetadataResponse.fromJson(Map json) { + return PathMetadataResponse( + path: (json['path'] as List).map(_parsePathSegment).toList(), + entityId: json['entityId'] as String?, + entityIds: (json['entityIds'] as List?)?.cast(), + ); + } +} + +DataConnectPathSegment _parsePathSegment(dynamic segment) { + if (segment is String) { + return DataConnectFieldPathSegment(segment); + } else if (segment is double || segment is int) { + int index = (segment is double) ? segment.toInt() : segment; + return DataConnectListIndexPathSegment(index); + } + throw ArgumentError('Invalid path segment type: ${segment.runtimeType}'); +} + +/// Represents the extension section within the server response +class ExtensionResponse { + final Duration? maxAge; + final List dataConnect; + + ExtensionResponse({this.maxAge, required this.dataConnect}); + + factory ExtensionResponse.fromJson(Map json) { + return ExtensionResponse( + maxAge: + json['ttl'] != null ? Duration(seconds: json['ttl'] as int) : null, + dataConnect: (json['dataConnect'] as List?) + ?.map((e) => + PathMetadataResponse.fromJson(e as Map)) + .toList() ?? + [], + ); + } + + Map flattenPathMetadata() { + final Map result = {}; + for (final pmr in dataConnect) { + if (pmr.entityId != null) { + final pm = PathMetadata( + path: DataConnectPath(pmr.path), entityId: pmr.entityId); + result[pm.path] = pm; + } + + if (pmr.entityIds != null) { + for (var i = 0; i < pmr.entityIds!.length; i++) { + final entityId = pmr.entityIds![i]; + final indexPath = DataConnectPath(pmr.path) + .appending(DataConnectListIndexPathSegment(i)); + final pm = PathMetadata(path: indexPath, entityId: entityId); + result[pm.path] = pm; + } + } + } + return result; + } +} /// Configuration for the cache class CacheSettings { /// The type of storage to use (e.g., "persistent", "memory") final CacheStorage storage; - /// The maximum size of the cache in bytes - final int maxSizeBytes; - /// Duration for which cache is used before revalidation with server final Duration maxAge; // Internal const constructor const CacheSettings._internal({ required this.storage, - required this.maxSizeBytes, required this.maxAge, }); // Factory constructor to handle the logic factory CacheSettings({ CacheStorage? storage, - int? maxSizeBytes, Duration maxAge = Duration.zero, }) { return CacheSettings._internal( storage: storage ?? (kIsWeb ? CacheStorage.memory : CacheStorage.persistent), - maxSizeBytes: maxSizeBytes ?? (kIsWeb ? 40000000 : 100000000), maxAge: maxAge, ); } @@ -203,7 +306,7 @@ class EntityNode { Map json, CacheProvider cacheProvider) { EntityDataObject? entity; if (json[kGlobalIDKey] != null) { - entity = cacheProvider.getEntityDataObject(json[kGlobalIDKey]); + entity = cacheProvider.getEntityData(json[kGlobalIDKey]); } Map? scalars; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_provider.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_provider.dart index 9f65aa127820..484e1e390a45 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_provider.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_provider.dart @@ -25,19 +25,16 @@ abstract class CacheProvider { Future initialize(); /// Stores a `ResultTree` object. - void saveResultTree(String queryId, ResultTree resultTree); + void setResultTree(String queryId, ResultTree resultTree); /// Retrieves a `ResultTree` object. ResultTree? getResultTree(String queryId); /// Stores an `EntityDataObject` object. - void saveEntityDataObject(EntityDataObject edo); + void updateEntityData(EntityDataObject edo); /// Retrieves an `EntityDataObject` object. - EntityDataObject getEntityDataObject(String guid); - - /// Manages the cache size and eviction policies. - void manageCacheSize(); + EntityDataObject getEntityData(String guid); /// Clears all data from the cache. void clear(); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/in_memory_cache_provider.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/in_memory_cache_provider.dart index b625921bbe05..cd44d4e25ac5 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/in_memory_cache_provider.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/in_memory_cache_provider.dart @@ -16,6 +16,7 @@ import 'cache_data_types.dart'; import 'cache_provider.dart'; /// An in-memory implementation of the `CacheProvider`. +/// This is used for the web platform class InMemoryCacheProvider implements CacheProvider { final Map _resultTrees = {}; final Map _edos = {}; @@ -31,12 +32,12 @@ class InMemoryCacheProvider implements CacheProvider { @override Future initialize() async { - // nothing to be intialized. + // nothing to be intialized return true; } @override - void saveResultTree(String queryId, ResultTree resultTree) { + void setResultTree(String queryId, ResultTree resultTree) { _resultTrees[queryId] = resultTree; } @@ -46,20 +47,15 @@ class InMemoryCacheProvider implements CacheProvider { } @override - void saveEntityDataObject(EntityDataObject edo) { + void updateEntityData(EntityDataObject edo) { _edos[edo.guid] = edo; } @override - EntityDataObject getEntityDataObject(String guid) { + EntityDataObject getEntityData(String guid) { return _edos.putIfAbsent(guid, () => EntityDataObject(guid: guid)); } - @override - void manageCacheSize() { - // In-memory cache doesn't have a size limit in this implementation. - } - @override void clear() { _resultTrees.clear(); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart index 34b557c31d71..ce4bf1bad24a 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:developer' as developer; + import '../common/common_library.dart'; import 'cache_data_types.dart'; import 'cache_provider.dart'; @@ -27,76 +29,109 @@ class DehydrationResult { class ResultTreeProcessor { /// Takes a server response, traverses the data, creates or updates `EntityDataObject`s, /// and builds a dehydrated `EntityNode` tree. - Future dehydrate(String queryId, - Map serverResponse, CacheProvider cacheProvider) async { + Future dehydrateResults( + String queryId, + Map serverResponse, + CacheProvider cacheProvider, + Map paths) async { final impactedQueryIds = {}; Map jsonData = serverResponse; if (serverResponse.containsKey('data')) { jsonData = serverResponse['data']; } - final rootNode = - _dehydrateNode(queryId, jsonData, cacheProvider, impactedQueryIds); + final rootNode = _dehydrateNode(queryId, jsonData, cacheProvider, + impactedQueryIds, DataConnectPath(), paths); return DehydrationResult(rootNode, impactedQueryIds); } - EntityNode _dehydrateNode(String queryId, dynamic data, - CacheProvider cacheProvider, Set impactedQueryIds) { + EntityNode _dehydrateNode( + String queryId, + dynamic data, + CacheProvider cacheProvider, + Set impactedQueryIds, + DataConnectPath path, + Map paths) { if (data is Map) { - // data contains a unique entity id. we can normalize - final guid = data[kGlobalIDKey] as String?; + // Look up entityId for current path + String? guid; + if (paths.containsKey(path)) { + guid = paths[path]?.entityId; + } - final serverValues = {}; + final scalarValues = {}; // scalars final nestedObjects = {}; final nestedObjectLists = >{}; for (final entry in data.entries) { final key = entry.key; final value = entry.value; - if (value is Map) { - EntityNode en = - _dehydrateNode(queryId, value, cacheProvider, impactedQueryIds); + //developer.log('detected Map for $key'); + EntityNode en = _dehydrateNode( + queryId, + value, + cacheProvider, + impactedQueryIds, + path.appending(DataConnectFieldPathSegment(key)), + paths); nestedObjects[key] = en; } else if (value is List) { + //developer.log('detected List for $key'); final nodeList = []; final scalarValueList = []; - for (final item in value) { + for (var i = 0; i < value.length; i++) { + final item = value[i]; if (item is Map) { nodeList.add(_dehydrateNode( - queryId, item, cacheProvider, impactedQueryIds)); + queryId, + item, + cacheProvider, + impactedQueryIds, + path + .appending(DataConnectFieldPathSegment(key)) + .appending(DataConnectListIndexPathSegment(i)), + paths)); } else { // assuming scalar - we don't handle array of arrays scalarValueList.add(item); } } - - // we either do object lists or scalar lists stored with scalars - // we don't handle mixed lists. - if (nodeList.isNotEmpty) { + // we either normalize object lists or scalar lists stored with scalars + // we don't normalize mixed lists. We store them as-is for reconstruction from cache. + if (nodeList.isNotEmpty && scalarValueList.isNotEmpty) { + // mixed type array - we directly store the json as-is + developer + .log('detected mixed type array for key $key. storing as-is'); + scalarValues[key] = value; + } else if (nodeList.isNotEmpty) { nestedObjectLists[key] = nodeList; + } else if (scalarValueList.isNotEmpty) { + scalarValues[key] = scalarValueList; } else { - serverValues[key] = scalarValueList; + // we have empty array. save key as scalar since we can't determine type + scalarValues[key] = value; } + // end list handling } else { - serverValues[key] = value; + //developer.log('detected Scalar for $key'); + scalarValues[key] = value; } } if (guid != null) { - final existingEdo = cacheProvider.getEntityDataObject(guid); - existingEdo.setServerValues(serverValues, queryId); - cacheProvider.saveEntityDataObject(existingEdo); + final existingEdo = cacheProvider.getEntityData(guid); + existingEdo.setServerValues(scalarValues, queryId); + cacheProvider.updateEntityData(existingEdo); impactedQueryIds.addAll(existingEdo.referencedFrom); - return EntityNode( entity: existingEdo, nestedObjects: nestedObjects, nestedObjectLists: nestedObjectLists); } else { return EntityNode( - scalarValues: serverValues, + scalarValues: scalarValues, nestedObjects: nestedObjects, nestedObjectLists: nestedObjectLists); } @@ -108,40 +143,8 @@ class ResultTreeProcessor { /// Takes a dehydrated `EntityNode` tree, fetches the corresponding `EntityDataObject`s /// from the `CacheProvider`, and reconstructs the original data structure. - Future> hydrate( + Future> hydrateResults( EntityNode dehydratedTree, CacheProvider cacheProvider) async { - return await _hydrateNode(dehydratedTree, cacheProvider) - as Map; - } - - Future _hydrateNode( - EntityNode node, CacheProvider cacheProvider) async { - final Map data = {}; - if (node.entity != null) { - final edo = cacheProvider.getEntityDataObject(node.entity!.guid); - data.addAll(edo.fields()); - } - - if (node.scalarValues != null) { - data.addAll(node.scalarValues!); - } - - if (node.nestedObjects != null) { - for (final entry in node.nestedObjects!.entries) { - data[entry.key] = await _hydrateNode(entry.value, cacheProvider); - } - } - - if (node.nestedObjectLists != null) { - for (final entry in node.nestedObjectLists!.entries) { - final list = []; - for (final item in entry.value) { - list.add(await _hydrateNode(item, cacheProvider)); - } - data[entry.key] = list; - } - } - - return data; + return dehydratedTree.toJson(); //default mode for toJson is hydrate } } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/sqlite_cache_provider.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/sqlite_cache_provider.dart index 3de7c1b44c95..1493f32a05ac 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/sqlite_cache_provider.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/sqlite_cache_provider.dart @@ -39,7 +39,19 @@ class SQLite3CacheProvider implements CacheProvider { final path = join(dbPath.path, '$_identifier.db'); _db = sqlite3.open(path); } - _createTables(); + + int curVersion = _getDatabaseVersion(); + if (curVersion == 0) { + _createTables(); + } else { + int major = curVersion ~/ 1000000; + if (major != 1) { + developer.log( + 'Unsupported schema major version $major detected. Expected 1'); + return false; + } + } + return true; } catch (e) { developer.log('Error initializing SQLiteProvider $e'); @@ -47,19 +59,37 @@ class SQLite3CacheProvider implements CacheProvider { } } + int _getDatabaseVersion() { + final resultSet = _db.select('PRAGMA user_version;'); + return resultSet.first.columnAt(0) as int; + } + + void _setDatabaseVersion(int version) { + _db.execute('PRAGMA user_version = $version;'); + } + void _createTables() { - _db.execute(''' - CREATE TABLE IF NOT EXISTS $resultTreeTable ( - query_id TEXT PRIMARY KEY, - result_tree TEXT - ); - '''); - _db.execute(''' - CREATE TABLE IF NOT EXISTS $entityDataTable ( - guid TEXT PRIMARY KEY, - entity_data_object TEXT - ); - '''); + _db.execute('BEGIN TRANSACTION'); + try { + _db.execute(''' + CREATE TABLE IF NOT EXISTS $resultTreeTable ( + query_id TEXT PRIMARY KEY NOT NULL, + last_accessed REAL NOT NULL, + data TEXT NOT NULL + ); + '''); + _db.execute(''' + CREATE TABLE IF NOT EXISTS $entityDataTable ( + entity_guid TEXT PRIMARY KEY NOT NULL, + data TEXT NOT NULL + ); + '''); + _setDatabaseVersion(1000000); // 1.0.0 + _db.execute('COMMIT'); + } catch (_) { + _db.execute('ROLLBACK'); + rethrow; + } } @override @@ -69,57 +99,84 @@ class SQLite3CacheProvider implements CacheProvider { @override void clear() { - _db.execute('DELETE FROM $resultTreeTable'); - _db.execute('DELETE FROM $entityDataTable'); + _db.execute('BEGIN TRANSACTION'); + try { + _db.execute('DELETE FROM $resultTreeTable'); + _db.execute('DELETE FROM $entityDataTable'); + _db.execute('COMMIT'); + } catch (_) { + _db.execute('ROLLBACK'); + rethrow; + } } @override - EntityDataObject getEntityDataObject(String guid) { + EntityDataObject getEntityData(String guid) { final resultSet = _db.select( - 'SELECT entity_data_object FROM $entityDataTable WHERE guid = ?', + 'SELECT data FROM $entityDataTable WHERE entity_guid = ?', [guid], ); if (resultSet.isEmpty) { - // not found lets create an empty one. + // not found lets create an empty one EntityDataObject edo = EntityDataObject(guid: guid); return edo; } - return EntityDataObject.fromRawJson( - resultSet.first['entity_data_object'] as String); + return EntityDataObject.fromRawJson(resultSet.first['data'] as String); } @override ResultTree? getResultTree(String queryId) { final resultSet = _db.select( - 'SELECT result_tree FROM $resultTreeTable WHERE query_id = ?', + 'SELECT data FROM $resultTreeTable WHERE query_id = ?', [queryId], ); if (resultSet.isEmpty) { return null; } - return ResultTree.fromRawJson(resultSet.first['result_tree'] as String); + _updateLastAccessedTime(queryId); + return ResultTree.fromRawJson(resultSet.first['data'] as String); } - @override - void manageCacheSize() { - // TODO: implement manageCacheSize + void _updateLastAccessedTime(String queryId) { + _db.execute( + 'UPDATE $resultTreeTable SET last_accessed = ? WHERE query_id = ?', + [DateTime.now().millisecondsSinceEpoch / 1000.0, queryId], + ); } @override - void saveEntityDataObject(EntityDataObject edo) { + void updateEntityData(EntityDataObject edo) { String rawJson = edo.toRawJson(); - _db.execute( - 'INSERT OR REPLACE INTO $entityDataTable (guid, entity_data_object) VALUES (?, ?)', - [edo.guid, rawJson], - ); + _db.execute('BEGIN TRANSACTION'); + try { + _db.execute( + 'INSERT OR REPLACE INTO $entityDataTable (entity_guid, data) VALUES (?, ?)', + [edo.guid, rawJson], + ); + _db.execute('COMMIT'); + } catch (_) { + _db.execute('ROLLBACK'); + rethrow; + } } @override - void saveResultTree(String queryId, ResultTree resultTree) { - _db.execute( - 'INSERT OR REPLACE INTO $resultTreeTable (query_id, result_tree) VALUES (?, ?)', - [queryId, resultTree.toRawJson()], - ); + void setResultTree(String queryId, ResultTree resultTree) { + _db.execute('BEGIN TRANSACTION'); + try { + _db.execute( + 'INSERT OR REPLACE INTO $resultTreeTable (query_id, last_accessed, data) VALUES (?, ?, ?)', + [ + queryId, + DateTime.now().millisecondsSinceEpoch / 1000.0, + resultTree.toRawJson() + ], + ); + _db.execute('COMMIT'); + } catch (_) { + _db.execute('ROLLBACK'); + rethrow; + } } } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart index e24e7a7e3b89..9247287f5adf 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart @@ -61,11 +61,18 @@ class TransportOptions { bool? isSecure; } +/// Encapsulates the response from server class ServerResponse { + /// Data returned from server final Map data; + + /// duration for which the results are considered not stale Duration? ttl; - ServerResponse(this.data); + /// Additional data provided in extensions + final Map? extensions; + + ServerResponse(this.data, {this.extensions}); } /// Interface for transports connecting to the DataConnect backend. diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart index 3928a9706536..43b7fd964418 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart @@ -58,16 +58,43 @@ class DataConnectOperationFailureResponseErrorInfo { } /// Path where error occurred. +@immutable sealed class DataConnectPathSegment {} class DataConnectFieldPathSegment extends DataConnectPathSegment { final String field; DataConnectFieldPathSegment(this.field); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DataConnectFieldPathSegment && + runtimeType == other.runtimeType && + field == other.field; + + @override + int get hashCode => field.hashCode; + + @override + String toString() => field; } class DataConnectListIndexPathSegment extends DataConnectPathSegment { final int index; DataConnectListIndexPathSegment(this.index); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DataConnectListIndexPathSegment && + runtimeType == other.runtimeType && + index == other.index; + + @override + int get hashCode => index.hashCode; + + @override + String toString() => index.toString(); } typedef Serializer = String Function(Variables vars); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart index e23a00134b70..5b359f6a15b3 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -251,7 +251,7 @@ class QueryRef extends OperationRef { final cacheManager = dataConnect.cacheManager!; bool allowStale = fetchPolicy == QueryFetchPolicy.cacheOnly; //if its cache only, we always allow stale - final cachedData = await cacheManager.get(_queryId, allowStale); + final cachedData = await cacheManager.resultTree(_queryId, allowStale); if (cachedData != null) { try { diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart index 436cdea88e95..8220eb06de2d 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// version number for the package, should be align with pubspec.yaml. -const packageVersion = '0.2.2+2'; +const packageVersion = '0.2.3'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart index 43cc4fb63e1a..d5813f94dc89 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart @@ -22,10 +22,11 @@ import 'package:flutter/foundation.dart'; import './network/transport_library.dart' if (dart.library.io) './network/grpc_library.dart' + if (dart.library.js_interop) './network/rest_library.dart' if (dart.library.html) './network/rest_library.dart'; import 'cache/cache_data_types.dart'; -import 'cache/cache_manager.dart'; +import 'cache/cache.dart'; /// DataConnect class class FirebaseDataConnect extends FirebasePluginPlatform { diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart index 54aed178ad40..1f38718bd4c9 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: connector_service.proto @@ -27,7 +14,8 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; import 'google/protobuf/struct.pb.dart' as $1; -import 'graphql_error.pb.dart' as $2; +import 'graphql_error.pb.dart' as $3; +import 'graphql_response_extensions.pb.dart' as $4; /// The ExecuteQuery request to Firebase Data Connect. class ExecuteQueryRequest extends $pb.GeneratedMessage { @@ -257,7 +245,8 @@ class ExecuteMutationRequest extends $pb.GeneratedMessage { class ExecuteQueryResponse extends $pb.GeneratedMessage { factory ExecuteQueryResponse({ $1.Struct? data, - $core.Iterable<$2.GraphqlError>? errors, + $core.Iterable<$3.GraphqlError>? errors, + $4.GraphqlResponseExtensions? extensions, }) { final $result = create(); if (data != null) { @@ -266,6 +255,9 @@ class ExecuteQueryResponse extends $pb.GeneratedMessage { if (errors != null) { $result.errors.addAll(errors); } + if (extensions != null) { + $result.extensions = extensions; + } return $result; } ExecuteQueryResponse._() : super(); @@ -283,9 +275,11 @@ class ExecuteQueryResponse extends $pb.GeneratedMessage { createEmptyInstance: create) ..aOM<$1.Struct>(1, _omitFieldNames ? '' : 'data', subBuilder: $1.Struct.create) - ..pc<$2.GraphqlError>( + ..pc<$3.GraphqlError>( 2, _omitFieldNames ? '' : 'errors', $pb.PbFieldType.PM, - subBuilder: $2.GraphqlError.create) + subBuilder: $3.GraphqlError.create) + ..aOM<$4.GraphqlResponseExtensions>(3, _omitFieldNames ? '' : 'extensions', + subBuilder: $4.GraphqlResponseExtensions.create) ..hasRequiredFields = false; @$core.Deprecated('Using this can add significant overhead to your binary. ' @@ -329,14 +323,30 @@ class ExecuteQueryResponse extends $pb.GeneratedMessage { /// Errors of this response. @$pb.TagNumber(2) - $core.List<$2.GraphqlError> get errors => $_getList(1); + $core.List<$3.GraphqlError> get errors => $_getList(1); + + /// Additional response information. + @$pb.TagNumber(3) + $4.GraphqlResponseExtensions get extensions => $_getN(2); + @$pb.TagNumber(3) + set extensions($4.GraphqlResponseExtensions v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasExtensions() => $_has(2); + @$pb.TagNumber(3) + void clearExtensions() => clearField(3); + @$pb.TagNumber(3) + $4.GraphqlResponseExtensions ensureExtensions() => $_ensure(2); } /// The ExecuteMutation response from Firebase Data Connect. class ExecuteMutationResponse extends $pb.GeneratedMessage { factory ExecuteMutationResponse({ $1.Struct? data, - $core.Iterable<$2.GraphqlError>? errors, + $core.Iterable<$3.GraphqlError>? errors, + $4.GraphqlResponseExtensions? extensions, }) { final $result = create(); if (data != null) { @@ -345,6 +355,9 @@ class ExecuteMutationResponse extends $pb.GeneratedMessage { if (errors != null) { $result.errors.addAll(errors); } + if (extensions != null) { + $result.extensions = extensions; + } return $result; } ExecuteMutationResponse._() : super(); @@ -362,9 +375,11 @@ class ExecuteMutationResponse extends $pb.GeneratedMessage { createEmptyInstance: create) ..aOM<$1.Struct>(1, _omitFieldNames ? '' : 'data', subBuilder: $1.Struct.create) - ..pc<$2.GraphqlError>( + ..pc<$3.GraphqlError>( 2, _omitFieldNames ? '' : 'errors', $pb.PbFieldType.PM, - subBuilder: $2.GraphqlError.create) + subBuilder: $3.GraphqlError.create) + ..aOM<$4.GraphqlResponseExtensions>(3, _omitFieldNames ? '' : 'extensions', + subBuilder: $4.GraphqlResponseExtensions.create) ..hasRequiredFields = false; @$core.Deprecated('Using this can add significant overhead to your binary. ' @@ -409,7 +424,22 @@ class ExecuteMutationResponse extends $pb.GeneratedMessage { /// Errors of this response. @$pb.TagNumber(2) - $core.List<$2.GraphqlError> get errors => $_getList(1); + $core.List<$3.GraphqlError> get errors => $_getList(1); + + /// Additional response information. + @$pb.TagNumber(3) + $4.GraphqlResponseExtensions get extensions => $_getN(2); + @$pb.TagNumber(3) + set extensions($4.GraphqlResponseExtensions v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasExtensions() => $_has(2); + @$pb.TagNumber(3) + void clearExtensions() => clearField(3); + @$pb.TagNumber(3) + $4.GraphqlResponseExtensions ensureExtensions() => $_ensure(2); } const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart index d53ea6876082..aabd1a01d514 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: connector_service.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart index b6704e57cca1..8b1732a117ae 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: connector_service.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart index 834e9aad147e..03a497345922 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: connector_service.proto @@ -108,6 +95,14 @@ const ExecuteQueryResponse$json = { '6': '.google.firebase.dataconnect.v1.GraphqlError', '10': 'errors' }, + { + '1': 'extensions', + '3': 3, + '4': 1, + '5': 11, + '6': '.google.firebase.dataconnect.v1.GraphqlResponseExtensions', + '10': 'extensions' + }, ], }; @@ -115,7 +110,9 @@ const ExecuteQueryResponse$json = { final $typed_data.Uint8List executeQueryResponseDescriptor = $convert.base64Decode( 'ChRFeGVjdXRlUXVlcnlSZXNwb25zZRIrCgRkYXRhGAEgASgLMhcuZ29vZ2xlLnByb3RvYnVmLl' 'N0cnVjdFIEZGF0YRJECgZlcnJvcnMYAiADKAsyLC5nb29nbGUuZmlyZWJhc2UuZGF0YWNvbm5l' - 'Y3QudjEuR3JhcGhxbEVycm9yUgZlcnJvcnM='); + 'Y3QudjEuR3JhcGhxbEVycm9yUgZlcnJvcnMSWQoKZXh0ZW5zaW9ucxgDIAEoCzI5Lmdvb2dsZS' + '5maXJlYmFzZS5kYXRhY29ubmVjdC52MS5HcmFwaHFsUmVzcG9uc2VFeHRlbnNpb25zUgpleHRl' + 'bnNpb25z'); @$core.Deprecated('Use executeMutationResponseDescriptor instead') const ExecuteMutationResponse$json = { @@ -137,6 +134,14 @@ const ExecuteMutationResponse$json = { '6': '.google.firebase.dataconnect.v1.GraphqlError', '10': 'errors' }, + { + '1': 'extensions', + '3': 3, + '4': 1, + '5': 11, + '6': '.google.firebase.dataconnect.v1.GraphqlResponseExtensions', + '10': 'extensions' + }, ], }; @@ -144,4 +149,6 @@ const ExecuteMutationResponse$json = { final $typed_data.Uint8List executeMutationResponseDescriptor = $convert.base64Decode( 'ChdFeGVjdXRlTXV0YXRpb25SZXNwb25zZRIrCgRkYXRhGAEgASgLMhcuZ29vZ2xlLnByb3RvYn' 'VmLlN0cnVjdFIEZGF0YRJECgZlcnJvcnMYAiADKAsyLC5nb29nbGUuZmlyZWJhc2UuZGF0YWNv' - 'bm5lY3QudjEuR3JhcGhxbEVycm9yUgZlcnJvcnM='); + 'bm5lY3QudjEuR3JhcGhxbEVycm9yUgZlcnJvcnMSWQoKZXh0ZW5zaW9ucxgDIAEoCzI5Lmdvb2' + 'dsZS5maXJlYmFzZS5kYXRhY29ubmVjdC52MS5HcmFwaHFsUmVzcG9uc2VFeHRlbnNpb25zUgpl' + 'eHRlbnNpb25z'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pb.dart new file mode 100644 index 000000000000..4bcbcd32a4c2 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pb.dart @@ -0,0 +1,166 @@ +// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +/// A Duration represents a signed, fixed-length span of time represented +/// as a count of seconds and fractions of seconds at nanosecond +/// resolution. It is independent of any calendar and concepts like "day" +/// or "month". It is related to Timestamp in that the difference between +/// two Timestamp values is a Duration and it can be added or subtracted +/// from a Timestamp. Range is approximately +-10,000 years. +/// +/// # Examples +/// +/// Example 1: Compute Duration from two Timestamps in pseudo code. +/// +/// Timestamp start = ...; +/// Timestamp end = ...; +/// Duration duration = ...; +/// +/// duration.seconds = end.seconds - start.seconds; +/// duration.nanos = end.nanos - start.nanos; +/// +/// if (duration.seconds < 0 && duration.nanos > 0) { +/// duration.seconds += 1; +/// duration.nanos -= 1000000000; +/// } else if (duration.seconds > 0 && duration.nanos < 0) { +/// duration.seconds -= 1; +/// duration.nanos += 1000000000; +/// } +/// +/// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. +/// +/// Timestamp start = ...; +/// Duration duration = ...; +/// Timestamp end = ...; +/// +/// end.seconds = start.seconds + duration.seconds; +/// end.nanos = start.nanos + duration.nanos; +/// +/// if (end.nanos < 0) { +/// end.seconds -= 1; +/// end.nanos += 1000000000; +/// } else if (end.nanos >= 1000000000) { +/// end.seconds += 1; +/// end.nanos -= 1000000000; +/// } +/// +/// Example 3: Compute Duration from datetime.timedelta in Python. +/// +/// td = datetime.timedelta(days=3, minutes=10) +/// duration = Duration() +/// duration.FromTimedelta(td) +/// +/// # JSON Mapping +/// +/// In JSON format, the Duration type is encoded as a string rather than an +/// object, where the string ends in the suffix "s" (indicating seconds) and +/// is preceded by the number of seconds, with nanoseconds expressed as +/// fractional seconds. For example, 3 seconds with 0 nanoseconds should be +/// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should +/// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 +/// microsecond should be expressed in JSON format as "3.000001s". +class Duration extends $pb.GeneratedMessage with $mixin.DurationMixin { + factory Duration({ + $fixnum.Int64? seconds, + $core.int? nanos, + }) { + final $result = create(); + if (seconds != null) { + $result.seconds = seconds; + } + if (nanos != null) { + $result.nanos = nanos; + } + return $result; + } + Duration._() : super(); + factory Duration.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Duration.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Duration', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.DurationMixin.toProto3JsonHelper, + fromProto3Json: $mixin.DurationMixin.fromProto3JsonHelper) + ..aInt64(1, _omitFieldNames ? '' : 'seconds') + ..a<$core.int>(2, _omitFieldNames ? '' : 'nanos', $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Duration clone() => Duration()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Duration copyWith(void Function(Duration) updates) => + super.copyWith((message) => updates(message as Duration)) as Duration; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Duration create() => Duration._(); + Duration createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Duration getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Duration? _defaultInstance; + + /// Signed seconds of the span of time. Must be from -315,576,000,000 + /// to +315,576,000,000 inclusive. Note: these bounds are computed from: + /// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + @$pb.TagNumber(1) + $fixnum.Int64 get seconds => $_getI64(0); + @$pb.TagNumber(1) + set seconds($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasSeconds() => $_has(0); + @$pb.TagNumber(1) + void clearSeconds() => clearField(1); + + /// Signed fractions of a second at nanosecond resolution of the span + /// of time. Durations less than one second are represented with a 0 + /// `seconds` field and a positive or negative `nanos` field. For durations + /// of one second or more, a non-zero value for the `nanos` field must be + /// of the same sign as the `seconds` field. Must be from -999,999,999 + /// to +999,999,999 inclusive. + @$pb.TagNumber(2) + $core.int get nanos => $_getIZ(1); + @$pb.TagNumber(2) + set nanos($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNanos() => $_has(1); + @$pb.TagNumber(2) + void clearNanos() => clearField(2); +} + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pbenum.dart new file mode 100644 index 000000000000..1a2c58d81056 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pbenum.dart @@ -0,0 +1,10 @@ +// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pbjson.dart new file mode 100644 index 000000000000..5847acb2d458 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pbjson.dart @@ -0,0 +1,28 @@ +// +// Generated code. Do not modify. +// source: google/protobuf/duration.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use durationDescriptor instead') +const Duration$json = { + '1': 'Duration', + '2': [ + {'1': 'seconds', '3': 1, '4': 1, '5': 3, '10': 'seconds'}, + {'1': 'nanos', '3': 2, '4': 1, '5': 5, '10': 'nanos'}, + ], +}; + +/// Descriptor for `Duration`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List durationDescriptor = $convert.base64Decode( + 'CghEdXJhdGlvbhIYCgdzZWNvbmRzGAEgASgDUgdzZWNvbmRzEhQKBW5hbm9zGAIgASgFUgVuYW' + '5vcw=='); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart index 7b9093d681ff..42d55e426602 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: google/protobuf/struct.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart index b5acd2512df2..7f9bf0cbf322 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: google/protobuf/struct.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart index 3f53dbf0a988..c0693f570058 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: google/protobuf/struct.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart index a50398e29488..2def4cc62994 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: graphql_error.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart index 9f28e16d3c23..53454c94a217 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: graphql_error.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart index ae48a28388dc..9a90ffc79685 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart @@ -1,16 +1,3 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. // // Generated code. Do not modify. // source: graphql_error.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pb.dart new file mode 100644 index 000000000000..c86ba89dbd75 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pb.dart @@ -0,0 +1,222 @@ +// +// Generated code. Do not modify. +// source: graphql_response_extensions.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'google/protobuf/duration.pb.dart' as $2; +import 'google/protobuf/struct.pb.dart' as $1; + +/// Data Connect specific properties for a path under response.data. +/// (-- Design doc: http://go/fdc-caching-wire-protocol --) +class GraphqlResponseExtensions_DataConnectProperties + extends $pb.GeneratedMessage { + factory GraphqlResponseExtensions_DataConnectProperties({ + $1.ListValue? path, + $core.String? entityId, + $core.Iterable<$core.String>? entityIds, + $2.Duration? maxAge, + }) { + final $result = create(); + if (path != null) { + $result.path = path; + } + if (entityId != null) { + $result.entityId = entityId; + } + if (entityIds != null) { + $result.entityIds.addAll(entityIds); + } + if (maxAge != null) { + $result.maxAge = maxAge; + } + return $result; + } + GraphqlResponseExtensions_DataConnectProperties._() : super(); + factory GraphqlResponseExtensions_DataConnectProperties.fromBuffer( + $core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory GraphqlResponseExtensions_DataConnectProperties.fromJson( + $core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames + ? '' + : 'GraphqlResponseExtensions.DataConnectProperties', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1'), + createEmptyInstance: create) + ..aOM<$1.ListValue>(1, _omitFieldNames ? '' : 'path', + subBuilder: $1.ListValue.create) + ..aOS(2, _omitFieldNames ? '' : 'entityId') + ..pPS(3, _omitFieldNames ? '' : 'entityIds') + ..aOM<$2.Duration>(4, _omitFieldNames ? '' : 'maxAge', + subBuilder: $2.Duration.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GraphqlResponseExtensions_DataConnectProperties clone() => + GraphqlResponseExtensions_DataConnectProperties()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GraphqlResponseExtensions_DataConnectProperties copyWith( + void Function(GraphqlResponseExtensions_DataConnectProperties) + updates) => + super.copyWith((message) => updates( + message as GraphqlResponseExtensions_DataConnectProperties)) + as GraphqlResponseExtensions_DataConnectProperties; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GraphqlResponseExtensions_DataConnectProperties create() => + GraphqlResponseExtensions_DataConnectProperties._(); + GraphqlResponseExtensions_DataConnectProperties createEmptyInstance() => + create(); + static $pb.PbList + createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GraphqlResponseExtensions_DataConnectProperties getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + GraphqlResponseExtensions_DataConnectProperties>(create); + static GraphqlResponseExtensions_DataConnectProperties? _defaultInstance; + + /// The path under response.data where the rest of the fields apply. + /// Each element may be a string (field name) or number (array index). + /// The root of response.data is denoted by the empty list `[]`. + /// (-- To simplify client logic, the server should never set this to null. + /// i.e. Use `[]` if the properties below apply to everything in data. --) + @$pb.TagNumber(1) + $1.ListValue get path => $_getN(0); + @$pb.TagNumber(1) + set path($1.ListValue v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasPath() => $_has(0); + @$pb.TagNumber(1) + void clearPath() => clearField(1); + @$pb.TagNumber(1) + $1.ListValue ensurePath() => $_ensure(0); + + /// A single Entity ID. Set if the path points to a single entity. + @$pb.TagNumber(2) + $core.String get entityId => $_getSZ(1); + @$pb.TagNumber(2) + set entityId($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasEntityId() => $_has(1); + @$pb.TagNumber(2) + void clearEntityId() => clearField(2); + + /// A list of Entity IDs. Set if the path points to an array of entities. An + /// ID is present for each element of the array at the corresponding index. + @$pb.TagNumber(3) + $core.List<$core.String> get entityIds => $_getList(2); + + /// The server-suggested duration before data under path is considered stale. + /// (-- Right now, this field is never set. For future plans, see + /// http://go/fdc-sdk-caching-config#heading=h.rmvncy2rao3g --) + @$pb.TagNumber(4) + $2.Duration get maxAge => $_getN(3); + @$pb.TagNumber(4) + set maxAge($2.Duration v) { + setField(4, v); + } + + @$pb.TagNumber(4) + $core.bool hasMaxAge() => $_has(3); + @$pb.TagNumber(4) + void clearMaxAge() => clearField(4); + @$pb.TagNumber(4) + $2.Duration ensureMaxAge() => $_ensure(3); +} + +/// GraphqlResponseExtensions contains additional information of +/// `GraphqlResponse` or `ExecuteQueryResponse`. +class GraphqlResponseExtensions extends $pb.GeneratedMessage { + factory GraphqlResponseExtensions({ + $core.Iterable? + dataConnect, + }) { + final $result = create(); + if (dataConnect != null) { + $result.dataConnect.addAll(dataConnect); + } + return $result; + } + GraphqlResponseExtensions._() : super(); + factory GraphqlResponseExtensions.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory GraphqlResponseExtensions.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GraphqlResponseExtensions', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1'), + createEmptyInstance: create) + ..pc( + 1, _omitFieldNames ? '' : 'dataConnect', $pb.PbFieldType.PM, + subBuilder: GraphqlResponseExtensions_DataConnectProperties.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GraphqlResponseExtensions clone() => + GraphqlResponseExtensions()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GraphqlResponseExtensions copyWith( + void Function(GraphqlResponseExtensions) updates) => + super.copyWith((message) => updates(message as GraphqlResponseExtensions)) + as GraphqlResponseExtensions; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GraphqlResponseExtensions create() => GraphqlResponseExtensions._(); + GraphqlResponseExtensions createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GraphqlResponseExtensions getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GraphqlResponseExtensions? _defaultInstance; + + /// Data Connect specific GraphQL extension, a list of paths and properties. + /// (-- Future fields should go inside to avoid name conflicts with other GQL + /// extensions in the wild unless we're implementing a common 3P pattern in + /// extensions such as versioning and telemetry. --) + @$pb.TagNumber(1) + $core.List get dataConnect => + $_getList(0); +} + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pbenum.dart new file mode 100644 index 000000000000..924da2c849bb --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pbenum.dart @@ -0,0 +1,10 @@ +// +// Generated code. Do not modify. +// source: graphql_response_extensions.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pbjson.dart new file mode 100644 index 000000000000..a1022d1267e2 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_response_extensions.pbjson.dart @@ -0,0 +1,65 @@ +// +// Generated code. Do not modify. +// source: graphql_response_extensions.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use graphqlResponseExtensionsDescriptor instead') +const GraphqlResponseExtensions$json = { + '1': 'GraphqlResponseExtensions', + '2': [ + { + '1': 'data_connect', + '3': 1, + '4': 3, + '5': 11, + '6': + '.google.firebase.dataconnect.v1.GraphqlResponseExtensions.DataConnectProperties', + '10': 'dataConnect' + }, + ], + '3': [GraphqlResponseExtensions_DataConnectProperties$json], +}; + +@$core.Deprecated('Use graphqlResponseExtensionsDescriptor instead') +const GraphqlResponseExtensions_DataConnectProperties$json = { + '1': 'DataConnectProperties', + '2': [ + { + '1': 'path', + '3': 1, + '4': 1, + '5': 11, + '6': '.google.protobuf.ListValue', + '10': 'path' + }, + {'1': 'entity_id', '3': 2, '4': 1, '5': 9, '10': 'entityId'}, + {'1': 'entity_ids', '3': 3, '4': 3, '5': 9, '10': 'entityIds'}, + { + '1': 'max_age', + '3': 4, + '4': 1, + '5': 11, + '6': '.google.protobuf.Duration', + '10': 'maxAge' + }, + ], +}; + +/// Descriptor for `GraphqlResponseExtensions`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List graphqlResponseExtensionsDescriptor = $convert.base64Decode( + 'ChlHcmFwaHFsUmVzcG9uc2VFeHRlbnNpb25zEnIKDGRhdGFfY29ubmVjdBgBIAMoCzJPLmdvb2' + 'dsZS5maXJlYmFzZS5kYXRhY29ubmVjdC52MS5HcmFwaHFsUmVzcG9uc2VFeHRlbnNpb25zLkRh' + 'dGFDb25uZWN0UHJvcGVydGllc1ILZGF0YUNvbm5lY3QatwEKFURhdGFDb25uZWN0UHJvcGVydG' + 'llcxIuCgRwYXRoGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLkxpc3RWYWx1ZVIEcGF0aBIbCgll' + 'bnRpdHlfaWQYAiABKAlSCGVudGl0eUlkEh0KCmVudGl0eV9pZHMYAyADKAlSCWVudGl0eUlkcx' + 'IyCgdtYXhfYWdlGAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uUgZtYXhBZ2U='); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart index e0c3e660333b..180bb209168b 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart @@ -173,6 +173,9 @@ ServerResponse handleResponse(CommonResponse commonResponse) { Map? jsond = commonResponse.data as Map?; String jsonEncoded = jsonEncode(commonResponse.data); + Map? jsonExt = + commonResponse.extensions as Map?; + if (commonResponse.errors.isNotEmpty) { Map? data = jsonDecode(jsonEncoded) as Map?; @@ -202,7 +205,7 @@ ServerResponse handleResponse(CommonResponse commonResponse) { // no errors - return a standard response if (jsond != null) { - return ServerResponse(jsond); + return ServerResponse(jsond, extensions: jsonExt); } else { return ServerResponse({}); } @@ -219,20 +222,21 @@ DataConnectTransport getTransport( GRPCTransport(transportOptions, options, appId, sdkType, appCheck); class CommonResponse { - CommonResponse(this.deserializer, this.data, this.errors); + CommonResponse(this.deserializer, this.data, this.errors, this.extensions); static CommonResponse fromExecuteMutation( Deserializer deserializer, ExecuteMutationResponse response) { return CommonResponse( - deserializer, response.data.toProto3Json(), response.errors); + deserializer, response.data.toProto3Json(), response.errors, null); } static CommonResponse fromExecuteQuery( Deserializer deserializer, ExecuteQueryResponse response) { - return CommonResponse( - deserializer, response.data.toProto3Json(), response.errors); + return CommonResponse(deserializer, response.data.toProto3Json(), + response.errors, response.extensions.toProto3Json()); } final Deserializer deserializer; final Object? data; final List errors; + final Object? extensions; } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart index 0ee037744ffd..708d6fa02010 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart @@ -117,6 +117,7 @@ class RestTransport implements DataConnectTransport { ); Map bodyJson = jsonDecode(r.body) as Map; + if (r.statusCode != 200) { String message = bodyJson.containsKey('message') ? bodyJson['message']! : r.body; @@ -127,7 +128,13 @@ class RestTransport implements DataConnectTransport { "Received a status code of ${r.statusCode} with a message '$message'", ); } - return ServerResponse(bodyJson); + final Map? extensions = + bodyJson['extensions'] as Map?; + final serverResponse = ServerResponse(bodyJson, extensions: extensions); + if (extensions != null && extensions.containsKey('ttl')) { + serverResponse.ttl = Duration(seconds: extensions['ttl'] as int); + } + return serverResponse; } on Exception catch (e) { if (e is DataConnectError) { rethrow; diff --git a/packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto b/packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto index a93b79234362..0d8e28c5221a 100644 --- a/packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto +++ b/packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto @@ -1,30 +1,29 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -// Adapted from http://google3/google/firebase/dataconnect/v1main/connector_service.proto;rcl=596717236 +// Adapted from third_party/firebase/dataconnect/emulator/server/api/connector_service.proto syntax = "proto3"; package google.firebase.dataconnect.v1; import "google/api/field_behavior.proto"; -import "graphql_error.proto"; import "google/protobuf/struct.proto"; +import "graphql_error.proto"; +import "graphql_response_extensions.proto"; -option java_package = "google.firebase.dataconnect.proto"; +option java_package = "com.google.firebase.dataconnect.api"; option java_multiple_files = true; // Firebase Data Connect provides means to deploy a set of predefined GraphQL @@ -40,12 +39,11 @@ option java_multiple_files = true; // token. service ConnectorService { // Execute a predefined query in a Connector. - rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) { - } + rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) {} // Execute a predefined mutation in a Connector. - rpc ExecuteMutation(ExecuteMutationRequest) returns (ExecuteMutationResponse) { - } + rpc ExecuteMutation(ExecuteMutationRequest) + returns (ExecuteMutationResponse) {} } // The ExecuteQuery request to Firebase Data Connect. @@ -94,6 +92,8 @@ message ExecuteQueryResponse { google.protobuf.Struct data = 1; // Errors of this response. repeated GraphqlError errors = 2; + // Additional response information. + GraphqlResponseExtensions extensions = 3; } // The ExecuteMutation response from Firebase Data Connect. @@ -102,4 +102,6 @@ message ExecuteMutationResponse { google.protobuf.Struct data = 1; // Errors of this response. repeated GraphqlError errors = 2; -} \ No newline at end of file + // Additional response information. + GraphqlResponseExtensions extensions = 3; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/protos/firebase/graphql_response_extensions.proto b/packages/firebase_data_connect/firebase_data_connect/protos/firebase/graphql_response_extensions.proto new file mode 100644 index 000000000000..e04e1927ada4 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/protos/firebase/graphql_response_extensions.proto @@ -0,0 +1,59 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Adapted from third_party/firebase/dataconnect/emulator/server/api/graphql_response_extensions.proto + +syntax = "proto3"; + +package google.firebase.dataconnect.v1; + +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; + +option java_multiple_files = true; +option java_outer_classname = "GraphqlResponseExtensionsProto"; +option java_package = "com.google.firebase.dataconnect.api"; + +// GraphqlResponseExtensions contains additional information of +// `GraphqlResponse` or `ExecuteQueryResponse`. +message GraphqlResponseExtensions { + // Data Connect specific properties for a path under response.data. + // (-- Design doc: http://go/fdc-caching-wire-protocol --) + message DataConnectProperties { + // The path under response.data where the rest of the fields apply. + // Each element may be a string (field name) or number (array index). + // The root of response.data is denoted by the empty list `[]`. + // (-- To simplify client logic, the server should never set this to null. + // i.e. Use `[]` if the properties below apply to everything in data. --) + google.protobuf.ListValue path = 1; + + // A single Entity ID. Set if the path points to a single entity. + string entity_id = 2; + + // A list of Entity IDs. Set if the path points to an array of entities. An + // ID is present for each element of the array at the corresponding index. + repeated string entity_ids = 3; + + // The server-suggested duration before data under path is considered stale. + // (-- Right now, this field is never set. For future plans, see + // http://go/fdc-sdk-caching-config#heading=h.rmvncy2rao3g --) + google.protobuf.Duration max_age = 4; + } + // Data Connect specific GraphQL extension, a list of paths and properties. + // (-- Future fields should go inside to avoid name conflicts with other GQL + // extensions in the wild unless we're implementing a common 3P pattern in + // extensions such as versioning and telemetry. --) + repeated DataConnectProperties data_connect = 1; +} + diff --git a/packages/firebase_data_connect/firebase_data_connect/protos/google/duration.proto b/packages/firebase_data_connect/firebase_data_connect/protos/google/duration.proto new file mode 100644 index 000000000000..cb7cf0e926cc --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/protos/google/duration.proto @@ -0,0 +1,116 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/durationpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DurationProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// A Duration represents a signed, fixed-length span of time represented +// as a count of seconds and fractions of seconds at nanosecond +// resolution. It is independent of any calendar and concepts like "day" +// or "month". It is related to Timestamp in that the difference between +// two Timestamp values is a Duration and it can be added or subtracted +// from a Timestamp. Range is approximately +-10,000 years. +// +// # Examples +// +// Example 1: Compute Duration from two Timestamps in pseudo code. +// +// Timestamp start = ...; +// Timestamp end = ...; +// Duration duration = ...; +// +// duration.seconds = end.seconds - start.seconds; +// duration.nanos = end.nanos - start.nanos; +// +// if (duration.seconds < 0 && duration.nanos > 0) { +// duration.seconds += 1; +// duration.nanos -= 1000000000; +// } else if (duration.seconds > 0 && duration.nanos < 0) { +// duration.seconds -= 1; +// duration.nanos += 1000000000; +// } +// +// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. +// +// Timestamp start = ...; +// Duration duration = ...; +// Timestamp end = ...; +// +// end.seconds = start.seconds + duration.seconds; +// end.nanos = start.nanos + duration.nanos; +// +// if (end.nanos < 0) { +// end.seconds -= 1; +// end.nanos += 1000000000; +// } else if (end.nanos >= 1000000000) { +// end.seconds += 1; +// end.nanos -= 1000000000; +// } +// +// Example 3: Compute Duration from datetime.timedelta in Python. +// +// td = datetime.timedelta(days=3, minutes=10) +// duration = Duration() +// duration.FromTimedelta(td) +// +// # JSON Mapping +// +// In JSON format, the Duration type is encoded as a string rather than an +// object, where the string ends in the suffix "s" (indicating seconds) and +// is preceded by the number of seconds, with nanoseconds expressed as +// fractional seconds. For example, 3 seconds with 0 nanoseconds should be +// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should +// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 +// microsecond should be expressed in JSON format as "3.000001s". +// +// +message Duration { + // Signed seconds of the span of time. Must be from -315,576,000,000 + // to +315,576,000,000 inclusive. Note: these bounds are computed from: + // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + int64 seconds = 1; + + // Signed fractions of a second at nanosecond resolution of the span + // of time. Durations less than one second are represented with a 0 + // `seconds` field and a positive or negative `nanos` field. For durations + // of one second or more, a non-zero value for the `nanos` field must be + // of the same sign as the `seconds` field. Must be from -999,999,999 + // to +999,999,999 inclusive. + int32 nanos = 2; +} \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml index e71288430a1e..014fa8b79b3d 100644 --- a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_data_connect description: 'Flutter plugin for Firebase Data Connect, a relational database service that lets you build and scale using a fully-managed PostgreSQL database powered by Cloud SQL.' -version: 0.2.2+2 +version: 0.2.3 homepage: https://firebase.google.com/docs/data-connect/quickstart?platform=flutter false_secrets: - example/** @@ -12,10 +12,11 @@ environment: dependencies: crypto: ^3.0.6 - firebase_app_check: ^0.4.1+3 - firebase_auth: ^6.1.3 - firebase_core: ^4.3.0 + firebase_app_check: ^0.4.1+5 + firebase_auth: ^6.2.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 + fixnum: ^1.1.1 flutter: sdk: flutter grpc: ^3.2.4 @@ -29,8 +30,8 @@ dependencies: dev_dependencies: build_runner: ^2.4.12 - firebase_app_check_platform_interface: ^0.2.1+4 - firebase_auth_platform_interface: ^8.1.6 + firebase_app_check_platform_interface: ^0.2.1+5 + firebase_auth_platform_interface: ^8.1.7 flutter_lints: ^4.0.0 flutter_test: sdk: flutter diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart index db40791ad2a0..dec53744b7e3 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart @@ -18,7 +18,7 @@ import 'package:firebase_data_connect/src/core/ref.dart'; import 'package:firebase_data_connect/src/network/rest_library.dart'; import 'package:firebase_data_connect/src/common/common_library.dart'; import 'package:firebase_data_connect/src/cache/cache_data_types.dart'; -import 'package:firebase_data_connect/src/cache/cache_manager.dart'; +import 'package:firebase_data_connect/src/cache/cache.dart'; import 'package:firebase_data_connect/src/cache/cache_provider.dart'; import 'package:firebase_data_connect/src/cache/in_memory_cache_provider.dart'; import 'package:firebase_data_connect/src/cache/sqlite_cache_provider.dart'; @@ -51,18 +51,27 @@ void main() { const String simpleQueryResponse = ''' {"data": {"items":[ - {"desc":"itemDesc1","name":"itemOne", "cacheId":"123","price":4}, - {"desc":"itemDesc2","name":"itemTwo", "cacheId":"345","price":7} + {"desc":"itemDesc1","name":"itemOne","price":4}, + {"desc":"itemDesc2","name":"itemTwo","price":7} ]}} '''; + final Map simpleQueryExtensions = { + 'dataConnect': [ + { + 'path': ['items'], + 'entityIds': ['123', '345'] + } + ] + }; + // query that updates the price for cacheId 123 to 11 const String simpleQueryResponseUpdate = ''' {"data": {"items":[ - {"desc":"itemDesc1","name":"itemOne", "cacheId":"123","price":11}, - {"desc":"itemDesc2","name":"itemTwo", "cacheId":"345","price":7} + {"desc":"itemDesc1","name":"itemOne","price":11}, + {"desc":"itemDesc2","name":"itemTwo","price":7} ]}} '''; @@ -70,10 +79,19 @@ void main() { // query two has same object as query one so should refer to same Entity. const String simpleQueryTwoResponse = ''' {"data": { - "item": { "desc":"itemDesc1","name":"itemOne", "cacheId":"123","price":4 } + "item": { "desc":"itemDesc1","name":"itemOne","price":4 } }} '''; + final Map simpleQueryTwoExtensions = { + 'dataConnect': [ + { + 'path': ['item'], + 'entityId': '123' + } + ] + }; + group('Cache Provider Tests', () { setUp(() async { mockApp = MockFirebaseApp(); @@ -126,9 +144,11 @@ void main() { Map jsonData = jsonDecode(simpleQueryResponse) as Map; - await cache.update('itemsSimple', ServerResponse(jsonData)); + await cache.update('itemsSimple', + ServerResponse(jsonData, extensions: simpleQueryExtensions)); - Map? cachedData = await cache.get('itemsSimple', true); + Map? cachedData = + await cache.resultTree('itemsSimple', true); expect(jsonData['data'], cachedData); }); // test set get @@ -144,8 +164,8 @@ void main() { edo.updateServerValue('name', 'test', null); edo.updateServerValue('desc', 'testDesc', null); - cp.saveEntityDataObject(edo); - EntityDataObject edo2 = cp.getEntityDataObject('1234'); + cp.updateEntityData(edo); + EntityDataObject edo2 = cp.getEntityData('1234'); expect(edo.fields().length, edo2.fields().length); expect(edo.fields()['name'], edo2.fields()['name']); @@ -162,21 +182,24 @@ void main() { Map jsonDataOne = jsonDecode(simpleQueryResponse) as Map; - await cache.update(queryOneId, ServerResponse(jsonDataOne)); + await cache.update(queryOneId, + ServerResponse(jsonDataOne, extensions: simpleQueryExtensions)); Map jsonDataTwo = jsonDecode(simpleQueryTwoResponse) as Map; - await cache.update(queryTwoId, ServerResponse(jsonDataTwo)); + await cache.update(queryTwoId, + ServerResponse(jsonDataTwo, extensions: simpleQueryTwoExtensions)); Map jsonDataOneUpdate = jsonDecode(simpleQueryResponseUpdate) as Map; - await cache.update(queryOneId, ServerResponse(jsonDataOneUpdate)); + await cache.update(queryOneId, + ServerResponse(jsonDataOneUpdate, extensions: simpleQueryExtensions)); // shared object should be updated. // now reload query two from cache and check object value. // it should be updated Map? jsonDataTwoUpdated = - await cache.get(queryTwoId, true); + await cache.resultTree(queryTwoId, true); if (jsonDataTwoUpdated == null) { fail('No query two found in cache'); } @@ -194,16 +217,16 @@ void main() { await cp.initialize(); String oid = '1234'; - EntityDataObject edo = cp.getEntityDataObject(oid); + EntityDataObject edo = cp.getEntityData(oid); String testValue = 'testValue'; String testProp = 'testProp'; edo.updateServerValue(testProp, testValue, null); - cp.saveEntityDataObject(edo); + cp.updateEntityData(edo); - EntityDataObject edo2 = cp.getEntityDataObject(oid); + EntityDataObject edo2 = cp.getEntityData(oid); String value = edo2.fields()[testProp]; expect(testValue, value); @@ -221,7 +244,8 @@ void main() { Map jsonData = jsonDecode(simpleQueryResponse) as Map; - await cache.update('itemsSimple', ServerResponse(jsonData)); + await cache.update('itemsSimple', + ServerResponse(jsonData, extensions: simpleQueryExtensions)); QueryRef ref = QueryRef( dataConnect, @@ -254,5 +278,47 @@ void main() { expect(resultDelayed.source, DataSource.server); }); }); + + test('Test AnyValue Caching', () async { + if (dataConnect.cacheManager == null) { + fail('No cache available'); + } + + Cache cache = dataConnect.cacheManager!; + + const String anyValueSingleData = ''' + {"data": {"anyValueItem": + { "name": "AnyItem B", + "blob": {"values":["A", 45, {"embedKey": "embedVal"}, ["A", "AA"]]} + } + }} + '''; + + final Map anyValueSingleExt = { + 'dataConnect': [ + { + 'path': ['anyValueItem'], + 'entityId': 'AnyValueItemSingle_ID' + } + ] + }; + + Map jsonData = + jsonDecode(anyValueSingleData) as Map; + + await cache.update('queryAnyValue', + ServerResponse(jsonData, extensions: anyValueSingleExt)); + + Map? cachedData = + await cache.resultTree('queryAnyValue', true); + + expect(cachedData?['anyValueItem']?['name'], 'AnyItem B'); + List values = cachedData?['anyValueItem']?['blob']?['values']; + expect(values.length, 4); + expect(values[0], 'A'); + expect(values[1], 45); + expect(values[2], {'embedKey': 'embedVal'}); + expect(values[3], ['A', 'AA']); + }); }); // test group } //main diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/result_tree_processor_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/result_tree_processor_test.dart index 6d3c71d09a17..9b347425c7dc 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/result_tree_processor_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/result_tree_processor_test.dart @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'package:firebase_data_connect/src/cache/cache_data_types.dart'; import 'package:firebase_data_connect/src/cache/result_tree_processor.dart'; +import 'package:firebase_data_connect/src/common/common_library.dart'; import 'package:flutter_test/flutter_test.dart'; import 'dart:convert'; @@ -23,19 +25,46 @@ void main() { const String simpleQueryResponse = ''' {"data": {"items":[ - {"desc":"itemDesc1","name":"itemOne", "cacheId":"123","price":4}, - {"desc":"itemDesc2","name":"itemTwo", "cacheId":"345","price":7} + {"desc":"itemDesc1","name":"itemOne","price":4}, + {"desc":"itemDesc2","name":"itemTwo","price":7} ]}} '''; + final Map simpleQueryPaths = { + DataConnectPath([ + DataConnectFieldPathSegment('items'), + DataConnectListIndexPathSegment(0) + ]): PathMetadata( + path: DataConnectPath([ + DataConnectFieldPathSegment('items'), + DataConnectListIndexPathSegment(0) + ]), + entityId: '123'), + DataConnectPath([ + DataConnectFieldPathSegment('items'), + DataConnectListIndexPathSegment(1) + ]): PathMetadata( + path: DataConnectPath([ + DataConnectFieldPathSegment('items'), + DataConnectListIndexPathSegment(1) + ]), + entityId: '345'), + }; + // query two has same object as query one so should refer to same Entity. const String simpleQueryResponseTwo = ''' {"data": { - "item": { "desc":"itemDesc1","name":"itemOne", "cacheId":"123","price":4 } + "item": { "desc":"itemDesc1","name":"itemOne","price":4 } }} '''; + final Map simpleQueryTwoPaths = { + DataConnectPath([DataConnectFieldPathSegment('item')]): PathMetadata( + path: DataConnectPath([DataConnectFieldPathSegment('item')]), + entityId: '123'), + }; + group('CacheProviderTests', () { // Dehydrate two queries sharing a single object. // Confirm that same EntityDataObject is present in both the dehydrated queries @@ -45,8 +74,8 @@ void main() { Map jsonData = jsonDecode(simpleQueryResponse) as Map; - DehydrationResult result = - await rp.dehydrate('itemsSimple', jsonData['data'], cp); + DehydrationResult result = await rp.dehydrateResults( + 'itemsSimple', jsonData['data'], cp, simpleQueryPaths); expect(result.dehydratedTree.nestedObjectLists?.length, 1); expect(result.dehydratedTree.nestedObjectLists?['items']?.length, 2); expect(result.dehydratedTree.nestedObjectLists?['items']?.first.entity, @@ -54,8 +83,8 @@ void main() { Map jsonDataTwo = jsonDecode(simpleQueryResponseTwo) as Map; - DehydrationResult resultTwo = - await rp.dehydrate('itemsSimpleTwo', jsonDataTwo, cp); + DehydrationResult resultTwo = await rp.dehydrateResults( + 'itemsSimpleTwo', jsonDataTwo['data'], cp, simpleQueryTwoPaths); List? guids = result.dehydratedTree.nestedObjectLists?['items'] ?.map((item) => item.entity?.guid) diff --git a/packages/firebase_database/firebase_database/CHANGELOG.md b/packages/firebase_database/firebase_database/CHANGELOG.md index af7fde6f7ffc..95074c6e12ef 100644 --- a/packages/firebase_database/firebase_database/CHANGELOG.md +++ b/packages/firebase_database/firebase_database/CHANGELOG.md @@ -1,3 +1,7 @@ +## 12.1.4 + + - Update a dependency to the latest release. + ## 12.1.3 - **FIX**(firebase_database): Add modifiers to keepSynced ref in android ([#17978](https://github.com/firebase/flutterfire/issues/17978)). ([8b1e05f6](https://github.com/firebase/flutterfire/commit/8b1e05f69544f22eaac568ea217cdce1299ded47)) diff --git a/packages/firebase_database/firebase_database/android/build.gradle b/packages/firebase_database/firebase_database/android/build.gradle index f49c3bf4250f..4826a7d16f7a 100755 --- a/packages/firebase_database/firebase_database/android/build.gradle +++ b/packages/firebase_database/firebase_database/android/build.gradle @@ -2,9 +2,14 @@ group 'io.flutter.plugins.firebase.database' version '1.0-SNAPSHOT' apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' apply from: file("local-config.gradle") +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly. +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +if (agpMajor < 9) { + apply plugin: 'kotlin-android' +} + buildscript { repositories { google() @@ -50,8 +55,10 @@ android { targetCompatibility project.ext.javaVersion } - kotlinOptions { - jvmTarget = project.ext.javaVersion + if (agpMajor < 9) { + kotlinOptions { + jvmTarget = project.ext.javaVersion + } } sourceSets { diff --git a/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist b/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist index 5458fc4188bf..52cf36cf82eb 100644 --- a/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist +++ b/packages/firebase_database/firebase_database/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_database/firebase_database/example/pubspec.yaml b/packages/firebase_database/firebase_database/example/pubspec.yaml index 0e8629e2c835..a2bb075ff7fd 100755 --- a/packages/firebase_database/firebase_database/example/pubspec.yaml +++ b/packages/firebase_database/firebase_database/example/pubspec.yaml @@ -6,8 +6,8 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 - firebase_database: ^12.1.3 + firebase_core: ^4.5.0 + firebase_database: ^12.1.4 flutter: sdk: flutter diff --git a/packages/firebase_database/firebase_database/ios/firebase_database/Sources/firebase_database/FirebaseDatabaseMessages.g.swift b/packages/firebase_database/firebase_database/ios/firebase_database/Sources/firebase_database/FirebaseDatabaseMessages.g.swift index 174bb8cfc69b..e91097627f7e 100644 --- a/packages/firebase_database/firebase_database/ios/firebase_database/Sources/firebase_database/FirebaseDatabaseMessages.g.swift +++ b/packages/firebase_database/firebase_database/ios/firebase_database/Sources/firebase_database/FirebaseDatabaseMessages.g.swift @@ -59,7 +59,8 @@ private func wrapError(_ error: Any) -> [Any?] { private func createConnectionError(withChannelName channelName: String) -> PigeonError { PigeonError( - code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", + code: "channel-error", + message: "Unable to establish connection on channel: '\(channelName)'.", details: "" ) } @@ -490,9 +491,10 @@ private class FirebaseDatabaseMessagesPigeonCodecReaderWriter: FlutterStandardRe } class FirebaseDatabaseMessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = FirebaseDatabaseMessagesPigeonCodec( - readerWriter: FirebaseDatabaseMessagesPigeonCodecReaderWriter() - ) + static let shared = + FirebaseDatabaseMessagesPigeonCodec( + readerWriter: FirebaseDatabaseMessagesPigeonCodecReaderWriter() + ) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. @@ -558,9 +560,9 @@ class FirebaseDatabaseHostApiSetup { messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" let goOnlineChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.goOnline\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.goOnline\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { goOnlineChannel.setMessageHandler { message, reply in @@ -579,9 +581,9 @@ class FirebaseDatabaseHostApiSetup { goOnlineChannel.setMessageHandler(nil) } let goOfflineChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.goOffline\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.goOffline\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { goOfflineChannel.setMessageHandler { message, reply in @@ -600,9 +602,9 @@ class FirebaseDatabaseHostApiSetup { goOfflineChannel.setMessageHandler(nil) } let setPersistenceEnabledChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.setPersistenceEnabled\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.setPersistenceEnabled\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { setPersistenceEnabledChannel.setMessageHandler { message, reply in @@ -622,9 +624,9 @@ class FirebaseDatabaseHostApiSetup { setPersistenceEnabledChannel.setMessageHandler(nil) } let setPersistenceCacheSizeBytesChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.setPersistenceCacheSizeBytes\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.setPersistenceCacheSizeBytes\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { setPersistenceCacheSizeBytesChannel.setMessageHandler { message, reply in @@ -644,9 +646,9 @@ class FirebaseDatabaseHostApiSetup { setPersistenceCacheSizeBytesChannel.setMessageHandler(nil) } let setLoggingEnabledChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.setLoggingEnabled\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.setLoggingEnabled\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { setLoggingEnabledChannel.setMessageHandler { message, reply in @@ -666,9 +668,9 @@ class FirebaseDatabaseHostApiSetup { setLoggingEnabledChannel.setMessageHandler(nil) } let useDatabaseEmulatorChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.useDatabaseEmulator\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.useDatabaseEmulator\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { useDatabaseEmulatorChannel.setMessageHandler { message, reply in @@ -689,9 +691,9 @@ class FirebaseDatabaseHostApiSetup { useDatabaseEmulatorChannel.setMessageHandler(nil) } let refChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.ref\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.ref\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { refChannel.setMessageHandler { message, reply in @@ -711,9 +713,9 @@ class FirebaseDatabaseHostApiSetup { refChannel.setMessageHandler(nil) } let refFromURLChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.refFromURL\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.refFromURL\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { refFromURLChannel.setMessageHandler { message, reply in @@ -733,9 +735,9 @@ class FirebaseDatabaseHostApiSetup { refFromURLChannel.setMessageHandler(nil) } let purgeOutstandingWritesChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.purgeOutstandingWrites\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.purgeOutstandingWrites\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { purgeOutstandingWritesChannel.setMessageHandler { message, reply in @@ -754,9 +756,9 @@ class FirebaseDatabaseHostApiSetup { purgeOutstandingWritesChannel.setMessageHandler(nil) } let databaseReferenceSetChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceSet\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceSet\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { databaseReferenceSetChannel.setMessageHandler { message, reply in @@ -776,9 +778,9 @@ class FirebaseDatabaseHostApiSetup { databaseReferenceSetChannel.setMessageHandler(nil) } let databaseReferenceSetWithPriorityChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceSetWithPriority\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceSetWithPriority\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { databaseReferenceSetWithPriorityChannel.setMessageHandler { message, reply in @@ -798,9 +800,9 @@ class FirebaseDatabaseHostApiSetup { databaseReferenceSetWithPriorityChannel.setMessageHandler(nil) } let databaseReferenceUpdateChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceUpdate\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceUpdate\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { databaseReferenceUpdateChannel.setMessageHandler { message, reply in @@ -820,9 +822,9 @@ class FirebaseDatabaseHostApiSetup { databaseReferenceUpdateChannel.setMessageHandler(nil) } let databaseReferenceSetPriorityChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceSetPriority\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceSetPriority\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { databaseReferenceSetPriorityChannel.setMessageHandler { message, reply in @@ -842,9 +844,9 @@ class FirebaseDatabaseHostApiSetup { databaseReferenceSetPriorityChannel.setMessageHandler(nil) } let databaseReferenceRunTransactionChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceRunTransaction\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceRunTransaction\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { databaseReferenceRunTransactionChannel.setMessageHandler { message, reply in @@ -864,32 +866,33 @@ class FirebaseDatabaseHostApiSetup { databaseReferenceRunTransactionChannel.setMessageHandler(nil) } let databaseReferenceGetTransactionResultChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceGetTransactionResult\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.databaseReferenceGetTransactionResult\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { databaseReferenceGetTransactionResultChannel.setMessageHandler { message, reply in let args = message as! [Any?] let appArg = args[0] as! DatabasePigeonFirebaseApp let transactionKeyArg = args[1] as! Int64 - api.databaseReferenceGetTransactionResult(app: appArg, transactionKey: transactionKeyArg) { - result in - switch result { - case let .success(res): - reply(wrapResult(res)) - case let .failure(error): - reply(wrapError(error)) + api + .databaseReferenceGetTransactionResult(app: appArg, + transactionKey: transactionKeyArg) { result in + switch result { + case let .success(res): + reply(wrapResult(res)) + case let .failure(error): + reply(wrapError(error)) + } } - } } } else { databaseReferenceGetTransactionResultChannel.setMessageHandler(nil) } let onDisconnectSetChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectSet\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectSet\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { onDisconnectSetChannel.setMessageHandler { message, reply in @@ -909,9 +912,9 @@ class FirebaseDatabaseHostApiSetup { onDisconnectSetChannel.setMessageHandler(nil) } let onDisconnectSetWithPriorityChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectSetWithPriority\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectSetWithPriority\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { onDisconnectSetWithPriorityChannel.setMessageHandler { message, reply in @@ -931,9 +934,9 @@ class FirebaseDatabaseHostApiSetup { onDisconnectSetWithPriorityChannel.setMessageHandler(nil) } let onDisconnectUpdateChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectUpdate\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectUpdate\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { onDisconnectUpdateChannel.setMessageHandler { message, reply in @@ -953,9 +956,9 @@ class FirebaseDatabaseHostApiSetup { onDisconnectUpdateChannel.setMessageHandler(nil) } let onDisconnectCancelChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectCancel\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.onDisconnectCancel\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { onDisconnectCancelChannel.setMessageHandler { message, reply in @@ -975,9 +978,9 @@ class FirebaseDatabaseHostApiSetup { onDisconnectCancelChannel.setMessageHandler(nil) } let queryObserveChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.queryObserve\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.queryObserve\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { queryObserveChannel.setMessageHandler { message, reply in @@ -997,9 +1000,9 @@ class FirebaseDatabaseHostApiSetup { queryObserveChannel.setMessageHandler(nil) } let queryKeepSyncedChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.queryKeepSynced\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.queryKeepSynced\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { queryKeepSyncedChannel.setMessageHandler { message, reply in @@ -1019,9 +1022,9 @@ class FirebaseDatabaseHostApiSetup { queryKeepSyncedChannel.setMessageHandler(nil) } let queryGetChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.queryGet\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec + name: "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseHostApi.queryGet\(channelSuffix)", + binaryMessenger: binaryMessenger, + codec: codec ) if let api { queryGetChannel.setMessageHandler { message, reply in @@ -1067,10 +1070,11 @@ class FirebaseDatabaseFlutterApi: FirebaseDatabaseFlutterApiProtocol { snapshotValue snapshotValueArg: Any?, completion: @escaping (Result) -> Void) { - let channelName = - "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseFlutterApi.callTransactionHandler\(messageChannelSuffix)" + let channelName = "dev.flutter.pigeon.firebase_database_platform_interface.FirebaseDatabaseFlutterApi.callTransactionHandler\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel( - name: channelName, binaryMessenger: binaryMessenger, codec: codec + name: channelName, + binaryMessenger: binaryMessenger, + codec: codec ) channel.sendMessage([transactionKeyArg, snapshotValueArg] as [Any?]) { response in guard let listResponse = response as? [Any?] else { @@ -1083,14 +1087,11 @@ class FirebaseDatabaseFlutterApi: FirebaseDatabaseFlutterApiProtocol { let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else if listResponse[0] == nil { - completion( - .failure( - PigeonError( - code: "null-error", - message: "Flutter api returned null value for non-null return value.", details: "" - ) - ) - ) + completion(.failure(PigeonError( + code: "null-error", + message: "Flutter api returned null value for non-null return value.", + details: "" + ))) } else { let result = listResponse[0] as! TransactionHandlerResult completion(.success(result)) diff --git a/packages/firebase_database/firebase_database/ios/generated_firebase_sdk_version.txt b/packages/firebase_database/firebase_database/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_database/firebase_database/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_database/firebase_database/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_database/firebase_database/pubspec.yaml b/packages/firebase_database/firebase_database/pubspec.yaml index 735010227d2f..abbe7eb8e522 100755 --- a/packages/firebase_database/firebase_database/pubspec.yaml +++ b/packages/firebase_database/firebase_database/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Database, a cloud-hosted NoSQL database with realtime data syncing across Android and iOS clients, and offline access. homepage: https://firebase.google.com/docs/database repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_database/firebase_database -version: 12.1.3 +version: 12.1.4 topics: - firebase - database @@ -17,10 +17,10 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_database_platform_interface: ^0.3.0+2 - firebase_database_web: ^0.2.7+3 + firebase_database_platform_interface: ^0.3.0+3 + firebase_database_web: ^0.2.7+4 flutter: sdk: flutter @@ -39,5 +39,7 @@ flutter: pluginClass: FLTFirebaseDatabasePlugin macos: pluginClass: FLTFirebaseDatabasePlugin + windows: + pluginClass: FirebaseDatabasePluginCApi web: default_package: firebase_database_web diff --git a/packages/firebase_database/firebase_database/windows/CMakeLists.txt b/packages/firebase_database/firebase_database/windows/CMakeLists.txt new file mode 100644 index 000000000000..b85c61d20594 --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.14) + +set(PROJECT_NAME "flutterfire_database") +project(${PROJECT_NAME} LANGUAGES CXX) + +set(PLUGIN_NAME "firebase_database_plugin") + +list(APPEND PLUGIN_SOURCES + "firebase_database_plugin.cpp" + "firebase_database_plugin.h" + "messages.g.cpp" + "messages.g.h" +) + +# Read version from pubspec.yaml +file(STRINGS "../pubspec.yaml" pubspec_content) +foreach(line ${pubspec_content}) + string(FIND ${line} "version: " has_version) + + if("${has_version}" STREQUAL "0") + string(FIND ${line} ": " version_start_pos) + math(EXPR version_start_pos "${version_start_pos} + 2") + string(LENGTH ${line} version_end_pos) + math(EXPR len "${version_end_pos} - ${version_start_pos}") + string(SUBSTRING ${line} ${version_start_pos} ${len} PLUGIN_VERSION) + break() + endif() +endforeach(line) + +configure_file(plugin_version.h.in ${CMAKE_BINARY_DIR}/generated/firebase_database/plugin_version.h) +include_directories(${CMAKE_BINARY_DIR}/generated/) + +add_library(${PLUGIN_NAME} STATIC + "include/firebase_database/firebase_database_plugin_c_api.h" + "firebase_database_plugin_c_api.cpp" + ${PLUGIN_SOURCES} + ${CMAKE_BINARY_DIR}/generated/firebase_database/plugin_version.h +) + +apply_standard_settings(${PLUGIN_NAME}) + +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PUBLIC FLUTTER_PLUGIN_IMPL) +target_compile_definitions(${PLUGIN_NAME} PRIVATE -DINTERNAL_EXPERIMENTAL=1) + +set(MSVC_RUNTIME_MODE MD) +set(firebase_libs firebase_core_plugin firebase_database) +set(ADDITIONAL_LIBS advapi32 ws2_32 crypt32 rpcrt4 ole32 shell32 Bcrypt.lib DbgHelp.lib) +set(RTDB_ADDITIONAL_LIBS iphlpapi psapi userenv) +target_link_libraries(${PLUGIN_NAME} PRIVATE "${firebase_libs}" "${ADDITIONAL_LIBS}" "${RTDB_ADDITIONAL_LIBS}") + +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +set(firebase_database_bundled_libraries + "" + PARENT_SCOPE +) diff --git a/packages/firebase_database/firebase_database/windows/firebase_database_plugin.cpp b/packages/firebase_database/firebase_database/windows/firebase_database_plugin.cpp new file mode 100644 index 000000000000..f523a86ffe59 --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/firebase_database_plugin.cpp @@ -0,0 +1,1123 @@ +// Copyright 2025, 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. + +#include "firebase_database_plugin.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "firebase/app.h" +#include "firebase/database.h" +#include "firebase/database/common.h" +#include "firebase/database/data_snapshot.h" +#include "firebase/database/database_reference.h" +#include "firebase/database/disconnection.h" +#include "firebase/database/listener.h" +#include "firebase/database/mutable_data.h" +#include "firebase/database/query.h" +#include "firebase/future.h" +#include "firebase/log.h" +#include "firebase/variant.h" +#include "firebase_database/plugin_version.h" +#include "messages.g.h" + +using firebase::App; +using firebase::Future; +using firebase::Variant; +using firebase::database::Database; +using firebase::database::DatabaseReference; +using firebase::database::DataSnapshot; +using firebase::database::Error; +using firebase::database::MutableData; +using firebase::database::TransactionResult; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +namespace firebase_database_windows { + +static const std::string kLibraryName = "flutter-fire-db"; + +// Static member initialization +flutter::BinaryMessenger* FirebaseDatabasePlugin::messenger_ = nullptr; +std::map>> + FirebaseDatabasePlugin::event_channels_; +std::map>> + FirebaseDatabasePlugin::stream_handlers_; +std::map + FirebaseDatabasePlugin::database_instances_; + +// atexit handler: clean up Database resources before static destruction. +// 1. Clear event channels to trigger StreamHandler destruction, which +// unregisters listeners from the Database while it's still alive. +// 2. Call GoOffline() to close WebSocket connections so thread joins +// during App::~App() → Database::DeleteInternal() complete quickly. +static void CleanupBeforeStaticDestruction() { + // Destroy event channels and stream handlers first. This triggers + // StreamHandler destructors which call RemoveValueListener / + // RemoveChildListener while the Database is still valid. + FirebaseDatabasePlugin::event_channels_.clear(); + FirebaseDatabasePlugin::stream_handlers_.clear(); + + // Disconnect all Database instances to close WebSocket connections. + for (auto& pair : FirebaseDatabasePlugin::database_instances_) { + if (pair.second) { + pair.second->GoOffline(); + } + } + // Give the scheduler thread time to process GoOffline callbacks. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +// --- Helper: Register an EventChannel with a generated name --- +static std::string RegisterEventChannel( + const std::string& prefix, + std::unique_ptr> handler) { + static int channel_counter = 0; + std::string channelName = + prefix + std::to_string(channel_counter++) + "_" + + std::to_string( + std::chrono::system_clock::now().time_since_epoch().count()); + + FirebaseDatabasePlugin::event_channels_[channelName] = + std::make_unique>( + FirebaseDatabasePlugin::messenger_, channelName, + &flutter::StandardMethodCodec::GetInstance()); + FirebaseDatabasePlugin::stream_handlers_[channelName] = std::move(handler); + FirebaseDatabasePlugin::event_channels_[channelName]->SetStreamHandler( + std::move(FirebaseDatabasePlugin::stream_handlers_[channelName])); + return channelName; +} + +// --- Helper: Convert firebase::Variant to flutter::EncodableValue --- +EncodableValue FirebaseDatabasePlugin::VariantToEncodableValue( + const Variant& variant) { + switch (variant.type()) { + case Variant::kTypeNull: + return EncodableValue(); + case Variant::kTypeInt64: + return EncodableValue(variant.int64_value()); + case Variant::kTypeDouble: + return EncodableValue(variant.double_value()); + case Variant::kTypeBool: + return EncodableValue(variant.bool_value()); + case Variant::kTypeStaticString: + return EncodableValue(std::string(variant.string_value())); + case Variant::kTypeMutableString: + return EncodableValue(variant.mutable_string()); + case Variant::kTypeVector: { + EncodableList list; + for (const auto& item : variant.vector()) { + list.push_back(VariantToEncodableValue(item)); + } + return EncodableValue(list); + } + case Variant::kTypeMap: { + EncodableMap map; + for (const auto& kv : variant.map()) { + EncodableValue key = VariantToEncodableValue(kv.first); + EncodableValue value = VariantToEncodableValue(kv.second); + map[key] = value; + } + return EncodableValue(map); + } + case Variant::kTypeStaticBlob: { + std::vector blob(variant.blob_data(), + variant.blob_data() + variant.blob_size()); + return EncodableValue(blob); + } + case Variant::kTypeMutableBlob: { + std::vector blob( + variant.mutable_blob_data(), + variant.mutable_blob_data() + variant.blob_size()); + return EncodableValue(blob); + } + default: + return EncodableValue(); + } +} + +// --- Helper: Convert flutter::EncodableValue to firebase::Variant --- +Variant FirebaseDatabasePlugin::EncodableValueToVariant( + const EncodableValue& value) { + if (std::holds_alternative(value)) { + return Variant::Null(); + } else if (std::holds_alternative(value)) { + return Variant(std::get(value)); + } else if (std::holds_alternative(value)) { + return Variant(static_cast(std::get(value))); + } else if (std::holds_alternative(value)) { + return Variant(std::get(value)); + } else if (std::holds_alternative(value)) { + return Variant(std::get(value)); + } else if (std::holds_alternative(value)) { + return Variant(std::get(value)); + } else if (std::holds_alternative>(value)) { + const auto& blob = std::get>(value); + return Variant::FromMutableBlob(blob.data(), blob.size()); + } else if (std::holds_alternative(value)) { + const auto& list = std::get(value); + std::vector vec; + vec.reserve(list.size()); + for (const auto& item : list) { + vec.push_back(EncodableValueToVariant(item)); + } + return Variant(vec); + } else if (std::holds_alternative(value)) { + const auto& map = std::get(value); + std::map variant_map; + for (const auto& kv : map) { + variant_map[EncodableValueToVariant(kv.first)] = + EncodableValueToVariant(kv.second); + } + return Variant(variant_map); + } + return Variant::Null(); +} + +// --- Helper: Error code string from C++ SDK Error enum --- +std::string FirebaseDatabasePlugin::GetDatabaseErrorCode(Error error) { + switch (error) { + case Error::kErrorNone: + return "none"; + case Error::kErrorDisconnected: + return "disconnected"; + case Error::kErrorExpiredToken: + return "expired-token"; + case Error::kErrorInvalidToken: + return "invalid-token"; + case Error::kErrorMaxRetries: + return "max-retries"; + case Error::kErrorNetworkError: + return "network-error"; + case Error::kErrorOperationFailed: + return "operation-failed"; + case Error::kErrorOverriddenBySet: + return "overridden-by-set"; + case Error::kErrorPermissionDenied: + return "permission-denied"; + case Error::kErrorUnavailable: + return "unavailable"; + case Error::kErrorWriteCanceled: + return "write-canceled"; + case Error::kErrorInvalidVariantType: + return "invalid-variant-type"; + case Error::kErrorConflictingOperationInProgress: + return "conflicting-operation-in-progress"; + case Error::kErrorTransactionAbortedByUser: + return "transaction-aborted-by-user"; + default: + return "unknown"; + } +} + +std::string FirebaseDatabasePlugin::GetDatabaseErrorMessage(Error error) { + const char* msg = firebase::database::GetErrorMessage(error); + return msg ? std::string(msg) : "Unknown error"; +} + +FlutterError FirebaseDatabasePlugin::ParseError( + const firebase::FutureBase& future) { + Error error = static_cast(future.error()); + std::string code = GetDatabaseErrorCode(error); + std::string message = + future.error_message() ? future.error_message() : "Unknown error"; + return FlutterError(code, message); +} + +// --- Helper: Convert DataSnapshot to EncodableMap --- +EncodableMap FirebaseDatabasePlugin::DataSnapshotToEncodableMap( + const DataSnapshot& snapshot) { + EncodableMap result; + result[EncodableValue("key")] = + snapshot.key() ? EncodableValue(std::string(snapshot.key())) + : EncodableValue(); + result[EncodableValue("value")] = VariantToEncodableValue(snapshot.value()); + result[EncodableValue("priority")] = + VariantToEncodableValue(snapshot.priority()); + + EncodableList childKeys; + std::vector children = snapshot.children(); + for (const auto& child : children) { + if (child.key()) { + childKeys.push_back(EncodableValue(std::string(child.key()))); + } + } + result[EncodableValue("childKeys")] = EncodableValue(childKeys); + + return result; +} + +// --- Helper: Apply query modifiers --- +firebase::database::Query FirebaseDatabasePlugin::ApplyQueryModifiers( + firebase::database::Query query, const EncodableList& modifiers) { + for (const auto& mod_value : modifiers) { + const auto& mod = std::get(mod_value); + + auto type_it = mod.find(EncodableValue("type")); + if (type_it == mod.end()) continue; + std::string type = std::get(type_it->second); + + auto name_it = mod.find(EncodableValue("name")); + if (name_it == mod.end()) continue; + std::string name = std::get(name_it->second); + + if (type == "orderBy") { + if (name == "orderByChild") { + auto path_it = mod.find(EncodableValue("path")); + if (path_it != mod.end()) { + query = query.OrderByChild( + std::get(path_it->second).c_str()); + } + } else if (name == "orderByKey") { + query = query.OrderByKey(); + } else if (name == "orderByValue") { + query = query.OrderByValue(); + } else if (name == "orderByPriority") { + query = query.OrderByPriority(); + } + } else if (type == "cursor") { + auto value_it = mod.find(EncodableValue("value")); + Variant cursor_value = Variant::Null(); + if (value_it != mod.end()) { + cursor_value = EncodableValueToVariant(value_it->second); + } + + auto key_it = mod.find(EncodableValue("key")); + const char* child_key = nullptr; + std::string key_str; + if (key_it != mod.end() && + std::holds_alternative(key_it->second)) { + key_str = std::get(key_it->second); + child_key = key_str.c_str(); + } + + if (name == "startAt") { + query = child_key ? query.StartAt(cursor_value, child_key) + : query.StartAt(cursor_value); + } else if (name == "startAfter") { + // C++ SDK doesn't have StartAfter; use StartAt workaround + query = child_key ? query.StartAt(cursor_value, child_key) + : query.StartAt(cursor_value); + } else if (name == "endAt") { + query = child_key ? query.EndAt(cursor_value, child_key) + : query.EndAt(cursor_value); + } else if (name == "endBefore") { + // C++ SDK doesn't have EndBefore; use EndAt workaround + query = child_key ? query.EndAt(cursor_value, child_key) + : query.EndAt(cursor_value); + } + } else if (type == "limit") { + auto limit_it = mod.find(EncodableValue("limit")); + if (limit_it != mod.end()) { + int limit = 0; + if (std::holds_alternative(limit_it->second)) { + limit = std::get(limit_it->second); + } else if (std::holds_alternative(limit_it->second)) { + limit = static_cast(std::get(limit_it->second)); + } + if (name == "limitToFirst") { + query = query.LimitToFirst(static_cast(limit)); + } else if (name == "limitToLast") { + query = query.LimitToLast(static_cast(limit)); + } + } + } + } + return query; +} + +// ===== Plugin Implementation ===== + +FirebaseDatabasePlugin::FirebaseDatabasePlugin() {} + +FirebaseDatabasePlugin::~FirebaseDatabasePlugin() {} + +void FirebaseDatabasePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto plugin = std::make_unique(); + messenger_ = registrar->messenger(); + FirebaseDatabaseHostApi::SetUp(registrar->messenger(), plugin.get()); + registrar->AddPlugin(std::move(plugin)); + App::RegisterLibrary(kLibraryName.c_str(), getPluginVersion().c_str(), + nullptr); + + // Register atexit handler to clean up listeners and disconnect + // before static destruction triggers thread joins in the C++ SDK. + std::atexit(CleanupBeforeStaticDestruction); +} + +// --- Helper: Get Database instance from Pigeon app --- +Database* FirebaseDatabasePlugin::GetDatabaseFromPigeon( + const DatabasePigeonFirebaseApp& app) { + App* firebase_app = App::GetInstance(app.app_name().c_str()); + if (!firebase_app) { + return nullptr; + } + + const auto& settings = app.settings(); + const std::string* url = app.database_u_r_l(); + + // Build a cache key from app name + effective URL (like Firestore does) + std::string effective_url; + if (url && !url->empty()) { + effective_url = *url; + } + + std::string cache_key = app.app_name() + "-" + effective_url; + + // Return cached instance if available (raw pointer, not owned). + // The C++ SDK manages Database instance lifetime internally. + // App::~App() triggers Database::DeleteInternal() during static destruction. + auto it = database_instances_.find(cache_key); + if (it != database_instances_.end()) { + return it->second; + } + + // Create new instance + // Always pass the URL explicitly - the C++ SDK on desktop may not + // properly read database_url from app options without it. + const char* app_db_url = firebase_app->options().database_url(); + if (effective_url.empty() && app_db_url && strlen(app_db_url) > 0) { + effective_url = app_db_url; + } + Database* database = nullptr; + if (!effective_url.empty()) { + database = Database::GetInstance(firebase_app, effective_url.c_str()); + } else { + database = Database::GetInstance(firebase_app); + } + + if (!database) return nullptr; + + if (settings.persistence_enabled()) { + database->set_persistence_enabled(*settings.persistence_enabled()); + } + if (settings.logging_enabled() && *settings.logging_enabled()) { + database->set_log_level(firebase::kLogLevelDebug); + } + + // Cache raw pointer. We do NOT take ownership — the C++ SDK manages + // the Database lifetime via App's CleanupNotifier. This matches the + // pattern used by firebase_auth and firebase_storage. + database_instances_[cache_key] = database; + + return database; +} + +// ===== Database methods ===== + +void FirebaseDatabasePlugin::GoOnline( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + database->GoOnline(); + result(std::nullopt); +} + +void FirebaseDatabasePlugin::GoOffline( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + database->GoOffline(); + result(std::nullopt); +} + +void FirebaseDatabasePlugin::SetPersistenceEnabled( + const DatabasePigeonFirebaseApp& app, bool enabled, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + database->set_persistence_enabled(enabled); + result(std::nullopt); +} + +void FirebaseDatabasePlugin::SetPersistenceCacheSizeBytes( + const DatabasePigeonFirebaseApp& app, int64_t cache_size, + std::function reply)> result) { + // C++ SDK doesn't directly support setting cache size + result(std::nullopt); +} + +void FirebaseDatabasePlugin::SetLoggingEnabled( + const DatabasePigeonFirebaseApp& app, bool enabled, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + database->set_log_level(enabled ? firebase::kLogLevelDebug + : firebase::kLogLevelInfo); + result(std::nullopt); +} + +void FirebaseDatabasePlugin::UseDatabaseEmulator( + const DatabasePigeonFirebaseApp& app, const std::string& host, int64_t port, + std::function reply)> result) { + // The C++ SDK for Realtime Database does not have a UseEmulator API. + // On Windows, tests run against the live Firebase instance. + result(std::nullopt); +} + +void FirebaseDatabasePlugin::Ref( + const DatabasePigeonFirebaseApp& app, const std::string* path, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref; + if (path && !path->empty()) { + ref = database->GetReference(path->c_str()); + } else { + ref = database->GetReference(); + } + + std::string ref_path; + if (ref.key()) { + // Build path from the URL + std::string url = ref.url(); + // Extract path from URL (after the host) + auto pos = url.find(".com/"); + if (pos != std::string::npos) { + ref_path = url.substr(pos + 4); + } else { + ref_path = path ? *path : "/"; + } + } else { + ref_path = path ? *path : "/"; + } + + result(DatabaseReferencePlatform(ref_path)); +} + +void FirebaseDatabasePlugin::RefFromURL( + const DatabasePigeonFirebaseApp& app, const std::string& url, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReferenceFromUrl(url.c_str()); + + std::string ref_path; + std::string ref_url = ref.url(); + auto pos = ref_url.find(".com/"); + if (pos != std::string::npos) { + ref_path = ref_url.substr(pos + 4); + } else { + ref_path = "/"; + } + + result(DatabaseReferencePlatform(ref_path)); +} + +void FirebaseDatabasePlugin::PurgeOutstandingWrites( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + database->PurgeOutstandingWrites(); + result(std::nullopt); +} + +// ===== DatabaseReference methods ===== + +void FirebaseDatabasePlugin::DatabaseReferenceSet( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + Variant value = request.value() ? EncodableValueToVariant(*request.value()) + : Variant::Null(); + + ref.SetValue(value).OnCompletion([result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +void FirebaseDatabasePlugin::DatabaseReferenceSetWithPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + Variant value = request.value() ? EncodableValueToVariant(*request.value()) + : Variant::Null(); + Variant priority = request.priority() + ? EncodableValueToVariant(*request.priority()) + : Variant::Null(); + + ref.SetValueAndPriority(value, priority) + .OnCompletion([result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +void FirebaseDatabasePlugin::DatabaseReferenceUpdate( + const DatabasePigeonFirebaseApp& app, const UpdateRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + Variant values = EncodableValueToVariant(EncodableValue(request.value())); + + ref.UpdateChildren(values).OnCompletion([result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +void FirebaseDatabasePlugin::DatabaseReferenceSetPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + Variant priority = request.priority() + ? EncodableValueToVariant(*request.priority()) + : Variant::Null(); + + ref.SetPriority(priority).OnCompletion([result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +void FirebaseDatabasePlugin::DatabaseReferenceRunTransaction( + const DatabasePigeonFirebaseApp& app, const TransactionRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + int64_t transaction_key = request.transaction_key(); + bool apply_locally = request.apply_locally(); + + struct TransactionContext { + flutter::BinaryMessenger* messenger; + int64_t transaction_key; + std::map* transaction_results; + std::function reply)> result; + }; + + auto* ctx = new TransactionContext{messenger_, transaction_key, + &transaction_results_, result}; + + ref.RunTransaction( + [](MutableData* data, + void* context) -> firebase::database::TransactionResult { + auto* ctx = static_cast(context); + + // Convert current data to EncodableValue + Variant current_value = data->value(); + EncodableValue snapshot_value = + FirebaseDatabasePlugin::VariantToEncodableValue(current_value); + + // Call the Flutter transaction handler synchronously using a semaphore + std::mutex mtx; + std::condition_variable cv; + bool handler_complete = false; + TransactionHandlerResult* handler_result = nullptr; + + auto flutter_api = + std::make_unique(ctx->messenger); + + const EncodableValue* snapshot_ptr = + std::holds_alternative(snapshot_value) + ? nullptr + : &snapshot_value; + + flutter_api->CallTransactionHandler( + ctx->transaction_key, snapshot_ptr, + [&](const TransactionHandlerResult& result) { + handler_result = new TransactionHandlerResult( + result.value(), result.aborted(), result.exception()); + std::lock_guard lock(mtx); + handler_complete = true; + cv.notify_one(); + }, + [&](const FlutterError& error) { + handler_result = new TransactionHandlerResult(true, true); + std::lock_guard lock(mtx); + handler_complete = true; + cv.notify_one(); + }); + + // Wait for the Flutter callback to complete + { + std::unique_lock lock(mtx); + cv.wait(lock, [&] { return handler_complete; }); + } + + if (!handler_result || handler_result->aborted() || + handler_result->exception()) { + delete handler_result; + return firebase::database::kTransactionResultAbort; + } + + // Apply the result value + if (handler_result->value()) { + Variant new_value = FirebaseDatabasePlugin::EncodableValueToVariant( + *handler_result->value()); + data->set_value(new_value); + } else { + data->set_value(Variant::Null()); + } + + delete handler_result; + return firebase::database::kTransactionResultSuccess; + }, + ctx, apply_locally); + + // Wait for the transaction to complete + ref.RunTransactionLastResult().OnCompletion( + [ctx](const Future& future) { + if (future.error() == Error::kErrorNone) { + const DataSnapshot* snapshot = future.result(); + EncodableMap result_map; + result_map[EncodableValue("committed")] = EncodableValue(true); + if (snapshot) { + result_map[EncodableValue("snapshot")] = EncodableValue( + FirebaseDatabasePlugin::DataSnapshotToEncodableMap(*snapshot)); + } else { + result_map[EncodableValue("snapshot")] = EncodableValue(); + } + (*ctx->transaction_results)[ctx->transaction_key] = result_map; + ctx->result(std::nullopt); + } else { + // Transaction failed but may have been aborted + EncodableMap result_map; + result_map[EncodableValue("committed")] = EncodableValue(false); + result_map[EncodableValue("snapshot")] = EncodableValue(EncodableMap{ + {EncodableValue("key"), EncodableValue()}, + {EncodableValue("value"), EncodableValue()}, + {EncodableValue("priority"), EncodableValue()}, + {EncodableValue("childKeys"), EncodableValue(EncodableList{})}, + }); + (*ctx->transaction_results)[ctx->transaction_key] = result_map; + + if (static_cast(future.error()) == + Error::kErrorTransactionAbortedByUser) { + // Aborted by user is not an error condition + ctx->result(std::nullopt); + } else { + ctx->result(FirebaseDatabasePlugin::ParseError(future)); + } + } + delete ctx; + }); +} + +void FirebaseDatabasePlugin::DatabaseReferenceGetTransactionResult( + const DatabasePigeonFirebaseApp& app, int64_t transaction_key, + std::function reply)> result) { + auto it = transaction_results_.find(transaction_key); + if (it != transaction_results_.end()) { + result(it->second); + transaction_results_.erase(it); + } else { + // Return default result + EncodableMap default_result; + default_result[EncodableValue("committed")] = EncodableValue(false); + default_result[EncodableValue("snapshot")] = EncodableValue(EncodableMap{ + {EncodableValue("key"), EncodableValue()}, + {EncodableValue("value"), EncodableValue()}, + {EncodableValue("priority"), EncodableValue()}, + {EncodableValue("childKeys"), EncodableValue(EncodableList{})}, + }); + result(default_result); + } +} + +// ===== OnDisconnect methods ===== + +void FirebaseDatabasePlugin::OnDisconnectSet( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + Variant value = request.value() ? EncodableValueToVariant(*request.value()) + : Variant::Null(); + + ref.OnDisconnect()->SetValue(value).OnCompletion( + [result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +void FirebaseDatabasePlugin::OnDisconnectSetWithPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + Variant value = request.value() ? EncodableValueToVariant(*request.value()) + : Variant::Null(); + Variant priority = request.priority() + ? EncodableValueToVariant(*request.priority()) + : Variant::Null(); + + ref.OnDisconnect() + ->SetValueAndPriority(value, priority) + .OnCompletion([result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +void FirebaseDatabasePlugin::OnDisconnectUpdate( + const DatabasePigeonFirebaseApp& app, const UpdateRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + Variant values = EncodableValueToVariant(EncodableValue(request.value())); + + ref.OnDisconnect()->UpdateChildren(values).OnCompletion( + [result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +void FirebaseDatabasePlugin::OnDisconnectCancel( + const DatabasePigeonFirebaseApp& app, const std::string& path, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(path.c_str()); + + ref.OnDisconnect()->Cancel().OnCompletion( + [result](const Future& future) { + if (future.error() == Error::kErrorNone) { + result(std::nullopt); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +// ===== Query methods ===== + +void FirebaseDatabasePlugin::QueryObserve( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + firebase::database::Query query = + ApplyQueryModifiers(ref, request.modifiers()); + + // The event type will be passed as an argument when the Dart side calls + // listen on the EventChannel. We need to create the appropriate handler. + // Since we don't know the event type here, we create both a value and child + // handler based on a shared approach: the Dart side passes eventType as an + // argument to the EventChannel's listen call. + + // We use a generic approach: create one handler that reads the eventType + // from the listen arguments. + class DatabaseGenericStreamHandler + : public flutter::StreamHandler { + public: + DatabaseGenericStreamHandler(firebase::database::Query query) + : query_(query), value_listener_(nullptr), child_listener_(nullptr) {} + + ~DatabaseGenericStreamHandler() override { + // Remove listeners before deleting to avoid dangling pointers in the + // Database's internal listener list. Query::RemoveXxxListener() checks + // if (internal_) first, so this is a safe no-op if the Database was + // already destroyed (the cleanup mechanism nullifies internal_). + if (value_listener_) { + query_.RemoveValueListener(value_listener_); + delete value_listener_; + value_listener_ = nullptr; + } + if (child_listener_) { + query_.RemoveChildListener(child_listener_); + delete child_listener_; + child_listener_ = nullptr; + } + } + + protected: + std::unique_ptr> + OnListenInternal( + const flutter::EncodableValue* arguments, + std::unique_ptr>&& events) + override { + events_ = std::move(events); + + // Extract eventType from arguments + std::string event_type = "value"; + if (arguments && std::holds_alternative(*arguments)) { + const auto& args_map = std::get(*arguments); + auto it = args_map.find(EncodableValue("eventType")); + if (it != args_map.end() && + std::holds_alternative(it->second)) { + event_type = std::get(it->second); + } + } + + if (event_type == "value") { + // Value listener + class VL : public firebase::database::ValueListener { + public: + VL(flutter::EventSink* events) + : events_(events) {} + void OnValueChanged(const DataSnapshot& snapshot) override { + EncodableMap event; + event[EncodableValue("eventType")] = EncodableValue("value"); + event[EncodableValue("previousChildKey")] = EncodableValue(); + event[EncodableValue("snapshot")] = EncodableValue( + FirebaseDatabasePlugin::DataSnapshotToEncodableMap(snapshot)); + events_->Success(EncodableValue(event)); + } + void OnCancelled(const Error& error, + const char* error_message) override { + events_->Error(FirebaseDatabasePlugin::GetDatabaseErrorCode(error), + error_message ? error_message : "Unknown error"); + } + + private: + flutter::EventSink* events_; + }; + value_listener_ = new VL(events_.get()); + query_.AddValueListener(value_listener_); + } else { + // Child listener + class CL : public firebase::database::ChildListener { + public: + CL(flutter::EventSink* events, + const std::string& event_type) + : events_(events), event_type_(event_type) {} + void OnChildAdded(const DataSnapshot& snapshot, + const char* prev) override { + if (event_type_ == "childAdded") Send("childAdded", snapshot, prev); + } + void OnChildChanged(const DataSnapshot& snapshot, + const char* prev) override { + if (event_type_ == "childChanged") + Send("childChanged", snapshot, prev); + } + void OnChildMoved(const DataSnapshot& snapshot, + const char* prev) override { + if (event_type_ == "childMoved") Send("childMoved", snapshot, prev); + } + void OnChildRemoved(const DataSnapshot& snapshot) override { + if (event_type_ == "childRemoved") + Send("childRemoved", snapshot, nullptr); + } + void OnCancelled(const Error& error, + const char* error_message) override { + events_->Error(FirebaseDatabasePlugin::GetDatabaseErrorCode(error), + error_message ? error_message : "Unknown error"); + } + + private: + void Send(const std::string& type, const DataSnapshot& snapshot, + const char* prev) { + EncodableMap event; + event[EncodableValue("eventType")] = EncodableValue(type); + event[EncodableValue("previousChildKey")] = + prev ? EncodableValue(std::string(prev)) : EncodableValue(); + event[EncodableValue("snapshot")] = EncodableValue( + FirebaseDatabasePlugin::DataSnapshotToEncodableMap(snapshot)); + events_->Success(EncodableValue(event)); + } + flutter::EventSink* events_; + std::string event_type_; + }; + child_listener_ = new CL(events_.get(), event_type); + query_.AddChildListener(child_listener_); + } + + return nullptr; + } + + std::unique_ptr> + OnCancelInternal(const flutter::EncodableValue* arguments) override { + if (value_listener_) { + query_.RemoveValueListener(value_listener_); + delete value_listener_; + value_listener_ = nullptr; + } + if (child_listener_) { + query_.RemoveChildListener(child_listener_); + delete child_listener_; + child_listener_ = nullptr; + } + if (events_) { + events_->EndOfStream(); + } + return nullptr; + } + + private: + firebase::database::Query query_; + firebase::database::ValueListener* value_listener_; + firebase::database::ChildListener* child_listener_; + std::unique_ptr> events_; + }; + + auto handler = std::make_unique(query); + std::string channelName = RegisterEventChannel( + "plugins.flutter.io/firebase_database/query/", std::move(handler)); + + result(channelName); +} + +void FirebaseDatabasePlugin::QueryKeepSynced( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + firebase::database::Query query = + ApplyQueryModifiers(ref, request.modifiers()); + + bool keep_synced = request.value() ? *request.value() : false; + query.SetKeepSynchronized(keep_synced); + result(std::nullopt); +} + +void FirebaseDatabasePlugin::QueryGet( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) { + Database* database = GetDatabaseFromPigeon(app); + if (!database) { + result(FlutterError("unknown", "Database instance not found")); + return; + } + + DatabaseReference ref = database->GetReference(request.path().c_str()); + firebase::database::Query query = + ApplyQueryModifiers(ref, request.modifiers()); + + query.GetValue().OnCompletion([result](const Future& future) { + if (future.error() == Error::kErrorNone) { + const DataSnapshot* snapshot = future.result(); + EncodableMap result_map; + if (snapshot) { + result_map[EncodableValue("snapshot")] = EncodableValue( + FirebaseDatabasePlugin::DataSnapshotToEncodableMap(*snapshot)); + } else { + result_map[EncodableValue("snapshot")] = EncodableValue(); + } + result(result_map); + } else { + result(FirebaseDatabasePlugin::ParseError(future)); + } + }); +} + +} // namespace firebase_database_windows diff --git a/packages/firebase_database/firebase_database/windows/firebase_database_plugin.h b/packages/firebase_database/firebase_database/windows/firebase_database_plugin.h new file mode 100644 index 000000000000..6a6fa145f6e0 --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/firebase_database_plugin.h @@ -0,0 +1,144 @@ +/* + * Copyright 2025, 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. + */ + +#ifndef FLUTTER_PLUGIN_FIREBASE_DATABASE_PLUGIN_H_ +#define FLUTTER_PLUGIN_FIREBASE_DATABASE_PLUGIN_H_ + +#include +#include +#include + +#include + +#include "firebase/database.h" +#include "firebase/database/common.h" +#include "firebase/database/data_snapshot.h" +#include "messages.g.h" + +namespace firebase_database_windows { + +class FirebaseDatabasePlugin : public flutter::Plugin, + public FirebaseDatabaseHostApi { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + FirebaseDatabasePlugin(); + + virtual ~FirebaseDatabasePlugin(); + + // Disallow copy and assign. + FirebaseDatabasePlugin(const FirebaseDatabasePlugin&) = delete; + FirebaseDatabasePlugin& operator=(const FirebaseDatabasePlugin&) = delete; + + // Helper functions + static flutter::EncodableValue VariantToEncodableValue( + const firebase::Variant& variant); + static firebase::Variant EncodableValueToVariant( + const flutter::EncodableValue& value); + static std::string GetDatabaseErrorCode(firebase::database::Error error); + static std::string GetDatabaseErrorMessage(firebase::database::Error error); + static FlutterError ParseError(const firebase::FutureBase& future); + static flutter::EncodableMap DataSnapshotToEncodableMap( + const firebase::database::DataSnapshot& snapshot); + + // FirebaseDatabaseHostApi methods + void GoOnline( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) override; + void GoOffline( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) override; + void SetPersistenceEnabled( + const DatabasePigeonFirebaseApp& app, bool enabled, + std::function reply)> result) override; + void SetPersistenceCacheSizeBytes( + const DatabasePigeonFirebaseApp& app, int64_t cache_size, + std::function reply)> result) override; + void SetLoggingEnabled( + const DatabasePigeonFirebaseApp& app, bool enabled, + std::function reply)> result) override; + void UseDatabaseEmulator( + const DatabasePigeonFirebaseApp& app, const std::string& host, + int64_t port, + std::function reply)> result) override; + void Ref(const DatabasePigeonFirebaseApp& app, const std::string* path, + std::function reply)> result) + override; + void RefFromURL(const DatabasePigeonFirebaseApp& app, const std::string& url, + std::function reply)> + result) override; + void PurgeOutstandingWrites( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) override; + void DatabaseReferenceSet( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) override; + void DatabaseReferenceSetWithPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) override; + void DatabaseReferenceUpdate( + const DatabasePigeonFirebaseApp& app, const UpdateRequest& request, + std::function reply)> result) override; + void DatabaseReferenceSetPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) override; + void DatabaseReferenceRunTransaction( + const DatabasePigeonFirebaseApp& app, const TransactionRequest& request, + std::function reply)> result) override; + void DatabaseReferenceGetTransactionResult( + const DatabasePigeonFirebaseApp& app, int64_t transaction_key, + std::function reply)> result) + override; + void OnDisconnectSet( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) override; + void OnDisconnectSetWithPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) override; + void OnDisconnectUpdate( + const DatabasePigeonFirebaseApp& app, const UpdateRequest& request, + std::function reply)> result) override; + void OnDisconnectCancel( + const DatabasePigeonFirebaseApp& app, const std::string& path, + std::function reply)> result) override; + void QueryObserve( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) override; + void QueryKeepSynced( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) override; + void QueryGet(const DatabasePigeonFirebaseApp& app, + const QueryRequest& request, + std::function reply)> + result) override; + + static flutter::BinaryMessenger* messenger_; + static std::map< + std::string, + std::unique_ptr>> + event_channels_; + static std::map>> + stream_handlers_; + static std::map + database_instances_; + + private: + firebase::database::Database* GetDatabaseFromPigeon( + const DatabasePigeonFirebaseApp& app); + firebase::database::Query ApplyQueryModifiers( + firebase::database::Query query, const flutter::EncodableList& modifiers); + + std::map transaction_results_; +}; + +} // namespace firebase_database_windows + +#endif /* FLUTTER_PLUGIN_FIREBASE_DATABASE_PLUGIN_H_ */ diff --git a/packages/firebase_database/firebase_database/windows/firebase_database_plugin_c_api.cpp b/packages/firebase_database/firebase_database/windows/firebase_database_plugin_c_api.cpp new file mode 100644 index 000000000000..41cf8fae1737 --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/firebase_database_plugin_c_api.cpp @@ -0,0 +1,16 @@ +// Copyright 2025, 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. + +#include "include/firebase_database/firebase_database_plugin_c_api.h" + +#include + +#include "firebase_database_plugin.h" + +void FirebaseDatabasePluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + firebase_database_windows::FirebaseDatabasePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/firebase_database/firebase_database/windows/include/firebase_database/firebase_database_plugin_c_api.h b/packages/firebase_database/firebase_database/windows/include/firebase_database/firebase_database_plugin_c_api.h new file mode 100644 index 000000000000..f333dc88edf3 --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/include/firebase_database/firebase_database_plugin_c_api.h @@ -0,0 +1,29 @@ +/* + * Copyright 2025, 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. + */ + +#ifndef FLUTTER_PLUGIN_FIREBASE_DATABASE_PLUGIN_C_API_H_ +#define FLUTTER_PLUGIN_FIREBASE_DATABASE_PLUGIN_C_API_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FirebaseDatabasePluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif /* FLUTTER_PLUGIN_FIREBASE_DATABASE_PLUGIN_C_API_H_ */ diff --git a/packages/firebase_database/firebase_database/windows/messages.g.cpp b/packages/firebase_database/firebase_database/windows/messages.g.cpp new file mode 100644 index 000000000000..8c0aeeb54adc --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/messages.g.cpp @@ -0,0 +1,1742 @@ +// Copyright 2025, 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. +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace firebase_database_windows { +using flutter::BasicMessageChannel; +using flutter::CustomEncodableValue; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +FlutterError CreateConnectionError(const std::string channel_name) { + return FlutterError( + "channel-error", + "Unable to establish connection on channel: '" + channel_name + "'.", + EncodableValue("")); +} + +// DatabasePigeonSettings + +DatabasePigeonSettings::DatabasePigeonSettings() {} + +DatabasePigeonSettings::DatabasePigeonSettings(const bool* persistence_enabled, + const int64_t* cache_size_bytes, + const bool* logging_enabled, + const std::string* emulator_host, + const int64_t* emulator_port) + : persistence_enabled_(persistence_enabled + ? std::optional(*persistence_enabled) + : std::nullopt), + cache_size_bytes_(cache_size_bytes + ? std::optional(*cache_size_bytes) + : std::nullopt), + logging_enabled_(logging_enabled ? std::optional(*logging_enabled) + : std::nullopt), + emulator_host_(emulator_host ? std::optional(*emulator_host) + : std::nullopt), + emulator_port_(emulator_port ? std::optional(*emulator_port) + : std::nullopt) {} + +const bool* DatabasePigeonSettings::persistence_enabled() const { + return persistence_enabled_ ? &(*persistence_enabled_) : nullptr; +} + +void DatabasePigeonSettings::set_persistence_enabled(const bool* value_arg) { + persistence_enabled_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabasePigeonSettings::set_persistence_enabled(bool value_arg) { + persistence_enabled_ = value_arg; +} + +const int64_t* DatabasePigeonSettings::cache_size_bytes() const { + return cache_size_bytes_ ? &(*cache_size_bytes_) : nullptr; +} + +void DatabasePigeonSettings::set_cache_size_bytes(const int64_t* value_arg) { + cache_size_bytes_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabasePigeonSettings::set_cache_size_bytes(int64_t value_arg) { + cache_size_bytes_ = value_arg; +} + +const bool* DatabasePigeonSettings::logging_enabled() const { + return logging_enabled_ ? &(*logging_enabled_) : nullptr; +} + +void DatabasePigeonSettings::set_logging_enabled(const bool* value_arg) { + logging_enabled_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabasePigeonSettings::set_logging_enabled(bool value_arg) { + logging_enabled_ = value_arg; +} + +const std::string* DatabasePigeonSettings::emulator_host() const { + return emulator_host_ ? &(*emulator_host_) : nullptr; +} + +void DatabasePigeonSettings::set_emulator_host( + const std::string_view* value_arg) { + emulator_host_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabasePigeonSettings::set_emulator_host(std::string_view value_arg) { + emulator_host_ = value_arg; +} + +const int64_t* DatabasePigeonSettings::emulator_port() const { + return emulator_port_ ? &(*emulator_port_) : nullptr; +} + +void DatabasePigeonSettings::set_emulator_port(const int64_t* value_arg) { + emulator_port_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabasePigeonSettings::set_emulator_port(int64_t value_arg) { + emulator_port_ = value_arg; +} + +EncodableList DatabasePigeonSettings::ToEncodableList() const { + EncodableList list; + list.reserve(5); + list.push_back(persistence_enabled_ ? EncodableValue(*persistence_enabled_) + : EncodableValue()); + list.push_back(cache_size_bytes_ ? EncodableValue(*cache_size_bytes_) + : EncodableValue()); + list.push_back(logging_enabled_ ? EncodableValue(*logging_enabled_) + : EncodableValue()); + list.push_back(emulator_host_ ? EncodableValue(*emulator_host_) + : EncodableValue()); + list.push_back(emulator_port_ ? EncodableValue(*emulator_port_) + : EncodableValue()); + return list; +} + +DatabasePigeonSettings DatabasePigeonSettings::FromEncodableList( + const EncodableList& list) { + DatabasePigeonSettings decoded; + auto& encodable_persistence_enabled = list[0]; + if (!encodable_persistence_enabled.IsNull()) { + decoded.set_persistence_enabled( + std::get(encodable_persistence_enabled)); + } + auto& encodable_cache_size_bytes = list[1]; + if (!encodable_cache_size_bytes.IsNull()) { + decoded.set_cache_size_bytes(std::get(encodable_cache_size_bytes)); + } + auto& encodable_logging_enabled = list[2]; + if (!encodable_logging_enabled.IsNull()) { + decoded.set_logging_enabled(std::get(encodable_logging_enabled)); + } + auto& encodable_emulator_host = list[3]; + if (!encodable_emulator_host.IsNull()) { + decoded.set_emulator_host(std::get(encodable_emulator_host)); + } + auto& encodable_emulator_port = list[4]; + if (!encodable_emulator_port.IsNull()) { + decoded.set_emulator_port(std::get(encodable_emulator_port)); + } + return decoded; +} + +// DatabasePigeonFirebaseApp + +DatabasePigeonFirebaseApp::DatabasePigeonFirebaseApp( + const std::string& app_name, const DatabasePigeonSettings& settings) + : app_name_(app_name), + settings_(std::make_unique(settings)) {} + +DatabasePigeonFirebaseApp::DatabasePigeonFirebaseApp( + const std::string& app_name, const std::string* database_u_r_l, + const DatabasePigeonSettings& settings) + : app_name_(app_name), + database_u_r_l_(database_u_r_l + ? std::optional(*database_u_r_l) + : std::nullopt), + settings_(std::make_unique(settings)) {} + +DatabasePigeonFirebaseApp::DatabasePigeonFirebaseApp( + const DatabasePigeonFirebaseApp& other) + : app_name_(other.app_name_), + database_u_r_l_(other.database_u_r_l_ + ? std::optional(*other.database_u_r_l_) + : std::nullopt), + settings_(std::make_unique(*other.settings_)) {} + +DatabasePigeonFirebaseApp& DatabasePigeonFirebaseApp::operator=( + const DatabasePigeonFirebaseApp& other) { + app_name_ = other.app_name_; + database_u_r_l_ = other.database_u_r_l_; + settings_ = std::make_unique(*other.settings_); + return *this; +} + +const std::string& DatabasePigeonFirebaseApp::app_name() const { + return app_name_; +} + +void DatabasePigeonFirebaseApp::set_app_name(std::string_view value_arg) { + app_name_ = value_arg; +} + +const std::string* DatabasePigeonFirebaseApp::database_u_r_l() const { + return database_u_r_l_ ? &(*database_u_r_l_) : nullptr; +} + +void DatabasePigeonFirebaseApp::set_database_u_r_l( + const std::string_view* value_arg) { + database_u_r_l_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabasePigeonFirebaseApp::set_database_u_r_l(std::string_view value_arg) { + database_u_r_l_ = value_arg; +} + +const DatabasePigeonSettings& DatabasePigeonFirebaseApp::settings() const { + return *settings_; +} + +void DatabasePigeonFirebaseApp::set_settings( + const DatabasePigeonSettings& value_arg) { + settings_ = std::make_unique(value_arg); +} + +EncodableList DatabasePigeonFirebaseApp::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(EncodableValue(app_name_)); + list.push_back(database_u_r_l_ ? EncodableValue(*database_u_r_l_) + : EncodableValue()); + list.push_back(CustomEncodableValue(*settings_)); + return list; +} + +DatabasePigeonFirebaseApp DatabasePigeonFirebaseApp::FromEncodableList( + const EncodableList& list) { + DatabasePigeonFirebaseApp decoded( + std::get(list[0]), + std::any_cast( + std::get(list[2]))); + auto& encodable_database_u_r_l = list[1]; + if (!encodable_database_u_r_l.IsNull()) { + decoded.set_database_u_r_l(std::get(encodable_database_u_r_l)); + } + return decoded; +} + +// DatabaseReferencePlatform + +DatabaseReferencePlatform::DatabaseReferencePlatform(const std::string& path) + : path_(path) {} + +const std::string& DatabaseReferencePlatform::path() const { return path_; } + +void DatabaseReferencePlatform::set_path(std::string_view value_arg) { + path_ = value_arg; +} + +EncodableList DatabaseReferencePlatform::ToEncodableList() const { + EncodableList list; + list.reserve(1); + list.push_back(EncodableValue(path_)); + return list; +} + +DatabaseReferencePlatform DatabaseReferencePlatform::FromEncodableList( + const EncodableList& list) { + DatabaseReferencePlatform decoded(std::get(list[0])); + return decoded; +} + +// DatabaseReferenceRequest + +DatabaseReferenceRequest::DatabaseReferenceRequest(const std::string& path) + : path_(path) {} + +DatabaseReferenceRequest::DatabaseReferenceRequest( + const std::string& path, const EncodableValue* value, + const EncodableValue* priority) + : path_(path), + value_(value ? std::optional(*value) : std::nullopt), + priority_(priority ? std::optional(*priority) + : std::nullopt) {} + +const std::string& DatabaseReferenceRequest::path() const { return path_; } + +void DatabaseReferenceRequest::set_path(std::string_view value_arg) { + path_ = value_arg; +} + +const EncodableValue* DatabaseReferenceRequest::value() const { + return value_ ? &(*value_) : nullptr; +} + +void DatabaseReferenceRequest::set_value(const EncodableValue* value_arg) { + value_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabaseReferenceRequest::set_value(const EncodableValue& value_arg) { + value_ = value_arg; +} + +const EncodableValue* DatabaseReferenceRequest::priority() const { + return priority_ ? &(*priority_) : nullptr; +} + +void DatabaseReferenceRequest::set_priority(const EncodableValue* value_arg) { + priority_ = + value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DatabaseReferenceRequest::set_priority(const EncodableValue& value_arg) { + priority_ = value_arg; +} + +EncodableList DatabaseReferenceRequest::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(EncodableValue(path_)); + list.push_back(value_ ? *value_ : EncodableValue()); + list.push_back(priority_ ? *priority_ : EncodableValue()); + return list; +} + +DatabaseReferenceRequest DatabaseReferenceRequest::FromEncodableList( + const EncodableList& list) { + DatabaseReferenceRequest decoded(std::get(list[0])); + auto& encodable_value = list[1]; + if (!encodable_value.IsNull()) { + decoded.set_value(encodable_value); + } + auto& encodable_priority = list[2]; + if (!encodable_priority.IsNull()) { + decoded.set_priority(encodable_priority); + } + return decoded; +} + +// UpdateRequest + +UpdateRequest::UpdateRequest(const std::string& path, const EncodableMap& value) + : path_(path), value_(value) {} + +const std::string& UpdateRequest::path() const { return path_; } + +void UpdateRequest::set_path(std::string_view value_arg) { path_ = value_arg; } + +const EncodableMap& UpdateRequest::value() const { return value_; } + +void UpdateRequest::set_value(const EncodableMap& value_arg) { + value_ = value_arg; +} + +EncodableList UpdateRequest::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(path_)); + list.push_back(EncodableValue(value_)); + return list; +} + +UpdateRequest UpdateRequest::FromEncodableList(const EncodableList& list) { + UpdateRequest decoded(std::get(list[0]), + std::get(list[1])); + return decoded; +} + +// TransactionRequest + +TransactionRequest::TransactionRequest(const std::string& path, + int64_t transaction_key, + bool apply_locally) + : path_(path), + transaction_key_(transaction_key), + apply_locally_(apply_locally) {} + +const std::string& TransactionRequest::path() const { return path_; } + +void TransactionRequest::set_path(std::string_view value_arg) { + path_ = value_arg; +} + +int64_t TransactionRequest::transaction_key() const { return transaction_key_; } + +void TransactionRequest::set_transaction_key(int64_t value_arg) { + transaction_key_ = value_arg; +} + +bool TransactionRequest::apply_locally() const { return apply_locally_; } + +void TransactionRequest::set_apply_locally(bool value_arg) { + apply_locally_ = value_arg; +} + +EncodableList TransactionRequest::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(EncodableValue(path_)); + list.push_back(EncodableValue(transaction_key_)); + list.push_back(EncodableValue(apply_locally_)); + return list; +} + +TransactionRequest TransactionRequest::FromEncodableList( + const EncodableList& list) { + TransactionRequest decoded(std::get(list[0]), + std::get(list[1]), + std::get(list[2])); + return decoded; +} + +// QueryRequest + +QueryRequest::QueryRequest(const std::string& path, + const EncodableList& modifiers) + : path_(path), modifiers_(modifiers) {} + +QueryRequest::QueryRequest(const std::string& path, + const EncodableList& modifiers, const bool* value) + : path_(path), + modifiers_(modifiers), + value_(value ? std::optional(*value) : std::nullopt) {} + +const std::string& QueryRequest::path() const { return path_; } + +void QueryRequest::set_path(std::string_view value_arg) { path_ = value_arg; } + +const EncodableList& QueryRequest::modifiers() const { return modifiers_; } + +void QueryRequest::set_modifiers(const EncodableList& value_arg) { + modifiers_ = value_arg; +} + +const bool* QueryRequest::value() const { + return value_ ? &(*value_) : nullptr; +} + +void QueryRequest::set_value(const bool* value_arg) { + value_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void QueryRequest::set_value(bool value_arg) { value_ = value_arg; } + +EncodableList QueryRequest::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(EncodableValue(path_)); + list.push_back(EncodableValue(modifiers_)); + list.push_back(value_ ? EncodableValue(*value_) : EncodableValue()); + return list; +} + +QueryRequest QueryRequest::FromEncodableList(const EncodableList& list) { + QueryRequest decoded(std::get(list[0]), + std::get(list[1])); + auto& encodable_value = list[2]; + if (!encodable_value.IsNull()) { + decoded.set_value(std::get(encodable_value)); + } + return decoded; +} + +// TransactionHandlerResult + +TransactionHandlerResult::TransactionHandlerResult(bool aborted, bool exception) + : aborted_(aborted), exception_(exception) {} + +TransactionHandlerResult::TransactionHandlerResult(const EncodableValue* value, + bool aborted, bool exception) + : value_(value ? std::optional(*value) : std::nullopt), + aborted_(aborted), + exception_(exception) {} + +const EncodableValue* TransactionHandlerResult::value() const { + return value_ ? &(*value_) : nullptr; +} + +void TransactionHandlerResult::set_value(const EncodableValue* value_arg) { + value_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void TransactionHandlerResult::set_value(const EncodableValue& value_arg) { + value_ = value_arg; +} + +bool TransactionHandlerResult::aborted() const { return aborted_; } + +void TransactionHandlerResult::set_aborted(bool value_arg) { + aborted_ = value_arg; +} + +bool TransactionHandlerResult::exception() const { return exception_; } + +void TransactionHandlerResult::set_exception(bool value_arg) { + exception_ = value_arg; +} + +EncodableList TransactionHandlerResult::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(value_ ? *value_ : EncodableValue()); + list.push_back(EncodableValue(aborted_)); + list.push_back(EncodableValue(exception_)); + return list; +} + +TransactionHandlerResult TransactionHandlerResult::FromEncodableList( + const EncodableList& list) { + TransactionHandlerResult decoded(std::get(list[1]), + std::get(list[2])); + auto& encodable_value = list[0]; + if (!encodable_value.IsNull()) { + decoded.set_value(encodable_value); + } + return decoded; +} + +PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} + +EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( + uint8_t type, flutter::ByteStreamReader* stream) const { + switch (type) { + case 129: { + return CustomEncodableValue(DatabasePigeonSettings::FromEncodableList( + std::get(ReadValue(stream)))); + } + case 130: { + return CustomEncodableValue(DatabasePigeonFirebaseApp::FromEncodableList( + std::get(ReadValue(stream)))); + } + case 131: { + return CustomEncodableValue(DatabaseReferencePlatform::FromEncodableList( + std::get(ReadValue(stream)))); + } + case 132: { + return CustomEncodableValue(DatabaseReferenceRequest::FromEncodableList( + std::get(ReadValue(stream)))); + } + case 133: { + return CustomEncodableValue(UpdateRequest::FromEncodableList( + std::get(ReadValue(stream)))); + } + case 134: { + return CustomEncodableValue(TransactionRequest::FromEncodableList( + std::get(ReadValue(stream)))); + } + case 135: { + return CustomEncodableValue(QueryRequest::FromEncodableList( + std::get(ReadValue(stream)))); + } + case 136: { + return CustomEncodableValue(TransactionHandlerResult::FromEncodableList( + std::get(ReadValue(stream)))); + } + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void PigeonInternalCodecSerializer::WriteValue( + const EncodableValue& value, flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(DatabasePigeonSettings)) { + stream->WriteByte(129); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(DatabasePigeonFirebaseApp)) { + stream->WriteByte(130); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(DatabaseReferencePlatform)) { + stream->WriteByte(131); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(DatabaseReferenceRequest)) { + stream->WriteByte(132); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(UpdateRequest)) { + stream->WriteByte(133); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TransactionRequest)) { + stream->WriteByte(134); + WriteValue(EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(QueryRequest)) { + stream->WriteByte(135); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TransactionHandlerResult)) { + stream->WriteByte(136); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +/// The codec used by FirebaseDatabaseHostApi. +const flutter::StandardMessageCodec& FirebaseDatabaseHostApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &PigeonInternalCodecSerializer::GetInstance()); +} + +// Sets up an instance of `FirebaseDatabaseHostApi` to handle messages through +// the `binary_messenger`. +void FirebaseDatabaseHostApi::SetUp(flutter::BinaryMessenger* binary_messenger, + FirebaseDatabaseHostApi* api) { + FirebaseDatabaseHostApi::SetUp(binary_messenger, api, ""); +} + +void FirebaseDatabaseHostApi::SetUp(flutter::BinaryMessenger* binary_messenger, + FirebaseDatabaseHostApi* api, + const std::string& message_channel_suffix) { + const std::string prepended_suffix = + message_channel_suffix.length() > 0 + ? std::string(".") + message_channel_suffix + : ""; + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.goOnline" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + api->GoOnline(app_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.goOffline" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + api->GoOffline(app_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.setPersistenceEnabled" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_enabled_arg = args.at(1); + if (encodable_enabled_arg.IsNull()) { + reply(WrapError("enabled_arg unexpectedly null.")); + return; + } + const auto& enabled_arg = std::get(encodable_enabled_arg); + api->SetPersistenceEnabled( + app_arg, enabled_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.setPersistenceCacheSizeBytes" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_cache_size_arg = args.at(1); + if (encodable_cache_size_arg.IsNull()) { + reply(WrapError("cache_size_arg unexpectedly null.")); + return; + } + const int64_t cache_size_arg = + encodable_cache_size_arg.LongValue(); + api->SetPersistenceCacheSizeBytes( + app_arg, cache_size_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.setLoggingEnabled" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_enabled_arg = args.at(1); + if (encodable_enabled_arg.IsNull()) { + reply(WrapError("enabled_arg unexpectedly null.")); + return; + } + const auto& enabled_arg = std::get(encodable_enabled_arg); + api->SetLoggingEnabled( + app_arg, enabled_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.useDatabaseEmulator" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_host_arg = args.at(1); + if (encodable_host_arg.IsNull()) { + reply(WrapError("host_arg unexpectedly null.")); + return; + } + const auto& host_arg = std::get(encodable_host_arg); + const auto& encodable_port_arg = args.at(2); + if (encodable_port_arg.IsNull()) { + reply(WrapError("port_arg unexpectedly null.")); + return; + } + const int64_t port_arg = encodable_port_arg.LongValue(); + api->UseDatabaseEmulator( + app_arg, host_arg, port_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.ref" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_path_arg = args.at(1); + const auto* path_arg = + std::get_if(&encodable_path_arg); + api->Ref(app_arg, path_arg, + [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue( + std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.refFromURL" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_url_arg = args.at(1); + if (encodable_url_arg.IsNull()) { + reply(WrapError("url_arg unexpectedly null.")); + return; + } + const auto& url_arg = std::get(encodable_url_arg); + api->RefFromURL( + app_arg, url_arg, + [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.purgeOutstandingWrites" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + api->PurgeOutstandingWrites( + app_arg, [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.databaseReferenceSet" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = + std::any_cast( + std::get(encodable_request_arg)); + api->DatabaseReferenceSet( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.databaseReferenceSetWithPriority" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = + std::any_cast( + std::get(encodable_request_arg)); + api->DatabaseReferenceSetWithPriority( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.databaseReferenceUpdate" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = std::any_cast( + std::get(encodable_request_arg)); + api->DatabaseReferenceUpdate( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.databaseReferenceSetPriority" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = + std::any_cast( + std::get(encodable_request_arg)); + api->DatabaseReferenceSetPriority( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.databaseReferenceRunTransaction" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = + std::any_cast( + std::get(encodable_request_arg)); + api->DatabaseReferenceRunTransaction( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.databaseReferenceGetTransactionResult" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_transaction_key_arg = args.at(1); + if (encodable_transaction_key_arg.IsNull()) { + reply(WrapError("transaction_key_arg unexpectedly null.")); + return; + } + const int64_t transaction_key_arg = + encodable_transaction_key_arg.LongValue(); + api->DatabaseReferenceGetTransactionResult( + app_arg, transaction_key_arg, + [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.onDisconnectSet" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = + std::any_cast( + std::get(encodable_request_arg)); + api->OnDisconnectSet( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.onDisconnectSetWithPriority" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = + std::any_cast( + std::get(encodable_request_arg)); + api->OnDisconnectSetWithPriority( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.onDisconnectUpdate" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = std::any_cast( + std::get(encodable_request_arg)); + api->OnDisconnectUpdate( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.onDisconnectCancel" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_path_arg = args.at(1); + if (encodable_path_arg.IsNull()) { + reply(WrapError("path_arg unexpectedly null.")); + return; + } + const auto& path_arg = std::get(encodable_path_arg); + api->OnDisconnectCancel( + app_arg, path_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.queryObserve" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = std::any_cast( + std::get(encodable_request_arg)); + api->QueryObserve( + app_arg, request_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.queryKeepSynced" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = std::any_cast( + std::get(encodable_request_arg)); + api->QueryKeepSynced( + app_arg, request_arg, + [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel( + binary_messenger, + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseHostApi.queryGet" + + prepended_suffix, + &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_app_arg = args.at(0); + if (encodable_app_arg.IsNull()) { + reply(WrapError("app_arg unexpectedly null.")); + return; + } + const auto& app_arg = + std::any_cast( + std::get(encodable_app_arg)); + const auto& encodable_request_arg = args.at(1); + if (encodable_request_arg.IsNull()) { + reply(WrapError("request_arg unexpectedly null.")); + return; + } + const auto& request_arg = std::any_cast( + std::get(encodable_request_arg)); + api->QueryGet(app_arg, request_arg, + [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue( + std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } +} + +EncodableValue FirebaseDatabaseHostApi::WrapError( + std::string_view error_message) { + return EncodableValue( + EncodableList{EncodableValue(std::string(error_message)), + EncodableValue("Error"), EncodableValue()}); +} + +EncodableValue FirebaseDatabaseHostApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{EncodableValue(error.code()), + EncodableValue(error.message()), + error.details()}); +} + +// Generated class from Pigeon that represents Flutter messages that can be +// called from C++. +FirebaseDatabaseFlutterApi::FirebaseDatabaseFlutterApi( + flutter::BinaryMessenger* binary_messenger) + : binary_messenger_(binary_messenger), message_channel_suffix_("") {} + +FirebaseDatabaseFlutterApi::FirebaseDatabaseFlutterApi( + flutter::BinaryMessenger* binary_messenger, + const std::string& message_channel_suffix) + : binary_messenger_(binary_messenger), + message_channel_suffix_(message_channel_suffix.length() > 0 + ? std::string(".") + message_channel_suffix + : "") {} + +const flutter::StandardMessageCodec& FirebaseDatabaseFlutterApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &PigeonInternalCodecSerializer::GetInstance()); +} + +void FirebaseDatabaseFlutterApi::CallTransactionHandler( + int64_t transaction_key_arg, const EncodableValue* snapshot_value_arg, + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = + "dev.flutter.pigeon.firebase_database_platform_interface." + "FirebaseDatabaseFlutterApi.callTransactionHandler" + + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + EncodableValue(transaction_key_arg), + snapshot_value_arg ? *snapshot_value_arg : EncodableValue(), + }); + channel.Send( + encoded_api_arguments, [channel_name, on_success = std::move(on_success), + on_error = std::move(on_error)]( + const uint8_t* reply, size_t reply_size) { + std::unique_ptr response = + GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = + std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error( + FlutterError(std::get(list_return_value->at(0)), + std::get(list_return_value->at(1)), + list_return_value->at(2))); + } else { + const auto& return_value = + std::any_cast( + std::get(list_return_value->at(0))); + on_success(return_value); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + +} // namespace firebase_database_windows diff --git a/packages/firebase_database/firebase_database/windows/messages.g.h b/packages/firebase_database/firebase_database/windows/messages.g.h new file mode 100644 index 000000000000..0a44f40b593f --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/messages.g.h @@ -0,0 +1,449 @@ +// Copyright 2025, 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. +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_MESSAGES_G_H_ +#define PIGEON_MESSAGES_G_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace firebase_database_windows { + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, + const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template +class ErrorOr { + public: + ErrorOr(const T& rhs) : v_(rhs) {} + ErrorOr(const T&& rhs) : v_(std::move(rhs)) {} + ErrorOr(const FlutterError& rhs) : v_(rhs) {} + ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {} + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class DatabasePigeonSettings { + public: + // Constructs an object setting all non-nullable fields. + DatabasePigeonSettings(); + + // Constructs an object setting all fields. + explicit DatabasePigeonSettings(const bool* persistence_enabled, + const int64_t* cache_size_bytes, + const bool* logging_enabled, + const std::string* emulator_host, + const int64_t* emulator_port); + + const bool* persistence_enabled() const; + void set_persistence_enabled(const bool* value_arg); + void set_persistence_enabled(bool value_arg); + + const int64_t* cache_size_bytes() const; + void set_cache_size_bytes(const int64_t* value_arg); + void set_cache_size_bytes(int64_t value_arg); + + const bool* logging_enabled() const; + void set_logging_enabled(const bool* value_arg); + void set_logging_enabled(bool value_arg); + + const std::string* emulator_host() const; + void set_emulator_host(const std::string_view* value_arg); + void set_emulator_host(std::string_view value_arg); + + const int64_t* emulator_port() const; + void set_emulator_port(const int64_t* value_arg); + void set_emulator_port(int64_t value_arg); + + private: + static DatabasePigeonSettings FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class DatabasePigeonFirebaseApp; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::optional persistence_enabled_; + std::optional cache_size_bytes_; + std::optional logging_enabled_; + std::optional emulator_host_; + std::optional emulator_port_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class DatabasePigeonFirebaseApp { + public: + // Constructs an object setting all non-nullable fields. + explicit DatabasePigeonFirebaseApp(const std::string& app_name, + const DatabasePigeonSettings& settings); + + // Constructs an object setting all fields. + explicit DatabasePigeonFirebaseApp(const std::string& app_name, + const std::string* database_u_r_l, + const DatabasePigeonSettings& settings); + + ~DatabasePigeonFirebaseApp() = default; + DatabasePigeonFirebaseApp(const DatabasePigeonFirebaseApp& other); + DatabasePigeonFirebaseApp& operator=(const DatabasePigeonFirebaseApp& other); + DatabasePigeonFirebaseApp(DatabasePigeonFirebaseApp&& other) = default; + DatabasePigeonFirebaseApp& operator=( + DatabasePigeonFirebaseApp&& other) noexcept = default; + const std::string& app_name() const; + void set_app_name(std::string_view value_arg); + + const std::string* database_u_r_l() const; + void set_database_u_r_l(const std::string_view* value_arg); + void set_database_u_r_l(std::string_view value_arg); + + const DatabasePigeonSettings& settings() const; + void set_settings(const DatabasePigeonSettings& value_arg); + + private: + static DatabasePigeonFirebaseApp FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string app_name_; + std::optional database_u_r_l_; + std::unique_ptr settings_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class DatabaseReferencePlatform { + public: + // Constructs an object setting all fields. + explicit DatabaseReferencePlatform(const std::string& path); + + const std::string& path() const; + void set_path(std::string_view value_arg); + + private: + static DatabaseReferencePlatform FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string path_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class DatabaseReferenceRequest { + public: + // Constructs an object setting all non-nullable fields. + explicit DatabaseReferenceRequest(const std::string& path); + + // Constructs an object setting all fields. + explicit DatabaseReferenceRequest(const std::string& path, + const flutter::EncodableValue* value, + const flutter::EncodableValue* priority); + + const std::string& path() const; + void set_path(std::string_view value_arg); + + const flutter::EncodableValue* value() const; + void set_value(const flutter::EncodableValue* value_arg); + void set_value(const flutter::EncodableValue& value_arg); + + const flutter::EncodableValue* priority() const; + void set_priority(const flutter::EncodableValue* value_arg); + void set_priority(const flutter::EncodableValue& value_arg); + + private: + static DatabaseReferenceRequest FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string path_; + std::optional value_; + std::optional priority_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class UpdateRequest { + public: + // Constructs an object setting all fields. + explicit UpdateRequest(const std::string& path, + const flutter::EncodableMap& value); + + const std::string& path() const; + void set_path(std::string_view value_arg); + + const flutter::EncodableMap& value() const; + void set_value(const flutter::EncodableMap& value_arg); + + private: + static UpdateRequest FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string path_; + flutter::EncodableMap value_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class TransactionRequest { + public: + // Constructs an object setting all fields. + explicit TransactionRequest(const std::string& path, int64_t transaction_key, + bool apply_locally); + + const std::string& path() const; + void set_path(std::string_view value_arg); + + int64_t transaction_key() const; + void set_transaction_key(int64_t value_arg); + + bool apply_locally() const; + void set_apply_locally(bool value_arg); + + private: + static TransactionRequest FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string path_; + int64_t transaction_key_; + bool apply_locally_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class QueryRequest { + public: + // Constructs an object setting all non-nullable fields. + explicit QueryRequest(const std::string& path, + const flutter::EncodableList& modifiers); + + // Constructs an object setting all fields. + explicit QueryRequest(const std::string& path, + const flutter::EncodableList& modifiers, + const bool* value); + + const std::string& path() const; + void set_path(std::string_view value_arg); + + const flutter::EncodableList& modifiers() const; + void set_modifiers(const flutter::EncodableList& value_arg); + + const bool* value() const; + void set_value(const bool* value_arg); + void set_value(bool value_arg); + + private: + static QueryRequest FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string path_; + flutter::EncodableList modifiers_; + std::optional value_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class TransactionHandlerResult { + public: + // Constructs an object setting all non-nullable fields. + explicit TransactionHandlerResult(bool aborted, bool exception); + + // Constructs an object setting all fields. + explicit TransactionHandlerResult(const flutter::EncodableValue* value, + bool aborted, bool exception); + + const flutter::EncodableValue* value() const; + void set_value(const flutter::EncodableValue* value_arg); + void set_value(const flutter::EncodableValue& value_arg); + + bool aborted() const; + void set_aborted(bool value_arg); + + bool exception() const; + void set_exception(bool value_arg); + + private: + static TransactionHandlerResult FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseDatabaseHostApi; + friend class FirebaseDatabaseFlutterApi; + friend class PigeonInternalCodecSerializer; + std::optional value_; + bool aborted_; + bool exception_; +}; + +class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { + public: + PigeonInternalCodecSerializer(); + inline static PigeonInternalCodecSerializer& GetInstance() { + static PigeonInternalCodecSerializer sInstance; + return sInstance; + } + + void WriteValue(const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const override; + + protected: + flutter::EncodableValue ReadValueOfType( + uint8_t type, flutter::ByteStreamReader* stream) const override; +}; + +// Generated interface from Pigeon that represents a handler of messages from +// Flutter. +class FirebaseDatabaseHostApi { + public: + FirebaseDatabaseHostApi(const FirebaseDatabaseHostApi&) = delete; + FirebaseDatabaseHostApi& operator=(const FirebaseDatabaseHostApi&) = delete; + virtual ~FirebaseDatabaseHostApi() {} + virtual void GoOnline( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) = 0; + virtual void GoOffline( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) = 0; + virtual void SetPersistenceEnabled( + const DatabasePigeonFirebaseApp& app, bool enabled, + std::function reply)> result) = 0; + virtual void SetPersistenceCacheSizeBytes( + const DatabasePigeonFirebaseApp& app, int64_t cache_size, + std::function reply)> result) = 0; + virtual void SetLoggingEnabled( + const DatabasePigeonFirebaseApp& app, bool enabled, + std::function reply)> result) = 0; + virtual void UseDatabaseEmulator( + const DatabasePigeonFirebaseApp& app, const std::string& host, + int64_t port, + std::function reply)> result) = 0; + virtual void Ref( + const DatabasePigeonFirebaseApp& app, const std::string* path, + std::function reply)> result) = 0; + virtual void RefFromURL( + const DatabasePigeonFirebaseApp& app, const std::string& url, + std::function reply)> result) = 0; + virtual void PurgeOutstandingWrites( + const DatabasePigeonFirebaseApp& app, + std::function reply)> result) = 0; + virtual void DatabaseReferenceSet( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) = 0; + virtual void DatabaseReferenceSetWithPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) = 0; + virtual void DatabaseReferenceUpdate( + const DatabasePigeonFirebaseApp& app, const UpdateRequest& request, + std::function reply)> result) = 0; + virtual void DatabaseReferenceSetPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) = 0; + virtual void DatabaseReferenceRunTransaction( + const DatabasePigeonFirebaseApp& app, const TransactionRequest& request, + std::function reply)> result) = 0; + virtual void DatabaseReferenceGetTransactionResult( + const DatabasePigeonFirebaseApp& app, int64_t transaction_key, + std::function reply)> result) = 0; + virtual void OnDisconnectSet( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) = 0; + virtual void OnDisconnectSetWithPriority( + const DatabasePigeonFirebaseApp& app, + const DatabaseReferenceRequest& request, + std::function reply)> result) = 0; + virtual void OnDisconnectUpdate( + const DatabasePigeonFirebaseApp& app, const UpdateRequest& request, + std::function reply)> result) = 0; + virtual void OnDisconnectCancel( + const DatabasePigeonFirebaseApp& app, const std::string& path, + std::function reply)> result) = 0; + virtual void QueryObserve( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) = 0; + virtual void QueryKeepSynced( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) = 0; + virtual void QueryGet( + const DatabasePigeonFirebaseApp& app, const QueryRequest& request, + std::function reply)> result) = 0; + + // The codec used by FirebaseDatabaseHostApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `FirebaseDatabaseHostApi` to handle messages through + // the `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + FirebaseDatabaseHostApi* api); + static void SetUp(flutter::BinaryMessenger* binary_messenger, + FirebaseDatabaseHostApi* api, + const std::string& message_channel_suffix); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + FirebaseDatabaseHostApi() = default; +}; +// Generated class from Pigeon that represents Flutter messages that can be +// called from C++. +class FirebaseDatabaseFlutterApi { + public: + FirebaseDatabaseFlutterApi(flutter::BinaryMessenger* binary_messenger); + FirebaseDatabaseFlutterApi(flutter::BinaryMessenger* binary_messenger, + const std::string& message_channel_suffix); + static const flutter::StandardMessageCodec& GetCodec(); + void CallTransactionHandler( + int64_t transaction_key, const flutter::EncodableValue* snapshot_value, + std::function&& on_success, + std::function&& on_error); + + private: + flutter::BinaryMessenger* binary_messenger_; + std::string message_channel_suffix_; +}; + +} // namespace firebase_database_windows +#endif // PIGEON_MESSAGES_G_H_ diff --git a/packages/firebase_database/firebase_database/windows/plugin_version.h.in b/packages/firebase_database/firebase_database/windows/plugin_version.h.in new file mode 100644 index 000000000000..7a405b7c9894 --- /dev/null +++ b/packages/firebase_database/firebase_database/windows/plugin_version.h.in @@ -0,0 +1,13 @@ +// Copyright 2025, 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. + +#ifndef PLUGIN_VERSION_CONFIG_H +#define PLUGIN_VERSION_CONFIG_H + +namespace firebase_database_windows { + +std::string getPluginVersion() { return "@PLUGIN_VERSION@"; } +} // namespace firebase_database_windows + +#endif // PLUGIN_VERSION_CONFIG_H diff --git a/packages/firebase_database/firebase_database_platform_interface/CHANGELOG.md b/packages/firebase_database/firebase_database_platform_interface/CHANGELOG.md index a8beaf72e012..fc1fd4215bcb 100755 --- a/packages/firebase_database/firebase_database_platform_interface/CHANGELOG.md +++ b/packages/firebase_database/firebase_database_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0+3 + + - Update a dependency to the latest release. + ## 0.3.0+2 - Update a dependency to the latest release. diff --git a/packages/firebase_database/firebase_database_platform_interface/pigeons/messages.dart b/packages/firebase_database/firebase_database_platform_interface/pigeons/messages.dart index 3a752d5c8640..2db035bbae4c 100644 --- a/packages/firebase_database/firebase_database_platform_interface/pigeons/messages.dart +++ b/packages/firebase_database/firebase_database_platform_interface/pigeons/messages.dart @@ -16,6 +16,9 @@ import 'package:pigeon/pigeon.dart'; ), swiftOut: '../firebase_database/ios/firebase_database/Sources/firebase_database/FirebaseDatabaseMessages.g.swift', + cppHeaderOut: '../firebase_database/windows/messages.g.h', + cppSourceOut: '../firebase_database/windows/messages.g.cpp', + cppOptions: CppOptions(namespace: 'firebase_database_windows'), copyrightHeader: 'pigeons/copyright.txt', ), ) diff --git a/packages/firebase_database/firebase_database_platform_interface/pubspec.yaml b/packages/firebase_database/firebase_database_platform_interface/pubspec.yaml index 6dcff2707418..cdac9d9cc00d 100755 --- a/packages/firebase_database/firebase_database_platform_interface/pubspec.yaml +++ b/packages/firebase_database/firebase_database_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_database_platform_interface description: A common platform interface for the firebase_database plugin. -version: 0.3.0+2 +version: 0.3.0+3 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_database/firebase_database_platform_interface environment: @@ -8,9 +8,9 @@ environment: flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 + _flutterfire_internals: ^1.3.67 collection: ^1.14.3 - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_database/firebase_database_web/CHANGELOG.md b/packages/firebase_database/firebase_database_web/CHANGELOG.md index ea6ee854301f..c6aa5b02655c 100644 --- a/packages/firebase_database/firebase_database_web/CHANGELOG.md +++ b/packages/firebase_database/firebase_database_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.7+4 + + - Update a dependency to the latest release. + ## 0.2.7+3 - Update a dependency to the latest release. diff --git a/packages/firebase_database/firebase_database_web/lib/src/firebase_database_version.dart b/packages/firebase_database/firebase_database_web/lib/src/firebase_database_version.dart index 6162ddc5882f..acc01b4460cc 100644 --- a/packages/firebase_database/firebase_database_web/lib/src/firebase_database_version.dart +++ b/packages/firebase_database/firebase_database_web/lib/src/firebase_database_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '12.1.3'; +const packageVersion = '12.1.4'; diff --git a/packages/firebase_database/firebase_database_web/pubspec.yaml b/packages/firebase_database/firebase_database_web/pubspec.yaml index 3c1fd199f608..308584e5bc44 100644 --- a/packages/firebase_database/firebase_database_web/pubspec.yaml +++ b/packages/firebase_database/firebase_database_web/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_database_web description: The web implementation of firebase_database -version: 0.2.7+3 +version: 0.2.7+4 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_database/firebase_database_web environment: @@ -9,9 +9,9 @@ environment: dependencies: collection: ^1.18.0 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 - firebase_database_platform_interface: ^0.3.0+2 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 + firebase_database_platform_interface: ^0.3.0+3 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/CHANGELOG.md b/packages/firebase_in_app_messaging/firebase_in_app_messaging/CHANGELOG.md index 7576907f481a..7e2a73fc49e9 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging/CHANGELOG.md +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.0+7 + + - Update a dependency to the latest release. + ## 0.9.0+6 - Update a dependency to the latest release. diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.h b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.m b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.m index 59a72e90be12..9c45e766f906 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/AppDelegate.m @@ -5,9 +5,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/Info.plist b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/Info.plist index bd55b3460d46..c6ae23d68982 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/Info.plist +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIViewControllerBasedStatusBarAppearance + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/pubspec.yaml b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/pubspec.yaml index 1f18d855a1b5..3a2cf9b32352 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/pubspec.yaml +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/example/pubspec.yaml @@ -6,10 +6,10 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - firebase_analytics: ^12.1.2 - firebase_core: ^4.4.0 - firebase_in_app_messaging: ^0.9.0+6 - firebase_in_app_messaging_platform_interface: ^0.2.5+17 + firebase_analytics: ^12.1.3 + firebase_core: ^4.5.0 + firebase_in_app_messaging: ^0.9.0+7 + firebase_in_app_messaging_platform_interface: ^0.2.5+18 flutter: sdk: flutter diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/ios/generated_firebase_sdk_version.txt b/packages/firebase_in_app_messaging/firebase_in_app_messaging/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging/pubspec.yaml b/packages/firebase_in_app_messaging/firebase_in_app_messaging/pubspec.yaml index 5903e53d8c69..4382d500ea75 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging/pubspec.yaml +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_in_app_messaging description: Flutter plugin for Firebase In-App Messaging. -version: 0.9.0+6 +version: 0.9.0+7 homepage: https://firebase.google.com/docs/in-app-messaging repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_in_app_messaging topics: @@ -17,9 +17,9 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_in_app_messaging_platform_interface: ^0.2.5+17 + firebase_in_app_messaging_platform_interface: ^0.2.5+18 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/CHANGELOG.md b/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/CHANGELOG.md index 53f9369fec42..07873b04d704 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/CHANGELOG.md +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.5+18 + + - Update a dependency to the latest release. + ## 0.2.5+17 - Update a dependency to the latest release. diff --git a/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/pubspec.yaml b/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/pubspec.yaml index 8eef6daa5b00..28f50abc46c6 100644 --- a/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/pubspec.yaml +++ b/packages/firebase_in_app_messaging/firebase_in_app_messaging_platform_interface/pubspec.yaml @@ -3,15 +3,15 @@ description: A common platform interface for the firebase_in_app_messaging plugi homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_in_app_messaging/firebase_in_app_messagin_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_in_app_messaging/firebase_in_app_messagin_platform_interface -version: 0.2.5+17 +version: 0.2.5+18 environment: sdk: '>=3.2.0 <4.0.0' flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_messaging/firebase_messaging/CHANGELOG.md b/packages/firebase_messaging/firebase_messaging/CHANGELOG.md index f52c156f59e8..3b440b50f9e4 100644 --- a/packages/firebase_messaging/firebase_messaging/CHANGELOG.md +++ b/packages/firebase_messaging/firebase_messaging/CHANGELOG.md @@ -1,3 +1,7 @@ +## 16.1.2 + + - Update a dependency to the latest release. + ## 16.1.1 - **FIX**(messaging,iOS): scope iOS 18 duplicate notification workaround to iOS 18.0 only ([#17932](https://github.com/firebase/flutterfire/issues/17932)). ([c78f56ea](https://github.com/firebase/flutterfire/commit/c78f56ea0fd0d5ba0b565a11cbf9acce73f93401)) diff --git a/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/firebase-messaging-sw.ts b/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/firebase-messaging-sw.ts index 8816259a0278..8e1d32b69e22 100644 --- a/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/firebase-messaging-sw.ts +++ b/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/firebase-messaging-sw.ts @@ -13,6 +13,22 @@ self.addEventListener('install', (event) => { console.log(event); }); +// Focus the existing app tab when a notification is clicked. +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + event.waitUntil( + self.clients + .matchAll({ type: 'window', includeUncontrolled: true }) + .then((clientList) => { + for (const client of clientList) { + if (!client.focused) { + return client.focus(); + } + } + }) + ); +}); + const app = initializeApp({ apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw', appId: '1:406099696497:web:87e25e51afe982cd3574d0', diff --git a/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/package.json b/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/package.json index a890537d619e..3f3d3ce3102d 100644 --- a/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/package.json +++ b/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "firebase": "10" + "firebase": "12" }, "devDependencies": { "esbuild": "^0.25.0" diff --git a/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/yarn.lock b/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/yarn.lock index c9ba8f2446c2..64d0ae8cf139 100644 --- a/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/yarn.lock +++ b/packages/firebase_messaging/firebase_messaging/example/bundled-service-worker/yarn.lock @@ -127,383 +127,396 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== -"@fastify/busboy@^2.0.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" - integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== - -"@firebase/analytics-compat@0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.7.tgz#affad547d6db9c13424950df972019fb0e2ecaeb" - integrity sha512-17VCly4P0VFBDqaaal7m1nhyYQwsygtaTpSsnc51sFPRrr9XIYtnD8ficon9fneEGEoJQ2g7OtASvhwX9EbK8g== - dependencies: - "@firebase/analytics" "0.10.1" - "@firebase/analytics-types" "0.8.0" - "@firebase/component" "0.6.5" - "@firebase/util" "1.9.4" +"@firebase/ai@2.9.0": + version "2.9.0" + resolved "https://registry.npmjs.org/@firebase/ai/-/ai-2.9.0.tgz#9e6f3546eb688e31488f3e081702773300d609f1" + integrity sha512-NPvBBuvdGo9x3esnABAucFYmqbBmXvyTMimBq2PCuLZbdANZoHzGlx7vfzbwNDaEtCBq4RGGNMliLIv6bZ+PtA== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" + tslib "^2.1.0" + +"@firebase/analytics-compat@0.2.26": + version "0.2.26" + resolved "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.26.tgz#2ec74dc4d41d075d38fab7670c33464803214f2f" + integrity sha512-0j2ruLOoVSwwcXAF53AMoniJKnkwiTjGVfic5LDzqiRkR13vb5j6TXMeix787zbLeQtN/m1883Yv1TxI0gItbA== + dependencies: + "@firebase/analytics" "0.10.20" + "@firebase/analytics-types" "0.8.3" + "@firebase/component" "0.7.1" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/analytics-types@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.0.tgz#551e744a29adbc07f557306530a2ec86add6d410" - integrity sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw== +"@firebase/analytics-types@0.8.3": + version "0.8.3" + resolved "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz#d08cd39a6209693ca2039ba7a81570dfa6c1518f" + integrity sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg== -"@firebase/analytics@0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.1.tgz#97d750020c5b3b41fd5191074c683a7a8c8900a5" - integrity sha512-5mnH1aQa99J5lZMJwTNzIoRc4yGXHf+fOn+EoEWhCDA3XGPweGHcylCbqq+G1wVJmfILL57fohDMa8ftMZ+44g== +"@firebase/analytics@0.10.20": + version "0.10.20" + resolved "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.20.tgz#ec3aaacaa157b979b6e2c12ac5a30e6484b19ddf" + integrity sha512-adGTNVUWH5q66tI/OQuKLSN6mamPpfYhj0radlH2xt+3eL6NFPtXoOs+ulvs+UsmK27vNFx5FjRDfWk+TyduHg== dependencies: - "@firebase/component" "0.6.5" - "@firebase/installations" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/installations" "0.6.20" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/app-check-compat@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.9.tgz#c67caa1cd5043fecab7f8ba1bc45ab047210ad83" - integrity sha512-7LxyupQ8XeEHRh72mO+tqm69kHT6KbWi2KtFMGedJ6tNbwzFzojcXESMKN8RpADXbYoQgY3loWMJjMx4r2Zt7w== +"@firebase/app-check-compat@0.4.1": + version "0.4.1" + resolved "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.1.tgz#2ff3f4b28fd4ee136e7ee12b99edac8cdc8cbbb1" + integrity sha512-yjSvSl5B1u4CirnxhzirN1uiTRCRfx+/qtfbyeyI+8Cx8Cw1RWAIO/OqytPSVwLYbJJ1vEC3EHfxazRaMoWKaA== dependencies: - "@firebase/app-check" "0.8.2" - "@firebase/app-check-types" "0.5.0" - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/app-check" "0.11.1" + "@firebase/app-check-types" "0.5.3" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/app-check-interop-types@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz#b27ea1397cb80427f729e4bbf3a562f2052955c4" - integrity sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg== +"@firebase/app-check-interop-types@0.3.3": + version "0.3.3" + resolved "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz#ed9c4a4f48d1395ef378f007476db3940aa5351a" + integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A== -"@firebase/app-check-types@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.0.tgz#1b02826213d7ce6a1cf773c329b46ea1c67064f4" - integrity sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ== +"@firebase/app-check-types@0.5.3": + version "0.5.3" + resolved "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz#38ba954acf4bffe451581a32fffa20337f11d8e5" + integrity sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng== -"@firebase/app-check@0.8.2": - version "0.8.2" - resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.2.tgz#9ede3558cc7dc1ac8206a772ba692e67daf7e65e" - integrity sha512-A2B5+ldOguYAeqW1quFN5qNdruSNRrg4W59ag1Eq6QzxuHNIkrE+TrapfrW/z5NYFjCxAYqr/unVCgmk80Dwcg== +"@firebase/app-check@0.11.1": + version "0.11.1" + resolved "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.1.tgz#f327a2190b405eb566a93cd5c7eb8ebe7556032b" + integrity sha512-gmKfwQ2k8aUQlOyRshc+fOQLq0OwUmibIZvpuY1RDNu2ho0aTMlwxOuEiJeYOs7AxzhSx7gnXPFNsXCFbnvXUQ== dependencies: - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/app-compat@0.2.29": - version "0.2.29" - resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.29.tgz#d55a5800acaebc0a1a0ea33d548bb80dc711ec93" - integrity sha512-NqUdegXJfwphx9i/2bOE2CTZ55TC9bbDg+iwkxVShsPBJhD3CzQJkFhoDz4ccfbJaKZGsqjY3fisgX5kbDROnA== +"@firebase/app-compat@0.5.9": + version "0.5.9" + resolved "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz#464efce323951283c6812893d251dddee15d61da" + integrity sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug== dependencies: - "@firebase/app" "0.9.29" - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/app" "0.14.9" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/app-types@0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.0.tgz#35b5c568341e9e263b29b3d2ba0e9cfc9ec7f01e" - integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q== +"@firebase/app-types@0.9.3": + version "0.9.3" + resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz#8408219eae9b1fb74f86c24e7150a148460414ad" + integrity sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw== -"@firebase/app@0.9.29": - version "0.9.29" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.9.29.tgz#444280f0ddf1da4b2a974c86a6a8c6405d950fb7" - integrity sha512-HbKTjfmILklasIu/ij6zKnFf3SgLYXkBDVN7leJfVGmohl+zA7Ig+eXM1ZkT1pyBJ8FTYR+mlOJer/lNEnUCtw== +"@firebase/app@0.14.9": + version "0.14.9" + resolved "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz#b7f740904deee2889a3d6115736b16fdbdc853c7" + integrity sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA== dependencies: - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" idb "7.1.1" tslib "^2.1.0" -"@firebase/auth-compat@0.5.4": - version "0.5.4" - resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.5.4.tgz#a7ae705e5f85e786f280bae87fe06bda2d686d05" - integrity sha512-EtRVW9s0YsuJv3GnOGDoLUW3Pp9f3HcqWA2WK92E30Qa0FEVRwCSRLVQwn9td+SLVY3AP9gi/auC1q3osd4yCg== +"@firebase/auth-compat@0.6.3": + version "0.6.3" + resolved "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.3.tgz#8e085d98bd133081e7e7d37b7fb421b876694847" + integrity sha512-nHOkupcYuGVxI1AJJ/OBhLPaRokbP14Gq4nkkoVvf1yvuREEWqdnrYB/CdsSnPxHMAnn5wJIKngxBF9jNX7s/Q== dependencies: - "@firebase/auth" "1.6.2" - "@firebase/auth-types" "0.12.0" - "@firebase/component" "0.6.5" - "@firebase/util" "1.9.4" + "@firebase/auth" "1.12.1" + "@firebase/auth-types" "0.13.0" + "@firebase/component" "0.7.1" + "@firebase/util" "1.14.0" tslib "^2.1.0" - undici "5.28.3" - -"@firebase/auth-interop-types@0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz#78884f24fa539e34a06c03612c75f222fcc33742" - integrity sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg== - -"@firebase/auth-types@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.0.tgz#f28e1b68ac3b208ad02a15854c585be6da3e8e79" - integrity sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA== - -"@firebase/auth@1.6.2": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.6.2.tgz#d8a9a622b8d4e8eb8c42ea544fcf647d0494651c" - integrity sha512-BFo/Nj1AAbKLbFiUyXCcnT/bSqMJicFOgdTAKzlXvCul7+eUE29vWmzd1g59O3iKAxvv3+fbQYjQVJpNTTHIyw== - dependencies: - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + +"@firebase/auth-interop-types@0.2.4": + version "0.2.4" + resolved "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz#176a08686b0685596ff03d7879b7e4115af53de0" + integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA== + +"@firebase/auth-types@0.13.0": + version "0.13.0" + resolved "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz#ae6e0015e3bd4bfe18edd0942b48a0a118a098d9" + integrity sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg== + +"@firebase/auth@1.12.1": + version "1.12.1" + resolved "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz#5eb1c3bf99dfbe7025578a5f1439cc073a4183f0" + integrity sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw== + dependencies: + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" - undici "5.28.3" -"@firebase/component@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.5.tgz#8cc7334f2081d700f2769caaa8dae3ac4c1fe37e" - integrity sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw== +"@firebase/component@0.7.1": + version "0.7.1" + resolved "https://registry.npmjs.org/@firebase/component/-/component-0.7.1.tgz#f16376146d77034ac5055834de25405e6c011491" + integrity sha512-mFzsm7CLHR60o08S23iLUY8m/i6kLpOK87wdEFPLhdlCahaxKmWOwSVGiWoENYSmFJJoDhrR3gKSCxz7ENdIww== dependencies: - "@firebase/util" "1.9.4" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/database-compat@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.3.tgz#f7a255af6208d2d4d7af10ec2c9ecd9af4ff52d5" - integrity sha512-7tHEOcMbK5jJzHWyphPux4osogH/adWwncxdMxdBpB9g1DNIyY4dcz1oJdlkXGM/i/AjUBesZsd5CuwTRTBNTw== +"@firebase/data-connect@0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.4.0.tgz#957d2e0ee602d7120b4c5dbcb8494f911b8a2e47" + integrity sha512-vLXM6WHNIR3VtEeYNUb/5GTsUOyl3Of4iWNZHBe1i9f88sYFnxybJNWVBjvJ7flhCyF8UdxGpzWcUnv6F5vGfg== dependencies: - "@firebase/component" "0.6.5" - "@firebase/database" "1.0.3" - "@firebase/database-types" "1.0.1" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" + tslib "^2.1.0" + +"@firebase/database-compat@2.1.1": + version "2.1.1" + resolved "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.1.tgz#8ab656d2f6b53d1645b86fa846295db4734b9ac5" + integrity sha512-heAEVZ9Z8c8PnBUcmGh91JHX0cXcVa1yESW/xkLuwaX7idRFyLiN8sl73KXpR8ZArGoPXVQDanBnk6SQiekRCQ== + dependencies: + "@firebase/component" "0.7.1" + "@firebase/database" "1.1.1" + "@firebase/database-types" "1.0.17" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/database-types@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.1.tgz#1e7cd9fec03f6ca772c019d839cc72d9b2eda63c" - integrity sha512-Tmcmx5XgiI7UVF/4oGg2P3AOTfq3WKEPsm2yf+uXtN7uG/a4WTWhVMrXGYRY2ZUL1xPxv9V33wQRJ+CcrUhVXw== +"@firebase/database-types@1.0.17": + version "1.0.17" + resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.17.tgz#6b7a14d81655e9ee5e87c26dc853c24d9737e4fe" + integrity sha512-4eWaM5fW3qEIHjGzfi3cf0Jpqi1xQsAdT6rSDE1RZPrWu8oGjgrq6ybMjobtyHQFgwGCykBm4YM89qDzc+uG/w== dependencies: - "@firebase/app-types" "0.9.0" - "@firebase/util" "1.9.4" + "@firebase/app-types" "0.9.3" + "@firebase/util" "1.14.0" -"@firebase/database@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.3.tgz#88caee93188d28aca355236e9ad69f373f628804" - integrity sha512-9fjqLt9JzL46gw9+NRqsgQEMjgRwfd8XtzcKqG+UYyhVeFCdVRQ0Wp6Dw/dvYHnbH5vNEKzNv36dcB4p+PIAAA== +"@firebase/database@1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@firebase/database/-/database-1.1.1.tgz#591610b5087ffc25cc56486ad03749b09c887759" + integrity sha512-LwIXe8+mVHY5LBPulWECOOIEXDiatyECp/BOlu0gOhe+WOcKjWHROaCbLlkFTgHMY7RHr5MOxkLP/tltWAH3dA== dependencies: - "@firebase/app-check-interop-types" "0.3.0" - "@firebase/auth-interop-types" "0.2.1" - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" faye-websocket "0.11.4" tslib "^2.1.0" -"@firebase/firestore-compat@0.3.27": - version "0.3.27" - resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.27.tgz#146024bf772f1b6aa65a7b9e17979d59c2fb5fe0" - integrity sha512-gY2q0fCDJvPg/IurZQbBM7MIVjxA1/LsvfgFOubUTrex5KTY9qm4/2V2R79eAs8Q+b4B8soDtlEjk6L8BW1Crw== +"@firebase/firestore-compat@0.4.6": + version "0.4.6" + resolved "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.6.tgz#30a20be30a72e80b0cfa32d5d693564daff6911a" + integrity sha512-NgVyR4hHHN2FvSNQOtbgBOuVsEdD/in30d9FKbEvvITiAChrBN2nBstmhfjI4EOTnHaP8zigwvkNYFI9yKGAkQ== dependencies: - "@firebase/component" "0.6.5" - "@firebase/firestore" "4.5.0" - "@firebase/firestore-types" "3.0.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/firestore" "4.12.0" + "@firebase/firestore-types" "3.0.3" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/firestore-types@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.0.tgz#f3440d5a1cc2a722d361b24cefb62ca8b3577af3" - integrity sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw== - -"@firebase/firestore@4.5.0": - version "4.5.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.5.0.tgz#f614495970d897b146c5d6cec17c213db0528497" - integrity sha512-rXS6v4HbsN6vZQlq2fLW1ZHb+J5SnS+8Zqb/McbKFIrGYjPUZo5CyO75mkgtlR1tCYAwCebaqoEWb6JHgZv/ww== - dependencies: - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" - "@firebase/webchannel-wrapper" "0.10.5" +"@firebase/firestore-types@3.0.3": + version "3.0.3" + resolved "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz#7d0c3dd8850c0193d8f5ee0cc8f11961407742c1" + integrity sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q== + +"@firebase/firestore@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.12.0.tgz#3321155f66d70c749924c635bb1f0deb92254df3" + integrity sha512-PM47OyiiAAoAMB8kkq4Je14mTciaRoAPDd3ng3Ckqz9i2TX9D9LfxIRcNzP/OxzNV4uBKRq6lXoOggkJBQR3Gw== + dependencies: + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" + "@firebase/webchannel-wrapper" "1.0.5" "@grpc/grpc-js" "~1.9.0" "@grpc/proto-loader" "^0.7.8" tslib "^2.1.0" - undici "5.28.3" -"@firebase/functions-compat@0.3.8": - version "0.3.8" - resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.8.tgz#a83a7ad2788db48483ccc86a80a12f0d824133da" - integrity sha512-VDHSw6UOu8RxfgAY/q8e+Jn+9Fh60Fc28yck0yfMsi2e0BiWgonIMWkFspFGGLgOJebTHl+hc+9v91rhzU6xlg== +"@firebase/functions-compat@0.4.2": + version "0.4.2" + resolved "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.2.tgz#5788b9d33a700164eefd0b4e455de87cd62d635c" + integrity sha512-YNxgnezvZDkqxqXa6cT7/oTeD4WXbxgIP7qZp4LFnathQv5o2omM6EoIhXiT9Ie5AoQDcIhG9Y3/dj+DFJGaGQ== dependencies: - "@firebase/component" "0.6.5" - "@firebase/functions" "0.11.2" - "@firebase/functions-types" "0.6.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/functions" "0.13.2" + "@firebase/functions-types" "0.6.3" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/functions-types@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.0.tgz#ccd7000dc6fc668f5acb4e6a6a042a877a555ef2" - integrity sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw== - -"@firebase/functions@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.11.2.tgz#bcd10d7e7fa3cd185a6c3efe1776731b0222c14d" - integrity sha512-2NULTYOZbu0rXczwfYdqQH0w1FmmYrKjTy1YPQSHLCAkMBdfewoKmVm4Lyo2vRn0H9ZndciLY7NszKDFt9MKCQ== - dependencies: - "@firebase/app-check-interop-types" "0.3.0" - "@firebase/auth-interop-types" "0.2.1" - "@firebase/component" "0.6.5" - "@firebase/messaging-interop-types" "0.2.0" - "@firebase/util" "1.9.4" +"@firebase/functions-types@0.6.3": + version "0.6.3" + resolved "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz#f5faf770248b13f45d256f614230da6a11bfb654" + integrity sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg== + +"@firebase/functions@0.13.2": + version "0.13.2" + resolved "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.2.tgz#2e7936898afcdfa391e564e39049e0e908282420" + integrity sha512-tHduUD+DeokM3NB1QbHCvEMoL16e8Z8JSkmuVA4ROoJKPxHn8ibnecHPO2e3nVCJR1D9OjuKvxz4gksfq92/ZQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.7.1" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.14.0" tslib "^2.1.0" - undici "5.28.3" -"@firebase/installations-compat@0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.5.tgz#e23ff86dc5a4b856f5f3d3abafeda7362daa38c5" - integrity sha512-usvoIaog5CHEw082HXLrKAZ1qd4hIC3N/LDe2NqBgI3pkGE/7auLVM4Gn5gvyryp0x8z/IP1+d9fkGUj2OaGLQ== +"@firebase/installations-compat@0.2.20": + version "0.2.20" + resolved "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.20.tgz#f17bcd7623f1283937ac3192c3293dd68037fcdc" + integrity sha512-9C9pL/DIEGucmoPj8PlZTnztbX3nhNj5RTYVpUM7wQq/UlHywaYv99969JU/WHLvi9ptzIogXYS9d1eZ6XFe9g== dependencies: - "@firebase/component" "0.6.5" - "@firebase/installations" "0.6.5" - "@firebase/installations-types" "0.5.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/installations" "0.6.20" + "@firebase/installations-types" "0.5.3" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/installations-types@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.0.tgz#2adad64755cd33648519b573ec7ec30f21fb5354" - integrity sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg== +"@firebase/installations-types@0.5.3": + version "0.5.3" + resolved "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz#cac8a14dd49f09174da9df8ae453f9b359c3ef2f" + integrity sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA== -"@firebase/installations@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.5.tgz#1d6e0a581747bfaca54f11bf722e1f3da00dcc9c" - integrity sha512-0xxnQWw8rSRzu0ZOCkZaO+MJ0LkDAfwwTB2Z1SxRK6FAz5xkxD1ZUwM0WbCRni49PKubCrZYOJ6yg7tSjU7AKA== +"@firebase/installations@0.6.20": + version "0.6.20" + resolved "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.20.tgz#a019da0e71d5a0bb59b58e43a8edef0153368b94" + integrity sha512-LOzvR7XHPbhS0YB5ANXhqXB5qZlntPpwU/4KFwhSNpXNsGk/sBQ9g5hepi0y0/MfenJLe2v7t644iGOOElQaHQ== dependencies: - "@firebase/component" "0.6.5" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/util" "1.14.0" idb "7.1.1" tslib "^2.1.0" -"@firebase/logger@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.0.tgz#15ecc03c452525f9d47318ad9491b81d1810f113" - integrity sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA== +"@firebase/logger@0.5.0": + version "0.5.0" + resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz#a9e55b1c669a0983dc67127fa4a5964ce8ed5e1b" + integrity sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g== dependencies: tslib "^2.1.0" -"@firebase/messaging-compat@0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.6.tgz#ea89934bff5f048576dc1c4ce87e0c4c2141829b" - integrity sha512-Q2xC1s4L7Vpss7P7Gy6GuIS+xmJrf/vm9+gX76IK1Bo1TjoKwleCLHt1LHkPz5Rvqg5pTgzzI8qqPhBpZosFCg== +"@firebase/messaging-compat@0.2.24": + version "0.2.24" + resolved "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.24.tgz#9ea9bf0d88d605c382dd416e231203310da7b867" + integrity sha512-wXH8FrKbJvFuFe6v98TBhAtvgknxKIZtGM/wCVsfpOGmaAE80bD8tBxztl+uochjnFb9plihkd6mC4y7sZXSpA== dependencies: - "@firebase/component" "0.6.5" - "@firebase/messaging" "0.12.6" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/messaging" "0.12.24" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/messaging-interop-types@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz#6056f8904a696bf0f7fdcf5f2ca8f008e8f6b064" - integrity sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ== +"@firebase/messaging-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz#e647c9cd1beecfe6a6e82018a6eec37555e4da3e" + integrity sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q== -"@firebase/messaging@0.12.6": - version "0.12.6" - resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.6.tgz#ac7c59ed39a89e00990e3b6dfd7929e13dd77563" - integrity sha512-IORsPp9IPWq4j4yEhTOZ6GAGi3gQwGc+4yexmTAlya+qeBRSdRnJg2iIU/aj+tcKDQYr9RQuQPgHHOdFIx//vA== +"@firebase/messaging@0.12.24": + version "0.12.24" + resolved "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.24.tgz#ac586f68a038d8595ee8cbaea2a4b60e1886029a" + integrity sha512-UtKoubegAhHyehcB7iQjvQ8OVITThPbbWk3g2/2ze42PrQr6oe6OmCElYQkBrE5RDCeMTNucXejbdulrQ2XwVg== dependencies: - "@firebase/component" "0.6.5" - "@firebase/installations" "0.6.5" - "@firebase/messaging-interop-types" "0.2.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/installations" "0.6.20" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.14.0" idb "7.1.1" tslib "^2.1.0" -"@firebase/performance-compat@0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.5.tgz#9b827b1801fca19d8c379792326c076877ac5b91" - integrity sha512-jJwJkVyDcIMBaVGrZ6CRGs4m5FCZsWB5QCWYI3FdsHyIa9/TfteNDilxj9wGciF2naFIHDW7TgE69U5dAH9Ktg== +"@firebase/performance-compat@0.2.23": + version "0.2.23" + resolved "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.23.tgz#e4e440878c5be1e11e01d5fe28e5e1fe73d36857" + integrity sha512-c7qOAGBUAOpIuUlHu1axWcrCVtIYKPMhH0lMnoCDWnPwn1HcPuPUBVTWETbC7UWw71RMJF8DpirfWXzMWJQfgA== dependencies: - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/performance" "0.6.5" - "@firebase/performance-types" "0.2.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/performance" "0.7.10" + "@firebase/performance-types" "0.2.3" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/performance-types@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.0.tgz#400685f7a3455970817136d9b48ce07a4b9562ff" - integrity sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA== +"@firebase/performance-types@0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz#5ce64e90fa20ab5561f8b62a305010cf9fab86fb" + integrity sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ== -"@firebase/performance@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.6.5.tgz#5255fb18329719bc1fb2db29262e5ec15cbace06" - integrity sha512-OzAGcWhOqEFH9GdwUuY0oC5FSlnMejcnmSAhR+EjpI7exdDvixyLyCR4txjSHYNTbumrFBG+EP8GO11CNXRaJA== +"@firebase/performance@0.7.10": + version "0.7.10" + resolved "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.10.tgz#a282de63f064477a62cf0379c3374f3cc693ffa4" + integrity sha512-8nRFld+Ntzp5cLKzZuG9g+kBaSn8Ks9dmn87UQGNFDygbmR6ebd8WawauEXiJjMj1n70ypkvAOdE+lzeyfXtGA== dependencies: - "@firebase/component" "0.6.5" - "@firebase/installations" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/installations" "0.6.20" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" - -"@firebase/remote-config-compat@0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.5.tgz#b6850a45567db5372778668c796a8d49723413f3" - integrity sha512-ImkNnLuGrD/bylBHDJigSY6LMwRrwt37wQbsGZhWG4QQ6KLzHzSf0nnFRRFvkOZodEUE57Ib8l74d6Yn/6TDUQ== - dependencies: - "@firebase/component" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/remote-config" "0.4.5" - "@firebase/remote-config-types" "0.3.0" - "@firebase/util" "1.9.4" + web-vitals "^4.2.4" + +"@firebase/remote-config-compat@0.2.22": + version "0.2.22" + resolved "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.22.tgz#5d34d4e856c8a9010e77be5fc2dc183657ade58c" + integrity sha512-uW/eNKKtRBot2gnCC5mnoy5Voo2wMzZuQ7dwqqGHU176fO9zFgMwKiRzk+aaC99NLrFk1KOmr0ZVheD+zdJmjQ== + dependencies: + "@firebase/component" "0.7.1" + "@firebase/logger" "0.5.0" + "@firebase/remote-config" "0.8.1" + "@firebase/remote-config-types" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/remote-config-types@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz#689900dcdb3e5c059e8499b29db393e4e51314b4" - integrity sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA== - -"@firebase/remote-config@0.4.5": - version "0.4.5" - resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.4.5.tgz#1aae1a4639bb0dddc14671c10dcd92c8a83c58c1" - integrity sha512-rGLqc/4OmxrS39RA9kgwa6JmgWytQuMo+B8pFhmGp3d++x2Hf9j+MLQfhOLyyUo64fNw20J19mLXhrXvKHsjZQ== - dependencies: - "@firebase/component" "0.6.5" - "@firebase/installations" "0.6.5" - "@firebase/logger" "0.4.0" - "@firebase/util" "1.9.4" +"@firebase/remote-config-types@0.5.0": + version "0.5.0" + resolved "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz#f0f503b32edda3384f5252f9900cd9613adbb99c" + integrity sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg== + +"@firebase/remote-config@0.8.1": + version "0.8.1" + resolved "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.1.tgz#47309f3e623d358652878935ac90c880b97ef118" + integrity sha512-L86TReBnPiiJOWd7k9iaiE9f7rHtMpjAoYN0fH2ey2ZRzsOChHV0s5sYf1+IIUYzplzsE46pjlmAUNkRRKwHSQ== + dependencies: + "@firebase/component" "0.7.1" + "@firebase/installations" "0.6.20" + "@firebase/logger" "0.5.0" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/storage-compat@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.5.tgz#4c55531dc5aa7d8b5f6c1ed4b5eeee09190072f1" - integrity sha512-5dJXfY5NxCF5NAk4dLvJqC+m6cgcf0Fr29nrMHwhwI34pBheQq2PdRZqALsqZCES9dnHTuFNlqGQDpLr+Ph4rw== +"@firebase/storage-compat@0.4.1": + version "0.4.1" + resolved "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.1.tgz#94c105a416f949fd1552ced075d2df613e761faa" + integrity sha512-bgl3FHHfXAmBgzIK/Fps6Xyv2HiAQlSTov07CBL+RGGhrC5YIk4lruS8JVIC+UkujRdYvnf8cpQFGn2RCilJ/A== dependencies: - "@firebase/component" "0.6.5" - "@firebase/storage" "0.12.2" - "@firebase/storage-types" "0.8.0" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/storage" "0.14.1" + "@firebase/storage-types" "0.8.3" + "@firebase/util" "1.14.0" tslib "^2.1.0" -"@firebase/storage-types@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.0.tgz#f1e40a5361d59240b6e84fac7fbbbb622bfaf707" - integrity sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg== +"@firebase/storage-types@0.8.3": + version "0.8.3" + resolved "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz#2531ef593a3452fc12c59117195d6485c6632d3d" + integrity sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg== -"@firebase/storage@0.12.2": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.12.2.tgz#73b1679fca74ec21a0f183beaa1b0b1a50f7e68b" - integrity sha512-MzanOBcxDx9oOwDaDPMuiYxd6CxcN1xZm+os5uNE3C1itbRKLhM9rzpODDKWzcbnHHFtXk3Q3lsK/d3Xa1WYYw== +"@firebase/storage@0.14.1": + version "0.14.1" + resolved "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.1.tgz#2cdc6523bac9fd85bdd369c77e02a785866d4c02" + integrity sha512-uIpYgBBsv1vIET+5xV20XT7wwqV+H4GFp6PBzfmLUcEgguS4SWNFof56Z3uOC2lNDh0KDda1UflYq2VwD9Nefw== dependencies: - "@firebase/component" "0.6.5" - "@firebase/util" "1.9.4" + "@firebase/component" "0.7.1" + "@firebase/util" "1.14.0" tslib "^2.1.0" - undici "5.28.3" -"@firebase/util@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.4.tgz#68eee380ab7e7828ec0d8684c46a1abed2d7e334" - integrity sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ== +"@firebase/util@1.14.0": + version "1.14.0" + resolved "https://registry.npmjs.org/@firebase/util/-/util-1.14.0.tgz#e0a5998fc30a065fe5cba8bd7546ae8f095f3d3e" + integrity sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw== dependencies: tslib "^2.1.0" -"@firebase/webchannel-wrapper@0.10.5": - version "0.10.5" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.5.tgz#cd9897680d0a2f1bce8d8c23a590e5874f4617c5" - integrity sha512-eSkJsnhBWv5kCTSU1tSUVl9mpFu+5NXXunZc83le8GMjMlsWwQArSc7cJJ4yl+aDFY0NGLi0AjZWMn1axOrkRg== +"@firebase/webchannel-wrapper@1.0.5": + version "1.0.5" + resolved "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz#39cf5a600450cb42f1f0b507cc385459bf103b27" + integrity sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw== "@grpc/grpc-js@~1.9.0": version "1.9.15" @@ -662,37 +675,39 @@ faye-websocket@0.11.4: dependencies: websocket-driver ">=0.5.1" -firebase@10: - version "10.9.0" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.9.0.tgz#748899beb0ed8e381864566c223c4208d2306091" - integrity sha512-R8rDU3mg2dq0uPOoZ5Nc3BeZTbXxBPJS8HcZLtnV0f5/YrmpNsHngzmMHRVB+91T+ViJGVL/42dV23gS9w9ccw== - dependencies: - "@firebase/analytics" "0.10.1" - "@firebase/analytics-compat" "0.2.7" - "@firebase/app" "0.9.29" - "@firebase/app-check" "0.8.2" - "@firebase/app-check-compat" "0.3.9" - "@firebase/app-compat" "0.2.29" - "@firebase/app-types" "0.9.0" - "@firebase/auth" "1.6.2" - "@firebase/auth-compat" "0.5.4" - "@firebase/database" "1.0.3" - "@firebase/database-compat" "1.0.3" - "@firebase/firestore" "4.5.0" - "@firebase/firestore-compat" "0.3.27" - "@firebase/functions" "0.11.2" - "@firebase/functions-compat" "0.3.8" - "@firebase/installations" "0.6.5" - "@firebase/installations-compat" "0.2.5" - "@firebase/messaging" "0.12.6" - "@firebase/messaging-compat" "0.2.6" - "@firebase/performance" "0.6.5" - "@firebase/performance-compat" "0.2.5" - "@firebase/remote-config" "0.4.5" - "@firebase/remote-config-compat" "0.2.5" - "@firebase/storage" "0.12.2" - "@firebase/storage-compat" "0.3.5" - "@firebase/util" "1.9.4" +firebase@12: + version "12.10.0" + resolved "https://registry.npmjs.org/firebase/-/firebase-12.10.0.tgz#2c000e889e8b423ce37399b6a0497cadfba890fe" + integrity sha512-tAjHnEirksqWpa+NKDUSUMjulOnsTcsPC1X1rQ+gwPtjlhJS572na91CwaBXQJHXharIrfj7sw/okDkXOsphjA== + dependencies: + "@firebase/ai" "2.9.0" + "@firebase/analytics" "0.10.20" + "@firebase/analytics-compat" "0.2.26" + "@firebase/app" "0.14.9" + "@firebase/app-check" "0.11.1" + "@firebase/app-check-compat" "0.4.1" + "@firebase/app-compat" "0.5.9" + "@firebase/app-types" "0.9.3" + "@firebase/auth" "1.12.1" + "@firebase/auth-compat" "0.6.3" + "@firebase/data-connect" "0.4.0" + "@firebase/database" "1.1.1" + "@firebase/database-compat" "2.1.1" + "@firebase/firestore" "4.12.0" + "@firebase/firestore-compat" "0.4.6" + "@firebase/functions" "0.13.2" + "@firebase/functions-compat" "0.4.2" + "@firebase/installations" "0.6.20" + "@firebase/installations-compat" "0.2.20" + "@firebase/messaging" "0.12.24" + "@firebase/messaging-compat" "0.2.24" + "@firebase/performance" "0.7.10" + "@firebase/performance-compat" "0.2.23" + "@firebase/remote-config" "0.8.1" + "@firebase/remote-config-compat" "0.2.22" + "@firebase/storage" "0.14.1" + "@firebase/storage-compat" "0.4.1" + "@firebase/util" "1.14.0" get-caller-file@^2.0.5: version "2.0.5" @@ -773,12 +788,10 @@ tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -undici@5.28.3: - version "5.28.3" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b" - integrity sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA== - dependencies: - "@fastify/busboy" "^2.0.0" +web-vitals@^4.2.4: + version "4.2.4" + resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7" + integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw== websocket-driver@>=0.5.1: version "0.7.4" diff --git a/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml b/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml index 43f7db38d69a..cca6f25b826a 100644 --- a/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml +++ b/packages/firebase_messaging/firebase_messaging/example/pubspec.yaml @@ -6,8 +6,8 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 - firebase_messaging: ^16.1.1 + firebase_core: ^4.5.0 + firebase_messaging: ^16.1.2 flutter: sdk: flutter flutter_local_notifications: ^17.2.1 diff --git a/packages/firebase_messaging/firebase_messaging/example/web/firebase-messaging-sw.js b/packages/firebase_messaging/firebase_messaging/example/web/firebase-messaging-sw.js index 63cac871b4d9..9092bc9b88a7 100644 --- a/packages/firebase_messaging/firebase_messaging/example/web/firebase-messaging-sw.js +++ b/packages/firebase_messaging/firebase_messaging/example/web/firebase-messaging-sw.js @@ -1,3 +1,15 @@ +// ⚠️ WARNING: This file uses the legacy Firebase compat SDK loaded via importScripts. +// This approach is deprecated and not recommended for production use. +// +// Instead, use the bundled service worker with the modular Firebase JS SDK: +// See: ../bundled-service-worker/ +// +// To build: +// cd bundled-service-worker +// yarn install && yarn build +// +// This outputs a bundled firebase-messaging-sw.js into this directory. + importScripts("https://www.gstatic.com/firebasejs/9.10.0/firebase-app-compat.js"); importScripts("https://www.gstatic.com/firebasejs/9.10.0/firebase-messaging-compat.js"); diff --git a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m index 3d8c9298a4e9..2dd3f536edcb 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m @@ -43,6 +43,9 @@ @implementation FLTFirebaseMessagingPlugin { // Track if scene delegate connected (for iOS 13+ scene delegate support) BOOL _sceneDidConnect; + // Guard against calling setupNotificationHandling twice + BOOL _notificationHandlingSetup; + #ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM API_AVAILABLE(ios(10), macosx(10.14)) __weak id _originalNotificationCenterDelegate; @@ -63,6 +66,7 @@ - (instancetype)initWithFlutterMethodChannel:(FlutterMethodChannel *)channel if (self) { _initialNotificationGathered = NO; _sceneDidConnect = NO; + _notificationHandlingSetup = NO; _channel = channel; _registrar = registrar; // Application @@ -222,6 +226,32 @@ - (void)messaging:(nonnull FIRMessaging *)messaging - (void)setupNotificationHandlingWithRemoteNotification: (nullable NSDictionary *)remoteNotification { + // If notification handling was already set up (e.g. from + // application_onDidFinishLaunchingNotification) and we're called again (e.g. from + // scene:willConnectToSession:), only process the notification but skip delegate/swizzler + // re-registration to avoid _originalNotificationCenterDelegate being set to self, which causes + // infinite recursion in didReceiveNotificationResponse. See #18037. + if (_notificationHandlingSetup) { + if (remoteNotification != nil) { + _initialNotification = + [FLTFirebaseMessagingPlugin remoteMessageUserInfoToDict:remoteNotification]; + _initialNotificationID = remoteNotification[@"gcm.message_id"]; + _initialNotificationGathered = YES; + [self initialNotificationCallback]; + } else if (_sceneDidConnect && !_initialNotificationGathered) { + // Scene connected with no notification — delay to allow didReceiveRemoteNotification + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + if (!self->_initialNotificationGathered) { + self->_initialNotificationGathered = YES; + [self initialNotificationCallback]; + } + }); + } + return; + } + _notificationHandlingSetup = YES; + if (remoteNotification != nil) { // If remoteNotification exists, it is the notification that opened the app. _initialNotification = @@ -299,12 +329,12 @@ - (void)setupNotificationHandlingWithRemoteNotification: respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]; _originalNotificationCenterDelegateRespondsTo.willPresentNotification = (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter: - willPresentNotification:withCompletionHandler:)]; + respondsToSelector:@selector(userNotificationCenter:willPresentNotification: + withCompletionHandler:)]; _originalNotificationCenterDelegateRespondsTo.didReceiveNotificationResponse = (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter: - didReceiveNotificationResponse:withCompletionHandler:)]; + respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse: + withCompletionHandler:)]; } } diff --git a/packages/firebase_messaging/firebase_messaging/ios/generated_firebase_sdk_version.txt b/packages/firebase_messaging/firebase_messaging/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_messaging/firebase_messaging/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_messaging/firebase_messaging/pubspec.yaml b/packages/firebase_messaging/firebase_messaging/pubspec.yaml index d421f08fbb23..69a11acf8dcc 100644 --- a/packages/firebase_messaging/firebase_messaging/pubspec.yaml +++ b/packages/firebase_messaging/firebase_messaging/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Cloud Messaging, a cross-platform messaging solution that lets you reliably deliver messages on Android and iOS. homepage: https://firebase.google.com/docs/cloud-messaging repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_messaging/firebase_messaging -version: 16.1.1 +version: 16.1.2 topics: - firebase - messaging @@ -17,10 +17,10 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_messaging_platform_interface: ^4.7.6 - firebase_messaging_web: ^4.1.2 + firebase_messaging_platform_interface: ^4.7.7 + firebase_messaging_web: ^4.1.3 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_messaging/firebase_messaging_platform_interface/CHANGELOG.md b/packages/firebase_messaging/firebase_messaging_platform_interface/CHANGELOG.md index 7c0eda5a8b10..ec9679d2badc 100644 --- a/packages/firebase_messaging/firebase_messaging_platform_interface/CHANGELOG.md +++ b/packages/firebase_messaging/firebase_messaging_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.7.7 + + - Update a dependency to the latest release. + ## 4.7.6 - Update a dependency to the latest release. diff --git a/packages/firebase_messaging/firebase_messaging_platform_interface/pubspec.yaml b/packages/firebase_messaging/firebase_messaging_platform_interface/pubspec.yaml index ae3e26f60fd1..1f6febcf8338 100644 --- a/packages/firebase_messaging/firebase_messaging_platform_interface/pubspec.yaml +++ b/packages/firebase_messaging/firebase_messaging_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_messaging_platform_interface description: A common platform interface for the firebase_messaging plugin. -version: 4.7.6 +version: 4.7.7 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_messaging/firebase_messaging_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_messaging/firebase_messaging_platform_interface @@ -9,8 +9,8 @@ environment: flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_messaging/firebase_messaging_web/CHANGELOG.md b/packages/firebase_messaging/firebase_messaging_web/CHANGELOG.md index bde922be65e5..81fa2fbb72bc 100644 --- a/packages/firebase_messaging/firebase_messaging_web/CHANGELOG.md +++ b/packages/firebase_messaging/firebase_messaging_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.3 + + - Update a dependency to the latest release. + ## 4.1.2 - Update a dependency to the latest release. diff --git a/packages/firebase_messaging/firebase_messaging_web/lib/src/firebase_messaging_version.dart b/packages/firebase_messaging/firebase_messaging_web/lib/src/firebase_messaging_version.dart index 4251b1c60353..3456e0d882eb 100644 --- a/packages/firebase_messaging/firebase_messaging_web/lib/src/firebase_messaging_version.dart +++ b/packages/firebase_messaging/firebase_messaging_web/lib/src/firebase_messaging_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '16.1.1'; +const packageVersion = '16.1.2'; diff --git a/packages/firebase_messaging/firebase_messaging_web/pubspec.yaml b/packages/firebase_messaging/firebase_messaging_web/pubspec.yaml index 97cd36fe2fa6..760d0da5f84b 100644 --- a/packages/firebase_messaging/firebase_messaging_web/pubspec.yaml +++ b/packages/firebase_messaging/firebase_messaging_web/pubspec.yaml @@ -2,17 +2,17 @@ name: firebase_messaging_web description: The web implementation of firebase_messaging homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_messaging/firebase_messaging_web repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_messaging/firebase_messaging_web -version: 4.1.2 +version: 4.1.3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 - firebase_messaging_platform_interface: ^4.7.6 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 + firebase_messaging_platform_interface: ^4.7.7 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/CHANGELOG.md b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/CHANGELOG.md index d6b0ebaaf1f9..0e28d560937b 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/CHANGELOG.md +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0+7 + + - Update a dependency to the latest release. + ## 0.4.0+6 - Update a dependency to the latest release. diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner/Info.plist b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner/Info.plist index 71d372f7c1c9..15835549ad8c 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner/Info.plist +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml index 5d6e3977e102..c1d72027fb5e 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/example/pubspec.yaml @@ -10,8 +10,8 @@ dependencies: flutter: sdk: flutter - firebase_core: ^4.4.0 - firebase_ml_model_downloader: ^0.4.0+6 + firebase_core: ^4.5.0 + firebase_ml_model_downloader: ^0.4.0+7 dev_dependencies: flutter_lints: ^4.0.0 diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/firebase_ml_model_downloader/Sources/firebase_ml_model_downloader/Constants.swift b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/firebase_ml_model_downloader/Sources/firebase_ml_model_downloader/Constants.swift index bbd958ad5897..6d856d7c77e6 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/firebase_ml_model_downloader/Sources/firebase_ml_model_downloader/Constants.swift +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/firebase_ml_model_downloader/Sources/firebase_ml_model_downloader/Constants.swift @@ -3,4 +3,4 @@ // found in the LICENSE file. /// Auto-generated file. Do not edit. -public let versionNumber = "0.4.0+6" +public let versionNumber = "0.4.0+7" diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/generated_firebase_sdk_version.txt b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/pubspec.yaml b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/pubspec.yaml index 61b4fa0a70e5..be29d4acb54b 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/pubspec.yaml +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_ml_model_downloader description: A Flutter plugin allowing you to use Firebase Ml Model Downloader. -version: 0.4.0+6 +version: 0.4.0+7 homepage: https://firebase.google.com/docs/ml/flutter/use-custom-models repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_ml_model_downloader/firebase_ml_model_downloader topics: @@ -17,9 +17,9 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_ml_model_downloader_platform_interface: ^0.1.5+17 + firebase_ml_model_downloader_platform_interface: ^0.1.5+18 flutter: sdk: flutter diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/CHANGELOG.md b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/CHANGELOG.md index 038bd21c6b1f..8ed3b54c9b62 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/CHANGELOG.md +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.5+18 + + - Update a dependency to the latest release. + ## 0.1.5+17 - Update a dependency to the latest release. diff --git a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/pubspec.yaml b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/pubspec.yaml index 328dcf6f94dc..d0486c2cf8fc 100644 --- a/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/pubspec.yaml +++ b/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_ml_model_downloader_platform_interface description: A common platform interface for the firebase_ml_model_downloader plugin. -version: 0.1.5+17 +version: 0.1.5+18 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_ml_model_downloader/firebase_ml_model_downloader_platform_interface @@ -9,7 +9,7 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_performance/firebase_performance/CHANGELOG.md b/packages/firebase_performance/firebase_performance/CHANGELOG.md index 43159e7fa328..666252e35f93 100644 --- a/packages/firebase_performance/firebase_performance/CHANGELOG.md +++ b/packages/firebase_performance/firebase_performance/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.11.1+5 + + - Update a dependency to the latest release. + ## 0.11.1+4 - Update a dependency to the latest release. diff --git a/packages/firebase_performance/firebase_performance/android/build.gradle b/packages/firebase_performance/firebase_performance/android/build.gradle index 682805cda15f..6d38c5620117 100644 --- a/packages/firebase_performance/firebase_performance/android/build.gradle +++ b/packages/firebase_performance/firebase_performance/android/build.gradle @@ -3,7 +3,12 @@ version '1.0-SNAPSHOT' apply plugin: 'com.android.library' apply from: file("local-config.gradle") -apply plugin: 'kotlin-android' + +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly. +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +if (agpMajor < 9) { + apply plugin: 'kotlin-android' +} buildscript { ext.kotlin_version = "1.8.22" @@ -55,15 +60,17 @@ android { targetCompatibility project.ext.javaVersion } + if (agpMajor < 9) { + kotlinOptions { + jvmTarget = project.ext.javaVersion + } + } + sourceSets { main.java.srcDirs += "src/main/kotlin" test.java.srcDirs += "src/test/kotlin" } - kotlinOptions { - jvmTarget = project.ext.javaVersion - } - buildFeatures { buildConfig true } diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.h b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.m b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.m index 59a72e90be12..9c45e766f906 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner/AppDelegate.m @@ -5,9 +5,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist b/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist index fa3d95238741..93ccf4a11c44 100644 --- a/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist +++ b/packages/firebase_performance/firebase_performance/example/ios/Runner/Info.plist @@ -47,5 +47,26 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_performance/firebase_performance/example/pubspec.yaml b/packages/firebase_performance/firebase_performance/example/pubspec.yaml index f4f7413a1f22..4803754e3538 100644 --- a/packages/firebase_performance/firebase_performance/example/pubspec.yaml +++ b/packages/firebase_performance/firebase_performance/example/pubspec.yaml @@ -7,8 +7,8 @@ environment: sdk: '>=3.2.0 <4.0.0' dependencies: - firebase_core: ^4.4.0 - firebase_performance: ^0.11.1+4 + firebase_core: ^4.5.0 + firebase_performance: ^0.11.1+5 flutter: sdk: flutter http: ^1.0.0 diff --git a/packages/firebase_performance/firebase_performance/ios/generated_firebase_sdk_version.txt b/packages/firebase_performance/firebase_performance/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_performance/firebase_performance/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_performance/firebase_performance/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_performance/firebase_performance/pubspec.yaml b/packages/firebase_performance/firebase_performance/pubspec.yaml index 17f1cc50952d..780301eec68b 100644 --- a/packages/firebase_performance/firebase_performance/pubspec.yaml +++ b/packages/firebase_performance/firebase_performance/pubspec.yaml @@ -5,7 +5,7 @@ description: iOS. homepage: https://firebase.google.com/docs/perf-mon repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_performance/firebase_performance -version: 0.11.1+4 +version: 0.11.1+5 topics: - firebase - performance @@ -20,10 +20,10 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_performance_platform_interface: ^0.1.6+4 - firebase_performance_web: ^0.1.8+2 + firebase_performance_platform_interface: ^0.1.6+5 + firebase_performance_web: ^0.1.8+3 flutter: sdk: flutter diff --git a/packages/firebase_performance/firebase_performance_platform_interface/CHANGELOG.md b/packages/firebase_performance/firebase_performance_platform_interface/CHANGELOG.md index 2dc917eaa159..314267213ea4 100644 --- a/packages/firebase_performance/firebase_performance_platform_interface/CHANGELOG.md +++ b/packages/firebase_performance/firebase_performance_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.6+5 + + - Update a dependency to the latest release. + ## 0.1.6+4 - Update a dependency to the latest release. diff --git a/packages/firebase_performance/firebase_performance_platform_interface/pubspec.yaml b/packages/firebase_performance/firebase_performance_platform_interface/pubspec.yaml index 92467cbba293..13b75dc56a38 100644 --- a/packages/firebase_performance/firebase_performance_platform_interface/pubspec.yaml +++ b/packages/firebase_performance/firebase_performance_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_performance_platform_interface description: A common platform interface for the firebase_performance plugin. -version: 0.1.6+4 +version: 0.1.6+5 homepage: https://firebase.google.com/docs/perf-mon/flutter/get-started environment: @@ -8,8 +8,8 @@ environment: flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 flutter: sdk: flutter plugin_platform_interface: ^2.1.3 diff --git a/packages/firebase_performance/firebase_performance_web/CHANGELOG.md b/packages/firebase_performance/firebase_performance_web/CHANGELOG.md index df84010dd68a..d7bd6783190a 100644 --- a/packages/firebase_performance/firebase_performance_web/CHANGELOG.md +++ b/packages/firebase_performance/firebase_performance_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.8+3 + + - Update a dependency to the latest release. + ## 0.1.8+2 - Update a dependency to the latest release. diff --git a/packages/firebase_performance/firebase_performance_web/lib/src/firebase_performance_version.dart b/packages/firebase_performance/firebase_performance_web/lib/src/firebase_performance_version.dart index 4b8c052c0488..efb1dedfae05 100644 --- a/packages/firebase_performance/firebase_performance_web/lib/src/firebase_performance_version.dart +++ b/packages/firebase_performance/firebase_performance_web/lib/src/firebase_performance_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '0.11.1+4'; +const packageVersion = '0.11.1+5'; diff --git a/packages/firebase_performance/firebase_performance_web/pubspec.yaml b/packages/firebase_performance/firebase_performance_web/pubspec.yaml index f638c10a0396..ade3fa02adc1 100644 --- a/packages/firebase_performance/firebase_performance_web/pubspec.yaml +++ b/packages/firebase_performance/firebase_performance_web/pubspec.yaml @@ -1,17 +1,17 @@ name: firebase_performance_web description: Web implementation of Firebase Performance monitoring. homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_performance/firebase_performance_web -version: 0.1.8+2 +version: 0.1.8+3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 - firebase_performance_platform_interface: ^0.1.6+4 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 + firebase_performance_platform_interface: ^0.1.6+5 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_remote_config/firebase_remote_config/CHANGELOG.md b/packages/firebase_remote_config/firebase_remote_config/CHANGELOG.md index 1941e7ee00d5..6e203596be7e 100644 --- a/packages/firebase_remote_config/firebase_remote_config/CHANGELOG.md +++ b/packages/firebase_remote_config/firebase_remote_config/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.2.0 + + - **FIX**(remote_config): correct `lastFetchTime` calculation ([#18004](https://github.com/firebase/flutterfire/issues/18004)). ([92f03e08](https://github.com/firebase/flutterfire/commit/92f03e08e9b5362c180da16d60d869568daf2c55)) + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + ## 6.1.4 - Update a dependency to the latest release. diff --git a/packages/firebase_remote_config/firebase_remote_config/android/build.gradle b/packages/firebase_remote_config/firebase_remote_config/android/build.gradle index 41fc6f44f3d2..a03f8e444446 100644 --- a/packages/firebase_remote_config/firebase_remote_config/android/build.gradle +++ b/packages/firebase_remote_config/firebase_remote_config/android/build.gradle @@ -19,7 +19,11 @@ rootProject.allprojects { } } -apply plugin: 'kotlin-android' +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly. +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +if (agpMajor < 9) { + apply plugin: 'kotlin-android' +} def firebaseCoreProject = findProject(':firebase_core') if (firebaseCoreProject == null) { @@ -47,8 +51,10 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17 + if (agpMajor < 9) { + kotlinOptions { + jvmTarget = project.ext.javaVersion + } } compileOptions { diff --git a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.h b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.m b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.m index 59a72e90be12..9c45e766f906 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/AppDelegate.m @@ -5,9 +5,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/Info.plist b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/Info.plist index c8812fec2507..d47a3d12a1e1 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/Info.plist +++ b/packages/firebase_remote_config/firebase_remote_config/example/ios/Runner/Info.plist @@ -49,5 +49,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_remote_config/firebase_remote_config/example/lib/home_page.dart b/packages/firebase_remote_config/firebase_remote_config/example/lib/home_page.dart index a47174ac6269..dacdfdc8c61a 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/lib/home_page.dart +++ b/packages/firebase_remote_config/firebase_remote_config/example/lib/home_page.dart @@ -176,8 +176,8 @@ class _ButtonAndTextState extends State<_ButtonAndText> { padding: const EdgeInsets.all(8), child: Row( children: [ - Text(_text ?? widget.defaultText), - const Spacer(), + Expanded(child: Text(_text ?? widget.defaultText)), + const SizedBox(width: 8), ElevatedButton( onPressed: () async { final result = await widget.onPressed(); diff --git a/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml b/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml index 61a977ff182a..8cc22d0854aa 100644 --- a/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml +++ b/packages/firebase_remote_config/firebase_remote_config/example/pubspec.yaml @@ -8,8 +8,8 @@ environment: dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - firebase_core: ^4.4.0 - firebase_remote_config: ^6.1.4 + firebase_core: ^4.5.0 + firebase_remote_config: ^6.2.0 flutter: sdk: flutter diff --git a/packages/firebase_remote_config/firebase_remote_config/ios/firebase_remote_config/Sources/firebase_remote_config/FirebaseRemoteConfigPlugin.swift b/packages/firebase_remote_config/firebase_remote_config/ios/firebase_remote_config/Sources/firebase_remote_config/FirebaseRemoteConfigPlugin.swift index db00d033998e..10a80253147c 100644 --- a/packages/firebase_remote_config/firebase_remote_config/ios/firebase_remote_config/Sources/firebase_remote_config/FirebaseRemoteConfigPlugin.swift +++ b/packages/firebase_remote_config/firebase_remote_config/ios/firebase_remote_config/Sources/firebase_remote_config/FirebaseRemoteConfigPlugin.swift @@ -60,6 +60,10 @@ public class FirebaseRemoteConfigPlugin: NSObject, FlutterPlugin, FlutterStreamH } public func didReinitializeFirebaseCore(_ completion: @escaping () -> Void) { + for listener in listenersMap.values { + listener.remove() + } + listenersMap.removeAll() completion() } diff --git a/packages/firebase_remote_config/firebase_remote_config/ios/generated_firebase_sdk_version.txt b/packages/firebase_remote_config/firebase_remote_config/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_remote_config/firebase_remote_config/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_remote_config/firebase_remote_config/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_remote_config/firebase_remote_config/pubspec.yaml b/packages/firebase_remote_config/firebase_remote_config/pubspec.yaml index efff370cab2b..5a42eb285061 100644 --- a/packages/firebase_remote_config/firebase_remote_config/pubspec.yaml +++ b/packages/firebase_remote_config/firebase_remote_config/pubspec.yaml @@ -4,7 +4,7 @@ description: re-releasing. homepage: https://firebase.google.com/docs/remote-config repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_remote_config/firebase_remote_config -version: 6.1.4 +version: 6.2.0 topics: - firebase - remote @@ -19,10 +19,10 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_remote_config_platform_interface: ^2.0.7 - firebase_remote_config_web: ^1.10.3 + firebase_remote_config_platform_interface: ^2.1.0 + firebase_remote_config_web: ^1.10.4 flutter: sdk: flutter diff --git a/packages/firebase_remote_config/firebase_remote_config_platform_interface/CHANGELOG.md b/packages/firebase_remote_config/firebase_remote_config_platform_interface/CHANGELOG.md index b779393285d0..1e51e6fb3683 100644 --- a/packages/firebase_remote_config/firebase_remote_config_platform_interface/CHANGELOG.md +++ b/packages/firebase_remote_config/firebase_remote_config_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + + - **FEAT**(remote-config,windows): add support for windows ([#18006](https://github.com/firebase/flutterfire/issues/18006)). ([a6ec167f](https://github.com/firebase/flutterfire/commit/a6ec167f4ece9c9b455a916366781f482cc380b3)) + ## 2.0.7 - Update a dependency to the latest release. diff --git a/packages/firebase_remote_config/firebase_remote_config_platform_interface/pubspec.yaml b/packages/firebase_remote_config/firebase_remote_config_platform_interface/pubspec.yaml index 45f4ed222532..0d069acf615c 100644 --- a/packages/firebase_remote_config/firebase_remote_config_platform_interface/pubspec.yaml +++ b/packages/firebase_remote_config/firebase_remote_config_platform_interface/pubspec.yaml @@ -4,15 +4,15 @@ homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_re repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_remote_config/firebase_remote_config_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.7 +version: 2.1.0 environment: sdk: '>=3.2.0 <4.0.0' flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_remote_config/firebase_remote_config_web/CHANGELOG.md b/packages/firebase_remote_config/firebase_remote_config_web/CHANGELOG.md index 5fb12fd22384..85dde9a875e3 100644 --- a/packages/firebase_remote_config/firebase_remote_config_web/CHANGELOG.md +++ b/packages/firebase_remote_config/firebase_remote_config_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.10.4 + + - Update a dependency to the latest release. + ## 1.10.3 - Update a dependency to the latest release. diff --git a/packages/firebase_remote_config/firebase_remote_config_web/lib/src/firebase_remote_config_version.dart b/packages/firebase_remote_config/firebase_remote_config_web/lib/src/firebase_remote_config_version.dart index 9623ca56f4d7..305156baf450 100644 --- a/packages/firebase_remote_config/firebase_remote_config_web/lib/src/firebase_remote_config_version.dart +++ b/packages/firebase_remote_config/firebase_remote_config_web/lib/src/firebase_remote_config_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '6.1.4'; +const packageVersion = '6.2.0'; diff --git a/packages/firebase_remote_config/firebase_remote_config_web/pubspec.yaml b/packages/firebase_remote_config/firebase_remote_config_web/pubspec.yaml index 0b5100130542..ca0e953aaf4e 100644 --- a/packages/firebase_remote_config/firebase_remote_config_web/pubspec.yaml +++ b/packages/firebase_remote_config/firebase_remote_config_web/pubspec.yaml @@ -3,17 +3,17 @@ description: The web implementation of firebase_remote_config homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_remote_config/firebase_remote_config_web repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_remote_config/firebase_remote_config_web -version: 1.10.3 +version: 1.10.4 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 - firebase_remote_config_platform_interface: ^2.0.7 + _flutterfire_internals: ^1.3.67 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 + firebase_remote_config_platform_interface: ^2.1.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/firebase_storage/firebase_storage/CHANGELOG.md b/packages/firebase_storage/firebase_storage/CHANGELOG.md index 1667f6cc8fc0..fbd8800cc7eb 100644 --- a/packages/firebase_storage/firebase_storage/CHANGELOG.md +++ b/packages/firebase_storage/firebase_storage/CHANGELOG.md @@ -1,3 +1,7 @@ +## 13.1.0 + + - **FEAT**(storage,windows): add emulator support ([#18030](https://github.com/firebase/flutterfire/issues/18030)). ([461dfa43](https://github.com/firebase/flutterfire/commit/461dfa43764469b518984052cb7bbc0a2a2675eb)) + ## 13.0.6 - Update a dependency to the latest release. diff --git a/packages/firebase_storage/firebase_storage/android/build.gradle b/packages/firebase_storage/firebase_storage/android/build.gradle index 7ef42fddf333..c9e5b5e5940d 100755 --- a/packages/firebase_storage/firebase_storage/android/build.gradle +++ b/packages/firebase_storage/firebase_storage/android/build.gradle @@ -44,7 +44,11 @@ def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) { return rootProject.ext.get('FlutterFire').get(name) } -apply plugin: 'kotlin-android' +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly. +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +if (agpMajor < 9) { + apply plugin: 'kotlin-android' +} android { // Conditional for compatibility with AGP <4.2. @@ -59,8 +63,10 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - kotlinOptions { - jvmTarget = project.ext.javaVersion + if (agpMajor < 9) { + kotlinOptions { + jvmTarget = project.ext.javaVersion + } } compileOptions { diff --git a/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.h b/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.h index d9e18e990f2e..6020ddf58053 100644 --- a/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.h @@ -5,6 +5,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.m b/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.m index a4b51c88eb60..b915a48d031c 100644 --- a/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_storage/firebase_storage/example/ios/Runner/AppDelegate.m @@ -9,8 +9,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_storage/firebase_storage/example/ios/Runner/Info.plist b/packages/firebase_storage/firebase_storage/example/ios/Runner/Info.plist index 364cf68705cd..21ed54231166 100755 --- a/packages/firebase_storage/firebase_storage/example/ios/Runner/Info.plist +++ b/packages/firebase_storage/firebase_storage/example/ios/Runner/Info.plist @@ -60,5 +60,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_storage/firebase_storage/example/pubspec.yaml b/packages/firebase_storage/firebase_storage/example/pubspec.yaml index 94ba0f665cdd..44110a521de4 100755 --- a/packages/firebase_storage/firebase_storage/example/pubspec.yaml +++ b/packages/firebase_storage/firebase_storage/example/pubspec.yaml @@ -5,8 +5,8 @@ environment: sdk: '>=3.4.0 <4.0.0' dependencies: - firebase_core: ^4.4.0 - firebase_storage: ^13.0.6 + firebase_core: ^4.5.0 + firebase_storage: ^13.1.0 flutter: sdk: flutter image_picker: ^1.1.2 diff --git a/packages/firebase_storage/firebase_storage/ios/generated_firebase_sdk_version.txt b/packages/firebase_storage/firebase_storage/ios/generated_firebase_sdk_version.txt index a54ec1fce4a4..3eb7353bcd3e 100644 --- a/packages/firebase_storage/firebase_storage/ios/generated_firebase_sdk_version.txt +++ b/packages/firebase_storage/firebase_storage/ios/generated_firebase_sdk_version.txt @@ -1 +1 @@ -12.8.0 \ No newline at end of file +12.9.0 \ No newline at end of file diff --git a/packages/firebase_storage/firebase_storage/lib/firebase_storage.dart b/packages/firebase_storage/firebase_storage/lib/firebase_storage.dart index e51e882d8b2e..9a19065a691d 100755 --- a/packages/firebase_storage/firebase_storage/lib/firebase_storage.dart +++ b/packages/firebase_storage/firebase_storage/lib/firebase_storage.dart @@ -8,9 +8,6 @@ library firebase_storage; import 'dart:async'; import 'dart:convert' show utf8, base64; import 'dart:io' show File; -// TODO(Lyokone): remove once we bump Flutter SDK min version to 3.3 -// ignore: unnecessary_import -import 'dart:typed_data' show Uint8List; // import 'package:flutter/foundation.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -18,6 +15,7 @@ import 'package:firebase_core_platform_interface/firebase_core_platform_interfac show FirebasePluginPlatform; import 'package:firebase_storage_platform_interface/firebase_storage_platform_interface.dart'; import 'package:flutter/foundation.dart'; +import 'package:mime/mime.dart'; import 'src/utils.dart'; diff --git a/packages/firebase_storage/firebase_storage/lib/src/reference.dart b/packages/firebase_storage/firebase_storage/lib/src/reference.dart index 5b3dbe555385..e5c59e20cf83 100644 --- a/packages/firebase_storage/firebase_storage/lib/src/reference.dart +++ b/packages/firebase_storage/firebase_storage/lib/src/reference.dart @@ -103,13 +103,35 @@ class Reference { return _delegate.getData(maxSize); } + /// Infers the content type from the reference [name] if not already set. + SettableMetadata? _withInferredContentType(SettableMetadata? metadata) { + if (metadata?.contentType != null) return metadata; + + final inferred = lookupMimeType(name); + if (inferred == null) return metadata; + + if (metadata == null) { + return SettableMetadata(contentType: inferred); + } + + return SettableMetadata( + cacheControl: metadata.cacheControl, + contentDisposition: metadata.contentDisposition, + contentEncoding: metadata.contentEncoding, + contentLanguage: metadata.contentLanguage, + contentType: inferred, + customMetadata: metadata.customMetadata, + ); + } + /// Uploads data to this reference's location. /// /// Use this method to upload fixed sized data as a [Uint8List]. /// /// Optionally, you can also set metadata onto the uploaded object. UploadTask putData(Uint8List data, [SettableMetadata? metadata]) { - return UploadTask._(storage, _delegate.putData(data, metadata)); + return UploadTask._( + storage, _delegate.putData(data, _withInferredContentType(metadata))); } /// Upload a [Blob]. Note; this is only supported on web platforms. @@ -117,7 +139,8 @@ class Reference { /// Optionally, you can also set metadata onto the uploaded object. UploadTask putBlob(dynamic blob, [SettableMetadata? metadata]) { assert(blob != null); - return UploadTask._(storage, _delegate.putBlob(blob, metadata)); + return UploadTask._( + storage, _delegate.putBlob(blob, _withInferredContentType(metadata))); } /// Upload a [File] from the filesystem. The file must exist. diff --git a/packages/firebase_storage/firebase_storage/pubspec.yaml b/packages/firebase_storage/firebase_storage/pubspec.yaml index d099f6808884..0c08a4da4b65 100755 --- a/packages/firebase_storage/firebase_storage/pubspec.yaml +++ b/packages/firebase_storage/firebase_storage/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Cloud Storage, a powerful, simple, and cost-effective object storage service for Android and iOS. homepage: https://firebase.google.com/docs/storage/flutter/start repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_storage/firebase_storage -version: 13.0.6 +version: 13.1.0 topics: - firebase - storage @@ -19,12 +19,13 @@ environment: flutter: '>=3.3.0' dependencies: - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_storage_platform_interface: ^5.2.17 - firebase_storage_web: ^3.11.2 + firebase_storage_platform_interface: ^5.2.18 + firebase_storage_web: ^3.11.3 flutter: sdk: flutter + mime: ^2.0.0 dev_dependencies: flutter_test: diff --git a/packages/firebase_storage/firebase_storage/test/reference_test.dart b/packages/firebase_storage/firebase_storage/test/reference_test.dart index 00b065cf9c6a..e38d3fed72bc 100644 --- a/packages/firebase_storage/firebase_storage/test/reference_test.dart +++ b/packages/firebase_storage/firebase_storage/test/reference_test.dart @@ -16,6 +16,7 @@ import 'package:mockito/mockito.dart'; import 'mock.dart'; MockReferencePlatform mockReference = MockReferencePlatform(); +MockReferencePlatform mockJpgReference = MockReferencePlatform(); MockListResultPlatform mockListResultPlatform = MockListResultPlatform(); MockUploadTaskPlatform mockUploadTaskPlatform = MockUploadTaskPlatform(); MockDownloadTaskPlatform mockDownloadTaskPlatform = MockDownloadTaskPlatform(); @@ -308,6 +309,130 @@ Future main() async { }); }); + group('putData() contentType inference', () { + late Reference jpgRef; + + setUp(() { + when(kMockStoragePlatform.ref(any)).thenReturn(mockJpgReference); + when(mockJpgReference.bucket).thenReturn(testBucket); + when(mockJpgReference.fullPath).thenReturn('foo/photo.jpg'); + when(mockJpgReference.name).thenReturn('photo.jpg'); + jpgRef = storage.ref('foo/photo.jpg'); + }); + + test('infers contentType from ref name when no metadata', () { + List list = utf8.encode('hello'); + Uint8List data = Uint8List.fromList(list); + when(mockJpgReference.putData(data, any)) + .thenReturn(mockUploadTaskPlatform); + + jpgRef.putData(data); + + final captured = verify(mockJpgReference.putData(data, captureAny)) + .captured + .single as SettableMetadata; + expect(captured.contentType, 'image/jpeg'); + }); + + test('infers contentType when metadata has no contentType', () { + List list = utf8.encode('hello'); + Uint8List data = Uint8List.fromList(list); + when(mockJpgReference.putData(data, any)) + .thenReturn(mockUploadTaskPlatform); + + jpgRef.putData(data, SettableMetadata(contentLanguage: 'en')); + + final captured = verify(mockJpgReference.putData(data, captureAny)) + .captured + .single as SettableMetadata; + expect(captured.contentType, 'image/jpeg'); + expect(captured.contentLanguage, 'en'); + }); + + test('preserves explicit contentType', () { + List list = utf8.encode('hello'); + Uint8List data = Uint8List.fromList(list); + when(mockJpgReference.putData(data, any)) + .thenReturn(mockUploadTaskPlatform); + + jpgRef.putData( + data, SettableMetadata(contentType: 'application/octet-stream')); + + final captured = verify(mockJpgReference.putData(data, captureAny)) + .captured + .single as SettableMetadata; + expect(captured.contentType, 'application/octet-stream'); + }); + + test('preserves customMetadata when inferring contentType', () { + List list = utf8.encode('hello'); + Uint8List data = Uint8List.fromList(list); + when(mockJpgReference.putData(data, any)) + .thenReturn(mockUploadTaskPlatform); + + jpgRef.putData( + data, SettableMetadata(customMetadata: {'activity': 'test'})); + + final captured = verify(mockJpgReference.putData(data, captureAny)) + .captured + .single as SettableMetadata; + expect(captured.contentType, 'image/jpeg'); + expect(captured.customMetadata, {'activity': 'test'}); + }); + + test('no inference when ref has no extension', () { + // Reset to the default mock with no extension + when(kMockStoragePlatform.ref(any)).thenReturn(mockReference); + when(mockReference.name).thenReturn(testName); + final noExtRef = storage.ref(); + + List list = utf8.encode('hello'); + Uint8List data = Uint8List.fromList(list); + when(mockReference.putData(data)).thenReturn(mockUploadTaskPlatform); + + noExtRef.putData(data); + + verify(mockReference.putData(data)); + }); + }); + + group('putBlob() contentType inference', () { + late Reference jpgRef; + + setUp(() { + when(kMockStoragePlatform.ref(any)).thenReturn(mockJpgReference); + when(mockJpgReference.bucket).thenReturn(testBucket); + when(mockJpgReference.fullPath).thenReturn('foo/photo.jpg'); + when(mockJpgReference.name).thenReturn('photo.jpg'); + jpgRef = storage.ref('foo/photo.jpg'); + }); + + test('infers contentType from ref name when no metadata', () { + when(mockJpgReference.putBlob(any, any)) + .thenReturn(mockUploadTaskPlatform); + + jpgRef.putBlob('blob-data'); + + final captured = verify(mockJpgReference.putBlob(any, captureAny)) + .captured + .single as SettableMetadata; + expect(captured.contentType, 'image/jpeg'); + }); + + test('preserves explicit contentType', () { + when(mockJpgReference.putBlob(any, any)) + .thenReturn(mockUploadTaskPlatform); + + jpgRef.putBlob( + 'blob-data', SettableMetadata(contentType: 'text/plain')); + + final captured = verify(mockJpgReference.putBlob(any, captureAny)) + .captured + .single as SettableMetadata; + expect(captured.contentType, 'text/plain'); + }); + }); + test('hashCode()', () { expect(testRef.hashCode, Object.hash(storage, testFullPath)); }); diff --git a/packages/firebase_storage/firebase_storage_platform_interface/CHANGELOG.md b/packages/firebase_storage/firebase_storage_platform_interface/CHANGELOG.md index b315b5db3180..40742c080982 100644 --- a/packages/firebase_storage/firebase_storage_platform_interface/CHANGELOG.md +++ b/packages/firebase_storage/firebase_storage_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.2.18 + + - Update a dependency to the latest release. + ## 5.2.17 - Update a dependency to the latest release. diff --git a/packages/firebase_storage/firebase_storage_platform_interface/pubspec.yaml b/packages/firebase_storage/firebase_storage_platform_interface/pubspec.yaml index aa3b74f77c99..94a97519737e 100644 --- a/packages/firebase_storage/firebase_storage_platform_interface/pubspec.yaml +++ b/packages/firebase_storage/firebase_storage_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_storage_platform_interface description: A common platform interface for the firebase_storage plugin. -version: 5.2.17 +version: 5.2.18 homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_storage/firebase_storage_platform_interface repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_storage/firebase_storage_platform_interface @@ -9,9 +9,9 @@ environment: flutter: '>=3.3.0' dependencies: - _flutterfire_internals: ^1.3.66 + _flutterfire_internals: ^1.3.67 collection: ^1.15.0 - firebase_core: ^4.4.0 + firebase_core: ^4.5.0 flutter: sdk: flutter meta: ^1.8.0 diff --git a/packages/firebase_storage/firebase_storage_web/CHANGELOG.md b/packages/firebase_storage/firebase_storage_web/CHANGELOG.md index d5995ac6ab69..9964e8328c65 100644 --- a/packages/firebase_storage/firebase_storage_web/CHANGELOG.md +++ b/packages/firebase_storage/firebase_storage_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.11.3 + + - Update a dependency to the latest release. + ## 3.11.2 - Update a dependency to the latest release. diff --git a/packages/firebase_storage/firebase_storage_web/README.md b/packages/firebase_storage/firebase_storage_web/README.md index 04207f058085..b47289d6b86c 100644 --- a/packages/firebase_storage/firebase_storage_web/README.md +++ b/packages/firebase_storage/firebase_storage_web/README.md @@ -48,7 +48,7 @@ firebase_storage/example$ cat cors.json And then, with `gsutil`: ``` -firebase_storage/example$ gsutil cors set cors.json gs://my-example-bucket.appspot.com +firebase_storage/example$ gcloud storage buckets update gs://my-example-bucket.appspot.com --cors-file=cors.json Setting CORS on gs://my-example-bucket.appspot.com/... ``` diff --git a/packages/firebase_storage/firebase_storage_web/lib/src/firebase_storage_version.dart b/packages/firebase_storage/firebase_storage_web/lib/src/firebase_storage_version.dart index bbf6df008c51..73ee2b26de13 100644 --- a/packages/firebase_storage/firebase_storage_web/lib/src/firebase_storage_version.dart +++ b/packages/firebase_storage/firebase_storage_web/lib/src/firebase_storage_version.dart @@ -13,4 +13,4 @@ // limitations under the License. /// generated version number for the package, do not manually edit -const packageVersion = '13.0.6'; +const packageVersion = '13.1.0'; diff --git a/packages/firebase_storage/firebase_storage_web/pubspec.yaml b/packages/firebase_storage/firebase_storage_web/pubspec.yaml index 236477eff4d0..3b6609c708c6 100644 --- a/packages/firebase_storage/firebase_storage_web/pubspec.yaml +++ b/packages/firebase_storage/firebase_storage_web/pubspec.yaml @@ -2,18 +2,18 @@ name: firebase_storage_web description: The web implementation of firebase_storage homepage: https://github.com/firebase/flutterfire/tree/main/packages/firebase_storage/firebase_storage_web repository: https://github.com/firebase/flutterfire/tree/main/packages/firebase_storage/firebase_storage_web -version: 3.11.2 +version: 3.11.3 environment: sdk: '>=3.4.0 <4.0.0' flutter: '>=3.22.0' dependencies: - _flutterfire_internals: ^1.3.66 + _flutterfire_internals: ^1.3.67 async: ^2.5.0 - firebase_core: ^4.4.0 - firebase_core_web: ^3.4.0 - firebase_storage_platform_interface: ^5.2.17 + firebase_core: ^4.5.0 + firebase_core_web: ^3.5.0 + firebase_storage_platform_interface: ^5.2.18 flutter: sdk: flutter flutter_web_plugins: diff --git a/scripts/versions.json b/scripts/versions.json index 537849fa0a7e..cfce66daa3a8 100644 --- a/scripts/versions.json +++ b/scripts/versions.json @@ -1,4 +1,32 @@ { + "4.10.0": { + "date": "2026-03-02", + "firebase_sdk": { + "android": "34.9.0", + "ios": "12.9.0", + "web": "12.9.0", + "windows": "13.4.0" + }, + "packages": { + "cloud_firestore": "6.1.3", + "cloud_functions": "6.0.7", + "firebase_ai": "3.9.0", + "firebase_analytics": "12.1.3", + "firebase_app_check": "0.4.1+5", + "firebase_app_installations": "0.4.0+7", + "firebase_auth": "6.2.0", + "firebase_core": "4.5.0", + "firebase_crashlytics": "5.0.8", + "firebase_data_connect": "0.2.3", + "firebase_database": "12.1.4", + "firebase_in_app_messaging": "0.9.0+7", + "firebase_messaging": "16.1.2", + "firebase_ml_model_downloader": "0.4.0+7", + "firebase_performance": "0.11.1+5", + "firebase_remote_config": "6.2.0", + "firebase_storage": "13.1.0" + } + }, "4.9.0": { "date": "2026-02-09", "firebase_sdk": { diff --git a/tests/integration_test/cloud_functions/cloud_functions_e2e_test.dart b/tests/integration_test/cloud_functions/cloud_functions_e2e_test.dart index f97c71cf737e..05b448a961bc 100644 --- a/tests/integration_test/cloud_functions/cloud_functions_e2e_test.dart +++ b/tests/integration_test/cloud_functions/cloud_functions_e2e_test.dart @@ -113,6 +113,28 @@ void main() { skip: kIsWeb, ); + test( + 'accepts raw data as arguments on web (excluding Int64List)', + () async { + HttpsCallableResult result = await callable({ + 'type': 'rawData', + 'list': Uint8List(100), + 'int': Int32List(39), + 'float': Float32List(23), + 'double': Float64List(1001), + }); + final data = result.data; + expect(data['list'], isA()); + expect(data['int'], isA()); + expect(data['float'], isA()); + expect(data['double'], isA()); + }, + // This test is the web counterpart of the above test, + // verifying that typed data serialization works on dart2js + // without triggering "Int64 accessor not supported by dart2js". + skip: !kIsWeb, + ); + test( '[HttpsCallableResult.data] should return Map type for returned objects', () async { @@ -346,6 +368,27 @@ void main() { await expectLater(stream, emits(isA())); }); + test( + 'concurrent streams on the same callable do not collide', + () async { + // Regression test for https://github.com/firebase/flutterfire/issues/18036 + final stream1 = callable + .stream('foo') + .where((event) => event is Chunk) + .map((event) => (event as Chunk).partialData) + .first; + final stream2 = callable + .stream(123) + .where((event) => event is Chunk) + .map((event) => (event as Chunk).partialData) + .first; + + final results = await Future.wait([stream1, stream2]); + expect(results[0], equals('string')); + expect(results[1], equals('number')); + }, + ); + test('should emit a [Result] as last value', () async { final stream = await callable.stream().last; expect( diff --git a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart index f7d309a3b306..674bfa0cdf80 100644 --- a/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart +++ b/tests/integration_test/firebase_analytics/firebase_analytics_e2e_test.dart @@ -5,6 +5,7 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:tests/firebase_options.dart'; @@ -356,5 +357,45 @@ void main() { }, skip: kIsWeb || defaultTargetPlatform != TargetPlatform.iOS, ); + + group('logTransaction', () { + test( + 'throws when transactionId is not a valid numeric string', + () async { + await expectLater( + FirebaseAnalytics.instance.logTransaction('not_a_number'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Invalid transactionId', + ), + ), + ); + }, + skip: kIsWeb || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS), + ); + + test( + 'throws when transactionId is valid format but transaction not found in StoreKit', + () async { + await expectLater( + FirebaseAnalytics.instance.logTransaction('12345'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Transaction not found', + ), + ), + ); + }, + skip: kIsWeb || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS), + ); + }); }); } diff --git a/tests/integration_test/firebase_app_installations/firebase_app_installations_e2e_test.dart b/tests/integration_test/firebase_app_installations/firebase_app_installations_e2e_test.dart index 994905e9fb91..50562a38aa53 100644 --- a/tests/integration_test/firebase_app_installations/firebase_app_installations_e2e_test.dart +++ b/tests/integration_test/firebase_app_installations/firebase_app_installations_e2e_test.dart @@ -52,15 +52,28 @@ void main() { () async { final id = await FirebaseInstallations.instance.getId(); - // Wait a little so we don't get a delete-pending exception - await Future.delayed(const Duration(seconds: 8)); + // Retry delete in case of delete-pending state + for (var attempt = 0; attempt < 5; attempt++) { + try { + await FirebaseInstallations.instance.delete(); + break; + } catch (e) { + if (attempt == 4) rethrow; + await Future.delayed(const Duration(seconds: 2)); + } + } - await FirebaseInstallations.instance.delete(); - - // Wait a little so we don't get a delete-pending exception - await Future.delayed(const Duration(seconds: 8)); - - final newId = await FirebaseInstallations.instance.getId(); + // Retry getId in case of delete-pending state + String? newId; + for (var attempt = 0; attempt < 5; attempt++) { + try { + newId = await FirebaseInstallations.instance.getId(); + break; + } catch (e) { + if (attempt == 4) rethrow; + await Future.delayed(const Duration(seconds: 2)); + } + } expect(newId, isNot(equals(id))); // macOS skipped because it needs keychain sharing entitlement. See: https://github.com/firebase/flutterfire/issues/9538 }, diff --git a/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart index f60a7a5b9eb3..7693e8ce69d4 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_e2e_test.dart @@ -34,29 +34,32 @@ void main() { setUp(() async { // Reset users on emulator. await emulatorClearAllUsers(); + await ensureSignedOut(); try { - // Create a generic testing user account. Wrapped around try/catch because web still seems to have knowledge of user. await FirebaseAuth.instance.createUserWithEmailAndPassword( email: testEmail, password: testPassword, ); - } catch (e) { - // ignore: avoid_print - print('Already existing user: $e'); + } on FirebaseAuthException catch (e) { + // 'email-already-in-use': web may retain user state after emulator clear + // 'keychain-error': known macOS issue needing keychain sharing entitlement + if (e.code != 'email-already-in-use' && e.code != 'keychain-error') { + rethrow; + } } try { - // Create a disabled user account. Wrapped around try/catch because web still seems to have knowledge of user. final disabledUserCredential = await FirebaseAuth.instance.createUserWithEmailAndPassword( email: testDisabledEmail, password: testPassword, ); await emulatorDisableUser(disabledUserCredential.user!.uid); - } catch (e) { - // ignore: avoid_print - print('Already existing disabled user: $e'); + } on FirebaseAuthException catch (e) { + if (e.code != 'email-already-in-use' && e.code != 'keychain-error') { + rethrow; + } } await ensureSignedOut(); }); diff --git a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart index 1935cf7dfabf..2c4e6d70c89f 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart @@ -271,6 +271,59 @@ void main() { fail(e.toString()); } }); + + test('returns correct operation for verifyEmail action code', + () async { + final email = generateRandomEmail(); + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + + await FirebaseAuth.instance.currentUser!.sendEmailVerification(); + + final oobCode = await emulatorOutOfBandCode( + email, + EmulatorOobCodeType.verifyEmail, + ); + expect(oobCode, isNotNull); + + final actionCodeInfo = await FirebaseAuth.instance.checkActionCode( + oobCode!.oobCode!, + ); + + expect( + actionCodeInfo.operation, + equals(ActionCodeInfoOperation.verifyEmail), + ); + }); + + test('returns correct operation for passwordReset action code', + () async { + final email = generateRandomEmail(); + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + await ensureSignedOut(); + + await FirebaseAuth.instance.sendPasswordResetEmail(email: email); + + final oobCode = await emulatorOutOfBandCode( + email, + EmulatorOobCodeType.passwordReset, + ); + expect(oobCode, isNotNull); + + final actionCodeInfo = await FirebaseAuth.instance.checkActionCode( + oobCode!.oobCode!, + ); + + expect( + actionCodeInfo.operation, + equals(ActionCodeInfoOperation.passwordReset), + ); + }); }, skip: !kIsWeb && Platform.isWindows, ); diff --git a/tests/integration_test/firebase_auth/firebase_auth_user_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_user_e2e_test.dart index 16c42b7cf353..7f3cd8d9ff74 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_user_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_user_e2e_test.dart @@ -490,6 +490,62 @@ void main() { } expect(FirebaseAuth.instance.currentUser, isNull); }); + + test( + 'should preserve photoURL after reload', + () async { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + await FirebaseAuth.instance.currentUser!.updatePhotoURL( + 'http://photo.url/test.jpg', + ); + await FirebaseAuth.instance.currentUser!.reload(); + + expect( + FirebaseAuth.instance.currentUser!.photoURL, + 'http://photo.url/test.jpg', + ); + + // Reload again to exercise the PigeonParser path + // with a non-null photoURL + await FirebaseAuth.instance.currentUser!.reload(); + + expect( + FirebaseAuth.instance.currentUser!.photoURL, + 'http://photo.url/test.jpg', + ); + expect( + FirebaseAuth.instance.currentUser!.displayName, + isNull, + ); + }, + skip: kIsWeb || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.windows, + ); + + test( + 'should handle reload when photoURL is null', + () async { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); + + // User created without photoURL — reload should not crash + await FirebaseAuth.instance.currentUser!.reload(); + + expect( + FirebaseAuth.instance.currentUser!.photoURL, + isNull, + ); + }, + skip: kIsWeb || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.windows, + ); }); group( diff --git a/tests/integration_test/firebase_database/data_snapshot_e2e.dart b/tests/integration_test/firebase_database/data_snapshot_e2e.dart index b5bd61879705..29ff36f4cd7d 100644 --- a/tests/integration_test/firebase_database/data_snapshot_e2e.dart +++ b/tests/integration_test/firebase_database/data_snapshot_e2e.dart @@ -164,9 +164,11 @@ void setupDataSnapshotTests() { 'b': 2, 'c': 1, }); - final s = await ref.orderByValue().get(); + // Use .once() instead of .get() because the REST API used by .get() + // does not guarantee ordered results from the emulator. + final event = await ref.orderByValue().once(); - List children = s.children.toList(); + List children = event.snapshot.children.toList(); expect(children[0].value, 1); expect(children[1].value, 2); expect(children[2].value, 3); diff --git a/tests/integration_test/firebase_database/database_reference_e2e.dart b/tests/integration_test/firebase_database/database_reference_e2e.dart index c778c2bf66b3..78fc9616ffe9 100644 --- a/tests/integration_test/firebase_database/database_reference_e2e.dart +++ b/tests/integration_test/firebase_database/database_reference_e2e.dart @@ -13,15 +13,10 @@ import 'firebase_database_e2e_test.dart'; void setupDatabaseReferenceTests() { group('DatabaseReference', () { - late DatabaseReference ref; - - setUp(() async { - ref = database.ref('tests'); - await ref.remove(); - }); - group('set()', () { test('sets value', () async { + final ref = database.ref('tests/set-value'); + await ref.remove(); final v = Random.secure().nextInt(1024); await ref.set(v); final actual = await ref.get(); @@ -55,6 +50,8 @@ void setupDatabaseReferenceTests() { ); test('removes a value if set to null', () async { + final ref = database.ref('tests/set-null'); + await ref.remove(); final v = Random.secure().nextInt(1024); await ref.set(v); final before = await ref.get(); @@ -69,6 +66,8 @@ void setupDatabaseReferenceTests() { group('setPriority()', () { test('sets a priority', () async { + final ref = database.ref('tests/set-priority'); + await ref.remove(); await ref.set('foo'); await ref.setPriority(2); final snapshot = await ref.get(); @@ -78,6 +77,8 @@ void setupDatabaseReferenceTests() { group('setWithPriority()', () { test('sets a non-null value with a non-null priority', () async { + final ref = database.ref('tests/set-with-priority'); + await ref.remove(); await Future.wait([ ref.child('first').setWithPriority(1, 10), ref.child('second').setWithPriority(2, 1), @@ -92,6 +93,8 @@ void setupDatabaseReferenceTests() { group('update()', () { test('updates value at given location', () async { + final ref = database.ref('tests/update'); + await ref.remove(); await ref.set({'foo': 'bar'}); final newValue = Random.secure().nextInt(255) + 1; await ref.update({'bar': newValue}); @@ -105,11 +108,8 @@ void setupDatabaseReferenceTests() { }); group('runTransaction()', () { - setUp(() async { - await ref.set(0); - }); - test('aborts a transaction', () async { + final ref = database.ref('tests/transaction-abort'); await ref.set(5); final snapshot = await ref.get(); expect(snapshot.value, 5); @@ -127,6 +127,8 @@ void setupDatabaseReferenceTests() { }); test('executes transaction', () async { + final ref = database.ref('tests/transaction-exec'); + await ref.set(0); final snapshot = await ref.get(); final value = (snapshot.value ?? 0) as int; final result = await ref.runTransaction((value) { diff --git a/tests/integration_test/firebase_database/query_e2e.dart b/tests/integration_test/firebase_database/query_e2e.dart index 4f4d5f713777..1196c13b80e9 100644 --- a/tests/integration_test/firebase_database/query_e2e.dart +++ b/tests/integration_test/firebase_database/query_e2e.dart @@ -441,22 +441,21 @@ void setupQueryTests() { test( 'emits an event when a child is added', () async { - expect( - ref.onChildAdded, - emitsInOrder([ - isA() - .having((s) => s.snapshot.value, 'value', 'foo') - .having((e) => e.type, 'type', DatabaseEventType.childAdded), - isA() - .having((s) => s.snapshot.value, 'value', 'bar') - .having((e) => e.type, 'type', DatabaseEventType.childAdded), - ]), - ); - - await ref.child('foo').set('foo'); - await ref.child('bar').set('bar'); + // Set data first, then subscribe. onChildAdded fires for + // existing children on initial listen, avoiding race conditions + // with native listener registration. + // Use keys that sort alphabetically in the expected order, + // since onChildAdded returns children in key order. + await ref.child('a_first').set('foo'); + await ref.child('b_second').set('bar'); + + final events = await ref.onChildAdded.take(2).toList(); + + expect(events[0].snapshot.value, 'foo'); + expect(events[0].type, DatabaseEventType.childAdded); + expect(events[1].snapshot.value, 'bar'); + expect(events[1].type, DatabaseEventType.childAdded); }, - retry: 2, ); }); @@ -467,24 +466,26 @@ void setupQueryTests() { await ref.child('foo').set('foo'); await ref.child('bar').set('bar'); - expect( - ref.onChildRemoved, - emitsInOrder([ - isA() - .having((s) => s.snapshot.value, 'value', 'bar') - .having( - (e) => e.type, - 'type', - DatabaseEventType.childRemoved, - ), - ]), - ); - // Give time for listen to be registered on native. - // TODO is there a better way to do this? - await Future.delayed(const Duration(seconds: 1)); + final completer = Completer(); + final subscription = ref.onChildRemoved.listen((event) { + // Skip probe events used for listener registration + if (event.snapshot.key == '__probe__') return; + if (!completer.isCompleted) completer.complete(event); + }); + + // Wait for native listener registration by doing a round-trip + await ref.child('__probe__').set(true); + await ref.child('__probe__').remove(); + await ref.child('bar').remove(); + + final event = + await completer.future.timeout(const Duration(seconds: 10)); + expect(event.snapshot.value, 'bar'); + expect(event.type, DatabaseEventType.childRemoved); + + await subscription.cancel(); }, - retry: 2, ); }); @@ -503,34 +504,33 @@ void setupQueryTests() { await childRef.child('foo').set('foo'); await childRef.child('bar').set('bar'); - expect( - childRef.onChildChanged, - emitsInOrder([ - isA() - .having((s) => s.snapshot.key, 'key', 'bar') - .having((s) => s.snapshot.value, 'value', 'baz') - .having( - (e) => e.type, - 'type', - DatabaseEventType.childChanged, - ), - isA() - .having((s) => s.snapshot.key, 'key', 'foo') - .having((s) => s.snapshot.value, 'value', 'bar') - .having( - (e) => e.type, - 'type', - DatabaseEventType.childChanged, - ), - ]), - ); - // Give time for listen to be registered on native. - // TODO is there a better way to do this? - await Future.delayed(const Duration(seconds: 1)); + final events = []; + final receivedTwo = Completer(); + final subscription = childRef.onChildChanged.listen((event) { + events.add(event); + if (events.length >= 2 && !receivedTwo.isCompleted) { + receivedTwo.complete(); + } + }); + + // Wait for native listener registration by doing a round-trip + await childRef.child('__probe__').set(true); + await childRef.child('__probe__').remove(); + await childRef.child('bar').set('baz'); await childRef.child('foo').set('bar'); + + await receivedTwo.future.timeout(const Duration(seconds: 10)); + + expect(events[0].snapshot.key, 'bar'); + expect(events[0].snapshot.value, 'baz'); + expect(events[0].type, DatabaseEventType.childChanged); + expect(events[1].snapshot.key, 'foo'); + expect(events[1].snapshot.value, 'bar'); + expect(events[1].type, DatabaseEventType.childChanged); + + await subscription.cancel(); }, - retry: 2, ); }); @@ -546,24 +546,32 @@ void setupQueryTests() { 'greg': {'nuggets': 52}, }); - expect( - ref.orderByChild('nuggets').onChildMoved, - emitsInOrder([ - isA().having((s) => s.snapshot.value, 'value', { - 'nuggets': 57, - }).having((e) => e.type, 'type', DatabaseEventType.childMoved), - isA().having((s) => s.snapshot.value, 'value', { - 'nuggets': 61, - }).having((e) => e.type, 'type', DatabaseEventType.childMoved), - ]), - ); - // Give time for listen to be registered on native. - // TODO is there a better way to do this? - await Future.delayed(const Duration(seconds: 1)); + final events = []; + final receivedTwo = Completer(); + final subscription = + ref.orderByChild('nuggets').onChildMoved.listen((event) { + events.add(event); + if (events.length >= 2 && !receivedTwo.isCompleted) { + receivedTwo.complete(); + } + }); + + // Wait for native listener registration by doing a round-trip + await ref.child('__probe__').set(true); + await ref.child('__probe__').remove(); + await ref.child('greg/nuggets').set(57); await ref.child('rob/nuggets').set(61); + + await receivedTwo.future.timeout(const Duration(seconds: 10)); + + expect(events[0].snapshot.value, {'nuggets': 57}); + expect(events[0].type, DatabaseEventType.childMoved); + expect(events[1].snapshot.value, {'nuggets': 61}); + expect(events[1].type, DatabaseEventType.childMoved); + + await subscription.cancel(); }, - retry: 2, ); }); diff --git a/tests/integration_test/firebase_storage/reference_e2e.dart b/tests/integration_test/firebase_storage/reference_e2e.dart index ee09eac47ae8..f8477798d98e 100644 --- a/tests/integration_test/firebase_storage/reference_e2e.dart +++ b/tests/integration_test/firebase_storage/reference_e2e.dart @@ -79,6 +79,8 @@ void setupReferenceTests() { test('should delete a file', () async { Reference ref = storage.ref('flutter-tests/deleteMe.jpeg'); await ref.putString('To Be Deleted :)'); + // Verify the file exists before attempting delete + await ref.getMetadata(); await ref.delete(); await expectLater( @@ -250,7 +252,7 @@ void setupReferenceTests() { Uint8List data = Uint8List.fromList(list); final Reference ref = - storage.ref('flutter-tests').child('flt-ok.txt'); + storage.ref('flutter-tests').child('flt-put-data.txt'); final TaskSnapshot complete = await ref.putData( data, @@ -305,6 +307,46 @@ void setupReferenceTests() { expect(complete.metadata?.contentType, 'application/json'); }, ); + + test( + 'infers contentType from .json ref path when no contentType set', + () async { + final Uint8List jsonData = + utf8.encode(jsonEncode({'key': 'value'})); + final Reference ref = + storage.ref('flutter-tests').child('flt-infer.json'); + final TaskSnapshot complete = await ref.putData(jsonData); + expect(complete.metadata?.contentType, 'application/json'); + }, + ); + + test( + 'infers contentType from .txt ref path and preserves customMetadata', + () async { + final Uint8List txtData = utf8.encode('hello world'); + final Reference ref = + storage.ref('flutter-tests').child('flt-infer.txt'); + final TaskSnapshot complete = await ref.putData( + txtData, + SettableMetadata( + customMetadata: {'activity': 'test'}, + ), + ); + expect(complete.metadata?.contentType, 'text/plain'); + expect(complete.metadata?.customMetadata?['activity'], 'test'); + }, + ); + + test( + 'infers contentType from .jpg ref path when no metadata provided', + () async { + final Uint8List imgData = Uint8List.fromList([0xFF, 0xD8, 0xFF]); + final Reference ref = + storage.ref('flutter-tests').child('flt-infer.jpg'); + final TaskSnapshot complete = await ref.putData(imgData); + expect(complete.metadata?.contentType, 'image/jpeg'); + }, + ); }, ); @@ -312,9 +354,9 @@ void setupReferenceTests() { test( 'throws [UnimplementedError] for native platforms', () async { - final File file = await createFile('flt-ok.txt'); + final File file = await createFile('flt-put-blob.txt'); final Reference ref = - storage.ref('flutter-tests').child('flt-ok.txt'); + storage.ref('flutter-tests').child('flt-put-blob.txt'); await expectLater( () => ref.putBlob( @@ -345,12 +387,12 @@ void setupReferenceTests() { 'uploads a file', () async { final File file = await createFile( - 'flt-ok.txt', + 'flt-put-file.txt', string: kTestString, ); final Reference ref = - storage.ref('flutter-tests').child('flt-ok.txt'); + storage.ref('flutter-tests').child('flt-put-file.txt'); final TaskSnapshot complete = await ref.putFile( file, @@ -429,7 +471,8 @@ void setupReferenceTests() { test('uploads a string and downloads to check its content', () async { const text = 'put string some text to compare with uploaded and downloaded'; - final Reference ref = storage.ref('flutter-tests').child('flt-ok.txt'); + final Reference ref = + storage.ref('flutter-tests').child('flt-put-string.txt'); final TaskSnapshot complete = await ref.putString(text); expect(complete.totalBytes, greaterThan(0)); expect(complete.state, TaskState.success); @@ -462,12 +505,21 @@ void setupReferenceTests() { }); group('updateMetadata', () { - test('updates metadata', () async { - Reference ref = storage.ref('flutter-tests').child('flt-ok.txt'); - FullMetadata fullMetadata = await ref - .updateMetadata(SettableMetadata(customMetadata: {'foo': 'bar'})); - expect(fullMetadata.customMetadata!['foo'], 'bar'); - }); + test( + 'updates metadata', + () async { + Reference ref = + storage.ref('flutter-tests').child('flt-update-metadata.txt'); + // Ensure the file exists before updating metadata + await ref.putString('metadata test content'); + // Verify the file is visible before updating metadata + await ref.getMetadata(); + FullMetadata fullMetadata = await ref + .updateMetadata(SettableMetadata(customMetadata: {'foo': 'bar'})); + expect(fullMetadata.customMetadata!['foo'], 'bar'); + }, + timeout: const Timeout(Duration(minutes: 2)), + ); test( 'errors if property does not exist', diff --git a/tests/integration_test/firebase_storage/task_e2e.dart b/tests/integration_test/firebase_storage/task_e2e.dart index d75d3dc566ff..fd00288c3398 100644 --- a/tests/integration_test/firebase_storage/task_e2e.dart +++ b/tests/integration_test/firebase_storage/task_e2e.dart @@ -49,8 +49,9 @@ void setupTaskTests() { // TODO(Salakar): Known issue with iOS SDK where pausing immediately will cause an 'unknown' error. if (defaultTargetPlatform == TargetPlatform.iOS) { + // Wait for the first snapshot event to confirm the task is running + // before attempting to pause. await task!.snapshotEvents.first; - await Future.delayed(const Duration(milliseconds: 750)); } // TODO(Salakar): Known issue with iOS where pausing/resuming doesn't immediately return as paused/resumed 'true'. @@ -59,8 +60,6 @@ void setupTaskTests() { expect(paused, isTrue); expect(task!.snapshot.state, TaskState.paused); - await Future.delayed(const Duration(milliseconds: 500)); - bool? resumed = await task!.resume(); expect(resumed, isTrue); expect(task!.snapshot.state, TaskState.running); @@ -106,14 +105,13 @@ void setupTaskTests() { } await _testPauseTask('Download'); }, - retry: 3, + retry: 2, // TODO(russellwheatley): Windows works on example app, but fails on tests. // Clue is in bytesTransferred + totalBytes which both equal: -3617008641903833651 - skip: !kIsWeb && ( - defaultTargetPlatform == TargetPlatform.windows || - defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.macOS - ), + skip: !kIsWeb && + (defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.macOS), ); // TODO(Salakar): Test is flaky on CI - needs investigating ('[firebase_storage/unknown] An unknown error occurred, please check the server response.') @@ -123,15 +121,14 @@ void setupTaskTests() { task = uploadRef.putString('This is an upload task!'); await _testPauseTask('Upload'); }, - retry: 3, + retry: 2, // This task is flaky on mac, skip for now. // TODO(russellwheatley): Windows works on example app, but fails on tests. // Clue is in bytesTransferred + totalBytes which both equal: -3617008641903833651 - skip: !kIsWeb && ( - defaultTargetPlatform == TargetPlatform.macOS || - defaultTargetPlatform == TargetPlatform.windows || - defaultTargetPlatform == TargetPlatform.android - ), + skip: !kIsWeb && + (defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.android), ); test( diff --git a/tests/ios/Runner/Info.plist b/tests/ios/Runner/Info.plist index 3dd732018fc0..79a2b4c4b828 100644 --- a/tests/ios/Runner/Info.plist +++ b/tests/ios/Runner/Info.plist @@ -69,5 +69,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/tests/pubspec.yaml b/tests/pubspec.yaml index 79194bc216c8..0fb1e979b9cd 100644 --- a/tests/pubspec.yaml +++ b/tests/pubspec.yaml @@ -9,42 +9,42 @@ environment: flutter: '>=3.22.0' dependencies: - cloud_functions: ^6.0.6 - cloud_functions_platform_interface: ^5.8.9 - cloud_functions_web: ^5.1.2 + cloud_functions: ^6.0.7 + cloud_functions_platform_interface: ^5.8.10 + cloud_functions_web: ^5.1.3 collection: ^1.15.0 - firebase_analytics: ^12.1.2 - firebase_analytics_platform_interface: ^5.0.6 - firebase_analytics_web: ^0.6.1+2 - firebase_app_check: ^0.4.1+4 - firebase_app_check_platform_interface: ^0.2.1+4 - firebase_app_check_web: ^0.2.2+2 - firebase_app_installations: ^0.4.0+6 - firebase_app_installations_platform_interface: ^0.1.4+65 - firebase_app_installations_web: ^0.1.7+2 - firebase_auth: ^6.1.4 - firebase_auth_platform_interface: ^8.1.6 - firebase_auth_web: ^6.1.2 - firebase_core: ^4.4.0 + firebase_analytics: ^12.1.3 + firebase_analytics_platform_interface: ^5.0.7 + firebase_analytics_web: ^0.6.1+3 + firebase_app_check: ^0.4.1+5 + firebase_app_check_platform_interface: ^0.2.1+5 + firebase_app_check_web: ^0.2.2+3 + firebase_app_installations: ^0.4.0+7 + firebase_app_installations_platform_interface: ^0.1.4+66 + firebase_app_installations_web: ^0.1.7+3 + firebase_auth: ^6.2.0 + firebase_auth_platform_interface: ^8.1.7 + firebase_auth_web: ^6.1.3 + firebase_core: ^4.5.0 firebase_core_platform_interface: ^6.0.2 - firebase_core_web: ^3.4.0 - firebase_crashlytics: ^5.0.7 - firebase_crashlytics_platform_interface: ^3.8.17 - firebase_database: ^12.1.3 - firebase_database_platform_interface: ^0.3.0+2 - firebase_database_web: ^0.2.7+3 - firebase_messaging: ^16.1.1 - firebase_messaging_platform_interface: ^4.7.6 - firebase_messaging_web: ^4.1.2 - firebase_ml_model_downloader: ^0.4.0+6 - firebase_ml_model_downloader_platform_interface: ^0.1.5+17 - firebase_performance: ^0.11.1+4 - firebase_remote_config: ^6.1.4 - firebase_remote_config_platform_interface: ^2.0.7 - firebase_remote_config_web: ^1.10.3 - firebase_storage: ^13.0.6 - firebase_storage_platform_interface: ^5.2.17 - firebase_storage_web: ^3.11.2 + firebase_core_web: ^3.5.0 + firebase_crashlytics: ^5.0.8 + firebase_crashlytics_platform_interface: ^3.8.18 + firebase_database: ^12.1.4 + firebase_database_platform_interface: ^0.3.0+3 + firebase_database_web: ^0.2.7+4 + firebase_messaging: ^16.1.2 + firebase_messaging_platform_interface: ^4.7.7 + firebase_messaging_web: ^4.1.3 + firebase_ml_model_downloader: ^0.4.0+7 + firebase_ml_model_downloader_platform_interface: ^0.1.5+18 + firebase_performance: ^0.11.1+5 + firebase_remote_config: ^6.2.0 + firebase_remote_config_platform_interface: ^2.1.0 + firebase_remote_config_web: ^1.10.4 + firebase_storage: ^13.1.0 + firebase_storage_platform_interface: ^5.2.18 + firebase_storage_web: ^3.11.3 flutter: sdk: flutter http: ^1.0.0 diff --git a/tests/windows/flutter/CMakeLists.txt b/tests/windows/flutter/CMakeLists.txt index 930d2071a324..903f4899d6fc 100644 --- a/tests/windows/flutter/CMakeLists.txt +++ b/tests/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/tests/windows/flutter/generated_plugin_registrant.cc b/tests/windows/flutter/generated_plugin_registrant.cc index 2fda6bf3095a..9a180bb54481 100644 --- a/tests/windows/flutter/generated_plugin_registrant.cc +++ b/tests/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -16,6 +17,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FirebaseDatabasePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseDatabasePluginCApi")); FirebaseRemoteConfigPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseRemoteConfigPluginCApi")); FirebaseStoragePluginCApiRegisterWithRegistrar( diff --git a/tests/windows/flutter/generated_plugins.cmake b/tests/windows/flutter/generated_plugins.cmake index 7a8854af51e0..1a9da22be867 100644 --- a/tests/windows/flutter/generated_plugins.cmake +++ b/tests/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_auth firebase_core + firebase_database firebase_remote_config firebase_storage )