From d184db7efefebb6c4e7b71d1005cae18903d493c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 22 Jan 2024 06:58:06 -0800 Subject: [PATCH 001/104] Fix Client App build issue (#12304) --- IntegrationTesting/ClientApp/Podfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IntegrationTesting/ClientApp/Podfile b/IntegrationTesting/ClientApp/Podfile index 724ff4dcad8..7efd2cdd1ea 100644 --- a/IntegrationTesting/ClientApp/Podfile +++ b/IntegrationTesting/ClientApp/Podfile @@ -8,6 +8,8 @@ target 'ClientApp-CocoaPods' do use_frameworks! pod 'FirebaseCore', :path => '../../' + pod 'FirebaseCoreInternal', :path => '../../' + pod 'FirebaseCoreExtension', :path => '../../' pod 'FirebaseInstallations', :path => '../../' pod 'FirebaseAnalytics' # Binary pods don't work with `:path`. pod 'FirebaseAnalyticsOnDeviceConversion', :path => '../../' From 5048195d8c6c069045bf60048372f0e56f7c2e66 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 23 Jan 2024 16:33:02 -0500 Subject: [PATCH 002/104] Update to Xcode 15.2 in CI workflows (#12302) --- .github/workflows/abtesting.yml | 4 ++-- .github/workflows/analytics.yml | 2 +- .github/workflows/appdistribution.yml | 4 ++-- .github/workflows/auth.yml | 4 ++-- .github/workflows/core.yml | 4 ++-- .github/workflows/core_extension.yml | 2 +- .github/workflows/core_internal.yml | 4 ++-- .github/workflows/crashlytics.yml | 4 ++-- .github/workflows/database.yml | 4 ++-- .github/workflows/dynamiclinks.yml | 4 ++-- .github/workflows/firebase_app_check.yml | 4 ++-- .github/workflows/firestore.yml | 6 +++--- .github/workflows/functions.yml | 4 ++-- .github/workflows/inappmessaging.yml | 4 ++-- .github/workflows/installations.yml | 4 ++-- .github/workflows/messaging.yml | 16 ++++++++-------- .github/workflows/mlmodeldownloader.yml | 4 ++-- .github/workflows/performance.yml | 4 ++-- .github/workflows/remoteconfig.yml | 4 ++-- .github/workflows/sessions.yml | 4 ++-- .github/workflows/shared-swift.yml | 4 ++-- .github/workflows/spm.yml | 6 +++--- .github/workflows/storage.yml | 12 ++++++------ .github/workflows/zip.yml | 20 ++++++++++---------- 24 files changed, 66 insertions(+), 66 deletions(-) diff --git a/.github/workflows/abtesting.yml b/.github/workflows/abtesting.yml index 8aaf1c20297..c8ee5c3cde9 100644 --- a/.github/workflows/abtesting.yml +++ b/.github/workflows/abtesting.yml @@ -29,7 +29,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -54,7 +54,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/analytics.yml b/.github/workflows/analytics.yml index 67872715210..503d4faa70f 100644 --- a/.github/workflows/analytics.yml +++ b/.github/workflows/analytics.yml @@ -28,7 +28,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/appdistribution.yml b/.github/workflows/appdistribution.yml index eb5d76880af..882d00b134c 100644 --- a/.github/workflows/appdistribution.yml +++ b/.github/workflows/appdistribution.yml @@ -27,7 +27,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -52,7 +52,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/auth.yml b/.github/workflows/auth.yml index 212007cf72b..95eb31e5da4 100644 --- a/.github/workflows/auth.yml +++ b/.github/workflows/auth.yml @@ -32,7 +32,7 @@ jobs: xcode: Xcode_14.2 tests: --skip-tests - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: runs-on: ${{ matrix.os }} steps: @@ -98,7 +98,7 @@ jobs: xcode: Xcode_14.2 test: spm - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 test: spmbuildonly runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 0b251d24426..cd74aa4e9ae 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -27,7 +27,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -50,7 +50,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/core_extension.yml b/.github/workflows/core_extension.yml index 0706c035fc0..fdfc4692616 100644 --- a/.github/workflows/core_extension.yml +++ b/.github/workflows/core_extension.yml @@ -25,7 +25,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/core_internal.yml b/.github/workflows/core_internal.yml index 3fa93f047a1..fc0edb777f6 100644 --- a/.github/workflows/core_internal.yml +++ b/.github/workflows/core_internal.yml @@ -23,7 +23,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -46,7 +46,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/crashlytics.yml b/.github/workflows/crashlytics.yml index 26cc1ceb3a7..e41daed1ced 100644 --- a/.github/workflows/crashlytics.yml +++ b/.github/workflows/crashlytics.yml @@ -31,7 +31,7 @@ jobs: xcode: Xcode_14.2 tests: --skip-tests - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: runs-on: ${{ matrix.os }} steps: @@ -57,7 +57,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 50fee76ee61..857e559b023 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -33,7 +33,7 @@ jobs: xcode: Xcode_14.2 tests: --skip-tests - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: --test-specs=unit runs-on: ${{ matrix.os }} steps: @@ -75,7 +75,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/dynamiclinks.yml b/.github/workflows/dynamiclinks.yml index 2cc95b49443..310b7a88de6 100644 --- a/.github/workflows/dynamiclinks.yml +++ b/.github/workflows/dynamiclinks.yml @@ -27,7 +27,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -49,7 +49,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/firebase_app_check.yml b/.github/workflows/firebase_app_check.yml index 3bfe934f3c2..7683e2aed1d 100644 --- a/.github/workflows/firebase_app_check.yml +++ b/.github/workflows/firebase_app_check.yml @@ -28,7 +28,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -107,7 +107,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/firestore.yml b/.github/workflows/firestore.yml index 49464754d05..cc257911e3f 100644 --- a/.github/workflows/firestore.yml +++ b/.github/workflows/firestore.yml @@ -369,7 +369,7 @@ jobs: - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - name: Pod lib lint # TODO(#9565, b/227461966): Remove --no-analyze when absl is fixed. @@ -411,7 +411,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: @@ -445,7 +445,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} needs: check env: diff --git a/.github/workflows/functions.yml b/.github/workflows/functions.yml index 1c19f64a0ae..423b44bb372 100644 --- a/.github/workflows/functions.yml +++ b/.github/workflows/functions.yml @@ -35,7 +35,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -93,7 +93,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/inappmessaging.yml b/.github/workflows/inappmessaging.yml index b29a51e0ae5..6c4ced921da 100644 --- a/.github/workflows/inappmessaging.yml +++ b/.github/workflows/inappmessaging.yml @@ -29,7 +29,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -75,7 +75,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/installations.yml b/.github/workflows/installations.yml index c89c8c695f0..72ba9d5bd23 100644 --- a/.github/workflows/installations.yml +++ b/.github/workflows/installations.yml @@ -31,7 +31,7 @@ jobs: xcode: Xcode_14.2 test-specs: unit,integration - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 test-specs: unit runs-on: ${{ matrix.os }} steps: @@ -69,7 +69,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/messaging.yml b/.github/workflows/messaging.yml index 618e629668b..0524dbb8f76 100644 --- a/.github/workflows/messaging.yml +++ b/.github/workflows/messaging.yml @@ -48,7 +48,7 @@ jobs: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/messaging-sample-plist.gpg \ FirebaseMessaging/Tests/IntegrationTests/Resources/GoogleService-Info.plist "$plist_secret" - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - name: BuildAndTest run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh Messaging all) @@ -65,7 +65,7 @@ jobs: xcode: Xcode_14.2 tests: --test-specs=unit - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: --skip-tests runs-on: ${{ matrix.os }} steps: @@ -89,7 +89,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -130,7 +130,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -192,7 +192,7 @@ jobs: xcode: Xcode_14.2 tests: --test-specs=unit - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: --skip-tests runs-on: ${{ matrix.os }} steps: @@ -226,7 +226,7 @@ jobs: - name: Prereqs run: scripts/install_prereqs.sh MessagingSample iOS - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - name: Build run: ([ -z $plist_secret ] || scripts/build.sh MessagingSample iOS) @@ -251,7 +251,7 @@ jobs: - name: Prereqs run: scripts/install_prereqs.sh SwiftUISample iOS - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - name: Build run: ([ -z $plist_secret ] || scripts/build.sh SwiftUISample iOS) @@ -276,7 +276,7 @@ jobs: - name: Prereqs run: scripts/install_prereqs.sh MessagingSampleStandaloneWatchApp watchOS - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer - name: Build run: ([ -z $plist_secret ] || scripts/build.sh MessagingSampleStandaloneWatchApp watchOS) diff --git a/.github/workflows/mlmodeldownloader.yml b/.github/workflows/mlmodeldownloader.yml index 5210baa686f..4cf1332519b 100644 --- a/.github/workflows/mlmodeldownloader.yml +++ b/.github/workflows/mlmodeldownloader.yml @@ -27,7 +27,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -80,7 +80,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 7f2cedfb6fb..1452e205385 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -60,7 +60,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -132,7 +132,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/remoteconfig.yml b/.github/workflows/remoteconfig.yml index f7704ff3b2e..60963dfc4cb 100644 --- a/.github/workflows/remoteconfig.yml +++ b/.github/workflows/remoteconfig.yml @@ -68,7 +68,7 @@ jobs: tests: # Flaky tests on CI - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: --skip-tests runs-on: ${{ matrix.os }} steps: @@ -95,7 +95,7 @@ jobs: xcode: Xcode_14.2 test: spm - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 test: spmbuildonly runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/sessions.yml b/.github/workflows/sessions.yml index ccf8fe00e09..312da50f72f 100644 --- a/.github/workflows/sessions.yml +++ b/.github/workflows/sessions.yml @@ -31,7 +31,7 @@ jobs: tests: # Flaky tests on CI - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: --skip-tests runs-on: ${{ matrix.os }} steps: @@ -57,7 +57,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/shared-swift.yml b/.github/workflows/shared-swift.yml index 5fe07b43f56..34edbe69a2c 100644 --- a/.github/workflows/shared-swift.yml +++ b/.github/workflows/shared-swift.yml @@ -29,7 +29,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -52,7 +52,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/spm.yml b/.github/workflows/spm.yml index 53fddd8d0f7..a2563af2cc4 100644 --- a/.github/workflows/spm.yml +++ b/.github/workflows/spm.yml @@ -34,7 +34,7 @@ jobs: test: spm # The integration tests are slow and flaky on Xcode 15, so just build. - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 test: spmbuildonly runs-on: ${{ matrix.os }} steps: @@ -62,7 +62,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -89,7 +89,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/storage.yml b/.github/workflows/storage.yml index dcedb25ff8a..fc9a6bf9daa 100644 --- a/.github/workflows/storage.yml +++ b/.github/workflows/storage.yml @@ -25,7 +25,7 @@ jobs: language: [Swift, ObjC] include: - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} runs-on: ${{ matrix.os }} @@ -63,7 +63,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -88,7 +88,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -113,7 +113,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} @@ -176,7 +176,7 @@ jobs: xcode: Xcode_14.2 tests: --skip-tests - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 tests: --test-specs=unit runs-on: ${{ matrix.os }} steps: @@ -204,7 +204,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} needs: pod-lib-lint steps: diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 569b12b472f..3998b60e3c9 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -108,7 +108,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -170,7 +170,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -224,7 +224,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -276,7 +276,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -352,7 +352,7 @@ jobs: xcode: Xcode_14.2 # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. # - os: macos-13 - # xcode: Xcode_15.1 + # xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -408,7 +408,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -469,7 +469,7 @@ jobs: xcode: Xcode_14.2 # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. # - os: macos-13 - # xcode: Xcode_15.1 + # xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -555,7 +555,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -612,7 +612,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -668,7 +668,7 @@ jobs: - os: macos-12 xcode: Xcode_14.2 - os: macos-13 - xcode: Xcode_15.1 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From e8ec72b4703cbd3de499749f0b8dfb56d6bb47dd Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 24 Jan 2024 13:18:52 -0500 Subject: [PATCH 003/104] Firestore: Another use foundation API instead of C API (#12315) --- Firestore/core/src/util/filesystem_apple.mm | 31 +++++++++++++++++++++ Firestore/core/src/util/filesystem_posix.cc | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Firestore/core/src/util/filesystem_apple.mm b/Firestore/core/src/util/filesystem_apple.mm index 761e37e0f1d..9802017a6f8 100644 --- a/Firestore/core/src/util/filesystem_apple.mm +++ b/Firestore/core/src/util/filesystem_apple.mm @@ -109,6 +109,37 @@ return Status::OK(); } +StatusOr Filesystem::FileSize(const Path& path) { + NSFileManager* file_manager = NSFileManager.defaultManager; + NSString* ns_path_str = path.ToNSString(); + NSError* error = nil; + + NSDictionary* attributes = [file_manager attributesOfItemAtPath:ns_path_str + error:&error]; + + if (attributes == nil) { + if ([error.domain isEqualToString:NSCocoaErrorDomain]) { + switch (error.code) { + case NSFileReadNoSuchFileError: + case NSFileNoSuchFileError: + return Status{Error::kErrorNotFound, path.ToUtf8String()}.CausedBy( + Status::FromNSError(error)); + } + } + + return Status{Error::kErrorInternal, + StringFormat("attributesOfItemAtPath failed for %s", + path.ToUtf8String())} + .CausedBy(Status::FromNSError(error)); + } + + NSNumber* fileSizeNumber = [attributes objectForKey:NSFileSize]; + + // Use brace initialization of the in64_t return value so that compilation + // will fail if the conversion from long long is narrowing. + return {[fileSizeNumber longLongValue]}; +} + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/util/filesystem_posix.cc b/Firestore/core/src/util/filesystem_posix.cc index 56c008a8d06..8688ce5897c 100644 --- a/Firestore/core/src/util/filesystem_posix.cc +++ b/Firestore/core/src/util/filesystem_posix.cc @@ -166,7 +166,6 @@ Status Filesystem::IsDirectory(const Path& path) { return Status::OK(); } -#endif // !__APPLE__ StatusOr Filesystem::FileSize(const Path& path) { struct stat st {}; @@ -177,6 +176,7 @@ StatusOr Filesystem::FileSize(const Path& path) { errno, StringFormat("Failed to stat file: %s", path.ToUtf8String())); } } +#endif // !__APPLE__ Status Filesystem::CreateDir(const Path& path) { if (::mkdir(path.c_str(), 0777)) { From ae68344a883c08400e9f811bd15195b8d0f14abf Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Thu, 25 Jan 2024 08:34:20 -0500 Subject: [PATCH 004/104] Support empty map in Firestore bundle (#12312) --- Firestore/core/src/bundle/bundle_serializer.cc | 10 ++++++++-- Firestore/core/test/unit/bundle/bundle_reader_test.cc | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Firestore/core/src/bundle/bundle_serializer.cc b/Firestore/core/src/bundle/bundle_serializer.cc index 9cc6f8e7b55..86942236791 100644 --- a/Firestore/core/src/bundle/bundle_serializer.cc +++ b/Firestore/core/src/bundle/bundle_serializer.cc @@ -584,10 +584,16 @@ Message BundleSerializer::DecodeValue( Message BundleSerializer::DecodeMapValue( JsonReader& reader, const json& map_json) const { - if (!map_json.is_object() || !map_json.contains("fields")) { - reader.Fail("mapValue is not a valid map"); + if (!map_json.is_object()) { + reader.Fail("mapValue is not a valid object"); return {}; } + + // Empty map doesn't have `fields` field. + if (!map_json.contains("fields")) { + return {}; + } + const auto& fields = map_json.at("fields"); if (!fields.is_object()) { reader.Fail("mapValue's 'field' is not a valid map"); diff --git a/Firestore/core/test/unit/bundle/bundle_reader_test.cc b/Firestore/core/test/unit/bundle/bundle_reader_test.cc index 54ddfc9f1d9..61b3f744845 100644 --- a/Firestore/core/test/unit/bundle/bundle_reader_test.cc +++ b/Firestore/core/test/unit/bundle/bundle_reader_test.cc @@ -231,10 +231,13 @@ class BundleReaderTest : public ::testing::Test { value3.set_null_value(google::protobuf::NULL_VALUE); ProtoValue value4; value4.mutable_array_value(); + ProtoValue value5; + value5.mutable_map_value(); document.mutable_fields()->insert({"\0\ud7ff\ue000\uffff\"", value1}); document.mutable_fields()->insert({"\"(╯°□°)╯︵ ┻━┻\"", value2}); document.mutable_fields()->insert({"nValue", value3}); document.mutable_fields()->insert({"emptyArray", value4}); + document.mutable_fields()->insert({"emptyMap", value5}); return document; } From 26184fc1308021c439944d0c509744b1bf7c8a78 Mon Sep 17 00:00:00 2001 From: leojaygoogle <98397998+leojaygoogle@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:41:42 -0800 Subject: [PATCH 005/104] Delete unnecessary file system API usages. (#12320) --- .../Sources/FIRMessagingSyncMessageManager.m | 8 ------- .../Sources/FIRMessagingUtilities.h | 1 - .../Sources/FIRMessagingUtilities.m | 23 ------------------- 3 files changed, 32 deletions(-) diff --git a/FirebaseMessaging/Sources/FIRMessagingSyncMessageManager.m b/FirebaseMessaging/Sources/FIRMessagingSyncMessageManager.m index 69a6bb028c1..925f701778d 100644 --- a/FirebaseMessaging/Sources/FIRMessagingSyncMessageManager.m +++ b/FirebaseMessaging/Sources/FIRMessagingSyncMessageManager.m @@ -24,8 +24,6 @@ #import "FirebaseMessaging/Sources/FIRMessagingUtilities.h" static const int64_t kDefaultSyncMessageTTL = 4 * 7 * 24 * 60 * 60; // 4 weeks -// 4 MB of free space is required to persist Sync messages -static const uint64_t kMinFreeDiskSpaceInMB = 1; @interface FIRMessagingSyncMessageManager () @@ -63,12 +61,6 @@ - (BOOL)didReceiveAPNSSyncMessage:(NSDictionary *)message { [self.rmqManager querySyncMessageWithRmqID:rmqID]; if (!persistentMessage) { - // Do not persist the new message if we don't have enough disk space - uint64_t freeDiskSpace = FIRMessagingGetFreeDiskSpaceInMB(); - if (freeDiskSpace < kMinFreeDiskSpaceInMB) { - return NO; - } - int64_t expirationTime = [[self class] expirationTimeForSyncMessage:message]; [self.rmqManager saveSyncMessageWithRmqID:rmqID expirationTime:expirationTime]; return NO; diff --git a/FirebaseMessaging/Sources/FIRMessagingUtilities.h b/FirebaseMessaging/Sources/FIRMessagingUtilities.h index d55040ca096..f2b10d9f06d 100644 --- a/FirebaseMessaging/Sources/FIRMessagingUtilities.h +++ b/FirebaseMessaging/Sources/FIRMessagingUtilities.h @@ -34,7 +34,6 @@ FOUNDATION_EXPORT BOOL FIRMessagingIsWatchKitExtension(void); #pragma mark - Others -FOUNDATION_EXPORT uint64_t FIRMessagingGetFreeDiskSpaceInMB(void); FOUNDATION_EXPORT NSSearchPathDirectory FIRMessagingSupportedDirectory(void); #pragma mark - Device Info diff --git a/FirebaseMessaging/Sources/FIRMessagingUtilities.m b/FirebaseMessaging/Sources/FIRMessagingUtilities.m index eea771f9942..713febdb833 100644 --- a/FirebaseMessaging/Sources/FIRMessagingUtilities.m +++ b/FirebaseMessaging/Sources/FIRMessagingUtilities.m @@ -21,7 +21,6 @@ #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseMessaging/Sources/FIRMessagingLogger.h" -static const uint64_t kBytesToMegabytesDivisor = 1024 * 1024LL; NSString *const kFIRMessagingInstanceIDUserDefaultsKeyLocale = @"com.firebase.instanceid.user_defaults.locale"; // locale key stored in GULUserDefaults static NSString *const kFIRMessagingAPNSSandboxPrefix = @"s_"; @@ -114,28 +113,6 @@ BOOL FIRMessagingIsWatchKitExtension(void) { #endif } -uint64_t FIRMessagingGetFreeDiskSpaceInMB(void) { - NSError *error; - NSArray *paths = - NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(), NSUserDomainMask, YES); - - NSDictionary *attributesMap = - [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] - error:&error]; - if (attributesMap) { - uint64_t totalSizeInBytes __unused = [attributesMap[NSFileSystemSize] longLongValue]; - uint64_t freeSizeInBytes = [attributesMap[NSFileSystemFreeSize] longLongValue]; - FIRMessagingLoggerDebug( - kFIRMessagingMessageCodeUtilities001, @"Device has capacity %llu MB with %llu MB free.", - totalSizeInBytes / kBytesToMegabytesDivisor, freeSizeInBytes / kBytesToMegabytesDivisor); - return ((double)freeSizeInBytes) / kBytesToMegabytesDivisor; - } else { - FIRMessagingLoggerError(kFIRMessagingMessageCodeUtilities002, - @"Error in retreiving device's free memory %@", error); - return 0; - } -} - NSSearchPathDirectory FIRMessagingSupportedDirectory(void) { #if TARGET_OS_TV return NSCachesDirectory; From b7d6209e5c53ad54d34b7d067db978753e15ed61 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 26 Jan 2024 13:38:44 -0800 Subject: [PATCH 006/104] Require at least CocoaPods version 1.12.0 (#12322) --- CONTRIBUTING.md | 2 +- Firebase.podspec | 2 +- FirebaseABTesting.podspec | 2 +- FirebaseAnalytics.podspec | 2 +- FirebaseAnalyticsOnDeviceConversion.podspec | 2 +- FirebaseAnalyticsSwift.podspec | 2 +- FirebaseAppCheck.podspec | 2 +- FirebaseAppDistribution.podspec | 2 +- FirebaseAuth.podspec | 2 +- FirebaseAuthTestingSupport.podspec | 2 +- FirebaseCombineSwift.podspec | 2 +- FirebaseCore.podspec | 2 +- FirebaseCore/CHANGELOG.md | 4 ++++ FirebaseCrashlytics.podspec | 2 +- FirebaseDatabase.podspec | 2 +- FirebaseDatabaseSwift.podspec | 2 +- FirebaseDynamicLinks.podspec | 2 +- FirebaseFirestore.podspec | 2 +- FirebaseFirestoreInternal.podspec | 2 +- FirebaseFirestoreSwift.podspec | 2 +- FirebaseFirestoreTestingSupport.podspec | 2 +- FirebaseFunctions.podspec | 2 +- FirebaseFunctions/README.md | 2 +- FirebaseInAppMessaging.podspec | 2 +- FirebaseInAppMessagingSwift.podspec | 2 +- FirebaseInstallations.podspec | 2 +- FirebaseMLModelDownloader.podspec | 2 +- FirebaseMessaging.podspec | 2 +- FirebasePerformance.podspec | 2 +- FirebaseRemoteConfig.podspec | 2 +- FirebaseRemoteConfigSwift.podspec | 2 +- FirebaseSessions.podspec | 2 +- FirebaseSharedSwift.podspec | 2 +- FirebaseStorage.podspec | 2 +- GoogleAppMeasurement.podspec | 2 +- GoogleAppMeasurementOnDeviceConversion.podspec | 2 +- GoogleUtilitiesComponents.podspec | 2 +- .../Cocoapods_multiprojects_frameworks/Gemfile | 2 +- README.md | 2 +- 39 files changed, 42 insertions(+), 38 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1c20896cb1..2023549978c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -187,7 +187,7 @@ To learn more about running tests with Swift Package Manager, visit the #### **[CocoaPods]** [CocoaPods] is another popular dependency manager used in Apple development. -Firebase supports development with CocoaPods 1.10.0 (or later). If you choose to +Firebase supports development with CocoaPods 1.12.0 (or later). If you choose to develop using CocoaPods, it's recommend to use [`cocoapods-generate`][cocoapods-generate], a plugin that generates a [workspace] from a [podspec]. This plugin allows you to quickly generate a diff --git a/Firebase.podspec b/Firebase.podspec index 166ff06f0a1..fa9c889398f 100644 --- a/Firebase.podspec +++ b/Firebase.podspec @@ -26,7 +26,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.osx.deployment_target = '10.13' s.tvos.deployment_target = '12.0' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.swift_version = '5.3' diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index 6fe6a1f909a..dd5cda94849 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -32,7 +32,7 @@ Firebase Cloud Messaging and Firebase Remote Config in your app. s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.swift_version = '5.3' diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index d9b4237cb23..d02db7df40e 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| :http => 'https://dl.google.com/firebase/ios/analytics/0199e7929b47e2d9/FirebaseAnalytics-10.20.0.tar.gz' } - s.cocoapods_version = '>= 1.10.0' + s.cocoapods_version = '>= 1.12.0' s.swift_version = '5.3' s.ios.deployment_target = '10.0' diff --git a/FirebaseAnalyticsOnDeviceConversion.podspec b/FirebaseAnalyticsOnDeviceConversion.podspec index e8e187baa8f..6de84f13eaa 100644 --- a/FirebaseAnalyticsOnDeviceConversion.podspec +++ b/FirebaseAnalyticsOnDeviceConversion.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| :tag => 'CocoaPods-' + s.version.to_s } - s.cocoapods_version = '>= 1.10.2' + s.cocoapods_version = '>= 1.12.0' s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.21.0' diff --git a/FirebaseAnalyticsSwift.podspec b/FirebaseAnalyticsSwift.podspec index 0d13db74422..4fb8790af9d 100644 --- a/FirebaseAnalyticsSwift.podspec +++ b/FirebaseAnalyticsSwift.podspec @@ -27,7 +27,7 @@ Firebase Analytics is a free, out-of-the-box analytics solution that inspires ac s.osx.deployment_target = osx_deployment_target s.tvos.deployment_target = tvos_deployment_target - s.cocoapods_version = '>= 1.10.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index d3bdec34b4f..ccf007ef013 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -29,7 +29,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseAppCheck/" diff --git a/FirebaseAppDistribution.podspec b/FirebaseAppDistribution.podspec index de683d47ebe..cfc10fe8bb2 100644 --- a/FirebaseAppDistribution.podspec +++ b/FirebaseAppDistribution.podspec @@ -19,7 +19,7 @@ iOS SDK for App Distribution for Firebase. s.swift_version = '5.3' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseAppDistribution/Sources/" diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index 03898b2799e..ae34d933eeb 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -31,7 +31,7 @@ supports email and password accounts, as well as several 3rd party authenticatio s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false source = 'FirebaseAuth/Sources/' diff --git a/FirebaseAuthTestingSupport.podspec b/FirebaseAuthTestingSupport.podspec index ff7544f518f..2bec66c180a 100644 --- a/FirebaseAuthTestingSupport.podspec +++ b/FirebaseAuthTestingSupport.podspec @@ -29,7 +29,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.requires_arc = true diff --git a/FirebaseCombineSwift.podspec b/FirebaseCombineSwift.podspec index a48fa8db5c1..75ce5dfbda4 100644 --- a/FirebaseCombineSwift.podspec +++ b/FirebaseCombineSwift.podspec @@ -31,7 +31,7 @@ for internal testing only. It should not be published. s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false source = 'FirebaseCombineSwift/Sources/' diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index c1e238771d7..f91e9507dc6 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -28,7 +28,7 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index 92a4297058b..7e0f3b37337 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,3 +1,7 @@ +# Firebase 10.21.0 +- Firebase now requires at least CocoaPods version 1.12.0 to enable privacy + manifest support. + # Firebase 10.20.0 - The following change only applies to those using a binary distribution of a Firebase SDK(s): In preparation for supporting Privacy Manifests, each diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index 395f941235a..75b29429e6d 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index f6e66bbe5f0..ef2d8d14487 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -29,7 +29,7 @@ Simplify your iOS development, grow your user base, and monetize more effectivel s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseDatabase/Sources/" diff --git a/FirebaseDatabaseSwift.podspec b/FirebaseDatabaseSwift.podspec index aba011e0dcf..3e066ab7a0f 100644 --- a/FirebaseDatabaseSwift.podspec +++ b/FirebaseDatabaseSwift.podspec @@ -21,7 +21,7 @@ Simplify your iOS development, grow your user base, and monetize more effectivel s.osx.deployment_target = '10.13' s.tvos.deployment_target = '12.0' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index 97af8532edb..91bc0f1f492 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -20,7 +20,7 @@ Firebase Dynamic Links are deep links that enhance user experience and increase s.swift_version = '5.3' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 51bd36d1dbd..f58d9ab929a 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -21,7 +21,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.weak_framework = 'FirebaseFirestoreInternal' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.public_header_files = 'FirebaseFirestoreInternal/**/*.h' diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index 4afa3804ff9..bef9568bf8e 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -22,7 +22,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.swift_version = '5.3' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false # Header files that constitute the interface to this module. Only Objective-C diff --git a/FirebaseFirestoreSwift.podspec b/FirebaseFirestoreSwift.podspec index 4045e2a12a3..4582f9f240f 100644 --- a/FirebaseFirestoreSwift.podspec +++ b/FirebaseFirestoreSwift.podspec @@ -26,7 +26,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.osx.deployment_target = '10.13' s.tvos.deployment_target = '12.0' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.requires_arc = true diff --git a/FirebaseFirestoreTestingSupport.podspec b/FirebaseFirestoreTestingSupport.podspec index ef3dcefe46c..c2c13ccb23e 100644 --- a/FirebaseFirestoreTestingSupport.podspec +++ b/FirebaseFirestoreTestingSupport.podspec @@ -29,7 +29,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.requires_arc = true diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index 484c291a383..446d31801dc 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -28,7 +28,7 @@ Cloud Functions for Firebase. s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.swift_version = '5.3' diff --git a/FirebaseFunctions/README.md b/FirebaseFunctions/README.md index fb80500cd58..765b6c1df9f 100644 --- a/FirebaseFunctions/README.md +++ b/FirebaseFunctions/README.md @@ -7,7 +7,7 @@ integration test FirebaseFunctions: ### Prereqs -- At least CocoaPods 1.10.0 +- At least CocoaPods 1.12.0 - Install [cocoapods-generate](https://github.com/square/cocoapods-generate) ### To Develop diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index 6bce213aba1..f8d09ed3bcd 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -22,7 +22,7 @@ See more product details at https://firebase.google.com/products/in-app-messagin s.swift_version = '5.3' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseInAppMessaging/" diff --git a/FirebaseInAppMessagingSwift.podspec b/FirebaseInAppMessagingSwift.podspec index 0a1f2e049c0..02e031b4456 100644 --- a/FirebaseInAppMessagingSwift.podspec +++ b/FirebaseInAppMessagingSwift.podspec @@ -20,7 +20,7 @@ See more product details at https://firebase.google.com/products/in-app-messagin s.swift_version = '5.3' s.ios.deployment_target = '13.0' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index 538e23fe81f..218de4dcc23 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -29,7 +29,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseInstallations/Source/" diff --git a/FirebaseMLModelDownloader.podspec b/FirebaseMLModelDownloader.podspec index c14e1851e43..a7153f42828 100644 --- a/FirebaseMLModelDownloader.podspec +++ b/FirebaseMLModelDownloader.podspec @@ -28,7 +28,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index f6c0393c7bc..4651a1fe1f3 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -32,7 +32,7 @@ device, and it is completely free. s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseMessaging/" diff --git a/FirebasePerformance.podspec b/FirebasePerformance.podspec index 0c1a877a595..769227e3012 100644 --- a/FirebasePerformance.podspec +++ b/FirebasePerformance.podspec @@ -25,7 +25,7 @@ Firebase Performance library to measure performance of Mobile and Web Apps. s.ios.deployment_target = ios_deployment_target s.tvos.deployment_target = tvos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebasePerformance/" diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index b5dc61b9925..202dc7b647d 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -30,7 +30,7 @@ app update. s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseRemoteConfig/Sources/" diff --git a/FirebaseRemoteConfigSwift.podspec b/FirebaseRemoteConfigSwift.podspec index fcedabe6795..3f0b3b3174e 100644 --- a/FirebaseRemoteConfigSwift.podspec +++ b/FirebaseRemoteConfigSwift.podspec @@ -31,7 +31,7 @@ app update. s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseSessions.podspec b/FirebaseSessions.podspec index 6ac318b41b0..9842afd247a 100644 --- a/FirebaseSessions.podspec +++ b/FirebaseSessions.podspec @@ -30,7 +30,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false base_dir = "FirebaseSessions/" diff --git a/FirebaseSharedSwift.podspec b/FirebaseSharedSwift.podspec index b146408fd59..4d99e11dafe 100644 --- a/FirebaseSharedSwift.podspec +++ b/FirebaseSharedSwift.podspec @@ -30,7 +30,7 @@ Firebase products. FirebaseSharedSwift is not supported for non-Firebase usage. s.tvos.deployment_target = tvos_deployment_target s.watchos.deployment_target = watchos_deployment_target - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index 081967ae0f5..448ea1853ed 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -29,7 +29,7 @@ Firebase Storage provides robust, secure file uploads and downloads from Firebas s.swift_version = '5.3' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.source_files = [ diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index f152f72c0b0..b80ec404ce7 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| :http => 'https://dl.google.com/firebase/ios/analytics/3fcc7b954e5d5458/GoogleAppMeasurement-10.20.0.tar.gz' } - s.cocoapods_version = '>= 1.10.2' + s.cocoapods_version = '>= 1.12.0' s.ios.deployment_target = '10.0' s.osx.deployment_target = '10.13' diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index 7cf396083be..e91e77d41b3 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -20,7 +20,7 @@ Pod::Spec.new do |s| :http => 'https://dl.google.com/firebase/ios/analytics/4ab453c686c6aac4/GoogleAppMeasurementOnDeviceConversion-10.20.0.tar.gz' } - s.cocoapods_version = '>= 1.10.2' + s.cocoapods_version = '>= 1.12.0' s.ios.deployment_target = '10.0' diff --git a/GoogleUtilitiesComponents.podspec b/GoogleUtilitiesComponents.podspec index 218e4e3fafb..b52edba3ff5 100644 --- a/GoogleUtilitiesComponents.podspec +++ b/GoogleUtilitiesComponents.podspec @@ -22,7 +22,7 @@ Not intended for direct public usage. s.osx.deployment_target = '10.13' s.tvos.deployment_target = '12.0' - s.cocoapods_version = '>= 1.4.0' + s.cocoapods_version = '>= 1.12.0' s.prefix_header_file = false s.static_framework = true diff --git a/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Gemfile b/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Gemfile index 428f6565baf..e54b6b9c765 100644 --- a/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Gemfile +++ b/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Gemfile @@ -2,4 +2,4 @@ source "https://rubygems.org" -gem 'cocoapods', '1.10.0.rc.1' +gem 'cocoapods', '1.14.3' diff --git a/README.md b/README.md index aef2138f63b..7ab83d9c446 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ development with Swift Package Manager. ### CocoaPods Install the following: -* CocoaPods 1.10.0 (or later) +* CocoaPods 1.12.0 (or later) * [CocoaPods generate](https://github.com/square/cocoapods-generate) For the pod that you want to develop: From 4914cc295e41427025ebf32c7bba86b839a876eb Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Sat, 27 Jan 2024 19:24:29 -0500 Subject: [PATCH 007/104] [Release Tooling] Only embed bundles containing privacy manifests (#12324) --- .../Sources/ZipBuilder/FrameworkBuilder.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index 8f6f8f2fbbe..3f4e7172e9a 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -635,6 +635,34 @@ struct FrameworkBuilder { "\(framework): \(error)") } + // Move any privacy manifest-containing resource bundles into the + // platform framework. + try? fileManager.contentsOfDirectory( + at: frameworkPath.deletingLastPathComponent(), + includingPropertiesForKeys: nil + ) + .filter { $0.pathExtension == "bundle" } + // TODO(ncooke3): Once the zip is built with Xcode 15, the following + // `filter` can be removed. The following block exists to preserve + // how resources (e.g. like FIAM's) are packaged for use in Xcode 14. + .filter { bundleURL in + let dirEnum = fileManager.enumerator(atPath: bundleURL.path) + var containsPrivacyManifest = false + while let relativeFilePath = dirEnum?.nextObject() as? String { + if relativeFilePath.hasSuffix("PrivacyInfo.xcprivacy") { + containsPrivacyManifest = true + break + } + } + return containsPrivacyManifest + } + // Bundles are moved rather than copied to prevent them from being + // packaged in a `Resources` directory at the root of the xcframework. + .forEach { try! fileManager.moveItem( + at: $0, + to: platformFrameworkDir.appendingPathComponent($0.lastPathComponent) + ) } + // Headers from slice do { let headersSrc: URL = frameworkPath.appendingPathComponent("Headers") From 3ab2ac81bc59891eae91b3a388e30231107750e1 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:44:12 +0000 Subject: [PATCH 008/104] undo SwiftApplication.plist changes --- .../SampleSwift/AuthenticationExample/SwiftApplication.plist | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist index 633b311e4b7..50378fe58b0 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist @@ -24,6 +24,7 @@ CFBundleURLName CFBundleURLSchemes + CFBundleVersion From ae27388d6672403b3d8b27fe70a04384fe42e9e5 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Mon, 29 Jan 2024 13:02:58 +0000 Subject: [PATCH 009/104] clang formatting --- .../SettingsViewController.swift | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift index a6e1421aa38..3c90b23ea4c 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/SettingsViewController.swift @@ -186,73 +186,74 @@ extension AuthSettings: DataSourceProvidable { detailTitle: "Current Access Group")] return Section(headerDescription: "Keychain Access Groups", items: items) } - + func truncatedString(string: String, length: Int) -> String { guard string.count > length else { return string } - + let half = (length - 3) / 2 let startIndex = string.startIndex - let midIndex = string.index(startIndex, offsetBy: half) // Ensure correct mid index + let midIndex = string.index(startIndex, offsetBy: half) // Ensure correct mid index let endIndex = string.index(startIndex, offsetBy: string.count - half) - - return "\(string[startIndex.. Void) { + func showPromptWithTitle(_ title: String, message: String, showCancelButton: Bool, + completion: @escaping (Bool, String?) -> Void) { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in let userInput = alertController.textFields?.first?.text completion(true, userInput) })) - + if showCancelButton { alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in completion(false, nil) })) } - + alertController.addTextField(configurationHandler: nil) - - // Present the alert controller - // Make sure to present it from a view controller - // For example, if this code is inside a UIViewController, you can use `self.present(alertController, animated: true, completion: nil)` + + // Present the alert controller + // Make sure to present it from a view controller + // For example, if this code is inside a UIViewController, you can use + // `self.present(alertController, animated: true, completion: nil)` } - - - + // TODO: Add ability to click and clear both of these fields. private var phoneAuthSection: Section { let items = [Item(title: APNSTokenString(), detailTitle: "APNs Token"), Item(title: appCredentialString(), detailTitle: "App Credential")] return Section(headerDescription: "Phone Auth - TODO toggle off", items: items) } - + func APNSTokenString() -> String { guard let token = AppManager.shared.auth().tokenManager.token else { return "No APNs token" } - + let truncatedToken = truncatedString(string: token.string, length: 19) let tokenType = token.type == .prod ? "P" : "S" return "\(truncatedToken)(\(tokenType))" } - + func clearAPNSToken() { guard let token = AppManager.shared.auth().tokenManager.token else { return } - + let tokenType = token.type == .prod ? "Production" : "Sandbox" let message = "token: \(token.string)\ntype: \(tokenType)" - - self.showPromptWithTitle("Clear APNs Token?", message: message, showCancelButton: true) { (userPressedOK, userInput) in + + showPromptWithTitle("Clear APNs Token?", message: message, + showCancelButton: true) { userPressedOK, userInput in if userPressedOK { AppManager.shared.auth().tokenManager.token = nil } } } - + func appCredentialString() -> String { if let credential = AppManager.shared.auth().appCredentialManager.credential { let truncatedReceipt = truncatedString(string: credential.receipt, length: 13) @@ -262,13 +263,13 @@ extension AuthSettings: DataSourceProvidable { return "No App Credential" } } - - + func clearAppCredential() { if let credential = AppManager.shared.auth().appCredentialManager.credential { let message = "receipt: \(credential.receipt)\nsecret: \(credential.secret)" - - showPromptWithTitle("Clear App Credential?", message: message, showCancelButton: true) { (userPressedOK, _) in + + showPromptWithTitle("Clear App Credential?", message: message, + showCancelButton: true) { userPressedOK, _ in if userPressedOK { AppManager.shared.auth().appCredentialManager.clearCredential() } @@ -276,7 +277,6 @@ extension AuthSettings: DataSourceProvidable { } } - private var languageSection: Section { let languageCode = AppManager.shared.auth().languageCode let items = [Item(title: languageCode ?? "[none]", detailTitle: "Auth Language"), From 6b0cc0276cef38f7bdf056923d4785cfc48a5077 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 29 Jan 2024 09:43:20 -0800 Subject: [PATCH 010/104] Several minor doc fixes for Functions (#12323) --- .../Sources/Callable+Codable.swift | 2 +- FirebaseFunctions/Sources/Functions.swift | 75 +++++------ .../Sources/FunctionsError.swift | 2 +- FirebaseFunctions/Sources/HTTPSCallable.swift | 118 ++++++++---------- 4 files changed, 89 insertions(+), 108 deletions(-) diff --git a/FirebaseFunctions/Sources/Callable+Codable.swift b/FirebaseFunctions/Sources/Callable+Codable.swift index 559b9846ce4..5a8e508a9f0 100644 --- a/FirebaseFunctions/Sources/Callable+Codable.swift +++ b/FirebaseFunctions/Sources/Callable+Codable.swift @@ -15,7 +15,7 @@ import FirebaseSharedSwift import Foundation -// A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions. +/// A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions. public struct Callable { /// The timeout to use when calling the function. Defaults to 60 seconds. public var timeoutInterval: TimeInterval { diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 61d00d5e040..14eeb4e4f1b 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -38,9 +38,7 @@ enum FunctionsConstants { static let defaultRegion = "us-central1" } -/** - * `Functions` is the client for Cloud Functions for a Firebase project. - */ +/// `Functions` is the client for Cloud Functions for a Firebase project. @objc(FIRFunctions) open class Functions: NSObject { // MARK: - Private Variables @@ -61,15 +59,12 @@ enum FunctionsConstants { // MARK: - Public APIs - /** - * The current emulator origin, or `nil` if it is not set. - */ + /// The current emulator origin, or `nil` if it is not set. open private(set) var emulatorOrigin: String? - /** - * Creates a Cloud Functions client using the default or returns a pre-existing instance if it already exists. - * - Returns: A shared Functions instance initialized with the default `FirebaseApp`. - */ + /// Creates a Cloud Functions client using the default or returns a pre-existing instance if it + /// already exists. + /// - Returns: A shared Functions instance initialized with the default `FirebaseApp`. @objc(functions) open class func functions() -> Functions { return functions( app: FirebaseApp.app(), @@ -78,57 +73,51 @@ enum FunctionsConstants { ) } - /** - * Creates a Cloud Functions client with the given app, or returns a pre-existing - * instance if one already exists. - * - Parameter app The app for the Firebase project. - * - Returns: A shared Functions instance initialized with the specified `FirebaseApp`. - */ + /// Creates a Cloud Functions client with the given app, or returns a pre-existing + /// instance if one already exists. + /// - Parameter app: The app for the Firebase project. + /// - Returns: A shared Functions instance initialized with the specified `FirebaseApp`. @objc(functionsForApp:) open class func functions(app: FirebaseApp) -> Functions { return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: nil) } - /** - * Creates a Cloud Functions client with the default app and given region. - * - Parameter region The region for the HTTP trigger, such as `us-central1`. - * - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a custom region. - */ + /// Creates a Cloud Functions client with the default app and given region. + /// - Parameter region: The region for the HTTP trigger, such as `us-central1`. + /// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a + /// custom region. @objc(functionsForRegion:) open class func functions(region: String) -> Functions { return functions(app: FirebaseApp.app(), region: region, customDomain: nil) } - /** - * Creates a Cloud Functions client with the given app and region, or returns a pre-existing - * instance if one already exists. - * - Parameter customDomain A custom domain for the HTTP trigger, such as "https://mydomain.com". - * - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a custom HTTP trigger domain. - */ + /// Creates a Cloud Functions client with the given custom domain or returns a pre-existing + /// instance if one already exists. + /// - Parameter customDomain: A custom domain for the HTTP trigger, such as + /// "https://mydomain.com". + /// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a + /// custom HTTP trigger domain. @objc(functionsForCustomDomain:) open class func functions(customDomain: String) -> Functions { return functions(app: FirebaseApp.app(), region: FunctionsConstants.defaultRegion, customDomain: customDomain) } - /** - * Creates a Cloud Functions client with the given app and region, or returns a pre-existing - * instance if one already exists. - * - Parameters: - * - app: The app for the Firebase project. - * - region: The region for the HTTP trigger, such as `us-central1`. - * - Returns: An instance of `Functions` with a custom app and region. - */ + /// Creates a Cloud Functions client with the given app and region, or returns a pre-existing + /// instance if one already exists. + /// - Parameters: + /// - app: The app for the Firebase project. + /// - region: The region for the HTTP trigger, such as `us-central1`. + /// - Returns: An instance of `Functions` with a custom app and region. @objc(functionsForApp:region:) open class func functions(app: FirebaseApp, region: String) -> Functions { return functions(app: app, region: region, customDomain: nil) } - /** - * Creates a Cloud Functions client with the given app and region, or returns a pre-existing - * instance if one already exists. - * - Parameters: - * - app The app for the Firebase project. - * - customDomain A custom domain for the HTTP trigger, such as `https://mydomain.com`. - * - Returns: An instance of `Functions` with a custom app and HTTP trigger domain. - */ + /// Creates a Cloud Functions client with the given app and custom domain, or returns a + /// pre-existing + /// instance if one already exists. + /// - Parameters: + /// - app: The app for the Firebase project. + /// - customDomain: A custom domain for the HTTP trigger, such as `https://mydomain.com`. + /// - Returns: An instance of `Functions` with a custom app and HTTP trigger domain. @objc(functionsForApp:customDomain:) open class func functions(app: FirebaseApp, customDomain: String) -> Functions { diff --git a/FirebaseFunctions/Sources/FunctionsError.swift b/FirebaseFunctions/Sources/FunctionsError.swift index 8227d1cd1e0..8af6df9e077 100644 --- a/FirebaseFunctions/Sources/FunctionsError.swift +++ b/FirebaseFunctions/Sources/FunctionsError.swift @@ -14,7 +14,7 @@ import Foundation -/// The error domain for codes in the `FunctionsErrorCode` enum. +/// The error domain for codes in the ``FunctionsErrorCode`` enum. public let FunctionsErrorDomain: String = "com.firebase.functions" /// The key for finding error details in the `NSError` userInfo. diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index 5f773d43463..8391d153236 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -14,18 +14,14 @@ import Foundation -/** - * A `HTTPSCallableResult` contains the result of calling a `HTTPSCallable`. - */ +/// A `HTTPSCallableResult` contains the result of calling a `HTTPSCallable`. @objc(FIRHTTPSCallableResult) open class HTTPSCallableResult: NSObject { - /** - * The data that was returned from the Callable HTTPS trigger. - * - * The data is in the form of native objects. For example, if your trigger returned an - * array, this object would be an `Array`. If your trigger returned a JavaScript object with - * keys and values, this object would be an instance of `[String: Any]`. - */ + /// The data that was returned from the Callable HTTPS trigger. + /// + /// The data is in the form of native objects. For example, if your trigger returned an + /// array, this object would be an `Array`. If your trigger returned a JavaScript object with + /// keys and values, this object would be an instance of `[String: Any]`. @objc public let data: Any init(data: Any) { @@ -54,9 +50,7 @@ open class HTTPSCallable: NSObject { // MARK: - Public Properties - /** - * The timeout to use when calling the function. Defaults to 70 seconds. - */ + /// The timeout to use when calling the function. Defaults to 70 seconds. @objc open var timeoutInterval: TimeInterval = 70 init(functions: Functions, name: String, options: HTTPSCallableOptions? = nil) { @@ -71,28 +65,27 @@ open class HTTPSCallable: NSObject { endpoint = .url(url) } - /** - * Executes this Callable HTTPS trigger asynchronously. - * - * The data passed into the trigger can be any of the following types: - * - `nil` or `NSNull` - * - `String` - * - `NSNumber`, or any Swift numeric type bridgeable to `NSNumber` - * - `[Any]`, where the contained objects are also one of these types. - * - `[String: Any]` where the values are also one of these types. - * - * The request to the Cloud Functions backend made by this method automatically includes a - * Firebase Installations ID token to identify the app instance. If a user is logged in with - * Firebase Auth, an auth ID token for the user is also automatically included. - * - * Firebase Cloud Messaging sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see `Messaging.deleteData()`. It - * resumes with a new FCM Token the next time you call this method. - * - * - Parameters: - * - data: Parameters to pass to the trigger. - * - completion: The block to call when the HTTPS request has completed. - */ + /// Executes this Callable HTTPS trigger asynchronously. + /// + /// The data passed into the trigger can be any of the following types: + /// - `nil` or `NSNull` + /// - `String` + /// - `NSNumber`, or any Swift numeric type bridgeable to `NSNumber` + /// - `[Any]`, where the contained objects are also one of these types. + /// - `[String: Any]` where the values are also one of these types. + /// + /// The request to the Cloud Functions backend made by this method automatically includes a + /// Firebase Installations ID token to identify the app instance. If a user is logged in with + /// Firebase Auth, an auth ID token for the user is also automatically included. + /// + /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect + /// information + /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It + /// resumes with a new FCM Token the next time you call this method. + /// + /// - Parameters: + /// - data: Parameters to pass to the trigger. + /// - completion: The block to call when the HTTPS request has completed. @objc(callWithObject:completion:) open func call(_ data: Any? = nil, completion: @escaping (HTTPSCallableResult?, Error?) -> Void) { @@ -121,39 +114,38 @@ open class HTTPSCallable: NSObject { } } - /** - * Executes this Callable HTTPS trigger asynchronously. This API should only be used from Objective-C. - * - * The request to the Cloud Functions backend made by this method automatically includes a - * Firebase Installations ID token to identify the app instance. If a user is logged in with - * Firebase Auth, an auth ID token for the user is also automatically included. - * - * Firebase Cloud Messaging sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see `Messaging.deleteData()`. It - * resumes with a new FCM Token the next time you call this method. - * - * - Parameter completion The block to call when the HTTPS request has completed. - */ + /// Executes this Callable HTTPS trigger asynchronously. This API should only be used from + /// Objective-C. + /// + /// The request to the Cloud Functions backend made by this method automatically includes a + /// Firebase Installations ID token to identify the app instance. If a user is logged in with + /// Firebase Auth, an auth ID token for the user is also automatically included. + /// + /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect + /// information + /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It + /// resumes with a new FCM Token the next time you call this method. + /// + /// - Parameter completion: The block to call when the HTTPS request has completed. @objc(callWithCompletion:) public func __call(completion: @escaping (HTTPSCallableResult?, Error?) -> Void) { call(nil, completion: completion) } - /** - * Executes this Callable HTTPS trigger asynchronously. - * - * The request to the Cloud Functions backend made by this method automatically includes a - * FCM token to identify the app instance. If a user is logged in with Firebase - * Auth, an auth ID token for the user is also automatically included. - * - * Firebase Cloud Messaging sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see `Messaging.deleteData()`. It - * resumes with a new FCM Token the next time you call this method. - * - * - Parameter data Parameters to pass to the trigger. - * - Throws: An error if the Cloud Functions invocation failed. - * - Returns: The result of the call. - */ + /// Executes this Callable HTTPS trigger asynchronously. + /// + /// The request to the Cloud Functions backend made by this method automatically includes a + /// FCM token to identify the app instance. If a user is logged in with Firebase + /// Auth, an auth ID token for the user is also automatically included. + /// + /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect + /// information + /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It + /// resumes with a new FCM Token the next time you call this method. + /// + /// - Parameter data: Parameters to pass to the trigger. + /// - Throws: An error if the Cloud Functions invocation failed. + /// - Returns: The result of the call. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func call(_ data: Any? = nil) async throws -> HTTPSCallableResult { return try await withCheckedThrowingContinuation { continuation in From 165f94a9b68ea253cce06e9123b80f4c35c4a66b Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:44:49 -0500 Subject: [PATCH 011/104] [Release] Update CHANGELOGs for 10.21.0 (#12332) --- Firestore/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 4e5bc22d8b9..c05338877fd 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# 10.21.0 - Add an error when trying to build Firestore's binary SPM distribution for visionOS (#12279). See Firestore's 10.12.0 release note for a supported workaround. From b21dfeb4465dfed8f72169fad7e632d8060d3fd8 Mon Sep 17 00:00:00 2001 From: pcfba <111909874+pcfba@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:40:21 -0800 Subject: [PATCH 012/104] Analytics 10.21.0 (#12335) --- FirebaseAnalytics.podspec | 2 +- GoogleAppMeasurement.podspec | 2 +- Package.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index d02db7df40e..19295bad4cd 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/0199e7929b47e2d9/FirebaseAnalytics-10.20.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/573d1b06cde0fa35/FirebaseAnalytics-10.21.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index b80ec404ce7..727d67b9985 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/3fcc7b954e5d5458/GoogleAppMeasurement-10.20.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/a95ed962e7ccb437/GoogleAppMeasurement-10.21.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/Package.swift b/Package.swift index 7243a111adb..b7f30fb9773 100644 --- a/Package.swift +++ b/Package.swift @@ -310,8 +310,8 @@ let package = Package( ), .binaryTarget( name: "FirebaseAnalytics", - url: "https://dl.google.com/firebase/ios/swiftpm/10.20.0/FirebaseAnalytics.zip", - checksum: "169e9983be26e31bff373ea3ae5b56559a49f6bf14986f8813be5af03c00b251" + url: "https://dl.google.com/firebase/ios/swiftpm/10.21.0/FirebaseAnalytics.zip", + checksum: "4d2c6daf6fd6f4d9e3071ed0051d4651648f44a01712a5949a36f169b1a2bd61" ), .target( name: "FirebaseAnalyticsSwiftTarget", @@ -1312,7 +1312,7 @@ func googleAppMeasurementDependency() -> Package.Dependency { return .package(url: appMeasurementURL, branch: "main") } - return .package(url: appMeasurementURL, exact: "10.20.0") + return .package(url: appMeasurementURL, exact: "10.21.0") } func abseilDependency() -> Package.Dependency { From c153f97f7890dd24fd5fcaa4bc0d2a9d7fc72de2 Mon Sep 17 00:00:00 2001 From: renkelvin Date: Tue, 30 Jan 2024 16:26:46 -0800 Subject: [PATCH 013/104] Clean auth e2e tests (#12336) --- .../Tests/Sample/E2eTests/BYOAuthTests.m | 86 ------------------- .../Tests/Sample/E2eTests/FIRAuthE2eTests.m | 56 ------------ .../Sample/E2eTests/FIRAuthE2eTestsBase.h | 42 --------- .../Sample/E2eTests/FIRAuthE2eTestsBase.m | 70 --------------- FirebaseAuth/Tests/Sample/E2eTests/Info.plist | 22 ----- .../Sample/E2eTests/VerifyIOSClientTests.m | 38 -------- FirebaseAuth/Tests/Sample/Podfile | 5 -- 7 files changed, 319 deletions(-) delete mode 100644 FirebaseAuth/Tests/Sample/E2eTests/BYOAuthTests.m delete mode 100644 FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTests.m delete mode 100644 FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.h delete mode 100644 FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.m delete mode 100644 FirebaseAuth/Tests/Sample/E2eTests/Info.plist delete mode 100644 FirebaseAuth/Tests/Sample/E2eTests/VerifyIOSClientTests.m diff --git a/FirebaseAuth/Tests/Sample/E2eTests/BYOAuthTests.m b/FirebaseAuth/Tests/Sample/E2eTests/BYOAuthTests.m deleted file mode 100644 index dbe71266b81..00000000000 --- a/FirebaseAuth/Tests/Sample/E2eTests/BYOAuthTests.m +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2019 Google - * - * 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. - */ - -#import "FIRAuthE2eTestsBase.h" - -/** The url for obtaining a valid custom token string used to test BYOAuth. */ -static NSString *const kCustomTokenUrl = @"https://gcip-testapps.wl.r.appspot.com/token"; - -/** The invalid custom token string for testing BYOAuth. */ -static NSString *const kInvalidCustomToken = @"invalid token."; - -/** The user name string for BYOAuth testing account. */ -static NSString *const kTestingAccountUserID = @"BYU_Test_User_ID"; - -@interface BYOAuthTests : FIRAuthE2eTestsBase - -@end - -@implementation BYOAuthTests - -/** Test sign in with a valid BYOAuth token retrived from a remote server. */ -- (void)testSignInWithValidBYOAuthToken { - NSError *error; - NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl] - encoding:NSUTF8StringEncoding - error:&error]; - if (!customToken) { - GREYFail(@"There was an error retrieving the custom token: %@", error); - } - - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])] - performAction:grey_replaceText(customToken)] assertWithMatcher:grey_text(customToken)]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()]; - - [self waitForElementWithText:@"OK" withDelay:kWaitForElementTimeOut]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; - - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(kTestingAccountUserID), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] assertWithMatcher:grey_sufficientlyVisible()]; -} - -- (void)testSignInWithInvalidBYOAuthToken { - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])] - performAction:grey_replaceText(kInvalidCustomToken)] - assertWithMatcher:grey_text(kInvalidCustomToken)]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()]; - - NSString *invalidTokenErrorMessage = @"Sign-In Error"; - - [self waitForElementWithText:invalidTokenErrorMessage withDelay:kWaitForElementTimeOut]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; -} - -@end diff --git a/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTests.m b/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTests.m deleted file mode 100644 index 50512f53699..00000000000 --- a/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTests.m +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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. - */ - -#import "FIRAuthE2eTestsBase.h" - -@interface FIRAuthE2eTests : FIRAuthE2eTestsBase - -@end - -@implementation FIRAuthE2eTests - -- (void)testSignInExistingUser { - NSString *email = @"123@abc.com"; - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign in with Email/Password"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - id comfirmationButtonMatcher = - grey_allOf(grey_kindOfClass([UILabel class]), grey_accessibilityLabel(@"OK"), nil); - - [[EarlGrey selectElementWithMatcher: - // TODO: Add accessibilityIdentifiers for the elements. - grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))] - performAction:grey_typeText(email)]; - - [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()]; - - [[EarlGrey - selectElementWithMatcher:grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))] - performAction:grey_typeText(@"password")]; - - [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()]; - - [[[EarlGrey - selectElementWithMatcher:grey_allOf(grey_text(email), grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] assertWithMatcher:grey_sufficientlyVisible()]; -} - -@end diff --git a/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.h b/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.h deleted file mode 100644 index 5c25b0d1194..00000000000 --- a/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2019 Google - * - * 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. - */ - -#import - -#import - -#import - -NS_ASSUME_NONNULL_BEGIN - -extern CGFloat const kShortScrollDistance; - -extern NSTimeInterval const kWaitForElementTimeOut; - -/** Convenience function for EarlGrey tests. */ -id grey_scrollView(void); - -@interface FIRAuthE2eTestsBase : XCTestCase - -/** Sign out current account. */ -- (void)signOut; - -/** Wait for an element with text to appear. */ -- (void)waitForElementWithText:(NSString *)text withDelay:(NSTimeInterval)maxDelay; - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.m b/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.m deleted file mode 100644 index 08fc7be79f8..00000000000 --- a/FirebaseAuth/Tests/Sample/E2eTests/FIRAuthE2eTestsBase.m +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2019 Google - * - * 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. - */ - -#import "FIRAuthE2eTestsBase.h" - -CGFloat const kShortScrollDistance = 400; - -NSTimeInterval const kWaitForElementTimeOut = 15; - -/** Convenience function for EarlGrey tests. */ -id grey_scrollView(void) { - return [GREYMatchers matcherForKindOfClass:[UIScrollView class]]; -} - -@implementation FIRAuthE2eTestsBase - -/** To reset the app so that each test sees the app in a clean state. */ -- (void)setUp { - [super setUp]; - - [self signOut]; - - [[EarlGrey selectElementWithMatcher:grey_allOf(grey_scrollView(), - grey_kindOfClass([UITableView class]), nil)] - performAction:grey_scrollToContentEdge(kGREYContentEdgeTop)]; -} - -- (void)tearDown { - [super tearDown]; -} - -/** Sign out current account. */ -- (void)signOut { - NSError *signOutError; - BOOL status = [[FIRAuth auth] signOut:&signOutError]; - - // Just log the error because we don't want to fail the test if signing out fails. - if (!status) { - NSLog(@"Error signing out: %@", signOutError); - } -} - -/** Wait for an element with text to appear. */ -- (void)waitForElementWithText:(NSString *)text withDelay:(NSTimeInterval)maxDelay { - GREYCondition *displayed = - [GREYCondition conditionWithName:@"Wait for element" - block:^BOOL { - NSError *error = nil; - [[EarlGrey selectElementWithMatcher:grey_text(text)] - assertWithMatcher:grey_sufficientlyVisible() - error:&error]; - return !error; - }]; - GREYAssertTrue([displayed waitWithTimeout:maxDelay], @"Failed to wait for element '%@'.", text); -} - -@end diff --git a/FirebaseAuth/Tests/Sample/E2eTests/Info.plist b/FirebaseAuth/Tests/Sample/E2eTests/Info.plist deleted file mode 100644 index 6c6c23c43ad..00000000000 --- a/FirebaseAuth/Tests/Sample/E2eTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/FirebaseAuth/Tests/Sample/E2eTests/VerifyIOSClientTests.m b/FirebaseAuth/Tests/Sample/E2eTests/VerifyIOSClientTests.m deleted file mode 100644 index 131a56430de..00000000000 --- a/FirebaseAuth/Tests/Sample/E2eTests/VerifyIOSClientTests.m +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2018 Google - * - * 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. - */ - -#import "FIRAuthE2eTestsBase.h" - -@interface VerifyIOSClientTests : FIRAuthE2eTestsBase - -@end - -@implementation VerifyIOSClientTests - -/** Test verify ios client*/ -- (void)testVerifyIOSClient { - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Verify iOS client"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - [self waitForElementWithText:@"OK" withDelay:kWaitForElementTimeOut]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; -} - -@end diff --git a/FirebaseAuth/Tests/Sample/Podfile b/FirebaseAuth/Tests/Sample/Podfile index 86e81c13d49..f7c904a35c7 100644 --- a/FirebaseAuth/Tests/Sample/Podfile +++ b/FirebaseAuth/Tests/Sample/Podfile @@ -27,9 +27,4 @@ target 'AuthSample' do target 'SwiftApiTests' do inherit! :search_paths end - - target 'Auth_E2eTests' do - inherit! :search_paths - pod 'EarlGrey' - end end From f91c8167141d0279726c6f6d9d4a47c026785cbc Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:18:37 -0500 Subject: [PATCH 014/104] [Release] Add Firestore release binary (#12338) --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index b7f30fb9773..2b2d6190bc6 100644 --- a/Package.swift +++ b/Package.swift @@ -1481,8 +1481,8 @@ func firestoreTargets() -> [Target] { } else { return .binaryTarget( name: "FirebaseFirestoreInternal", - url: "https://dl.google.com/firebase/ios/bin/firestore/10.20.0/FirebaseFirestoreInternal.zip", - checksum: "7fe8f913d35e257979eddc8e2df0fedd3b89735c7030494307079747f03279c7" + url: "https://dl.google.com/firebase/ios/bin/firestore/10.21.0/FirebaseFirestoreInternal.zip", + checksum: "50d864ef4e7e090ea1388926674d7095ae5a83ac429f788c3d6e3497e7a5b175" ) } }() From dd77b8ed7ade276cfd52bd6ac9aa98466b64786c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 1 Feb 2024 07:04:00 -0800 Subject: [PATCH 015/104] Update GHA artifact functions (#12337) --- .github/workflows/api_diff_report.yml | 2 +- .github/workflows/firebase_app_check.yml | 2 +- .../workflows/health-metrics-presubmit.yml | 24 +++--- .github/workflows/prerelease.yml | 30 +++---- .github/workflows/release.yml | 30 +++---- .github/workflows/spectesting.yml | 2 +- .github/workflows/zip.yml | 79 ++++++++----------- 7 files changed, 79 insertions(+), 90 deletions(-) diff --git a/.github/workflows/api_diff_report.yml b/.github/workflows/api_diff_report.yml index 872d36d1372..650f94f4727 100644 --- a/.github/workflows/api_diff_report.yml +++ b/.github/workflows/api_diff_report.yml @@ -82,7 +82,7 @@ jobs: --commit $GITHUB_SHA \ --run_id ${{github.run_id}} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: api_info_and_report diff --git a/.github/workflows/firebase_app_check.yml b/.github/workflows/firebase_app_check.yml index 7683e2aed1d..4893fe26042 100644 --- a/.github/workflows/firebase_app_check.yml +++ b/.github/workflows/firebase_app_check.yml @@ -72,7 +72,7 @@ jobs: run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseAppCheckUnit iOS spm ${{ matrix.diagnostic }} - name: Upload raw logs if failed if: ${{ failure() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: failure-xcodebuild-raw-logs path: xcodebuild.log diff --git a/.github/workflows/health-metrics-presubmit.yml b/.github/workflows/health-metrics-presubmit.yml index 4f233c6c806..0a91c454492 100644 --- a/.github/workflows/health-metrics-presubmit.yml +++ b/.github/workflows/health-metrics-presubmit.yml @@ -72,7 +72,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseABTesting --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -92,7 +92,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseAuth --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -115,7 +115,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseDatabase --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -138,7 +138,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseDynamicLinks --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -164,7 +164,7 @@ jobs: run: | export EXPERIMENTAL_MODE=true ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseFirestore --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -187,7 +187,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseFunctions --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -210,7 +210,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseInAppMessaging --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -233,7 +233,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseMessaging --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -258,7 +258,7 @@ jobs: run: gem install xcpretty - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebasePerformance --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -281,7 +281,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseRemoteConfig --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -304,7 +304,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseStorage --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: codecoverage path: /Users/runner/*.xcresult @@ -331,7 +331,7 @@ jobs: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/metrics_service_access.json.gpg \ metrics-access.json "${{ env.METRICS_SERVICE_SECRET }}" gcloud auth activate-service-account --key-file metrics-access.json - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 id: download with: path: /Users/runner/test diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b4252adb6ba..87164ab1c1d 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -62,7 +62,7 @@ jobs: if: ${{ always() }} run: | rm -rf oss-bot-access.txt - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: firebase-ios-sdk path: | @@ -80,7 +80,7 @@ jobs: targeted_pod: FirebaseCore steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: firebase-ios-sdk path: ${{ env.local_sdk_repo_dir }} @@ -122,7 +122,7 @@ jobs: targeted_pod: ${{ matrix.podspec }} steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: firebase-ios-sdk path: ${{ env.local_sdk_repo_dir }} @@ -234,7 +234,7 @@ jobs: env: LEGACY: true run: scripts/remove_data.sh config release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_abtesting @@ -266,7 +266,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Authentication false) - name: Remove data before upload run: scripts/remove_data.sh authentication release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_auth @@ -315,7 +315,7 @@ jobs: env: LEGACY: true run: scripts/remove_data.sh crashlytics release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_crashlytics @@ -351,7 +351,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false swift) - name: Remove data before upload run: scripts/remove_data.sh database release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_database @@ -393,7 +393,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh DynamicLinks true swift) - name: Remove data before upload run: scripts/remove_data.sh dynamiclinks release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_dynamiclinks @@ -428,7 +428,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Firestore false) - name: Remove data before upload run: scripts/remove_data.sh firestore release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_firestore @@ -469,7 +469,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Functions true swift) - name: Remove data before upload run: scripts/remove_data.sh functions release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_functions @@ -507,7 +507,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true swift) - name: Remove data before upload run: scripts/remove_data.sh inappmessaging release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_inappmessaging @@ -545,7 +545,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Messaging false swift) - name: Remove data before upload run: scripts/remove_data.sh messaging release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_messaging @@ -577,7 +577,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Config true) - name: Remove data before upload run: scripts/remove_data.sh config release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_config @@ -614,7 +614,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true swift) - name: Remove data before upload run: scripts/remove_data.sh storage release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_storage @@ -651,7 +651,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true swift) - name: Remove data before upload run: scripts/remove_data.sh performance release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_performance diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1434c97c119..dcf67d29dba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,7 @@ jobs: if: ${{ always() }} run: | rm -rf bot-access.txt - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: firebase-ios-sdk path: | @@ -84,7 +84,7 @@ jobs: targeted_pod: FirebaseCore steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: firebase-ios-sdk path: ${{ env.local_sdk_repo_dir }} @@ -124,7 +124,7 @@ jobs: targeted_pod: ${{ matrix.podspec }} steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: firebase-ios-sdk path: ${{ env.local_sdk_repo_dir }} @@ -185,7 +185,7 @@ jobs: env: LEGACY: true run: scripts/remove_data.sh config release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_abtesting @@ -217,7 +217,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Authentication false) - name: Remove data before upload run: scripts/remove_data.sh authentication release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_auth @@ -266,7 +266,7 @@ jobs: env: LEGACY: true run: scripts/remove_data.sh crashlytics release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_crashlytics @@ -302,7 +302,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false swift) - name: Remove data before upload run: scripts/remove_data.sh database release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_database @@ -344,7 +344,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh DynamicLinks true swift) - name: Remove data before upload run: scripts/remove_data.sh dynamiclinks release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_dynamiclinks @@ -379,7 +379,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Firestore false) - name: Remove data before upload run: scripts/remove_data.sh firestore release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_firestore @@ -420,7 +420,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Functions true swift) - name: Remove data before upload run: scripts/remove_data.sh functions release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_functions @@ -458,7 +458,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true swift) - name: Remove data before upload run: scripts/remove_data.sh inappmessaging release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_inappmessaging @@ -496,7 +496,7 @@ jobs: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Messaging false swift) - name: Remove data before upload run: scripts/remove_data.sh messaging release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_messaging @@ -528,7 +528,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Config true) - name: Remove data before upload run: scripts/remove_data.sh config release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_config @@ -565,7 +565,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true swift) - name: Remove data before upload run: scripts/remove_data.sh storage release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_storage @@ -602,7 +602,7 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true swift) - name: Remove data before upload run: scripts/remove_data.sh performance release_testing - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_performance diff --git a/.github/workflows/spectesting.yml b/.github/workflows/spectesting.yml index 7db44c2e70d..fe1e0c042a5 100644 --- a/.github/workflows/spectesting.yml +++ b/.github/workflows/spectesting.yml @@ -62,7 +62,7 @@ jobs: swift run podspecs-tester --git-root "${GITHUB_WORKSPACE}" --podspec ${PODSPEC} --skip-tests --temp-log-dir "${GITHUB_WORKSPACE}/specTestingLogs" - name: Upload Failed Testing Logs if: failure() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: specTestingLogs path: specTestingLogs/*.txt diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 3998b60e3c9..2bebb1f7da2 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -44,7 +44,7 @@ jobs: mkdir -p release_zip_dir sh -x scripts/build_zip.sh release_zip_dir \ "${{ github.event.inputs.custom_spec_repos || 'https://github.com/firebase/SpecsStaging.git' }}" - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 with: name: Firebase-release-zip-zip # Zip the entire output directory since the builder adds subdirectories we don't know the @@ -85,7 +85,7 @@ jobs: sh -x scripts/build_zip.sh \ zip_output_dir "${{ github.event.inputs.custom_spec_repos || 'https://github.com/firebase/SpecsStaging.git,https://github.com/firebase/SpecsDev.git' }}" \ build-head - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 with: name: Firebase-actions-dir # Zip the entire output directory since the builder adds subdirectories we don't know the @@ -99,7 +99,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "ABTesting" strategy: matrix: @@ -113,7 +112,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -122,7 +121,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer @@ -148,7 +147,7 @@ jobs: LEGACY: true if: ${{ failure() }} run: scripts/remove_data.sh abtesting - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_abtesting @@ -161,7 +160,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "Authentication" strategy: matrix: @@ -175,7 +173,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -184,7 +182,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Setup Swift Quickstart @@ -202,7 +200,7 @@ jobs: - name: Remove data before upload if: ${{ failure() }} run: scripts/remove_data.sh authentiation - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_auth @@ -215,7 +213,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "Config" strategy: matrix: @@ -229,7 +226,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -238,7 +235,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Setup Swift Quickstart @@ -254,7 +251,7 @@ jobs: - name: Remove data before upload if: ${{ failure() }} run: scripts/remove_data.sh config - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_config @@ -267,7 +264,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "Crashlytics" strategy: matrix: @@ -281,7 +277,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -290,7 +286,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer @@ -329,7 +325,7 @@ jobs: LEGACY: true if: ${{ failure() }} run: scripts/remove_data.sh crashlytics - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_crashlytics @@ -342,7 +338,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "Database" strategy: matrix: @@ -357,7 +352,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -366,7 +361,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer @@ -386,7 +381,7 @@ jobs: - name: Remove data before upload if: ${{ failure() }} run: scripts/remove_data.sh database - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts database @@ -399,7 +394,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "DynamicLinks" strategy: matrix: @@ -413,7 +407,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -422,7 +416,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - name: Setup Objc Quickstart run: SAMPLE="$SDK" TARGET="${SDK}Example" scripts/setup_quickstart_framework.sh \ "${HOME}"/ios_frameworks/Firebase/FirebaseDynamicLinks/* \ @@ -446,7 +440,7 @@ jobs: - name: Remove data before upload if: ${{ failure() }} run: scripts/remove_data.sh dynamiclinks - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_dynamiclinks @@ -459,7 +453,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "Firestore" strategy: matrix: @@ -474,7 +467,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -483,7 +476,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Setup quickstart run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="SDWebImage FirebaseAuthUI FirebaseEmailAuthUI" scripts/setup_quickstart_framework.sh \ @@ -501,7 +494,7 @@ jobs: - name: Remove data before upload if: ${{ failure() }} run: scripts/remove_data.sh firestore - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_firestore @@ -512,7 +505,6 @@ jobs: if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head env: - FRAMEWORK_DIR: "Firebase-actions-dir" FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 runs-on: macos-13 steps: @@ -520,7 +512,7 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_14.1.app/Contents/Developer - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -531,7 +523,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Check linked Firestore.xcframework for unlinked symbols. run: | @@ -546,7 +538,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "InAppMessaging" strategy: matrix: @@ -560,7 +551,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -569,7 +560,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Setup quickstart run: SAMPLE="$SDK" TARGET="${SDK}Example" scripts/setup_quickstart_framework.sh \ @@ -590,7 +581,7 @@ jobs: - name: Remove data before upload if: ${{ failure() }} run: scripts/remove_data.sh inappmessaging - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_ihappmessaging @@ -603,7 +594,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "Messaging" strategy: matrix: @@ -617,7 +607,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -626,7 +616,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Setup quickstart run: SAMPLE="$SDK" TARGET="${SDK}Example" scripts/setup_quickstart_framework.sh \ @@ -646,7 +636,7 @@ jobs: - name: Remove data before upload if: ${{ failure() }} run: scripts/remove_data.sh messaging - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_messaging @@ -659,7 +649,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - FRAMEWORK_DIR: "Firebase-actions-dir" SDK: "Storage" strategy: matrix: @@ -673,7 +662,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get framework dir - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: Firebase-actions-dir - uses: ruby/setup-ruby@v1 @@ -682,7 +671,7 @@ jobs: - name: Move frameworks run: | mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}/${FRAMEWORK_DIR}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - uses: actions/checkout@v4 - name: Setup quickstart env: @@ -713,7 +702,7 @@ jobs: LEGACY: true if: ${{ failure() }} run: scripts/remove_data.sh storage - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: quickstart_artifacts_storage From 8ec4afc4c79e9d2b5b33523c54999b392dc59432 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 1 Feb 2024 14:04:06 -0800 Subject: [PATCH 016/104] Try GHA retry (#12341) --- .github/workflows/storage.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/storage.yml b/.github/workflows/storage.yml index fc9a6bf9daa..4585647f128 100644 --- a/.github/workflows/storage.yml +++ b/.github/workflows/storage.yml @@ -51,8 +51,13 @@ jobs: FirebaseStorage/Tests/Integration/Credentials.swift "$plist_secret" - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: BuildAndTest # can be replaced with pod lib lint with CocoaPods 1.10 - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh Storage${{ matrix.language }} all) + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: ([ -z $plist_secret ] || scripts/build.sh Storage${{ matrix.language }} all) spm: # Don't run on private repo unless it is a PR. From 2d3ecf96588079a19734d32713f44e22b1b41451 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 1 Feb 2024 14:57:41 -0800 Subject: [PATCH 017/104] Storage docs fixes (#12344) --- FirebaseStorage/Sources/AsyncAwait.swift | 26 +- FirebaseStorage/Sources/Result.swift | 35 +- FirebaseStorage/Sources/Storage.swift | 188 ++++---- .../Sources/StorageDownloadTask.swift | 3 + .../Sources/StorageListResult.swift | 8 +- FirebaseStorage/Sources/StorageMetadata.swift | 13 +- .../Sources/StorageObservableTask.swift | 5 +- .../Sources/StorageReference.swift | 418 ++++++++---------- FirebaseStorage/Sources/StorageTask.swift | 2 + .../Sources/StorageUploadTask.swift | 3 + 10 files changed, 318 insertions(+), 383 deletions(-) diff --git a/FirebaseStorage/Sources/AsyncAwait.swift b/FirebaseStorage/Sources/AsyncAwait.swift index d38c859d818..564a2378aaa 100644 --- a/FirebaseStorage/Sources/AsyncAwait.swift +++ b/FirebaseStorage/Sources/AsyncAwait.swift @@ -24,8 +24,7 @@ public extension StorageReference { /// - Parameters: /// - size: The maximum size in bytes to download. If the download exceeds this size, /// the task will be cancelled and an error will be thrown. - /// - Throws: - /// - An error if the operation failed, for example if the data exceeded `maxSize`. + /// - Throws: An error if the operation failed, for example if the data exceeded `maxSize`. /// - Returns: Data object. func data(maxSize: Int64) async throws -> Data { return try await withCheckedThrowingContinuation { continuation in @@ -45,8 +44,7 @@ public extension StorageReference { /// about the object being uploaded. /// - onProgress: An optional closure function to return a `Progress` instance while the /// upload proceeds. - /// - Throws: - /// - An error if the operation failed, for example if Storage was unreachable. + /// - Throws: An error if the operation failed, for example if Storage was unreachable. /// - Returns: StorageMetadata with additional information about the object being uploaded. func putDataAsync(_ uploadData: Data, metadata: StorageMetadata? = nil, @@ -83,9 +81,8 @@ public extension StorageReference { /// about the object being uploaded. /// - onProgress: An optional closure function to return a `Progress` instance while the /// upload proceeds. - /// - Throws: - /// - An error if the operation failed, for example if no file was present at the specified - /// `url`. + /// - Throws: An error if the operation failed, for example if no file was present at the + /// specified `url`. /// - Returns: `StorageMetadata` with additional information about the object being uploaded. func putFileAsync(from url: URL, metadata: StorageMetadata? = nil, @@ -119,8 +116,7 @@ public extension StorageReference { /// - fileUrl: A URL representing the system file path of the object to be uploaded. /// - onProgress: An optional closure function to return a `Progress` instance while the /// download proceeds. - /// - Throws: - /// - An error if the operation failed, for example if Storage was unreachable + /// - Throws: An error if the operation failed, for example if Storage was unreachable /// or `fileURL` did not reference a valid path on disk. /// - Returns: A `URL` pointing to the file path of the downloaded file. func writeAsync(toFile fileURL: URL, @@ -157,13 +153,11 @@ public extension StorageReference { /// Only available for projects using Firebase Rules Version 2. /// /// - Parameters: - /// - maxResults The maximum number of results to return in a single page. Must be + /// - maxResults: The maximum number of results to return in a single page. Must be /// greater than 0 and at most 1000. - /// - Throws: - /// - An error if the operation failed, for example if Storage was unreachable + /// - Throws: An error if the operation failed, for example if Storage was unreachable /// or the storage reference referenced an invalid path. - /// - Returns: - /// - A `StorageListResult` containing the contents of the storage reference. + /// - Returns: A `StorageListResult` containing the contents of the storage reference. func list(maxResults: Int64) async throws -> StorageListResult { typealias ListContinuation = CheckedContinuation return try await withCheckedThrowingContinuation { (continuation: ListContinuation) in @@ -182,9 +176,9 @@ public extension StorageReference { /// Only available for projects using Firebase Rules Version 2. /// /// - Parameters: - /// - maxResults The maximum number of results to return in a single page. Must be + /// - maxResults: The maximum number of results to return in a single page. Must be /// greater than 0 and at most 1000. - /// - pageToken A page token from a previous call to list. + /// - pageToken: A page token from a previous call to list. /// - Throws: /// - An error if the operation failed, for example if Storage was unreachable /// or the storage reference referenced an invalid path. diff --git a/FirebaseStorage/Sources/Result.swift b/FirebaseStorage/Sources/Result.swift index 7aee82f439c..d1260fe57f9 100644 --- a/FirebaseStorage/Sources/Result.swift +++ b/FirebaseStorage/Sources/Result.swift @@ -38,6 +38,7 @@ private func getResultCallback(completion: @escaping (Result) -> Vo public extension StorageReference { /// Asynchronously retrieves a long lived download URL with a revokable token. + /// /// This can be used to share the file with others, but can be revoked by a developer /// in the Firebase Console. /// @@ -48,6 +49,7 @@ public extension StorageReference { } /// Asynchronously downloads the object at the `StorageReference` to a `Data` object. + /// /// A `Data` of the provided max size will be allocated, so ensure that the device has enough /// memory to complete. For downloading large files, the `write` API may be a better option. @@ -73,6 +75,7 @@ public extension StorageReference { } /// Resumes a previous `list` call, starting after a pagination token. + /// /// Returns the next set of items (files) and prefixes (folders) under this StorageReference. /// /// "/" is treated as a path delimiter. Firebase Storage does not support unsupported object @@ -82,10 +85,10 @@ public extension StorageReference { /// Only available for projects using Firebase Rules Version 2. /// /// - Parameters: - /// - maxResults The maximum number of results to return in a single page. Must be + /// - maxResults: The maximum number of results to return in a single page. Must be /// greater than 0 and at most 1000. - /// - pageToken A page token from a previous call to list. - /// - completion A completion handler that will be invoked with the next items and + /// - pageToken: A page token from a previous call to list. + /// - completion: A completion handler that will be invoked with the next items and /// prefixes under the current StorageReference. It returns a `Result` enum /// with either the list or an `Error`. func list(maxResults: Int64, @@ -105,9 +108,9 @@ public extension StorageReference { /// Only available for projects using Firebase Rules Version 2. /// /// - Parameters: - /// - maxResults The maximum number of results to return in a single page. Must be + /// - maxResults: The maximum number of results to return in a single page. Must be /// greater than 0 and at most 1000. - /// - completion A completion handler that will be invoked with the next items and + /// - completion: A completion handler that will be invoked with the next items and /// prefixes under the current `StorageReference`. It returns a `Result` enum /// with either the list or an `Error`. func list(maxResults: Int64, @@ -125,7 +128,7 @@ public extension StorageReference { /// Only available for projects using Firebase Rules Version 2. /// /// - Parameters: - /// - completion A completion handler that will be invoked with all items and prefixes + /// - completion: A completion handler that will be invoked with all items and prefixes /// under the current StorageReference. It returns a `Result` enum with either the /// list or an `Error`. func listAll(completion: @escaping (Result) -> Void) { @@ -136,10 +139,10 @@ public extension StorageReference { /// This is not recommended for large files, and one should instead upload a file from disk. /// /// - Parameters: - /// - uploadData The `Data` to upload. - /// - metadata `StorageMetadata` containing additional information (MIME type, etc.) + /// - uploadData: The `Data` to upload. + /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.) /// about the object being uploaded. - /// - completion A completion block that returns a `Result` enum with either the + /// - completion: A completion block that returns a `Result` enum with either the /// object metadata or an `Error`. /// /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage @@ -157,10 +160,10 @@ public extension StorageReference { /// Asynchronously uploads a file to the currently specified `StorageReference`. /// /// - Parameters: - /// - from A URL representing the system file path of the object to be uploaded. - /// - metadata `StorageMetadata` containing additional information (MIME type, etc.) + /// - from: A URL representing the system file path of the object to be uploaded. + /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.) /// about the object being uploaded. - /// - completion A completion block that returns a `Result` enum with either the + /// - completion: A completion block that returns a `Result` enum with either the /// object metadata or an `Error`. /// /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage @@ -178,8 +181,8 @@ public extension StorageReference { /// Updates the metadata associated with an object at the current path. /// /// - Parameters: - /// - metadata A `StorageMetadata` object with the metadata to update. - /// - completion A completion block which returns a `Result` enum with either the + /// - metadata: A `StorageMetadata` object with the metadata to update. + /// - completion: A completion block which returns a `Result` enum with either the /// object metadata or an `Error`. func updateMetadata(_ metadata: StorageMetadata, completion: @escaping (Result) -> Void) { @@ -189,8 +192,8 @@ public extension StorageReference { /// Asynchronously downloads the object at the current path to a specified system filepath. /// /// - Parameters: - /// - toFile A file system URL representing the path the object should be downloaded to. - /// - completion A completion block that fires when the file download completes. The + /// - toFile: A file system URL representing the path the object should be downloaded to. + /// - completion: A completion block that fires when the file download completes. The /// block returns a `Result` enum with either an NSURL pointing to the file /// path of the downloaded file or an `Error`. /// diff --git a/FirebaseStorage/Sources/Storage.swift b/FirebaseStorage/Sources/Storage.swift index 197122d05bd..c4590bd28e7 100644 --- a/FirebaseStorage/Sources/Storage.swift +++ b/FirebaseStorage/Sources/Storage.swift @@ -26,61 +26,53 @@ import FirebaseCore // Avoids exposing internal FirebaseCore APIs to Swift users. @_implementationOnly import FirebaseCoreExtension -/** - * Firebase Storage is a service that supports uploading and downloading binary objects, - * such as images, videos, and other files to Google Cloud Storage. Instances of `Storage` - * are not thread-safe, but can be accessed from any thread. - * - * If you call `Storage.storage()`, the instance will initialize with the default `FirebaseApp`, - * `FirebaseApp.app()`, and the storage location will come from the provided - * `GoogleService-Info.plist`. - * - * If you provide a custom instance of `FirebaseApp`, - * the storage location will be specified via the `FirebaseOptions.storageBucket` property. - */ +/// Firebase Storage is a service that supports uploading and downloading binary objects, +/// such as images, videos, and other files to Google Cloud Storage. Instances of `Storage` +/// are not thread-safe, but can be accessed from any thread. +/// +/// If you call `Storage.storage()`, the instance will initialize with the default `FirebaseApp`, +/// `FirebaseApp.app()`, and the storage location will come from the provided +/// `GoogleService-Info.plist`. +/// +/// If you provide a custom instance of `FirebaseApp`, +/// the storage location will be specified via the `FirebaseOptions.storageBucket` property. @objc(FIRStorage) open class Storage: NSObject { // MARK: - Public APIs - /** - * The default `Storage` instance. - * - Returns: An instance of `Storage`, configured with the default `FirebaseApp`. - */ + /// The default `Storage` instance. + /// - Returns: An instance of `Storage`, configured with the default `FirebaseApp`. @objc(storage) open class func storage() -> Storage { return storage(app: FirebaseApp.app()!) } - /** - * A method used to create `Storage` instances initialized with a custom storage bucket URL. - * Any `StorageReferences` generated from this instance of `Storage` will reference files - * and directories within the specified bucket. - * - Parameter url The `gs://` URL to your Firebase Storage bucket. - * - Returns: A `Storage` instance, configured with the custom storage bucket. - */ + /// A method used to create `Storage` instances initialized with a custom storage bucket URL. + /// + /// Any `StorageReferences` generated from this instance of `Storage` will reference files + /// and directories within the specified bucket. + /// - Parameter url: The `gs://` URL to your Firebase Storage bucket. + /// - Returns: A `Storage` instance, configured with the custom storage bucket. @objc(storageWithURL:) open class func storage(url: String) -> Storage { return storage(app: FirebaseApp.app()!, url: url) } - /** - * Creates an instance of `Storage`, configured with a custom `FirebaseApp`. `StorageReference`s - * generated from a resulting instance will reference files in the Firebase project - * associated with custom `FirebaseApp`. - * - Parameter app The custom `FirebaseApp` used for initialization. - * - Returns: A `Storage` instance, configured with the custom `FirebaseApp`. - */ + /// Creates an instance of `Storage`, configured with a custom `FirebaseApp`. `StorageReference`s + /// generated from a resulting instance will reference files in the Firebase project + /// associated with custom `FirebaseApp`. + /// - Parameter app: The custom `FirebaseApp` used for initialization. + /// - Returns: A `Storage` instance, configured with the custom `FirebaseApp`. @objc(storageForApp:) open class func storage(app: FirebaseApp) -> Storage { let provider = ComponentType.instance(for: StorageProvider.self, in: app.container) return provider.storage(for: Storage.bucket(for: app)) } - /** - * Creates an instance of `Storage`, configured with a custom `FirebaseApp` and a custom storage - * bucket URL. - * - Parameters: - * - app: The custom `FirebaseApp` used for initialization. - * - url: The `gs://` url to your Firebase Storage bucket. - * - Returns: the `Storage` instance, configured with the custom `FirebaseApp` and storage bucket URL. - */ + /// Creates an instance of `Storage`, configured with a custom `FirebaseApp` and a custom storage + /// bucket URL. + /// - Parameters: + /// - app: The custom `FirebaseApp` used for initialization. + /// - url: The `gs://` url to your Firebase Storage bucket. + /// - Returns: The `Storage` instance, configured with the custom `FirebaseApp` and storage bucket + /// URL. @objc(storageForApp:URL:) open class func storage(app: FirebaseApp, url: String) -> Storage { let provider = ComponentType.instance(for: StorageProvider.self, @@ -88,50 +80,40 @@ import FirebaseCore return provider.storage(for: Storage.bucket(for: app, urlString: url)) } - /** - * The `FirebaseApp` associated with this Storage instance. - */ + /// The `FirebaseApp` associated with this Storage instance. @objc public let app: FirebaseApp - /** - * The maximum time in seconds to retry an upload if a failure occurs. - * Defaults to 10 minutes (600 seconds). - */ + /// The maximum time in seconds to retry an upload if a failure occurs. + /// Defaults to 10 minutes (600 seconds). @objc public var maxUploadRetryTime: TimeInterval { didSet { maxUploadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxUploadRetryTime) } } - /** - * The maximum time in seconds to retry a download if a failure occurs. - * Defaults to 10 minutes (600 seconds). - */ + /// The maximum time in seconds to retry a download if a failure occurs. + /// Defaults to 10 minutes (600 seconds). @objc public var maxDownloadRetryTime: TimeInterval { didSet { maxDownloadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxDownloadRetryTime) } } - /** - * The maximum time in seconds to retry operations other than upload and download if a failure occurs. - * Defaults to 2 minutes (120 seconds). - */ + /// The maximum time in seconds to retry operations other than upload and download if a failure + /// occurs. + /// Defaults to 2 minutes (120 seconds). @objc public var maxOperationRetryTime: TimeInterval { didSet { maxOperationRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxOperationRetryTime) } } - /** - * Specify the maximum upload chunk size. Values less than 256K (262144) will be rounded up to 256K. Values - * above 256K will be rounded down to the nearest 256K multiple. The default is no maximum. - */ + /// Specify the maximum upload chunk size. Values less than 256K (262144) will be rounded up to + /// 256K. Values + /// above 256K will be rounded down to the nearest 256K multiple. The default is no maximum. @objc public var uploadChunkSizeBytes: Int64 = .max - /** - * A `DispatchQueue` that all developer callbacks are fired on. Defaults to the main queue. - */ + /// A `DispatchQueue` that all developer callbacks are fired on. Defaults to the main queue. @objc public var callbackQueue: DispatchQueue { get { ensureConfigured() @@ -146,26 +128,25 @@ import FirebaseCore } } - /** - * Creates a `StorageReference` initialized at the root Firebase Storage location. - * - Returns: An instance of `StorageReference` referencing the root of the storage bucket. - */ + /// Creates a `StorageReference` initialized at the root Firebase Storage location. + /// - Returns: An instance of `StorageReference` referencing the root of the storage bucket. @objc open func reference() -> StorageReference { ensureConfigured() let path = StoragePath(with: storageBucket) return StorageReference(storage: self, path: path) } - /** - * Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a - * Firebase Storage location. For example, you can pass in an `https://` download URL retrieved from - * `StorageReference.downloadURL(completion:)` or the `gs://` URL from - * `StorageReference.description`. - * - Parameter url A gs:// or https:// URL to initialize the reference with. - * - Returns: An instance of StorageReference at the given child path. - * - Throws: Throws a fatal error if `url` is not associated with the `FirebaseApp` used to initialize - * this Storage instance. - */ + /// Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a + /// Firebase Storage location. + /// + /// For example, you can pass in an `https://` download URL retrieved from + /// `StorageReference.downloadURL(completion:)` or the `gs://` URL from + /// `StorageReference.description`. + /// - Parameter url: A gs:// or https:// URL to initialize the reference with. + /// - Returns: An instance of StorageReference at the given child path. + /// - Throws: Throws a fatal error if `url` is not associated with the `FirebaseApp` used to + /// initialize + /// this Storage instance. @objc open func reference(forURL url: String) -> StorageReference { ensureConfigured() do { @@ -188,16 +169,16 @@ import FirebaseCore } } - /** - * Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a - * Firebase Storage location. For example, you can pass in an `https://` download URL retrieved from - * `StorageReference.downloadURL(completion:)` or the `gs://` URL from - * `StorageReference.description`. - * - Parameter url A gs:// or https:// URL to initialize the reference with. - * - Returns: An instance of StorageReference at the given child path. - * - Throws: Throws an Error if `url` is not associated with the `FirebaseApp` used to initialize - * this Storage instance. - */ + /// Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a + /// Firebase Storage location. + /// + /// For example, you can pass in an `https://` download URL retrieved from + /// `StorageReference.downloadURL(completion:)` or the `gs://` URL from + /// `StorageReference.description`. + /// - Parameter url: A gs:// or https:// URL to initialize the reference with. + /// - Returns: An instance of StorageReference at the given child path. + /// - Throws: Throws an Error if `url` is not associated with the `FirebaseApp` used to initialize + /// this Storage instance. open func reference(for url: URL) throws -> StorageReference { ensureConfigured() var path: StoragePath @@ -222,20 +203,20 @@ import FirebaseCore return StorageReference(storage: self, path: path) } - /** - * Creates a `StorageReference` initialized at a location specified by the `path` parameter. - * - Parameter path A relative path from the root of the storage bucket, - * for instance @"path/to/object". - * - Returns: An instance of `StorageReference` pointing to the given path. - */ + /// Creates a `StorageReference` initialized at a location specified by the `path` parameter. + /// - Parameter path: A relative path from the root of the storage bucket, + /// for instance @"path/to/object". + /// - Returns: An instance of `StorageReference` pointing to the given path. @objc(referenceWithPath:) open func reference(withPath path: String) -> StorageReference { return reference().child(path) } - /** - * Configures the Storage SDK to use an emulated backend instead of the default remote backend. - * This method should be called before invoking any other methods on a new instance of `Storage`. - */ + /// Configures the Storage SDK to use an emulated backend instead of the default remote backend. + /// + /// This method should be called before invoking any other methods on a new instance of `Storage`. + /// - Parameter host: A string specifying the host. + /// - Parameter port: The port specified as an `Int`. + @objc open func useEmulator(withHost host: String, port: Int) { guard host.count > 0 else { fatalError("Invalid host argument: Cannot connect to empty host.") @@ -364,14 +345,13 @@ import FirebaseCore var maxOperationRetryInterval: TimeInterval var maxUploadRetryInterval: TimeInterval - /** - * Performs a crude translation of the user provided timeouts to the retry intervals that - * GTMSessionFetcher accepts. GTMSessionFetcher times out operations if the time between individual - * retry attempts exceed a certain threshold, while our API contract looks at the total observed - * time of the operation (i.e. the sum of all retries). - * @param retryTime A timeout that caps the sum of all retry attempts - * @return A timeout that caps the timeout of the last retry attempt - */ + /// Performs a crude translation of the user provided timeouts to the retry intervals that + /// GTMSessionFetcher accepts. GTMSessionFetcher times out operations if the time between + /// individual + /// retry attempts exceed a certain threshold, while our API contract looks at the total observed + /// time of the operation (i.e. the sum of all retries). + /// @param retryTime A timeout that caps the sum of all retry attempts + /// @return A timeout that caps the timeout of the last retry attempt static func computeRetryInterval(fromRetryTime retryTime: TimeInterval) -> TimeInterval { // GTMSessionFetcher's retry starts at 1 second and then doubles every time. We use this // information to compute a best-effort estimate of what to translate the user provided retry @@ -387,9 +367,7 @@ import FirebaseCore return lastInterval } - /** - * Configures the storage instance. Freezes the host setting. - */ + /// Configures the storage instance. Freezes the host setting. private func ensureConfigured() { guard fetcherService == nil else { return diff --git a/FirebaseStorage/Sources/StorageDownloadTask.swift b/FirebaseStorage/Sources/StorageDownloadTask.swift index b37eedc7dfe..62217182c0e 100644 --- a/FirebaseStorage/Sources/StorageDownloadTask.swift +++ b/FirebaseStorage/Sources/StorageDownloadTask.swift @@ -22,10 +22,13 @@ import Foundation /** * `StorageDownloadTask` implements resumable downloads from an object in Firebase Storage. + * * Downloads can be returned on completion with a completion handler, and can be monitored * by attaching observers, or controlled by calling `pause()`, `resume()`, * or `cancel()`. + * * Downloads can currently be returned as `Data` in memory, or as a `URL` to a file on disk. + * * Downloads are performed on a background queue, and callbacks are raised on the developer * specified `callbackQueue` in Storage, or the main queue if left unspecified. */ diff --git a/FirebaseStorage/Sources/StorageListResult.swift b/FirebaseStorage/Sources/StorageListResult.swift index 872db7140f9..b32f62bf0d8 100644 --- a/FirebaseStorage/Sources/StorageListResult.swift +++ b/FirebaseStorage/Sources/StorageListResult.swift @@ -18,23 +18,17 @@ import Foundation @objc(FIRStorageListResult) open class StorageListResult: NSObject { /** * The prefixes (folders) returned by a `list()` operation. - * - * - Returns: A list of prefixes (folders). */ @objc public let prefixes: [StorageReference] /** * The objects (files) returned by a `list()` operation. - * - * - Returns: A page token if more results are available. */ @objc public let items: [StorageReference] /** - * Returns a token that can be used to resume a previous `list()` operation. `nil` + * A token that can be used to resume a previous `list()` operation. `nil` * indicates that there are no more results. - * - * - Returns: A page token if more results are available. */ @objc public let pageToken: String? diff --git a/FirebaseStorage/Sources/StorageMetadata.swift b/FirebaseStorage/Sources/StorageMetadata.swift index 770aeada8f1..0f45ca345a5 100644 --- a/FirebaseStorage/Sources/StorageMetadata.swift +++ b/FirebaseStorage/Sources/StorageMetadata.swift @@ -15,11 +15,12 @@ import Foundation /** - * Class which represents the metadata on an object in Firebase Storage. This metadata is + * Class which represents the metadata on an object in Firebase Storage. + * + * This metadata is * returned on successful operations, and can be used to retrieve download URLs, content types, - * and a Storage reference to the object in question. Full documentation can be found at the GCS - * Objects#resource docs. - * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource + * and a Storage reference to the object in question. Full documentation can be found in the + * [GCS documentation](https://cloud.google.com/storage/docs/json_api/v1/objects#resource) */ @objc(FIRStorageMetadata) open class StorageMetadata: NSObject { // MARK: - Public APIs @@ -145,6 +146,10 @@ import Foundation // MARK: - Public Initializers + /** + * Creates an empty instance of StorageMetadata. + * @return An empty instance of StorageMetadata. + */ @objc override public convenience init() { self.init(dictionary: [:]) } diff --git a/FirebaseStorage/Sources/StorageObservableTask.swift b/FirebaseStorage/Sources/StorageObservableTask.swift index 33a9667f420..f302fe152be 100644 --- a/FirebaseStorage/Sources/StorageObservableTask.swift +++ b/FirebaseStorage/Sources/StorageObservableTask.swift @@ -23,6 +23,7 @@ import Foundation /** * An extended `StorageTask` providing observable semantics that can be used for responding to changes * in task state. + * * Observers produce a `StorageHandle`, which is used to keep track of and remove specific * observers at a later date. */ @@ -80,7 +81,7 @@ import Foundation /** * Removes the single observer with the provided handle. - * - Parameter handle The handle of the task to remove. + * - Parameter handle: The handle of the task to remove. */ @objc(removeObserverWithHandle:) open func removeObserver(withHandle handle: String) { if let status = handleToStatusMap[handle] { @@ -93,7 +94,7 @@ import Foundation /** * Removes all observers for a single status. - * - Parameter status A `StorageTaskStatus` to remove all listeners for. + * - Parameter status: A `StorageTaskStatus` to remove all listeners for. */ @objc(removeAllObserversForStatus:) open func removeAllObservers(for status: StorageTaskStatus) { diff --git a/FirebaseStorage/Sources/StorageReference.swift b/FirebaseStorage/Sources/StorageReference.swift index d81377d5c7d..98eb595c733 100644 --- a/FirebaseStorage/Sources/StorageReference.swift +++ b/FirebaseStorage/Sources/StorageReference.swift @@ -14,61 +14,49 @@ import Foundation -/** - * `StorageReference` represents a reference to a Google Cloud Storage object. Developers can - * upload and download objects, as well as get/set object metadata, and delete an object at the - * path. See the Cloud docs for more details: https://cloud.google.com/storage/ - */ - +/// `StorageReference` represents a reference to a Google Cloud Storage object. Developers can +/// upload and download objects, as well as get/set object metadata, and delete an object at the +/// path. See the [Cloud docs](https://cloud.google.com/storage/) for more details. @objc(FIRStorageReference) open class StorageReference: NSObject { // MARK: - Public APIs - /** - * The `Storage` service object which created this reference. - */ + /// The `Storage` service object which created this reference. @objc public let storage: Storage - /** - * The name of the Google Cloud Storage bucket associated with this reference. - * For example, in `gs://bucket/path/to/object.txt`, the bucket would be 'bucket'. - */ + /// The name of the Google Cloud Storage bucket associated with this reference. + /// For example, in `gs://bucket/path/to/object.txt`, the bucket would be 'bucket'. @objc public var bucket: String { return path.bucket } - /** - * The full path to this object, not including the Google Cloud Storage bucket. - * In `gs://bucket/path/to/object.txt`, the full path would be: `path/to/object.txt` - */ + /// The full path to this object, not including the Google Cloud Storage bucket. + /// In `gs://bucket/path/to/object.txt`, the full path would be: `path/to/object.txt`. @objc public var fullPath: String { return path.object ?? "" } - /** - * The short name of the object associated with this reference. - * In `gs://bucket/path/to/object.txt`, the name of the object would be `object.txt`. - */ + /// The short name of the object associated with this reference. + /// + /// In `gs://bucket/path/to/object.txt`, the name of the object would be `object.txt`. @objc public var name: String { return (path.object as? NSString)?.lastPathComponent ?? "" } - /** - * Creates a new `StorageReference` pointing to the root object. - * - Returns: A new `StorageReference` pointing to the root object. - */ + /// Creates a new `StorageReference` pointing to the root object. + /// - Returns: A new `StorageReference` pointing to the root object. @objc open func root() -> StorageReference { return StorageReference(storage: storage, path: path.root()) } - /** - * Creates a new `StorageReference` pointing to the parent of the current reference - * or `nil` if this instance references the root location. - * For example: - * path = foo/bar/baz parent = foo/bar - * path = foo parent = (root) - * path = (root) parent = nil - * - Returns: A new `StorageReference` pointing to the parent of the current reference. - */ + /// Creates a new `StorageReference` pointing to the parent of the current reference + /// or `nil` if this instance references the root location. + /// ``` + /// For example: + /// path = foo/bar/baz parent = foo/bar + /// path = foo parent = (root) + /// path = (root) parent = nil + /// ``` + /// - Returns: A new `StorageReference` pointing to the parent of the current reference. @objc open func parent() -> StorageReference? { guard let parentPath = path.parent() else { return nil @@ -76,61 +64,59 @@ import Foundation return StorageReference(storage: storage, path: parentPath) } - /** - * Creates a new `StorageReference` pointing to a child object of the current reference. - * path = foo child = bar newPath = foo/bar - * path = foo/bar child = baz ntask.impl.snapshotwPath = foo/bar/baz - * All leading and trailing slashes will be removed, and consecutive slashes will be - * compressed to single slashes. For example: - * child = /foo/bar newPath = foo/bar - * child = foo/bar/ newPath = foo/bar - * child = foo///bar newPath = foo/bar - * - Parameter path The path to append to the current path. - * - Returns: A new `StorageReference` pointing to a child location of the current reference. - */ + /// Creates a new `StorageReference` pointing to a child object of the current reference. + /// ``` + /// path = foo child = bar newPath = foo/bar + /// path = foo/bar child = baz ntask.impl.snapshotwPath = foo/bar/baz + /// All leading and trailing slashes will be removed, and consecutive slashes will be + /// compressed to single slashes. For example: + /// child = /foo/bar newPath = foo/bar + /// child = foo/bar/ newPath = foo/bar + /// child = foo///bar newPath = foo/bar + /// ``` + /// + /// - Parameter path: The path to append to the current path. + /// - Returns: A new `StorageReference` pointing to a child location of the current reference. @objc(child:) open func child(_ path: String) -> StorageReference { return StorageReference(storage: storage, path: self.path.child(path)) } // MARK: - Uploads - /** - * Asynchronously uploads data to the currently specified `StorageReference`, - * without additional metadata. - * This is not recommended for large files, and one should instead upload a file from disk. - * - Parameters: - * - uploadData: The data to upload. - * - metadata: `StorageMetadata` containing additional information (MIME type, etc.) - * about the object being uploaded. - * - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the upload. - */ + /// Asynchronously uploads data to the currently specified `StorageReference`, + /// without additional metadata. + /// This is not recommended for large files, and one should instead upload a file from disk. + /// - Parameters: + /// - uploadData: The data to upload. + /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.) + /// about the object being uploaded. + /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the + /// upload. @objc(putData:metadata:) @discardableResult open func putData(_ uploadData: Data, metadata: StorageMetadata? = nil) -> StorageUploadTask { return putData(uploadData, metadata: metadata, completion: nil) } - /** - * Asynchronously uploads data to the currently specified `StorageReference`. - * This is not recommended for large files, and one should instead upload a file from disk. - * - Parameter uploadData The data to upload. - * - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the upload. - */ + /// Asynchronously uploads data to the currently specified `StorageReference`. + /// This is not recommended for large files, and one should instead upload a file from disk. + /// - Parameter uploadData The data to upload. + /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the + /// upload. @objc(putData:) @discardableResult open func __putData(_ uploadData: Data) -> StorageUploadTask { return putData(uploadData, metadata: nil, completion: nil) } - /** - * Asynchronously uploads data to the currently specified `StorageReference`. - * This is not recommended for large files, and one should instead upload a file from disk. - * - Parameters: - * - uploadData: The data to upload. - * - metadata: `StorageMetadata` containing additional information (MIME type, etc.) - * about the object being uploaded. - * - completion: A closure that either returns the object metadata on success, - * or an error on failure. - * - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the upload. - */ + /// Asynchronously uploads data to the currently specified `StorageReference`. + /// This is not recommended for large files, and one should instead upload a file from disk. + /// - Parameters: + /// - uploadData: The data to upload. + /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.) + /// about the object being uploaded. + /// - completion: A closure that either returns the object metadata on success, + /// or an error on failure. + /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the + /// upload. @objc(putData:metadata:completion:) @discardableResult open func putData(_ uploadData: Data, metadata: StorageMetadata? = nil, @@ -149,42 +135,38 @@ import Foundation return task } - /** - * Asynchronously uploads a file to the currently specified `StorageReference`. - * `putData` should be used instead of `putFile` in Extensions. - * - Parameters: - * - fileURL: A URL representing the system file path of the object to be uploaded. - * - metadata: `StorageMetadata` containing additional information (MIME type, etc.) - * about the object being uploaded. - * - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the upload. - */ + /// Asynchronously uploads a file to the currently specified `StorageReference`. + /// `putData` should be used instead of `putFile` in Extensions. + /// - Parameters: + /// - fileURL: A URL representing the system file path of the object to be uploaded. + /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.) + /// about the object being uploaded. + /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the + /// upload. @objc(putFile:metadata:) @discardableResult open func putFile(from fileURL: URL, metadata: StorageMetadata? = nil) -> StorageUploadTask { return putFile(from: fileURL, metadata: metadata, completion: nil) } - /** - * Asynchronously uploads a file to the currently specified `StorageReference`, - * without additional metadata. - * `putData` should be used instead of `putFile` in Extensions. - * @param fileURL A URL representing the system file path of the object to be uploaded. - * @return An instance of StorageUploadTask, which can be used to monitor or manage the upload. - */ + /// Asynchronously uploads a file to the currently specified `StorageReference`, + /// without additional metadata. + /// `putData` should be used instead of `putFile` in Extensions. + /// @param fileURL A URL representing the system file path of the object to be uploaded. + /// @return An instance of StorageUploadTask, which can be used to monitor or manage the upload. @objc(putFile:) @discardableResult open func __putFile(from fileURL: URL) -> StorageUploadTask { return putFile(from: fileURL, metadata: nil, completion: nil) } - /** - * Asynchronously uploads a file to the currently specified `StorageReference`. - * `putData` should be used instead of `putFile` in Extensions. - * - Parameters: - * - fileURL: A URL representing the system file path of the object to be uploaded. - * - metadata: `StorageMetadata` containing additional information (MIME type, etc.) - * about the object being uploaded. - * - completion: A completion block that either returns the object metadata on success, - * or an error on failure. - * - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the upload. - */ + /// Asynchronously uploads a file to the currently specified `StorageReference`. + /// `putData` should be used instead of `putFile` in Extensions. + /// - Parameters: + /// - fileURL: A URL representing the system file path of the object to be uploaded. + /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.) + /// about the object being uploaded. + /// - completion: A completion block that either returns the object metadata on success, + /// or an error on failure. + /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the + /// upload. @objc(putFile:metadata:completion:) @discardableResult open func putFile(from fileURL: URL, metadata: StorageMetadata? = nil, @@ -205,17 +187,17 @@ import Foundation // MARK: - Downloads - /** - * Asynchronously downloads the object at the `StorageReference` to a `Data` instance in memory. - * A `Data` buffer of the provided max size will be allocated, so ensure that the device has enough free - * memory to complete the download. For downloading large files, `write(toFile:)` may be a better option. - * - Parameters: - * - maxSize: The maximum size in bytes to download. If the download exceeds this size, - * the task will be cancelled and an error will be returned. - * - completion: A completion block that either returns the object data on success, - * or an error on failure. - * - Returns: An `StorageDownloadTask` that can be used to monitor or manage the download. - */ + /// Asynchronously downloads the object at the `StorageReference` to a `Data` instance in memory. + /// A `Data` buffer of the provided max size will be allocated, so ensure that the device has + /// enough free + /// memory to complete the download. For downloading large files, `write(toFile:)` may be a better + /// option. + /// - Parameters: + /// - maxSize: The maximum size in bytes to download. If the download exceeds this size, + /// the task will be cancelled and an error will be returned. + /// - completion: A completion block that either returns the object data on success, + /// or an error on failure. + /// - Returns: A `StorageDownloadTask` that can be used to monitor or manage the download. @objc(dataWithMaxSize:completion:) @discardableResult open func getData(maxSize: Int64, completion: @escaping ((_: Data?, _: Error?) -> Void)) -> StorageDownloadTask { @@ -253,13 +235,11 @@ import Foundation return task } - /** - * Asynchronously retrieves a long lived download URL with a revokable token. - * This can be used to share the file with others, but can be revoked by a developer - * in the Firebase Console. - * - Parameter completion A completion block that either returns the URL on success, - * or an error on failure. - */ + /// Asynchronously retrieves a long lived download URL with a revokable token. + /// This can be used to share the file with others, but can be revoked by a developer + /// in the Firebase Console. + /// - Parameter completion: A completion block that either returns the URL on success, + /// or an error on failure. @objc(downloadURLWithCompletion:) open func downloadURL(completion: @escaping ((_: URL?, _: Error?) -> Void)) { let fetcherService = storage.fetcherServiceForApp @@ -270,13 +250,11 @@ import Foundation task.enqueue() } - /** - * Asynchronously retrieves a long lived download URL with a revokable token. - * This can be used to share the file with others, but can be revoked by a developer - * in the Firebase Console. - * - Throws: An error if the download URL could not be retrieved. - * - Returns: The URL on success. - */ + /// Asynchronously retrieves a long lived download URL with a revokable token. + /// This can be used to share the file with others, but can be revoked by a developer + /// in the Firebase Console. + /// - Throws: An error if the download URL could not be retrieved. + /// - Returns: The URL on success. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open func downloadURL() async throws -> URL { return try await withCheckedThrowingContinuation { continuation in @@ -286,25 +264,22 @@ import Foundation } } - /** - * Asynchronously downloads the object at the current path to a specified system filepath. - * - Parameter fileURL A file system URL representing the path the object should be downloaded to. - * - Returns An `StorageDownloadTask` that can be used to monitor or manage the download. - */ + /// Asynchronously downloads the object at the current path to a specified system filepath. + /// - Parameter fileURL: A file system URL representing the path the object should be downloaded + /// to. + /// - Returns A `StorageDownloadTask` that can be used to monitor or manage the download. @objc(writeToFile:) @discardableResult open func write(toFile fileURL: URL) -> StorageDownloadTask { return write(toFile: fileURL, completion: nil) } - /** - * Asynchronously downloads the object at the current path to a specified system filepath. - * - Parameters: - * - fileURL: A file system URL representing the path the object should be downloaded to. - * - completion: A closure that fires when the file download completes, passed either - * a URL pointing to the file path of the downloaded file on success, - * or an error on failure. - * - Returns: A `StorageDownloadTask` that can be used to monitor or manage the download. - */ + /// Asynchronously downloads the object at the current path to a specified system filepath. + /// - Parameters: + /// - fileURL: A file system URL representing the path the object should be downloaded to. + /// - completion: A closure that fires when the file download completes, passed either + /// a URL pointing to the file path of the downloaded file on success, + /// or an error on failure. + /// - Returns: A `StorageDownloadTask` that can be used to monitor or manage the download. @objc(writeToFile:completion:) @discardableResult open func write(toFile fileURL: URL, completion: ((_: URL?, _: Error?) -> Void)?) -> StorageDownloadTask { @@ -337,18 +312,17 @@ import Foundation // MARK: - List Support - /** - * Lists all items (files) and prefixes (folders) under this `StorageReference`. - * - * This is a helper method for calling `list()` repeatedly until there are no more results. - * Consistency of the result is not guaranteed if objects are inserted or removed while this - * operation is executing. All results are buffered in memory. - * - * `listAll(completion:)` is only available for projects using Firebase Rules Version 2. - * - * - Parameter completion A completion handler that will be invoked with all items and prefixes under - * the current `StorageReference`. - */ + /// Lists all items (files) and prefixes (folders) under this `StorageReference`. + /// + /// This is a helper method for calling `list()` repeatedly until there are no more results. + /// + /// Consistency of the result is not guaranteed if objects are inserted or removed while this + /// operation is executing. All results are buffered in memory. + /// + /// `listAll(completion:)` is only available for projects using Firebase Rules Version 2. + /// - Parameter completion: A completion handler that will be invoked with all items and prefixes + /// under + /// the current `StorageReference`. @objc(listAllWithCompletion:) open func listAll(completion: @escaping ((_: StorageListResult?, _: Error?) -> Void)) { let fetcherService = storage.fetcherServiceForApp @@ -396,18 +370,13 @@ import Foundation task.enqueue() } - /** - * Lists all items (files) and prefixes (folders) under this StorageReference. - * - * This is a helper method for calling list() repeatedly until there are no more results. - * Consistency of the result is not guaranteed if objects are inserted or removed while this - * operation is executing. All results are buffered in memory. - * - * `listAll()` is only available for projects using Firebase Rules Version 2. - * - * - Throws: An error if the list operation failed. - * - Returns: All items and prefixes under the current `StorageReference`. - */ + /// Lists all items (files) and prefixes (folders) under this StorageReference. + /// This is a helper method for calling list() repeatedly until there are no more results. + /// Consistency of the result is not guaranteed if objects are inserted or removed while this + /// operation is executing. All results are buffered in memory. + /// `listAll()` is only available for projects using Firebase Rules Version 2. + /// - Throws: An error if the list operation failed. + /// - Returns: All items and prefixes under the current `StorageReference`. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open func listAll() async throws -> StorageListResult { return try await withCheckedThrowingContinuation { continuation in @@ -417,21 +386,18 @@ import Foundation } } - /** - * List up to `maxResults` items (files) and prefixes (folders) under this StorageReference. - * - * "/" is treated as a path delimiter. Firebase Storage does not support unsupported object - * paths that end with "/" or contain two consecutive "/"s. All invalid objects in GCS will be - * filtered. - * - * `list(maxResults:completion:)` is only available for projects using Firebase Rules Version 2. - * - * - Parameters: - * - maxResults: The maximum number of results to return in a single page. Must be greater - * than 0 and at most 1000. - * - completion: A completion handler that will be invoked with up to `maxResults` items and - * prefixes under the current `StorageReference`. - */ + /// List up to `maxResults` items (files) and prefixes (folders) under this StorageReference. + /// + /// "/" is treated as a path delimiter. Firebase Storage does not support unsupported object + /// paths that end with "/" or contain two consecutive "/"s. All invalid objects in GCS will be + /// filtered. + /// + /// Only available for projects using Firebase Rules Version 2. + /// - Parameters: + /// - maxResults: The maximum number of results to return in a single page. Must be + /// greater than 0 and at most 1000. + /// - completion: A completion handler that will be invoked with up to `maxResults` items and + /// prefixes under the current `StorageReference`. @objc(listWithMaxResults:completion:) open func list(maxResults: Int64, completion: @escaping ((_: StorageListResult?, _: Error?) -> Void)) { @@ -453,24 +419,22 @@ import Foundation } } - /** - * Resumes a previous call to `list(maxResults:completion:)`, starting after a pagination token. - * Returns the next set of items (files) and prefixes (folders) under this `StorageReference`. - * - * "/" is treated as a path delimiter. Storage does not support unsupported object - * paths that end with "/" or contain two consecutive "/"s. All invalid objects in GCS will be - * filtered. - * - * `list(maxResults:pageToken:completion:)`is only available for projects using Firebase Rules - * Version 2. - * - * - Parameters: - * - maxResults: The maximum number of results to return in a single page. Must be greater - * than 0 and at most 1000. - * - pageToken: A page token from a previous call to list. - * - completion: A completion handler that will be invoked with the next items and prefixes - * under the current StorageReference. - */ + /// Resumes a previous call to `list(maxResults:completion:)`, starting after a pagination token. + /// + /// Returns the next set of items (files) and prefixes (folders) under this `StorageReference`. + /// + /// "/" is treated as a path delimiter. Storage does not support unsupported object + /// paths that end with "/" or contain two consecutive "/"s. All invalid objects in GCS will be + /// filtered. + /// + /// `list(maxResults:pageToken:completion:)`is only available for projects using Firebase Rules + /// Version 2. + /// - Parameters: + /// - maxResults: The maximum number of results to return in a single page. Must be greater + /// than 0 and at most 1000. + /// - pageToken: A page token from a previous call to list. + /// - completion: A completion handler that will be invoked with the next items and prefixes + /// under the current StorageReference. @objc(listWithMaxResults:pageToken:completion:) open func list(maxResults: Int64, pageToken: String, @@ -495,11 +459,9 @@ import Foundation // MARK: - Metadata Operations - /** - * Retrieves metadata associated with an object at the current path. - * - Parameter completion A completion block which returns the object metadata on success, - * or an error on failure. - */ + /// Retrieves metadata associated with an object at the current path. + /// - Parameter completion: A completion block which returns the object metadata on success, + /// or an error on failure. @objc(metadataWithCompletion:) open func getMetadata(completion: @escaping ((_: StorageMetadata?, _: Error?) -> Void)) { let fetcherService = storage.fetcherServiceForApp @@ -510,11 +472,9 @@ import Foundation task.enqueue() } - /** - * Retrieves metadata associated with an object at the current path. - * - Throws: An error if the object metadata could not be retrieved. - * - Returns: The object metadata on success. - */ + /// Retrieves metadata associated with an object at the current path. + /// - Throws: An error if the object metadata could not be retrieved. + /// - Returns: The object metadata on success. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open func getMetadata() async throws -> StorageMetadata { return try await withCheckedThrowingContinuation { continuation in @@ -524,13 +484,11 @@ import Foundation } } - /** - * Updates the metadata associated with an object at the current path. - * - Parameters: - * - metadata: A `StorageMetadata` object with the metadata to update. - * - completion: A completion block which returns the `StorageMetadata` on success, - * or an error on failure. - */ + /// Updates the metadata associated with an object at the current path. + /// - Parameters: + /// - metadata: A `StorageMetadata` object with the metadata to update. + /// - completion: A completion block which returns the `StorageMetadata` on success, + /// or an error on failure. @objc(updateMetadata:completion:) open func updateMetadata(_ metadata: StorageMetadata, completion: ((_: StorageMetadata?, _: Error?) -> Void)?) { @@ -543,12 +501,10 @@ import Foundation task.enqueue() } - /** - * Updates the metadata associated with an object at the current path. - * - Parameter metadata A `StorageMetadata` object with the metadata to update. - * - Throws: An error if the metadata update operation failed. - * - Returns: The object metadata on success. - */ + /// Updates the metadata associated with an object at the current path. + /// - Parameter metadata: A `StorageMetadata` object with the metadata to update. + /// - Throws: An error if the metadata update operation failed. + /// - Returns: The object metadata on success. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open func updateMetadata(_ metadata: StorageMetadata) async throws -> StorageMetadata { return try await withCheckedThrowingContinuation { continuation in @@ -560,10 +516,8 @@ import Foundation // MARK: - Delete - /** - * Deletes the object at the current path. - * - Parameter completion A completion block which returns a nonnull error on failure. - */ + /// Deletes the object at the current path. + /// - Parameter completion: A completion block which returns a nonnull error on failure. @objc(deleteWithCompletion:) open func delete(completion: ((_: Error?) -> Void)?) { let fetcherService = storage.fetcherServiceForApp @@ -574,10 +528,8 @@ import Foundation task.enqueue() } - /** - * Deletes the object at the current path. - * - Throws: An error if the delete operation failed. - */ + /// Deletes the object at the current path. + /// - Throws: An error if the delete operation failed. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open func delete() async throws { return try await withCheckedThrowingContinuation { continuation in @@ -593,10 +545,12 @@ import Foundation // MARK: - NSObject overrides + /// NSObject override @objc override open func copy() -> Any { return StorageReference(storage: storage, path: path) } + /// NSObject override @objc override open func isEqual(_ object: Any?) -> Bool { guard let ref = object as? StorageReference else { return false @@ -604,19 +558,19 @@ import Foundation return storage == ref.storage && path == ref.path } + /// NSObject override @objc override public var hash: Int { return storage.hash ^ path.bucket.hashValue } + /// NSObject override @objc override public var description: String { return "gs://\(path.bucket)/\(path.object ?? "")" } // MARK: - Internal APIs - /** - * The current path which points to an object in the Google Cloud Storage bucket. - */ + /// The current path which points to an object in the Google Cloud Storage bucket. let path: StoragePath override init() { @@ -630,9 +584,7 @@ import Foundation self.path = path } - /** - * For maxSize API, return an error if the size is exceeded. - */ + /// For maxSize API, return an error if the size is exceeded. private func checkSizeOverflow(task: StorageTask, maxSize: Int64) -> NSError? { if task.progress.totalUnitCount > maxSize || task.progress.completedUnitCount > maxSize { return StorageErrorCode.error(withCode: .downloadSizeExceeded, diff --git a/FirebaseStorage/Sources/StorageTask.swift b/FirebaseStorage/Sources/StorageTask.swift index 521beabeaaa..fe119c05c92 100644 --- a/FirebaseStorage/Sources/StorageTask.swift +++ b/FirebaseStorage/Sources/StorageTask.swift @@ -24,6 +24,7 @@ import Foundation * A superclass to all Storage tasks, including `StorageUploadTask` * and `StorageDownloadTask`, to provide state transitions, event raising, and common storage * for metadata and errors. + * * Callbacks are always fired on the developer-specified callback queue. * If no queue is specified, it defaults to the main queue. * This class is thread-safe. @@ -98,6 +99,7 @@ import Foundation /** * Defines task operations such as pause, resume, cancel, and enqueue for all tasks. + * * All tasks are required to implement enqueue, which begins the task, and may optionally * implement pause, resume, and cancel, which operate on the task to pause, resume, and cancel * operations. diff --git a/FirebaseStorage/Sources/StorageUploadTask.swift b/FirebaseStorage/Sources/StorageUploadTask.swift index 77e01f83cc4..759fd6b47c5 100644 --- a/FirebaseStorage/Sources/StorageUploadTask.swift +++ b/FirebaseStorage/Sources/StorageUploadTask.swift @@ -22,10 +22,13 @@ import Foundation /** * `StorageUploadTask` implements resumable uploads to a file in Firebase Storage. + * * Uploads can be returned on completion with a completion callback, and can be monitored * by attaching observers, or controlled by calling `pause()`, `resume()`, * or `cancel()`. + * * Uploads can be initialized from `Data` in memory, or a URL to a file on disk. + * * Uploads are performed on a background queue, and callbacks are raised on the developer * specified `callbackQueue` in Storage, or the main queue if unspecified. */ From d603c7bcb36bf52b6f24a7149b874e647b404799 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 2 Feb 2024 07:08:31 -0800 Subject: [PATCH 018/104] Fix health-metrics (#12345) --- .../workflows/health-metrics-presubmit.yml | 26 ++++++++++--------- scripts/health_metrics/README.md | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/health-metrics-presubmit.yml b/.github/workflows/health-metrics-presubmit.yml index 0a91c454492..61ef5a17a26 100644 --- a/.github/workflows/health-metrics-presubmit.yml +++ b/.github/workflows/health-metrics-presubmit.yml @@ -72,7 +72,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseABTesting --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -92,7 +92,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseAuth --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -115,7 +115,9 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseDatabase --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + # TODO: Make sure that https://github.com/actions/upload-artifact/issues/478 is resolved + # before going to actions/upload-artifact@v4. + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -138,7 +140,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseDynamicLinks --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -164,7 +166,7 @@ jobs: run: | export EXPERIMENTAL_MODE=true ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseFirestore --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -187,7 +189,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseFunctions --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -210,7 +212,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseInAppMessaging --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -233,7 +235,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseMessaging --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -258,7 +260,7 @@ jobs: run: gem install xcpretty - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebasePerformance --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -281,7 +283,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseRemoteConfig --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -304,7 +306,7 @@ jobs: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh --sdk=FirebaseStorage --platform=${{ matrix.target }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult @@ -331,7 +333,7 @@ jobs: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/metrics_service_access.json.gpg \ metrics-access.json "${{ env.METRICS_SERVICE_SECRET }}" gcloud auth activate-service-account --key-file metrics-access.json - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 id: download with: path: /Users/runner/test diff --git a/scripts/health_metrics/README.md b/scripts/health_metrics/README.md index d3f22f25412..829cfeae24c 100644 --- a/scripts/health_metrics/README.md +++ b/scripts/health_metrics/README.md @@ -33,7 +33,7 @@ pod-lib-lint-newsdk: run: scripts/setup_bundler.sh - name: Build and test run: ./scripts/health_metrics/pod_test_code_coverage_report.sh FirebaseNewSDK "${{ matrix.target }}" - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: codecoverage path: /Users/runner/*.xcresult From 90e24d0a48d7b5ecf77d2185c484e4a55a8a7878 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Sun, 4 Feb 2024 14:31:27 -0500 Subject: [PATCH 019/104] Increase `swift-tools-version` to `5.7.1` (#12350) --- FirebaseCore/CHANGELOG.md | 4 ++++ Package.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index 7e0f3b37337..e406249fa6d 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [Swift Package Manager] Firebase now enforces a Swift 5.7.1 minimum version, + which is aligned with the Xcode 14.1 minimum. (#12350) + # Firebase 10.21.0 - Firebase now requires at least CocoaPods version 1.12.0 to enable privacy manifest support. diff --git a/Package.swift b/Package.swift index 2b2d6190bc6..9092a709f3f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7.1 // The swift-tools-version declares the minimum version of Swift required to // build this package. From ec8e20eb3234be7500ba9414c6c09af1f5b30ba0 Mon Sep 17 00:00:00 2001 From: Sam Edson Date: Tue, 6 Feb 2024 11:05:17 -0500 Subject: [PATCH 020/104] Remove calls to statfs for supporting Privacy Manifests (#12355) --- Crashlytics/CHANGELOG.md | 3 +++ Crashlytics/Crashlytics/Components/FIRCLSHost.m | 12 +++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 997798c64db..f040eecef96 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [changed] Removed calls to statfs in the Crashlytics SDK to comply with Apple Privacy Manifests. This change removes support for collecting Disk Space Free in Crashlytics reports. + # 10.16.0 - [fixed] Fixed a memory leak regression when generating session events (#11725). diff --git a/Crashlytics/Crashlytics/Components/FIRCLSHost.m b/Crashlytics/Crashlytics/Components/FIRCLSHost.m index f4b117c4eac..c1f41a0182b 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSHost.m +++ b/Crashlytics/Crashlytics/Components/FIRCLSHost.m @@ -15,7 +15,6 @@ #include "Crashlytics/Crashlytics/Components/FIRCLSHost.h" #include -#include #include #import "Crashlytics/Crashlytics/Components/FIRCLSApplication.h" @@ -180,16 +179,15 @@ bool FIRCLSHostRecord(FIRCLSFile* file) { } void FIRCLSHostWriteDiskUsage(FIRCLSFile* file) { - struct statfs tStats; - FIRCLSFileWriteSectionStart(file, "storage"); FIRCLSFileWriteHashStart(file); - if (statfs(_firclsContext.readonly->host.documentDirectoryPath, &tStats) == 0) { - FIRCLSFileWriteHashEntryUint64(file, "free", tStats.f_bavail * tStats.f_bsize); - FIRCLSFileWriteHashEntryUint64(file, "total", tStats.f_blocks * tStats.f_bsize); - } + // Due to Apple Privacy Manifests, Crashlytics is not collecting + // disk space using statfs. When we find a solution, we can update + // this to actually track disk space correctly. + FIRCLSFileWriteHashEntryUint64(file, "free", 0); + FIRCLSFileWriteHashEntryUint64(file, "total", 0); FIRCLSFileWriteHashEnd(file); From aa0b985eeb1c08e7ca93152f9d1ccd99274131ff Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 6 Feb 2024 08:54:56 -0800 Subject: [PATCH 021/104] Fix auth integration test build issue (#12362) --- .github/workflows/auth.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auth.yml b/.github/workflows/auth.yml index 95eb31e5da4..43efa7a522c 100644 --- a/.github/workflows/auth.yml +++ b/.github/workflows/auth.yml @@ -55,7 +55,7 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v4 - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126 @@ -82,7 +82,8 @@ jobs: FirebaseAuth/Tests/Sample/Sample/Sample.entitlements "$plist_secret" scripts/decrypt_gha_secret.sh scripts/gha-encrypted/AuthSample/Credentials.swift.gpg \ FirebaseAuth/Tests/Sample/SwiftApiTests/Credentials.swift "$plist_secret" - + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer - name: BuildAndTest # can be replaced with pod lib lint with CocoaPods 1.10 run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh Auth iOS) From 3e7e30f16f6722318d8867431733230c200cb351 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:12:04 -0500 Subject: [PATCH 022/104] [Infra] Delete health-metrics-release workflow (#12363) --- .github/workflows/health-metrics-release.yml | 36 -------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/health-metrics-release.yml diff --git a/.github/workflows/health-metrics-release.yml b/.github/workflows/health-metrics-release.yml deleted file mode 100644 index 86934534175..00000000000 --- a/.github/workflows/health-metrics-release.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: health-metrics-release - -on: - release: - types: [published] - -env: - gpg_passphrase: ${{ secrets.GHASecretsGPGPassphrase1 }} - - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -jobs: - release-diffing: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@main - - name: Authenticate Google Cloud SDK - run: | - scripts/decrypt_gha_secret.sh \ - scripts/gha-encrypted/metrics_service_access.json.gpg \ - service_account.json \ - "${{ env.gpg_passphrase }}" - gcloud auth activate-service-account --key-file service_account.json - - name: Produce health metric diff reports - uses: FirebaseExtended/github-actions/health-metrics/release-diffing@master - with: - repo: ${{ github.repository }} - ref: ${{ github.ref }} - commit: ${{ github.sha }} - releaseId: ${{ github.event.release.id }} From 85da438acd224388ef4af03de769908428bfe188 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:39:02 -0500 Subject: [PATCH 023/104] [Carthage] Update Carthage artifacts for Firebase 10.21.0 (#12364) --- ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json | 1 + .../CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json | 1 + 19 files changed, 19 insertions(+) diff --git a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json index 6de1cc558ce..eff8f0425c6 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseABTesting-1a1916232af9cd1a.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseABTesting-3c0e5c9ddc52bccd.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseABTesting-8dad3d6af34cb26c.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseABTesting-0ad6c6c2f729706c.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseABTesting-e87c686cee02758a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseABTesting-6a65ab8b888172af.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseABTesting-197f0cb4125363b6.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json index 383f955a6f6..20fadca4a21 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/Google-Mobile-Ads-SDK-db6e557426f37394.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/Google-Mobile-Ads-SDK-c8bc252ed3323212.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/Google-Mobile-Ads-SDK-5f8bb98bb2467b85.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/Google-Mobile-Ads-SDK-23be5a73a2ce3dcc.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/Google-Mobile-Ads-SDK-8b0d1ce3d1162b67.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/Google-Mobile-Ads-SDK-046511c3fd0189eb.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/Google-Mobile-Ads-SDK-50008c143ad8f268.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json index 1b8cfd0f1c4..67115c7bd7f 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseAnalytics-040a907770027c1e.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAnalytics-6f8b70c8ee2efc85.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAnalytics-4d7ca295e8b44c0c.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAnalytics-620570dc24ce7d7b.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAnalytics-95669fcf109f74a2.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAnalytics-c0db6cb0e858e397.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAnalytics-e8ebe991b5743f71.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json index 7dee6fe1842..33081e98162 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseAnalyticsOnDeviceConversion-219e668e914bee4c.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAnalyticsOnDeviceConversion-37cf6277991d7d75.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAnalyticsOnDeviceConversion-d3913995b7344202.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAnalyticsOnDeviceConversion-202ed30074984af7.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAnalyticsOnDeviceConversion-091f5252d693a9f9.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAnalyticsOnDeviceConversion-7bbb73d46383a042.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAnalyticsOnDeviceConversion-eca2f83d40e0278d.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json index 5011e733d47..08e59ee59b7 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseAppCheck-09ecedc08c2562d0.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAppCheck-b0ead84a126d24d4.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAppCheck-8f7dfe411eeaccdf.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAppCheck-a458ebf606a7b451.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAppCheck-d19e46a728b1ac4f.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAppCheck-8339fde989fe8f24.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAppCheck-3ce0f074bfcd2596.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json index 93e41514e2b..05008847a77 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseAppDistribution-30aec5c329204ede.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAppDistribution-45b5c85bba08a85b.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAppDistribution-264a5e036b72a526.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAppDistribution-e08ef26e391c7b0b.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAppDistribution-cefc3327ddfceda6.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAppDistribution-7931e42d39575534.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAppDistribution-79dc2b1348d9aee9.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json index fb86a849673..b8540b8da37 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseAuth-9b66f8a440352f9b.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAuth-2165e27f89d4959e.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAuth-222a2417c3c21b41.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAuth-da6796caf834f09f.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAuth-e43e66353617f093.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAuth-8a9591e6daa7e207.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAuth-7e18a510d0a5b02e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json index de53576ff28..750c9ad6d44 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseCrashlytics-8126c26e6374c8b1.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseCrashlytics-054718c61ef054f9.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseCrashlytics-029c76d79754388c.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseCrashlytics-0f5ccfdbf0de85f7.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseCrashlytics-d29d3285a7d9fa1d.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseCrashlytics-165beb64483b4278.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseCrashlytics-53604573442e756b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json index af8fcab629a..71042ee6c80 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseDatabase-a62dd446dac4b89f.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseDatabase-8b7048f7890bb665.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseDatabase-a7f5c6d032473b01.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseDatabase-a05cb524bec955b2.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseDatabase-5b22f689cb66d83a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseDatabase-e1a9d1f0c4222cf7.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseDatabase-aea9249d81841ee1.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json index 7f49cc9370c..4cac083f472 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseDynamicLinks-a4f0deb13e7b39fd.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseDynamicLinks-bfdce6ac5d591ab3.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseDynamicLinks-693c6213bc87f8c0.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseDynamicLinks-ad0ac7b8fdf4c1b5.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseDynamicLinks-7cf4ae5e96882ca8.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseDynamicLinks-c3bdeb37651a5d5d.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseDynamicLinks-bcb5df6ec32f6684.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json index 56e3f67a9f3..5ed8fc07b0d 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseFirestore-b46da5ea686c3422.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseFirestore-4c3d1568e379a98c.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseFirestore-88b0aaac6fe277fe.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseFirestore-dcf15ce0975bfa3c.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseFirestore-73ba0700b1aa6d6a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseFirestore-02eb8da05f81fca5.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseFirestore-46fa68ddf287f76e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json index f5220531424..fa18832e784 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseFunctions-8e91f34c0289f5ba.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseFunctions-b949cfeca4e7a80f.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseFunctions-23d6ba97d95db62c.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseFunctions-b77aca8c98dba58d.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseFunctions-47189f2c99cdf806.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseFunctions-17c4b760141e38ad.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseFunctions-688a38b567392fcf.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json index 1a9722b791f..d605a90f573 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/GoogleSignIn-7d252d83bad9f2b6.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/GoogleSignIn-c887dbc6bd07c787.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/GoogleSignIn-e55954e1a3ca9ee8.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/GoogleSignIn-82fc8f5e20a9345b.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/GoogleSignIn-a5b49807be66100b.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/GoogleSignIn-0d2e746eb3ff9f92.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/GoogleSignIn-5cb2a2f1f74efd5e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json index 77a02b25789..bedb6a1e462 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseInAppMessaging-ab8486f9415476ca.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseInAppMessaging-f29d7b7839cda915.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseInAppMessaging-c6a82f2dccc9a092.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseInAppMessaging-940786963f9ac384.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseInAppMessaging-91e5426eade46bca.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseInAppMessaging-10801bd111df59de.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseInAppMessaging-91d4dd9878a06b7e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json index dd4d3949ac9..e9f09a5570e 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseMLModelDownloader-865fc851458ff97e.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseMLModelDownloader-ee2af587027e74d3.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseMLModelDownloader-e45969e88bf879cd.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseMLModelDownloader-d779b84cfdf214f3.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseMLModelDownloader-559cb113c0cfd8f2.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseMLModelDownloader-9c909894999c92e4.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseMLModelDownloader-9abf9b0e24bfb921.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json index 581cec20e53..b8ce415798e 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseMessaging-536acbc043d0e42a.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseMessaging-289a04c85f7e771d.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseMessaging-236bb6f578c05ed1.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseMessaging-4a481ad8d3446844.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseMessaging-59ef1cc63c660712.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseMessaging-76c02a69e3fe1008.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseMessaging-439a17dcc8b8172b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json index 2a8ace17067..42bc4ea2a3e 100644 --- a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebasePerformance-5ef59eac9e09086e.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebasePerformance-7a7398acc615dbb6.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebasePerformance-6494eb8091be4e03.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebasePerformance-4b6c574e0645b449.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebasePerformance-36ac6dfb99caa11b.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebasePerformance-f9f5be8ffad5cbb0.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebasePerformance-0ffe559f7554d8a5.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json index cc135d0dfc7..6e28e9f7a9b 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseRemoteConfig-13b390a0fae2a496.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseRemoteConfig-45a7f541a654884c.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseRemoteConfig-44d640335ebdfea7.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseRemoteConfig-933eae5291c343cc.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseRemoteConfig-edd1b427b8bbe782.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseRemoteConfig-10b62ee5663aaab3.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseRemoteConfig-2237eb5fcd4a4525.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json index 9fae2c91442..56dffe7b95e 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json @@ -13,6 +13,7 @@ "10.19.0": "https://dl.google.com/dl/firebase/ios/carthage/10.19.0/FirebaseStorage-93e56fb9268ffd5f.zip", "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseStorage-347e6a3402706596.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseStorage-fac47c17aae220e0.zip", + "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseStorage-6f3adc4f2b871f04.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseStorage-ac463d14593d10a8.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseStorage-fdf8479115660ce6.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseStorage-04f255ea8c3a7420.zip", From 7dc3e52a10ed4bbd05054790e60eb3f13d29ba3b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 6 Feb 2024 14:29:51 -0800 Subject: [PATCH 024/104] Update to CocoaPods 1.15.2 (#12325) --- Gemfile | 2 +- Gemfile.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 77136b400b9..662939268b3 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,6 @@ source 'https://rubygems.org' # gem 'cocoapods-core', git: "https://github.com/CocoaPods/Core.git", ref: "f7cf05720eab935d7d50e35224d263952176fb53" # gem 'xcodeproj', git: "https://github.com/CocoaPods/Xcodeproj.git", ref: "eeccae7275645753cbaf45d96fc4b23e4b8b3b9f" -gem 'cocoapods', '1.14.3' +gem 'cocoapods', '1.15.2' gem 'cocoapods-generate', '2.2.5' gem 'danger', '8.4.5' diff --git a/Gemfile.lock b/Gemfile.lock index 3d7e1067b67..d09fdd38cce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.1.2) + activesupport (7.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -20,16 +20,16 @@ GEM json (>= 1.5.1) atomos (0.1.3) base64 (0.2.0) - bigdecimal (3.1.4) + bigdecimal (3.1.6) claide (1.0.3) claide-plugins (0.9.2) cork nap open4 (~> 1.3) - cocoapods (1.14.3) + cocoapods (1.15.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.14.3) + cocoapods-core (= 1.15.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -44,7 +44,7 @@ GEM nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.14.3) + cocoapods-core (1.15.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -67,7 +67,7 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) cork (0.3.0) colored2 (~> 3.1) @@ -124,12 +124,12 @@ GEM httpclient (2.8.3) i18n (1.14.1) concurrent-ruby (~> 1.0) - json (2.6.3) + json (2.7.1) kramdown (2.3.1) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - minitest (5.20.0) + minitest (5.22.0) molinillo (0.8.0) multipart-post (2.1.1) mutex_m (0.2.0) @@ -156,7 +156,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.1.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -168,7 +168,7 @@ PLATFORMS ruby DEPENDENCIES - cocoapods (= 1.14.3) + cocoapods (= 1.15.2) cocoapods-generate (= 2.2.5) danger (= 8.4.5) From d95c62be19c378d80e2bc64b70f94993ca5687e2 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:52:52 -0500 Subject: [PATCH 025/104] Update versions for Release 10.22.0 (#12366) --- Firebase.podspec | 48 +++++++++---------- FirebaseABTesting.podspec | 2 +- FirebaseAnalytics.podspec | 6 +-- FirebaseAnalyticsOnDeviceConversion.podspec | 4 +- FirebaseAppCheck.podspec | 2 +- FirebaseAppCheckInterop.podspec | 2 +- FirebaseAppDistribution.podspec | 2 +- FirebaseAuth.podspec | 2 +- FirebaseAuthInterop.podspec | 2 +- FirebaseCore.podspec | 2 +- FirebaseCoreExtension.podspec | 2 +- FirebaseCoreInternal.podspec | 2 +- FirebaseCrashlytics.podspec | 2 +- FirebaseDatabase.podspec | 2 +- FirebaseDynamicLinks.podspec | 2 +- FirebaseFirestore.podspec | 2 +- FirebaseFirestoreInternal.podspec | 2 +- FirebaseFunctions.podspec | 2 +- FirebaseInAppMessaging.podspec | 2 +- FirebaseInstallations.podspec | 2 +- FirebaseMLModelDownloader.podspec | 2 +- FirebaseMessaging.podspec | 2 +- FirebaseMessagingInterop.podspec | 2 +- FirebasePerformance.podspec | 2 +- FirebaseRemoteConfig.podspec | 2 +- FirebaseSessions.podspec | 2 +- FirebaseSharedSwift.podspec | 2 +- FirebaseStorage.podspec | 2 +- GoogleAppMeasurement.podspec | 4 +- ...leAppMeasurementOnDeviceConversion.podspec | 2 +- Package.swift | 2 +- .../FirebaseManifest/FirebaseManifest.swift | 2 +- 32 files changed, 59 insertions(+), 59 deletions(-) diff --git a/Firebase.podspec b/Firebase.podspec index fa9c889398f..c7e3709a691 100644 --- a/Firebase.podspec +++ b/Firebase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Firebase' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase' s.description = <<-DESC @@ -36,14 +36,14 @@ Simplify your app development, grow your user base, and monetize more effectivel ss.ios.deployment_target = '10.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' - ss.ios.dependency 'FirebaseAnalytics', '~> 10.21.0' - ss.osx.dependency 'FirebaseAnalytics', '~> 10.21.0' - ss.tvos.dependency 'FirebaseAnalytics', '~> 10.21.0' + ss.ios.dependency 'FirebaseAnalytics', '~> 10.22.0' + ss.osx.dependency 'FirebaseAnalytics', '~> 10.22.0' + ss.tvos.dependency 'FirebaseAnalytics', '~> 10.22.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'CoreOnly' do |ss| - ss.dependency 'FirebaseCore', '10.21.0' + ss.dependency 'FirebaseCore', '10.22.0' ss.source_files = 'CoreOnly/Sources/Firebase.h' ss.preserve_paths = 'CoreOnly/Sources/module.modulemap' if ENV['FIREBASE_POD_REPO_FOR_DEV_POD'] then @@ -79,13 +79,13 @@ Simplify your app development, grow your user base, and monetize more effectivel ss.ios.deployment_target = '10.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' - ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 10.21.0' + ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 10.22.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'ABTesting' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseABTesting', '~> 10.21.0' + ss.dependency 'FirebaseABTesting', '~> 10.22.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -95,13 +95,13 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'AppDistribution' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseAppDistribution', '~> 10.21.0-beta' + ss.ios.dependency 'FirebaseAppDistribution', '~> 10.22.0-beta' ss.ios.deployment_target = '11.0' end s.subspec 'AppCheck' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAppCheck', '~> 10.21.0' + ss.dependency 'FirebaseAppCheck', '~> 10.22.0' ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' @@ -110,7 +110,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Auth' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAuth', '~> 10.21.0' + ss.dependency 'FirebaseAuth', '~> 10.22.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -120,7 +120,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Crashlytics' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseCrashlytics', '~> 10.21.0' + ss.dependency 'FirebaseCrashlytics', '~> 10.22.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -130,7 +130,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Database' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseDatabase', '~> 10.21.0' + ss.dependency 'FirebaseDatabase', '~> 10.22.0' # Standard platforms PLUS watchOS 7. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -140,13 +140,13 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'DynamicLinks' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseDynamicLinks', '~> 10.21.0' + ss.ios.dependency 'FirebaseDynamicLinks', '~> 10.22.0' ss.ios.deployment_target = '11.0' end s.subspec 'Firestore' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFirestore', '~> 10.21.0' + ss.dependency 'FirebaseFirestore', '~> 10.22.0' ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' @@ -154,7 +154,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Functions' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFunctions', '~> 10.21.0' + ss.dependency 'FirebaseFunctions', '~> 10.22.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -164,20 +164,20 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'InAppMessaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseInAppMessaging', '~> 10.21.0-beta' - ss.tvos.dependency 'FirebaseInAppMessaging', '~> 10.21.0-beta' + ss.ios.dependency 'FirebaseInAppMessaging', '~> 10.22.0-beta' + ss.tvos.dependency 'FirebaseInAppMessaging', '~> 10.22.0-beta' ss.ios.deployment_target = '11.0' ss.tvos.deployment_target = '12.0' end s.subspec 'Installations' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseInstallations', '~> 10.21.0' + ss.dependency 'FirebaseInstallations', '~> 10.22.0' end s.subspec 'Messaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMessaging', '~> 10.21.0' + ss.dependency 'FirebaseMessaging', '~> 10.22.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -187,7 +187,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'MLModelDownloader' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMLModelDownloader', '~> 10.21.0-beta' + ss.dependency 'FirebaseMLModelDownloader', '~> 10.22.0-beta' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -197,15 +197,15 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Performance' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebasePerformance', '~> 10.21.0' - ss.tvos.dependency 'FirebasePerformance', '~> 10.21.0' + ss.ios.dependency 'FirebasePerformance', '~> 10.22.0' + ss.tvos.dependency 'FirebasePerformance', '~> 10.22.0' ss.ios.deployment_target = '11.0' ss.tvos.deployment_target = '12.0' end s.subspec 'RemoteConfig' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseRemoteConfig', '~> 10.21.0' + ss.dependency 'FirebaseRemoteConfig', '~> 10.22.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -215,7 +215,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Storage' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseStorage', '~> 10.21.0' + ss.dependency 'FirebaseStorage', '~> 10.22.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index dd5cda94849..2321afaf72f 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseABTesting' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase ABTesting' s.description = <<-DESC diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index 19295bad4cd..82b191480c4 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalytics' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Analytics for iOS' s.description = <<-DESC @@ -37,12 +37,12 @@ Pod::Spec.new do |s| s.default_subspecs = 'AdIdSupport' s.subspec 'AdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement', '10.21.0' + ss.dependency 'GoogleAppMeasurement', '10.22.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end s.subspec 'WithoutAdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.21.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.22.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end diff --git a/FirebaseAnalyticsOnDeviceConversion.podspec b/FirebaseAnalyticsOnDeviceConversion.podspec index 6de84f13eaa..3dddf28a777 100644 --- a/FirebaseAnalyticsOnDeviceConversion.podspec +++ b/FirebaseAnalyticsOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsOnDeviceConversion' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'On device conversion measurement plugin for FirebaseAnalytics. Not intended for direct use.' s.description = <<-DESC @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.cocoapods_version = '>= 1.12.0' - s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.21.0' + s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.22.0' s.static_framework = true diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index ccf007ef013..232d4b29318 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheck' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase App Check SDK.' s.description = <<-DESC diff --git a/FirebaseAppCheckInterop.podspec b/FirebaseAppCheckInterop.podspec index 1485738593b..f1a238848b6 100644 --- a/FirebaseAppCheckInterop.podspec +++ b/FirebaseAppCheckInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheckInterop' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Interfaces that allow other Firebase SDKs to use AppCheck functionality.' s.description = <<-DESC diff --git a/FirebaseAppDistribution.podspec b/FirebaseAppDistribution.podspec index cfc10fe8bb2..ccf859c21bb 100644 --- a/FirebaseAppDistribution.podspec +++ b/FirebaseAppDistribution.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppDistribution' - s.version = '10.21.0-beta' + s.version = '10.22.0-beta' s.summary = 'App Distribution for Firebase iOS SDK.' s.description = <<-DESC diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index ae34d933eeb..63dc3f0fcc0 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuth' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Apple platform client for Firebase Authentication' s.description = <<-DESC diff --git a/FirebaseAuthInterop.podspec b/FirebaseAuthInterop.podspec index d82a8dd0115..65d84553e7b 100644 --- a/FirebaseAuthInterop.podspec +++ b/FirebaseAuthInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuthInterop' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Auth functionality.' s.description = <<-DESC diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index f91e9507dc6..f629afae4cd 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCore' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Core' s.description = <<-DESC diff --git a/FirebaseCoreExtension.podspec b/FirebaseCoreExtension.podspec index 3ba07f3d5c9..a5e6a1344d8 100644 --- a/FirebaseCoreExtension.podspec +++ b/FirebaseCoreExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreExtension' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Extended FirebaseCore APIs for Firebase product SDKs' s.description = <<-DESC diff --git a/FirebaseCoreInternal.podspec b/FirebaseCoreInternal.podspec index cb545f2e8b7..bae9c0dc743 100644 --- a/FirebaseCoreInternal.podspec +++ b/FirebaseCoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreInternal' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'APIs for internal FirebaseCore usage.' s.description = <<-DESC diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index 75b29429e6d..ab12afd74e5 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCrashlytics' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Best and lightest-weight crash reporting for mobile, desktop and tvOS.' s.description = 'Firebase Crashlytics helps you track, prioritize, and fix stability issues that erode app quality.' s.homepage = 'https://firebase.google.com/' diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index ef2d8d14487..ab9551fac82 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDatabase' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Realtime Database' s.description = <<-DESC diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index 91bc0f1f492..d81f919e856 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDynamicLinks' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Dynamic Links' s.description = <<-DESC diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index f58d9ab929a..0f80b831868 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestore' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC Google Cloud Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development. diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index bef9568bf8e..cbe2eabeaaf 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestoreInternal' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index 446d31801dc..75f42c569ff 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFunctions' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Cloud Functions for Firebase' s.description = <<-DESC diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index f8d09ed3bcd..29cb090198e 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessaging' - s.version = '10.21.0-beta' + s.version = '10.22.0-beta' s.summary = 'Firebase In-App Messaging for iOS' s.description = <<-DESC diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index 218de4dcc23..7ba214b8d73 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInstallations' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Installations' s.description = <<-DESC diff --git a/FirebaseMLModelDownloader.podspec b/FirebaseMLModelDownloader.podspec index a7153f42828..317b3ddb3b6 100644 --- a/FirebaseMLModelDownloader.podspec +++ b/FirebaseMLModelDownloader.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMLModelDownloader' - s.version = '10.21.0-beta' + s.version = '10.22.0-beta' s.summary = 'Firebase ML Model Downloader' s.description = <<-DESC diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 4651a1fe1f3..625576dc9c7 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessaging' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Messaging' s.description = <<-DESC diff --git a/FirebaseMessagingInterop.podspec b/FirebaseMessagingInterop.podspec index 414a2a8ea37..f93f2b9dec7 100644 --- a/FirebaseMessagingInterop.podspec +++ b/FirebaseMessagingInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessagingInterop' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Messaging functionality.' s.description = <<-DESC diff --git a/FirebasePerformance.podspec b/FirebasePerformance.podspec index 769227e3012..e2a543d0dd2 100644 --- a/FirebasePerformance.podspec +++ b/FirebasePerformance.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebasePerformance' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Performance' s.description = <<-DESC diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 202dc7b647d..a19552b9463 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfig' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Remote Config' s.description = <<-DESC diff --git a/FirebaseSessions.podspec b/FirebaseSessions.podspec index 9842afd247a..271158893a2 100644 --- a/FirebaseSessions.podspec +++ b/FirebaseSessions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSessions' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Sessions' s.description = <<-DESC diff --git a/FirebaseSharedSwift.podspec b/FirebaseSharedSwift.podspec index 4d99e11dafe..9412356fc06 100644 --- a/FirebaseSharedSwift.podspec +++ b/FirebaseSharedSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSharedSwift' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Shared Swift Extensions for Firebase' s.description = <<-DESC diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index 448ea1853ed..a9ea3bd4127 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseStorage' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Firebase Storage' s.description = <<-DESC diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index 727d67b9985..e924e6250ef 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurement' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = 'Shared measurement methods for Google libraries. Not intended for direct use.' s.description = <<-DESC @@ -37,7 +37,7 @@ Pod::Spec.new do |s| s.default_subspecs = 'AdIdSupport' s.subspec 'AdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.21.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.22.0' ss.vendored_frameworks = 'Frameworks/GoogleAppMeasurementIdentitySupport.xcframework' end diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index e91e77d41b3..7fa66feaf3f 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurementOnDeviceConversion' - s.version = '10.21.0' + s.version = '10.22.0' s.summary = <<-SUMMARY On device conversion measurement plugin for Google App Measurement. Not intended for direct use. diff --git a/Package.swift b/Package.swift index 9092a709f3f..c9850b123dc 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ import class Foundation.ProcessInfo import PackageDescription -let firebaseVersion = "10.21.0" +let firebaseVersion = "10.22.0" let package = Package( name: "Firebase", diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index e0492f0f58a..093aacae131 100755 --- a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift +++ b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift @@ -21,7 +21,7 @@ import Foundation /// The version and releasing fields of the non-Firebase pods should be reviewed every release. /// The array should be ordered so that any pod's dependencies precede it in the list. public let shared = Manifest( - version: "10.21.0", + version: "10.22.0", pods: [ Pod("FirebaseSharedSwift"), Pod("FirebaseCoreInternal"), From c959f1bdd364f3df62f2e445f7c6504ae545f759 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 7 Feb 2024 08:25:52 -0800 Subject: [PATCH 026/104] [auth-swift] Modernize Auth API docs (#12353) --- .github/workflows/auth.yml | 5 +- .../Swift/ActionCode/ActionCodeInfo.swift | 26 +- .../ActionCode/ActionCodeOperation.swift | 18 +- .../Swift/ActionCode/ActionCodeSettings.swift | 68 +- .../Swift/ActionCode/ActionCodeURL.swift | 36 +- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 1568 +++++++---------- .../Sources/Swift/Auth/AuthDataResult.swift | 36 +- .../Sources/Swift/Auth/AuthDispatcher.swift | 26 +- .../Swift/Auth/AuthOperationType.swift | 21 +- .../Sources/Swift/Auth/AuthSettings.swift | 10 +- .../Sources/Swift/Auth/AuthTokenResult.swift | 61 +- .../Swift/AuthProvider/AuthCredential.swift | 6 +- .../AuthProvider/AuthProviderStrings.swift | 1 + .../AuthProvider/EmailAuthProvider.swift | 27 +- .../AuthProvider/FacebookAuthProvider.swift | 14 +- .../AuthProvider/FederatedAuthProvider.swift | 15 +- .../AuthProvider/GameCenterAuthProvider.swift | 38 +- .../AuthProvider/GitHubAuthProvider.swift | 14 +- .../AuthProvider/GoogleAuthProvider.swift | 20 +- .../Swift/AuthProvider/OAuthCredential.swift | 18 +- .../Swift/AuthProvider/OAuthProvider.swift | 182 +- .../AuthProvider/PhoneAuthCredential.swift | 7 +- .../AuthProvider/PhoneAuthProvider.swift | 183 +- .../AuthProvider/TwitterAuthProvider.swift | 16 +- .../Sources/Swift/Backend/AuthBackend.swift | 62 +- .../Swift/Backend/AuthRPCResponse.swift | 22 +- .../Backend/AuthRequestConfiguration.swift | 36 +- .../Backend/IdentityToolkitRequest.swift | 12 +- .../Backend/RPC/CreateAuthURIRequest.swift | 76 +- .../Backend/RPC/CreateAuthURIResponse.swift | 30 +- .../Backend/RPC/DeleteAccountRequest.swift | 23 +- .../Backend/RPC/DeleteAccountResponse.swift | 7 +- .../Backend/RPC/EmailLinkSignInRequest.swift | 33 +- .../Backend/RPC/EmailLinkSignInResponse.swift | 34 +- .../Backend/RPC/GetAccountInfoRequest.swift | 29 +- .../Backend/RPC/GetAccountInfoResponse.swift | 105 +- .../RPC/GetOOBConfirmationCodeRequest.swift | 173 +- .../Backend/RPC/GetProjectConfigRequest.swift | 5 +- .../RPC/GetRecaptchaConfigRequest.swift | 34 +- .../Enroll/FinalizeMFAEnrollmentRequest.swift | 4 +- .../Enroll/StartMFAEnrollmentRequest.swift | 4 +- .../SignIn/FinalizeMFASignInRequest.swift | 4 +- .../SignIn/StartMFASignInRequest.swift | 5 +- .../Unenroll/WithdrawMFARequest.swift | 4 +- .../AuthProtoStartMFAPhoneRequestInfo.swift | 16 +- ...otoStartMFATOTPEnrollmentRequestInfo.swift | 6 +- .../Backend/RPC/ResetPasswordRequest.swift | 35 +- .../Backend/RPC/ResetPasswordResponse.swift | 32 +- .../Backend/RPC/RevokeTokenRequest.swift | 43 +- .../Backend/RPC/SecureTokenRequest.swift | 66 +- .../Backend/RPC/SecureTokenResponse.swift | 15 +- .../RPC/SendVerificationTokenRequest.swift | 33 +- .../Backend/RPC/SetAccountInfoRequest.swift | 147 +- .../Backend/RPC/SetAccountInfoResponse.swift | 58 +- .../RPC/SignInWithGameCenterRequest.swift | 60 +- .../Backend/RPC/SignUpNewUserRequest.swift | 78 +- .../Backend/RPC/SignUpNewUserResponse.swift | 16 +- .../Backend/RPC/VerifyAssertionRequest.swift | 163 +- .../Backend/RPC/VerifyAssertionResponse.swift | 160 +- .../RPC/VerifyCustomTokenRequest.swift | 16 +- .../RPC/VerifyCustomTokenResponse.swift | 24 +- .../Backend/RPC/VerifyPasswordRequest.swift | 79 +- .../Backend/RPC/VerifyPasswordResponse.swift | 47 +- .../RPC/VerifyPhoneNumberRequest.swift | 90 +- .../RPC/VerifyPhoneNumberResponse.swift | 33 +- .../Swift/MultiFactor/MultiFactor.swift | 75 +- .../MultiFactor/MultiFactorAssertion.swift | 13 +- .../Swift/MultiFactor/MultiFactorInfo.swift | 23 +- .../MultiFactor/MultiFactorResolver.swift | 43 +- .../MultiFactor/MultiFactorSession.swift | 35 +- .../Phone/PhoneMultiFactorAssertion.swift | 9 +- .../Phone/PhoneMultiFactorGenerator.swift | 21 +- .../Phone/PhoneMultiFactorInfo.swift | 24 +- .../TOTP/TOTPMultFactorAssertion.swift | 9 +- .../TOTP/TOTPMultiFactorGenerator.swift | 62 +- .../TOTP/TOTPMultiFactorInfo.swift | 23 +- .../Swift/MultiFactor/TOTP/TOTPSecret.swift | 65 +- .../Swift/Storage/AuthKeychainServices.swift | 72 +- .../Swift/Storage/AuthKeychainStorage.swift | 5 +- .../Storage/AuthKeychainStorageReal.swift | 5 +- .../Swift/Storage/AuthUserDefaults.swift | 21 +- .../Swift/SystemService/AuthAPNSToken.swift | 18 +- .../SystemService/AuthAPNSTokenManager.swift | 51 +- .../SystemService/AuthAPNSTokenType.swift | 20 +- .../SystemService/AuthAppCredential.swift | 23 +- .../AuthAppCredentialManager.swift | 28 +- .../AuthNotificationManager.swift | 85 +- .../SystemService/SecureTokenService.swift | 78 +- .../Swift/User/AdditionalUserInfo.swift | 18 +- FirebaseAuth/Sources/Swift/User/User.swift | 1158 +++++------- .../Sources/Swift/User/UserInfo.swift | 31 +- .../Sources/Swift/User/UserInfoImpl.swift | 26 +- .../Sources/Swift/User/UserMetadata.swift | 13 +- .../Swift/User/UserProfileChangeRequest.swift | 42 +- .../Utilities/AuthDefaultUIDelegate.swift | 15 +- .../Swift/Utilities/AuthErrorUtils.swift | 22 +- .../Sources/Swift/Utilities/AuthErrors.swift | 921 +++------- .../Swift/Utilities/AuthInternalErrors.swift | 124 +- .../Swift/Utilities/AuthUIDelegate.swift | 33 +- .../Swift/Utilities/AuthURLPresenter.swift | 66 +- .../Swift/Utilities/AuthWebUtils.swift | 6 +- .../Sources/Swift/Utilities/AuthWebView.swift | 4 +- .../Utilities/AuthWebViewController.swift | 39 +- .../AuthBackendRPCImplentationTests.swift | 2 +- 104 files changed, 2830 insertions(+), 4833 deletions(-) diff --git a/.github/workflows/auth.yml b/.github/workflows/auth.yml index a4627ff26cc..7a66d131cef 100644 --- a/.github/workflows/auth.yml +++ b/.github/workflows/auth.yml @@ -58,7 +58,7 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-12 + runs-on: macos-13 steps: - uses: actions/checkout@v4 - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126 @@ -85,7 +85,8 @@ jobs: FirebaseAuth/Tests/SampleSwift/Sample.entitlements "$plist_secret" scripts/decrypt_gha_secret.sh scripts/gha-encrypted/AuthSample/Credentials.swift.gpg \ FirebaseAuth/Tests/SampleSwift/SwiftApiTests/Credentials.swift "$plist_secret" - + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer - name: BuildAndTest # can be replaced with pod lib lint with CocoaPods 1.10 run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh Auth iOS) diff --git a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeInfo.swift b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeInfo.swift index 677a8e5819d..09fcac26a46 100644 --- a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeInfo.swift +++ b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeInfo.swift @@ -14,24 +14,16 @@ import Foundation -/** @class ActionCodeInfo - @brief Manages information regarding action codes. - */ +/// Manages information regarding action codes. @objc(FIRActionCodeInfo) open class ActionCodeInfo: NSObject { - /** - @brief The operation being performed. - */ + /// The operation being performed. @objc public let operation: ActionCodeOperation - /** @property email - @brief The email address to which the code was sent. The new email address in the case of - `ActionCodeOperationRecoverEmail`. - */ + /// The email address to which the code was sent. The new email address in the case of + /// `ActionCodeOperation.recoverEmail`. @objc public let email: String? - /** @property previousEmail - @brief The email that is being recovered in the case of `ActionCodeOperationRecoverEmail`. - */ + /// The email that is being recovered in the case of `ActionCodeOperation.recoverEmail`. @objc public let previousEmail: String? init(withOperation operation: ActionCodeOperation, email: String, newEmail: String?) { @@ -45,11 +37,9 @@ import Foundation } } - /** @fn actionCodeOperationForRequestType: - @brief Returns the corresponding operation type per provided request type string. - @param requestType Request type returned in in the server response. - @return The corresponding ActionCodeOperation for the supplied request type. - */ + /// Map a request type string to the corresponding operation type. + /// - Parameter requestType: Request type returned in in the server response. + /// - Returns: The corresponding ActionCodeOperation for the supplied request type. class func actionCodeOperation(forRequestType requestType: String?) -> ActionCodeOperation { switch requestType { case "PASSWORD_RESET": return .passwordReset diff --git a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeOperation.swift b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeOperation.swift index 14dc28dd19a..e345b6e91e8 100644 --- a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeOperation.swift +++ b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeOperation.swift @@ -14,28 +14,26 @@ import Foundation -/** - @brief Operations which can be performed with action codes. - */ +/// Operations which can be performed with action codes. @objc(FIRActionCodeOperation) public enum ActionCodeOperation: Int, @unchecked Sendable { - /** Action code for unknown operation. */ + /// Action code for unknown operation. case unknown = 0 - /** Action code for password reset operation. */ + /// Action code for password reset operation. case passwordReset = 1 - /** Action code for verify email operation. */ + /// Action code for verify email operation. case verifyEmail = 2 - /** Action code for recover email operation. */ + /// Action code for recover email operation. case recoverEmail = 3 - /** Action code for email link operation. */ + /// Action code for email link operation. case emailLink = 4 - /** Action code for verifying and changing email */ + /// Action code for verifying and changing email. case verifyAndChangeEmail = 5 - /** Action code for reverting second factor addition */ + /// Action code for reverting second factor addition. case revertSecondFactorAddition = 6 } diff --git a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift index 7c04d3a1ec8..f9cb95190d3 100644 --- a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift +++ b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift @@ -14,68 +14,49 @@ import Foundation -/** @class FIRActionCodeSettings - @brief Used to set and retrieve settings related to handling action codes. - */ +/// Used to set and retrieve settings related to handling action codes. @objc(FIRActionCodeSettings) open class ActionCodeSettings: NSObject { - /** @property URL - @brief This URL represents the state/Continue URL in the form of a universal link. - @remarks This URL can should be constructed as a universal link that would either directly open - the app where the action code would be handled or continue to the app after the action code - is handled by Firebase. - */ + /// This URL represents the state/Continue URL in the form of a universal link. + /// + /// This URL can should be constructed as a universal link that would either directly open + /// the app where the action code would be handled or continue to the app after the action code + /// is handled by Firebase. @objc(URL) open var url: URL? - /** @property handleCodeInApp - @brief Indicates whether the action code link will open the app directly or after being - redirected from a Firebase owned web widget. - */ + /// Indicates whether the action code link will open the app directly or after being + /// redirected from a Firebase owned web widget. @objc open var handleCodeInApp: Bool = false - /** @property iOSBundleID - @brief The iOS bundle ID, if available. The default value is the current app's bundle ID. - */ + /// The iOS bundle ID, if available. The default value is the current app's bundle ID. @objc open var iOSBundleID: String? - /** @property androidPackageName - @brief The Android package name, if available. - */ + /// The Android package name, if available. @objc open var androidPackageName: String? - /** @property androidMinimumVersion - @brief The minimum Android version supported, if available. - */ + /// The minimum Android version supported, if available. @objc open var androidMinimumVersion: String? - /** @property androidInstallIfNotAvailable - @brief Indicates whether the Android app should be installed on a device where it is not - available. - */ + /// Indicates whether the Android app should be installed on a device where it is not available. @objc open var androidInstallIfNotAvailable: Bool = false - /** @property dynamicLinkDomain - @brief The Firebase Dynamic Link domain used for out of band code flow. - */ + /// The Firebase Dynamic Link domain used for out of band code flow. @objc open var dynamicLinkDomain: String? - /** @fn - @brief Sets the iOS bundle Id. - */ - + /// Sets the iOS bundle ID. @objc override public init() { iOSBundleID = Bundle.main.bundleIdentifier } - /** @fn - @brief Sets the Android package name, the flag to indicate whether or not to install the app - and the minimum Android version supported. - @param androidPackageName The Android package name. - @param installIfNotAvailable Indicates whether or not the app should be installed if not - available. - @param minimumVersion The minimum version of Android supported. - @remarks If installIfNotAvailable is set to YES and the link is opened on an android device, it - will try to install the app if not already available. Otherwise the web URL is used. - */ + /// Sets the Android package name, the flag to indicate whether or not to install the app, + /// and the minimum Android version supported. + /// + /// If `installIfNotAvailable` is set to `true` and the link is opened on an android device, it + /// will try to install the app if not already available. Otherwise the web URL is used. + /// - Parameters: + /// - androidPackageName: The Android package name. + /// - installIfNotAvailable: Indicates whether or not the app should be installed if not + /// available. + /// - minimumVersion: The minimum version of Android supported. @objc open func setAndroidPackageName(_ androidPackageName: String, installIfNotAvailable: Bool, minimumVersion: String?) { @@ -84,6 +65,7 @@ import Foundation androidMinimumVersion = minimumVersion } + /// Sets the iOS bundle ID. open func setIOSBundleID(_ bundleID: String) { iOSBundleID = bundleID } diff --git a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeURL.swift b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeURL.swift index 48c99710c31..29bb8070c1a 100644 --- a/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeURL.swift +++ b/FirebaseAuth/Sources/Swift/ActionCode/ActionCodeURL.swift @@ -14,41 +14,29 @@ import Foundation -/** @class FIRActionCodeURL - @brief This class will allow developers to easily extract information about out of band links. - */ +/// This class will allow developers to easily extract information about out of band links. @objc(FIRActionCodeURL) open class ActionCodeURL: NSObject { - /** @property APIKey - @brief Returns the API key from the link. nil, if not provided. - */ + /// Returns the API key from the link. nil, if not provided. @objc(APIKey) public let apiKey: String? - /** @property operation - @brief Returns the mode of oob action. The property will be of `FIRActionCodeOperation` type. - It will return `FIRActionCodeOperationUnknown` if no oob action is provided. - */ + /// Returns the mode of oob action. + /// + /// The property will be of `ActionCodeOperation` type. + /// It will return `.unknown` if no oob action is provided. @objc public let operation: ActionCodeOperation - /** @property code - @brief Returns the email action code from the link. nil, if not provided. - */ + /// Returns the email action code from the link. nil, if not provided. @objc public let code: String? - /** @property continueURL - @brief Returns the continue URL from the link. nil, if not provided. - */ + /// Returns the continue URL from the link. nil, if not provided. @objc public let continueURL: URL? - /** @property languageCode - @brief Returns the language code from the link. nil, if not provided. - */ + /// Returns the language code from the link. nil, if not provided. @objc public let languageCode: String? - /** @fn actionCodeURLWithLink: - @brief Construct an `ActionCodeURL` from an out of band link (e.g. email link). - @param link The oob link string used to construct the action code URL. - @return The `ActionCodeURL` object constructed based on the oob link provided. - */ + /// Construct an `ActionCodeURL` from an out of band link (e.g. email link). + /// - Parameter link: The oob link string used to construct the action code URL. + /// - Returns: The ActionCodeURL object constructed based on the oob link provided. @objc(actionCodeURLWithLink:) public init?(link: String) { var queryItems = ActionCodeURL.parseURL(link) if queryItems.count == 0 { diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 279ba10a416..a67ebe77c2a 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -82,9 +82,12 @@ import FirebaseCoreExtension @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension Auth: AuthInterop { + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// + /// This method is not for public use. It is for Firebase clients of AuthInterop. @objc(getTokenForcingRefresh:withCallback:) - open func getToken(forcingRefresh forceRefresh: Bool, - completion callback: @escaping (String?, Error?) -> Void) { + public func getToken(forcingRefresh forceRefresh: Bool, + completion callback: @escaping (String?, Error?) -> Void) { kAuthGlobalWorkQueue.async { [weak self] in if let strongSelf = self { // Enable token auto-refresh if not already enabled. @@ -134,22 +137,22 @@ extension Auth: AuthInterop { } } + /// Get the current Auth user's UID. Returns nil if there is no user signed in. + /// + /// This method is not for public use. It is for Firebase clients of AuthInterop. open func getUserID() -> String? { return currentUser?.uid } } -/** @class Auth - @brief Manages authentication for Firebase apps. - @remarks This class is thread-safe. - */ +/// Manages authentication for Firebase apps. +/// +/// This class is thread-safe. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRAuth) open class Auth: NSObject { - /** @fn auth - @brief Gets the auth object for the default Firebase app. - @remarks The default Firebase app must have already been configured or an exception will be - raised. - */ + /// Gets the auth object for the default Firebase app. + /// + /// The default Firebase app must have already been configured or an exception will be raised. @objc open class func auth() -> Auth { guard let defaultApp = FirebaseApp.app() else { fatalError("The default FirebaseApp instance must be configured before the default Auth " + @@ -161,32 +164,25 @@ extension Auth: AuthInterop { return auth(app: defaultApp) } - /** @fn authWithApp: - @brief Gets the auth object for a `FirebaseApp`. - - @param app The app for which to retrieve the associated `Auth` instance. - @return The `Auth` instance associated with the given app. - */ + /// Gets the auth object for a `FirebaseApp`. + /// - Parameter app: The app for which to retrieve the associated `Auth` instance. + /// - Returns: The `Auth` instance associated with the given app. @objc open class func auth(app: FirebaseApp) -> Auth { return ComponentType.instance(for: AuthProvider.self, in: app.container).auth() } - /** @property app - @brief Gets the `FirebaseApp` object that this auth object is connected to. - */ + /// Gets the `FirebaseApp` object that this auth object is connected to. @objc public internal(set) weak var app: FirebaseApp? - /** @property currentUser - @brief Synchronously gets the cached current user, or null if there is none. - */ + /// Synchronously gets the cached current user, or null if there is none. @objc public internal(set) var currentUser: User? - /** @property languageCode - @brief The current user language code. This property can be set to the app's current language by - calling `useAppLanguage()`. - - @remarks The string used to set this property must be a language code that follows BCP 47. - */ + /// The current user language code. + /// + /// This property can be set to the app's current language by + /// calling `useAppLanguage()`. + /// + /// The string used to set this property must be a language code that follows BCP 47. @objc open var languageCode: String? { get { kAuthGlobalWorkQueue.sync { @@ -200,42 +196,34 @@ extension Auth: AuthInterop { } } - /** @property settings - @brief Contains settings related to the auth object. - */ + /// Contains settings related to the auth object. @NSCopying @objc open var settings: AuthSettings? - /** @property userAccessGroup - @brief The current user access group that the Auth instance is using. Default is nil. - */ + /// The current user access group that the Auth instance is using. + /// + /// Default is `nil`. @objc public internal(set) var userAccessGroup: String? - /** @property shareAuthStateAcrossDevices - @brief Contains shareAuthStateAcrossDevices setting related to the auth object. - @remarks If userAccessGroup is not set, setting shareAuthStateAcrossDevices will - have no effect. You should set shareAuthStateAcrossDevices to it's desired - state and then set the userAccessGroup after. - */ + /// Contains shareAuthStateAcrossDevices setting related to the auth object. + /// + /// If userAccessGroup is not set, setting shareAuthStateAcrossDevices will + /// have no effect. You should set shareAuthStateAcrossDevices to it's desired + /// state and then set the userAccessGroup after. @objc open var shareAuthStateAcrossDevices: Bool = false - /** @property tenantID - @brief The tenant ID of the auth instance. nil if none is available. - */ + /// The tenant ID of the auth instance. `nil` if none is available. @objc open var tenantID: String? - /** - * @property customAuthDomain - * @brief The custom authentication domain used to handle all sign-in redirects. End-users will see - * this domain when signing in. This domain must be allowlisted in the Firebase Console. - */ + /// The custom authentication domain used to handle all sign-in redirects. + /// End-users will see + /// this domain when signing in. This domain must be allowlisted in the Firebase Console. @objc open var customAuthDomain: String? - /** @fn updateCurrentUser:completion: - @brief Sets the `currentUser` on the receiver to the provided user object. - @param user The user object to be set as the current user of the calling Auth instance. - @param completion Optionally; a block invoked after the user of the calling Auth instance has - been updated or an error was encountered. - */ + /// Sets the `currentUser` on the receiver to the provided user object. + /// - Parameters: + /// - user: The user object to be set as the current user of the calling Auth instance. + /// - completion: Optionally; a block invoked after the user of the calling Auth instance has + /// been updated or an error was encountered. @objc open func updateCurrentUser(_ user: User?, completion: ((Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { guard let user else { @@ -268,12 +256,10 @@ extension Auth: AuthInterop { } } - /** @fn updateCurrentUser:completion: - @brief Sets the `currentUser` on the receiver to the provided user object. - @param user The user object to be set as the current user of the calling Auth instance. - @param completion Optionally; a block invoked after the user of the calling Auth instance has - been updated or an error was encountered. - */ + /// Sets the `currentUser` on the receiver to the provided user object. + /// - Parameter user: The user object to be set as the current user of the calling Auth instance. + /// - Parameter completion: Optionally; a block invoked after the user of the calling Auth + /// instance has been updated or an error was encountered. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func updateCurrentUser(_ user: User) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -287,22 +273,18 @@ extension Auth: AuthInterop { } } - /** @fn fetchSignInMethodsForEmail:completion: - @brief [Deprecated] Fetches the list of all sign-in methods previously used for the provided - email address. This method returns an empty list when [Email Enumeration - Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled, irrespective of the number of authentication methods available for the given email. - @param email The email address for which to obtain a list of sign-in methods. - @param completion Optionally; a block which is invoked when the list of sign in methods for the - specified email address is ready or an error was encountered. Invoked asynchronously on the - main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - - @remarks See @c AuthErrors for a list of error codes that are common to all API methods. - */ + /// [Deprecated] Fetches the list of all sign-in methods previously used for the provided + /// email address. This method returns an empty list when [Email Enumeration + /// Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled, irrespective of the number of authentication methods available for the given + /// email. + /// + /// Possible error codes: `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// + /// - Parameter email: The email address for which to obtain a list of sign-in methods. + /// - Parameter completion: Optionally; a block which is invoked when the list of sign in methods + /// for the specified email address is ready or an error was encountered. Invoked asynchronously + /// on the main thread in the future. @available( *, deprecated, @@ -325,19 +307,16 @@ extension Auth: AuthInterop { } } - /** @fn fetchSignInMethodsForEmail:completion: - @brief [Deprecated] Fetches the list of all sign-in methods previously used for the provided - email address. This method returns an empty list when [Email Enumeration - Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled, irrespective of the number of authentication methods available for the given email. - @param email The email address for which to obtain a list of sign-in methods. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - - @remarks See @c AuthErrors for a list of error codes that are common to all API methods. - */ + /// [Deprecated] Fetches the list of all sign-in methods previously used for the provided + /// email address. This method returns an empty list when [Email Enumeration + /// Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled, irrespective of the number of authentication methods available for the given + /// email. + /// + /// Possible error codes: `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// + /// - Parameter email: The email address for which to obtain a list of sign-in methods. + /// - Returns: List of sign-in methods @available( *, deprecated, @@ -355,29 +334,25 @@ extension Auth: AuthInterop { } } - /** @fn signInWithEmail:password:completion: - @brief Signs in using an email address and password. When [Email Enumeration - Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled, this method fails with FIRAuthErrorCodeInvalidCredentials in case of an invalid - email/password. - - @param email The user's email address. - @param password The user's password. - @param completion Optionally; a block which is invoked when the sign in flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates that email and password - accounts are not enabled. Enable them in the Auth section of the - Firebase console. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeWrongPassword` - Indicates the user attempted - sign in with an incorrect password. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Signs in using an email address and password. + /// + /// When [Email Enumeration + /// Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled, this method fails with an error in case of an invalid + /// email/password. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password + /// accounts are not enabled. Enable them in the Auth section of the + /// Firebase console. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted + /// sign in with an incorrect password. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// - Parameter email: The user's email address. + /// - Parameter password: The user's password. + /// - Parameter completion: Optionally; a block which is invoked when the sign in flow finishes, + /// or is canceled. Invoked asynchronously on the main thread in the future. @objc open func signIn(withEmail email: String, password: String, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { @@ -397,18 +372,23 @@ extension Auth: AuthInterop { } } - /** @fn signInWithEmail:password:callback: - @brief Signs in using an email address and password. When [Email Enumeration - Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled, this method fails with FIRAuthErrorCodeInvalidCredentials in case of an invalid - email/password. - @param email The user's email address. - @param password The user's password. - @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked - asynchronously on the global auth work queue in the future. - @remarks This is the internal counterpart of this method, which uses a callback that does not - update the current user. - */ + /// Signs in using an email address and password. + /// + /// When [Email Enumeration + /// Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled, this method throws in case of an invalid email/password. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password + /// accounts are not enabled. Enable them in the Auth section of the + /// Firebase console. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted + /// sign in with an incorrect password. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// - Parameter email: The user's email address. + /// - Parameter password: The user's password. + /// - Returns: The signed in user. func internalSignInUser(withEmail email: String, password: String) async throws -> User { let request = VerifyPasswordRequest(email: email, @@ -431,24 +411,19 @@ extension Auth: AuthInterop { ) } - /** @fn signInWithEmail:password:completion: - @brief Signs in using an email address and password. - - @param email The user's email address. - @param password The user's password. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates that email and password - accounts are not enabled. Enable them in the Auth section of the - Firebase console. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeWrongPassword` - Indicates the user attempted - sign in with an incorrect password. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Signs in using an email address and password. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password + /// accounts are not enabled. Enable them in the Auth section of the + /// Firebase console. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted + /// sign in with an incorrect password. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// - Parameter email: The user's email address. + /// - Parameter password: The user's password. + /// - Returns: The `AuthDataResult` after a successful signin. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func signIn(withEmail email: String, password: String) async throws -> AuthDataResult { @@ -463,24 +438,20 @@ extension Auth: AuthInterop { } } - /** @fn signInWithEmail:link:completion: - @brief Signs in using an email address and email sign-in link. - - @param email The user's email address. - @param link The email sign-in link. - @param completion Optionally; a block which is invoked when the sign in flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates that email and email sign-in link - accounts are not enabled. Enable them in the Auth section of the - Firebase console. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is invalid. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Signs in using an email address and email sign-in link. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password + /// accounts are not enabled. Enable them in the Auth section of the + /// Firebase console. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted + /// sign in with an incorrect password. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// - Parameter email: The user's email address. + /// - Parameter link: The email sign-in link. + /// - Parameter completion: Optionally; a block which is invoked when the sign in flow finishes, + /// or is canceled. Invoked asynchronously on the main thread in the future. @objc open func signIn(withEmail email: String, link: String, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { @@ -499,24 +470,18 @@ extension Auth: AuthInterop { } } - /** @fn signInWithEmail:link:completion: - @brief Signs in using an email address and email sign-in link. - - @param email The user's email address. - @param link The email sign-in link. - @param completion Optionally; a block which is invoked when the sign in flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates that email and email sign-in link - accounts are not enabled. Enable them in the Auth section of the - Firebase console. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is invalid. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Signs in using an email address and email sign-in link. + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password + /// accounts are not enabled. Enable them in the Auth section of the + /// Firebase console. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted + /// sign in with an incorrect password. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// - Parameter email: The user's email address. + /// - Parameter link: The email sign-in link. + /// - Returns: The `AuthDataResult` after a successful signin. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func signIn(withEmail email: String, link: String) async throws -> AuthDataResult { return try await withCheckedThrowingContinuation { continuation in @@ -531,54 +496,40 @@ extension Auth: AuthInterop { } #if os(iOS) + /// Signs in using the provided auth provider instance. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password + /// accounts are not enabled. Enable them in the Auth section of the + /// Firebase console. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted + /// sign in with an incorrect password. + /// * `AuthErrorCodeWebNetworkRequestFailed` - Indicates that a network request within a + /// SFSafariViewController or WKWebView failed. + /// * `AuthErrorCodeWebInternalError` - Indicates that an internal error occurred within a + /// SFSafariViewController or WKWebView. + /// * `AuthErrorCodeWebSignInUserInteractionFailure` - Indicates a general failure during + /// a web sign-in flow. + /// * `AuthErrorCodeWebContextAlreadyPresented` - Indicates that an attempt was made to + /// present a new web context while one was already being presented. + /// * `AuthErrorCodeWebContextCancelled` - Indicates that the URL presentation was + /// cancelled prematurely by the user. + /// * `AuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted + /// by the credential (e.g. the email in a Facebook access token) is already in use by an + /// existing account, that cannot be authenticated with this sign-in method. Call + /// fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + /// the sign-in providers returned. This error will only be thrown if the "One account per + /// email address" setting is enabled in the Firebase console, under Auth settings. + /// - Parameter provider: An instance of an auth provider used to initiate the sign-in flow. + /// - Parameter uiDelegate: Optionally an instance of a class conforming to the AuthUIDelegate + /// protocol, this is used for presenting the web context. If nil, a default AuthUIDelegate + /// will be used. + /// - Parameter completion: Optionally; a block which is invoked when the sign in flow finishes, + /// or is canceled. Invoked asynchronously on the main thread in the future. @available(tvOS, unavailable) @available(macOS, unavailable) @available(watchOS, unavailable) - /** @fn signInWithProvider:UIDelegate:completion: - @brief Signs in using the provided auth provider instance. - This method is available on iOS, macOS Catalyst, and tvOS only. - - @param provider An instance of an auth provider used to initiate the sign-in flow. - @param uiDelegate Optionally an instance of a class conforming to the AuthUIDelegate - protocol, this is used for presenting the web context. If nil, a default AuthUIDelegate - will be used. - @param completion Optionally; a block which is invoked when the sign in flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: -
    -
  • @c AuthErrorCodeOperationNotAllowed - Indicates that email and password - accounts are not enabled. Enable them in the Auth section of the - Firebase console. -
  • -
  • @c AuthErrorCodeUserDisabled - Indicates the user's account is disabled. -
  • -
  • @c AuthErrorCodeWebNetworkRequestFailed - Indicates that a network request within a - SFSafariViewController or WKWebView failed. -
  • -
  • @c AuthErrorCodeWebInternalError - Indicates that an internal error occurred within a - SFSafariViewController or WKWebView. -
  • -
  • @c AuthErrorCodeWebSignInUserInteractionFailure - Indicates a general failure during - a web sign-in flow. -
  • -
  • @c AuthErrorCodeWebContextAlreadyPresented - Indicates that an attempt was made to - present a new web context while one was already being presented. -
  • -
  • @c AuthErrorCodeWebContextCancelled - Indicates that the URL presentation was - cancelled prematurely by the user. -
  • -
  • @c AuthErrorCodeAccountExistsWithDifferentCredential - Indicates the email asserted - by the credential (e.g. the email in a Facebook access token) is already in use by an - existing account, that cannot be authenticated with this sign-in method. Call - fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of - the sign-in providers returned. This error will only be thrown if the "One account per - email address" setting is enabled in the Firebase console, under Auth settings. -
  • -
- - @remarks See @c AuthErrors for a list of error codes that are common to all API methods. - */ @objc(signInWithProvider:UIDelegate:completion:) open func signIn(with provider: FederatedAuthProvider, uiDelegate: AuthUIDelegate?, @@ -600,49 +551,36 @@ extension Auth: AuthInterop { } } - /** @fn signInWithProvider:UIDelegate:completion: - @brief Signs in using the provided auth provider instance. - This method is available on iOS, macOS Catalyst, and tvOS only. - - @param provider An instance of an auth provider used to initiate the sign-in flow. - @param uiDelegate Optionally an instance of a class conforming to the AuthUIDelegate - protocol, this is used for presenting the web context. If nil, a default AuthUIDelegate - will be used. - - @remarks Possible error codes: -
    -
  • @c AuthErrorCodeOperationNotAllowed - Indicates that email and password - accounts are not enabled. Enable them in the Auth section of the - Firebase console. -
  • -
  • @c AuthErrorCodeUserDisabled - Indicates the user's account is disabled. -
  • -
  • @c AuthErrorCodeWebNetworkRequestFailed - Indicates that a network request within a - SFSafariViewController or WKWebView failed. -
  • -
  • @c AuthErrorCodeWebInternalError - Indicates that an internal error occurred within a - SFSafariViewController or WKWebView. -
  • -
  • @c AuthErrorCodeWebSignInUserInteractionFailure - Indicates a general failure during - a web sign-in flow. -
  • -
  • @c AuthErrorCodeWebContextAlreadyPresented - Indicates that an attempt was made to - present a new web context while one was already being presented. -
  • -
  • @c AuthErrorCodeWebContextCancelled - Indicates that the URL presentation was - cancelled prematurely by the user. -
  • -
  • @c AuthErrorCodeAccountExistsWithDifferentCredential - Indicates the email asserted - by the credential (e.g. the email in a Facebook access token) is already in use by an - existing account, that cannot be authenticated with this sign-in method. Call - fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of - the sign-in providers returned. This error will only be thrown if the "One account per - email address" setting is enabled in the Firebase console, under Auth settings. -
  • -
- - @remarks See @c AuthErrors for a list of error codes that are common to all API methods. - */ + /// Signs in using the provided auth provider instance. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password + /// accounts are not enabled. Enable them in the Auth section of the + /// Firebase console. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted + /// sign in with an incorrect password. + /// * `AuthErrorCodeWebNetworkRequestFailed` - Indicates that a network request within a + /// SFSafariViewController or WKWebView failed. + /// * `AuthErrorCodeWebInternalError` - Indicates that an internal error occurred within a + /// SFSafariViewController or WKWebView. + /// * `AuthErrorCodeWebSignInUserInteractionFailure` - Indicates a general failure during + /// a web sign-in flow. + /// * `AuthErrorCodeWebContextAlreadyPresented` - Indicates that an attempt was made to + /// present a new web context while one was already being presented. + /// * `AuthErrorCodeWebContextCancelled` - Indicates that the URL presentation was + /// cancelled prematurely by the user. + /// * `AuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted + /// by the credential (e.g. the email in a Facebook access token) is already in use by an + /// existing account, that cannot be authenticated with this sign-in method. Call + /// fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + /// the sign-in providers returned. This error will only be thrown if the "One account per + /// email address" setting is enabled in the Firebase console, under Auth settings. + /// - Parameter provider: An instance of an auth provider used to initiate the sign-in flow. + /// - Parameter uiDelegate: Optionally an instance of a class conforming to the AuthUIDelegate + /// protocol, this is used for presenting the web context. If nil, a default AuthUIDelegate + /// will be used. + /// - Returns: The `AuthDataResult` after the successful signin. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @available(tvOS, unavailable) @available(macOS, unavailable) @@ -662,44 +600,38 @@ extension Auth: AuthInterop { } #endif // iOS - /** @fn signInWithCredential:completion: - @brief Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook - login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional - identity provider data. - - @param credential The credential supplied by the IdP. - @param completion Optionally; a block which is invoked when the sign in flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. - This could happen if it has expired or it is malformed. - + `AuthErrorCodeOperationNotAllowed` - Indicates that accounts - with the identity provider represented by the credential are not enabled. - Enable them in the Auth section of the Firebase console. - + `AuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted - by the credential (e.g. the email in a Facebook access token) is already in use by an - existing account, that cannot be authenticated with this sign-in method. Call - fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of - the sign-in providers returned. This error will only be thrown if the "One account per - email address" setting is enabled in the Firebase console, under Auth settings. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeWrongPassword` - Indicates the user attempted sign in with an - incorrect password, if credential is of the type EmailPasswordAuthCredential. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - + `AuthErrorCodeMissingVerificationID` - Indicates that the phone auth credential was - created with an empty verification ID. - + `AuthErrorCodeMissingVerificationCode` - Indicates that the phone auth credential - was created with an empty verification code. - + `AuthErrorCodeInvalidVerificationCode` - Indicates that the phone auth credential - was created with an invalid verification Code. - + `AuthErrorCodeInvalidVerificationID` - Indicates that the phone auth credential was - created with an invalid verification ID. - + `AuthErrorCodeSessionExpired` - Indicates that the SMS code has expired. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods - */ + /// Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook + /// login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional + /// identity provider data. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + /// This could happen if it has expired or it is malformed. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts + /// with the identity provider represented by the credential are not enabled. + /// Enable them in the Auth section of the Firebase console. + /// * `AuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted + /// by the credential (e.g. the email in a Facebook access token) is already in use by an + /// existing account, that cannot be authenticated with this sign-in method. Call + /// fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + /// the sign-in providers returned. This error will only be thrown if the "One account per + /// email address" setting is enabled in the Firebase console, under Auth settings. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted sign in with an + /// incorrect password, if credential is of the type EmailPasswordAuthCredential. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// * `AuthErrorCodeMissingVerificationID` - Indicates that the phone auth credential was + /// created with an empty verification ID. + /// * `AuthErrorCodeMissingVerificationCode` - Indicates that the phone auth credential + /// was created with an empty verification code. + /// * `AuthErrorCodeInvalidVerificationCode` - Indicates that the phone auth credential + /// was created with an invalid verification Code. + /// * `AuthErrorCodeInvalidVerificationID` - Indicates that the phone auth credential was + /// created with an invalid verification ID. + /// * `AuthErrorCodeSessionExpired` - Indicates that the SMS code has expired. + /// - Parameter credential: The credential supplied by the IdP. + /// - Parameter completion: Optionally; a block which is invoked when the sign in flow finishes, + /// or is canceled. Invoked asynchronously on the main thread in the future. @objc(signInWithCredential:completion:) open func signIn(with credential: AuthCredential, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { @@ -717,44 +649,37 @@ extension Auth: AuthInterop { } } - /** @fn signInWithCredential:completion: - @brief Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook - login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional - identity provider data. - - @param credential The credential supplied by the IdP. - @param completion Optionally; a block which is invoked when the sign in flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. - This could happen if it has expired or it is malformed. - + `AuthErrorCodeOperationNotAllowed` - Indicates that accounts - with the identity provider represented by the credential are not enabled. - Enable them in the Auth section of the Firebase console. - + `AuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted - by the credential (e.g. the email in a Facebook access token) is already in use by an - existing account, that cannot be authenticated with this sign-in method. Call - fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of - the sign-in providers returned. This error will only be thrown if the "One account per - email address" setting is enabled in the Firebase console, under Auth settings. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeWrongPassword` - Indicates the user attempted sign in with an - incorrect password, if credential is of the type EmailPasswordAuthCredential. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - + `AuthErrorCodeMissingVerificationID` - Indicates that the phone auth credential was - created with an empty verification ID. - + `AuthErrorCodeMissingVerificationCode` - Indicates that the phone auth credential - was created with an empty verification code. - + `AuthErrorCodeInvalidVerificationCode` - Indicates that the phone auth credential - was created with an invalid verification Code. - + `AuthErrorCodeInvalidVerificationID` - Indicates that the phone auth credential was - created with an invalid verification ID. - + `AuthErrorCodeSessionExpired` - Indicates that the SMS code has expired. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods - */ + /// Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook + /// login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional + /// identity provider data. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + /// This could happen if it has expired or it is malformed. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts + /// with the identity provider represented by the credential are not enabled. + /// Enable them in the Auth section of the Firebase console. + /// * `AuthErrorCodeAccountExistsWithDifferentCredential` - Indicates the email asserted + /// by the credential (e.g. the email in a Facebook access token) is already in use by an + /// existing account, that cannot be authenticated with this sign-in method. Call + /// fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + /// the sign-in providers returned. This error will only be thrown if the "One account per + /// email address" setting is enabled in the Firebase console, under Auth settings. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted sign in with an + /// incorrect password, if credential is of the type EmailPasswordAuthCredential. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// * `AuthErrorCodeMissingVerificationID` - Indicates that the phone auth credential was + /// created with an empty verification ID. + /// * `AuthErrorCodeMissingVerificationCode` - Indicates that the phone auth credential + /// was created with an empty verification code. + /// * `AuthErrorCodeInvalidVerificationCode` - Indicates that the phone auth credential + /// was created with an invalid verification Code. + /// * `AuthErrorCodeInvalidVerificationID` - Indicates that the phone auth credential was + /// created with an invalid verification ID. + /// * `AuthErrorCodeSessionExpired` - Indicates that the SMS code has expired. + /// - Parameter credential: The credential supplied by the IdP. + /// - Returns: The `AuthDataResult` after the successful signin. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func signIn(with credential: AuthCredential) async throws -> AuthDataResult { @@ -769,21 +694,16 @@ extension Auth: AuthInterop { } } - /** @fn signInAnonymouslyWithCompletion: - @brief Asynchronously creates and becomes an anonymous user. - @param completion Optionally; a block which is invoked when the sign in finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks If there is already an anonymous user signed in, that user will be returned instead. - If there is any other existing user signed in, that user will be signed out. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates that anonymous accounts are - not enabled. Enable them in the Auth section of the Firebase console. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Asynchronously creates and becomes an anonymous user. + /// + /// If there is already an anonymous user signed in, that user will be returned instead. + /// If there is any other existing user signed in, that user will be signed out. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that anonymous accounts are + /// not enabled. Enable them in the Auth section of the Firebase console. + /// - Parameter completion: Optionally; a block which is invoked when the sign in finishes, or is + /// canceled. Invoked asynchronously on the main thread in the future. @objc open func signInAnonymously(completion: ((AuthDataResult?, Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { let decoratedCallback = self.signInFlowAuthDataResultCallback(byDecorating: completion) @@ -816,19 +736,15 @@ extension Auth: AuthInterop { } } - /** @fn signInAnonymouslyWithCompletion: - @brief Asynchronously creates and becomes an anonymous user. - - @remarks If there is already an anonymous user signed in, that user will be returned instead. - If there is any other existing user signed in, that user will be signed out. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates that anonymous accounts are - not enabled. Enable them in the Auth section of the Firebase console. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Asynchronously creates and becomes an anonymous user. + /// + /// If there is already an anonymous user signed in, that user will be returned instead. + /// If there is any other existing user signed in, that user will be signed out. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that anonymous accounts are + /// not enabled. Enable them in the Auth section of the Firebase console. + /// - Returns: The `AuthDataResult` after the successful signin. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult @objc open func signInAnonymously() async throws -> AuthDataResult { @@ -843,22 +759,16 @@ extension Auth: AuthInterop { } } - /** @fn signInWithCustomToken:completion: - @brief Asynchronously signs in to Firebase with the given Auth token. - - @param token A self-signed custom auth token. - @param completion Optionally; a block which is invoked when the sign in finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidCustomToken` - Indicates a validation error with - the custom token. - + `AuthErrorCodeCustomTokenMismatch` - Indicates the service account and the API key - belong to different projects. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Asynchronously signs in to Firebase with the given Auth token. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidCustomToken` - Indicates a validation error with + /// the custom token. + /// * `AuthErrorCodeCustomTokenMismatch` - Indicates the service account and the API key + /// belong to different projects. + /// - Parameter token: A self-signed custom auth token. + /// - Parameter completion: Optionally; a block which is invoked when the sign in finishes, or is + /// canceled. Invoked asynchronously on the main thread in the future. @objc open func signIn(withCustomToken token: String, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { @@ -888,22 +798,15 @@ extension Auth: AuthInterop { } } - /** @fn signInWithCustomToken:completion: - @brief Asynchronously signs in to Firebase with the given Auth token. - - @param token A self-signed custom auth token. - @param completion Optionally; a block which is invoked when the sign in finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidCustomToken` - Indicates a validation error with - the custom token. - + `AuthErrorCodeCustomTokenMismatch` - Indicates the service account and the API key - belong to different projects. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Asynchronously signs in to Firebase with the given Auth token. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidCustomToken` - Indicates a validation error with + /// the custom token. + /// * `AuthErrorCodeCustomTokenMismatch` - Indicates the service account and the API key + /// belong to different projects. + /// - Parameter token: A self-signed custom auth token. + /// - Returns: The `AuthDataResult` after the successful signin. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func signIn(withCustomToken token: String) async throws -> AuthDataResult { @@ -918,28 +821,22 @@ extension Auth: AuthInterop { } } - /** @fn createUserWithEmail:password:completion: - @brief Creates and, on success, signs in a user with the given email address and password. - - @param email The user's email address. - @param password The user's desired password. - @param completion Optionally; a block which is invoked when the sign up flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - + `AuthErrorCodeEmailAlreadyInUse` - Indicates the email used to attempt sign up - already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user - used, and prompt the user to sign in with one of those. - + `AuthErrorCodeOperationNotAllowed` - Indicates that email and password accounts - are not enabled. Enable them in the Auth section of the Firebase console. - + `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is - considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo - dictionary object will contain more detailed explanation that can be shown to the user. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Creates and, on success, signs in a user with the given email address and password. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email used to attempt sign up + /// already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user + /// used, and prompt the user to sign in with one of those. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password accounts + /// are not enabled. Enable them in the Auth section of the Firebase console. + /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + /// considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo + /// dictionary object will contain more detailed explanation that can be shown to the user. + /// - Parameter email: The user's email address. + /// - Parameter password: The user's desired password. + /// - Parameter completion: Optionally; a block which is invoked when the sign up flow finishes, + /// or is canceled. Invoked asynchronously on the main thread in the future. @objc open func createUser(withEmail email: String, password: String, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { @@ -1011,28 +908,21 @@ extension Auth: AuthInterop { } } - /** @fn createUserWithEmail:password:completion: - @brief Creates and, on success, signs in a user with the given email address and password. - - @param email The user's email address. - @param password The user's desired password. - @param completion Optionally; a block which is invoked when the sign up flow finishes, or is - canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - + `AuthErrorCodeEmailAlreadyInUse` - Indicates the email used to attempt sign up - already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user - used, and prompt the user to sign in with one of those. - + `AuthErrorCodeOperationNotAllowed` - Indicates that email and password accounts - are not enabled. Enable them in the Auth section of the Firebase console. - + `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is - considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo - dictionary object will contain more detailed explanation that can be shown to the user. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Creates and, on success, signs in a user with the given email address and password. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email used to attempt sign up + /// already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user + /// used, and prompt the user to sign in with one of those. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that email and password accounts + /// are not enabled. Enable them in the Auth section of the Firebase console. + /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + /// considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo + /// dictionary object will contain more detailed explanation that can be shown to the user. + /// - Parameter email: The user's email address. + /// - Parameter password: The user's desired password. + /// - Returns: The `AuthDataResult` after the successful signin. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func createUser(withEmail email: String, password: String) async throws -> AuthDataResult { @@ -1047,25 +937,20 @@ extension Auth: AuthInterop { } } - /** @fn confirmPasswordResetWithCode:newPassword:completion: - @brief Resets the password given a code sent to the user outside of the app and a new password - for the user. - - @param newPassword The new password. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is - considered too weak. - + `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled sign - in with the specified identity provider. - + `AuthErrorCodeExpiredActionCode` - Indicates the OOB code is expired. - + `AuthErrorCodeInvalidActionCode` - Indicates the OOB code is invalid. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Resets the password given a code sent to the user outside of the app and a new password + /// for the user. + /// + /// Possible error codes: + /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + /// considered too weak. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled sign + /// in with the specified identity provider. + /// * `AuthErrorCodeExpiredActionCode` - Indicates the OOB code is expired. + /// * `AuthErrorCodeInvalidActionCode` - Indicates the OOB code is invalid. + /// - Parameter code: The reset code. + /// - Parameter newPassword: The new password. + /// - Parameter completion: Optionally; a block which is invoked when the request finishes. + /// Invoked asynchronously on the main thread in the future. @objc open func confirmPasswordReset(withCode code: String, newPassword: String, completion: @escaping (Error?) -> Void) { kAuthGlobalWorkQueue.async { @@ -1076,25 +961,18 @@ extension Auth: AuthInterop { } } - /** @fn confirmPasswordResetWithCode:newPassword:completion: - @brief Resets the password given a code sent to the user outside of the app and a new password - for the user. - - @param newPassword The new password. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is - considered too weak. - + `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled sign - in with the specified identity provider. - + `AuthErrorCodeExpiredActionCode` - Indicates the OOB code is expired. - + `AuthErrorCodeInvalidActionCode` - Indicates the OOB code is invalid. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Resets the password given a code sent to the user outside of the app and a new password + /// for the user. + /// + /// Possible error codes: + /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + /// considered too weak. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled sign + /// in with the specified identity provider. + /// * `AuthErrorCodeExpiredActionCode` - Indicates the OOB code is expired. + /// * `AuthErrorCodeInvalidActionCode` - Indicates the OOB code is invalid. + /// - Parameter code: The reset code. + /// - Parameter newPassword: The new password. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func confirmPasswordReset(withCode code: String, newPassword: String) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -1108,13 +986,11 @@ extension Auth: AuthInterop { } } - /** @fn checkActionCode:completion: - @brief Checks the validity of an out of band code. - - @param code The out of band code to check validity. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - */ + /// Checks the validity of an out of band code. + /// - Parameter code: The out of band code to check validity. + /// - Parameter completion: Optionally; a block which is invoked when the request finishes. + /// Invoked + /// asynchronously on the main thread in the future. @objc open func checkActionCode(_ code: String, completion: @escaping (ActionCodeInfo?, Error?) -> Void) { kAuthGlobalWorkQueue.async { @@ -1140,13 +1016,9 @@ extension Auth: AuthInterop { } } - /** @fn checkActionCode:completion: - @brief Checks the validity of an out of band code. - - @param code The out of band code to check validity. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - */ + /// Checks the validity of an out of band code. + /// - Parameter code: The out of band code to check validity. + /// - Returns: An `ActionCodeInfo`. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func checkActionCode(_ code: String) async throws -> ActionCodeInfo { return try await withCheckedThrowingContinuation { continuation in @@ -1160,13 +1032,10 @@ extension Auth: AuthInterop { } } - /** @fn verifyPasswordResetCode:completion: - @brief Checks the validity of a verify password reset code. - - @param code The password reset code to be verified. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - */ + /// Checks the validity of a verify password reset code. + /// - Parameter code: The password reset code to be verified. + /// - Parameter completion: Optionally; a block which is invoked when the request finishes. + /// Invoked asynchronously on the main thread in the future. @objc open func verifyPasswordResetCode(_ code: String, completion: @escaping (String?, Error?) -> Void) { checkActionCode(code) { info, error in @@ -1178,13 +1047,9 @@ extension Auth: AuthInterop { } } - /** @fn verifyPasswordResetCode:completion: - @brief Checks the validity of a verify password reset code. - - @param code The password reset code to be verified. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - */ + /// Checks the validity of a verify password reset code. + /// - Parameter code: The password reset code to be verified. + /// - Returns: An email. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func verifyPasswordResetCode(_ code: String) async throws -> String { return try await withCheckedThrowingContinuation { continuation in @@ -1198,16 +1063,13 @@ extension Auth: AuthInterop { } } - /** @fn applyActionCode:completion: - @brief Applies out of band code. - - @param code The out of band code to be applied. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - - @remarks This method will not work for out of band codes which require an additional parameter, - such as password reset code. - */ + /// Applies out of band code. + /// + /// This method will not work for out of band codes which require an additional parameter, + /// such as password reset code. + /// - Parameter code: The out of band code to be applied. + /// - Parameter completion: Optionally; a block which is invoked when the request finishes. + /// Invoked asynchronously on the main thread in the future. @objc open func applyActionCode(_ code: String, completion: @escaping (Error?) -> Void) { kAuthGlobalWorkQueue.async { let request = SetAccountInfoRequest(requestConfiguration: self.requestConfiguration) @@ -1216,16 +1078,11 @@ extension Auth: AuthInterop { } } - /** @fn applyActionCode:completion: - @brief Applies out of band code. - - @param code The out of band code to be applied. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - - @remarks This method will not work for out of band codes which require an additional parameter, - such as password reset code. - */ + /// Applies out of band code. + /// + /// This method will not work for out of band codes which require an additional parameter, + /// such as password reset code. + /// - Parameter code: The out of band code to be applied. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func applyActionCode(_ code: String) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -1239,58 +1096,56 @@ extension Auth: AuthInterop { } } - /** @fn sendPasswordResetWithEmail:completion: - @brief Initiates a password reset for the given email address. This method does not throw an - error when there's no user account with the given email address and [Email Enumeration - Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled. - - @param email The email address of the user. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - - */ + /// Initiates a password reset for the given email address. + /// + /// This method does not throw an + /// error when there's no user account with the given email address and [Email Enumeration + /// Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// - Parameter email: The email address of the user. + /// - Parameter completion: Optionally; a block which is invoked when the request finishes. + /// Invoked + /// asynchronously on the main thread in the future. @objc open func sendPasswordReset(withEmail email: String, completion: ((Error?) -> Void)? = nil) { sendPasswordReset(withEmail: email, actionCodeSettings: nil, completion: completion) } - /** @fn sendPasswordResetWithEmail:actionCodeSetting:completion: - @brief Initiates a password reset for the given email address and `ActionCodeSettings` object. - - @param email The email address of the user. - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - + `AuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when - `handleCodeInApp` is set to true. - + `AuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name - is missing when the `androidInstallApp` flag is set to true. - + `AuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the - continue URL is not allowlisted in the Firebase console. - + `AuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the - continue URL is not valid. - - */ + /// Initiates a password reset for the given email address and `ActionCodeSettings` object. + /// + /// This method does not throw an + /// error when there's no user account with the given email address and [Email Enumeration + /// Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// * `AuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when + /// `handleCodeInApp` is set to true. + /// * `AuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name + /// is missing when the `androidInstallApp` flag is set to true. + /// * `AuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the + /// continue URL is not allowlisted in the Firebase console. + /// * `AuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the + /// continue URL is not valid. + /// - Parameter email: The email address of the user. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. + /// - Parameter completion: Optionally; a block which is invoked when the request finishes. + /// Invoked asynchronously on the main thread in the future. @objc open func sendPasswordReset(withEmail email: String, actionCodeSettings: ActionCodeSettings?, completion: ((Error?) -> Void)? = nil) { @@ -1315,37 +1170,31 @@ extension Auth: AuthInterop { } } - /** @fn sendPasswordResetWithEmail:actionCodeSetting:completion: - @brief Initiates a password reset for the given email address and `ActionCodeSettings` object. - This method does not throw an - error when there's no user account with the given email address and [Email Enumeration - Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled. - - @param email The email address of the user. - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - + `AuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when - `handleCodeInApp` is set to true. - + `AuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name - is missing when the `androidInstallApp` flag is set to true. - + `AuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the - continue URL is not allowlisted in the Firebase console. - + `AuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the - continue URL is not valid. - - */ + /// Initiates a password reset for the given email address and `ActionCodeSettings` object. + /// + /// This method does not throw an + /// error when there's no user account with the given email address and [Email Enumeration + /// Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// * `AuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when + /// `handleCodeInApp` is set to true. + /// * `AuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name + /// is missing when the `androidInstallApp` flag is set to true. + /// * `AuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the + /// continue URL is not allowlisted in the Firebase console. + /// * `AuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the + /// continue URL is not valid. + /// - Parameter email: The email address of the user. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func sendPasswordReset(withEmail email: String, actionCodeSettings: ActionCodeSettings? = nil) async throws { @@ -1360,15 +1209,12 @@ extension Auth: AuthInterop { } } - /** @fn sendSignInLinkToEmail:actionCodeSettings:completion: - @brief Sends a sign in with email link to provided email address. - - @param email The email address of the user. - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - */ + /// Sends a sign in with email link to provided email address. + /// - Parameter email: The email address of the user. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. + /// - Parameter completion: Optionally; a block which is invoked when the request finishes. + /// Invoked asynchronously on the main thread in the future. @objc open func sendSignInLink(toEmail email: String, actionCodeSettings: ActionCodeSettings, completion: ((Error?) -> Void)? = nil) { @@ -1397,15 +1243,10 @@ extension Auth: AuthInterop { } } - /** @fn sendSignInLinkToEmail:actionCodeSettings:completion: - @brief Sends a sign in with email link to provided email address. - - @param email The email address of the user. - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - @param completion Optionally; a block which is invoked when the request finishes. Invoked - asynchronously on the main thread in the future. - */ + /// Sends a sign in with email link to provided email address. + /// - Parameter email: The email address of the user. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func sendSignInLink(toEmail email: String, actionCodeSettings: ActionCodeSettings) async throws { @@ -1420,20 +1261,12 @@ extension Auth: AuthInterop { } } - /** @fn signOut: - @brief Signs out the current user. - - @param error Optionally; if an error occurs, upon return contains an NSError object that - describes the problem; is nil otherwise. - @return @YES when the sign out request was successful. @NO otherwise. - - @remarks Possible error codes: - - + `AuthErrorCodeKeychainError` - Indicates an error occurred when accessing the - keychain. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo` - dictionary will contain more information about the error encountered. - - */ + /// Signs out the current user. + /// + /// Possible error codes: + /// * `AuthErrorCodeKeychainError` - Indicates an error occurred when accessing the + /// keychain. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo` + /// dictionary will contain more information about the error encountered. @objc(signOut:) open func signOut() throws { try kAuthGlobalWorkQueue.sync { guard self.currentUser != nil else { @@ -1443,12 +1276,9 @@ extension Auth: AuthInterop { } } - /** @fn isSignInWithEmailLink - @brief Checks if link is an email sign-in link. - - @param link The email sign-in link. - @return Returns true when the link passed matches the expected format of an email sign-in link. - */ + /// Checks if link is an email sign-in link. + /// - Parameter link: The email sign-in link. + /// - Returns: `true` when the link passed matches the expected format of an email sign-in link. @objc open func isSignIn(withEmailLink link: String) -> Bool { guard link.count > 0 else { return false @@ -1463,13 +1293,11 @@ extension Auth: AuthInterop { } #if os(iOS) && !targetEnvironment(macCatalyst) - /** @fn initializeRecaptchaConfigWithCompletion:completion: - @brief Initializes reCAPTCHA using the settings configured for the project or - tenant. - If you change the tenant ID of the `Auth` instance, the configuration will be - reloaded. - */ + /// Initializes reCAPTCHA using the settings configured for the project or tenant. + /// + /// If you change the tenant ID of the `Auth` instance, the configuration will be + /// reloaded. @objc(initializeRecaptchaConfigWithCompletion:) open func initializeRecaptchaConfig(completion: ((Error?) -> Void)?) { Task { @@ -1486,13 +1314,10 @@ extension Auth: AuthInterop { } } - /** @fn initializeRecaptchaConfig - @brief Initializes reCAPTCHA using the settings configured for the project or - tenant. - - If you change the tenant ID of the `Auth` instance, the configuration will be - reloaded. - */ + /// Initializes reCAPTCHA using the settings configured for the project or tenant. + /// + /// If you change the tenant ID of the `Auth` instance, the configuration will be + /// reloaded. open func initializeRecaptchaConfig() async throws { // Trigger recaptcha verification flow to initialize the recaptcha client and // config. Recaptcha token will be returned. @@ -1501,24 +1326,21 @@ extension Auth: AuthInterop { } #endif - /** @fn addAuthStateDidChangeListener: - @brief Registers a block as an "auth state did change" listener. To be invoked when: - - + The block is registered as a listener, - + A user with a different UID from the current user has signed in, or - + The current user has signed out. - - @param listener The block to be invoked. The block is always invoked asynchronously on the main - thread, even for it's initial invocation after having been added as a listener. - - @remarks The block is invoked immediately after adding it according to it's standard invocation - semantics, asynchronously on the main thread. Users should pay special attention to - making sure the block does not inadvertently retain objects which should not be retained by - the long-lived block. The block itself will be retained by `Auth` until it is - unregistered or until the `Auth` instance is otherwise deallocated. - - @return A handle useful for manually unregistering the block as a listener. - */ + /// Registers a block as an "auth state did change" listener. + /// + /// To be invoked when: + /// * The block is registered as a listener, + /// * A user with a different UID from the current user has signed in, or + /// * The current user has signed out. + /// + /// The block is invoked immediately after adding it according to it's standard invocation + /// semantics, asynchronously on the main thread. Users should pay special attention to + /// making sure the block does not inadvertently retain objects which should not be retained by + /// the long-lived block. The block itself will be retained by `Auth` until it is + /// unregistered or until the `Auth` instance is otherwise deallocated. + /// - Parameter listener: The block to be invoked. The block is always invoked asynchronously on + /// the main thread, even for it's initial invocation after having been added as a listener. + /// - Returns: A handle useful for manually unregistering the block as a listener. @objc(addAuthStateDidChangeListener:) open func addStateDidChangeListener(_ listener: @escaping (Auth, User?) -> Void) -> NSObjectProtocol { @@ -1534,11 +1356,8 @@ extension Auth: AuthInterop { } } - /** @fn removeAuthStateDidChangeListener: - @brief Unregisters a block as an "auth state did change" listener. - - @param listenerHandle The handle for the listener. - */ + /// Unregisters a block as an "auth state did change" listener. + /// - Parameter listenerHandle: The handle for the listener. @objc(removeAuthStateDidChangeListener:) open func removeStateDidChangeListener(_ listenerHandle: NSObjectProtocol) { NotificationCenter.default.removeObserver(listenerHandle) @@ -1547,25 +1366,22 @@ extension Auth: AuthInterop { listenerHandles.remove(listenerHandle) } - /** @fn addIDTokenDidChangeListener: - @brief Registers a block as an "ID token did change" listener. To be invoked when: - - + The block is registered as a listener, - + A user with a different UID from the current user has signed in, - + The ID token of the current user has been refreshed, or - + The current user has signed out. - - @param listener The block to be invoked. The block is always invoked asynchronously on the main - thread, even for it's initial invocation after having been added as a listener. - - @remarks The block is invoked immediately after adding it according to it's standard invocation - semantics, asynchronously on the main thread. Users should pay special attention to - making sure the block does not inadvertently retain objects which should not be retained by - the long-lived block. The block itself will be retained by `Auth` until it is - unregistered or until the `Auth` instance is otherwise deallocated. - - @return A handle useful for manually unregistering the block as a listener. - */ + /// Registers a block as an "ID token did change" listener. + /// + /// To be invoked when: + /// * The block is registered as a listener, + /// * A user with a different UID from the current user has signed in, + /// * The ID token of the current user has been refreshed, or + /// * The current user has signed out. + /// + /// The block is invoked immediately after adding it according to it's standard invocation + /// semantics, asynchronously on the main thread. Users should pay special attention to + /// making sure the block does not inadvertently retain objects which should not be retained by + /// the long-lived block. The block itself will be retained by `Auth` until it is + /// unregistered or until the `Auth` instance is otherwise deallocated. + /// - Parameter listener: The block to be invoked. The block is always invoked asynchronously on + /// the main thread, even for it's initial invocation after having been added as a listener. + /// - Returns: A handle useful for manually unregistering the block as a listener. @objc open func addIDTokenDidChangeListener(_ listener: @escaping (Auth, User?) -> Void) -> NSObjectProtocol { let handle = NotificationCenter.default.addObserver( @@ -1586,11 +1402,8 @@ extension Auth: AuthInterop { return handle } - /** @fn removeIDTokenDidChangeListener: - @brief Unregisters a block as an "ID token did change" listener. - - @param listenerHandle The handle for the listener. - */ + /// Unregisters a block as an "ID token did change" listener. + /// - Parameter listenerHandle: The handle for the listener. @objc open func removeIDTokenDidChangeListener(_ listenerHandle: NSObjectProtocol) { NotificationCenter.default.removeObserver(listenerHandle) objc_sync_enter(Auth.self) @@ -1598,18 +1411,14 @@ extension Auth: AuthInterop { objc_sync_exit(Auth.self) } - /** @fn useAppLanguage - @brief Sets `languageCode` to the app's current language. - */ + /// Sets `languageCode` to the app's current language. @objc open func useAppLanguage() { kAuthGlobalWorkQueue.sync { self.requestConfiguration.languageCode = Locale.preferredLanguages.first } } - /** @fn useEmulatorWithHost:port - @brief Configures Firebase Auth to connect to an emulated host instead of the remote backend. - */ + /// Configures Firebase Auth to connect to an emulated host instead of the remote backend. @objc open func useEmulator(withHost host: String, port: Int) { guard host.count > 0 else { fatalError("Cannot connect to empty host") @@ -1624,11 +1433,9 @@ extension Auth: AuthInterop { } } - /** @fn revokeTokenWithAuthorizationCode:Completion - @brief Revoke the users token with authorization code. - @param completion (Optional) the block invoked when the request to revoke the token is - complete, or fails. Invoked asynchronously on the main thread in the future. - */ + /// Revoke the users token with authorization code. + /// - Parameter completion: (Optional) the block invoked when the request to revoke the token is + /// complete, or fails. Invoked asynchronously on the main thread in the future. @objc open func revokeToken(withAuthorizationCode authorizationCode: String, completion: ((Error?) -> Void)? = nil) { currentUser?.internalGetToken { idToken, error in @@ -1646,11 +1453,9 @@ extension Auth: AuthInterop { } } - /** @fn revokeTokenWithAuthorizationCode:Completion - @brief Revoke the users token with authorization code. - @param completion (Optional) the block invoked when the request to revoke the token is - complete, or fails. Invoked asynchronously on the main thread in the future. - */ + /// Revoke the users token with authorization code. + /// - Parameter completion: (Optional) the block invoked when the request to revoke the token is + /// complete, or fails. Invoked asynchronously on the main thread in the future. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func revokeToken(withAuthorizationCode authorizationCode: String) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -1664,10 +1469,7 @@ extension Auth: AuthInterop { } } - /** @fn useUserAccessGroup:error: - @brief Switch userAccessGroup and current user to the given accessGroup and the user stored in - it. - */ + /// Switch userAccessGroup and current user to the given accessGroup and the user stored in it. @objc open func useUserAccessGroup(_ accessGroup: String?) throws { // self.storedUserManager is initialized asynchronously. Make sure it is done. kAuthGlobalWorkQueue.sync {} @@ -1686,12 +1488,11 @@ extension Auth: AuthInterop { lastNotifiedUserToken = user?.rawAccessToken() } - /** @fn getStoredUserForAccessGroup:error: - @brief Get the stored user in the given accessGroup. - @note This API is not supported on tvOS when `shareAuthStateAcrossDevices` is set to `true`. - This case will return `nil`. - Please refer to https://github.com/firebase/firebase-ios-sdk/issues/8878 for details. - */ + /// Get the stored user in the given accessGroup. + /// + /// This API is not supported on tvOS when `shareAuthStateAcrossDevices` is set to `true`. + /// and will return `nil`. + /// Please refer to https://github.com/firebase/firebase-ios-sdk/issues/8878 for details. @available(swift 1000.0) // Objective-C only API @objc(getStoredUserForAccessGroup:error:) open func __getStoredUser(forAccessGroup accessGroup: String?, @@ -1704,12 +1505,12 @@ extension Auth: AuthInterop { } } - /** @fn getStoredUserForAccessGroup - @brief Get the stored user in the given accessGroup. - @note This API is not supported on tvOS when `shareAuthStateAcrossDevices` is set to `true`. - This case will return `nil`. - Please refer to https://github.com/firebase/firebase-ios-sdk/issues/8878 for details. - */ + /// Get the stored user in the given accessGroup. + /// + /// This API is not supported on tvOS when `shareAuthStateAcrossDevices` is set to `true`. + /// and will return `nil`. + /// + /// Please refer to https://github.com/firebase/firebase-ios-sdk/issues/8878 for details. open func getStoredUser(forAccessGroup accessGroup: String?) throws -> User? { var user: User? if let accessGroup { @@ -1742,24 +1543,63 @@ extension Auth: AuthInterop { } #if os(iOS) + /// The APNs token used for phone number authentication. + /// + /// The type of the token (production or sandbox) will be automatically + /// detected based on your provisioning profile. + /// + /// This property is available on iOS only. + /// + /// If swizzling is disabled, the APNs Token must be set for phone number auth to work, + /// by either setting this property or by calling `setAPNSToken(_:type:)`. @objc(APNSToken) open var apnsToken: Data? { kAuthGlobalWorkQueue.sync { self.tokenManager.token?.data } } + /// Sets the APNs token along with its type. + /// + /// This method is available on iOS only. + /// + /// If swizzling is disabled, the APNs Token must be set for phone number auth to work, + /// by either setting calling this method or by setting the `APNSToken` property. @objc open func setAPNSToken(_ token: Data, type: AuthAPNSTokenType) { kAuthGlobalWorkQueue.sync { self.tokenManager.token = AuthAPNSToken(withData: token, type: type) } } + /// Whether the specific remote notification is handled by `Auth` . + /// + /// This method is available on iOS only. + /// + /// If swizzling is disabled, related remote notifications must be forwarded to this method + /// for phone number auth to work. + /// - Parameter userInfo: A dictionary that contains information related to the + /// notification in question. + /// - Returns: Whether or the notification is handled. A return value of `true` means the + /// notification is for Firebase Auth so the caller should ignore the notification from further + /// processing, and `false` means the notification is for the app (or another library) so + /// the caller should continue handling this notification as usual. @objc open func canHandleNotification(_ userInfo: [AnyHashable: Any]) -> Bool { kAuthGlobalWorkQueue.sync { self.notificationManager.canHandle(notification: userInfo) } } + /// Whether the specific URL is handled by `Auth` . + /// + /// This method is available on iOS only. + /// + /// If swizzling is disabled, URLs received by the application delegate must be forwarded + /// to this method for phone number auth to work. + /// - Parameter url: The URL received by the application delegate from any of the openURL + /// method. + /// - Returns: Whether or the URL is handled. `true` means the URL is for Firebase Auth + /// so the caller should ignore the URL from further processing, and `false` means the + /// the URL is for the app (or another library) so the caller should continue handling + /// this URL as usual. @objc(canHandleURL:) open func canHandle(_ url: URL) -> Bool { kAuthGlobalWorkQueue.sync { guard let authURLPresenter = self.authURLPresenter as? AuthURLPresenter else { @@ -1770,6 +1610,10 @@ extension Auth: AuthInterop { } #endif + /// The name of the `NSNotificationCenter` notification which is posted when the auth state + /// changes (for example, a new token has been produced, a user signs in or signs out). + /// + /// The object parameter of the notification is the sender `Auth` instance. public static let authStateDidChangeNotification = NSNotification.Name(rawValue: "FIRAuthStateDidChangeNotification") @@ -1937,10 +1781,8 @@ extension Auth: AuthInterop { return user } - /** @fn keychainServiceNameForAppName: - @brief Gets the keychain service name global data for the particular app by name. - @param appName The name of the Firebase app to get keychain service name for. - */ + /// Gets the keychain service name global data for the particular app by name. + /// - Parameter appName: The name of the Firebase app to get keychain service name for. class func keychainServiceForAppID(_ appID: String) -> String { return "firebase_auth_\(appID)" } @@ -1960,38 +1802,31 @@ extension Auth: AuthInterop { return nil } - /** @var gKeychainServiceNameForAppName - @brief A map from Firebase app name to keychain service names. - @remarks This map is needed for looking up the keychain service name after the FIRApp instance - is deleted, to remove the associated keychain item. Accessing should occur within a - @syncronized([FIRAuth class]) context."" - */ + /// A map from Firebase app name to keychain service names. + /// + /// This map is needed for looking up the keychain service name after the FirebaseApp instance + /// is deleted, to remove the associated keychain item. Accessing should occur within a + /// @syncronized([FIRAuth class]) context. fileprivate static var gKeychainServiceNameForAppName: [String: String] = [:] - /** @fn setKeychainServiceNameForApp - @brief Sets the keychain service name global data for the particular app. - @param app The Firebase app to set keychain service name for. - */ + /// Sets the keychain service name global data for the particular app. + /// - Parameter app: The Firebase app to set keychain service name for. class func setKeychainServiceNameForApp(_ app: FirebaseApp) { objc_sync_enter(Auth.self) gKeychainServiceNameForAppName[app.name] = "firebase_auth_\(app.options.googleAppID)" objc_sync_exit(Auth.self) } - /** @fn keychainServiceNameForAppName: - @brief Gets the keychain service name global data for the particular app by name. - @param appName The name of the Firebase app to get keychain service name for. - */ + /// Gets the keychain service name global data for the particular app by name. + /// - Parameter appName: The name of the Firebase app to get keychain service name for. class func keychainServiceName(forAppName appName: String) -> String? { objc_sync_enter(Auth.self) defer { objc_sync_exit(Auth.self) } return gKeychainServiceNameForAppName[appName] } - /** @fn deleteKeychainServiceNameForAppName: - @brief Deletes the keychain service name global data for the particular app by name. - @param appName The name of the Firebase app to delete keychain service name for. - */ + /// Deletes the keychain service name global data for the particular app by name. + /// - Parameter appName: The name of the Firebase app to delete keychain service name for. class func deleteKeychainServiceNameForAppName(_ appName: String) { objc_sync_enter(Auth.self) gKeychainServiceNameForAppName.removeValue(forKey: appName) @@ -2007,9 +1842,7 @@ extension Auth: AuthInterop { // MARK: Private methods - /** @fn possiblyPostAuthStateChangeNotification - @brief Posts the auth state change notificaton if current user's token has been changed. - */ + /// Posts the auth state change notificaton if current user's token has been changed. private func possiblyPostAuthStateChangeNotification() { let token = currentUser?.rawAccessToken() if lastNotifiedUserToken == token || @@ -2039,23 +1872,20 @@ extension Auth: AuthInterop { } } - /** @fn scheduleAutoTokenRefreshWithDelay: - @brief Schedules a task to automatically refresh tokens on the current user. The0 token refresh - is scheduled 5 minutes before the scheduled expiration time. - @remarks If the token expires in less than 5 minutes, schedule the token refresh immediately. - */ + /// Schedules a task to automatically refresh tokens on the current user. The0 token refresh + /// is scheduled 5 minutes before the scheduled expiration time. + /// + /// If the token expires in less than 5 minutes, schedule the token refresh immediately. private func scheduleAutoTokenRefresh() { let tokenExpirationInterval = (currentUser?.accessTokenExpirationDate()?.timeIntervalSinceNow ?? 0) - 5 * 60 scheduleAutoTokenRefresh(withDelay: max(tokenExpirationInterval, 0), retry: false) } - /** @fn scheduleAutoTokenRefreshWithDelay: - @brief Schedules a task to automatically refresh tokens on the current user. - @param delay The delay in seconds after which the token refresh task should be scheduled to be - executed. - @param retry Flag to determine whether the invocation is a retry attempt or not. - */ + /// Schedules a task to automatically refresh tokens on the current user. + /// - Parameter delay: The delay in seconds after which the token refresh task should be scheduled + /// to be executed. + /// - Parameter retry: Flag to determine whether the invocation is a retry attempt or not. private func scheduleAutoTokenRefresh(withDelay delay: TimeInterval, retry: Bool) { guard let accessToken = currentUser?.rawAccessToken() else { return @@ -2099,16 +1929,15 @@ extension Auth: AuthInterop { } } - /** @fn updateCurrentUser:byForce:savingToDisk:error: - @brief Update the current user; initializing the user's internal properties correctly, and - optionally saving the user to disk. - @remarks This method is called during: sign in and sign out events, as well as during class - initialization time. The only time the saveToDisk parameter should be set to NO is during - class initialization time because the user was just read from disk. - @param user The user to use as the current user (including nil, which is passed at sign out - time.) - @param saveToDisk Indicates the method should persist the user data to disk. - */ + /// Update the current user; initializing the user's internal properties correctly, and + /// optionally saving the user to disk. + /// + /// This method is called during: sign in and sign out events, as well as during class + /// initialization time. The only time the saveToDisk parameter should be set to NO is during + /// class initialization time because the user was just read from disk. + /// - Parameter user: The user to use as the current user (including nil, which is passed at sign + /// out time.) + /// - Parameter saveToDisk: Indicates the method should persist the user data to disk. func updateCurrentUser(_ user: User?, byForce force: Bool, savingToDisk saveToDisk: Bool) throws { if user == currentUser { @@ -2169,15 +1998,11 @@ extension Auth: AuthInterop { } } - /** @fn completeSignInWithTokenService:callback: - @brief Completes a sign-in flow once we have access and refresh tokens for the user. - @param accessToken The STS access token. - @param accessTokenExpirationDate The approximate expiration date of the access token. - @param refreshToken The STS refresh token. - @param anonymous Whether or not the user is anonymous. - @param callback Called when the user has been signed in or when an error occurred. Invoked - asynchronously on the global auth work queue in the future. - */ + /// Completes a sign-in flow once we have access and refresh tokens for the user. + /// - Parameter accessToken: The STS access token. + /// - Parameter accessTokenExpirationDate: The approximate expiration date of the access token. + /// - Parameter refreshToken: The STS refresh token. + /// - Parameter anonymous: Whether or not the user is anonymous. @discardableResult func completeSignIn(withAccessToken accessToken: String?, accessTokenExpirationDate: Date?, @@ -2190,15 +2015,12 @@ extension Auth: AuthInterop { anonymous: anonymous) } - /** @fn internalSignInAndRetrieveDataWithEmail:password:callback: - @brief Signs in using an email address and password. - @param email The user's email address. - @param password The user's password. - @param completion A block which is invoked when the sign in finishes (or is cancelled.) Invoked - asynchronously on the global auth work queue in the future. - @remarks This is the internal counterpart of this method, which uses a callback that does not - update the current user. - */ + /// Signs in using an email address and password. + /// + /// This is the internal counterpart of this method, which uses a callback that does not + /// update the current user. + /// - Parameter email: The user's email address. + /// - Parameter password: The user's password. private func internalSignInAndRetrieveData(withEmail email: String, password: String) async throws -> AuthDataResult { let credential = EmailAuthCredential(withEmail: email, password: password) @@ -2286,13 +2108,9 @@ extension Auth: AuthInterop { } #if os(iOS) - /** @fn signInWithPhoneCredential:callback: - @brief Signs in using a phone credential. - @param credential The Phone Auth credential used to sign in. - @param operation The type of operation for which this sign-in attempt is initiated. - @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked - asynchronously on the global auth work queue in the future. - */ + /// Signs in using a phone credential. + /// - Parameter credential: The Phone Auth credential used to sign in. + /// - Parameter operation: The type of operation for which this sign-in attempt is initiated. private func signIn(withPhoneCredential credential: PhoneAuthCredential, operation: AuthOperationType) async throws -> VerifyPhoneNumberResponse { switch credential.credentialKind { @@ -2319,12 +2137,8 @@ extension Auth: AuthInterop { #endif #if !os(watchOS) - /** @fn signInAndRetrieveDataWithGameCenterCredential:callback: - @brief Signs in using a game center credential. - @param credential The Game Center Auth Credential used to sign in. - @param callback A block which is invoked when the sign in finished (or is cancelled). Invoked - asynchronously on the global auth work queue in the future. - */ + /// Signs in using a game center credential. + /// - Parameter credential: The Game Center Auth Credential used to sign in. private func signInAndRetrieveData(withGameCenterCredential credential: GameCenterAuthCredential) async throws -> AuthDataResult { guard let publicKeyURL = credential.publicKeyURL, @@ -2358,13 +2172,9 @@ extension Auth: AuthInterop { #endif - /** @fn internalSignInAndRetrieveDataWithEmail:link:completion: - @brief Signs in using an email and email sign-in link. - @param email The user's email address. - @param link The email sign-in link. - @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked - asynchronously on the global auth work queue in the future. - */ + /// Signs in using an email and email sign-in link. + /// - Parameter email: The user's email address. + /// - Parameter link: The email sign-in link. private func internalSignInAndRetrieveData(withEmail email: String, link: String) async throws -> AuthDataResult { guard isSignIn(withEmailLink: link) else { @@ -2404,15 +2214,15 @@ extension Auth: AuthInterop { return queryItems } - /** @fn signInFlowAuthDataResultCallbackByDecoratingCallback: - @brief Creates a AuthDataResultCallback block which wraps another AuthDataResultCallback; - trying to update the current user before forwarding it's invocations along to a subject block. - @param callback Called when the user has been updated or when an error has occurred. Invoked - asynchronously on the main thread in the future. - @return Returns a block that updates the current user. - @remarks Typically invoked as part of the complete sign-in flow. For any other uses please - consider alternative ways of updating the current user. - */ + /// Creates a AuthDataResultCallback block which wraps another AuthDataResultCallback; + /// trying to update the current user before forwarding it's invocations along to a subject + /// block. + /// + /// Typically invoked as part of the complete sign-in flow. For any other uses please + /// consider alternative ways of updating the current user. + /// - Parameter callback: Called when the user has been updated or when an error has occurred. + /// Invoked asynchronously on the main thread in the future. + /// - Returns: Returns a block that updates the current user. func signInFlowAuthDataResultCallback(byDecorating callback: ((AuthDataResult?, Error?) -> Void)?) -> (AuthDataResult?, Error?) -> Void { let authDataCallback: (((AuthDataResult?, Error?) -> Void)?, AuthDataResult?, Error?) -> Void = @@ -2514,106 +2324,72 @@ extension Auth: AuthInterop { // MARK: Internal properties - /** @property mainBundle - @brief Allow tests to swap in an alternate mainBundle. - */ + /// Allow tests to swap in an alternate mainBundle. var mainBundleUrlTypes: [[String: Any]]! - /** @property requestConfiguration - @brief The configuration object comprising of parameters needed to make a request to Firebase - Auth's backend. - */ + /// The configuration object comprising of parameters needed to make a request to Firebase + /// Auth's backend. var requestConfiguration: AuthRequestConfiguration #if os(iOS) - /** @property tokenManager - @brief The manager for APNs tokens used by phone number auth. - */ + + /// The manager for APNs tokens used by phone number auth. var tokenManager: AuthAPNSTokenManager! - /** @property appCredentailManager - @brief The manager for app credentials used by phone number auth. - */ + /// The manager for app credentials used by phone number auth. var appCredentialManager: AuthAppCredentialManager! - /** @property notificationManager - @brief The manager for remote notifications used by phone number auth. - */ + /// The manager for remote notifications used by phone number auth. var notificationManager: AuthNotificationManager! - /** @property authURLPresenter - @brief An object that takes care of presenting URLs via the auth instance. - */ + /// An object that takes care of presenting URLs via the auth instance. var authURLPresenter: AuthWebViewControllerDelegate #endif // TARGET_OS_IOS // MARK: Private properties - /** @property storedUserManager - @brief The stored user manager. - */ + /// The stored user manager. private var storedUserManager: AuthStoredUserManager! - /** @var _firebaseAppName - @brief The Firebase app name. - */ + /// The Firebase app name. private let firebaseAppName: String - /** @var _keychainServices - @brief The keychain service. - */ + /// The keychain service. private var keychainServices: AuthKeychainServices! - /** @var _lastNotifiedUserToken - @brief The user access (ID) token used last time for posting auth state changed notification. - */ + /// The user access (ID) token used last time for posting auth state changed notification. private var lastNotifiedUserToken: String? - /** @var _autoRefreshTokens - @brief This flag denotes whether or not tokens should be automatically refreshed. - @remarks Will only be set to @YES if the another Firebase service is included (additionally to - Firebase Auth). - */ + /// This flag denotes whether or not tokens should be automatically refreshed. + /// Will only be set to `true` if the another Firebase service is included (additionally to + /// Firebase Auth). private var autoRefreshTokens = false - /** @var _autoRefreshScheduled - @brief Whether or not token auto-refresh is currently scheduled. - */ + /// Whether or not token auto-refresh is currently scheduled. private var autoRefreshScheduled = false - /** @var _isAppInBackground - @brief A flag that is set to YES if the app is put in the background and no when the app is - returned to the foreground. - */ + /// A flag that is set to YES if the app is put in the background and no when the app is + /// returned to the foreground. private var isAppInBackground = false - /** @var _applicationDidBecomeActiveObserver - @brief An opaque object to act as the observer for UIApplicationDidBecomeActiveNotification. - */ + /// An opaque object to act as the observer for UIApplicationDidBecomeActiveNotification. private var applicationDidBecomeActiveObserver: NSObjectProtocol? - /** @var _applicationDidBecomeActiveObserver - @brief An opaque object to act as the observer for - UIApplicationDidEnterBackgroundNotification. - */ + /// An opaque object to act as the observer for + /// UIApplicationDidEnterBackgroundNotification. private var applicationDidEnterBackgroundObserver: NSObjectProtocol? - /** @var _protectedDataDidBecomeAvailableObserver - @brief An opaque object to act as the observer for - UIApplicationProtectedDataDidBecomeAvailable. - */ + /// An opaque object to act as the observer for + /// UIApplicationProtectedDataDidBecomeAvailable. private var protectedDataDidBecomeAvailableObserver: NSObjectProtocol? - /** @var kUserKey - @brief Key of user stored in the keychain. Prefixed with a Firebase app name. - */ + /// Key of user stored in the keychain. Prefixed with a Firebase app name. private let kUserKey = "_firebase_user" - /** @var _listenerHandles - @brief Handles returned from @c NSNotificationCenter for blocks which are "auth state did - change" notification listeners. - @remarks Mutations should occur within a @syncronized(self) context. - */ + /// Handles returned from `NSNotificationCenter` for blocks which are "auth state did + /// change" notification listeners. + /// + /// Mutations should occur within a @syncronized(self) context. private var listenerHandles: NSMutableArray = [] } diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthDataResult.swift b/FirebaseAuth/Sources/Swift/Auth/AuthDataResult.swift index 7462ff87725..bd09c1ddac2 100644 --- a/FirebaseAuth/Sources/Swift/Auth/AuthDataResult.swift +++ b/FirebaseAuth/Sources/Swift/Auth/AuthDataResult.swift @@ -17,35 +17,29 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension AuthDataResult: NSSecureCoding {} -/** @class AuthDataResult - @brief Helper object that contains the result of a successful sign-in, link and reauthenticate - action. It contains references to a `User` instance and a `AdditionalUserInfo` instance. - */ +/// Helper object that contains the result of a successful sign-in, link and reauthenticate +/// action. +/// +/// It contains references to a `User` instance and an `AdditionalUserInfo` instance. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRAuthDataResult) open class AuthDataResult: NSObject { - /** @property user - @brief The signed in user. - */ + /// The signed in user. @objc public let user: User - /** @property additionalUserInfo - @brief If available contains the additional IdP specific information about signed in user. - */ + /// If available, contains the additional IdP specific information about signed in user. @objc public let additionalUserInfo: AdditionalUserInfo? - /** @property credential - @brief This property will be non-nil after a successful headful-lite sign-in via - `signIn(with:uiDelegate:completion:)`. May be used to obtain the accessToken and/or IDToken - pertaining to a recently signed-in user. - */ + /// This property will be non-nil after a successful headful-lite sign-in via + /// `signIn(with:uiDelegate:completion:)`. + /// + /// May be used to obtain the accessToken and/or IDToken + /// pertaining to a recently signed-in user. @objc public let credential: OAuthCredential? - /** @fn initWithUser:additionalUserInfo: - @brief Designated initializer. - @param user The signed in user reference. - @param additionalUserInfo The additional user info. - @param credential The updated OAuth credential if available. - */ + /// Designated initializer. + /// - Parameter user: The signed in user reference. + /// - Parameter additionalUserInfo: The additional user info. + /// - Parameter credential: The updated OAuth credential if available. init(withUser user: User, additionalUserInfo: AdditionalUserInfo?, credential: OAuthCredential? = nil) { diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthDispatcher.swift b/FirebaseAuth/Sources/Swift/Auth/AuthDispatcher.swift index b2bc47322de..6373cdfb4cc 100644 --- a/FirebaseAuth/Sources/Swift/Auth/AuthDispatcher.swift +++ b/FirebaseAuth/Sources/Swift/Auth/AuthDispatcher.swift @@ -14,26 +14,22 @@ import Foundation -/** @class AuthDispatcher - @brief A utility class used to facilitate scheduling tasks to be executed in the future. - */ +/// A utility class used to facilitate scheduling tasks to be executed in the future. class AuthDispatcher { static let shared = AuthDispatcher() - /** @property dispatchAfterImplementation - @brief Allows custom implementation of dispatchAfterDelay:queue:callback:. - @remarks Set to nil to restore default implementation. - */ + /// Allows custom implementation of dispatchAfterDelay:queue:callback:. + /// + /// Set to nil to restore default implementation. var dispatchAfterImplementation: ((TimeInterval, DispatchQueue, @escaping () -> Void) -> Void)? - /** @fn dispatchAfterDelay:queue:callback: - @brief Schedules task in the future after a specified delay. - - @param delay The delay in seconds after which the task will be scheduled to execute. - @param queue The dispatch queue on which the task will be submitted. - @param task The task (block) to be scheduled for future execution. - */ - func dispatch(afterDelay delay: TimeInterval, queue: DispatchQueue, task: @escaping () -> Void) { + /// Schedules task in the future after a specified delay. + /// - Parameter delay: The delay in seconds after which the task will be scheduled to execute. + /// - Parameter queue: The dispatch queue on which the task will be submitted. + /// - Parameter task: The task(block) to be scheduled for future execution. + func dispatch(afterDelay delay: TimeInterval, + queue: DispatchQueue, + task: @escaping () -> Void) { if let dispatchAfterImplementation { dispatchAfterImplementation(delay, queue, task) } else { diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthOperationType.swift b/FirebaseAuth/Sources/Swift/Auth/AuthOperationType.swift index c21b561b9b6..08ffe5cf8c0 100644 --- a/FirebaseAuth/Sources/Swift/Auth/AuthOperationType.swift +++ b/FirebaseAuth/Sources/Swift/Auth/AuthOperationType.swift @@ -13,28 +13,21 @@ // limitations under the License. import Foundation -/** - @brief Indicates the type of operation performed for RPCs that support the operation - parameter. - */ + +/// Indicates the type of operation performed for RPCs that support the operation parameter. enum AuthOperationType: Int { - /** Indicates that the operation type is uspecified. - */ + /// Indicates that the operation type is uspecified. case unspecified = 0 - /** Indicates that the operation type is sign in or sign up. - */ + /// Indicates that the operation type is sign in or sign up. case signUpOrSignIn = 1 - /** Indicates that the operation type is reauthentication. - */ + /// Indicates that the operation type is reauthentication. case reauth = 2 - /** Indicates that the operation type is update. - */ + /// Indicates that the operation type is update. case update = 3 - /** Indicates that the operation type is link. - */ + /// Indicates that the operation type is link. case link = 4 } diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthSettings.swift b/FirebaseAuth/Sources/Swift/Auth/AuthSettings.swift index 10ce42396dc..dbd77df5306 100644 --- a/FirebaseAuth/Sources/Swift/Auth/AuthSettings.swift +++ b/FirebaseAuth/Sources/Swift/Auth/AuthSettings.swift @@ -14,14 +14,12 @@ import Foundation -/** @class AuthSettings - @brief Determines settings related to an auth object. - */ +/// Determines settings related to an auth object. @objc(FIRAuthSettings) open class AuthSettings: NSObject, NSCopying { - /** @property appVerificationDisabledForTesting - @brief Flag to determine whether app verification should be disabled for testing or not. - */ + /// Flag to determine whether app verification should be disabled for testing or not. @objc open var appVerificationDisabledForTesting: Bool + + /// Flag to determine whether app verification should be disabled for testing or not. @objc open var isAppVerificationDisabledForTesting: Bool { get { return appVerificationDisabledForTesting diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthTokenResult.swift b/FirebaseAuth/Sources/Swift/Auth/AuthTokenResult.swift index 47c3e592947..76bdbb73204 100644 --- a/FirebaseAuth/Sources/Swift/Auth/AuthTokenResult.swift +++ b/FirebaseAuth/Sources/Swift/Auth/AuthTokenResult.swift @@ -16,48 +16,35 @@ import Foundation extension AuthTokenResult: NSSecureCoding {} -/** @class FIRAuthTokenResult - @brief A data class containing the ID token JWT string and other properties associated with the - token including the decoded payload claims. - */ +/// A data class containing the ID token JWT string and other properties associated with the +/// token including the decoded payload claims. @objc(FIRAuthTokenResult) open class AuthTokenResult: NSObject { - /** @property token - @brief Stores the JWT string of the ID token. - */ + /// Stores the JWT string of the ID token. @objc open var token: String - /** @property expirationDate - @brief Stores the ID token's expiration date. - */ + /// Stores the ID token's expiration date. @objc open var expirationDate: Date - /** @property authDate - @brief Stores the ID token's authentication date. - @remarks This is the date the user was signed in and NOT the date the token was refreshed. - */ + /// Stores the ID token's authentication date. + /// + /// This is the date the user was signed in and NOT the date the token was refreshed. @objc open var authDate: Date - /** @property issuedAtDate - @brief Stores the date that the ID token was issued. - @remarks This is the date last refreshed and NOT the last authentication date. - */ + /// Stores the date that the ID token was issued. + /// + /// This is the date last refreshed and NOT the last authentication date. @objc open var issuedAtDate: Date - /** @property signInProvider - @brief Stores sign-in provider through which the token was obtained. - @remarks This does not necessarily map to provider IDs. - */ + /// Stores sign-in provider through which the token was obtained. @objc open var signInProvider: String - /** @property signInSecondFactor - @brief Stores sign-in second factor through which the token was obtained. - */ + /// Stores sign-in second factor through which the token was obtained. @objc open var signInSecondFactor: String - /** @property claims - @brief Stores the entire payload of claims found on the ID token. This includes the standard - reserved claims as well as custom claims set by the developer via the Admin SDK. - */ + /// Stores the entire payload of claims found on the ID token. + /// + /// This includes the standard + /// reserved claims as well as custom claims set by the developer via the Admin SDK. @objc open var claims: [String: Any] private class func getTokenPayloadData(_ token: String) -> Data? { @@ -68,12 +55,12 @@ extension AuthTokenResult: NSSecureCoding {} return nil } - // The token payload is always the second index of the array. + /// The token payload is always the second index of the array. let IDToken = tokenStringArray[1] - // Convert the base64URL encoded string to a base64 encoded string. - // Replace "_" with "/" - // Replace "-" with "+" + /// Convert the base64URL encoded string to a base64 encoded string. + /// * Replace "_" with "/" + /// * Replace "-" with "+" var tokenPayload = IDToken.replacingOccurrences(of: "_", with: "/") .replacingOccurrences(of: "-", with: "+") @@ -104,11 +91,9 @@ extension AuthTokenResult: NSSecureCoding {} return jwt } - /** @fn tokenResultWithToken: - @brief Parse a token string to a structured token. - @param token The token string to parse. - @return A structured token result. - */ + /// Parse a token string to a structured token. + /// - Parameter token: The token string to parse. + /// - Returns: A structured token result. @objc open class func tokenResult(token: String) -> AuthTokenResult? { guard let payloadData = getTokenPayloadData(token), let claims = getTokenPayloadDictionary(payloadData), diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/AuthCredential.swift b/FirebaseAuth/Sources/Swift/AuthProvider/AuthCredential.swift index ea2cebbe7d3..0500875b10f 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/AuthCredential.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/AuthCredential.swift @@ -14,12 +14,12 @@ import Foundation -/** - @brief Public representation of a credential. - */ +/// Public representation of a credential. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRAuthCredential) open class AuthCredential: NSObject { + /// The name of the identity provider for the credential. @objc public let provider: String + init(provider: String) { self.provider = provider } diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/AuthProviderStrings.swift b/FirebaseAuth/Sources/Swift/AuthProvider/AuthProviderStrings.swift index a11e7e7ec25..978497fc8f4 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/AuthProviderStrings.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/AuthProviderStrings.swift @@ -14,6 +14,7 @@ import Foundation +/// Enumeration of the available Auth Providers. public enum AuthProviderString: String { case apple = "apple.com" case email = "password" diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/EmailAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/EmailAuthProvider.swift index 047df6a46a9..0675f02f737 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/EmailAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/EmailAuthProvider.swift @@ -14,31 +14,24 @@ import Foundation -/** - @brief A concrete implementation of `AuthProvider` for Email & Password Sign In. - */ +/// A concrete implementation of `AuthProvider` for Email & Password Sign In. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIREmailAuthProvider) open class EmailAuthProvider: NSObject { + /// A string constant identifying the email & password identity provider. @objc public static let id = "password" - /** - @brief Creates an `AuthCredential` for an email & password sign in. - - @param email The user's email address. - @param password The user's password. - @return An `AuthCredential` containing the email & password credential. - */ + /// Creates an `AuthCredential` for an email & password sign in + /// - Parameter email: The user's email address. + /// - Parameter password: The user's password. + /// - Returns: An `AuthCredential` containing the email & password credential. @objc open class func credential(withEmail email: String, password: String) -> AuthCredential { return EmailAuthCredential(withEmail: email, password: password) } - /** @fn credentialWithEmail:Link: - @brief Creates an `AuthCredential` for an email & link sign in. - - @param email The user's email address. - @param link The email sign-in link. - @return An `AuthCredential` containing the email & link credential. - */ + /// Creates an `AuthCredential` for an email & link sign in. + /// - Parameter email: The user's email address. + /// - Parameter link: The email sign-in link. + /// - Returns: An `AuthCredential` containing the email & link credential. @objc open class func credential(withEmail email: String, link: String) -> AuthCredential { return EmailAuthCredential(withEmail: email, link: link) } diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/FacebookAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/FacebookAuthProvider.swift index e868f1c9b91..9ffbec55dba 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/FacebookAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/FacebookAuthProvider.swift @@ -14,19 +14,15 @@ import Foundation -/** - @brief Utility class for constructing Facebook Sign In credentials. - */ +/// Utility class for constructing Facebook Sign In credentials. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRFacebookAuthProvider) open class FacebookAuthProvider: NSObject { + /// A string constant identifying the Facebook identity provider. @objc public static let id = "facebook.com" - /** - @brief Creates an `AuthCredential` for a Facebook sign in. - - @param accessToken The Access Token from Facebook. - @return An AuthCredential containing the Facebook credentials. - */ + /// Creates an `AuthCredential` for a Facebook sign in. + /// - Parameter accessToken: The Access Token from Facebook. + /// - Returns: An `AuthCredential` containing the Facebook credentials. @objc open class func credential(withAccessToken accessToken: String) -> AuthCredential { return FacebookAuthCredential(withAccessToken: accessToken) } diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/FederatedAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/FederatedAuthProvider.swift index 44969082bec..419f87f06af 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/FederatedAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/FederatedAuthProvider.swift @@ -14,19 +14,16 @@ import Foundation -/** - Utility type for constructing federated auth provider credentials. - */ +/// Utility type for constructing federated auth provider credentials. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRFederatedAuthProvider) public protocol FederatedAuthProvider: NSObjectProtocol { #if os(iOS) - /** @fn getCredentialWithUIDelegate:completion: - @brief Used to obtain an auth credential via a mobile web flow. - This method is available on iOS only. - @param UIDelegate An optional UI delegate used to present the mobile web flow. - */ + + /// Used to obtain an auth credential via a mobile web flow. + /// This method is available on iOS only. + /// - Parameter uiDelegate: An optional UI delegate used to present the mobile web flow. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) @objc(getCredentialWithUIDelegate:completion:) - func credential(with UIDelegate: AuthUIDelegate?) async throws -> AuthCredential + func credential(with uiDelegate: AuthUIDelegate?) async throws -> AuthCredential #endif } diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/GameCenterAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/GameCenterAuthProvider.swift index 45168981047..aecab7ca77d 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/GameCenterAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/GameCenterAuthProvider.swift @@ -28,16 +28,13 @@ @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension GameCenterAuthProvider: WarningWorkaround {} - /** - @brief A concrete implementation of `AuthProvider` for Game Center Sign In. Not available on watchOS. - */ + /// A concrete implementation of `AuthProvider` for Game Center Sign In. Not available on watchOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRGameCenterAuthProvider) open class GameCenterAuthProvider: NSObject { + /// A string constant identifying the Game Center identity provider. @objc public static let id = "gc.apple.com" - /** @fn - @brief Creates an `AuthCredential` for a Game Center sign in. - */ + /// Creates an `AuthCredential` for a Game Center sign in. @objc open class func getCredential(completion: @escaping (AuthCredential?, Error?) -> Void) { /** Linking GameKit.framework without using it on macOS results in App Store rejection. @@ -92,7 +89,7 @@ completion(nil, error) } else { /** - @c `localPlayer.alias` is actually the displayname needed, instead of + `localPlayer.alias` is actually the displayname needed, instead of `localPlayer.displayname`. For more information, check https://developer.apple.com/documentation/gamekit/gkplayer **/ @@ -110,9 +107,7 @@ } } - /** @fn - @brief Creates an `AuthCredential` for a Game Center sign in. - */ + /// Creates an `AuthCredential` for a Game Center sign in. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open class func getCredential() async throws -> AuthCredential { return try await withCheckedThrowingContinuation { continuation in @@ -133,8 +128,8 @@ } @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - @objc(FIRGameCenterAuthCredential) class GameCenterAuthCredential: AuthCredential, - NSSecureCoding { + @objc(FIRGameCenterAuthCredential) + class GameCenterAuthCredential: AuthCredential, NSSecureCoding { let playerID: String let teamPlayerID: String? let gamePlayerID: String? @@ -144,17 +139,14 @@ let timestamp: UInt64 let displayName: String - /** - @brief Designated initializer. - @param playerID The ID of the Game Center local player. - @param teamPlayerID The teamPlayerID of the Game Center local player. - @param gamePlayerID The gamePlayerID of the Game Center local player. - @param publicKeyURL The URL for the public encryption key. - @param signature The verification signature generated. - @param salt A random string used to compute the hash and keep it randomized. - @param timestamp The date and time that the signature was created. - @param displayName The display name of the Game Center player. - */ + /// - Parameter playerID: The ID of the Game Center local player. + /// - Parameter teamPlayerID: The teamPlayerID of the Game Center local player. + /// - Parameter gamePlayerID: The gamePlayerID of the Game Center local player. + /// - Parameter publicKeyURL: The URL for the public encryption key. + /// - Parameter signature: The verification signature generated. + /// - Parameter salt: A random string used to compute the hash and keep it randomized. + /// - Parameter timestamp: The date and time that the signature was created. + /// - Parameter displayName: The display name of the Game Center player. init(withPlayerID playerID: String, teamPlayerID: String?, gamePlayerID: String?, publicKeyURL: URL?, signature: Data?, salt: Data?, timestamp: UInt64, displayName: String) { diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/GitHubAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/GitHubAuthProvider.swift index 0385cfca5b4..4704e4fe014 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/GitHubAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/GitHubAuthProvider.swift @@ -14,19 +14,15 @@ import Foundation -/** - @brief Utility class for constructing GitHub Sign In credentials. - */ +/// Utility class for constructing GitHub Sign In credentials. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRGitHubAuthProvider) open class GitHubAuthProvider: NSObject { + /// A string constant identifying the GitHub identity provider. @objc public static let id = "github.com" - /** - @brief Creates an `AuthCredential` for a GitHub sign in. - - @param token The GitHub OAuth access token. - @return An AuthCredential containing the GitHub credentials. - */ + /// Creates an `AuthCredential` for a GitHub sign in. + /// - Parameter token: The GitHub OAuth access token. + /// - Returns: An AuthCredential containing the GitHub credentials. @objc open class func credential(withToken token: String) -> AuthCredential { return GitHubAuthCredential(withToken: token) } diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/GoogleAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/GoogleAuthProvider.swift index ce198358803..b4f3b806837 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/GoogleAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/GoogleAuthProvider.swift @@ -14,23 +14,19 @@ import Foundation -/** - @brief Utility class for constructing Google Sign In credentials. - */ +/// Utility class for constructing Google Sign In credentials. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRGoogleAuthProvider) open class GoogleAuthProvider: NSObject { + /// A string constant identifying the Google identity provider. @objc public static let id = "google.com" - /** - @brief Creates an `AuthCredential` for a Google sign in. - - @param IDToken The ID Token from Google. - @param accessToken The Access Token from Google. - @return An AuthCredential containing the Google credentials. - */ - @objc open class func credential(withIDToken IDToken: String, + /// Creates an `AuthCredential` for a Google sign in. + /// - Parameter idToken: The ID Token from Google. + /// - Parameter accessToken: The Access Token from Google. + /// - Returns: An AuthCredential containing the Google credentials. + @objc open class func credential(withIDToken idToken: String, accessToken: String) -> AuthCredential { - return GoogleAuthCredential(withIDToken: IDToken, accessToken: accessToken) + return GoogleAuthCredential(withIDToken: idToken, accessToken: accessToken) } @available(*, unavailable) diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthCredential.swift b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthCredential.swift index 8d21df49cff..a7fc244e16e 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthCredential.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthCredential.swift @@ -14,23 +14,19 @@ import Foundation +/// Internal implementation of `AuthCredential` for generic credentials. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIROAuthCredential) open class OAuthCredential: AuthCredential, NSSecureCoding { - /** @property IDToken - @brief The ID Token associated with this credential. - */ + /// The ID Token associated with this credential. @objc(IDToken) public let idToken: String? - /** @property accessToken - @brief The access token associated with this credential. - */ + /// The access token associated with this credential. @objc public let accessToken: String? - /** @property secret - @brief The secret associated with this credential. This will be nil for OAuth 2.0 providers. - @detail OAuthCredential already exposes a providerId getter. This will help the developer - determine whether an access token/secret pair is needed. - */ + /// The secret associated with this credential. This will be nil for OAuth 2.0 providers. + /// + /// OAuthCredential already exposes a `provider` getter. This will help the developer + /// determine whether an access token / secret pair is needed. @objc public let secret: String? // internal diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift index 2cdb8b33611..1566513855f 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/OAuthProvider.swift @@ -15,55 +15,40 @@ import CommonCrypto import Foundation -/** - @brief Utility class for constructing OAuth Sign In credentials. - */ +/// Utility class for constructing OAuth Sign In credentials. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIROAuthProvider) open class OAuthProvider: NSObject, FederatedAuthProvider { @objc public static let id = "OAuth" - /** @property scopes - @brief Array used to configure the OAuth scopes. - */ + /// Array used to configure the OAuth scopes. @objc open var scopes: [String]? - /** @property customParameters - @brief Dictionary used to configure the OAuth custom parameters. - */ + /// Dictionary used to configure the OAuth custom parameters. @objc open var customParameters: [String: String]? - /** @property providerID - @brief The provider ID indicating the specific OAuth provider this OAuthProvider instance - represents. - */ + /// The provider ID indicating the specific OAuth provider this OAuthProvider instance represents. @objc public let providerID: String - /** - @param providerID The provider ID of the IDP for which this auth provider instance will be - configured. - @return An instance of `OAuthProvider` corresponding to the specified provider ID. - */ + /// - Parameter providerID: The provider ID of the IDP for which this auth provider instance will + /// be configured. + /// - Returns: An instance of OAuthProvider corresponding to the specified provider ID. @objc(providerWithProviderID:) open class func provider(providerID: String) -> OAuthProvider { return OAuthProvider(providerID: providerID, auth: Auth.auth()) } - /** - @param providerID The provider ID of the IDP for which this auth provider instance will be - configured. - @param auth The auth instance to be associated with the `OAuthProvider` instance. - @return An instance of `OAuthProvider` corresponding to the specified provider ID. - */ + /// - Parameter providerID: The provider ID of the IDP for which this auth provider instance will + /// be configured. + /// - Parameter auth: The auth instance to be associated with the OAuthProvider instance. + /// - Returns: An instance of OAuthProvider corresponding to the specified provider ID. @objc(providerWithProviderID:auth:) open class func provider(providerID: String, auth: Auth) -> OAuthProvider { return OAuthProvider(providerID: providerID, auth: auth) } - /** - @param providerID The provider ID of the IDP for which this auth provider instance will be - configured. - @param auth The auth instance to be associated with the `OAuthProvider` instance. - @return An instance of `OAuthProvider` corresponding to the specified provider ID. - */ + /// - Parameter providerID: The provider ID of the IDP for which this auth provider instance will + /// be configured. + /// - Parameter auth: The auth instance to be associated with the OAuthProvider instance. + /// - Returns: An instance of OAuthProvider corresponding to the specified provider ID. public init(providerID: String, auth: Auth = Auth.auth()) { if auth.requestConfiguration.emulatorHostAndPort == nil { if providerID == FacebookAuthProvider.id { @@ -99,16 +84,13 @@ import Foundation } } - /** - @brief Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID, ID - token, and access token. - - @param providerID The provider ID associated with the Auth credential being created. - @param idToken The IDToken associated with the Auth credential being created. - @param accessToken The access token associated with the Auth credential be created, if - available. - @return A `AuthCredential` for the specified provider ID, ID token and access token. - */ + /// Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID, ID + /// token, and access token. + /// - Parameter providerID: The provider ID associated with the Auth credential being created. + /// - Parameter idToken: The IDToken associated with the Auth credential being created. + /// - Parameter accessToken: The access token associated with the Auth credential be created, if + /// available. + /// - Returns: An AuthCredential for the specified provider ID, ID token and access token. @objc(credentialWithProviderID:IDToken:accessToken:) public static func credential(withProviderID providerID: String, idToken: String, @@ -116,31 +98,25 @@ import Foundation return OAuthCredential(withProviderID: providerID, idToken: idToken, accessToken: accessToken) } - /** - @brief Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID using - an ID token. - - @param providerID The provider ID associated with the Auth credential being created. - @param accessToken The access token associated with the Auth credential be created - @return An `AuthCredential`. - */ + /// Creates an `AuthCredential` for the OAuth 2 provider identified by provider ID using + /// an ID token. + /// - Parameter providerID: The provider ID associated with the Auth credential being created. + /// - Parameter accessToken: The access token associated with the Auth credential be created + /// - Returns: An AuthCredential. @objc(credentialWithProviderID:accessToken:) public static func credential(withProviderID providerID: String, accessToken: String) -> OAuthCredential { return OAuthCredential(withProviderID: providerID, accessToken: accessToken) } - /** - @brief Creates an `AuthCredential` for that OAuth 2 provider identified by provider ID, ID - token, raw nonce, and access token. - - @param providerID The provider ID associated with the Auth credential being created. - @param idToken The IDToken associated with the Auth credential being created. - @param rawNonce The raw nonce associated with the Auth credential being created. - @param accessToken The access token associated with the Auth credential be created, if - available. - @return A `AuthCredential` for the specified provider ID, ID token and access token. - */ + /// Creates an `AuthCredential` for that OAuth 2 provider identified by provider ID, ID + /// token, raw nonce, and access token. + /// - Parameter providerID: The provider ID associated with the Auth credential being created. + /// - Parameter idToken: The IDToken associated with the Auth credential being created. + /// - Parameter rawNonce: The raw nonce associated with the Auth credential being created. + /// - Parameter accessToken: The access token associated with the Auth credential be created, if + /// available. + /// - Returns: An AuthCredential for the specified provider ID, ID token and access token. @objc(credentialWithProviderID:IDToken:rawNonce:accessToken:) public static func credential(withProviderID providerID: String, idToken: String, rawNonce: String, @@ -153,15 +129,12 @@ import Foundation ) } - /** - @brief Creates an `AuthCredential` for that OAuth 2 provider identified by providerID using - an ID token and raw nonce. - - @param providerID The provider ID associated with the Auth credential being created. - @param idToken The IDToken associated with the Auth credential being created. - @param rawNonce The raw nonce associated with the Auth credential being created. - @return A `AuthCredential`. - */ + /// Creates an `AuthCredential` for that OAuth 2 provider identified by providerID using + /// an ID token and raw nonce. + /// - Parameter providerID: The provider ID associated with the Auth credential being created. + /// - Parameter idToken: The IDToken associated with the Auth credential being created. + /// - Parameter rawNonce: The raw nonce associated with the Auth credential being created. + /// - Returns: An AuthCredential. @objc(credentialWithProviderID:IDToken:rawNonce:) public static func credential(withProviderID providerID: String, idToken: String, rawNonce: String) -> OAuthCredential { @@ -169,13 +142,12 @@ import Foundation } #if os(iOS) - /** @fn getCredentialWithUIDelegate:completion: - @brief Used to obtain an auth credential via a mobile web flow. - This method is available on iOS only. - @param uiDelegate An optional UI delegate used to present the mobile web flow. - @param completion Optionally; a block which is invoked asynchronously on the main thread when - the mobile web flow is completed. - */ + /// Used to obtain an auth credential via a mobile web flow. + /// + /// This method is available on iOS only. + /// - Parameter uiDelegate: An optional UI delegate used to present the mobile web flow. + /// - Parameter completion: Optionally; a block which is invoked asynchronously on the main + /// thread when the mobile web flow is completed. open func getCredentialWith(_ uiDelegate: AuthUIDelegate?, completion: ((AuthCredential?, Error?) -> Void)? = nil) { guard let urlTypes = auth.mainBundleUrlTypes, @@ -243,11 +215,9 @@ import Foundation } } - /** @fn getCredentialWithUIDelegate:completion: - @brief Used to obtain an auth credential via a mobile web flow. - This method is available on iOS only. - @param uiDelegate An optional UI delegate used to present the mobile web flow. - */ + /// Used to obtain an auth credential via a mobile web flow. + /// This method is available on iOS only. + /// - Parameter uiDelegate: An optional UI delegate used to present the mobile web flow. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) @objc(getCredentialWithUIDelegate:completion:) open func credential(with uiDelegate: AuthUIDelegate?) async throws -> AuthCredential { @@ -263,18 +233,16 @@ import Foundation } #endif - /** @fn appleCredentialWithIDToken:rawNonce:fullName: - * @brief Creates an `AuthCredential` for the Sign in with Apple OAuth 2 provider identified by ID - * token, raw nonce, and full name. This method is specific to the Sign in with Apple OAuth 2 - * provider as this provider requires the full name to be passed explicitly. - * - * @param idToken The IDToken associated with the Sign in with Apple Auth credential being created. - * @param rawNonce The raw nonce associated with the Sign in with Apple Auth credential being - * created. - * @param fullName The full name associated with the Sign in with Apple Auth credential being - * created. - * @return An `AuthCredential`. - */ + /// Creates an `AuthCredential` for the Sign in with Apple OAuth 2 provider identified by ID + /// token, raw nonce, and full name.This method is specific to the Sign in with Apple OAuth 2 + /// provider as this provider requires the full name to be passed explicitly. + /// - Parameter idToken: The IDToken associated with the Sign in with Apple Auth credential being + /// created. + /// - Parameter rawNonce: The raw nonce associated with the Sign in with Apple Auth credential + /// being created. + /// - Parameter fullName: The full name associated with the Sign in with Apple Auth credential + /// being created. + /// - Returns: An AuthCredential. @objc(appleCredentialWithIDToken:rawNonce:fullName:) public static func appleCredential(withIDToken idToken: String, rawNonce: String?, @@ -287,12 +255,9 @@ import Foundation // MARK: - Private Methods - /** @fn OAuthResponseForURL:error: - @brief Parses the redirected URL and returns a string representation of the OAuth response URL. - @param URL The url to be parsed for an OAuth response URL. - @param error The error that occurred if any. - @return The OAuth response if successful. - */ + /// Parses the redirected URL and returns a string representation of the OAuth response URL. + /// - Parameter url: The url to be parsed for an OAuth response URL. + /// - Returns: The OAuth response if successful. private func oAuthResponseForURL(url: URL) -> (String?, Error?) { var urlQueryItems = AuthWebUtils.dictionary(withHttpArgumentsString: url.query) if let item = urlQueryItems["deep_link_id"], @@ -317,14 +282,11 @@ import Foundation )) } - /** @fn getHeadfulLiteURLWithEventID - @brief Constructs a URL used for opening a headful-lite flow using a given event - ID and session ID. - @param eventID The event ID used for this purpose. - @param sessionID The session ID used when completing the headful lite flow. - @param completion The callback invoked after the URL has been constructed or an error - has been encountered. - */ + /// Constructs a URL used for opening a headful-lite flow using a given event + /// ID and session ID. + /// - Parameter eventID: The event ID used for this purpose. + /// - Parameter sessionID: The session ID used when completing the headful lite flow. + /// - Returns: A url. private func getHeadfulLiteUrl(eventID: String, sessionID: String) async throws -> URL? { let authDomain = try await AuthWebUtils @@ -395,11 +357,9 @@ import Foundation return components?.url } - /** @fn hashforString: - @brief Returns the SHA256 hash representation of a given string object. - @param string The string for which a SHA256 hash is desired. - @return An hexadecimal string representation of the SHA256 hash. - */ + /// Returns the SHA256 hash representation of a given string object. + /// - Parameter string: The string for which a SHA256 hash is desired. + /// - Returns: An hexadecimal string representation of the SHA256 hash. private func hash(forString string: String) -> String { guard let sessionIdData = string.data(using: .utf8) as? NSData else { fatalError("FirebaseAuth Internal error: Failed to create hash for sessionID") diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthCredential.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthCredential.swift index 0b55c5979e7..2b42df11e7d 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthCredential.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthCredential.swift @@ -14,10 +14,9 @@ import Foundation -/** @class PhoneAuthCredential - @brief Implementation of FIRAuthCredential for Phone Auth credentials. - This class is available on iOS only. - */ +/// Implementation of AuthCredential for Phone Auth credentials. +/// +/// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRPhoneAuthCredential) open class PhoneAuthCredential: AuthCredential, NSSecureCoding { enum CredentialKind { diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift index f6bd4d1228e..8a12d7cb5cf 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift @@ -15,47 +15,40 @@ import FirebaseCore import Foundation -/** - @brief A concrete implementation of `AuthProvider` for phone auth providers. - This class is available on iOS only. - */ +/// A concrete implementation of `AuthProvider` for phone auth providers. +/// +/// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRPhoneAuthProvider) open class PhoneAuthProvider: NSObject { + /// A string constant identifying the phone identity provider. @objc public static let id = "phone" #if os(iOS) - /** - @brief Returns an instance of `PhoneAuthProvider` for the default `Auth` object. - */ + /// Returns an instance of `PhoneAuthProvider` for the default `Auth` object. @objc(provider) open class func provider() -> PhoneAuthProvider { return PhoneAuthProvider(auth: Auth.auth()) } - /** - @brief Returns an instance of `PhoneAuthProvider` for the provided `Auth` object. - @param auth The auth object to associate with the phone auth provider instance. - */ + /// Returns an instance of `PhoneAuthProvider` for the provided `Auth` object. + /// - Parameter auth: The auth object to associate with the phone auth provider instance. @objc(providerWithAuth:) open class func provider(auth: Auth) -> PhoneAuthProvider { return PhoneAuthProvider(auth: auth) } - /** - @brief Starts the phone number authentication flow by sending a verification code to the - specified phone number. - @param phoneNumber The phone number to be verified. - @param uiDelegate An object used to present the SFSafariViewController. The object is retained - by this method until the completion block is executed. - @param completion The callback to be invoked when the verification flow is finished. - @remarks Possible error codes: - - + `AuthErrorCodeCaptchaCheckFailed` - Indicates that the reCAPTCHA token obtained by - the Firebase Auth is invalid or has expired. - + `AuthErrorCodeQuotaExceeded` - Indicates that the phone verification quota for this - project has been exceeded. - + `AuthErrorCodeInvalidPhoneNumber` - Indicates that the phone number provided is - invalid. - + `AuthErrorCodeMissingPhoneNumber` - Indicates that a phone number was not provided. - */ + /// Starts the phone number authentication flow by sending a verification code to the + /// specified phone number. + /// + /// Possible error codes: + /// * `AuthErrorCodeCaptchaCheckFailed` - Indicates that the reCAPTCHA token obtained by + /// the Firebase Auth is invalid or has expired. + /// * `AuthErrorCodeQuotaExceeded` - Indicates that the phone verification quota for this + /// project has been exceeded. + /// * `AuthErrorCodeInvalidPhoneNumber` - Indicates that the phone number provided is invalid. + /// * `AuthErrorCodeMissingPhoneNumber` - Indicates that a phone number was not provided. + /// - Parameter phoneNumber: The phone number to be verified. + /// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is + /// retained by this method until the completion block is executed. + /// - Parameter completion: The callback to be invoked when the verification flow is finished. @objc(verifyPhoneNumber:UIDelegate:completion:) open func verifyPhoneNumber(_ phoneNumber: String, uiDelegate: AuthUIDelegate? = nil, @@ -66,16 +59,14 @@ import Foundation completion: completion) } - /** - @brief Verify ownership of the second factor phone number by the current user. - @param phoneNumber The phone number to be verified. - @param uiDelegate An object used to present the SFSafariViewController. The object is retained - by this method until the completion block is executed. - @param multiFactorSession A session to identify the MFA flow. For enrollment, this identifies the user - trying to enroll. For sign-in, this identifies that the user already passed the first - factor challenge. - @param completion The callback to be invoked when the verification flow is finished. - */ + /// Verify ownership of the second factor phone number by the current user. + /// - Parameter phoneNumber: The phone number to be verified. + /// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is + /// retained by this method until the completion block is executed. + /// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this + /// identifies the user trying to enroll. For sign-in, this identifies that the user already + /// passed the first factor challenge. + /// - Parameter completion: The callback to be invoked when the verification flow is finished. @objc(verifyPhoneNumber:UIDelegate:multiFactorSession:completion:) open func verifyPhoneNumber(_ phoneNumber: String, uiDelegate: AuthUIDelegate? = nil, @@ -103,16 +94,14 @@ import Foundation } } - /** - @brief Verify ownership of the second factor phone number by the current user. - @param phoneNumber The phone number to be verified. - @param uiDelegate An object used to present the SFSafariViewController. The object is retained - by this method until the completion block is executed. - @param multiFactorSession A session to identify the MFA flow. For enrollment, this identifies the user - trying to enroll. For sign-in, this identifies that the user already passed the first - factor challenge. - @returns The verification ID - */ + /// Verify ownership of the second factor phone number by the current user. + /// - Parameter phoneNumber: The phone number to be verified. + /// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is + /// retained by this method until the completion block is executed. + /// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this + /// identifies the user trying to enroll. For sign-in, this identifies that the user already + /// passed the first factor challenge. + /// - Returns: The verification ID @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open func verifyPhoneNumber(_ phoneNumber: String, uiDelegate: AuthUIDelegate? = nil, @@ -131,16 +120,14 @@ import Foundation } } - /** - @brief Verify ownership of the second factor phone number by the current user. - @param multiFactorInfo The phone multi factor whose number need to be verified. - @param uiDelegate An object used to present the SFSafariViewController. The object is retained - by this method until the completion block is executed. - @param multiFactorSession A session to identify the MFA flow. For enrollment, this identifies the user - trying to enroll. For sign-in, this identifies that the user already passed the first - factor challenge. - @param completion The callback to be invoked when the verification flow is finished. - */ + /// Verify ownership of the second factor phone number by the current user. + /// - Parameter multiFactorInfo: The phone multi factor whose number need to be verified. + /// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is + /// retained by this method until the completion block is executed. + /// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this + /// identifies the user trying to enroll. For sign-in, this identifies that the user already + /// passed the first factor challenge. + /// - Parameter completion: The callback to be invoked when the verification flow is finished. @objc(verifyPhoneNumberWithMultiFactorInfo:UIDelegate:multiFactorSession:completion:) open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo, uiDelegate: AuthUIDelegate? = nil, @@ -153,6 +140,14 @@ import Foundation completion: completion) } + /// Verify ownership of the second factor phone number by the current user. + /// - Parameter multiFactorInfo: The phone multi factor whose number need to be verified. + /// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is + /// retained by this method until the completion block is executed. + /// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this + /// identifies the user trying to enroll. For sign-in, this identifies that the user already + /// passed the first factor challenge. + /// - Returns: The verification ID. @available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *) open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo, uiDelegate: AuthUIDelegate? = nil, @@ -170,16 +165,14 @@ import Foundation } } - /** - @brief Creates an `AuthCredential` for the phone number provider identified by the - verification ID and verification code. - - @param verificationID The verification ID obtained from invoking - verifyPhoneNumber:completion: - @param verificationCode The verification code obtained from the user. - @return The corresponding phone auth credential for the verification ID and verification code - provided. - */ + /// Creates an `AuthCredential` for the phone number provider identified by the + /// verification ID and verification code. + /// + /// - Parameter verificationID: The verification ID obtained from invoking + /// verifyPhoneNumber:completion: + /// - Parameter verificationCode: The verification code obtained from the user. + /// - Returns: The corresponding phone auth credential for the verification ID and verification + /// code provided. @objc(credentialWithVerificationID:verificationCode:) open func credential(withVerificationID verificationID: String, verificationCode: String) -> PhoneAuthCredential { @@ -207,14 +200,12 @@ import Foundation uiDelegate: uiDelegate) } - /** @fn - @brief Starts the flow to verify the client via silent push notification. - @param retryOnInvalidAppCredential Whether of not the flow should be retried if an - AuthErrorCodeInvalidAppCredential error is returned from the backend. - @param phoneNumber The phone number to be verified. - @param callback The callback to be invoked on the global work queue when the flow is - finished. - */ + /// Starts the flow to verify the client via silent push notification. + /// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an + /// AuthErrorCodeInvalidAppCredential error is returned from the backend. + /// - Parameter phoneNumber: The phone number to be verified. + /// - Parameter callback: The callback to be invoked on the global work queue when the flow is + /// finished. private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String, retryOnInvalidAppCredential: Bool, uiDelegate: AuthUIDelegate?) async throws @@ -237,14 +228,10 @@ import Foundation } } - /** @fn - @brief Starts the flow to verify the client via silent push notification. - @param retryOnInvalidAppCredential Whether of not the flow should be retried if an - AuthErrorCodeInvalidAppCredential error is returned from the backend. - @param phoneNumber The phone number to be verified. - @param callback The callback to be invoked on the global work queue when the flow is - finished. - */ + /// Starts the flow to verify the client via silent push notification. + /// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an + /// AuthErrorCodeInvalidAppCredential error is returned from the backend. + /// - Parameter phoneNumber: The phone number to be verified. private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String, retryOnInvalidAppCredential: Bool, multiFactorSession session: MultiFactorSession?, @@ -316,10 +303,7 @@ import Foundation throw error } - /** @fn - @brief Continues the flow to verify the client via silent push notification. - @param completion The callback to be invoked when the client verification flow is finished. - */ + /// Continues the flow to verify the client via silent push notification. private func verifyClient(withUIDelegate uiDelegate: AuthUIDelegate?) async throws -> CodeIdentity { // Remove the simulator check below after FCM supports APNs in simulators @@ -374,10 +358,7 @@ import Foundation } } - /** @fn - @brief Continues the flow to verify the client via silent push notification. - @param completion The callback to be invoked when the client verification flow is finished. - */ + /// Continues the flow to verify the client via silent push notification. private func reCAPTCHAFlowWithUIDelegate(withUIDelegate uiDelegate: AuthUIDelegate?) async throws -> String { let eventID = AuthWebUtils.randomString(withLength: 10) @@ -412,12 +393,9 @@ import Foundation } } - /** - @brief Parses the reCAPTCHA URL and returns the reCAPTCHA token. - @param URL The url to be parsed for a reCAPTCHA token. - @param error The error that occurred if any. - @return The reCAPTCHA token if successful. - */ + /// Parses the reCAPTCHA URL and returns the reCAPTCHA token. + /// - Parameter url: The url to be parsed for a reCAPTCHA token. + /// - Returns: The reCAPTCHA token if successful. private func reCAPTCHAToken(forURL url: URL?) throws -> String { guard let url = url else { let reason = "Internal Auth Error: nil URL trying to access RECAPTCHA token" @@ -457,13 +435,8 @@ import Foundation throw AuthErrorUtils.appVerificationUserInteractionFailure(reason: reason) } - /** @fn - @brief Constructs a URL used for opening a reCAPTCHA app verification flow using a given event - ID. - @param eventID The event ID used for this purpose. - @param completion The callback invoked after the URL has been constructed or an error - has been encountered. - */ + /// Constructs a URL used for opening a reCAPTCHA app verification flow using a given event ID. + /// - Parameter eventID: The event ID used for this purpose. private func reCAPTCHAURL(withEventID eventID: String) async throws -> URL? { let authDomain = try await AuthWebUtils .fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration) diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/TwitterAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/TwitterAuthProvider.swift index dd1984892d1..177b08ed894 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/TwitterAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/TwitterAuthProvider.swift @@ -14,20 +14,16 @@ import Foundation -/** - @brief Utility class for constructing Twitter Sign In credentials. - */ +/// Utility class for constructing Twitter Sign In credentials. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRTwitterAuthProvider) open class TwitterAuthProvider: NSObject { + /// A string constant identifying the Twitter identity provider. @objc public static let id = "twitter.com" - /** - @brief Creates an `AuthCredential` for a Twitter sign in. - - @param token The Twitter OAuth token. - @param secret The Twitter OAuth secret. - @return An AuthCredential containing the Twitter credentials. - */ + /// Creates an `AuthCredential` for a Twitter sign in. + /// - Parameter token: The Twitter OAuth token. + /// - Parameter secret: The Twitter OAuth secret. + /// - Returns: An AuthCredential containing the Twitter credentials. @objc open class func credential(withToken token: String, secret: String) -> AuthCredential { return TwitterAuthCredential(withToken: token, secret: secret) } diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index ba7bb4e6137..8aa81c56419 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -24,15 +24,12 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) protocol AuthBackendRPCIssuer: NSObjectProtocol { - /** @fn - @brief Asynchronously sends a HTTP request. - @param requestConfiguration The request to be made. - @param URL The request URL. - @param body Request body. - @param contentType Content type of the body. - @param handler provided that handles HTTP response. Invoked asynchronously on the auth global - work queue in the future. - */ + /// Asynchronously send a HTTP request. + /// - Parameter request: The request to be made. + /// - Parameter body: Request body. + /// - Parameter contentType: Content type of the body. + /// - Parameter completionHandler: Handles HTTP response. Invoked asynchronously + /// on the auth global work queue in the future. func asyncCallToURL(with request: T, body: Data?, contentType: String, @@ -152,19 +149,16 @@ private class AuthBackendRPCImplementation: NSObject, AuthBackendImplementation rpcIssuer = AuthBackendRPCIssuerImplementation() } - /** @fn call - @brief Calls the RPC using HTTP request. - @remarks Possible error responses: - @see FIRAuthInternalErrorCodeRPCRequestEncodingError - @see FIRAuthInternalErrorCodeJSONSerializationError - @see FIRAuthInternalErrorCodeNetworkError - @see FIRAuthInternalErrorCodeUnexpectedErrorResponse - @see FIRAuthInternalErrorCodeUnexpectedResponse - @see FIRAuthInternalErrorCodeRPCResponseDecodingError - @param request The request. - @param response The empty response to be filled. - @param callback The callback for both success and failure. - */ + /// Calls the RPC using HTTP request. + /// Possible error responses: + /// * See FIRAuthInternalErrorCodeRPCRequestEncodingError + /// * See FIRAuthInternalErrorCodeJSONSerializationError + /// * See FIRAuthInternalErrorCodeNetworkError + /// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse + /// * See FIRAuthInternalErrorCodeUnexpectedResponse + /// * See FIRAuthInternalErrorCodeRPCResponseDecodingError + /// - Parameter request: The request. + /// - Returns: The response. fileprivate func call(with request: T) async throws -> T.Response { let response = try await callInternal(with: request) if let auth = request.requestConfiguration().auth, @@ -233,19 +227,17 @@ private class AuthBackendRPCImplementation: NSObject, AuthBackendImplementation } #endif - /** @fn call - @brief Calls the RPC using HTTP request. - @remarks Possible error responses: - @see FIRAuthInternalErrorCodeRPCRequestEncodingError - @see FIRAuthInternalErrorCodeJSONSerializationError - @see FIRAuthInternalErrorCodeNetworkError - @see FIRAuthInternalErrorCodeUnexpectedErrorResponse - @see FIRAuthInternalErrorCodeUnexpectedResponse - @see FIRAuthInternalErrorCodeRPCResponseDecodingError - @param request The request. - @param response The empty response to be filled. - @param callback The callback for both success and failure. - */ + /// Calls the RPC using HTTP request. + /// + /// Possible error responses: + /// * See FIRAuthInternalErrorCodeRPCRequestEncodingError + /// * See FIRAuthInternalErrorCodeJSONSerializationError + /// * See FIRAuthInternalErrorCodeNetworkError + /// * See FIRAuthInternalErrorCodeUnexpectedErrorResponse + /// * See FIRAuthInternalErrorCodeUnexpectedResponse + /// * See FIRAuthInternalErrorCodeRPCResponseDecodingError + /// - Parameter request: The request. + /// - Returns: The response. fileprivate func callInternal(with request: T) async throws -> T.Response { var bodyData: Data? if request.containsPostBody { diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthRPCResponse.swift b/FirebaseAuth/Sources/Swift/Backend/AuthRPCResponse.swift index 02651cf818a..b1284762fb2 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRPCResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRPCResponse.swift @@ -18,21 +18,17 @@ protocol AuthRPCResponse { /// Bare initializer for a response. init() - /** @fn setFieldsWithDictionary:error: - @brief Sets the response instance from the decoded JSON response. - @param dictionary The dictionary decoded from HTTP JSON response. - @param error An out field for an error which occurred constructing the request. - @return Whether the operation was successful or not. - */ + /// Sets the response instance from the decoded JSON response. + /// - Parameter dictionary: The dictionary decoded from HTTP JSON response. + /// - Parameter error: An out field for an error which occurred constructing the request. + /// - Returns: Whether the operation was successful or not. func setFields(dictionary: [String: AnyHashable]) throws - /** @fn clientErrorWithshortErrorMessage:detailErrorMessage - @brief This optional method allows response classes to create client errors given a short error - message and a detail error message from the server. - @param shortErrorMessage The short error message from the server. - @param detailErrorMessage The detailed error message from the server. - @return A client error, if any. - */ + /// This optional method allows response classes to create client errors given a short error + /// message and a detail error message from the server. + /// - Parameter shortErrorMessage: The short error message from the server. + /// - Parameter detailErrorMessage: The detailed error message from the server. + /// - Returns: A client error, if any. func clientError(shortErrorMessage: String, detailedErrorMessage: String?) -> Error? } diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift index 80ce4a79ac7..9aa53d7e68f 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift @@ -17,52 +17,34 @@ import Foundation import FirebaseAppCheckInterop import FirebaseCoreExtension -/** @class FIRAuthRequestConfiguration - @brief Defines configurations to be added to a request to Firebase Auth's backend. - */ +/// Defines configurations to be added to a request to Firebase Auth's backend. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AuthRequestConfiguration: NSObject { - /** @property APIKey - @brief The Firebase Auth API key used in the request. - */ + /// The Firebase Auth API key used in the request. let apiKey: String - /** @property LanguageCode - @brief The language code used in the request. - */ + /// The language code used in the request. var languageCode: String? - /** @property appID - @brief The Firebase appID used in the request. - */ + /// The Firebase appID used in the request. let appID: String - /** @property auth - @brief The FIRAuth instance used in the request. - */ + /// The `Auth` instance used in the request. weak var auth: Auth? /// The heartbeat logger used to add heartbeats to the corresponding request's header. var heartbeatLogger: FIRHeartbeatLoggerProtocol? - /** @property appCheck - @brief The appCheck is used to generate a token. - */ + /// The appCheck is used to generate a token. var appCheck: AppCheckInterop? - /** @property HTTPMethod - @brief The HTTP method used in the request. - */ + /// The HTTP method used in the request. var httpMethod: String - /** @property additionalFrameworkMarker - @brief Additional framework marker that will be added as part of the header of every request. - */ + /// Additional framework marker that will be added as part of the header of every request. var additionalFrameworkMarker: String? - /** @property emulatorHostAndPort - @brief If set, the local emulator host and port to point to instead of the remote backend. - */ + /// If set, the local emulator host and port to point to instead of the remote backend. var emulatorHostAndPort: String? init(apiKey: String, diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index b31292c807e..ebc549b3288 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -40,19 +40,13 @@ class IdentityToolkitRequest { /// The tenant ID of the request. nil if none is available. let tenantID: String? - /** @property useIdentityPlatform - @brief The toggle of using Identity Platform endpoints. - */ + /// The toggle of using Identity Platform endpoints. let useIdentityPlatform: Bool - /** @property useStaging - @brief The toggle of using staging endpoints. - */ + /// The toggle of using staging endpoints. let useStaging: Bool - /** @property clientType - @brief The type of the client that the request sent from, which should be CLIENT_TYPE_IOS; - */ + /// The type of the client that the request sent from, which should be CLIENT_TYPE_IOS; var clientType: String private let _requestConfiguration: AuthRequestConfiguration diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift index 6fadfcfde04..1a7503ced9a 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIRequest.swift @@ -14,95 +14,61 @@ import Foundation -/** @var kCreateAuthURIEndpoint - @brief The "createAuthUri" endpoint. - */ +/// The "createAuthUri" endpoint. private let kCreateAuthURIEndpoint = "createAuthUri" -/** @var kProviderIDKey - @brief The key for the "providerId" value in the request. - */ +/// The key for the "providerId" value in the request. private let kProviderIDKey = "providerId" -/** @var kIdentifierKey - @brief The key for the "identifier" value in the request. - */ +/// The key for the "identifier" value in the request. private let kIdentifierKey = "identifier" -/** @var kContinueURIKey - @brief The key for the "continueUri" value in the request. - */ +/// The key for the "continueUri" value in the request. private let kContinueURIKey = "continueUri" -/** @var kOpenIDRealmKey - @brief The key for the "openidRealm" value in the request. - */ +/// The key for the "openidRealm" value in the request. private let kOpenIDRealmKey = "openidRealm" -/** @var kClientIDKey - @brief The key for the "clientId" value in the request. - */ +/// The key for the "clientId" value in the request. private let kClientIDKey = "clientId" -/** @var kContextKey - @brief The key for the "context" value in the request. - */ +/// The key for the "context" value in the request. private let kContextKey = "context" -/** @var kAppIDKey - @brief The key for the "appId" value in the request. - */ +/// The key for the "appId" value in the request. private let kAppIDKey = "appId" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" -/** @class FIRCreateAuthURIRequest - @brief Represents the parameters for the createAuthUri endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri - */ +/// Represents the parameters for the createAuthUri endpoint. +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class CreateAuthURIRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = CreateAuthURIResponse - /** @property identifier - @brief The email or federated ID of the user. - */ + /// The email or federated ID of the user. let identifier: String - /** @property continueURI - @brief The URI to which the IDP redirects the user after the federated login flow. - */ + /// The URI to which the IDP redirects the user after the federated login flow. let continueURI: String - /** @property openIDRealm - @brief Optional realm for OpenID protocol. The sub string "scheme://domain:port" of the param - "continueUri" is used if this is not set. - */ + /// Optional realm for OpenID protocol. The sub string "scheme://domain:port" of the param + /// "continueUri" is used if this is not set. var openIDRealm: String? - /** @property providerID - @brief The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com, - live.net and yahoo.com. For other OpenID IdPs it's the OP identifier. - */ + /// The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com, + /// live.net and yahoo.com. For other OpenID IdPs it's the OP identifier. var providerID: String? - /** @property clientID - @brief The relying party OAuth client ID. - */ + /// The relying party OAuth client ID. var clientID: String? - /** @property context - @brief The opaque value used by the client to maintain context info between the authentication - request and the IDP callback. - */ + /// The opaque value used by the client to maintain context info between the authentication + /// request and the IDP callback. var context: String? - /** @property appID - @brief The iOS client application's bundle identifier. - */ + /// The iOS client application's bundle identifier. var appID: String? init(identifier: String, continueURI: String, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIResponse.swift index b5cdef16482..d80b09e1aaa 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/CreateAuthURIResponse.swift @@ -14,40 +14,26 @@ import Foundation -/** @class FIRCreateAuthURIResponse - @brief Represents the parameters for the createAuthUri endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri - */ +/// Represents the parameters for the createAuthUri endpoint. +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/createAuthUri class CreateAuthURIResponse: AuthRPCResponse { - /** @property authUri - @brief The URI used by the IDP to authenticate the user. - */ + /// The URI used by the IDP to authenticate the user. var authURI: String? - /** @property registered - @brief Whether the user is registered if the identifier is an email. - */ + /// Whether the user is registered if the identifier is an email. var registered: Bool = false - /** @property providerId - @brief The provider ID of the auth URI. - */ + /// The provider ID of the auth URI. var providerID: String? - /** @property forExistingProvider - @brief True if the authUri is for user's existing provider. - */ + /// True if the authUri is for user's existing provider. var forExistingProvider: Bool = false - /** @property allProviders - @brief A list of provider IDs the passed @c identifier could use to sign in with. - */ + /// A list of provider IDs the passed identifier could use to sign in with. var allProviders: [String]? - /** @property signinMethods - @brief A list of sign-in methods available for the passed @c identifier. - */ + /// A list of sign-in methods available for the passed identifier. var signinMethods: [String]? /// Bare initializer. diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift index bf059cb7967..430b74a4b76 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountRequest.swift @@ -14,34 +14,25 @@ import Foundation -/** @var kCreateAuthURIEndpoint - @brief The "deleteAccount" endpoint. - */ +/// The "deleteAccount" endpoint. + private let kDeleteAccountEndpoint = "deleteAccount" -/** @var kIDTokenKey - @brief The key for the "idToken" value in the request. This is actually the STS Access Token, - despite it's confusing (backwards compatiable) parameter name. - */ +/// The key for the "idToken" value in the request. This is actually the STS Access Token, +/// despite its confusing (backwards compatiable) parameter name. private let kIDTokenKey = "idToken" -/** @var kLocalIDKey - @brief The key for the "localID" value in the request. - */ +/// The key for the "localID" value in the request. private let kLocalIDKey = "localId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class DeleteAccountRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = DeleteAccountResponse - /** @var _accessToken - @brief The STS Access Token of the authenticated user. - */ + /// The STS Access Token of the authenticated user. let accessToken: String - /** @var _localID - @brief The localID of the user. - */ + /// The localID of the user. let localID: String init(localID: String, accessToken: String, requestConfiguration: AuthRequestConfiguration) { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountResponse.swift index c5d80c863a2..92208e7581b 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/DeleteAccountResponse.swift @@ -14,10 +14,9 @@ import Foundation -/** @class FIRDeleteAccountResponse - @brief Represents the response from the deleteAccount endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount - */ +/// Represents the response from the deleteAccount endpoint. +/// +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount class DeleteAccountResponse: NSObject, AuthRPCResponse { override required init() {} diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift index cfbf1247a25..b57c2170450 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInRequest.swift @@ -14,34 +14,22 @@ import Foundation -/** @var kEmailLinkSigninEndpoint - @brief The "EmailLinkSignin" endpoint. - */ +/// The "EmailLinkSignin" endpoint. private let kEmailLinkSigninEndpoint = "emailLinkSignin" -/** @var kEmailKey - @brief The key for the "identifier" value in the request. - */ +/// The key for the "identifier" value in the request. private let kEmailKey = "email" -/** @var kEmailLinkKey - @brief The key for the "emailLink" value in the request. - */ +/// The key for the "emailLink" value in the request. private let kOOBCodeKey = "oobCode" -/** @var kIDTokenKey - @brief The key for the "IDToken" value in the request. - */ +/// The key for the "IDToken" value in the request. private let kIDTokenKey = "idToken" -/** @var kPostBodyKey - @brief The key for the "postBody" value in the request. - */ +/// The key for the "postBody" value in the request. private let kPostBodyKey = "postBody" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @@ -50,15 +38,10 @@ class EmailLinkSignInRequest: IdentityToolkitRequest, AuthRPCRequest { let email: String - /** @property oobCode - @brief The OOB code used to complete the email link sign-in flow. - */ + /// The OOB code used to complete the email link sign-in flow. let oobCode: String - /** @property IDToken - @brief The ID Token code potentially used to complete the email link sign-in flow. - */ - + /// The ID Token code potentially used to complete the email link sign-in flow. var idToken: String? init(email: String, oobCode: String, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInResponse.swift index f41d0bde732..4147c4962e0 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/EmailLinkSignInResponse.swift @@ -14,48 +14,32 @@ import Foundation -/** @class FIRVerifyAssertionResponse - @brief Represents the response from the emailLinkSignin endpoint. - */ +/// Represents the response from the emailLinkSignin endpoint. class EmailLinkSignInResponse: NSObject, AuthRPCResponse, AuthMFAResponse { override required init() {} - /** @property IDToken - @brief The ID token in the email link sign-in response. - */ + /// The ID token in the email link sign-in response. private(set) var idToken: String? - /** @property email - @brief The email returned by the IdP. - */ + /// The email returned by the IdP. var email: String? - /** @property refreshToken - @brief The refreshToken returned by the server. - */ + /// The refreshToken returned by the server. var refreshToken: String? - /** @property approximateExpirationDate - @brief The approximate expiration date of the access token. - */ + /// The approximate expiration date of the access token. var approximateExpirationDate: Date? - /** @property isNewUser - @brief Flag indicating that the user signing in is a new user and not a returning user. - */ + /// Flag indicating that the user signing in is a new user and not a returning user. var isNewUser: Bool = false // MARK: - AuthMFAResponse - /** @property MFAPendingCredential - @brief An opaque string that functions as proof that the user has successfully passed the first - factor check. - */ + /// An opaque string that functions as proof that the user has successfully passed the first + /// factor check. private(set) var mfaPendingCredential: String? - /** @property MFAInfo - @brief Info on which multi-factor authentication providers are enabled. - */ + /// Info on which multi-factor authentication providers are enabled. private(set) var mfaInfo: [AuthProtoMFAEnrollment]? func setFields(dictionary: [String: AnyHashable]) throws { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift index 9a0e19af0be..ce94425d311 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoRequest.swift @@ -14,35 +14,26 @@ import Foundation -/** @var kGetAccountInfoEndpoint - @brief The "getAccountInfo" endpoint. - */ +/// The "getAccountInfo" endpoint. private let kGetAccountInfoEndpoint = "getAccountInfo" -/** @var kIDTokenKey - @brief The key for the "idToken" value in the request. This is actually the STS Access Token, - despite it's confusing (backwards compatiable) parameter name. - */ +/// The key for the "idToken" value in the request. This is actually the STS Access Token, +/// despite its confusing (backwards compatiable) parameter name. private let kIDTokenKey = "idToken" -/** @class FIRGetAccountInfoRequest - @brief Represents the parameters for the getAccountInfo endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo - */ +/// Represents the parameters for the getAccountInfo endpoint. +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class GetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = GetAccountInfoResponse - /** @property accessToken - @brief The STS Access Token for the authenticated user. - */ + /// The STS Access Token for the authenticated user. let accessToken: String - /** @fn initWithAccessToken:requestConfiguration - @brief Designated initializer. - @param accessToken The Access Token of the authenticated user. - @param requestConfiguration An object containing configurations to be added to the request. - */ + /// Designated initializer. + /// - Parameter accessToken: The Access Token of the authenticated user. + /// - Parameter requestConfiguration: An object containing configurations to be added to the + /// request. init(accessToken: String, requestConfiguration: AuthRequestConfiguration) { self.accessToken = accessToken super.init(endpoint: kGetAccountInfoEndpoint, requestConfiguration: requestConfiguration) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoResponse.swift index 07413e94c84..2f39fdfe68d 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoResponse.swift @@ -14,50 +14,32 @@ import Foundation -/** @var kErrorKey - @brief The key for the "error" value in JSON responses from the server. - */ +/// The key for the "error" value in JSON responses from the server. private let kErrorKey = "error" -/** @class FIRGetAccountInfoResponseProviderUserInfo - @brief Represents the provider user info part of the response from the getAccountInfo endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo - */ +/// Represents the provider user info part of the response from the getAccountInfo endpoint. +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo class GetAccountInfoResponseProviderUserInfo: NSObject { - /** @property providerID - @brief The ID of the identity provider. - */ + /// The ID of the identity provider. let providerID: String? - /** @property displayName - @brief The user's display name at the identity provider. - */ + /// The user's display name at the identity provider. let displayName: String? - /** @property photoURL - @brief The user's photo URL at the identity provider. - */ + /// The user's photo URL at the identity provider. let photoURL: URL? - /** @property federatedID - @brief The user's identifier at the identity provider. - */ + /// The user's identifier at the identity provider. let federatedID: String? - /** @property email - @brief The user's email at the identity provider. - */ + /// The user's email at the identity provider. let email: String? - /** @property phoneNumber - @brief A phone number associated with the user. - */ + /// A phone number associated with the user. let phoneNumber: String? - /** @fn initWithAPIKey: - @brief Designated initializer. - @param dictionary The provider user info data from endpoint. - */ + /// Designated initializer. + /// - Parameter dictionary: The provider user info data from endpoint. init(dictionary: [String: Any]) { providerID = dictionary["providerId"] as? String displayName = dictionary["displayName"] as? String @@ -73,69 +55,44 @@ class GetAccountInfoResponseProviderUserInfo: NSObject { } } -/** @class FIRGetAccountInfoResponseUser - @brief Represents the firebase user info part of the response from the getAccountInfo endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo - */ +/// Represents the firebase user info part of the response from the getAccountInfo endpoint. +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo class GetAccountInfoResponseUser: NSObject { - /** @property localID - @brief The ID of the user. - */ + /// The ID of the user. let localID: String? - /** @property email - @brief The email or the user. - */ + /// The email or the user. let email: String? - /** @property emailVerified - @brief Whether the email has been verified. - */ + /// Whether the email has been verified. let emailVerified: Bool - /** @property displayName - @brief The display name of the user. - */ + /// The display name of the user. let displayName: String? - /** @property photoURL - @brief The user's photo URL. - */ + /// The user's photo URL. let photoURL: URL? - /** @property creationDate - @brief The user's creation date. - */ + /// The user's creation date. let creationDate: Date? - /** @property lastSignInDate - @brief The user's last login date. - */ + /// The user's last login date. let lastLoginDate: Date? - /** @property providerUserInfo - @brief The user's profiles at the associated identity providers. - */ + /// The user's profiles at the associated identity providers. let providerUserInfo: [GetAccountInfoResponseProviderUserInfo]? - /** @property passwordHash - @brief Information about user's password. - @remarks This is not necessarily the hash of user's actual password. - */ - + /// Information about user's password. + /// This is not necessarily the hash of user's actual password. let passwordHash: String? - /** @property phoneNumber - @brief A phone number associated with the user. - */ + /// A phone number associated with the user. let phoneNumber: String? let mfaEnrollments: [AuthProtoMFAEnrollment]? - /** @fn initWithAPIKey: - @brief Designated initializer. - @param dictionary The provider user info data from endpoint. - */ + /// Designated initializer. + /// - Parameter dictionary: The provider user info data from endpoint. init(dictionary: [String: Any]) { if let providerUserInfoData = dictionary["providerUserInfo"] as? [[String: Any]] { providerUserInfo = providerUserInfoData.map { @@ -179,17 +136,13 @@ class GetAccountInfoResponseUser: NSObject { } } -/** @class FIRGetAccountInfoResponse - @brief Represents the response from the setAccountInfo endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo - */ +/// Represents the response from the setAccountInfo endpoint. +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/getAccountInfo @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class GetAccountInfoResponse: AuthRPCResponse { required init() {} - /** @property providerUserInfo - @brief The requested users' profiles. - */ + /// The requested users' profiles. var users: [GetAccountInfoResponseUser]? func setFields(dictionary: [String: AnyHashable]) throws { guard let usersData = dictionary["users"] as? [[String: AnyHashable]] else { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift index 7b33352e102..bd3925d1f67 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift @@ -15,24 +15,16 @@ import Foundation enum GetOOBConfirmationCodeRequestType: Int { - /** @var FIRGetOOBConfirmationCodeRequestTypePasswordReset - @brief Requests a password reset code. - */ + /// Requests a password reset code. case passwordReset - /** @var FIRGetOOBConfirmationCodeRequestTypeVerifyEmail - @brief Requests an email verification code. - */ + /// Requests an email verification code. case verifyEmail - /** @var FIRGetOOBConfirmationCodeRequestTypeEmailLink - @brief Requests an email sign-in link. - */ + /// Requests an email sign-in link. case emailLink - /** @var FIRGetOOBConfirmationCodeRequestTypeVerifyBeforeUpdateEmail - @brief Requests an verify before update email. - */ + /// Requests an verify before update email. case verifyBeforeUpdateEmail var value: String { @@ -51,187 +43,118 @@ enum GetOOBConfirmationCodeRequestType: Int { private let kGetOobConfirmationCodeEndpoint = "getOobConfirmationCode" -/** @var kRequestTypeKey - @brief The name of the required "requestType" property in the request. - */ +/// The name of the required "requestType" property in the request. private let kRequestTypeKey = "requestType" -/** @var kEmailKey - @brief The name of the "email" property in the request. - */ +/// The name of the "email" property in the request. private let kEmailKey = "email" -/** @var kNewEmailKey - @brief The name of the "newEmail" property in the request. - */ +/// The name of the "newEmail" property in the request. private let kNewEmailKey = "newEmail" -/** @var kIDTokenKey - @brief The key for the "idToken" value in the request. This is actually the STS Access Token, - despite it's confusing (backwards compatiable) parameter name. - */ +/// The key for the "idToken" value in the request. This is actually the STS Access Token, +/// despite its confusing (backwards compatiable) parameter name. private let kIDTokenKey = "idToken" -/** @var kContinueURLKey - @brief The key for the "continue URL" value in the request. - */ +/// The key for the "continue URL" value in the request. private let kContinueURLKey = "continueUrl" -/** @var kIosBundeIDKey - @brief The key for the "iOS Bundle Identifier" value in the request. - */ +/// The key for the "iOS Bundle Identifier" value in the request. private let kIosBundleIDKey = "iOSBundleId" -/** @var kAndroidPackageNameKey - @brief The key for the "Android Package Name" value in the request. - */ +/// The key for the "Android Package Name" value in the request. private let kAndroidPackageNameKey = "androidPackageName" -/** @var kAndroidInstallAppKey - @brief The key for the request parameter indicating whether the android app should be installed - or not. - */ +/// The key for the request parameter indicating whether the android app should be installed or not. private let kAndroidInstallAppKey = "androidInstallApp" -/** @var kAndroidMinimumVersionKey - @brief The key for the "minimum Android version supported" value in the request. - */ +/// The key for the "minimum Android version supported" value in the request. private let kAndroidMinimumVersionKey = "androidMinimumVersion" -/** @var kCanHandleCodeInAppKey - @brief The key for the request parameter indicating whether the action code can be handled in - the app or not. - */ +/// The key for the request parameter indicating whether the action code can be handled in the app +/// or not. private let kCanHandleCodeInAppKey = "canHandleCodeInApp" -/** @var kDynamicLinkDomainKey - @brief The key for the "dynamic link domain" value in the request. - */ +/// The key for the "dynamic link domain" value in the request. private let kDynamicLinkDomainKey = "dynamicLinkDomain" -/** @var kPasswordResetRequestTypeValue - @brief The value for the "PASSWORD_RESET" request type. - */ +/// The value for the "PASSWORD_RESET" request type. private let kPasswordResetRequestTypeValue = "PASSWORD_RESET" -/** @var kEmailLinkSignInTypeValue - @brief The value for the "EMAIL_SIGNIN" request type. - */ +/// The value for the "EMAIL_SIGNIN" request type. private let kEmailLinkSignInTypeValue = "EMAIL_SIGNIN" -/** @var kVerifyEmailRequestTypeValue - @brief The value for the "VERIFY_EMAIL" request type. - */ +/// The value for the "VERIFY_EMAIL" request type. private let kVerifyEmailRequestTypeValue = "VERIFY_EMAIL" -/** @var kVerifyBeforeUpdateEmailRequestTypeValue - @brief The value for the "VERIFY_AND_CHANGE_EMAIL" request type. - */ +/// The value for the "VERIFY_AND_CHANGE_EMAIL" request type. private let kVerifyBeforeUpdateEmailRequestTypeValue = "VERIFY_AND_CHANGE_EMAIL" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" -/** @var kCaptchaResponseKey - @brief The key for the "captchaResponse" value in the request. - */ +/// The key for the "captchaResponse" value in the request. private let kCaptchaResponseKey = "captchaResp" -/** @var kClientType - @brief The key for the "clientType" value in the request. - */ +/// The key for the "clientType" value in the request. private let kClientType = "clientType" -/** @var kRecaptchaVersion - @brief The key for the "recaptchaVersion" value in the request. - */ +/// The key for the "recaptchaVersion" value in the request. private let kRecaptchaVersion = "recaptchaVersion" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = GetOOBConfirmationCodeResponse - /** @property requestType - @brief The types of OOB Confirmation Code to request. - */ + /// The types of OOB Confirmation Code to request. let requestType: GetOOBConfirmationCodeRequestType - /** @property email - @brief The email of the user. - @remarks For password reset. - */ + /// The email of the user for password reset. private(set) var email: String? - /** @property updatedEmail - @brief The new email to be updated. - @remarks For verifyBeforeUpdateEmail. - */ + /// The new email to be updated for verifyBeforeUpdateEmail. private(set) var updatedEmail: String? - /** @property accessToken - @brief The STS Access Token of the authenticated user. - @remarks For email change. - */ + /// The STS Access Token of the authenticated user for email change. private(set) var accessToken: String? - /** @property continueURL - @brief This URL represents the state/Continue URL in the form of a universal link. - */ + /// This URL represents the state/Continue URL in the form of a universal link. private(set) var continueURL: String? - /** @property iOSBundleID - @brief The iOS bundle Identifier, if available. - */ + /// The iOS bundle Identifier, if available. private(set) var iOSBundleID: String? - /** @property androidPackageName - @brief The Android package name, if available. - */ + /// The Android package name, if available. private(set) var androidPackageName: String? - /** @property androidMinimumVersion - @brief The minimum Android version supported, if available. - */ + /// The minimum Android version supported, if available. private(set) var androidMinimumVersion: String? - /** @property androidInstallIfNotAvailable - @brief Indicates whether or not the Android app should be installed if not already available. - */ + /// Indicates whether or not the Android app should be installed if not already available. private(set) var androidInstallApp: Bool - /** @property handleCodeInApp - @brief Indicates whether the action code link will open the app directly or after being - redirected from a Firebase owned web widget. - */ + /// Indicates whether the action code link will open the app directly or after being + /// redirected from a Firebase owned web widget. private(set) var handleCodeInApp: Bool - /** @property dynamicLinkDomain - @brief The Firebase Dynamic Link domain used for out of band code flow. - */ + /// The Firebase Dynamic Link domain used for out of band code flow. private(set) var dynamicLinkDomain: String? - /** @property captchaResponse - @brief Response to the captcha. - */ + /// Response to the captcha. var captchaResponse: String? - /** @property captchaResponse - @brief The reCAPTCHA version. - */ + /// The reCAPTCHA version. var recaptchaVersion: String? - /** @fn initWithRequestType:email:APIKey: - @brief Designated initializer. - @param requestType The types of OOB Confirmation Code to request. - @param email The email of the user. - @param newEmail The email of the user to be updated. - @param accessToken The STS Access Token of the currently signed in user. - @param actionCodeSettings An object of FIRActionCodeSettings which specifies action code - settings to be applied to the OOB code request. - @param requestConfiguration An object containing configurations to be added to the request. - */ + /// Designated initializer. + /// - Parameter requestType: The types of OOB Confirmation Code to request. + /// - Parameter email: The email of the user. + /// - Parameter newEmail: The email of the user to be updated. + /// - Parameter accessToken: The STS Access Token of the currently signed in user. + /// - Parameter actionCodeSettings: An object of FIRActionCodeSettings which specifies action code + /// settings to be applied to the OOB code request. + /// - Parameter requestConfiguration: An object containing configurations to be added to the + /// request. required init(requestType: GetOOBConfirmationCodeRequestType, email: String?, newEmail: String?, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift index 33c9b442d21..f731e6df8fc 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetProjectConfigRequest.swift @@ -14,9 +14,8 @@ import Foundation -/** @var kGetProjectConfigEndPoint - @brief The "getProjectConfig" endpoint. - */ +/// The "getProjectConfig" endpoint. + private let kGetProjectConfigEndPoint = "getProjectConfig" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift index 4b57f74d315..885f407a052 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/GetRecaptchaConfigRequest.swift @@ -18,45 +18,29 @@ private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE" private let kGetOobConfirmationCodeEndpoint = "getOobConfirmationCode" -/** @var kRequestTypeKey - @brief The name of the required "requestType" property in the request. - */ +/// The name of the required "requestType" property in the request. private let kRequestTypeKey = "requestType" -/** @var kEmailKey - @brief The name of the "email" property in the request. - */ +/// The name of the "email" property in the request. private let kEmailKey = "email" -/** @var kNewEmailKey - @brief The name of the "newEmail" property in the request. - */ +/// The name of the "newEmail" property in the request. private let kNewEmailKey = "newEmail" -/** @var kIDTokenKey - @brief The key for the "idToken" value in the request. This is actually the STS Access Token, - despite it's confusing (backwards compatiable) parameter name. - */ +/// The key for the "idToken" value in the request. This is actually the STS Access Token, +/// despite its confusing (backwards compatiable) parameter name. private let kIDTokenKey = "idToken" -/** @var kGetRecaptchaConfigEndpoint - @brief The "getRecaptchaConfig" endpoint. - */ +/// The "getRecaptchaConfig" endpoint. private let kGetRecaptchaConfigEndpoint = "recaptchaConfig" -/** @var kClientType - @brief The key for the "clientType" value in the request. - */ +/// The key for the "clientType" value in the request. private let kClientTypeKey = "clientType" -/** @var kVersionKey - @brief The key for the "version" value in the request. - */ +/// The key for the "version" value in the request. private let kVersionKey = "version" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift index d2adb4d9cdb..773b448a136 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/FinalizeMFAEnrollmentRequest.swift @@ -16,9 +16,7 @@ import Foundation private let kFinalizeMFAEnrollmentEndPoint = "accounts/mfaEnrollment:finalize" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift index ffaf18f6e97..2ae904eac46 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Enroll/StartMFAEnrollmentRequest.swift @@ -16,9 +16,7 @@ import Foundation private let kStartMFAEnrollmentEndPoint = "accounts/mfaEnrollment:start" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift index 8c94ea5dc9d..53f8a783b67 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/FinalizeMFASignInRequest.swift @@ -16,9 +16,7 @@ import Foundation private let kFinalizeMFASignInEndPoint = "accounts/mfaSignIn:finalize" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift index 977901fc563..413245776a7 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/SignIn/StartMFASignInRequest.swift @@ -16,9 +16,8 @@ import Foundation private let kStartMFASignInEndPoint = "accounts/mfaSignIn:start" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. + private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift index 240f909e262..5f8156a5191 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/MultiFactor/Unenroll/WithdrawMFARequest.swift @@ -16,9 +16,7 @@ import Foundation private let kWithdrawMFAEndPoint = "accounts/mfaEnrollment:withdraw" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/Phone/AuthProtoStartMFAPhoneRequestInfo.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/Phone/AuthProtoStartMFAPhoneRequestInfo.swift index fc52c4cfb94..274a7b97883 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/Phone/AuthProtoStartMFAPhoneRequestInfo.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/Phone/AuthProtoStartMFAPhoneRequestInfo.swift @@ -14,24 +14,16 @@ import Foundation -/** @var kPhoneNumberKey - @brief The key for the Phone Number parameter in the request. - */ +/// The key for the Phone Number parameter in the request. private let kPhoneNumberKey = "phoneNumber" -/** @var kReceiptKey - @brief The key for the receipt parameter in the request. - */ +/// The key for the receipt parameter in the request. private let kReceiptKey = "iosReceipt" -/** @var kSecretKey - @brief The key for the Secret parameter in the request. - */ +/// The key for the Secret parameter in the request. private let kSecretKey = "iosSecret" -/** @var kreCAPTCHATokenKey - @brief The key for the reCAPTCHAToken parameter in the request. - */ +/// The key for the reCAPTCHAToken parameter in the request. private let kreCAPTCHATokenKey = "recaptchaToken" class AuthProtoStartMFAPhoneRequestInfo: NSObject, AuthProto { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/TOTP/AuthProtoStartMFATOTPEnrollmentRequestInfo.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/TOTP/AuthProtoStartMFATOTPEnrollmentRequestInfo.swift index f6c0af69381..69f65349aa1 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/TOTP/AuthProtoStartMFATOTPEnrollmentRequestInfo.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/Proto/TOTP/AuthProtoStartMFATOTPEnrollmentRequestInfo.swift @@ -14,10 +14,8 @@ import Foundation -/** - @brief AuthProtoFinalizeMFATOTPSignInRequestInfo class. This class is used to compose - finalizeMFASignInRequest for TOTP case. - */ +/// AuthProtoFinalizeMFATOTPSignInRequestInfo class. This class is used to compose +/// finalizeMFASignInRequest for TOTP case . class AuthProtoFinalizeMFATOTPSignInRequestInfo: NSObject, AuthProto { required init(dictionary: [String: AnyHashable]) { fatalError() diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordRequest.swift index 69b6c085624..aa39c927b83 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordRequest.swift @@ -14,46 +14,33 @@ import Foundation -/** @var kResetPasswordEndpoint - @brief The "resetPassword" endpoint. - */ +/// The "resetPassword" endpoint. private let kResetPasswordEndpoint = "resetPassword" -/** @var kOOBCodeKey - @brief The "resetPassword" key. - */ +/// The "resetPassword" key. private let kOOBCodeKey = "oobCode" -/** @var kCurrentPasswordKey - @brief The "newPassword" key. - */ +/// The "newPassword" key. private let kCurrentPasswordKey = "newPassword" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class ResetPasswordRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = ResetPasswordResponse - /** @property oobCode - @brief The oobCode sent in the request. - */ + /// The oobCode sent in the request. let oobCode: String - /** @property updatedPassword - @brief The new password sent in the request. - */ + /// The new password sent in the request. let updatedPassword: String? - /** @fn initWithOobCode:newPassword:requestConfiguration: - @brief Designated initializer. - @param oobCode The OOB Code. - @param newPassword The new password. - @param requestConfiguration An object containing configurations to be added to the request. - */ + /// Designated initializer. + /// - Parameter oobCode: The OOB Code. + /// - Parameter newPassword: The new password. + /// - Parameter requestConfiguration: An object containing configurations to be added to the + /// request. init(oobCode: String, newPassword: String?, requestConfiguration: AuthRequestConfiguration) { self.oobCode = oobCode diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordResponse.swift index 8cb85aaffd6..fcfb619732e 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ResetPasswordResponse.swift @@ -14,32 +14,26 @@ import Foundation -/** @class FIRAuthResetPasswordResponse - @brief Represents the response from the resetPassword endpoint. - @remarks Possible error codes: - - FIRAuthErrorCodeWeakPassword - - FIRAuthErrorCodeUserDisabled - - FIRAuthErrorCodeOperationNotAllowed - - FIRAuthErrorCodeExpiredActionCode - - FIRAuthErrorCodeInvalidActionCode - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/resetPassword - */ +/// Represents the response from the resetPassword endpoint. +/// +/// Possible error codes: +/// * FIRAuthErrorCodeWeakPassword +/// * FIRAuthErrorCodeUserDisabled +/// * FIRAuthErrorCodeOperationNotAllowed +/// * FIRAuthErrorCodeExpiredActionCode +/// * FIRAuthErrorCodeInvalidActionCode +/// +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/resetPassword class ResetPasswordResponse: AuthRPCResponse { required init() {} - /** @property email - @brief The email address corresponding to the reset password request. - */ + /// The email address corresponding to the reset password request. var email: String? - /** @property verifiedEmail - @brief The verified email returned from the backend. - */ + /// The verified email returned from the backend. var verifiedEmail: String? - /** @property requestType - @brief The type of request as returned by the backend. - */ + /// The type of request as returned by the backend. var requestType: String? func setFields(dictionary: [String: AnyHashable]) throws { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift index c50151fc93c..c69ab2b60b0 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/RevokeTokenRequest.swift @@ -14,57 +14,38 @@ import Foundation -/** @var kRevokeTokenEndpoint - @brief The endpoint for the revokeToken request. - */ +/// The endpoint for the revokeToken request. private let kRevokeTokenEndpoint = "accounts:revokeToken" -/** @var kProviderIDKey - @brief The key for the provider that issued the token to revoke. - */ +/// The key for the provider that issued the token to revoke. private let kProviderIDKey = "providerId" -/** @var kTokenTypeKey - @brief The key for the type of the token to revoke. - */ +/// The key for the type of the token to revoke. private let kTokenTypeKey = "tokenType" -/** @var kTokenKey - @brief The key for the token to be revoked. - */ +/// The key for the token to be revoked. private let kTokenKey = "token" -/** @var kIDTokenKey - @brief The key for the ID Token associated with this credential. - */ +/// The key for the ID Token associated with this credential. private let kIDTokenKey = "idToken" -/** @class FIRVerifyPasswordRequest - @brief Represents the parameters for the verifyPassword endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword - */ +/// Represents the parameters for the verifyPassword endpoint. +/// +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class RevokeTokenRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = RevokeTokenResponse - /** @property providerID - @brief The provider that issued the token to revoke. - */ + /// The provider that issued the token to revoke. private(set) var providerID: String - /** @property tokenType - @brief The type of the token to revoke. - */ + /// The type of the token to revoke. private(set) var tokenType: TokenType - /** @property token - @brief The token to be revoked. - */ + /// The token to be revoked. private(set) var token: String - /** @property idToken - @brief The ID Token associated with this credential. - */ + /// The ID Token associated with this credential. private(set) var idToken: String enum TokenType: Int { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenRequest.swift index e0f0829fe95..60a504a367a 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenRequest.swift @@ -28,84 +28,54 @@ enum SecureTokenRequestGrantType: Int { } } -/** @var kFIRSecureTokenServiceGetTokenURLFormat - @brief The format of the secure token service URLs. Requires string format substitution with - the client's API Key. - */ +/// The format of the secure token service URLs. Requires string format substitution with +/// the client's API Key. private let kFIRSecureTokenServiceGetTokenURLFormat = "https://%@/v1/token?key=%@" -/** @var kFIREmulatorURLFormat - @brief The format of the emulated secure token service URLs. Requires string format substitution - with the emulator host, the gAPIHost, and the client's API Key. - */ +/// The format of the emulated secure token service URLs. Requires string format substitution +/// with the emulator host, the gAPIHost, and the client's API Key. private let kFIREmulatorURLFormat = "http://%@/%@/v1/token?key=%@" -/** @var kFIRSecureTokenServiceGrantTypeRefreshToken - @brief The string value of the @c FIRSecureTokenRequestGrantTypeRefreshToken request type. - */ +/// The string value of the `SecureTokenRequestGrantTypeRefreshToken` request type. private let kFIRSecureTokenServiceGrantTypeRefreshToken = "refresh_token" -/** @var kFIRSecureTokenServiceGrantTypeAuthorizationCode - @brief The string value of the @c FIRSecureTokenRequestGrantTypeAuthorizationCode request type. - */ +/// The string value of the `SecureTokenRequestGrantTypeAuthorizationCode` request type. private let kFIRSecureTokenServiceGrantTypeAuthorizationCode = "authorization_code" -/** @var kGrantTypeKey - @brief The key for the "grantType" parameter in the request. - */ +/// The key for the "grantType" parameter in the request. private let kGrantTypeKey = "grantType" -/** @var kScopeKey - @brief The key for the "scope" parameter in the request. - */ +/// The key for the "scope" parameter in the request. private let kScopeKey = "scope" -/** @var kRefreshTokenKey - @brief The key for the "refreshToken" parameter in the request. - */ +/// The key for the "refreshToken" parameter in the request. private let kRefreshTokenKey = "refreshToken" -/** @var kCodeKey - @brief The key for the "code" parameter in the request. - */ +/// The key for the "code" parameter in the request. private let kCodeKey = "code" -/** @var gAPIHost - @brief Host for server API calls. - */ +/// Host for server API calls. private var gAPIHost = "securetoken.googleapis.com" -/** @class FIRSecureTokenRequest - @brief Represents the parameters for the token endpoint. - */ +/// Represents the parameters for the token endpoint. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class SecureTokenRequest: AuthRPCRequest { typealias Response = SecureTokenResponse - /** @property grantType - @brief The type of grant requested. - @see FIRSecureTokenRequestGrantType - */ + /// The type of grant requested. + /// See FIRSecureTokenRequestGrantType var grantType: SecureTokenRequestGrantType - /** @property scope - @brief The scopes requested (a comma-delimited list of scope strings.) - */ + /// The scopes requested (a comma-delimited list of scope strings). var scope: String? - /** @property refreshToken - @brief The client's refresh token. - */ + /// The client's refresh token. var refreshToken: String? - /** @property code - @brief The client's authorization code (legacy Gitkit "ID Token"). - */ + /// The client's authorization code (legacy Gitkit "ID Token"). var code: String? - /** @property APIKey - @brief The client's API Key. - */ + /// The client's API Key. let apiKey: String let _requestConfiguration: AuthRequestConfiguration diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenResponse.swift index d2331b44bf5..f16bd573da0 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SecureTokenResponse.swift @@ -16,19 +16,16 @@ import Foundation private let kExpiresInKey = "expires_in" -/** @var kRefreshTokenKey - @brief The key for the refresh token. - */ +/// The key for the refresh token. + private let kRefreshTokenKey = "refresh_token" -/** @var kAccessTokenKey - @brief The key for the access token. - */ +/// The key for the access token. + private let kAccessTokenKey = "access_token" -/** @var kIDTokenKey - @brief The key for the "id_token" value in the response. - */ +/// The key for the "id_token" value in the response. + private let kIDTokenKey = "id_token" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SendVerificationTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SendVerificationTokenRequest.swift index a3ae44f95ac..af090fdd3b3 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SendVerificationTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SendVerificationTokenRequest.swift @@ -14,34 +14,22 @@ import Foundation -/** @var kSendVerificationCodeEndPoint - @brief The "sendVerificationCodeEnd" endpoint. - */ +/// The "sendVerificationCodeEnd" endpoint. private let kSendVerificationCodeEndPoint = "sendVerificationCode" -/** @var kPhoneNumberKey - @brief The key for the Phone Number parameter in the request. - */ +/// The key for the Phone Number parameter in the request. private let kPhoneNumberKey = "phoneNumber" -/** @var kReceiptKey - @brief The key for the receipt parameter in the request. - */ +/// The key for the receipt parameter in the request. private let kReceiptKey = "iosReceipt" -/** @var kSecretKey - @brief The key for the Secret parameter in the request. - */ +/// The key for the Secret parameter in the request. private let kSecretKey = "iosSecret" -/** @var kreCAPTCHATokenKey - @brief The key for the reCAPTCHAToken parameter in the request. - */ +/// The key for the reCAPTCHAToken parameter in the request. private let kreCAPTCHATokenKey = "recaptchaToken" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" /// A verification code can be an appCredential or a reCaptcha Token @@ -55,14 +43,11 @@ enum CodeIdentity { class SendVerificationCodeRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = SendVerificationCodeResponse - /** @property phoneNumber - @brief The phone number to which the verification code should be sent. - */ + /// The phone number to which the verification code should be sent. let phoneNumber: String - /** @property verificationCode - @brief The credential or reCAPTCHA token to prove the identity of the app in order to send the verification code. - */ + /// The credential or reCAPTCHA token to prove the identity of the app in order to send the + /// verification code. let codeIdentity: CodeIdentity init(phoneNumber: String, codeIdentity: CodeIdentity, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoRequest.swift index a6f242ab92d..2680d41e16e 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoRequest.swift @@ -24,179 +24,114 @@ private let FIRSetAccountInfoUserAttributePhotoURL = "PHOTO_URL" private let FIRSetAccountInfoUserAttributePassword = "PASSWORD" -/** @var kCreateAuthURIEndpoint - @brief The "setAccountInfo" endpoint. - */ +/// The "setAccountInfo" endpoint. private let kSetAccountInfoEndpoint = "setAccountInfo" -/** @var kIDTokenKey - @brief The key for the "idToken" value in the request. This is actually the STS Access Token, - despite it's confusing (backwards compatiable) parameter name. - */ +/// The key for the "idToken" value in the request. This is actually the STS Access Token, +/// despite its confusing (backwards compatiable) parameter name. private let kIDTokenKey = "idToken" -/** @var kDisplayNameKey - @brief The key for the "displayName" value in the request. - */ +/// The key for the "displayName" value in the request. private let kDisplayNameKey = "displayName" -/** @var kLocalIDKey - @brief The key for the "localID" value in the request. - */ +/// The key for the "localID" value in the request. private let kLocalIDKey = "localId" -/** @var kEmailKey - @brief The key for the "email" value in the request. - */ +/// The key for the "email" value in the request. private let kEmailKey = "email" -/** @var kPasswordKey - @brief The key for the "password" value in the request. - */ +/// The key for the "password" value in the request. private let kPasswordKey = "password" -/** @var kPhotoURLKey - @brief The key for the "photoURL" value in the request. - */ +/// The key for the "photoURL" value in the request. private let kPhotoURLKey = "photoUrl" -/** @var kProvidersKey - @brief The key for the "providers" value in the request. - */ +/// The key for the "providers" value in the request. private let kProvidersKey = "provider" -/** @var kOOBCodeKey - @brief The key for the "OOBCode" value in the request. - */ +/// The key for the "OOBCode" value in the request. private let kOOBCodeKey = "oobCode" -/** @var kEmailVerifiedKey - @brief The key for the "emailVerified" value in the request. - */ +/// The key for the "emailVerified" value in the request. private let kEmailVerifiedKey = "emailVerified" -/** @var kUpgradeToFederatedLoginKey - @brief The key for the "upgradeToFederatedLogin" value in the request. - */ +/// The key for the "upgradeToFederatedLogin" value in the request. private let kUpgradeToFederatedLoginKey = "upgradeToFederatedLogin" -/** @var kCaptchaChallengeKey - @brief The key for the "captchaChallenge" value in the request. - */ +/// The key for the "captchaChallenge" value in the request. private let kCaptchaChallengeKey = "captchaChallenge" -/** @var kCaptchaResponseKey - @brief The key for the "captchaResponse" value in the request. - */ +/// The key for the "captchaResponse" value in the request. private let kCaptchaResponseKey = "captchaResponse" -/** @var kDeleteAttributesKey - @brief The key for the "deleteAttribute" value in the request. - */ +/// The key for the "deleteAttribute" value in the request. private let kDeleteAttributesKey = "deleteAttribute" -/** @var kDeleteProvidersKey - @brief The key for the "deleteProvider" value in the request. - */ +/// The key for the "deleteProvider" value in the request. private let kDeleteProvidersKey = "deleteProvider" -/** @var kReturnSecureTokenKey - @brief The key for the "returnSecureToken" value in the request. - */ +/// The key for the "returnSecureToken" value in the request. private let kReturnSecureTokenKey = "returnSecureToken" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" -/** @class FIRSetAccountInfoRequest - @brief Represents the parameters for the setAccountInfo endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo - */ +/// Represents the parameters for the setAccountInfo endpoint. +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class SetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = SetAccountInfoResponse - /** @property accessToken - @brief The STS Access Token of the authenticated user. - */ + + /// The STS Access Token of the authenticated user. var accessToken: String? - /** @property displayName - @brief The name of the user. - */ + /// The name of the user. var displayName: String? - /** @property localID - @brief The local ID of the user. - */ + /// The local ID of the user. var localID: String? - /** @property email - @brief The email of the user. - */ + /// The email of the user. var email: String? - /** @property photoURL - @brief The photoURL of the user. - */ + /// The photoURL of the user. var photoURL: URL? - /** @property password - @brief The new password of the user. - */ + /// The new password of the user. var password: String? - /** @property providers - @brief The associated identity providers of the user. - */ + /// The associated identity providers of the user. var providers: [String]? - /** @property OOBCode - @brief The out-of-band code of the change email request. - */ + /// The out-of-band code of the change email request. var oobCode: String? - /** @property emailVerified - @brief Whether to mark the email as verified or not. - */ + /// Whether to mark the email as verified or not. var emailVerified: Bool = false - /** @property upgradeToFederatedLogin - @brief Whether to mark the user to upgrade to federated login. - */ + /// Whether to mark the user to upgrade to federated login. var upgradeToFederatedLogin: Bool = false - /** @property captchaChallenge - @brief The captcha challenge. - */ + /// The captcha challenge. var captchaChallenge: String? - /** @property captchaResponse - @brief Response to the captcha. - */ + /// Response to the captcha. var captchaResponse: String? - /** @property deleteAttributes - @brief The list of user attributes to delete. - @remarks Every element of the list must be one of the predefined constant starts with - "FIRSetAccountInfoUserAttribute". - */ + /// The list of user attributes to delete. + /// + /// Every element of the list must be one of the predefined constant starts with + /// `SetAccountInfoUserAttribute`. var deleteAttributes: [String]? - /** @property deleteProviders - @brief The list of identity providers to delete. - */ + /// The list of identity providers to delete. var deleteProviders: [String]? - /** @property returnSecureToken - @brief Whether the response should return access token and refresh token directly. - @remarks The default value is @c YES . - */ - var returnSecureToken: Bool = false + /// Whether the response should return access token and refresh token directly. + /// The default value is `true` . + var returnSecureToken: Bool = true init(requestConfiguration: AuthRequestConfiguration) { - returnSecureToken = true super.init(endpoint: kSetAccountInfoEndpoint, requestConfiguration: requestConfiguration) } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoResponse.swift index ac498ce458c..d6bd97ccd70 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoResponse.swift @@ -14,30 +14,20 @@ import Foundation -/** @class FIRSetAccountInfoResponseProviderUserInfo - @brief Represents the provider user info part of the response from the setAccountInfo endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo - */ +/// Represents the provider user info part of the response from the setAccountInfo endpoint. +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo class SetAccountInfoResponseProviderUserInfo: NSObject { - /** @property providerID - @brief The ID of the identity provider. - */ + /// The ID of the identity provider. var providerID: String? - /** @property displayName - @brief The user's display name at the identity provider. - */ + /// The user's display name at the identity provider. var displayName: String? - /** @property photoURL - @brief The user's photo URL at the identity provider. - */ + /// The user's photo URL at the identity provider. var photoURL: URL? - /** @fn initWithAPIKey: - @brief Designated initializer. - @param dictionary The provider user info data from endpoint. - */ + /// Designated initializer. + /// - Parameter dictionary: The provider user info data from endpoint. init(dictionary: [String: Any]) { providerID = dictionary["providerId"] as? String displayName = dictionary["displayName"] as? String @@ -47,43 +37,29 @@ class SetAccountInfoResponseProviderUserInfo: NSObject { } } -/** @class FIRSetAccountInfoResponse - @brief Represents the response from the setAccountInfo endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo - */ +/// Represents the response from the setAccountInfo endpoint. +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/setAccountInfo class SetAccountInfoResponse: AuthRPCResponse { required init() {} - /** @property email - @brief The email or the user. - */ + /// The email or the user. var email: String? - /** @property displayName - @brief The display name of the user. - */ + /// The display name of the user. var displayName: String? - /** @property providerUserInfo - @brief The user's profiles at the associated identity providers. - */ + /// The user's profiles at the associated identity providers. var providerUserInfo: [SetAccountInfoResponseProviderUserInfo]? - /** @property idToken - @brief Either an authorization code suitable for performing an STS token exchange, or the - access token from Secure Token Service, depending on whether @c returnSecureToken is set - on the request. - */ + /// Either an authorization code suitable for performing an STS token exchange, or the + /// access token from Secure Token Service, depending on whether `returnSecureToken` is set + /// on the request. var idToken: String? - /** @property approximateExpirationDate - @brief The approximate expiration date of the access token. - */ + /// The approximate expiration date of the access token. var approximateExpirationDate: Date? - /** @property refreshToken - @brief The refresh token from Secure Token Service. - */ + /// The refresh token from Secure Token Service. var refreshToken: String? func setFields(dictionary: [String: AnyHashable]) throws { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithGameCenterRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithGameCenterRequest.swift index 39e346f6325..adab76dcd34 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithGameCenterRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithGameCenterRequest.swift @@ -16,69 +16,47 @@ import Foundation private let kSignInWithGameCenterEndPoint = "signInWithGameCenter" -/** @class FIRSignInWithGameCenterRequest - @brief The request to sign in with Game Center account - */ +/// The request to sign in with Game Center account @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class SignInWithGameCenterRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = SignInWithGameCenterResponse - /** @property playerID - @brief The playerID to verify. - */ + /// The playerID to verify. var playerID: String - /** @property teamPlayerID - @brief The team player ID of the Game Center local player. - */ + /// The team player ID of the Game Center local player. var teamPlayerID: String? - /** @property gamePlayerID - @brief The game player ID of the Game Center local player. - */ + /// The game player ID of the Game Center local player. var gamePlayerID: String? - /** @property publicKeyURL - @brief The URL for the encryption key. - */ + /// The URL for the encryption key. var publicKeyURL: URL - /** @property signature - @brief The verification signature data generated by Game Center. - */ + /// The verification signature data generated by Game Center. var signature: Data - /** @property salt - @brief A random strong used to compute the hash and keep it randomized. - */ + /// A random strong used to compute the hash and keep it randomized. var salt: Data - /** @property timestamp - @brief The date and time that the signature was created. - */ + /// The date and time that the signature was created. var timestamp: UInt64 - /** @property accessToken - @brief The STS Access Token for the authenticated user, only needed for linking the user. - */ + /// The STS Access Token for the authenticated user, only needed for linking the user. var accessToken: String? - /** @property displayName - @brief The display name of the local Game Center player. - */ + /// The display name of the local Game Center player. var displayName: String? - /** @fn initWithPlayerID:publicKeyURL:signature:salt:timestamp:displayName:requestConfiguration: - @brief Designated initializer. - @param playerID The ID of the Game Center player. - @param teamPlayerID The teamPlayerID of the Game Center local player. - @param gamePlayerID The gamePlayerID of the Game Center local player. - @param publicKeyURL The URL for the encryption key. - @param signature The verification signature generated. - @param salt A random string used to compute the hash and keep it randomized. - @param timestamp The date and time that the signature was created. - @param displayName The display name of the Game Center player. - */ + /// Designated initializer. + /// - Parameter playerID: The ID of the Game Center player. + /// - Parameter teamPlayerID: The teamPlayerID of the Game Center local player. + /// - Parameter gamePlayerID: The gamePlayerID of the Game Center local player. + /// - Parameter publicKeyURL: The URL for the encryption key. + /// - Parameter signature: The verification signature generated. + /// - Parameter salt: A random string used to compute the hash and keep it randomized. + /// - Parameter timestamp: The date and time that the signature was created. + /// - Parameter displayName: The display name of the Game Center player. init(playerID: String, teamPlayerID: String?, gamePlayerID: String?, publicKeyURL: URL, signature: Data, salt: Data, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift index f96703fff35..9a2d7096418 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserRequest.swift @@ -14,105 +14,69 @@ import Foundation -/** @var kSignupNewUserEndpoint - @brief The "SingupNewUserEndpoint" endpoint. - */ +/// The "SignupNewUserEndpoint" endpoint. private let kSignupNewUserEndpoint = "signupNewUser" -/** @var kEmailKey - @brief The key for the "email" value in the request. - */ +/// The key for the "email" value in the request. private let kEmailKey = "email" -/** @var kPasswordKey - @brief The key for the "password" value in the request. - */ +/// The key for the "password" value in the request. private let kPasswordKey = "password" -/** @var kDisplayNameKey - @brief The key for the "kDisplayName" value in the request. - */ +/// The key for the "kDisplayName" value in the request. private let kDisplayNameKey = "displayName" -/** @var kIDToken - @brief The key for the "kIDToken" value in the request. - */ +/// The key for the "kIDToken" value in the request. private let kIDToken = "idToken" -/** @var kCaptchaResponseKey - @brief The key for the "captchaResponse" value in the request. - */ +/// The key for the "captchaResponse" value in the request. private let kCaptchaResponseKey = "captchaResponse" -/** @var kClientType - @brief The key for the "clientType" value in the request. - */ +/// The key for the "clientType" value in the request. private let kClientType = "clientType" -/** @var kRecaptchaVersion - @brief The key for the "recaptchaVersion" value in the request. - */ +/// The key for the "recaptchaVersion" value in the request. private let kRecaptchaVersion = "recaptchaVersion" -/** @var kReturnSecureTokenKey - @brief The key for the "returnSecureToken" value in the request. - */ +/// The key for the "returnSecureToken" value in the request. private let kReturnSecureTokenKey = "returnSecureToken" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class SignUpNewUserRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = SignUpNewUserResponse - /** @property email - @brief The email of the user. - */ + /// The email of the user. private(set) var email: String? - /** @property password - @brief The password inputed by the user. - */ + /// The password inputed by the user. private(set) var password: String? - /** @property displayName - @brief The password inputed by the user. - */ + /// The password inputed by the user. private(set) var displayName: String? - /** @property idToken - @brief The idToken of the user. - */ + /// The idToken of the user. private(set) var idToken: String? - /** @property captchaResponse - @brief Response to the captcha. - */ - + /// Response to the captcha. var captchaResponse: String? - /** @property captchaResponse - @brief The reCAPTCHA version. - */ + /// The reCAPTCHA version. var recaptchaVersion: String? - /** @property returnSecureToken - @brief Whether the response should return access token and refresh token directly. - @remarks The default value is @c YES . - */ + /// Whether the response should return access token and refresh token directly. + /// The default value is `true`. var returnSecureToken: Bool = true init(requestConfiguration: AuthRequestConfiguration) { super.init(endpoint: kSignupNewUserEndpoint, requestConfiguration: requestConfiguration) } - /** @fn initWithAPIKey:email:password:displayName:requestConfiguration - @brief Designated initializer. - @param requestConfiguration An object containing configurations to be added to the request. - */ + /// Designated initializer. + /// - Parameter requestConfiguration: An object containing configurations to be added to the + /// request. init(email: String?, password: String?, displayName: String?, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserResponse.swift index d46ae7398dd..fe6c8d9556e 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignUpNewUserResponse.swift @@ -17,21 +17,15 @@ import Foundation class SignUpNewUserResponse: AuthRPCResponse { required init() {} - /** @property IDToken - @brief Either an authorization code suitable for performing an STS token exchange, or the - access token from Secure Token Service, depending on whether @c returnSecureToken is set - on the request. - */ + /// Either an authorization code suitable for performing an STS token exchange, or the + /// access token from Secure Token Service, depending on whether `returnSecureToken` is set + /// on the request. var idToken: String? - /** @property approximateExpirationDate - @brief The approximate expiration date of the access token. - */ + /// The approximate expiration date of the access token. var approximateExpirationDate: Date? - /** @property refreshToken - @brief The refresh token from Secure Token Service. - */ + /// The refresh token from Secure Token Service. var refreshToken: String? func setFields(dictionary: [String: AnyHashable]) throws { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionRequest.swift index fd97de6c282..59f6f52f84f 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionRequest.swift @@ -14,197 +14,124 @@ import Foundation -/** @var kVerifyAssertionEndpoint - @brief The "verifyAssertion" endpoint. - */ +/// The "verifyAssertion" endpoint. private let kVerifyAssertionEndpoint = "verifyAssertion" -/** @var kProviderIDKey - @brief The key for the "providerId" value in the request. - */ +/// The key for the "providerId" value in the request. private let kProviderIDKey = "providerId" -/** @var kProviderIDTokenKey - @brief The key for the "id_token" value in the request. - */ +/// The key for the "id_token" value in the request. private let kProviderIDTokenKey = "id_token" -/** @var kProviderNonceKey - @brief The key for the "nonce" value in the request. - */ +/// The key for the "nonce" value in the request. private let kProviderNonceKey = "nonce" -/** @var kProviderAccessTokenKey - @brief The key for the "access_token" value in the request. - */ +/// The key for the "access_token" value in the request. private let kProviderAccessTokenKey = "access_token" -/** @var kProviderOAuthTokenSecretKey - @brief The key for the "oauth_token_secret" value in the request. - */ +/// The key for the "oauth_token_secret" value in the request. private let kProviderOAuthTokenSecretKey = "oauth_token_secret" -/** @var kIdentifierKey - @brief The key for the "identifier" value in the request. - */ +/// The key for the "identifier" value in the request. private let kIdentifierKey = "identifier" -/** @var kRequestURIKey - @brief The key for the "requestUri" value in the request. - */ +/// The key for the "requestUri" value in the request. private let kRequestURIKey = "requestUri" -/** @var kPostBodyKey - @brief The key for the "postBody" value in the request. - */ +/// The key for the "postBody" value in the request. private let kPostBodyKey = "postBody" -/** @var kPendingTokenKey - @brief The key for the "pendingToken" value in the request. - */ +/// The key for the "pendingToken" value in the request. private let kPendingTokenKey = "pendingToken" -/** @var kAutoCreateKey - @brief The key for the "autoCreate" value in the request. - */ +/// The key for the "autoCreate" value in the request. private let kAutoCreateKey = "autoCreate" -/** @var kIDTokenKey - @brief The key for the "idToken" value in the request. This is actually the STS Access Token, - despite it's confusing (backwards compatiable) parameter name. - */ +/// The key for the "idToken" value in the request. This is actually the STS Access Token, +/// despite its confusing (backwards compatiable) parameter name. private let kIDTokenKey = "idToken" -/** @var kReturnSecureTokenKey - @brief The key for the "returnSecureToken" value in the request. - */ +/// The key for the "returnSecureToken" value in the request. private let kReturnSecureTokenKey = "returnSecureToken" -/** @var kReturnIDPCredentialKey - @brief The key for the "returnIdpCredential" value in the request. - */ +/// The key for the "returnIdpCredential" value in the request. private let kReturnIDPCredentialKey = "returnIdpCredential" -/** @var kSessionIDKey - @brief The key for the "sessionID" value in the request. - */ +/// The key for the "sessionID" value in the request. private let kSessionIDKey = "sessionId" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" -/** @var kUserKey - @brief The key for the "user" value in the request. The value is a JSON object that contains the - name of the user. - */ +/// The key for the "user" value in the request. The value is a JSON object that contains the +/// name of the user. private let kUserKey = "user" -/** @var kNameKey - @brief The key for the "name" value in the request. The value is a JSON object that contains the - first and/or last name of the user. - */ +/// The key for the "name" value in the request. The value is a JSON object that contains the +/// first and/or last name of the user. private let kNameKey = "name" -/** @var kFirstNameKey - @brief The key for the "firstName" value in the request. - */ +/// The key for the "firstName" value in the request. private let kFirstNameKey = "firstName" -/** @var kLastNameKey - @brief The key for the "lastName" value in the request. - */ +/// The key for the "lastName" value in the request. private let kLastNameKey = "lastName" -/** @class FIRVerifyAssertionRequest - @brief Represents the parameters for the verifyAssertion endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion - */ +/// Represents the parameters for the verifyAssertion endpoint. +/// See https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class VerifyAssertionRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = VerifyAssertionResponse - /** @property requestURI - @brief The URI to which the IDP redirects the user back. It may contain federated login result - params added by the IDP. - */ + /// The URI to which the IDP redirects the user back. It may contain federated login result + /// params added by the IDP. var requestURI: String? - /** @property pendingToken - @brief The Firebase ID Token for the IDP pending to be confirmed by the user. - */ + /// The Firebase ID Token for the IDP pending to be confirmed by the user. var pendingToken: String? - /** @property accessToken - @brief The STS Access Token for the authenticated user, only needed for linking the user. - */ + /// The STS Access Token for the authenticated user, only needed for linking the user. var accessToken: String? - /** @property returnSecureToken - @brief Whether the response should return access token and refresh token directly. - @remarks The default value is @c YES . - */ - var returnSecureToken: Bool = false + /// Whether the response should return access token and refresh token directly. + /// The default value is `true` . + + var returnSecureToken: Bool = true // MARK: - Components of "postBody" - /** @property providerID - @brief The ID of the IDP whose credentials are being presented to the endpoint. - */ + /// The ID of the IDP whose credentials are being presented to the endpoint. let providerID: String - /** @property providerAccessToken - @brief An access token from the IDP. - */ + /// An access token from the IDP. var providerAccessToken: String? - /** @property providerIDToken - @brief An ID Token from the IDP. - */ + /// An ID Token from the IDP. var providerIDToken: String? - /** @property providerRawNonce - @brief An raw nonce from the IDP. - */ + /// An raw nonce from the IDP. var providerRawNonce: String? - /** @property returnIDPCredential - @brief Whether the response should return the IDP credential directly. - */ - var returnIDPCredential: Bool = false + /// Whether the response should return the IDP credential directly. + var returnIDPCredential: Bool = true - /** @property providerOAuthTokenSecret - @brief A session ID used to map this request to a headful-lite flow. - */ + /// A session ID used to map this request to a headful-lite flow. var sessionID: String? - /** @property providerOAuthTokenSecret - @brief An OAuth client secret from the IDP. - */ + /// An OAuth client secret from the IDP. var providerOAuthTokenSecret: String? - /** @property inputEmail - @brief The originally entered email in the UI. - */ + /// The originally entered email in the UI. var inputEmail: String? - /** @property autoCreate - @brief A flag that indicates whether or not the user should be automatically created. - */ - var autoCreate: Bool = false + /// A flag that indicates whether or not the user should be automatically created. + var autoCreate: Bool = true - /** @property fullName - @brief A full name from the IdP. - */ + /// A full name from the IdP. var fullName: PersonNameComponents? init(providerID: String, requestConfiguration: AuthRequestConfiguration) { self.providerID = providerID - returnSecureToken = true - autoCreate = true - returnIDPCredential = true - super.init(endpoint: kVerifyAssertionEndpoint, requestConfiguration: requestConfiguration) } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionResponse.swift index b8610028df5..a2077ba8bb9 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyAssertionResponse.swift @@ -14,191 +14,119 @@ import Foundation -/** @class FIRVerifyAssertionResponse - @brief Represents the response from the verifyAssertion endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion - */ +/// Represents the response from the verifyAssertion endpoint. +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/verifyAssertion class VerifyAssertionResponse: AuthRPCResponse, AuthMFAResponse { required init() {} - /** @property federatedID - @brief The unique ID identifies the IdP account. - */ + /// The unique ID identifies the IdP account. var federatedID: String? - /** @property providerID - @brief The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com, - live.net and yahoo.com. If the "providerId" param is set to OpenID OP identifer other than - the whilte listed IdPs the OP identifier is returned. If the "identifier" param is federated - ID in the createAuthUri request. The domain part of the federated ID is returned. - */ + /// The IdP ID. For white listed IdPs it's a short domain name e.g. google.com, aol.com, + /// live.net and yahoo.com.If the "providerId" param is set to OpenID OP identifer other than + /// the white listed IdPs the OP identifier is returned.If the "identifier" param is federated + /// ID in the createAuthUri request.The domain part of the federated ID is returned. var providerID: String? - /** @property localID - @brief The RP local ID if it's already been mapped to the IdP account identified by the - federated ID. - */ + /// The RP local ID if it's already been mapped to the IdP account identified by the federated ID. var localID: String? - /** @property email - @brief The email returned by the IdP. NOTE: The federated login user may not own the email. - */ + /// The email returned by the IdP. NOTE: The federated login user may not own the email. var email: String? - /** @property inputEmail - @brief It's the identifier param in the createAuthUri request if the identifier is an email. It - can be used to check whether the user input email is different from the asserted email. - */ + /// It's the identifier param in the createAuthUri request if the identifier is an email. It + /// can be used to check whether the user input email is different from the asserted email. var inputEmail: String? - /** @property originalEmail - @brief The original email stored in the mapping storage. It's returned when the federated ID is - associated to a different email. - */ + /// The original email stored in the mapping storage. It's returned when the federated ID is + /// associated to a different email. var originalEmail: String? - /** @property oauthRequestToken - @brief The user approved request token for the OpenID OAuth extension. - */ + /// The user approved request token for the OpenID OAuth extension. var oauthRequestToken: String? - /** @property oauthScope - @brief The scope for the OpenID OAuth extension. - */ + /// The scope for the OpenID OAuth extension. var oauthScope: String? - /** @property firstName - @brief The first name of the user. - */ + /// The first name of the user. var firstName: String? - /** @property lastName - @brief The last name of the user. - */ + /// The last name of the user. var lastName: String? - /** @property fullName - @brief The full name of the user. - */ + /// The full name of the user. var fullName: String? - /** @property nickName - @brief The nick name of the user. - */ + /// The nickname of the user. var nickName: String? - /** @property displayName - @brief The display name of the user. - */ + /// The display name of the user. var displayName: String? - /** @property idToken - @brief Either an authorization code suitable for performing an STS token exchange, or the - access token from Secure Token Service, depending on whether @c returnSecureToken is set - on the request. - */ + /// Either an authorization code suitable for performing an STS token exchange, or the + /// access token from Secure Token Service, depending on whether `returnSecureToken` is set + /// on the request. private(set) var idToken: String? - /** @property approximateExpirationDate - @brief The approximate expiration date of the access token. - */ + /// The approximate expiration date of the access token. var approximateExpirationDate: Date? - /** @property refreshToken - @brief The refresh token from Secure Token Service. - */ + /// The refresh token from Secure Token Service. var refreshToken: String? - /** @property action - @brief The action code. - */ + /// The action code. var action: String? - /** @property language - @brief The language preference of the user. - */ + /// The language preference of the user. var language: String? - /** @property timeZone - @brief The timezone of the user. - */ + /// The timezone of the user. var timeZone: String? - /** @property photoURL - @brief The URI of the accessible profile picture. - */ + /// The URI of the accessible profile picture. var photoURL: URL? - /** @property dateOfBirth - @brief The birth date of the IdP account. - */ + /// The birth date of the IdP account. var dateOfBirth: String? - /** @property context - @brief The opaque value used by the client to maintain context info between the authentication - request and the IDP callback. - */ + /// The opaque value used by the client to maintain context info between the authentication + /// request and the IDP callback. var context: String? - /** @property verifiedProvider - @brief When action is 'map', contains the idps which can be used for confirmation. - */ + /// When action is 'map', contains the idps which can be used for confirmation. var verifiedProvider: [String]? - /** @property needConfirmation - @brief Whether the assertion is from a non-trusted IDP and need account linking confirmation. - */ + /// Whether the assertion is from a non-trusted IDP and need account linking confirmation. var needConfirmation: Bool = false - /** @property emailRecycled - @brief It's true if the email is recycled. - */ + /// It's true if the email is recycled. var emailRecycled: Bool = false - /** @property emailVerified - @brief The value is true if the IDP is also the email provider. It means the user owns the - email. - */ + /// The value is true if the IDP is also the email provider. It means the user owns the email. var emailVerified: Bool = false - /** @property isNewUser - @brief Flag indicating that the user signing in is a new user and not a returning user. - */ + /// Flag indicating that the user signing in is a new user and not a returning user. var isNewUser: Bool = false - /** @property profile - @brief Dictionary containing the additional IdP specific information. - */ + /// Dictionary containing the additional IdP specific information. var profile: [String: Any]? - /** @property username - @brief The name of the user. - */ + /// The name of the user. var username: String? - /** @property oauthIDToken - @brief The ID token for the OpenID OAuth extension. - */ + /// The ID token for the OpenID OAuth extension. var oauthIDToken: String? - /** @property oauthExpirationDate - @brief The approximate expiration date of the oauth access token. - */ + /// The approximate expiration date of the oauth access token. var oauthExpirationDate: Date? - /** @property oauthAccessToken - @brief The access token for the OpenID OAuth extension. - */ + /// The access token for the OpenID OAuth extension. var oauthAccessToken: String? - /** @property oauthSecretToken - @brief The secret for the OpenID OAuth extention. - */ + /// The secret for the OpenID OAuth extention. var oauthSecretToken: String? - /** @property pendingToken - @brief The pending ID Token string. - */ + /// The pending ID Token string. var pendingToken: String? // MARK: - AuthMFAResponse diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenRequest.swift index e23143c1026..af1518ee653 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenRequest.swift @@ -14,24 +14,16 @@ import Foundation -/** @var kVerifyCustomTokenEndpoint - @brief The "verifyPassword" endpoint. - */ +/// The "verifyPassword" endpoint. private let kVerifyCustomTokenEndpoint = "verifyCustomToken" -/** @var kTokenKey - @brief The key for the "token" value in the request. - */ +/// The key for the "token" value in the request. private let kTokenKey = "token" -/** @var kReturnSecureTokenKey - @brief The key for the "returnSecureToken" value in the request. - */ +/// The key for the "returnSecureToken" value in the request. private let kReturnSecureTokenKey = "returnSecureToken" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenResponse.swift index a309707b325..74053b5f3b5 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyCustomTokenResponse.swift @@ -14,32 +14,22 @@ import Foundation -/** @class FIRVerifyCustomTokenResponse - @brief Represents the response from the verifyCustomToken endpoint. - */ +/// Represents the response from the verifyCustomToken endpoint. class VerifyCustomTokenResponse: AuthRPCResponse { required init() {} - /** @property idToken - @brief Either an authorization code suitable for performing an STS token exchange, or the - access token from Secure Token Service, depending on whether @c returnSecureToken is set - on the request. - */ + /// Either an authorization code suitable for performing an STS token exchange, or the + /// access token from Secure Token Service, depending on whether `returnSecureToken` is set + /// on the request. var idToken: String? - /** @property approximateExpirationDate - @brief The approximate expiration date of the access token. - */ + /// The approximate expiration date of the access token. var approximateExpirationDate: Date? - /** @property refreshToken - @brief The refresh token from Secure Token Service. - */ + /// The refresh token from Secure Token Service. var refreshToken: String? - /** @property isNewUser - @brief Flag indicating that the user signing in is a new user and not a returning user. - */ + /// Flag indicating that the user signing in is a new user and not a returning user. var isNewUser: Bool = false func setFields(dictionary: [String: AnyHashable]) throws { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift index de351d79c67..7a95848c985 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordRequest.swift @@ -14,105 +14,68 @@ import Foundation -/** @var kVerifyPasswordEndpoint - @brief The "verifyPassword" endpoint. - */ +/// The "verifyPassword" endpoint. private let kVerifyPasswordEndpoint = "verifyPassword" -/** @var kEmailKey - @brief The key for the "email" value in the request. - */ +/// The key for the "email" value in the request. private let kEmailKey = "email" -/** @var kPasswordKey - @brief The key for the "password" value in the request. - */ +/// The key for the "password" value in the request. private let kPasswordKey = "password" -/** @var kPendingIDTokenKey - @brief The key for the "pendingIdToken" value in the request. - */ +/// The key for the "pendingIdToken" value in the request. private let kPendingIDTokenKey = "pendingIdToken" -/** @var kCaptchaChallengeKey - @brief The key for the "captchaChallenge" value in the request. - */ +/// The key for the "captchaChallenge" value in the request. private let kCaptchaChallengeKey = "captchaChallenge" -/** @var kCaptchaResponseKey - @brief The key for the "captchaResponse" value in the request. - */ +/// The key for the "captchaResponse" value in the request. private let kCaptchaResponseKey = "captchaResponse" -/** @var kClientType - @brief The key for the "clientType" value in the request. - */ +/// The key for the "clientType" value in the request. private let kClientType = "clientType" -/** @var kRecaptchaVersion - @brief The key for the "recaptchaVersion" value in the request. - */ +/// The key for the "recaptchaVersion" value in the request. private let kRecaptchaVersion = "recaptchaVersion" -/** @var kReturnSecureTokenKey - @brief The key for the "returnSecureToken" value in the request. - */ +/// The key for the "returnSecureToken" value in the request. private let kReturnSecureTokenKey = "returnSecureToken" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" -/** @class FIRVerifyPasswordRequest - @brief Represents the parameters for the verifyPassword endpoint. - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword - */ +/// Represents the parameters for the verifyPassword endpoint. +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class VerifyPasswordRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = VerifyPasswordResponse - /** @property email - @brief The email of the user. - */ + /// The email of the user. private(set) var email: String - /** @property password - @brief The password inputed by the user. - */ + /// The password inputed by the user. private(set) var password: String - /** @property pendingIDToken - @brief The GITKit token for the non-trusted IDP, which is to be confirmed by the user. - */ + /// The GITKit token for the non-trusted IDP, which is to be confirmed by the user. var pendingIDToken: String? - /** @property captchaChallenge - @brief The captcha challenge. - */ + /// The captcha challenge. var captchaChallenge: String? - /** @property captchaResponse - @brief Response to the captcha. - */ + /// Response to the captcha. var captchaResponse: String? - /** @property captchaResponse - @brief The reCAPTCHA version. - */ + /// The reCAPTCHA version. var recaptchaVersion: String? - /** @property returnSecureToken - @brief Whether the response should return access token and refresh token directly. - @remarks The default value is @c YES . - */ - private(set) var returnSecureToken: Bool + /// Whether the response should return access token and refresh token directly. + /// The default value is `true`. + private(set) var returnSecureToken: Bool = true init(email: String, password: String, requestConfiguration: AuthRequestConfiguration) { self.email = email self.password = password - returnSecureToken = true super.init(endpoint: kVerifyPasswordEndpoint, requestConfiguration: requestConfiguration) } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordResponse.swift index 8a5e03262c6..7735450ed76 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPasswordResponse.swift @@ -14,52 +14,37 @@ import Foundation -/** @class FIRVerifyPasswordResponse - @brief Represents the response from the verifyPassword endpoint. - @remarks Possible error codes: - - FIRAuthInternalErrorCodeUserDisabled - - FIRAuthInternalErrorCodeEmailNotFound - @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword - */ +/// Represents the response from the verifyPassword endpoint. +/// +/// Possible error codes: +/// * FIRAuthInternalErrorCodeUserDisabled +/// * FIRAuthInternalErrorCodeEmailNotFound +/// +/// See https: // developers.google.com/identity/toolkit/web/reference/relyingparty/verifyPassword class VerifyPasswordResponse: AuthRPCResponse, AuthMFAResponse { required init() {} - /** @property localID - @brief The RP local ID if it's already been mapped to the IdP account identified by the - federated ID. - */ + /// The RP local ID if it's already been mapped to the IdP account identified by the federated ID. var localID: String? - /** @property email - @brief The email returned by the IdP. NOTE: The federated login user may not own the email. - */ + /// The email returned by the IdP. NOTE: The federated login user may not own the email. var email: String? - /** @property displayName - @brief The display name of the user. - */ + /// The display name of the user. var displayName: String? - /** @property IDToken - @brief Either an authorization code suitable for performing an STS token exchange, or the - access token from Secure Token Service, depending on whether @c returnSecureToken is set - on the request. - */ + /// Either an authorization code suitable for performing an STS token exchange, or the + /// access token from Secure Token Service, depending on whether `returnSecureToken` is set + /// on the request. private(set) var idToken: String? - /** @property approximateExpirationDate - @brief The approximate expiration date of the access token. - */ + /// The approximate expiration date of the access token. var approximateExpirationDate: Date? - /** @property refreshToken - @brief The refresh token from Secure Token Service. - */ + /// The refresh token from Secure Token Service. var refreshToken: String? - /** @property photoURL - @brief The URI of the accessible profile picture. - */ + /// The URI of the accessible profile picture. var photoURL: URL? // MARK: - AuthMFAResponse diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberRequest.swift index 93389e654b3..2fb0b183709 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberRequest.swift @@ -14,53 +14,32 @@ import Foundation -/** @var kVerifyPhoneNumberEndPoint - @brief The "verifyPhoneNumber" endpoint. - */ +/// The "verifyPhoneNumber" endpoint. private let kVerifyPhoneNumberEndPoint = "verifyPhoneNumber" -/** @var kVerificationIDKey - @brief The key for the verification ID parameter in the request. - */ +/// The key for the verification ID parameter in the request. private let kVerificationIDKey = "sessionInfo" -/** @var kVerificationCodeKey - @brief The key for the verification code parameter in the request. - */ +/// The key for the verification code parameter in the request. private let kVerificationCodeKey = "code" -/** @var kIDTokenKey - @brief The key for the "ID Token" value in the request. - */ +/// The key for the "ID Token" value in the request. private let kIDTokenKey = "idToken" -/** @var kTemporaryProofKey - @brief The key for the temporary proof value in the request. - */ +/// The key for the temporary proof value in the request. private let kTemporaryProofKey = "temporaryProof" -/** @var kPhoneNumberKey - @brief The key for the phone number value in the request. - */ +/// The key for the phone number value in the request. private let kPhoneNumberKey = "phoneNumber" -/** @var kOperationKey - @brief The key for the operation value in the request. - */ +/// The key for the operation value in the request. private let kOperationKey = "operation" -/** @var kTenantIDKey - @brief The key for the tenant id value in the request. - */ +/// The key for the tenant id value in the request. private let kTenantIDKey = "tenantId" extension AuthOperationType { - /** @fn FIRAuthOperationString - @brief Returns a string object corresponding to the provided FIRAuthOperationType value. - @param operationType The value of the FIRAuthOperationType enum which will be translated to its - corresponding string value. - @return The string value corresponding to the FIRAuthOperationType argument. - */ + /// - Returns: The string value corresponding to the AuthOperationType. var operationString: String { switch self { case .unspecified: @@ -81,43 +60,30 @@ extension AuthOperationType { class VerifyPhoneNumberRequest: IdentityToolkitRequest, AuthRPCRequest { typealias Response = VerifyPhoneNumberResponse - /** @property verificationID - @brief The verification ID obtained from the response of @c sendVerificationCode. - */ + /// The verification ID obtained from the response of `sendVerificationCode`. var verificationID: String? - /** @property verificationCode - @brief The verification code provided by the user. - */ + /// The verification code provided by the user. var verificationCode: String? - /** @property accessToken - @brief The STS Access Token for the authenticated user. - */ + /// The STS Access Token for the authenticated user. var accessToken: String? - /** @var temporaryProof - @brief The temporary proof code, previously returned from the backend. - */ + /// The temporary proof code, previously returned from the backend. var temporaryProof: String? - /** @var phoneNumber - @brief The phone number to be verified in the request. - */ + /// The phone number to be verified in the request. var phoneNumber: String? - /** @var operation - @brief The type of operation triggering this verify phone number request. - */ + /// The type of operation triggering this verify phone number request. var operation: AuthOperationType - /** @fn initWithTemporaryProof:phoneNumberAPIKey - @brief Designated initializer. - @param temporaryProof The temporary proof sent by the backed. - @param phoneNumber The phone number associated with the credential to be signed in. - @param operation Indicates what operation triggered the verify phone number request. - @param requestConfiguration An object containing configurations to be added to the request. - */ + /// Designated initializer. + /// - Parameter temporaryProof: The temporary proof sent by the backed. + /// - Parameter phoneNumber: The phone number associated with the credential to be signed in . + /// - Parameter operation: Indicates what operation triggered the verify phone number request. + /// - Parameter requestConfiguration: An object containing configurations to be added to the + /// request. init(temporaryProof: String, phoneNumber: String, operation: AuthOperationType, requestConfiguration: AuthRequestConfiguration) { self.temporaryProof = temporaryProof @@ -126,13 +92,13 @@ class VerifyPhoneNumberRequest: IdentityToolkitRequest, AuthRPCRequest { super.init(endpoint: kVerifyPhoneNumberEndPoint, requestConfiguration: requestConfiguration) } - /** @fn initWithVerificationID:verificationCode:requestConfiguration - @brief Designated initializer. - @param verificationID The verification ID obtained from the response of @c sendVerificationCode. - @param verificationCode The verification code provided by the user. - @param operation Indicates what operation triggered the verify phone number request. - @param requestConfiguration An object containing configurations to be added to the request. - */ + /// Designated initializer. + /// - Parameter verificationID: The verification ID obtained from the response of + /// `sendVerificationCode`. + /// - Parameter verificationCode: The verification code provided by the user. + /// - Parameter operation: Indicates what operation triggered the verify phone number request. + /// - Parameter requestConfiguration: An object containing configurations to be added to the + /// request. init(verificationID: String, verificationCode: String, operation: AuthOperationType, diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberResponse.swift index 9ac4821ed37..82325a25fbe 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/VerifyPhoneNumberResponse.swift @@ -17,42 +17,27 @@ import Foundation class VerifyPhoneNumberResponse: AuthRPCResponse { required init() {} - /** @property IDToken - @brief Either an authorization code suitable for performing an STS token exchange, or the - access token from Secure Token Service, depending on whether @c returnSecureToken is set - on the request. - */ + /// Either an authorization code suitable for performing an STS token exchange, or the + /// access token from Secure Token Service, depending on whether `returnSecureToken` is set + /// on the request. var idToken: String? - /** @property refreshToken - @brief The refresh token from Secure Token Service. - */ + /// The refresh token from Secure Token Service. var refreshToken: String? - /** @property localID - @brief The Firebase Auth user ID. - */ + /// The Firebase Auth user ID. var localID: String? - /** @property phoneNumber - @brief The verified phone number. - */ + /// The verified phone number. var phoneNumber: String? - /** @property temporaryProof - @brief The temporary proof code returned by the backend. - */ + /// The temporary proof code returned by the backend. var temporaryProof: String? - /** @property isNewUser - @brief Flag indicating that the user signing in is a new user and not a returning user. - */ - + /// Flag indicating that the user signing in is a new user and not a returning user. var isNewUser: Bool = false - /** @property approximateExpirationDate - @brief The approximate expiration date of the access token. - */ + /// The approximate expiration date of the access token. var approximateExpirationDate: Date? // XXX TODO(ObjC): What might this be? diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift index dce862d2ed0..8b6cd0b0512 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift @@ -18,20 +18,20 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension MultiFactor: NSSecureCoding {} - /** @class FIRMultiFactor - @brief The interface defining the multi factor related properties and operations pertaining to a - user. - This class is available on iOS only. - */ + + /// The interface defining the multi factor related properties and operations pertaining to a + /// user. + /// + /// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactor) open class MultiFactor: NSObject { @objc open var enrolledFactors: [MultiFactorInfo] - /** @fn getSessionWithCompletion: - @brief Get a session for a second factor enrollment operation. - @param completion A block with the session identifier for a second factor enrollment operation. - This is used to identify the current user trying to enroll a second factor. - */ + /// Get a session for a second factor enrollment operation. + /// + /// This is used to identify the current user trying to enroll a second factor. + /// - Parameter completion: A block with the session identifier for a second factor enrollment + /// operation. @objc(getSessionWithCompletion:) open func getSessionWithCompletion(_ completion: ((MultiFactorSession?, Error?) -> Void)?) { let session = MultiFactorSession.sessionForCurrentUser @@ -40,11 +40,9 @@ import Foundation } } - /** @fn getSessionWithCompletion: - @brief Get a session for a second factor enrollment operation. - @param completion A block with the session identifier for a second factor enrollment operation. - This is used to identify the current user trying to enroll a second factor. - */ + /// Get a session for a second factor enrollment operation. + /// + /// This is used to identify the current user trying to enroll a second factor. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func session() async throws -> MultiFactorSession { return try await withCheckedThrowingContinuation { continuation in @@ -58,12 +56,12 @@ import Foundation } } - /** @fn enrollWithAssertion:displayName:completion: - @brief Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the - current user. - @param displayName An optional display name associated with the multi factor to enroll. - @param completion The block invoked when the request is complete, or fails. - */ + /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the + /// current user. + /// - Parameter assertion: The `MultiFactorAssertion`. + /// - Parameter displayName: An optional display name associated with the multi factor to + /// enroll. + /// - Parameter completion: The block invoked when the request is complete, or fails. @objc(enrollWithAssertion:displayName:completion:) open func enroll(with assertion: MultiFactorAssertion, displayName: String?, @@ -168,12 +166,12 @@ import Foundation } } - /** @fn enrollWithAssertion:displayName:completion: - @brief Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the - current user. - @param displayName An optional display name associated with the multi factor to enroll. - @param completion The block invoked when the request is complete, or fails. - */ + /// Enrolls a second factor as identified by the `MultiFactorAssertion` parameter for the + /// current user. + /// - Parameter assertion: The `MultiFactorAssertion`. + /// - Parameter displayName: An optional display name associated with the multi factor to + /// enroll. + /// - Parameter completion: The block invoked when the request is complete, or fails. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func enroll(with assertion: MultiFactorAssertion, displayName: String?) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -187,32 +185,24 @@ import Foundation } } - /** @fn unenrollWithInfo:completion: - @brief Unenroll the given multi factor. - @param completion The block invoked when the request to send the verification email is complete, - or fails. - */ + /// Unenroll the given multi factor. + /// - Parameter completion: The block invoked when the request to send the verification email is + /// complete, or fails. @objc(unenrollWithInfo:completion:) open func unenroll(with factorInfo: MultiFactorInfo, completion: ((Error?) -> Void)?) { unenroll(withFactorUID: factorInfo.uid, completion: completion) } - /** @fn unenrollWithInfo:completion: - @brief Unenroll the given multi factor. - @param completion The block invoked when the request to send the verification email is complete, - or fails. - */ + /// Unenroll the given multi factor. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func unenroll(with factorInfo: MultiFactorInfo) async throws { try await unenroll(withFactorUID: factorInfo.uid) } - /** @fn unenrollWithFactorUID:completion: - @brief Unenroll the given multi factor. - @param completion The block invoked when the request to send the verification email is complete, - or fails. - */ + /// Unenroll the given multi factor. + /// - Parameter completion: The block invoked when the request to send the verification email is + /// complete, or fails. @objc(unenrollWithFactorUID:completion:) open func unenroll(withFactorUID factorUID: String, completion: ((Error?) -> Void)?) { @@ -252,6 +242,7 @@ import Foundation } } + /// Unenroll the given multi factor. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func unenroll(withFactorUID factorUID: String) async throws { return try await withCheckedThrowingContinuation { continuation in diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift index 1c313b3a18c..ff3edc94dd0 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift @@ -16,15 +16,12 @@ import Foundation #if os(iOS) - /** @class FIRMultiFactorAssertion - @brief The base class for asserting ownership of a second factor. This is equivalent to the - AuthCredential class. - This class is available on iOS only. - */ + /// The base class for asserting ownership of a second factor. This is equivalent to the + /// AuthCredential class. + /// + /// This class is available on iOS only. @objc(FIRMultiFactorAssertion) open class MultiFactorAssertion: NSObject { - /** - @brief The second factor identifier for this opaque object asserting a second factor. - */ + /// The second factor identifier for this opaque object asserting a second factor. @objc open var factorID: String init(factorID: String) { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift index 60d51cbb5dd..2b19b4963f0 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift @@ -17,29 +17,20 @@ import Foundation #if os(iOS) extension MultiFactorInfo: NSSecureCoding {} - /** @class FIRMultiFactorInfo - @brief Safe public structure used to represent a second factor entity from a client perspective. - This class is available on iOS only. - */ + /// Safe public structure used to represent a second factor entity from a client perspective. + /// + /// This class is available on iOS only. @objc(FIRMultiFactorInfo) open class MultiFactorInfo: NSObject { - /** - @brief The multi-factor enrollment ID. - */ + /// The multi-factor enrollment ID. @objc(UID) public let uid: String - /** - @brief The user friendly name of the current second factor. - */ + /// The user friendly name of the current second factor. @objc public let displayName: String? - /** - @brief The second factor enrollment date. - */ + /// The second factor enrollment date. @objc public let enrollmentDate: Date - /** - @brief The identifier of the second factor. - */ + /// The identifier of the second factor. @objc public let factorID: String init(proto: AuthProtoMFAEnrollment, factorID: String) { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift index fd0eef4c8d9..a9936928fec 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift @@ -15,36 +15,28 @@ import Foundation #if os(iOS) - /** @class FIRPhoneMultiFactorAssertion - @brief The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone - second factor. - This class is available on iOS only. - */ + + /// The subclass of base class `MultiFactorAssertion`, used to assert ownership of a phone + /// second factor. + /// + /// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactorResolver) open class MultiFactorResolver: NSObject { - /** - @brief The opaque session identifier for the current sign-in flow. - */ + /// The opaque session identifier for the current sign-in flow. @objc public let session: MultiFactorSession - /** - @brief The list of hints for the second factors needed to complete the sign-in for the current - session. - */ + /// The list of hints for the second factors needed to complete the sign-in for the current + /// session. @objc public let hints: [MultiFactorInfo] - /** - @brief The Auth reference for the current FIRMultiResolver. - */ + /// The Auth reference for the current `MultiResolver`. @objc public let auth: Auth - /** @fn resolveSignInWithAssertion:completion: - @brief A helper function to help users complete sign in with a second factor using an - FIRMultiFactorAssertion confirming the user successfully completed the second factor - challenge. - @param completion The block invoked when the request is complete, or fails. - */ + /// A helper function to help users complete sign in with a second factor using a + /// `MultiFactorAssertion` confirming the user successfully completed the second factor + /// challenge. + /// - Parameter completion: The block invoked when the request is complete, or fails. @objc(resolveSignInWithAssertion:completion:) open func resolveSignIn(with assertion: MultiFactorAssertion, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { @@ -97,12 +89,9 @@ import Foundation } } - /** @fn resolveSignInWithAssertion:completion: - @brief A helper function to help users complete sign in with a second factor using an - FIRMultiFactorAssertion confirming the user successfully completed the second factor - challenge. - @param completion The block invoked when the request is complete, or fails. - */ + /// A helper function to help users complete sign in with a second factor using a + /// `MultiFactorAssertion` confirming the user successfully completed the second factor + /// challenge. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func resolveSignIn(with assertion: MultiFactorAssertion) async throws -> AuthDataResult { return try await withCheckedThrowingContinuation { continuation in diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift index c1b8979f76e..cc83e3f2f0d 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift @@ -15,38 +15,31 @@ import Foundation #if os(iOS) - /** @class FIRMultiFactorSession - @brief Opaque object that identifies the current session to enroll a second factor or to - complete sign in when previously enrolled. Identifies the current session to enroll a second factor or to complete sign in when - previously enrolled. It contains additional context on the existing user, notably the confirmation - that the user passed the first factor challenge. - This class is available on iOS only. - */ + + /// Opaque object that identifies the current session to enroll a second factor or to + /// complete sign in when previously enrolled. + /// + /// Identifies the current session to enroll a second factor + /// or to complete sign in when previously enrolled. It contains additional context on the + /// existing user, notably the confirmation that the user passed the first factor challenge. + /// + /// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactorSession) open class MultiFactorSession: NSObject { - /** - @brief The ID token for an enroll flow. This has to be retrieved after recent authentication. - */ + /// The ID token for an enroll flow. This has to be retrieved after recent authentication. var idToken: String? - /** - @brief The pending credential after an enrolled second factor user signs in successfully with the - first factor - */ + /// The pending credential after an enrolled second factor user signs in successfully with the + /// first factor. var mfaPendingCredential: String? - /** - @brief Multi factor info for the current user. - */ + /// Multi factor info for the current user. var multiFactorInfo: MultiFactorInfo? - /** - @brief Current user object - */ + /// Current user object. var currentUser: User? class var sessionForCurrentUser: MultiFactorSession { - // TODO: Fix for the right Auth instance. (broken in ObjC) guard let currentUser = Auth.auth().currentUser else { fatalError("Internal Auth Error: missing user for multifactor auth") } diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift index 7271bb1a7ff..999809e4bd3 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift @@ -16,11 +16,10 @@ import Foundation #if os(iOS) - /** @class FIRPhoneMultiFactorAssertion - @brief The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone - second factor. - This class is available on iOS only. - */ + /// The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone + /// second factor. + /// + /// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRPhoneMultiFactorAssertion) open class PhoneMultiFactorAssertion: MultiFactorAssertion { var authCredential: PhoneAuthCredential? diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift index 9370402de5d..cd213c14196 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift @@ -16,20 +16,19 @@ import Foundation #if os(iOS) - /** @class FIRPhoneMultiFactorGenerator - @brief The data structure used to help initialize an assertion for a second factor entity to the - Firebase Auth/CICP server. Depending on the type of second factor, this will help generate - the assertion. - This class is available on iOS only. - */ + /// The data structure used to help initialize an assertion for a second factor entity to the + /// Firebase Auth/CICP server. + /// + /// Depending on the type of second factor, this will help generate the assertion. + /// + /// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRPhoneMultiFactorGenerator) open class PhoneMultiFactorGenerator: NSObject { - /** @fn assertionWithCredential: - @brief Initializes the MFA assertion to confirm ownership of the phone second factor. Note that - this API is used for both enrolling and signing in with a phone second factor. - @param phoneAuthCredential The phone auth credential used for multi factor flows. - */ + /// Initializes the MFA assertion to confirm ownership of the phone second factor. + /// + /// Note that this API is used for both enrolling and signing in with a phone second factor. + /// - Parameter phoneAuthCredential: The phone auth credential used for multi factor flows. @objc(assertionWithCredential:) open class func assertion(with phoneAuthCredential: PhoneAuthCredential) -> PhoneMultiFactorAssertion { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift index 35be568d8a8..58e5fc5fb8b 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift @@ -16,27 +16,19 @@ import Foundation #if os(iOS) - /** @class FIRPhoneMultiFactorInfo - @brief Extends the MultiFactorInfo class for phone number second factors. - The identifier of this second factor is "phone". - This class is available on iOS only. - */ + /// Extends the MultiFactorInfo class for phone number second factors. + /// + /// The identifier of this second factor is "phone". + /// + /// This class is available on iOS only. @objc(FIRPhoneMultiFactorInfo) open class PhoneMultiFactorInfo: MultiFactorInfo { - /** - @brief The string identifier for using phone as a second factor. - This constant is available on iOS only. - */ + /// The string identifier for using phone as a second factor. @objc(FIRPhoneMultiFactorID) public static let PhoneMultiFactorID = "phone" - /** - @brief The string identifier for using TOTP as a second factor. - This constant is available on iOS only. - */ + /// The string identifier for using TOTP as a second factor. @objc(FIRTOTPMultiFactorID) public static let TOTPMultiFactorID = "totp" - /** - @brief This is the phone number associated with the current second factor. - */ + /// This is the phone number associated with the current second factor. @objc open var phoneNumber: String init(proto: AuthProtoMFAEnrollment) { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift index a11eed8074d..b5b1c43f3d6 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift @@ -21,11 +21,10 @@ import Foundation case enrollmentID(String) } - /** @class FIRTOTPMultiFactorAssertion - @brief The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP - (Time-based One Time Password) second factor. - This class is available on iOS only. - */ + /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP + /// (Time-based One Time Password) second factor. + /// + /// This class is available on iOS only. @objc(FIRTOTPMultiFactorAssertion) open class TOTPMultiFactorAssertion: MultiFactorAssertion { let oneTimePassword: String let secretOrID: SecretOrID diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift index 970eb3b1881..4fc574caedb 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift @@ -15,23 +15,19 @@ import Foundation #if os(iOS) - /** - @class TOTPMultiFactorGenerator - @brief The data structure used to help initialize an assertion for a second factor entity to the - Firebase Auth/CICP server. Depending on the type of second factor, this will help generate - the assertion. - This class is available on iOS only. - */ + + /// The data structure used to help initialize an assertion for a second factor entity to the + /// Firebase Auth/CICP server. Depending on the type of second factor, this will help generate + /// the assertion. + /// + /// This class is available on iOS only. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRTOTPMultiFactorGenerator) open class TOTPMultiFactorGenerator: NSObject { - /** - @fn generateSecretWithMultiFactorSession - @brief Creates a TOTP secret as part of enrolling a TOTP second factor. Used for generating a - QR code URL or inputting into a TOTP app. This method uses the auth instance corresponding to the - user in the multiFactorSession. - @param session The multiFactorSession instance. - @param completion Completion block - */ + /// Creates a TOTP secret as part of enrolling a TOTP second factor. Used for generating a + /// QR code URL or inputting into a TOTP app. This method uses the auth instance corresponding + /// to the user in the multiFactorSession. + /// - Parameter session: The multiFactorSession instance. + /// - Parameter completion: Completion block @objc(generateSecretWithMultiFactorSession:completion:) open class func generateSecret(with session: MultiFactorSession, completion: @escaping (TOTPSecret?, Error?) -> Void) { @@ -71,9 +67,14 @@ import Foundation } } + /// Creates a TOTP secret as part of enrolling a TOTP second factor. + /// + /// Used for generating a QR code URL or inputting into a TOTP app. This + /// method uses the auth instance correspondingto the user in the multiFactorSession. + /// - Parameter session: The multiFactorSession instance. + /// - Returns: The TOTP secret. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - open class func generateSecret(with session: MultiFactorSession) async throws - -> TOTPSecret { + open class func generateSecret(with session: MultiFactorSession) async throws -> TOTPSecret { return try await withCheckedThrowingContinuation { continuation in self.generateSecret(with: session) { secret, error in if let secret { @@ -85,13 +86,12 @@ import Foundation } } - /** - @fn assertionForEnrollmentWithSecret: - @brief Initializes the MFA assertion to confirm ownership of the TOTP second factor. This assertion - is used to complete enrollment of TOTP as a second factor. - @param secret The TOTP secret. - @param oneTimePassword one time password string. - */ + /// Initializes the MFA assertion to confirm ownership of the TOTP second factor. + /// + /// This assertion is used to complete enrollment of TOTP as a second factor. + /// - Parameter secret: The TOTP secret. + /// - Parameter oneTimePassword: One time password string. + /// - Returns: The MFA assertion. @objc(assertionForEnrollmentWithSecret:oneTimePassword:) open class func assertionForEnrollment(with secret: TOTPSecret, oneTimePassword: String) -> TOTPMultiFactorAssertion { @@ -99,13 +99,12 @@ import Foundation oneTimePassword: oneTimePassword) } - /** - @fn assertionForSignInWithenrollmentID: - @brief Initializes the MFA assertion to confirm ownership of the TOTP second factor. This - assertion is used to complete signIn with TOTP as a second factor. - @param enrollmentID The ID that identifies the enrolled TOTP second factor. - @param oneTimePassword one time password string. - */ + /// Initializes the MFA assertion to confirm ownership of the TOTP second factor. + /// + /// This assertion is used to complete signIn with TOTP as a second factor. + /// - Parameter enrollmentID: The ID that identifies the enrolled TOTP second factor. + /// - Parameter oneTimePassword: one time password string. + /// - Returns: The MFA assertion. @objc(assertionForSignInWithEnrollmentID:oneTimePassword:) open class func assertionForSignIn(withEnrollmentID enrollmentID: String, oneTimePassword: String) -> TOTPMultiFactorAssertion { @@ -113,5 +112,4 @@ import Foundation oneTimePassword: oneTimePassword) } } - #endif diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift index 7cc3f01ae1c..2273b2aea9e 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift @@ -16,23 +16,17 @@ import Foundation #if os(iOS) - /** - @class FIRTotpMultiFactorInfo - @brief Extends the MultiFactorInfo class for time based one-time password second factors. - The identifier of this second factor is "totp". - This class is available on iOS only. - */ + /// Extends the MultiFactorInfo class for time based one-time password second factors. + /// + /// The identifier of this second factor is "totp". + /// + /// This class is available on iOS only. class TOTPMultiFactorInfo: MultiFactorInfo { - /** - @brief This is the totp info for the second factor. - */ + /// This is the totp info for the second factor. let totpInfo: NSObject? - /** - @fn initWithProto: - @brief Initilize the FIRAuthProtoMFAEnrollment instance with proto. - @param proto FIRAuthProtoMFAEnrollment proto object. - */ + /// Initialize the AuthProtoMFAEnrollment instance with proto. + /// - Parameter proto: AuthProtoMFAEnrollment proto object. init(proto: AuthProtoMFAEnrollment) { totpInfo = proto.totpInfo super.init(proto: proto, factorID: PhoneMultiFactorInfo.TOTPMultiFactorID) @@ -43,5 +37,4 @@ import Foundation fatalError("init(coder:) has not been implemented") } } - #endif diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift index b8d34281a79..5588539a89b 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift @@ -22,28 +22,24 @@ import Foundation #if os(iOS) import UIKit - /** @class FIRTOTPMultiFactorAssertion - @brief The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP - (Time-based One Time Password) second factor. - This class is available on iOS only. - */ + /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP + /// (Time-based One Time Password) second factor. + /// + /// This class is available on iOS only. @objc(FIRTOTPSecret) open class TOTPSecret: NSObject { - /** - @brief Returns the shared secret key/seed used to generate time-based one-time passwords. - */ + /// Returns the shared secret key/seed used to generate time-based one-time passwords. @objc open func sharedSecretKey() -> String { return secretKey } - /** - @brief Returns a QRCode URL as described in - https://github.com/google/google-authenticator/wiki/Key-Uri-Format - This can be displayed to the user as a QRCode to be scanned into a TOTP app like Google - Authenticator. - @param accountName the name of the account/app. - @param issuer issuer of the TOTP(likely the app name). - @returns A QRCode URL string. - */ + /// Returns a QRCode URL as described in + /// https://github.com/google/google-authenticator/wiki/Key-Uri-Format. + /// + /// This can be displayed to the user as a QRCode to be scanned into a TOTP app like Google + /// Authenticator. + /// - Parameter accountName: The name of the account/app. + /// - Parameter issuer: Issuer of the TOTP(likely the app name). + /// - Returns: A QRCode URL string. @objc(generateQRCodeURLWithAccountName:issuer:) open func generateQRCodeURL(withAccountName accountName: String, issuer: String) -> String { @@ -54,11 +50,10 @@ import Foundation "&algorithm=%\(hashingAlgorithm)&digits=\(codeLength)" } - /** - @brief Opens the specified QR Code URL in a password manager like iCloud Keychain. - * See more details here: - https://developer.apple.com/documentation/authenticationservices/securing_logins_with_icloud_keychain_verification_codes - */ + /// Opens the specified QR Code URL in a password manager like iCloud Keychain. + /// + /// See more details + /// [here](https://developer.apple.com/documentation/authenticationservices/securing_logins_with_icloud_keychain_verification_codes) @objc(openInOTPAppWithQRCodeURL:) open func openInOTPApp(withQRCodeURL qrCodeURL: String) { if GULAppEnvironmentUtil.isAppExtension() { @@ -82,35 +77,23 @@ import Foundation } } - /** - @brief Shared secret key/seed used for enrolling in TOTP MFA and generating OTPs. - */ + /// Shared secret key/seed used for enrolling in TOTP MFA and generating OTPs. private let secretKey: String - /** - @brief Hashing algorithm used. - */ + /// Hashing algorithm used. private let hashingAlgorithm: String? - /** - @brief Length of the one-time passwords to be generated. - */ + /// Length of the one-time passwords to be generated. private let codeLength: Int - /** - @brief The interval (in seconds) when the OTP codes should change. - */ + /// The interval (in seconds) when the OTP codes should change. private let codeIntervalSeconds: Int - /** - @brief The timestamp by which TOTP enrollment should be completed. This can be used by callers to - show a countdown of when to enter OTP code by. - */ + /// The timestamp by which TOTP enrollment should be completed. This can be used by callers to + /// show a countdown of when to enter OTP code by. private let enrollmentCompletionDeadline: Date? - /** - @brief Additional session information. - */ + /// Additional session information. let sessionInfo: String? init(secretKey: String, hashingAlgorithm: String?, codeLength: Int, codeIntervalSeconds: Int, diff --git a/FirebaseAuth/Sources/Swift/Storage/AuthKeychainServices.swift b/FirebaseAuth/Sources/Swift/Storage/AuthKeychainServices.swift index 785ea8b85e5..fcf24de126a 100644 --- a/FirebaseAuth/Sources/Swift/Storage/AuthKeychainServices.swift +++ b/FirebaseAuth/Sources/Swift/Storage/AuthKeychainServices.swift @@ -15,20 +15,15 @@ import FirebaseCoreExtension import Foundation -/** @var kAccountPrefix - @brief The prefix string for keychain item account attribute before the key. - @remarks A number "1" is encoded in the prefix in case we need to upgrade the scheme in future. - */ +/// The prefix string for keychain item account attribute before the key. +/// +/// A number "1" is encoded in the prefix in case we need to upgrade the scheme in future. private let kAccountPrefix = "firebase_auth_1_" -/** @class FIRAuthKeychain - @brief The utility class to manipulate data in iOS Keychain. - */ +/// The utility class to manipulate data in iOS Keychain. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) final class AuthKeychainServices { - /** @var _service - @brief The name of the keychain service. - */ + /// The name of the keychain service. let service: String let keychainStorage: AuthKeychainStorage @@ -41,11 +36,9 @@ final class AuthKeychainServices { keychainStorage = storage } - /** @fn getItemWithQuery:error: - @brief Get the item from keychain by given query. - @param query The query to query the keychain. - @return The item of the given query. `nil`` if not exist. - */ + /// Get the item from keychain by given query. + /// - Parameter query: The query to query the keychain. + /// - Returns: The item of the given query. `nil` if it doesn't exist. func getItem(query: [String: Any]) throws -> Data? { var mutableQuery = query mutableQuery[kSecReturnData as String] = true @@ -71,12 +64,10 @@ final class AuthKeychainServices { } } - /** @fn setItem:withQuery:error: - @brief Set the item into keychain with given query. - @param item The item to be added into keychain. - @param query The query to query the keychain. - @return Whether the operation succeed. - */ + /// Set the item into keychain with given query. + /// - Parameter item: The item to be added into keychain. + /// - Parameter query: The query to query the keychain. + /// - Returns: Whether the operation succeed. func setItem(_ item: Data, withQuery query: [String: Any]) throws { let status: OSStatus let function: String @@ -97,11 +88,8 @@ final class AuthKeychainServices { throw AuthErrorUtils.keychainError(function: function, status: status) } - /** @fn removeItemWithQuery:error: - @brief Remove the item with given queryfrom keychain. - @param query The query to query the keychain. - @return Whether the operation succeed. - */ + /// Remove the item with given queryfrom keychain. + /// - Parameter query: The query to query the keychain. func removeItem(query: [String: Any]) throws { let status = keychainStorage.delete(query: query) if status == noErr || status == errSecItemNotFound { @@ -110,12 +98,10 @@ final class AuthKeychainServices { throw AuthErrorUtils.keychainError(function: "SecItemDelete", status: status) } - /** @var _legacyItemDeletedForKey - @brief Indicates whether or not this class knows that the legacy item for a particular key has - been deleted. - @remarks This dictionary is to avoid unecessary keychain operations against legacy items. - */ - + /// Indicates whether or not this class knows that the legacy item for a particular key has + /// been deleted. + /// + /// This dictionary is to avoid unnecessary keychain operations against legacy items. private var legacyEntryDeletedForKey: Set = [] static func storage(identifier: String) -> Self { @@ -225,10 +211,8 @@ final class AuthKeychainServices { throw AuthErrorUtils.keychainError(function: function, status: status) } - /** @fn deleteLegacyItemsWithKey: - @brief Deletes legacy item from the keychain if it is not already known to be deleted. - @param key The key for the item. - */ + /// Deletes legacy item from the keychain if it is not already known to be deleted. + /// - Parameter key: The key for the item. private func deleteLegacyItem(key: String) { if legacyEntryDeletedForKey.contains(key) { return @@ -238,10 +222,9 @@ final class AuthKeychainServices { legacyEntryDeletedForKey.insert(key) } - /** @fn genericPasswordQueryWithKey: - @brief Returns a keychain query of generic password to be used to manipulate key'ed value. - @param key The key for the value being manipulated, used as the account field in the query. - */ + /// Returns a keychain query of generic password to be used to manipulate key'ed value. + /// - Parameter key: The key for the value being manipulated, used as the account field in the + /// query. private func genericPasswordQuery(key: String) -> [String: Any] { if key.isEmpty { fatalError("The key cannot be empty.") @@ -255,11 +238,10 @@ final class AuthKeychainServices { return query } - /** @fn legacyGenericPasswordQueryWithKey: - @brief Returns a keychain query of generic password without service field, which is used by - previous version of this class. - @param key The key for the value being manipulated, used as the account field in the query. - */ + /// Returns a keychain query of generic password without service field, which is used by + /// previous version of this class . + /// - Parameter key: The key for the value being manipulated, used as the account field in the + /// query. private func legacyGenericPasswordQuery(key: String) -> [String: Any] { [ kSecClass as String: kSecClassGenericPassword, diff --git a/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorage.swift b/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorage.swift index 41838c6974c..d53bb612402 100644 --- a/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorage.swift +++ b/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorage.swift @@ -14,9 +14,8 @@ import Foundation -/** @class AuthKeychainStorage - @brief Protocol to manage keychain updates. Tests can do a fake implementation. - */ +/// Protocol to manage keychain updates. Tests can do a fake implementation. + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) protocol AuthKeychainStorage { func get(query: [String: Any], result: inout AnyObject?) -> OSStatus diff --git a/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorageReal.swift b/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorageReal.swift index 48f3c775df2..777ff056c3b 100644 --- a/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorageReal.swift +++ b/FirebaseAuth/Sources/Swift/Storage/AuthKeychainStorageReal.swift @@ -14,9 +14,8 @@ import Foundation -/** @class AuthKeychainStorage - @brief The utility class to update the real keychain - */ +/// The utility class to update the real keychain + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AuthKeychainStorageReal: AuthKeychainStorage { func get(query: [String: Any], result: inout AnyObject?) -> OSStatus { diff --git a/FirebaseAuth/Sources/Swift/Storage/AuthUserDefaults.swift b/FirebaseAuth/Sources/Swift/Storage/AuthUserDefaults.swift index fb7d03645ed..54be969dc01 100644 --- a/FirebaseAuth/Sources/Swift/Storage/AuthUserDefaults.swift +++ b/FirebaseAuth/Sources/Swift/Storage/AuthUserDefaults.swift @@ -16,18 +16,14 @@ import Foundation private let kPersistentDomainNamePrefix = "com.google.Firebase.Auth." -/** @class AuthUserDefaults - @brief The utility class to storage data in NSUserDefaults. - */ +/// The utility class to manage data storage in NSUserDefaults. class AuthUserDefaults { - /** @var _persistentDomainName - @brief The name of the persistent domain in user defaults. - */ + /// The name of the persistent domain in user defaults. + private let persistentDomainName: String - /** @var _storage - @brief The backing NSUserDefaults storage for this instance. - */ + /// The backing NSUserDefaults storage for this instance. + private let storage: UserDefaults static func storage(identifier: String) -> Self { @@ -60,10 +56,9 @@ class AuthUserDefaults { storage.setPersistentDomain(allData, forName: persistentDomainName) } - /** @fn clear - @brief Clears all data from the storage. - @remarks This method is only supposed to be called from tests. - */ + /// Clears all data from the storage. + /// + /// This method is only supposed to be called from tests. func clear() { storage.setPersistentDomain([:], forName: persistentDomainName) } diff --git a/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSToken.swift b/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSToken.swift index 72e3d658209..5031a0e212e 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSToken.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSToken.swift @@ -15,27 +15,21 @@ #if !os(macOS) import Foundation - /** @class AuthAPNSToken - @brief A data structure for an APNs token. - */ + /// A data structure for an APNs token. class AuthAPNSToken: NSObject { let data: Data let type: AuthAPNSTokenType - /** @fn initWithData:type: - @brief Initializes the instance. - @param data The APNs token data. - @param type The APNs token type. - @return The initialized instance. - */ + /// Initializes the instance. + /// - Parameter data: The APNs token data. + /// - Parameter type: The APNs token type. + /// - Returns: The initialized instance. init(withData data: Data, type: AuthAPNSTokenType) { self.data = data self.type = type } - /** @property string - @brief The uppercase hexadecimal string form of the APNs token data. - */ + /// The uppercase hexadecimal string form of the APNs token data. lazy var string: String = { let byteArray = [UInt8](data) var s = "" diff --git a/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenManager.swift b/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenManager.swift index dc821748ab0..0e4f87d3b42 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenManager.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenManager.swift @@ -32,32 +32,26 @@ extension UIApplication: AuthAPNSTokenApplication {} - /** @class AuthAPNSToken - @brief A data structure for an APNs token. - */ + /// A class to manage APNs token in memory. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AuthAPNSTokenManager: NSObject { - /** @property timeout - @brief The timeout for registering for remote notification. - @remarks Only tests should access this property. - */ + /// The timeout for registering for remote notification. + /// + /// Only tests should access this property. var timeout: TimeInterval = 5 - /** @fn initWithApplication: - @brief Initializes the instance. - @param application The @c UIApplication to request the token from. - @return The initialized instance. - */ + /// Initializes the instance. + /// - Parameter application: The `UIApplication` to request the token from. + /// - Returns: The initialized instance. init(withApplication application: AuthAPNSTokenApplication) { self.application = application } - // This function is internal to make visible for tests. - /** @fn getTokenWithCallback: - @brief Attempts to get the APNs token. - @param callback The block to be called either immediately or in future, either when a token - becomes available, or when timeout occurs, whichever happens earlier. - */ + /// Attempts to get the APNs token. + /// - Parameter callback: The block to be called either immediately or in future, either when a + /// token becomes available, or when timeout occurs, whichever happens earlier. + /// + /// This function is internal to make visible for tests. func getTokenInternal(callback: @escaping (AuthAPNSToken?, Error?) -> Void) { if let token = tokenStore { callback(token, nil) @@ -94,14 +88,13 @@ } } - /** @property token - @brief The APNs token, if one is available. - @remarks Setting a token with AuthAPNSTokenTypeUnknown will automatically converts it to - a token with the automatically detected type. - */ + /// The APNs token, if one is available. + /// + /// Setting a token with AuthAPNSTokenTypeUnknown will automatically converts it to + /// a token with the automatically detected type. var token: AuthAPNSToken? { get { - return tokenStore + tokenStore } set(setToken) { guard let setToken else { @@ -119,18 +112,16 @@ } } - // Should only be written to in tests + /// Should only be written to in tests var tokenStore: AuthAPNSToken? - /** @fn cancelWithError: - @brief Cancels any pending `getTokenWithCallback:` request. - @param error The error to return. - */ + /// Cancels any pending `getTokenWithCallback:` request. + /// - Parameter error: The error to return . func cancel(withError error: Error) { callback(withToken: nil, error: error) } - // `application` is a var to enable unit test faking. + /// Enable unit test faking. var application: AuthAPNSTokenApplication private var pendingCallbacks: [(AuthAPNSToken?, Error?) -> Void] = [] diff --git a/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenType.swift b/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenType.swift index 35f8e73a6f5..a11ab157bd4 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenType.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/AuthAPNSTokenType.swift @@ -15,22 +15,20 @@ #if !os(macOS) import Foundation - /** - * @brief The APNs token type for the app. - * This enum is available on iOS, macOS Catalyst, tvOS, and watchOS only. - */ + /// The APNs token type for the app. + /// + /// This enum is available on iOS, macOS Catalyst, tvOS, and watchOS only. + @objc(FIRAuthAPNSTokenType) public enum AuthAPNSTokenType: Int { - /** Unknown token type. - The actual token type will be detected from the provisioning profile in the app's bundle. - */ + /// Unknown token type. + /// + /// The actual token type will be detected from the provisioning profile in the app's bundle. case unknown - /** Sandbox token type. - */ + /// Sandbox token type. case sandbox - /** Production token type. - */ + /// Production token type. case prod } #endif diff --git a/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredential.swift b/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredential.swift index e1218f84678..38fdcb06e34 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredential.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredential.swift @@ -14,26 +14,19 @@ import Foundation -/** @class FIRAuthAppCredential - @brief A class represents a credential that proves the identity of the app. - */ +/// A class represents a credential that proves the identity of the app. @objc(FIRAuthAppCredential) class AuthAppCredential: NSObject, NSSecureCoding { - /** @property receipt - @brief The server acknowledgement of receiving client's claim of identity. - */ + /// The server acknowledgement of receiving client's claim of identity. var receipt: String - /** @property secret - @brief The secret that the client received from server via a trusted channel, if ever. - */ + /// The secret that the client received from server via a trusted channel, if ever. var secret: String? - /** @fn initWithReceipt:secret: - @brief Initializes the instance. - @param receipt The server acknowledgement of receiving client's claim of identity. - @param secret The secret that the client received from server via a trusted channel, if ever. - @return The initialized instance. - */ + /// Initializes the instance. + /// - Parameter receipt: The server acknowledgement of receiving client's claim of identity. + /// - Parameter secret: The secret that the client received from server via a trusted channel, if + /// ever. + /// - Returns: The initialized instance. init(receipt: String, secret: String?) { self.secret = secret self.receipt = receipt diff --git a/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredentialManager.swift b/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredentialManager.swift index 094cac5f292..511d354e2fd 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredentialManager.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/AuthAppCredentialManager.swift @@ -15,24 +15,20 @@ #if !os(macOS) import Foundation - /** @class FIRAuthAppCredentialManager - @brief A class to manage app credentials backed by iOS Keychain. - */ + /// A class to manage app credentials backed by iOS Keychain. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AuthAppCredentialManager: NSObject { let kKeychainDataKey = "app_credentials" let kFullCredentialKey = "full_credential" let kPendingReceiptsKey = "pending_receipts" - /** @property credential - @brief The full credential (which has a secret) to be used by the app, if one is available. - */ + /// The full credential (which has a secret) to be used by the app, if one is available. + var credential: AuthAppCredential? - /** @property maximumNumberOfPendingReceipts - @brief The maximum (but not necessarily the minimum) number of pending receipts to be kept. - @remarks Only tests should access this property. - */ + /// The maximum (but not necessarily the minimum) number of pending receipts to be kept. + /// + /// Only tests should access this property. let maximumNumberOfPendingReceipts = 32 init(withKeychain keychain: AuthKeychainServices) { @@ -116,19 +112,13 @@ } } - /** @var _keychainServices - @brief The keychain for app credentials to load from and to save to. - */ + /// The keychain for app credentials to load from and to save to. private let keychainServices: AuthKeychainServices - /** @var pendingReceipts - @brief A list of pending receipts sorted in the order they were recorded. - */ + /// A list of pending receipts sorted in the order they were recorded. private var pendingReceipts: [String] = [] - /** @var callbacksByReceipt - @brief A map from pending receipts to callbacks. - */ + /// A map from pending receipts to callbacks. private var callbacksByReceipt: [String: (AuthAppCredential) -> Void] = [:] // Only for testing. diff --git a/FirebaseAuth/Sources/Swift/SystemService/AuthNotificationManager.swift b/FirebaseAuth/Sources/Swift/SystemService/AuthNotificationManager.swift index d6383ef3d6e..2d78927bec0 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/AuthNotificationManager.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/AuthNotificationManager.swift @@ -16,79 +16,54 @@ import Foundation import UIKit - /** @class FIRAuthAppCredential - @brief A class represents a credential that proves the identity of the app. - */ + /// A class represents a credential that proves the identity of the app. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AuthNotificationManager: NSObject { - /** @var kNotificationKey - @brief The key to locate payload data in the remote notification. - */ + /// The key to locate payload data in the remote notification. private let kNotificationDataKey = "com.google.firebase.auth" - /** @var kNotificationReceiptKey - @brief The key for the receipt in the remote notification payload data. - */ + /// The key for the receipt in the remote notification payload data. private let kNotificationReceiptKey = "receipt" - /** @var kNotificationSecretKey - @brief The key for the secret in the remote notification payload data. - */ + /// The key for the secret in the remote notification payload data. private let kNotificationSecretKey = "secret" - /** @var kNotificationProberKey - @brief The key for marking the prober in the remote notification payload data. - */ + /// The key for marking the prober in the remote notification payload data. private let kNotificationProberKey = "warning" - /** @var kProbingTimeout - @brief Timeout for probing whether the app delegate forwards the remote notification to us. - */ + /// Timeout for probing whether the app delegate forwards the remote notification to us. private let kProbingTimeout = 1.0 - /** @var _application - @brief The application. - */ + /// The application. private let application: UIApplication - /** @var _appCredentialManager - @brief The object to handle app credentials delivered via notification. - */ + /// The object to handle app credentials delivered via notification. private let appCredentialManager: AuthAppCredentialManager - /** @var _hasCheckedNotificationForwarding - @brief Whether notification forwarding has been checked or not. - */ + /// Whether notification forwarding has been checked or not. private var hasCheckedNotificationForwarding: Bool = false - /** @var _isNotificationBeingForwarded - @brief Whether or not notification is being forwarded - */ + /// Whether or not notification is being forwarded private var isNotificationBeingForwarded: Bool = false - /** @property timeout - @brief The timeout for checking for notification forwarding. - @remarks Only tests should access this property. - */ + /// The timeout for checking for notification forwarding. + /// + /// Only tests should access this property. let timeout: TimeInterval - /** @property immediateCallbackForTestFaking - @brief Disable callback waiting for tests. - @remarks Only tests should access this property. - */ + /// Disable callback waiting for tests. + /// + /// Only tests should access this property. var immediateCallbackForTestFaking: (() -> Bool)? - /** @var _pendingCallbacks - @brief All pending callbacks while a check is being performed. - */ + /// All pending callbacks while a check is being performed. private var pendingCallbacks: [(Bool) -> Void]? - /** @fn initWithApplication:appCredentialManager: - @brief Initializes the instance. - @param application The application. - @param appCredentialManager The object to handle app credentials delivered via notification. - @return The initialized instance. - */ + /// Initializes the instance. + /// - Parameter application: The application. + /// - Parameter appCredentialManager: The object to handle app credentials delivered via + /// notification. + /// - Returns: The initialized instance. init(withApplication application: UIApplication, appCredentialManager: AuthAppCredentialManager) { self.application = application @@ -96,11 +71,9 @@ timeout = kProbingTimeout } - /** @fn checkNotificationForwardingWithCallback: - @brief Checks whether or not remote notifications are being forwarded to this class. - @param callback The block to be called either immediately or in future once a result - is available. - */ + /// Checks whether or not remote notifications are being forwarded to this class. + /// - Parameter callback: The block to be called either immediately or in future once a result + /// is available. func checkNotificationForwardingInternal(withCallback callback: @escaping (Bool) -> Void) { if pendingCallbacks != nil { pendingCallbacks?.append(callback) @@ -154,11 +127,9 @@ } } - /** @fn canHandleNotification: - @brief Attempts to handle the remote notification. - @param notification The notification in question. - @return Whether or the notification has been handled. - */ + /// Attempts to handle the remote notification. + /// - Parameter notification: The notification in question. + /// - Returns: Whether or the notification has been handled. func canHandle(notification: [AnyHashable: Any]) -> Bool { var stringDictionary: [String: Any]? let data = notification[kNotificationDataKey] diff --git a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift index 6e3dabf164c..e7dc9c8d0d6 100644 --- a/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift +++ b/FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift @@ -16,43 +16,33 @@ import Foundation private let kFiveMinutes = 5 * 60.0 -/** @class FIRAuthAppCredential - @brief A class represents a credential that proves the identity of the app. - */ +/// A class represents a credential that proves the identity of the app. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRSecureTokenService) // objc Needed for decoding old versions class SecureTokenService: NSObject, NSSecureCoding { - /** @property requestConfiguration - @brief The configuration for making requests to server. - */ + /// The configuration for making requests to server. var requestConfiguration: AuthRequestConfiguration? - /** @property accessToken - @brief The cached access token. - @remarks This method is specifically for providing the access token to internal clients during - deserialization and sign-in events, and should not be used to retrieve the access token by - anyone else. - */ + /// The cached access token. + /// + /// This method is specifically for providing the access token to internal clients during + /// deserialization and sign-in events, and should not be used to retrieve the access token by + /// anyone else. var accessToken: String - /** @property refreshToken - @brief The refresh token for the user, or @c nil if the user has yet completed sign-in flow. - @remarks This property needs to be set manually after the instance is decoded from archive. - */ + /// The refresh token for the user, or `nil` if the user has yet completed sign-in flow. + /// + /// This property needs to be set manually after the instance is decoded from archive. var refreshToken: String? - /** @property accessTokenExpirationDate - @brief The expiration date of the cached access token. - */ + /// The expiration date of the cached access token. var accessTokenExpirationDate: Date? - /** @fn initWithRequestConfiguration:accessToken:accessTokenExpirationDate:refreshToken - @brief Creates a @c FIRSecureTokenService with access and refresh tokens. - @param requestConfiguration The configuration for making requests to server. - @param accessToken The STS access token. - @param accessTokenExpirationDate The approximate expiration date of the access token. - @param refreshToken The STS refresh token. - */ + /// Creates a `SecureTokenService` with access and refresh tokens. + /// - Parameter requestConfiguration: The configuration for making requests to server. + /// - Parameter accessToken: The STS access token. + /// - Parameter accessTokenExpirationDate: The approximate expiration date of the access token. + /// - Parameter refreshToken: The STS refresh token. init(withRequestConfiguration requestConfiguration: AuthRequestConfiguration?, accessToken: String, accessTokenExpirationDate: Date?, @@ -64,13 +54,13 @@ class SecureTokenService: NSObject, NSSecureCoding { taskQueue = AuthSerialTaskQueue() } - /** @fn fetchAccessTokenForcingRefresh:callback: - @brief Fetch a fresh ephemeral access token for the ID associated with this instance. The token - received in the callback should be considered short lived and not cached. - @param forceRefresh Forces the token to be refreshed. - @param callback Callback block that will be called to return either the token or an error. - Invoked asyncronously on the auth global work queue in the future. - */ + /// Fetch a fresh ephemeral access token for the ID associated with this instance. The token + /// received in the callback should be considered short lived and not cached. + /// + /// Invoked asyncronously on the auth global work queue in the future. + /// - Parameter forceRefresh: Forces the token to be refreshed. + /// - Parameter callback: Callback block that will be called to return either the token or an + /// error. func fetchAccessToken(forcingRefresh forceRefresh: Bool, callback: @escaping (String?, Error?, Bool) -> Void) { taskQueue.enqueueTask { complete in @@ -139,17 +129,17 @@ class SecureTokenService: NSObject, NSSecureCoding { // MARK: Private methods - /** @fn requestAccessToken: - @brief Makes a request to STS for an access token. - @details This handles both the case that the token has not been granted yet and that it just - needs to be refreshed. The caller is responsible for making sure that this is occurring in - a @c _taskQueue task. - @Returns token and Bool indicating if update occurred. - @remarks Because this method is guaranteed to only be called from tasks enqueued in - @c _taskQueue, we do not need any @synchronized guards around access to _accessToken/etc. - since only one of those tasks is ever running at a time, and those tasks are the only - access to and mutation of these instance variables. - */ + /// Makes a request to STS for an access token. + /// + /// This handles both the case that the token has not been granted yet and that it just + /// needs to be refreshed. The caller is responsible for making sure that this is occurring in + /// a `_taskQueue` task. + /// + /// Because this method is guaranteed to only be called from tasks enqueued in + /// `_taskQueue`, we do not need any @synchronized guards around access to _accessToken/etc. + /// since only one of those tasks is ever running at a time, and those tasks are the only + /// access to and mutation of these instance variables. + /// - Returns: Token and Bool indicating if update occurred. private func requestAccessToken(retryIfExpired: Bool) async throws -> (String?, Bool) { // TODO: This was a crash in ObjC SDK, should it callback with an error? guard let refreshToken, let requestConfiguration else { diff --git a/FirebaseAuth/Sources/Swift/User/AdditionalUserInfo.swift b/FirebaseAuth/Sources/Swift/User/AdditionalUserInfo.swift index a6b9f101a89..455075903eb 100644 --- a/FirebaseAuth/Sources/Swift/User/AdditionalUserInfo.swift +++ b/FirebaseAuth/Sources/Swift/User/AdditionalUserInfo.swift @@ -16,27 +16,21 @@ import Foundation extension AdditionalUserInfo: NSSecureCoding {} @objc(FIRAdditionalUserInfo) open class AdditionalUserInfo: NSObject { - /** @property providerID - @brief The provider identifier. - */ + /// The provider identifier. @objc public let providerID: String - /** @property profile - @brief Dictionary containing the additional IdP specific information. - */ + /// Dictionary containing the additional IdP specific information. @objc public let profile: [String: Any]? - /** @property username - @brief username The name of the user. - */ + /// The name of the user. @objc public let username: String? - /** @property isMewUser - @brief Indicates whether or not the current user was signed in for the first time. - */ + /// Indicates whether or not the current user was signed in for the first time. @objc public let isNewUser: Bool // Maintain newUser for Objective C API. + + /// Indicates whether or not the current user was signed in for the first time. @objc open func newUser() -> Bool { return isNewUser } diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index 6e28a54bd14..cf6ec906231 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -17,86 +17,79 @@ import Foundation @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension User: NSSecureCoding {} -/** @class User - @brief Represents a user. Firebase Auth does not attempt to validate users - when loading them from the keychain. Invalidated users (such as those - whose passwords have been changed on another client) are automatically - logged out when an auth-dependent operation is attempted or when the - ID token is automatically refreshed. - @remarks This class is thread-safe. - */ +/// Represents a user. +/// +/// Firebase Auth does not attempt to validate users +/// when loading them from the keychain. Invalidated users (such as those +/// whose passwords have been changed on another client) are automatically +/// logged out when an auth-dependent operation is attempted or when the +/// ID token is automatically refreshed. +/// +/// This class is thread-safe. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRUser) open class User: NSObject, UserInfo { - /** @property anonymous - @brief Indicates the user represents an anonymous user. - */ + /// Indicates the user represents an anonymous user. @objc public private(set) var isAnonymous: Bool + + /// Indicates the user represents an anonymous user. @objc open func anonymous() -> Bool { return isAnonymous } - /** @property emailVerified - @brief Indicates the email address associated with this user has been verified. - */ + /// Indicates the email address associated with this user has been verified. @objc public private(set) var isEmailVerified: Bool + + /// Indicates the email address associated with this user has been verified. @objc open func emailVerified() -> Bool { return isEmailVerified } - /** @property providerData - @brief Profile data for each identity provider, if any. - @remarks This data is cached on sign-in and updated when linking or unlinking. - */ + /// Profile data for each identity provider, if any. + /// + /// This data is cached on sign-in and updated when linking or unlinking. @objc open var providerData: [UserInfo] { return Array(providerDataRaw.values) } private var providerDataRaw: [String: UserInfoImpl] - /** @property metadata - @brief Metadata associated with the Firebase user in question. - */ + /// Metadata associated with the Firebase user in question. @objc public private(set) var metadata: UserMetadata - /** @property tenantID - @brief The tenant ID of the current user. nil if none is available. - */ + /// The tenant ID of the current user. `nil` if none is available. @objc public private(set) var tenantID: String? #if os(iOS) - /** @property multiFactor - @brief Multi factor object associated with the user. - This property is available on iOS only. - */ + /// Multi factor object associated with the user. + /// + /// This property is available on iOS only. @objc public private(set) var multiFactor: MultiFactor #endif - /** @fn updateEmail:completion: - @brief [Deprecated] Updates the email address for the user. On success, the cached user - profile data is updated. Returns AuthErrorCodeInvalidCredentials error when - [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled. - @remarks May fail if there is already an account with this email address that was created using - email and password authentication. - - @param email The email address for the user. - @param completion Optionally; the block invoked when the user profile change has finished. - Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - + `AuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another - account. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - + `AuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security - sensitive operation that requires a recent login from the user. This error indicates - the user has not signed in recently enough. To resolve, reauthenticate the user by - calling `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// [Deprecated] Updates the email address for the user. + /// + /// On success, the cached user profile data is updated. Returns an error when + /// [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled. + /// + /// May fail if there is already an account with this email address that was created using + /// email and password authentication. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another + /// account. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security + /// sensitive operation that requires a recent login from the user. This error indicates + /// the user has not signed in recently enough. To resolve, reauthenticate the user by + /// calling `reauthenticate(with:)`. + /// - Parameter email: The email address for the user. + /// - Parameter completion: Optionally; the block invoked when the user profile change has + /// finished. @available( *, deprecated, @@ -111,35 +104,32 @@ extension User: NSSecureCoding {} } } - /** @fn updateEmail - @brief [Deprecated] Updates the email address for the user. On success, the cached user - profile data is updated. Returns AuthErrorCodeInvalidCredentials error when - [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) - is enabled. - @remarks May fail if there is already an account with this email address that was created using - email and password authentication. - - @param email The email address for the user. - @throws Error on failure. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - + `AuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another - account. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - + `AuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security - sensitive operation that requires a recent login from the user. This error indicates - the user has not signed in recently enough. To resolve, reauthenticate the user by - calling `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// [Deprecated] Updates the email address for the user. + /// + /// On success, the cached user profile data is updated. Throws when + /// [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) + /// is enabled. + /// + /// May fail if there is already an account with this email address that was created using + /// email and password authentication. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email is already in use by another + /// account. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s email is a security + /// sensitive operation that requires a recent login from the user. This error indicates + /// the user has not signed in recently enough. To resolve, reauthenticate the user by + /// calling `reauthenticate(with:)`. + /// - Parameter email: The email address for the user. @available( *, deprecated, @@ -158,27 +148,23 @@ extension User: NSSecureCoding {} } } - /** @fn updatePassword:completion: - @brief Updates the password for the user. On success, the cached user profile data is updated. - - @param password The new password for the user. - @param completion Optionally; the block invoked when the user profile change has finished. - Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled - sign in with the specified identity provider. - + `AuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security - sensitive operation that requires a recent login from the user. This error indicates - the user has not signed in recently enough. To resolve, reauthenticate the user by - calling `reauthenticate(with:)`. - + `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is - considered too weak. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo` - dictionary object will contain more detailed explanation that can be shown to the user. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Updates the password for the user. On success, the cached user profile data is updated. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled + /// sign in with the specified identity provider. + /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security + /// sensitive operation that requires a recent login from the user. This error indicates + /// the user has not signed in recently enough. To resolve, reauthenticate the user by + /// calling `reauthenticate(with:)`. + /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + /// considered too weak. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo` + /// dictionary object will contain more detailed explanation that can be shown to the user. + /// - Parameter password: The new password for the user. + /// - Parameter completion: Optionally; the block invoked when the user profile change has + /// finished. @objc(updatePassword:completion:) open func updatePassword(to password: String, completion: ((Error?) -> Void)? = nil) { guard password.count > 0 else { @@ -194,26 +180,21 @@ extension User: NSSecureCoding {} } } - /** @fn updatePassword - @brief Updates the password for the user. On success, the cached user profile data is updated. - - @param password The new password for the user. - @throws Error on failure. - - @remarks Possible error codes: - - + `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled - sign in with the specified identity provider. - + `AuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security - sensitive operation that requires a recent login from the user. This error indicates - the user has not signed in recently enough. To resolve, reauthenticate the user by - calling `reauthenticate(with:)`. - + `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is - considered too weak. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo` - dictionary object will contain more detailed explanation that can be shown to the user. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Updates the password for the user. On success, the cached user profile data is updated. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeOperationNotAllowed` - Indicates the administrator disabled + /// sign in with the specified identity provider. + /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s password is a security + /// sensitive operation that requires a recent login from the user. This error indicates + /// the user has not signed in recently enough. To resolve, reauthenticate the user by + /// calling `reauthenticate(with:)`. + /// * `AuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is + /// considered too weak. The `NSLocalizedFailureReasonErrorKey` field in the `userInfo` + /// dictionary object will contain more detailed explanation that can be shown to the user. + /// - Parameter password: The new password for the user. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func updatePassword(to password: String) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -228,26 +209,22 @@ extension User: NSSecureCoding {} } #if os(iOS) - /** @fn updatePhoneNumberCredential:completion: - @brief Updates the phone number for the user. On success, the cached user profile data is - updated. - This method is available on iOS only. - - @param phoneNumberCredential The new phone number credential corresponding to the phone number - to be added to the Firebase account, if a phone number is already linked to the account this - new phone number will replace it. - @param completion Optionally; the block invoked when the user profile change has finished. - Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security - sensitive operation that requires a recent login from the user. This error indicates - the user has not signed in recently enough. To resolve, reauthenticate the user by - calling `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Updates the phone number for the user. On success, the cached user profile data is updated. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// This method is available on iOS only. + /// + /// Possible error codes: + /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security + /// sensitive operation that requires a recent login from the user. This error indicates + /// the user has not signed in recently enough. To resolve, reauthenticate the user by + /// calling `reauthenticate(with:)`. + /// - Parameter credential: The new phone number credential corresponding to the + /// phone number to be added to the Firebase account, if a phone number is already linked to the + /// account this new phone number will replace it. + /// - Parameter completion: Optionally; the block invoked when the user profile change has + /// finished. @objc(updatePhoneNumberCredential:completion:) open func updatePhoneNumber(_ credential: PhoneAuthCredential, completion: ((Error?) -> Void)? = nil) { @@ -259,25 +236,20 @@ extension User: NSSecureCoding {} } } - /** @fn updatePhoneNumberCredential - @brief Updates the phone number for the user. On success, the cached user profile data is - updated. - This method is available on iOS only. - - @param phoneNumberCredential The new phone number credential corresponding to the phone number - to be added to the Firebase account, if a phone number is already linked to the account this - new phone number will replace it. - @throws an error. - - @remarks Possible error codes: - - + `AuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security - sensitive operation that requires a recent login from the user. This error indicates - the user has not signed in recently enough. To resolve, reauthenticate the user by - calling `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Updates the phone number for the user. On success, the cached user profile data is updated. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// This method is available on iOS only. + /// + /// Possible error codes: + /// * `AuthErrorCodeRequiresRecentLogin` - Updating a user’s phone number is a security + /// sensitive operation that requires a recent login from the user. This error indicates + /// the user has not signed in recently enough. To resolve, reauthenticate the user by + /// calling `reauthenticate(with:)`. + /// - Parameter phoneNumberCredential: The new phone number credential corresponding to the + /// phone number to be added to the Firebase account, if a phone number is already linked to the + /// account this new phone number will replace it. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func updatePhoneNumber(_ credential: PhoneAuthCredential) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -292,14 +264,11 @@ extension User: NSSecureCoding {} } #endif - /** @fn profileChangeRequest - @brief Creates an object which may be used to change the user's profile data. - - @remarks Set the properties of the returned object, then call - `UserProfileChangeRequest.commitChanges()` to perform the updates atomically. - - @return An object which may be used to change the user's profile data atomically. - */ + /// Creates an object which may be used to change the user's profile data. + /// + /// Set the properties of the returned object, then call + /// `UserProfileChangeRequest.commitChanges()` to perform the updates atomically. + /// - Returns: An object which may be used to change the user's profile data atomically. @objc(profileChangeRequest) open func createProfileChangeRequest() -> UserProfileChangeRequest { var result: UserProfileChangeRequest! @@ -309,10 +278,9 @@ extension User: NSSecureCoding {} return result } - /** @property refreshToken - @brief A refresh token; useful for obtaining new access tokens independently. - @remarks This property should only be used for advanced scenarios, and is not typically needed. - */ + /// A refresh token; useful for obtaining new access tokens independently. + /// + /// This property should only be used for advanced scenarios, and is not typically needed. @objc open var refreshToken: String? { var result: String? kAuthGlobalWorkQueue.sync { @@ -321,18 +289,13 @@ extension User: NSSecureCoding {} return result } - /** @fn reloadWithCompletion: - @brief Reloads the user's profile data from the server. - - @param completion Optionally; the block invoked when the reload has finished. Invoked - asynchronously on the main thread in the future. - - @remarks May fail with a `AuthErrorCodeRequiresRecentLogin` error code. In this case - you should call `reauthenticate(with:)` before re-invoking - `updateEmail(to:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Reloads the user's profile data from the server. + /// + /// May fail with an `AuthErrorCodeRequiresRecentLogin` error code. In this case + /// you should call `reauthenticate(with:)` before re-invoking + /// `updateEmail(to:)`. + /// - Parameter completion: Optionally; the block invoked when the reload has finished. Invoked + /// asynchronously on the main thread in the future. @objc open func reload(completion: ((Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { self.getAccountInfoRefreshingCache { user, error in @@ -341,18 +304,11 @@ extension User: NSSecureCoding {} } } - /** @fn reload - @brief Reloads the user's profile data from the server. - - @param completion Optionally; the block invoked when the reload has finished. Invoked - asynchronously on the main thread in the future. - - @remarks May fail with a `AuthErrorCodeRequiresRecentLogin` error code. In this case - you should call `reauthenticate(with:)` before re-invoking - `updateEmail(to:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Reloads the user's profile data from the server. + /// + /// May fail with an `AuthErrorCodeRequiresRecentLogin` error code. In this case + /// you should call `reauthenticate(with:)` before re-invoking + /// `updateEmail(to:)`. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func reload() async throws { return try await withCheckedThrowingContinuation { continuation in @@ -366,41 +322,36 @@ extension User: NSSecureCoding {} } } - /** @fn reauthenticateWithCredential:completion: - @brief Renews the user's authentication tokens by validating a fresh set of credentials supplied - by the user and returns additional identity provider data. - - @param credential A user-supplied credential, which will be validated by the server. This can be - a successful third-party identity provider sign-in, or an email address and password. - @param completion Optionally; the block invoked when the re-authentication operation has - finished. Invoked asynchronously on the main thread in the future. - - @remarks If the user associated with the supplied credential is different from the current user, - or if the validation of the supplied credentials fails; an error is returned and the current - user remains signed in. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. - This could happen if it has expired or it is malformed. - + `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the - identity provider represented by the credential are not enabled. Enable them in the - Auth section of the Firebase console. - + `AuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential - (e.g. the email in a Facebook access token) is already in use by an existing account, - that cannot be authenticated with this method. This error will only be thrown if the - "One account per email address" setting is enabled in the Firebase console, under Auth - settings. Please note that the error code raised in this specific situation may not be - the same on Web and Android. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with - an incorrect password, if credential is of the type `EmailPasswordAuthCredential`. - + `AuthErrorCodeUserMismatch` - Indicates that an attempt was made to - reauthenticate with a user which is not the current user. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Renews the user's authentication tokens by validating a fresh set of credentials supplied + /// by the user and returns additional identity provider data. + /// + /// If the user associated with the supplied credential is different from the current user, + /// or if the validation of the supplied credentials fails; an error is returned and the current + /// user remains signed in. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + /// This could happen if it has expired or it is malformed. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the + /// identity provider represented by the credential are not enabled. Enable them in the + /// Auth section of the Firebase console. + /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential + /// (e.g. the email in a Facebook access token) is already in use by an existing account, + /// that cannot be authenticated with this method. This error will only be thrown if the + /// "One account per email address" setting is enabled in the Firebase console, under Auth + /// settings. Please note that the error code raised in this specific situation may not be + /// the same on Web and Android. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with + /// an incorrect password, if credential is of the type `EmailPasswordAuthCredential`. + /// * `AuthErrorCodeUserMismatch` - Indicates that an attempt was made to + /// reauthenticate with a user which is not the current user. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// - Parameter credential: A user-supplied credential, which will be validated by the server. + /// This can be a successful third-party identity provider sign-in, or an email address and + /// password. + /// - Parameter completion: Optionally; the block invoked when the re-authentication operation has + /// finished. Invoked asynchronously on the main thread in the future. @objc(reauthenticateWithCredential:completion:) open func reauthenticate(with credential: AuthCredential, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { @@ -442,40 +393,35 @@ extension User: NSSecureCoding {} } } - /** @fn reauthenticateWithCredential - @brief Renews the user's authentication tokens by validating a fresh set of credentials supplied - by the user and returns additional identity provider data. - - @param credential A user-supplied credential, which will be validated by the server. This can be - a successful third-party identity provider sign-in, or an email address and password. - @returns An AuthDataResult. - - @remarks If the user associated with the supplied credential is different from the current user, - or if the validation of the supplied credentials fails; an error is returned and the current - user remains signed in. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. - This could happen if it has expired or it is malformed. - + `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the - identity provider represented by the credential are not enabled. Enable them in the - Auth section of the Firebase console. - + `AuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential - (e.g. the email in a Facebook access token) is already in use by an existing account, - that cannot be authenticated with this method. This error will only be thrown if the - "One account per email address" setting is enabled in the Firebase console, under Auth - settings. Please note that the error code raised in this specific situation may not be - the same on Web and Android. - + `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `AuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with - an incorrect password, if credential is of the type `EmailPasswordAuthCredential`. - + `AuthErrorCodeUserMismatch` - Indicates that an attempt was made to - reauthenticate with a user which is not the current user. - + `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Renews the user's authentication tokens by validating a fresh set of credentials supplied + /// by the user and returns additional identity provider data. + /// + /// If the user associated with the supplied credential is different from the current user, + /// or if the validation of the supplied credentials fails; an error is returned and the current + /// user remains signed in. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidCredential` - Indicates the supplied credential is invalid. + /// This could happen if it has expired or it is malformed. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the + /// identity provider represented by the credential are not enabled. Enable them in the + /// Auth section of the Firebase console. + /// * `AuthErrorCodeEmailAlreadyInUse` - Indicates the email asserted by the credential + /// (e.g. the email in a Facebook access token) is already in use by an existing account, + /// that cannot be authenticated with this method. This error will only be thrown if the + /// "One account per email address" setting is enabled in the Firebase console, under Auth + /// settings. Please note that the error code raised in this specific situation may not be + /// the same on Web and Android. + /// * `AuthErrorCodeUserDisabled` - Indicates the user's account is disabled. + /// * `AuthErrorCodeWrongPassword` - Indicates the user attempted reauthentication with + /// an incorrect password, if credential is of the type `EmailPasswordAuthCredential`. + /// * `AuthErrorCodeUserMismatch` - Indicates that an attempt was made to + /// reauthenticate with a user which is not the current user. + /// * `AuthErrorCodeInvalidEmail` - Indicates the email address is malformed. + /// - Parameter credential: A user-supplied credential, which will be validated by the server. + /// This can be a successful third-party identity provider sign-in, or an email address and + /// password. + /// - Returns: The `AuthDataResult` after the reauthentication. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func reauthenticate(with credential: AuthCredential) async throws -> AuthDataResult { @@ -491,17 +437,16 @@ extension User: NSSecureCoding {} } #if os(iOS) - /** @fn reauthenticateWithProvider:UIDelegate:completion: - @brief Renews the user's authentication using the provided auth provider instance. - This method is available on iOS only. - - @param provider An instance of an auth provider used to initiate the reauthenticate flow. - @param UIDelegate Optionally an instance of a class conforming to the `AuthUIDelegate` - protocol, used for presenting the web context. If nil, a default `AuthUIDelegate` - will be used. - @param completion Optionally; a block which is invoked when the reauthenticate flow finishes, or - is canceled. Invoked asynchronously on the main thread in the future. - */ + /// Renews the user's authentication using the provided auth provider instance. + /// + /// This method is available on iOS only. + /// - Parameter provider: An instance of an auth provider used to initiate the reauthenticate + /// flow. + /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate` + /// protocol, used for presenting the web context. If nil, a default `AuthUIDelegate` + /// will be used. + /// - Parameter completion: Optionally; a block which is invoked when the reauthenticate flow + /// finishes, or is canceled. Invoked asynchronously on the main thread in the future. @objc(reauthenticateWithProvider:UIDelegate:completion:) open func reauthenticate(with provider: FederatedAuthProvider, uiDelegate: AuthUIDelegate?, @@ -520,16 +465,15 @@ extension User: NSSecureCoding {} } } - /** @fn reauthenticateWithProvider:UIDelegate - @brief Renews the user's authentication using the provided auth provider instance. - This method is available on iOS only. - - @param provider An instance of an auth provider used to initiate the reauthenticate flow. - @param UIDelegate Optionally an instance of a class conforming to the `AuthUIDelegate` - protocol, used for presenting the web context. If nil, a default `AuthUIDelegate` - will be used. - @returns An AuthDataResult. - */ + /// Renews the user's authentication using the provided auth provider instance. + /// + /// This method is available on iOS only. + /// - Parameter provider: An instance of an auth provider used to initiate the reauthenticate + /// flow. + /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate` + /// protocol, used for presenting the web context. If nil, a default `AuthUIDelegate` + /// will be used. + /// - Returns: The `AuthDataResult` after the reauthentication. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func reauthenticate(with provider: FederatedAuthProvider, @@ -546,14 +490,9 @@ extension User: NSSecureCoding {} } #endif - /** @fn getIDTokenWithCompletion: - @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. - - @param completion Optionally; the block invoked when the token is available. Invoked - asynchronously on the main thread in the future. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked + /// asynchronously on the main thread in the future. @objc(getIDTokenWithCompletion:) open func getIDToken(completion: ((String?, Error?) -> Void)?) { // |getIDTokenForcingRefresh:completion:| is also a public API so there is no need to dispatch to @@ -561,19 +500,14 @@ extension User: NSSecureCoding {} getIDTokenForcingRefresh(false, completion: completion) } - /** @fn getIDTokenForcingRefresh:completion: - @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. - - @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason - other than an expiration. - @param completion Optionally; the block invoked when the token is available. Invoked - asynchronously on the main thread in the future. - - @remarks The authentication token will be refreshed (by making a network request) if it has - expired, or if `forceRefresh` is true. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// + /// The authentication token will be refreshed (by making a network request) if it has + /// expired, or if `forceRefresh` is `true`. + /// - Parameter forceRefresh: Forces a token refresh. Useful if the token becomes invalid for some + /// reason other than an expiration. + /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked + /// asynchronously on the main thread in the future. @objc(getIDTokenForcingRefresh:completion:) open func getIDTokenForcingRefresh(_ forceRefresh: Bool, completion: ((String?, Error?) -> Void)?) { @@ -586,18 +520,13 @@ extension User: NSSecureCoding {} } } - /** @fn getIDTokenForcingRefresh:completion: - @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. - - @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason - other than an expiration. - @returns The Token. - - @remarks The authentication token will be refreshed (by making a network request) if it has - expired, or if `forceRefresh` is true. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// + /// The authentication token will be refreshed (by making a network request) if it has + /// expired, or if `forceRefresh` is `true`. + /// - Parameter forceRefresh: Forces a token refresh. Useful if the token becomes invalid for some + /// reason other than an expiration. + /// - Returns: The Firebase authentication token. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func getIDToken() async throws -> String { return try await withCheckedThrowingContinuation { continuation in @@ -611,14 +540,9 @@ extension User: NSSecureCoding {} } } - /** @fn getIDTokenResultWithCompletion: - @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. - - @param completion Optionally; the block invoked when the token is available. Invoked - asynchronously on the main thread in the future. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked + /// asynchronously on the main thread in the future. @objc(getIDTokenResultWithCompletion:) open func getIDTokenResult(completion: ((AuthTokenResult?, Error?) -> Void)?) { getIDTokenResult(forcingRefresh: false) { tokenResult, error in @@ -630,19 +554,15 @@ extension User: NSSecureCoding {} } } - /** @fn getIDTokenResultForcingRefresh:completion: - @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. - - @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason - other than an expiration. - @param completion Optionally; the block invoked when the token is available. Invoked - asynchronously on the main thread in the future. - - @remarks The authentication token will be refreshed (by making a network request) if it has - expired, or if `forceRefresh` is YES. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// + /// The authentication token will be refreshed (by making a network request) if it has + /// expired, or if `forcingRefresh` is `true`. + /// - Parameter forcingRefresh: Forces a token refresh. Useful if the token becomes invalid for + /// some + /// reason other than an expiration. + /// - Parameter completion: Optionally; the block invoked when the token is available. Invoked + /// asynchronously on the main thread in the future. @objc(getIDTokenResultForcingRefresh:completion:) open func getIDTokenResult(forcingRefresh: Bool, completion: ((AuthTokenResult?, Error?) -> Void)?) { @@ -664,18 +584,13 @@ extension User: NSSecureCoding {} } } - /** @fn getIDTokenResultForcingRefresh - @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. - - @param forceRefresh Forces a token refresh. Useful if the token becomes invalid for some reason - other than an expiration. - @returns The token. - - @remarks The authentication token will be refreshed (by making a network request) if it has - expired, or if `forceRefresh` is YES. - - @remarks See `AuthErrors` for a list of error codes that are common to all API methods. - */ + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// + /// The authentication token will be refreshed (by making a network request) if it has + /// expired, or if `forceRefresh` is `true`. + /// - Parameter forceRefresh: Forces a token refresh. Useful if the token becomes invalid for some + /// reason other than an expiration. + /// - Returns: The Firebase authentication token. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func getIDTokenResult(forcingRefresh forceRefresh: Bool = false) async throws -> AuthTokenResult { @@ -690,29 +605,25 @@ extension User: NSSecureCoding {} } } - /** @fn linkWithCredential:completion: - @brief Associates a user account from a third-party identity provider with this user and - returns additional identity provider data. - - @param credential The credential for the identity provider. - @param completion Optionally; the block invoked when the unlinking is complete, or fails. - Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a - type already linked to this account. - + `AuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a - credential that has already been linked with a different Firebase account. - + `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity - provider represented by the credential are not enabled. Enable them in the Auth section - of the Firebase console. - - @remarks This method may also return error codes associated with `updateEmail(to:)` and - `updatePassword(to:)` on `User`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Associates a user account from a third-party identity provider with this user and + /// returns additional identity provider data. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a + /// type already linked to this account. + /// * `AuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a + /// credential that has already been linked with a different Firebase account. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity + /// provider represented by the credential are not enabled. Enable them in the Auth section + /// of the Firebase console. + /// + /// This method may also return error codes associated with `updateEmail(to:)` and + /// `updatePassword(to:)` on `User`. + /// - Parameter credential: The credential for the identity provider. + /// - Parameter completion: Optionally; the block invoked when the unlinking is complete, or + /// fails. @objc(linkWithCredential:completion:) open func link(with credential: AuthCredential, completion: ((AuthDataResult?, Error?) -> Void)? = nil) { @@ -794,28 +705,24 @@ extension User: NSSecureCoding {} } } - /** @fn linkWithCredential: - @brief Associates a user account from a third-party identity provider with this user and - returns additional identity provider data. - - @param credential The credential for the identity provider. - @returns The AuthDataResult. - - @remarks Possible error codes: - - + `AuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a - type already linked to this account. - + `AuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a - credential that has already been linked with a different Firebase account. - + `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity - provider represented by the credential are not enabled. Enable them in the Auth section - of the Firebase console. - - @remarks This method may also return error codes associated with `updateEmail(to:)` and - `updatePassword(to:)` on `User`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Associates a user account from a third-party identity provider with this user and + /// returns additional identity provider data. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeProviderAlreadyLinked` - Indicates an attempt to link a provider of a + /// type already linked to this account. + /// * `AuthErrorCodeCredentialAlreadyInUse` - Indicates an attempt to link with a + /// credential that has already been linked with a different Firebase account. + /// * `AuthErrorCodeOperationNotAllowed` - Indicates that accounts with the identity + /// provider represented by the credential are not enabled. Enable them in the Auth section + /// of the Firebase console. + /// + /// This method may also return error codes associated with `updateEmail(to:)` and + /// `updatePassword(to:)` on `User`. + /// - Parameter credential: The credential for the identity provider. + /// - Returns: An `AuthDataResult`. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func link(with credential: AuthCredential) async throws -> AuthDataResult { @@ -831,17 +738,15 @@ extension User: NSSecureCoding {} } #if os(iOS) - /** @fn linkWithProvider:UIDelegate:completion: - @brief link the user with the provided auth provider instance. - This method is available on iOSonly. - - @param provider An instance of an auth provider used to initiate the link flow. - @param UIDelegate Optionally an instance of a class conforming to the `AuthUIDelegate` - protocol used for presenting the web context. If nil, a default `AuthUIDelegate` - will be used. - @param completion Optionally; a block which is invoked when the link flow finishes, or - is canceled. Invoked asynchronously on the main thread in the future. - */ + /// Link the user with the provided auth provider instance. + /// + /// This method is available on iOSonly. + /// - Parameter provider: An instance of an auth provider used to initiate the link flow. + /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate` + /// protocol used for presenting the web context. If nil, a default `AuthUIDelegate` will be + /// used. + /// - Parameter completion: Optionally; a block which is invoked when the link flow finishes, or + /// is canceled. Invoked asynchronously on the main thread in the future. @objc(linkWithProvider:UIDelegate:completion:) open func link(with provider: FederatedAuthProvider, uiDelegate: AuthUIDelegate?, @@ -860,16 +765,16 @@ extension User: NSSecureCoding {} } } - /** @fn linkWithProvider:UIDelegate: - @brief link the user with the provided auth provider instance. - This method is available on iOS, macOS Catalyst, and tvOS only. - - @param provider An instance of an auth provider used to initiate the link flow. - @param UIDelegate Optionally an instance of a class conforming to the `AuthUIDelegate` - protocol used for presenting the web context. If nil, a default `AuthUIDelegate` - will be used. - @returns An AuthDataResult. - */ + /// Link the user with the provided auth provider instance. + /// + /// This method is available on iOSonly. + /// - Parameter provider: An instance of an auth provider used to initiate the link flow. + /// - Parameter uiDelegate: Optionally an instance of a class conforming to the `AuthUIDelegate` + /// protocol used for presenting the web context. If nil, a default `AuthUIDelegate` + /// will be used. + /// - Parameter completion: Optionally; a block which is invoked when the link flow finishes, or + /// is canceled. Invoked asynchronously on the main thread in the future. + /// - Returns: An AuthDataResult. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @discardableResult open func link(with provider: FederatedAuthProvider, @@ -886,24 +791,20 @@ extension User: NSSecureCoding {} } #endif - /** @fn unlinkFromProvider:completion: - @brief Disassociates a user account from a third-party identity provider with this user. - - @param provider The provider ID of the provider to unlink. - @param completion Optionally; the block invoked when the unlinking is complete, or fails. - Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider - that is not linked to the account. - + `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive - operation that requires a recent login from the user. This error indicates the user - has not signed in recently enough. To resolve, reauthenticate the user by calling - `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Disassociates a user account from a third-party identity provider with this user. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider + /// that is not linked to the account. + /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + /// operation that requires a recent login from the user. This error indicates the user + /// has not signed in recently enough. To resolve, reauthenticate the user by calling + /// `reauthenticate(with:)`. + /// - Parameter provider: The provider ID of the provider to unlink. + /// - Parameter completion: Optionally; the block invoked when the unlinking is complete, or + /// fails. @objc open func unlink(fromProvider provider: String, completion: ((User?, Error?) -> Void)? = nil) { taskQueue.enqueueTask { complete in @@ -972,23 +873,19 @@ extension User: NSSecureCoding {} } } - /** @fn unlinkFromProvider: - @brief Disassociates a user account from a third-party identity provider with this user. - - @param provider The provider ID of the provider to unlink. - @returns The user. - - @remarks Possible error codes: - - + `AuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider - that is not linked to the account. - + `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive - operation that requires a recent login from the user. This error indicates the user - has not signed in recently enough. To resolve, reauthenticate the user by calling - `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Disassociates a user account from a third-party identity provider with this user. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// Possible error codes: + /// * `AuthErrorCodeNoSuchProvider` - Indicates an attempt to unlink a provider + /// that is not linked to the account. + /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + /// operation that requires a recent login from the user. This error indicates the user + /// has not signed in recently enough. To resolve, reauthenticate the user by calling + /// `reauthenticate(with:)`. + /// - Parameter provider: The provider ID of the provider to unlink. + /// - Returns: The user. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func unlink(fromProvider provider: String) async throws -> User { return try await withCheckedThrowingContinuation { continuation in @@ -1002,53 +899,37 @@ extension User: NSSecureCoding {} } } - /** @fn sendEmailVerificationWithCompletion: - @brief Initiates email verification for the user. - - @param completion Optionally; the block invoked when the request to send an email verification - is complete, or fails. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - + `AuthErrorCodeUserNotFound` - Indicates the user account was not found. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Initiates email verification for the user. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// * `AuthErrorCodeUserNotFound` - Indicates the user account was not found. + /// - Parameter completion: Optionally; the block invoked when the request to send an email + /// verification is complete, or fails. Invoked asynchronously on the main thread in the future. @objc(sendEmailVerificationWithCompletion:) open func __sendEmailVerification(withCompletion completion: ((Error?) -> Void)?) { sendEmailVerification(completion: completion) } - /** @fn sendEmailVerificationWithActionCodeSettings:completion: - @brief Initiates email verification for the user. - - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - + `AuthErrorCodeUserNotFound` - Indicates the user account was not found. - + `AuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when - a iOS App Store ID is provided. - + `AuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name - is missing when the `androidInstallApp` flag is set to true. - + `AuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the - continue URL is not allowlisted in the Firebase console. - + `AuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the - continue URL is not valid. - */ + /// Initiates email verification for the user. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// * `AuthErrorCodeUserNotFound` - Indicates the user account was not found. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. + /// - Parameter completion: Optionally; the block invoked when the request to send an email + /// verification is complete, or fails. Invoked asynchronously on the main thread in the future. @objc(sendEmailVerificationWithActionCodeSettings:completion:) open func sendEmailVerification(with actionCodeSettings: ActionCodeSettings? = nil, completion: ((Error?) -> Void)? = nil) { @@ -1082,30 +963,18 @@ extension User: NSSecureCoding {} } } - /** @fn sendEmailVerificationWithActionCodeSettings: - @brief Initiates email verification for the user. - - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - - @remarks Possible error codes: - - + `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was - sent in the request. - + `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in - the console for this action. - + `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for - sending update email. - + `AuthErrorCodeUserNotFound` - Indicates the user account was not found. - + `AuthErrorCodeMissingIosBundleID` - Indicates that the iOS bundle ID is missing when - a iOS App Store ID is provided. - + `AuthErrorCodeMissingAndroidPackageName` - Indicates that the android package name - is missing when the `androidInstallApp` flag is set to true. - + `AuthErrorCodeUnauthorizedDomain` - Indicates that the domain specified in the - continue URL is not allowlisted in the Firebase console. - + `AuthErrorCodeInvalidContinueURI` - Indicates that the domain specified in the - continue URL is not valid. - */ + /// Initiates email verification for the user. + /// + /// Possible error codes: + /// * `AuthErrorCodeInvalidRecipientEmail` - Indicates an invalid recipient email was + /// sent in the request. + /// * `AuthErrorCodeInvalidSender` - Indicates an invalid sender email is set in + /// the console for this action. + /// * `AuthErrorCodeInvalidMessagePayload` - Indicates an invalid email template for + /// sending update email. + /// * `AuthErrorCodeUserNotFound` - Indicates the user account was not found. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. The default value is `nil`. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func sendEmailVerification(with actionCodeSettings: ActionCodeSettings? = nil) async throws { return try await withCheckedThrowingContinuation { continuation in @@ -1119,21 +988,15 @@ extension User: NSSecureCoding {} } } - /** @fn deleteWithCompletion: - @brief Deletes the user account (also signs out the user, if this was the current user). - - @param completion Optionally; the block invoked when the request to delete the account is - complete, or fails. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive - operation that requires a recent login from the user. This error indicates the user - has not signed in recently enough. To resolve, reauthenticate the user by calling - `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Deletes the user account (also signs out the user, if this was the current user). + /// + /// Possible error codes: + /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + /// operation that requires a recent login from the user. This error indicates the user + /// has not signed in recently enough. To resolve, reauthenticate the user by calling + /// `reauthenticate(with:)`. + /// - Parameter completion: Optionally; the block invoked when the request to delete the account + /// is complete, or fails. Invoked asynchronously on the main thread in the future. @objc open func delete(completion: ((Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { self.internalGetToken { accessToken, error in @@ -1165,21 +1028,13 @@ extension User: NSSecureCoding {} } } - /** @fn delete - @brief Deletes the user account (also signs out the user, if this was the current user). - - @param completion Optionally; the block invoked when the request to delete the account is - complete, or fails. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive - operation that requires a recent login from the user. This error indicates the user - has not signed in recently enough. To resolve, reauthenticate the user by calling - `reauthenticate(with:)`. - - @remarks See `AuthErrors` for a list of error codes that are common to all `User` methods. - */ + /// Deletes the user account (also signs out the user, if this was the current user). + /// + /// Possible error codes: + /// * `AuthErrorCodeRequiresRecentLogin` - Updating email is a security sensitive + /// operation that requires a recent login from the user. This error indicates the user + /// has not signed in recently enough. To resolve, reauthenticate the user by calling + /// `reauthenticate(with:)`. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func delete() async throws { return try await withCheckedThrowingContinuation { continuation in @@ -1193,25 +1048,21 @@ extension User: NSSecureCoding {} } } - /** @fn sendEmailVerificationBeforeUpdatingEmail:completion: - @brief Send an email to verify the ownership of the account then update to the new email. - @param email The email to be updated to. - @param completion Optionally; the block invoked when the request to send the verification - email is complete, or fails. - */ + /// Send an email to verify the ownership of the account then update to the new email. + /// - Parameter email: The email to be updated to. + /// - Parameter completion: Optionally; the block invoked when the request to send the + /// verification email is complete, or fails. @objc(sendEmailVerificationBeforeUpdatingEmail:completion:) open func __sendEmailVerificationBeforeUpdating(email: String, completion: ((Error?) -> Void)?) { sendEmailVerification(beforeUpdatingEmail: email, completion: completion) } - /** @fn sendEmailVerificationBeforeUpdatingEmail:completion: - @brief Send an email to verify the ownership of the account then update to the new email. - @param email The email to be updated to. - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - @param completion Optionally; the block invoked when the request to send the verification - email is complete, or fails. - */ + /// Send an email to verify the ownership of the account then update to the new email. + /// - Parameter email: The email to be updated to. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. + /// - Parameter completion: Optionally; the block invoked when the request to send the + /// verification email is complete, or fails. @objc open func sendEmailVerification(beforeUpdatingEmail email: String, actionCodeSettings: ActionCodeSettings? = nil, completion: ((Error?) -> Void)? = nil) { @@ -1245,13 +1096,10 @@ extension User: NSSecureCoding {} } } - /** @fn sendEmailVerificationBeforeUpdatingEmail:completion: - @brief Send an email to verify the ownership of the account then update to the new email. - @param email The email to be updated to. - @param actionCodeSettings An `ActionCodeSettings` object containing settings related to - handling action codes. - @throws on failure. - */ + /// Send an email to verify the ownership of the account then update to the new email. + /// - Parameter email: The email to be updated to. + /// - Parameter actionCodeSettings: An `ActionCodeSettings` object containing settings related to + /// handling action codes. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func sendEmailVerification(beforeUpdatingEmail newEmail: String, actionCodeSettings: ActionCodeSettings? = nil) async throws { @@ -1267,16 +1115,16 @@ extension User: NSSecureCoding {} } } - @objc open func rawAccessToken() -> String { + // MARK: Internal implementations below + + func rawAccessToken() -> String { return tokenService.accessToken } - @objc open func accessTokenExpirationDate() -> Date? { + func accessTokenExpirationDate() -> Date? { return tokenService.accessTokenExpirationDate } - // MARK: Internal implementations below - init(withTokenService tokenService: SecureTokenService) { providerDataRaw = [:] taskQueue = AuthSerialTaskQueue() @@ -1325,57 +1173,39 @@ extension User: NSSecureCoding {} return "Firebase" } - /** @property uid - @brief The provider's user ID for the user. - */ + /// The provider's user ID for the user. @objc open var uid: String? - /** @property displayName - @brief The name of the user. - */ + /// The name of the user. @objc open var displayName: String? - /** @property photoURL - @brief The URL of the user's profile photo. - */ + /// The URL of the user's profile photo. @objc open var photoURL: URL? - /** @property email - @brief The user's email address. - */ + /// The user's email address. @objc open var email: String? - /** @property phoneNumber - @brief A phone number associated with the user. - @remarks This property is only available for users authenticated via phone number auth. - */ + /// A phone number associated with the user. + /// + /// This property is only available for users authenticated via phone number auth. @objc open var phoneNumber: String? - /** @var hasEmailPasswordCredential - @brief Whether or not the user can be authenticated by using Firebase email and password. - */ + /// Whether or not the user can be authenticated by using Firebase email and password. private var hasEmailPasswordCredential: Bool - /** @var _taskQueue - @brief Used to serialize the update profile calls. - */ + /// Used to serialize the update profile calls. private var taskQueue: AuthSerialTaskQueue - /** @property requestConfiguration - @brief A strong reference to a requestConfiguration instance associated with this user instance. - */ + /// A strong reference to a requestConfiguration instance associated with this user instance. var requestConfiguration: AuthRequestConfiguration - /** @var _tokenService - @brief A secure token service associated with this user. For performing token exchanges and - refreshing access tokens. - */ + /// A secure token service associated with this user. For performing token exchanges and + /// refreshing access tokens. var tokenService: SecureTokenService private weak var _auth: Auth? - /** @property auth - @brief A weak reference to a FIRAuth instance associated with this instance. - */ + + /// A weak reference to an `Auth` instance associated with this instance. weak var auth: Auth? { set { _auth = newValue @@ -1467,13 +1297,11 @@ extension User: NSSecureCoding {} } } - /** @fn executeUserUpdateWithChanges:callback: - @brief Performs a setAccountInfo request by mutating the results of a getAccountInfo response, - atomically in regards to other calls to this method. - @param changeBlock A block responsible for mutating a template @c FIRSetAccountInfoRequest - @param callback A block to invoke when the change is complete. Invoked asynchronously on the - auth global work queue in the future. - */ + /// Performs a setAccountInfo request by mutating the results of a getAccountInfo response, + /// atomically in regards to other calls to this method. + /// - Parameter changeBlock: A block responsible for mutating a template `SetAccountInfoRequest` + /// - Parameter callback: A block to invoke when the change is complete. Invoked asynchronously on + /// the auth global work queue in the future. func executeUserUpdateWithChanges(changeBlock: @escaping (GetAccountInfoResponseUser, SetAccountInfoRequest) -> Void, callback: @escaping (Error?) -> Void) { @@ -1529,13 +1357,12 @@ extension User: NSSecureCoding {} } } - /** @fn setTokenService:callback: - @brief Sets a new token service for the @c FIRUser instance. - @param tokenService The new token service object. - @param callback The block to be called in the global auth working queue once finished. - @remarks The method makes sure the token service has access and refresh token and the new tokens - are saved in the keychain before calling back. - */ + /// Sets a new token service for the `User` instance. + /// + /// The method makes sure the token service has access and refresh token and the new tokens + /// are saved in the keychain before calling back. + /// - Parameter tokenService: The new token service object. + /// - Parameter callback: The block to be called in the global auth working queue once finished. private func setTokenService(tokenService: SecureTokenService, callback: @escaping (Error?) -> Void) { tokenService.fetchAccessToken(forcingRefresh: false) { token, error, tokenUpdated in @@ -1552,11 +1379,9 @@ extension User: NSSecureCoding {} } } - /** @fn getAccountInfoRefreshingCache: - @brief Gets the users's account data from the server, updating our local values. - @param callback Invoked when the request to getAccountInfo has completed, or when an error has - been detected. Invoked asynchronously on the auth global work queue in the future. - */ + /// Gets the users' account data from the server, updating our local values. + /// - Parameter callback: Invoked when the request to getAccountInfo has completed, or when an + /// error has been detected. Invoked asynchronously on the auth global work queue in the future. private func getAccountInfoRefreshingCache(callback: @escaping (GetAccountInfoResponseUser?, Error?) -> Void) { internalGetToken { token, error in @@ -1624,17 +1449,16 @@ extension User: NSSecureCoding {} } #if os(iOS) - /** @fn internalUpdateOrLinkPhoneNumber - @brief Updates the phone number for the user. On success, the cached user profile data is - updated. - - @param phoneAuthCredential The new phone number credential corresponding to the phone number - to be added to the Firebase account, if a phone number is already linked to the account this - new phone number will replace it. - @param isLinkOperation Boolean value indicating whether or not this is a link operation. - @param completion Optionally; the block invoked when the user profile change has finished. - Invoked asynchronously on the global work queue in the future. - */ + /// Updates the phone number for the user. On success, the cached user profile data is updated. + /// + /// Invoked asynchronously on the global work queue in the future. + /// - Parameter credential: The new phone number credential corresponding to the phone + /// number to be added to the Firebase account. If a phone number is already linked to the + /// account, this new phone number will replace it. + /// - Parameter isLinkOperation: Boolean value indicating whether or not this is a link + /// operation. + /// - Parameter completion: Optionally; the block invoked when the user profile change has + /// finished. private func internalUpdateOrLinkPhoneNumber(credential: PhoneAuthCredential, isLinkOperation: Bool, completion: @escaping (Error?) -> Void) { @@ -1956,10 +1780,8 @@ extension User: NSSecureCoding {} } } - /** @fn signOutIfTokenIsInvalidWithError: - @brief Signs out this user if the user or the token is invalid. - @param error The error from the server. - */ + /// Signs out this user if the user or the token is invalid. + /// - Parameter error: The error from the server. private func signOutIfTokenIsInvalid(withError error: Error) { guard let uid else { return @@ -1975,11 +1797,9 @@ extension User: NSSecureCoding {} } } - /** @fn internalGetToken - @brief Retrieves the Firebase authentication token, possibly refreshing it if it has expired. - @param callback The block to invoke when the token is available. Invoked asynchronously on the - global work thread in the future. - */ + /// Retrieves the Firebase authentication token, possibly refreshing it if it has expired. + /// - Parameter callback: The block to invoke when the token is available. Invoked asynchronously + /// on the global work thread in the future. func internalGetToken(forceRefresh: Bool = false, callback: @escaping (String?, Error?) -> Void) { tokenService.fetchAccessToken(forcingRefresh: forceRefresh) { token, error, tokenUpdated in @@ -2010,20 +1830,16 @@ extension User: NSSecureCoding {} } } - /** @fn updateKeychain: - @brief Updates the keychain for user token or info changes. - @param error The error if NO is returned. - @return Whether the operation is successful. - */ + /// Updates the keychain for user token or info changes. + /// - Returns: An `Error` on failure. func updateKeychain() -> Error? { return auth?.updateKeychain(withUser: self) } - /** @fn callInMainThreadWithError - @brief Calls a callback in main thread with error. - @param callback The callback to be called in main thread. - @param error The error to pass to callback. - */ + /// Calls a callback in main thread with error. + /// - Parameter callback: The callback to be called in main thread. + /// - Parameter error: The error to pass to callback. + class func callInMainThreadWithError(callback: ((Error?) -> Void)?, error: Error?) { if let callback { DispatchQueue.main.async { @@ -2032,12 +1848,10 @@ extension User: NSSecureCoding {} } } - /** @fn callInMainThreadWithUserAndError - @brief Calls a callback in main thread with user and error. - @param callback The callback to be called in main thread. - @param user The user to pass to callback if there is no error. - @param error The error to pass to callback. - */ + /// Calls a callback in main thread with user and error. + /// - Parameter callback: The callback to be called in main thread. + /// - Parameter user: The user to pass to callback if there is no error. + /// - Parameter error: The error to pass to callback. private class func callInMainThreadWithUserAndError(callback: ((User?, Error?) -> Void)?, user: User, error: Error?) { @@ -2048,12 +1862,8 @@ extension User: NSSecureCoding {} } } - /** @fn callInMainThreadWithAuthDataResultAndError - @brief Calls a callback in main thread with user and error. - @param callback The callback to be called in main thread. - @param result The result to pass to callback if there is no error. - @param error The error to pass to callback. - */ + /// Calls a callback in main thread with user and error. + /// - Parameter callback: The callback to be called in main thread. private class func callInMainThreadWithAuthDataResultAndError(callback: ( (AuthDataResult?, Error?) -> Void )?, diff --git a/FirebaseAuth/Sources/Swift/User/UserInfo.swift b/FirebaseAuth/Sources/Swift/User/UserInfo.swift index 65e244ba6c7..9ce14dcb26a 100644 --- a/FirebaseAuth/Sources/Swift/User/UserInfo.swift +++ b/FirebaseAuth/Sources/Swift/User/UserInfo.swift @@ -14,38 +14,25 @@ import Foundation -/** - @brief Represents user data returned from an identity provider. - */ +/// Represents user data returned from an identity provider. @objc(FIRUserInfo) public protocol UserInfo: NSObjectProtocol { - /** @property providerID - @brief The provider identifier. - */ + /// The provider identifier. var providerID: String { get } - /** @property uid - @brief The provider's user ID for the user. - */ + /// The provider's user ID for the user. var uid: String? { get } - /** @property displayName - @brief The name of the user. - */ + /// The name of the user. var displayName: String? { get } - /** @property photoURL - @brief The URL of the user's profile photo. - */ + /// The URL of the user's profile photo. var photoURL: URL? { get } - /** @property email - @brief The user's email address. - */ + /// The user's email address. var email: String? { get } - /** @property phoneNumber - @brief A phone number associated with the user. - @remarks This property is only available for users authenticated via phone number auth. - */ + /// A phone number associated with the user. + /// + /// This property is only available for users authenticated via phone number auth. var phoneNumber: String? { get } } diff --git a/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift b/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift index ad3e4acca89..5e68a2d2cb7 100644 --- a/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift +++ b/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift @@ -17,12 +17,10 @@ import Foundation extension UserInfoImpl: NSSecureCoding {} @objc(FIRUserInfoImpl) class UserInfoImpl: NSObject, UserInfo { - /** @fn userInfoWithGetAccountInfoResponseProviderUserInfo: - @brief A convenience factory method for constructing a @c FIRUserInfo instance from data - returned by the getAccountInfo endpoint. - @param providerUserInfo Data returned by the getAccountInfo endpoint. - @return A new instance of @c FIRUserInfo using data from the getAccountInfo endpoint. - */ + /// A convenience factory method for constructing a `UserInfo` instance from data + /// returned by the getAccountInfo endpoint. + /// - Parameter providerUserInfo: Data returned by the getAccountInfo endpoint. + /// - Returns: A new instance of `UserInfo` using data from the getAccountInfo endpoint. class func userInfo(withGetAccountInfoResponseProviderUserInfo providerUserInfo: GetAccountInfoResponseProviderUserInfo) -> UserInfoImpl { guard let providerID = providerUserInfo.providerID else { @@ -37,15 +35,13 @@ extension UserInfoImpl: NSSecureCoding {} phoneNumber: providerUserInfo.phoneNumber) } - /** @fn initWithProviderID:userID:displayName:photoURL:email: - @brief Designated initializer. - @param providerID The provider identifier. - @param userID The unique user ID for the user (the value of the @c uid field in the token.) - @param displayName The name of the user. - @param photoURL The URL of the user's profile photo. - @param email The user's email address. - @param phoneNumber The user's phone number. - */ + /// Designated initializer. + /// - Parameter providerID: The provider identifier. + /// - Parameter userID: The unique user ID for the user (the value of the uid field in the token.) + /// - Parameter displayName: The name of the user. + /// - Parameter photoURL: The URL of the user's profile photo. + /// - Parameter email: The user's email address. + /// - Parameter phoneNumber: The user's phone number. private init(withProviderID providerID: String, userID: String?, displayName: String?, diff --git a/FirebaseAuth/Sources/Swift/User/UserMetadata.swift b/FirebaseAuth/Sources/Swift/User/UserMetadata.swift index bff7875625c..b126350c449 100644 --- a/FirebaseAuth/Sources/Swift/User/UserMetadata.swift +++ b/FirebaseAuth/Sources/Swift/User/UserMetadata.swift @@ -14,23 +14,16 @@ import Foundation -/** @class UserMetadata - @brief A data class representing the metadata corresponding to a Firebase user. - */ - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension UserMetadata: NSSecureCoding {} +/// A data class representing the metadata corresponding to a Firebase user. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRUserMetadata) open class UserMetadata: NSObject { - /** @property lastSignInDate - @brief Stores the last sign in date for the corresponding Firebase user. - */ + /// Stores the last sign in date for the corresponding Firebase user. @objc public let lastSignInDate: Date? - /** @property creationDate - @brief Stores the creation date for the corresponding Firebase user. - */ + /// Stores the creation date for the corresponding Firebase user. @objc public let creationDate: Date? init(withCreationDate creationDate: Date?, lastSignInDate: Date?) { diff --git a/FirebaseAuth/Sources/Swift/User/UserProfileChangeRequest.swift b/FirebaseAuth/Sources/Swift/User/UserProfileChangeRequest.swift index d73da417a1c..493f3d80f92 100644 --- a/FirebaseAuth/Sources/Swift/User/UserProfileChangeRequest.swift +++ b/FirebaseAuth/Sources/Swift/User/UserProfileChangeRequest.swift @@ -14,16 +14,13 @@ import Foundation -/** @class UserProfileChangeRequest - @brief Represents an object capable of updating a user's profile data. - @remarks Properties are marked as being part of a profile update when they are set. Setting a - property value to nil is not the same as leaving the property unassigned. - */ +/// Represents an object capable of updating a user's profile data. +/// +/// Properties are marked as being part of a profile update when they are set. Setting a +/// property value to nil is not the same as leaving the property unassigned. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRUserProfileChangeRequest) open class UserProfileChangeRequest: NSObject { - /** @property displayName - @brief The name of the user. - */ + /// The name of the user. @objc open var displayName: String? { get { return _displayName } set(newDisplayName) { @@ -39,9 +36,7 @@ import Foundation private var _displayName: String? - /** @property photoURL - @brief The URL of the user's profile photo. - */ + /// The URL of the user's profile photo. @objc open var photoURL: URL? { get { return _photoURL } set(newPhotoURL) { @@ -57,14 +52,13 @@ import Foundation private var _photoURL: URL? - /** @fn commitChangesWithCompletion: - @brief Commits any pending changes. - @remarks This method should only be called once. Once called, property values should not be - changed. - - @param completion Optionally; the block invoked when the user profile change has been applied. - Invoked asynchronously on the main thread in the future. - */ + /// Commits any pending changes. + /// + /// Invoked asynchronously on the main thread in the future. + /// + /// This method should only be called once.Once called, property values should not be changed. + /// - Parameter completion: Optionally; the block invoked when the user profile change has been + /// applied. @objc open func commitChanges(completion: ((Error?) -> Void)? = nil) { kAuthGlobalWorkQueue.async { if self.consumed { @@ -107,13 +101,9 @@ import Foundation } } - /** @fn commitChanges - @brief Commits any pending changes. - @remarks This method should only be called once. Once called, property values should not be - changed. - - @throws on error. - */ + /// Commits any pending changes. + /// + /// This method should only be called once. Once called, property values should not be changed. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) open func commitChanges() async throws { return try await withCheckedThrowingContinuation { continuation in diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift index 55d8a880801..6e7a5b30a57 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthDefaultUIDelegate.swift @@ -22,16 +22,13 @@ @_implementationOnly import GoogleUtilities_Environment #endif - /** @class AuthDefaultUIDelegate - @brief Class responsible for providing a default FIRAuthUIDelegate. - @remarks This class should be used in the case that a UIDelegate was expected and necessary to - continue a given flow, but none was provided. - */ + /// Class responsible for providing a default AuthUIDelegate. + /// + /// This class should be used in the case that a UIDelegate was expected and necessary to + /// continue a given flow, but none was provided. class AuthDefaultUIDelegate: NSObject, AuthUIDelegate { - /** @fn defaultUIDelegate - @brief Returns a default FIRAuthUIDelegate object. - @return The default FIRAuthUIDelegate object. - */ + /// Returns a default AuthUIDelegate object. + /// - Returns: The default AuthUIDelegate object. class func defaultUIDelegate() -> AuthUIDelegate? { if GULAppEnvironmentUtil.isAppExtension() { // iOS App extensions should not call [UIApplication sharedApplication], even if diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift index 0496997fc33..353e21aa67a 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift @@ -16,21 +16,15 @@ import Foundation // MARK: - URL response error codes -/** @var kURLResponseErrorCodeInvalidClientID - @brief Error code that indicates that the client ID provided was invalid. - */ +/// Error code that indicates that the client ID provided was invalid. private let kURLResponseErrorCodeInvalidClientID = "auth/invalid-oauth-client-id" -/** @var kURLResponseErrorCodeNetworkRequestFailed - @brief Error code that indicates that a network request within the SFSafariViewController or - WKWebView failed. - */ +/// Error code that indicates that a network request within the SFSafariViewController or WKWebView +/// failed. private let kURLResponseErrorCodeNetworkRequestFailed = "auth/network-request-failed" -/** @var kURLResponseErrorCodeInternalError - @brief Error code that indicates that an internal error occurred within the - SFSafariViewController or WKWebView failed. - */ +/// Error code that indicates that an internal error occurred within the +/// SFSafariViewController or WKWebView failed. private let kURLResponseErrorCodeInternalError = "auth/internal-error" private let kFIRAuthErrorMessageMalformedJWT = @@ -42,10 +36,8 @@ class AuthErrorUtils: NSObject { static let userInfoDeserializedResponseKey = "FIRAuthErrorUserInfoDeserializedResponseKey" static let userInfoDataKey = "FIRAuthErrorUserInfoDataKey" - /** @var kServerErrorDetailMarker - @brief This marker indicates that the server error message contains a detail error message which - should be used instead of the hardcoded client error message. - */ + /// This marker indicates that the server error message contains a detail error message which + /// should be used instead of the hardcoded client error message. private static let kServerErrorDetailMarker = " : " static func error(code: SharedErrorCode, userInfo: [String: Any]? = nil) -> Error { diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift index ac12c7e459e..d0ab6604c2a 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift @@ -14,575 +14,466 @@ import Foundation -/* - @remarks Error Codes common to all API Methods: - - + `FIRAuthErrorCodeNetworkError` - + `FIRAuthErrorCodeUserNotFound` - + `FIRAuthErrorCodeUserTokenExpired` - + `FIRAuthErrorCodeTooManyRequests` - + `FIRAuthErrorCodeInvalidAPIKey` - + `FIRAuthErrorCodeAppNotAuthorized` - + `FIRAuthErrorCodeKeychainError` - + `FIRAuthErrorCodeInternalError` - - @remarks Common error codes for `FIRUser` operations: - - + `FIRAuthErrorCodeInvalidUserToken` - + `FIRAuthErrorCodeUserDisabled` - */ +/// Error Codes common to all API Methods: @objc(FIRAuthErrors) open class AuthErrors: NSObject { + /// The Firebase Auth error domain. @objc public static let domain: String = "FIRAuthErrorDomain" + /// The name of the key for the error short string of an error code. @objc public static let userInfoNameKey: String = "FIRAuthErrorUserInfoNameKey" - /** - @brief Errors with one of the following three codes: - - `FIRAuthErrorCodeAccountExistsWithDifferentCredential` - - `FIRAuthErrorCodeCredentialAlreadyInUse` - - `FIRAuthErrorCodeEmailAlreadyInUse` - may contain an `NSError.userInfo` dictinary object which contains this key. The value - associated with this key is an NSString of the email address of the account that already - exists. - */ + /// Error codes for Email operations + /// + /// Errors with one of the following three codes: + /// * `accountExistsWithDifferentCredential` + /// * `credentialAlreadyInUse` + /// * emailAlreadyInUse` + /// + /// may contain an `NSError.userInfo` dictionary object which contains this key. The value + /// associated with this key is an NSString of the email address of the account that already + /// exists. @objc public static let userInfoEmailKey: String = "FIRAuthErrorUserInfoEmailKey" - /** - @brief The key used to read the updated Auth credential from the userInfo dictionary of the - NSError object returned. This is the updated auth credential the developer should use for - recovery if applicable. - */ + /// The key used to read the updated Auth credential from the userInfo dictionary of the + /// NSError object returned. This is the updated auth credential the developer should use for + /// recovery if applicable. @objc public static let userInfoUpdatedCredentialKey: String = "FIRAuthErrorUserInfoUpdatedCredentialKey" - /** - @brief The key used to read the MFA resolver from the userInfo dictionary of the NSError object - returned when 2FA is required for sign-incompletion. - */ + /// The key used to read the MFA resolver from the userInfo dictionary of the NSError object + /// returned when 2FA is required for sign-incompletion. @objc(FIRAuthErrorUserInfoMultiFactorResolverKey) public static let userInfoMultiFactorResolverKey: String = "FIRAuthErrorUserInfoMultiFactorResolverKey" } +/// Error codes used by Firebase Auth. @objc(FIRAuthErrorCode) public enum AuthErrorCode: Int { - /** Indicates a validation error with the custom token. - */ + /// Indicates a validation error with the custom token. case invalidCustomToken = 17000 - /** Indicates the service account and the API key belong to different projects. - */ + + /// Indicates the service account and the API key belong to different projects. case customTokenMismatch = 17002 - /** Indicates the IDP token or requestUri is invalid. - */ + /// Indicates the IDP token or requestUri is invalid. case invalidCredential = 17004 - /** Indicates the user's account is disabled on the server. - */ + /// Indicates the user's account is disabled on the server. case userDisabled = 17005 - /** Indicates the administrator disabled sign in with the specified identity provider. - */ + /// Indicates the administrator disabled sign in with the specified identity provider. case operationNotAllowed = 17006 - /** Indicates the email used to attempt a sign up is already in use. - */ + /// Indicates the email used to attempt a sign up is already in use. case emailAlreadyInUse = 17007 - /** Indicates the email is invalid. - */ + /// Indicates the email is invalid. case invalidEmail = 17008 - /** Indicates the user attempted sign in with a wrong password. - */ + /// Indicates the user attempted sign in with a wrong password. case wrongPassword = 17009 - /** Indicates that too many requests were made to a server method. - */ + /// Indicates that too many requests were made to a server method. case tooManyRequests = 17010 - /** Indicates the user account was not found. - */ + /// Indicates the user account was not found. case userNotFound = 17011 - /** Indicates account linking is required. - */ + /// Indicates account linking is required. case accountExistsWithDifferentCredential = 17012 - /** Indicates the user has attemped to change email or password more than 5 minutes after - signing in. - */ + /// Indicates the user has attemped to change email or password more than 5 minutes after + /// signing in. case requiresRecentLogin = 17014 - /** Indicates an attempt to link a provider to which the account is already linked. - */ + /// Indicates an attempt to link a provider to which the account is already linked. case providerAlreadyLinked = 17015 - /** Indicates an attempt to unlink a provider that is not linked. - */ + /// Indicates an attempt to unlink a provider that is not linked. case noSuchProvider = 17016 - /** Indicates user's saved auth credential is invalid the user needs to sign in again. - */ + /// Indicates user's saved auth credential is invalid the user needs to sign in again. case invalidUserToken = 17017 - /** Indicates a network error occurred (such as a timeout interrupted connection or - unreachable host). These types of errors are often recoverable with a retry. The - `NSUnderlyingError` field in the `NSError.userInfo` dictionary will contain the error - encountered. - */ + /// Indicates a network error occurred (such as a timeout interrupted connection or + /// unreachable host). These types of errors are often recoverable with a retry. The + /// `NSUnderlyingError` field in the `NSError.userInfo` dictionary will contain the error + /// encountered. case networkError = 17020 - /** Indicates the saved token has expired for example the user may have changed account - password on another device. The user needs to sign in again on the device that made this - request. - */ + /// Indicates the saved token has expired for example the user may have changed account + /// password on another device. The user needs to sign in again on the device that made this + /// request. case userTokenExpired = 17021 - /** Indicates an invalid API key was supplied in the request. - */ + /// Indicates an invalid API key was supplied in the request. case invalidAPIKey = 17023 - /** Indicates that an attempt was made to reauthenticate with a user which is not the current - user. - */ + /// Indicates that an attempt was made to reauthenticate with a user which is not the current + /// user. case userMismatch = 17024 - /** Indicates an attempt to link with a credential that has already been linked with a - different Firebase account - */ + /// Indicates an attempt to link with a credential that has already been linked with a + /// different Firebase account. case credentialAlreadyInUse = 17025 - /** Indicates an attempt to set a password that is considered too weak. - */ + /// Indicates an attempt to set a password that is considered too weak. case weakPassword = 17026 - /** Indicates the App is not authorized to use Firebase Authentication with the - provided API Key. - */ + /// Indicates the App is not authorized to use Firebase Authentication with the + /// provided API Key. case appNotAuthorized = 17028 - /** Indicates the OOB code is expired. - */ + /// Indicates the OOB code is expired. case expiredActionCode = 17029 - /** Indicates the OOB code is invalid. - */ + /// Indicates the OOB code is invalid. case invalidActionCode = 17030 - /** Indicates that there are invalid parameters in the payload during a "send password reset - * email" attempt. - */ + /// Indicates that there are invalid parameters in the payload during a + /// "send password reset email" attempt. case invalidMessagePayload = 17031 - /** Indicates that the sender email is invalid during a "send password reset email" attempt. - */ + /// Indicates that the sender email is invalid during a "send password reset email" attempt. case invalidSender = 17032 - /** Indicates that the recipient email is invalid. - */ + /// Indicates that the recipient email is invalid. case invalidRecipientEmail = 17033 - /** Indicates that an email address was expected but one was not provided. - */ + /// Indicates that an email address was expected but one was not provided. case missingEmail = 17034 // The enum values 17035 is reserved and should NOT be used for new error codes. - /** Indicates that the iOS bundle ID is missing when a iOS App Store ID is provided. - */ + /// Indicates that the iOS bundle ID is missing when a iOS App Store ID is provided. case missingIosBundleID = 17036 - /** Indicates that the android package name is missing when the `androidInstallApp` flag is set - to true. - */ + /// Indicates that the android package name is missing when the `androidInstallApp` flag is set + /// to `true`. case missingAndroidPackageName = 17037 - /** Indicates that the domain specified in the continue URL is not allowlisted in the Firebase - console. - */ + /// Indicates that the domain specified in the continue URL is not allowlisted in the Firebase + /// console. case unauthorizedDomain = 17038 - /** Indicates that the domain specified in the continue URI is not valid. - */ + /// Indicates that the domain specified in the continue URI is not valid. case invalidContinueURI = 17039 - /** Indicates that a continue URI was not provided in a request to the backend which requires - one. - */ + /// Indicates that a continue URI was not provided in a request to the backend which requires one. case missingContinueURI = 17040 - /** Indicates that a phone number was not provided in a call to - `verifyPhoneNumber:completion:`. - */ + /// Indicates that a phone number was not provided in a call to + /// `verifyPhoneNumber:completion:`. case missingPhoneNumber = 17041 - /** Indicates that an invalid phone number was provided in a call to - `verifyPhoneNumber:completion:`. - */ + /// Indicates that an invalid phone number was provided in a call to + /// `verifyPhoneNumber:completion:`. case invalidPhoneNumber = 17042 - /** Indicates that the phone auth credential was created with an empty verification code. - */ + /// Indicates that the phone auth credential was created with an empty verification code. case missingVerificationCode = 17043 - /** Indicates that an invalid verification code was used in the verifyPhoneNumber request. - */ + /// Indicates that an invalid verification code was used in the verifyPhoneNumber request. case invalidVerificationCode = 17044 - /** Indicates that the phone auth credential was created with an empty verification ID. - */ + /// Indicates that the phone auth credential was created with an empty verification ID. case missingVerificationID = 17045 - /** Indicates that an invalid verification ID was used in the verifyPhoneNumber request. - */ + /// Indicates that an invalid verification ID was used in the verifyPhoneNumber request. case invalidVerificationID = 17046 - /** Indicates that the APNS device token is missing in the verifyClient request. - */ + /// Indicates that the APNS device token is missing in the verifyClient request. case missingAppCredential = 17047 - /** Indicates that an invalid APNS device token was used in the verifyClient request. - */ + /// Indicates that an invalid APNS device token was used in the verifyClient request. case invalidAppCredential = 17048 // The enum values between 17048 and 17051 are reserved and should NOT be used for new error // codes. - /** Indicates that the SMS code has expired. - */ + /// Indicates that the SMS code has expired. case sessionExpired = 17051 - /** Indicates that the quota of SMS messages for a given project has been exceeded. - */ + /// Indicates that the quota of SMS messages for a given project has been exceeded. case quotaExceeded = 17052 - /** Indicates that the APNs device token could not be obtained. The app may not have set up - remote notification correctly or may fail to forward the APNs device token to FIRAuth - if app delegate swizzling is disabled. - */ + /// Indicates that the APNs device token could not be obtained. The app may not have set up + /// remote notification correctly or may fail to forward the APNs device token to Auth + /// if app delegate swizzling is disabled. case missingAppToken = 17053 - /** Indicates that the app fails to forward remote notification to FIRAuth. - */ + /// Indicates that the app fails to forward remote notification to FIRAuth. case notificationNotForwarded = 17054 - /** Indicates that the app could not be verified by Firebase during phone number authentication. - */ + /// Indicates that the app could not be verified by Firebase during phone number authentication. case appNotVerified = 17055 - /** Indicates that the reCAPTCHA token is not valid. - */ + /// Indicates that the reCAPTCHA token is not valid. case captchaCheckFailed = 17056 - /** Indicates that an attempt was made to present a new web context while one was already being - presented. - */ + /// Indicates that an attempt was made to present a new web context while one was already being + /// presented. case webContextAlreadyPresented = 17057 - /** Indicates that the URL presentation was cancelled prematurely by the user. - */ + /// Indicates that the URL presentation was cancelled prematurely by the user. case webContextCancelled = 17058 - /** Indicates a general failure during the app verification flow. - */ + /// Indicates a general failure during the app verification flow. case appVerificationUserInteractionFailure = 17059 - /** Indicates that the clientID used to invoke a web flow is invalid. - */ + /// Indicates that the clientID used to invoke a web flow is invalid. case invalidClientID = 17060 - /** Indicates that a network request within a SFSafariViewController or WKWebView failed. - */ + /// Indicates that a network request within a SFSafariViewController or WKWebView failed. case webNetworkRequestFailed = 17061 - /** Indicates that an internal error occurred within a SFSafariViewController or WKWebView. - */ + /// Indicates that an internal error occurred within a SFSafariViewController or WKWebView. case webInternalError = 17062 - /** Indicates a general failure during a web sign-in flow. - */ + /// Indicates a general failure during a web sign-in flow. case webSignInUserInteractionFailure = 17063 - /** Indicates that the local player was not authenticated prior to attempting Game Center - signin. - */ + /// Indicates that the local player was not authenticated prior to attempting Game Center signin. case localPlayerNotAuthenticated = 17066 - /** Indicates that a non-null user was expected as an argmument to the operation but a null - user was provided. - */ + /// Indicates that a non-null user was expected as an argmument to the operation but a null + /// user was provided. case nullUser = 17067 - /** Indicates that a Firebase Dynamic Link is not activated. - */ + /// Indicates that a Firebase Dynamic Link is not activated. case dynamicLinkNotActivated = 17068 - /** - * Represents the error code for when the given provider id for a web operation is invalid. - */ + /// Represents the error code for when the given provider id for a web operation is invalid. case invalidProviderID = 17071 - /** - * Represents the error code for when an attempt is made to update the current user with a - * tenantId that differs from the current FirebaseAuth instance's tenantId. - */ + /// Represents the error code for when an attempt is made to update the current user with a + /// tenantId that differs from the current FirebaseAuth instance's tenantId. case tenantIDMismatch = 17072 - /** - * Represents the error code for when a request is made to the backend with an associated tenant - * ID for an operation that does not support multi-tenancy. - */ + /// Represents the error code for when a request is made to the backend with an associated tenant + /// ID for an operation that does not support multi-tenancy. case unsupportedTenantOperation = 17073 - /** Indicates that the Firebase Dynamic Link domain used is either not configured or is - unauthorized for the current project. - */ + /// Indicates that the Firebase Dynamic Link domain used is either not configured or is + /// unauthorized for the current project. case invalidDynamicLinkDomain = 17074 - /** Indicates that the credential is rejected because it's misformed or mismatching. - */ + /// Indicates that the credential is rejected because it's misformed or mismatching. case rejectedCredential = 17075 - /** Indicates that the GameKit framework is not linked prior to attempting Game Center signin. - */ + /// Indicates that the GameKit framework is not linked prior to attempting Game Center signin. case gameKitNotLinked = 17076 - /** Indicates that the second factor is required for signin. - */ + /// Indicates that the second factor is required for signin. case secondFactorRequired = 17078 - /** Indicates that the multi factor session is missing. - */ + /// Indicates that the multi factor session is missing. case missingMultiFactorSession = 17081 - /** Indicates that the multi factor info is missing. - */ + /// Indicates that the multi factor info is missing. case missingMultiFactorInfo = 17082 - /** Indicates that the multi factor session is invalid. - */ + /// Indicates that the multi factor session is invalid. case invalidMultiFactorSession = 17083 - /** Indicates that the multi factor info is not found. - */ + /// Indicates that the multi factor info is not found. case multiFactorInfoNotFound = 17084 - /** Indicates that the operation is admin restricted. - */ + /// Indicates that the operation is admin restricted. case adminRestrictedOperation = 17085 - /** Indicates that the email is required for verification. - */ + /// Indicates that the email is required for verification. case unverifiedEmail = 17086 - /** Indicates that the second factor is already enrolled. - */ + /// Indicates that the second factor is already enrolled. case secondFactorAlreadyEnrolled = 17087 - /** Indicates that the maximum second factor count is exceeded. - */ + /// Indicates that the maximum second factor count is exceeded. case maximumSecondFactorCountExceeded = 17088 - /** Indicates that the first factor is not supported. - */ + /// Indicates that the first factor is not supported. case unsupportedFirstFactor = 17089 - /** Indicates that the a verifed email is required to changed to. - */ + /// Indicates that the a verifed email is required to changed to. case emailChangeNeedsVerification = 17090 - /** Indicates that the request does not contain a client identifier. - */ + /// Indicates that the request does not contain a client identifier. case missingClientIdentifier = 17093 - /** Indicates that the nonce is missing or invalid. - */ + /// Indicates that the nonce is missing or invalid. case missingOrInvalidNonce = 17094 - /** Raised when n Cloud Function returns a blocking error. Will include a message returned from - * the function. - */ + /// Raised when n Cloud Function returns a blocking error. Will include a message returned from + /// the function. case blockingCloudFunctionError = 17105 - /** Indicates that reCAPTCHA Enterprise integration is not enabled for this project. - */ + /// Indicates that reCAPTCHA Enterprise integration is not enabled for this project. case recaptchaNotEnabled = 17200 - /** Indicates that the reCAPTCHA token is missing from the backend request. - */ + /// Indicates that the reCAPTCHA token is missing from the backend request. case missingRecaptchaToken = 17201 - /** Indicates that the reCAPTCHA token sent with the backend request is invalid. - */ + /// Indicates that the reCAPTCHA token sent with the backend request is invalid. case invalidRecaptchaToken = 17202 - /** Indicates that the requested reCAPTCHA action is invalid. - */ + /// Indicates that the requested reCAPTCHA action is invalid. case invalidRecaptchaAction = 17203 - /** Indicates that the client type is missing from the request. - */ + /// Indicates that the client type is missing from the request. case missingClientType = 17204 - /** Indicates that the reCAPTCHA version is missing from the request. - */ + /// Indicates that the reCAPTCHA version is missing from the request. case missingRecaptchaVersion = 17205 - /** Indicates that the reCAPTCHA version sent to the backend is invalid. - */ + /// Indicates that the reCAPTCHA version sent to the backend is invalid. case invalidRecaptchaVersion = 17206 - /** Indicates that the request type sent to the backend is invalid. - */ + /// Indicates that the request type sent to the backend is invalid. case invalidReqType = 17207 - /** Indicates that the reCAPTCHA SDK is not linked to the app. - */ + /// Indicates that the reCAPTCHA SDK is not linked to the app. case recaptchaSDKNotLinked = 17208 - /** Indicates an error occurred while attempting to access the keychain. - */ + /// Indicates an error occurred while attempting to access the keychain. case keychainError = 17995 - /** Indicates an internal error occurred. - */ + /// Indicates an internal error occurred. case internalError = 17999 - /** Raised when a JWT fails to parse correctly. May be accompanied by an underlying error - describing which step of the JWT parsing process failed. - */ + /// Raised when a JWT fails to parse correctly. May be accompanied by an underlying error + /// describing which step of the JWT parsing process failed. case malformedJWT = 18000 var errorDescription: String { switch self { case .invalidCustomToken: - return kFIRAuthErrorMessageInvalidCustomToken + return kErrorInvalidCustomToken case .customTokenMismatch: - return kFIRAuthErrorMessageCustomTokenMismatch + return kErrorCustomTokenMismatch case .invalidEmail: - return kFIRAuthErrorMessageInvalidEmail + return kErrorInvalidEmail case .invalidCredential: - return kFIRAuthErrorMessageInvalidCredential + return kErrorInvalidCredential case .userDisabled: - return kFIRAuthErrorMessageUserDisabled + return kErrorUserDisabled case .emailAlreadyInUse: - return kFIRAuthErrorMessageEmailAlreadyInUse + return kErrorEmailAlreadyInUse case .wrongPassword: - return kFIRAuthErrorMessageWrongPassword + return kErrorWrongPassword case .tooManyRequests: - return kFIRAuthErrorMessageTooManyRequests + return kErrorTooManyRequests case .accountExistsWithDifferentCredential: - return kFIRAuthErrorMessageAccountExistsWithDifferentCredential + return kErrorAccountExistsWithDifferentCredential case .requiresRecentLogin: - return kFIRAuthErrorMessageRequiresRecentLogin + return kErrorRequiresRecentLogin case .providerAlreadyLinked: - return kFIRAuthErrorMessageProviderAlreadyLinked + return kErrorProviderAlreadyLinked case .noSuchProvider: - return kFIRAuthErrorMessageNoSuchProvider + return kErrorNoSuchProvider case .invalidUserToken: - return kFIRAuthErrorMessageInvalidUserToken + return kErrorInvalidUserToken case .networkError: - return kFIRAuthErrorMessageNetworkError + return kErrorNetworkError case .keychainError: - return kFIRAuthErrorMessageKeychainError + return kErrorKeychainError case .missingClientIdentifier: - return kFIRAuthErrorMessageMissingClientIdentifier + return kErrorMissingClientIdentifier case .userTokenExpired: - return kFIRAuthErrorMessageUserTokenExpired + return kErrorUserTokenExpired case .userNotFound: - return kFIRAuthErrorMessageUserNotFound + return kErrorUserNotFound case .invalidAPIKey: - return kFIRAuthErrorMessageInvalidAPIKey + return kErrorInvalidAPIKey case .credentialAlreadyInUse: - return kFIRAuthErrorMessageCredentialAlreadyInUse + return kErrorCredentialAlreadyInUse case .internalError: - return kFIRAuthErrorMessageInternalError + return kErrorInternalError case .userMismatch: return FIRAuthErrorMessageUserMismatch case .operationNotAllowed: - return kFIRAuthErrorMessageOperationNotAllowed + return kErrorOperationNotAllowed case .weakPassword: - return kFIRAuthErrorMessageWeakPassword + return kErrorWeakPassword case .appNotAuthorized: - return kFIRAuthErrorMessageAppNotAuthorized + return kErrorAppNotAuthorized case .expiredActionCode: - return kFIRAuthErrorMessageExpiredActionCode + return kErrorExpiredActionCode case .invalidActionCode: - return kFIRAuthErrorMessageInvalidActionCode + return kErrorInvalidActionCode case .invalidSender: - return kFIRAuthErrorMessageInvalidSender + return kErrorInvalidSender case .invalidMessagePayload: - return kFIRAuthErrorMessageInvalidMessagePayload + return kErrorInvalidMessagePayload case .invalidRecipientEmail: - return kFIRAuthErrorMessageInvalidRecipientEmail + return kErrorInvalidRecipientEmail case .missingIosBundleID: - return kFIRAuthErrorMessageMissingIosBundleID + return kErrorMissingIosBundleID case .missingAndroidPackageName: - return kFIRAuthErrorMessageMissingAndroidPackageName + return kErrorMissingAndroidPackageName case .unauthorizedDomain: - return kFIRAuthErrorMessageUnauthorizedDomain + return kErrorUnauthorizedDomain case .invalidContinueURI: - return kFIRAuthErrorMessageInvalidContinueURI + return kErrorInvalidContinueURI case .missingContinueURI: - return kFIRAuthErrorMessageMissingContinueURI + return kErrorMissingContinueURI case .missingEmail: - return kFIRAuthErrorMessageMissingEmail + return kErrorMissingEmail case .missingPhoneNumber: - return kFIRAuthErrorMessageMissingPhoneNumber + return kErrorMissingPhoneNumber case .invalidPhoneNumber: - return kFIRAuthErrorMessageInvalidPhoneNumber + return kErrorInvalidPhoneNumber case .missingVerificationCode: - return kFIRAuthErrorMessageMissingVerificationCode + return kErrorMissingVerificationCode case .invalidVerificationCode: - return kFIRAuthErrorMessageInvalidVerificationCode + return kErrorInvalidVerificationCode case .missingVerificationID: - return kFIRAuthErrorMessageMissingVerificationID + return kErrorMissingVerificationID case .invalidVerificationID: - return kFIRAuthErrorMessageInvalidVerificationID + return kErrorInvalidVerificationID case .sessionExpired: - return kFIRAuthErrorMessageSessionExpired + return kErrorSessionExpired case .missingAppCredential: - return kFIRAuthErrorMessageMissingAppCredential + return kErrorMissingAppCredential case .invalidAppCredential: - return kFIRAuthErrorMessageInvalidAppCredential + return kErrorInvalidAppCredential case .quotaExceeded: - return kFIRAuthErrorMessageQuotaExceeded + return kErrorQuotaExceeded case .missingAppToken: - return kFIRAuthErrorMessageMissingAppToken + return kErrorMissingAppToken case .notificationNotForwarded: - return kFIRAuthErrorMessageNotificationNotForwarded + return kErrorNotificationNotForwarded case .appNotVerified: - return kFIRAuthErrorMessageAppNotVerified + return kErrorAppNotVerified case .captchaCheckFailed: - return kFIRAuthErrorMessageCaptchaCheckFailed + return kErrorCaptchaCheckFailed case .webContextAlreadyPresented: - return kFIRAuthErrorMessageWebContextAlreadyPresented + return kErrorWebContextAlreadyPresented case .webContextCancelled: - return kFIRAuthErrorMessageWebContextCancelled + return kErrorWebContextCancelled case .invalidClientID: - return kFIRAuthErrorMessageInvalidClientID + return kErrorInvalidClientID case .appVerificationUserInteractionFailure: - return kFIRAuthErrorMessageAppVerificationUserInteractionFailure + return kErrorAppVerificationUserInteractionFailure case .webNetworkRequestFailed: - return kFIRAuthErrorMessageWebRequestFailed + return kErrorWebRequestFailed case .nullUser: - return kFIRAuthErrorMessageNullUser + return kErrorNullUser case .invalidProviderID: - return kFIRAuthErrorMessageInvalidProviderID + return kErrorInvalidProviderID case .invalidDynamicLinkDomain: - return kFIRAuthErrorMessageInvalidDynamicLinkDomain + return kErrorInvalidDynamicLinkDomain case .webInternalError: - return kFIRAuthErrorMessageWebInternalError + return kErrorWebInternalError case .webSignInUserInteractionFailure: - return kFIRAuthErrorMessageAppVerificationUserInteractionFailure + return kErrorAppVerificationUserInteractionFailure case .malformedJWT: - return kFIRAuthErrorMessageMalformedJWT + return kErrorMalformedJWT case .localPlayerNotAuthenticated: - return kFIRAuthErrorMessageLocalPlayerNotAuthenticated + return kErrorLocalPlayerNotAuthenticated case .gameKitNotLinked: - return kFIRAuthErrorMessageGameKitNotLinked + return kErrorGameKitNotLinked case .secondFactorRequired: - return kFIRAuthErrorMessageSecondFactorRequired + return kErrorSecondFactorRequired case .missingMultiFactorSession: return FIRAuthErrorMessageMissingMultiFactorSession case .missingMultiFactorInfo: @@ -604,35 +495,35 @@ import Foundation case .emailChangeNeedsVerification: return FIRAuthErrorMessageEmailChangeNeedsVerification case .dynamicLinkNotActivated: - return kFIRAuthErrorMessageDynamicLinkNotActivated + return kErrorDynamicLinkNotActivated case .rejectedCredential: - return kFIRAuthErrorMessageRejectedCredential + return kErrorRejectedCredential case .missingOrInvalidNonce: - return kFIRAuthErrorMessageMissingOrInvalidNonce + return kErrorMissingOrInvalidNonce case .tenantIDMismatch: - return kFIRAuthErrorMessageTenantIDMismatch + return kErrorTenantIDMismatch case .unsupportedTenantOperation: - return kFIRAuthErrorMessageUnsupportedTenantOperation + return kErrorUnsupportedTenantOperation case .blockingCloudFunctionError: - return kFIRAuthErrorMessageBlockingCloudFunctionReturnedError + return kErrorBlockingCloudFunctionReturnedError case .recaptchaNotEnabled: - return kFIRAuthErrorMessageRecaptchaNotEnabled + return kErrorRecaptchaNotEnabled case .missingRecaptchaToken: - return kFIRAuthErrorMessageMissingRecaptchaToken + return kErrorMissingRecaptchaToken case .invalidRecaptchaToken: - return kFIRAuthErrorMessageInvalidRecaptchaToken + return kErrorInvalidRecaptchaToken case .invalidRecaptchaAction: - return kFIRAuthErrorMessageInvalidRecaptchaAction + return kErrorInvalidRecaptchaAction case .missingClientType: - return kFIRAuthErrorMessageMissingClientType + return kErrorMissingClientType case .missingRecaptchaVersion: - return kFIRAuthErrorMessageMissingRecaptchaVersion + return kErrorMissingRecaptchaVersion case .invalidRecaptchaVersion: - return kFIRAuthErrorMessageInvalidRecaptchaVersion + return kErrorInvalidRecaptchaVersion case .invalidReqType: - return kFIRAuthErrorMessageInvalidReqType + return kErrorInvalidReqType case .recaptchaSDKNotLinked: - return kFIRAuthErrorMessageRecaptchaSDKNotLinked + return kErrorRecaptchaSDKNotLinked } } @@ -822,501 +713,263 @@ import Foundation // MARK: - Standard Error Messages -/** @var kFIRAuthErrorMessageInvalidCustomToken - @brief Message for @c FIRAuthErrorCodeInvalidCustomToken error code. - */ -private let kFIRAuthErrorMessageInvalidCustomToken = +private let kErrorInvalidCustomToken = "The custom token format is incorrect. Please check the documentation." -/** @var kFIRAuthErrorMessageCustomTokenMismatch - @brief Message for @c FIRAuthErrorCodeCustomTokenMismatch error code. - */ -private let kFIRAuthErrorMessageCustomTokenMismatch = +private let kErrorCustomTokenMismatch = "The custom token corresponds to a different audience." -/** @var kFIRAuthErrorMessageInvalidEmail - @brief Message for @c FIRAuthErrorCodeInvalidEmail error code. - */ -private let kFIRAuthErrorMessageInvalidEmail = "The email address is badly formatted." +private let kErrorInvalidEmail = "The email address is badly formatted." -/** @var kFIRAuthErrorMessageInvalidCredential - @brief Message for @c FIRAuthErrorCodeInvalidCredential error code. - */ -private let kFIRAuthErrorMessageInvalidCredential = +private let kErrorInvalidCredential = "The supplied auth credential is malformed or has expired." -/** @var kFIRAuthErrorMessageUserDisabled - @brief Message for @c FIRAuthErrorCodeUserDisabled error code. - */ -private let kFIRAuthErrorMessageUserDisabled = +private let kErrorUserDisabled = "The user account has been disabled by an administrator." -/** @var kFIRAuthErrorMessageEmailAlreadyInUse - @brief Message for @c FIRAuthErrorCodeEmailAlreadyInUse error code. - */ -private let kFIRAuthErrorMessageEmailAlreadyInUse = +private let kErrorEmailAlreadyInUse = "The email address is already in use by another account." -/** @var kFIRAuthErrorMessageWrongPassword - @brief Message for @c FIRAuthErrorCodeWrongPassword error code. - */ -private let kFIRAuthErrorMessageWrongPassword = +private let kErrorWrongPassword = "The password is invalid or the user does not have a password." -/** @var kFIRAuthErrorMessageTooManyRequests - @brief Message for @c FIRAuthErrorCodeTooManyRequests error code. - */ -private let kFIRAuthErrorMessageTooManyRequests = +private let kErrorTooManyRequests = "We have blocked all requests from this device due to unusual activity. Try again later." -/** @var kFIRAuthErrorMessageAccountExistsWithDifferentCredential - @brief Message for @c FIRAuthErrorCodeAccountExistsWithDifferentCredential error code. - */ -private let kFIRAuthErrorMessageAccountExistsWithDifferentCredential = +private let kErrorAccountExistsWithDifferentCredential = "An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address." -/** @var kFIRAuthErrorMessageRequiresRecentLogin - @brief Message for @c FIRAuthErrorCodeRequiresRecentLogin error code. - */ -private let kFIRAuthErrorMessageRequiresRecentLogin = +private let kErrorRequiresRecentLogin = "This operation is sensitive and requires recent authentication. Log in again before retrying this request." -/** @var kFIRAuthErrorMessageProviderAlreadyLinked - @brief Message for @c FIRAuthErrorCodeProviderAlreadyExists error code. - */ -private let kFIRAuthErrorMessageProviderAlreadyLinked = +private let kErrorProviderAlreadyLinked = "[ERROR_PROVIDER_ALREADY_LINKED] - User can only be linked to one identity for the given provider." -/** @var kFIRAuthErrorMessageNoSuchProvider - @brief Message for @c FIRAuthErrorCodeNoSuchProvider error code. - */ -private let kFIRAuthErrorMessageNoSuchProvider = +private let kErrorNoSuchProvider = "User was not linked to an account with the given provider." -/** @var kFIRAuthErrorMessageInvalidUserToken - @brief Message for @c FIRAuthErrorCodeInvalidUserToken error code. - */ -private let kFIRAuthErrorMessageInvalidUserToken = +private let kErrorInvalidUserToken = "This user's credential isn't valid for this project. This can happen if the user's token has been tampered with, or if the user doesn’t belong to the project associated with the API key used in your request." -/** @var kFIRAuthErrorMessageNetworkError - @brief Message for @c FIRAuthErrorCodeNetworkError error code. - */ -private let kFIRAuthErrorMessageNetworkError = +private let kErrorNetworkError = "Network error (such as timeout, interrupted connection or unreachable host) has occurred." -/** @var kFIRAuthErrorMessageKeychainError - @brief Message for @c FIRAuthErrorCodeKeychainError error code. - */ -private let kFIRAuthErrorMessageKeychainError = +private let kErrorKeychainError = "An error occurred when accessing the keychain. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo dictionary will contain more information about the error encountered" -/** @var kFIRAuthErrorMessageUserTokenExpired - @brief Message for @c FIRAuthErrorCodeTokenExpired error code. - */ -private let kFIRAuthErrorMessageUserTokenExpired = +private let kErrorUserTokenExpired = "The user's credential is no longer valid. The user must sign in again." -/** @var kFIRAuthErrorMessageUserNotFound - @brief Message for @c FIRAuthErrorCodeUserNotFound error code. - */ -private let kFIRAuthErrorMessageUserNotFound = +private let kErrorUserNotFound = "There is no user record corresponding to this identifier. The user may have been deleted." -/** @var kFIRAuthErrorMessageInvalidAPIKey - @brief Message for @c FIRAuthErrorCodeInvalidAPIKey error code. - @remarks This error is not thrown by the server. - */ -private let kFIRAuthErrorMessageInvalidAPIKey = "An invalid API Key was supplied in the request." +private let kErrorInvalidAPIKey = "An invalid API Key was supplied in the request." -/** @var kFIRAuthErrorMessageUserMismatch. - @brief Message for @c FIRAuthErrorCodeInvalidAPIKey error code. - */ private let FIRAuthErrorMessageUserMismatch = "The supplied credentials do not correspond to the previously signed in user." -/** @var kFIRAuthErrorMessageCredentialAlreadyInUse - @brief Message for @c FIRAuthErrorCodeCredentialAlreadyInUse error code. - */ -private let kFIRAuthErrorMessageCredentialAlreadyInUse = +private let kErrorCredentialAlreadyInUse = "This credential is already associated with a different user account." -/** @var kFIRAuthErrorMessageOperationNotAllowed - @brief Message for @c FIRAuthErrorCodeOperationNotAllowed error code. - */ -private let kFIRAuthErrorMessageOperationNotAllowed = +private let kErrorOperationNotAllowed = "The given sign-in provider is disabled for this Firebase project. Enable it in the Firebase console, under the sign-in method tab of the Auth section." -/** @var kFIRAuthErrorMessageWeakPassword - @brief Message for @c FIRAuthErrorCodeWeakPassword error code. - */ -private let kFIRAuthErrorMessageWeakPassword = "The password must be 6 characters long or more." +private let kErrorWeakPassword = "The password must be 6 characters long or more." -/** @var kFIRAuthErrorMessageAppNotAuthorized - @brief Message for @c FIRAuthErrorCodeAppNotAuthorized error code. - */ -private let kFIRAuthErrorMessageAppNotAuthorized = +private let kErrorAppNotAuthorized = "This app is not authorized to use Firebase Authentication with the provided API key. Review your key configuration in the Google API console and ensure that it accepts requests from your app's bundle ID." -/** @var kFIRAuthErrorMessageExpiredActionCode - @brief Message for @c FIRAuthErrorCodeExpiredActionCode error code. - */ -private let kFIRAuthErrorMessageExpiredActionCode = "The action code has expired." +private let kErrorExpiredActionCode = "The action code has expired." -/** @var kFIRAuthErrorMessageInvalidActionCode - @brief Message for @c FIRAuthErrorCodeInvalidActionCode error code. - */ -private let kFIRAuthErrorMessageInvalidActionCode = +private let kErrorInvalidActionCode = "The action code is invalid. This can happen if the code is malformed, expired, or has already been used." -/** @var kFIRAuthErrorMessageInvalidMessagePayload - @brief Message for @c FIRAuthErrorCodeInvalidMessagePayload error code. - */ -private let kFIRAuthErrorMessageInvalidMessagePayload = +private let kErrorInvalidMessagePayload = "The action code is invalid. This can happen if the code is malformed, expired, or has already been used." -/** @var kFIRAuthErrorMessageInvalidSender - @brief Message for @c FIRAuthErrorCodeInvalidSender error code. - */ -private let kFIRAuthErrorMessageInvalidSender = +private let kErrorInvalidSender = "The email template corresponding to this action contains invalid characters in its message. Please fix by going to the Auth email templates section in the Firebase Console." -/** @var kFIRAuthErrorMessageInvalidRecipientEmail - @brief Message for @c FIRAuthErrorCodeInvalidRecipient error code. - */ -private let kFIRAuthErrorMessageInvalidRecipientEmail = +private let kErrorInvalidRecipientEmail = "The action code is invalid. This can happen if the code is malformed, expired, or has already been used." -/** @var kFIRAuthErrorMessageMissingIosBundleID - @brief Message for @c FIRAuthErrorCodeMissingIosbundleID error code. - */ -private let kFIRAuthErrorMessageMissingIosBundleID = +private let kErrorMissingIosBundleID = "An iOS Bundle ID must be provided if an App Store ID is provided." -/** @var kFIRAuthErrorMessageMissingAndroidPackageName - @brief Message for @c FIRAuthErrorCodeMissingAndroidPackageName error code. - */ -private let kFIRAuthErrorMessageMissingAndroidPackageName = +private let kErrorMissingAndroidPackageName = "An Android Package Name must be provided if the Android App is required to be installed." -/** @var kFIRAuthErrorMessageUnauthorizedDomain - @brief Message for @c FIRAuthErrorCodeUnauthorizedDomain error code. - */ -private let kFIRAuthErrorMessageUnauthorizedDomain = +private let kErrorUnauthorizedDomain = "The domain of the continue URL is not allowlisted. Please allowlist the domain in the Firebase console." -/** @var kFIRAuthErrorMessageInvalidContinueURI - @brief Message for @c FIRAuthErrorCodeInvalidContinueURI error code. - */ -private let kFIRAuthErrorMessageInvalidContinueURI = +private let kErrorInvalidContinueURI = "The continue URL provided in the request is invalid." -/** @var kFIRAuthErrorMessageMissingEmail - @brief Message for @c FIRAuthErrorCodeMissingEmail error code. - */ -private let kFIRAuthErrorMessageMissingEmail = "An email address must be provided." +private let kErrorMissingEmail = "An email address must be provided." -/** @var kFIRAuthErrorMessageMissingContinueURI - @brief Message for @c FIRAuthErrorCodeMissingContinueURI error code. - */ -private let kFIRAuthErrorMessageMissingContinueURI = +private let kErrorMissingContinueURI = "A continue URL must be provided in the request." -/** @var kFIRAuthErrorMessageMissingPhoneNumber - @brief Message for @c FIRAuthErrorCodeMissingPhoneNumber error code. - */ -private let kFIRAuthErrorMessageMissingPhoneNumber = +private let kErrorMissingPhoneNumber = "To send verification codes, provide a phone number for the recipient." -/** @var kFIRAuthErrorMessageInvalidPhoneNumber - @brief Message for @c FIRAuthErrorCodeInvalidPhoneNumber error code. - */ -private let kFIRAuthErrorMessageInvalidPhoneNumber = +private let kErrorInvalidPhoneNumber = "The format of the phone number provided is incorrect. Please enter the phone number in a format that can be parsed into E.164 format. E.164 phone numbers are written in the format [+][country code][subscriber number including area code]." -/** @var kFIRAuthErrorMessageMissingVerificationCode - @brief Message for @c FIRAuthErrorCodeMissingVerificationCode error code. - */ -private let kFIRAuthErrorMessageMissingVerificationCode = +private let kErrorMissingVerificationCode = "The phone auth credential was created with an empty SMS verification Code." -/** @var kFIRAuthErrorMessageInvalidVerificationCode - @brief Message for @c FIRAuthErrorCodeInvalidVerificationCode error code. - */ -private let kFIRAuthErrorMessageInvalidVerificationCode = +private let kErrorInvalidVerificationCode = "The multifactor verification code used to create the auth credential is invalid. " + "Re-collect the verification code and be sure to use the verification code provided by the user." -/** @var kFIRAuthErrorMessageMissingVerificationID - @brief Message for @c FIRAuthErrorCodeInvalidVerificationID error code. - */ -private let kFIRAuthErrorMessageMissingVerificationID = +private let kErrorMissingVerificationID = "The phone auth credential was created with an empty verification ID." -/** @var kFIRAuthErrorMessageInvalidVerificationID - @brief Message for @c FIRAuthErrorCodeInvalidVerificationID error code. - */ -private let kFIRAuthErrorMessageInvalidVerificationID = +private let kErrorInvalidVerificationID = "The verification ID used to create the phone auth credential is invalid." -/** @var kFIRAuthErrorMessageLocalPlayerNotAuthenticated - @brief Message for @c FIRAuthErrorCodeLocalPlayerNotAuthenticated error code. - */ -private let kFIRAuthErrorMessageLocalPlayerNotAuthenticated = +private let kErrorLocalPlayerNotAuthenticated = "The local player is not authenticated. Please log the local player in to Game Center." -/** @var kFIRAuthErrorMessageGameKitNotLinked - @brief Message for @c kFIRAuthErrorMessageGameKitNotLinked error code. - */ -private let kFIRAuthErrorMessageGameKitNotLinked = +private let kErrorGameKitNotLinked = "The GameKit framework is not linked. Please turn on the Game Center capability." -/** @var kFIRAuthErrorMessageSessionExpired - @brief Message for @c FIRAuthErrorCodeSessionExpired error code. - */ -private let kFIRAuthErrorMessageSessionExpired = +private let kErrorSessionExpired = "The SMS code has expired. Please re-send the verification code to try again." -/** @var kFIRAuthErrorMessageMissingAppCredential - @brief Message for @c FIRAuthErrorCodeMissingAppCredential error code. - */ -private let kFIRAuthErrorMessageMissingAppCredential = +private let kErrorMissingAppCredential = "The phone verification request is missing an APNs Device token. Firebase Auth automatically detects APNs Device Tokens, however, if method swizzling is disabled, the APNs token must be set via the APNSToken property on FIRAuth or by calling setAPNSToken:type on FIRAuth." -/** @var kFIRAuthErrorMessageInvalidAppCredential - @brief Message for @c FIRAuthErrorCodeInvalidAppCredential error code. - */ -private let kFIRAuthErrorMessageInvalidAppCredential = +private let kErrorInvalidAppCredential = "The APNs device token provided is either incorrect or does not match the private certificate uploaded to the Firebase Console." -/** @var kFIRAuthErrorMessageQuotaExceeded - @brief Message for @c FIRAuthErrorCodeQuotaExceeded error code. - */ -private let kFIRAuthErrorMessageQuotaExceeded = "The quota for this operation has been exceeded." +private let kErrorQuotaExceeded = "The quota for this operation has been exceeded." -/** @var kFIRAuthErrorMessageMissingAppToken - @brief Message for @c FIRAuthErrorCodeMissingAppToken error code. - */ -private let kFIRAuthErrorMessageMissingAppToken = +private let kErrorMissingAppToken = "There seems to be a problem with your project's Firebase phone number authentication set-up, please make sure to follow the instructions found at https://firebase.google.com/docs/auth/ios/phone-auth" -/** @var kFIRAuthErrorMessageMissingAppToken - @brief Message for @c FIRAuthErrorCodeMissingAppToken error code. - */ -private let kFIRAuthErrorMessageNotificationNotForwarded = +private let kErrorNotificationNotForwarded = "If app delegate swizzling is disabled, remote notifications received by UIApplicationDelegate need to" + "be forwarded to FirebaseAuth's canHandleNotificaton method." -/** @var kFIRAuthErrorMessageAppNotVerified - @brief Message for @c FIRAuthErrorCodeMissingAppToken error code. - */ -private let kFIRAuthErrorMessageAppNotVerified = +private let kErrorAppNotVerified = "Firebase could not retrieve the silent push notification and therefore could not verify your app. Ensure that you configured your app correctly to receive push notifications." -/** @var kFIRAuthErrorMessageCaptchaCheckFailed - @brief Message for @c FIRAuthErrorCodeCaptchaCheckFailed error code. - */ -private let kFIRAuthErrorMessageCaptchaCheckFailed = +private let kErrorCaptchaCheckFailed = "The reCAPTCHA response token provided is either invalid, expired or already" -/** @var kFIRAuthErrorMessageWebContextAlreadyPresented - @brief Message for @c FIRAuthErrorCodeWebContextAlreadyPresented error code. - */ -private let kFIRAuthErrorMessageWebContextAlreadyPresented = +private let kErrorWebContextAlreadyPresented = "User interaction is still ongoing, another view cannot be presented." -/** @var kFIRAuthErrorMessageWebContextCancelled - @brief Message for @c FIRAuthErrorCodeWebContextCancelled error code. - */ -private let kFIRAuthErrorMessageWebContextCancelled = "The interaction was cancelled by the user." +private let kErrorWebContextCancelled = "The interaction was cancelled by the user." -/** @var kFIRAuthErrorMessageInvalidClientID - @brief Message for @c FIRAuthErrorCodeInvalidClientID error code. - */ -private let kFIRAuthErrorMessageInvalidClientID = +private let kErrorInvalidClientID = "The OAuth client ID provided is either invalid or does not match the specified API key." -/** @var kFIRAuthErrorMessageWebRequestFailed - @brief Message for @c FIRAuthErrorCodeWebRequestFailed error code. - */ -private let kFIRAuthErrorMessageWebRequestFailed = +private let kErrorWebRequestFailed = "A network error (such as timeout, interrupted connection, or unreachable host) has occurred within the web context." -/** @var kFIRAuthErrorMessageWebInternalError - @brief Message for @c FIRAuthErrorCodeWebInternalError error code. - */ -private let kFIRAuthErrorMessageWebInternalError = +private let kErrorWebInternalError = "An internal error has occurred within the SFSafariViewController or WKWebView." -/** @var kFIRAuthErrorMessageAppVerificationUserInteractionFailure - @brief Message for @c FIRAuthErrorCodeInvalidClientID error code. - */ -private let kFIRAuthErrorMessageAppVerificationUserInteractionFailure = +private let kErrorAppVerificationUserInteractionFailure = "The app verification process has failed, print and inspect the error details for more information" -/** @var kFIRAuthErrorMessageNullUser - @brief Message for @c FIRAuthErrorCodeNullUser error code. - */ -private let kFIRAuthErrorMessageNullUser = +private let kErrorNullUser = "A null user object was provided as the argument for an operation which requires a non-null user object." -/** @var kFIRAuthErrorMessageInvalidProviderID - @brief Message for @c FIRAuthErrorCodeInvalidProviderID error code. - */ -private let kFIRAuthErrorMessageInvalidProviderID = +private let kErrorInvalidProviderID = "The provider ID provided for the attempted web operation is invalid." -/** @var kFIRAuthErrorMessageInvalidDynamicLinkDomain - @brief Message for @c kFIRAuthErrorMessageInvalidDynamicLinkDomain error code. - */ -private let kFIRAuthErrorMessageInvalidDynamicLinkDomain = +private let kErrorInvalidDynamicLinkDomain = "The Firebase Dynamic Link domain used is either not configured or is unauthorized for the current project." -/** @var kFIRAuthErrorMessageInternalError - @brief Message for @c FIRAuthErrorCodeInternalError error code. - */ -private let kFIRAuthErrorMessageInternalError = +private let kErrorInternalError = "An internal error has occurred, print and inspect the error details for more information." -/** @var kFIRAuthErrorMessageMalformedJWT - @brief Error message constant describing @c FIRAuthErrorCodeMalformedJWT errors. - */ -private let kFIRAuthErrorMessageMalformedJWT = +private let kErrorMalformedJWT = "Failed to parse JWT. Check the userInfo dictionary for the full token." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ -private let kFIRAuthErrorMessageSecondFactorRequired = +private let kErrorSecondFactorRequired = "Please complete a second factor challenge to finish signing into this account." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageMissingMultiFactorSession = "The request is missing proof of first factor successful sign-in." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageMissingMultiFactorInfo = "No second factor identifier is provided." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageInvalidMultiFactorSession = "The request does not contain a valid proof of first factor successful sign-in." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageMultiFactorInfoNotFound = "The user does not have a second factor matching the identifier provided." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageAdminRestrictedOperation = "This operation is restricted to administrators only." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageUnverifiedEmail = "The operation requires a verified email." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageSecondFactorAlreadyEnrolled = "The second factor is already enrolled on this account." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageMaximumSecondFactorCountExceeded = "The maximum allowed number of second factors on a user has been exceeded." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageUnsupportedFirstFactor = "Enrolling a second factor or signing in with a multi-factor account requires sign-in with a supported first factor." -/** @var kFIRAuthErrorMessageSecondFactorRequired - @brief Message for @c kFIRAuthErrorMessageSecondFactorRequired error code. - */ private let FIRAuthErrorMessageEmailChangeNeedsVerification = "Multi-factor users must always have a verified email." -/** @var kFIRAuthErrorMessageDynamicLinkNotActivated - @brief Error message constant describing @c FIRAuthErrorCodeDynamicLinkNotActivated errors. - */ -private let kFIRAuthErrorMessageDynamicLinkNotActivated = +private let kErrorDynamicLinkNotActivated = "Please activate Dynamic Links in the Firebase Console and agree to the terms and conditions." -/** @var kFIRAuthErrorMessageRejectedCredential - @brief Error message constant describing @c FIRAuthErrorCodeRejectedCredential errors. - */ -private let kFIRAuthErrorMessageRejectedCredential = +private let kErrorRejectedCredential = "The request contains malformed or mismatching credentials." -/** @var kFIRAuthErrorMessageMissingClientIdentifier - @brief Error message constant describing @c FIRAuthErrorCodeMissingClientIdentifier errors. - */ -private let kFIRAuthErrorMessageMissingClientIdentifier = +private let kErrorMissingClientIdentifier = "The request does not contain a client identifier." -/** @var kFIRAuthErrorMessageMissingOrInvalidNonce - @brief Error message constant describing @c FIRAuthErrorCodeMissingOrInvalidNonce errors. - */ -private let kFIRAuthErrorMessageMissingOrInvalidNonce = +private let kErrorMissingOrInvalidNonce = "The request contains malformed or mismatched credentials." -/** @var kFIRAuthErrorMessageTenantIDMismatch. - @brief Message for @c FIRAuthErrorCodeTenantIDMismatch error code. - */ -private let kFIRAuthErrorMessageTenantIDMismatch = +private let kErrorTenantIDMismatch = "The provided user's tenant ID does not match the Auth instance's tenant ID." -/** @var kFIRAuthErrorMessageUnsupportedTenantOperation - @brief Message for @c FIRAuthErrorCodeUnsupportedTenantOperation error code. - */ -private let kFIRAuthErrorMessageUnsupportedTenantOperation = +private let kErrorUnsupportedTenantOperation = "This operation is not supported in a multi-tenant context." -/** @var kFIRAuthErrorMessageBlockingCloudFunctionReturnedError - @brief Message for @c FIRAuthErrorCodeBlockingCloudFunctionError error code. - */ -private let kFIRAuthErrorMessageBlockingCloudFunctionReturnedError = +private let kErrorBlockingCloudFunctionReturnedError = "Blocking cloud function returned an error." -private let kFIRAuthErrorMessageRecaptchaNotEnabled = +private let kErrorRecaptchaNotEnabled = "reCAPTCHA Enterprise is not enabled for this project." -private let kFIRAuthErrorMessageMissingRecaptchaToken = +private let kErrorMissingRecaptchaToken = "The backend request is missing the reCAPTCHA verification token." -private let kFIRAuthErrorMessageInvalidRecaptchaToken = +private let kErrorInvalidRecaptchaToken = "The reCAPTCHA verification token is invalid or has expired." -private let kFIRAuthErrorMessageInvalidRecaptchaAction = +private let kErrorInvalidRecaptchaAction = "The reCAPTCHA verification failed due to an invalid action." -private let kFIRAuthErrorMessageMissingClientType = +private let kErrorMissingClientType = "The request is missing a client type or the client type is invalid." -private let kFIRAuthErrorMessageMissingRecaptchaVersion = +private let kErrorMissingRecaptchaVersion = "The request is missing the reCAPTCHA version parameter." -private let kFIRAuthErrorMessageInvalidRecaptchaVersion = +private let kErrorInvalidRecaptchaVersion = "The request specifies an invalid version of reCAPTCHA." -private let kFIRAuthErrorMessageInvalidReqType = +private let kErrorInvalidReqType = "The request is not supported or is invalid." // TODO(chuanr, ObjC): point the link to GCIP doc once available. -private let kFIRAuthErrorMessageRecaptchaSDKNotLinked = +private let kErrorRecaptchaSDKNotLinked = "The reCAPTCHA SDK is not linked to your app. See " + "https://cloud.google.com/recaptcha-enterprise/docs/instrument-ios-apps" diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthInternalErrors.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthInternalErrors.swift index 19a3b188c0a..51b1c51ba41 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthInternalErrors.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthInternalErrors.swift @@ -14,87 +14,83 @@ import Foundation -/** @var FIRAuthPublicErrorCodeFlag - @brief Bitmask value indicating the error represents a public error code when this bit is - zeroed. Error codes which don't contain this flag will be wrapped in an @c NSError whose - code is @c FIRAuthErrorCodeInternalError. - */ +/// Bitmask value indicating the error represents a public error code when this bit is +/// zeroed. Error codes which don't contain this flag will be wrapped in an `NSError` whose +/// code is `AuthErrorCodeInternalError`. let FIRAuthPublicErrorCodeFlag: Int = 1 << 20 -/** @var FIRAuthInternalErrorCode - @brief Error codes used internally by Firebase Auth. - @remarks All errors are generated using an internal error code. These errors are automatically - converted to the appropriate public version of the @c NSError by the methods in - @c FIRAuthErrorUtils - */ - enum SharedErrorCode { case `public`(AuthErrorCode) case `internal`(AuthInternalErrorCode) } +/// Error codes used internally by Firebase Auth. +/// +/// All errors are generated using an internal error code. These errors are automatically +/// converted to the appropriate public version of the `NSError` by the methods in AuthErrorUtils. enum AuthInternalErrorCode: Int { + /// Indicates a network error occurred (such as a timeout, interrupted connection, or + /// unreachable host.) + /// + /// These types of errors are often recoverable with a retry. + /// + /// See the `NSUnderlyingError` value in the `NSError.userInfo` dictionary for details about + /// the network error which occurred. case networkError = 17020 - /** @var FIRAuthInternalErrorCodeRPCRequestEncodingError - @brief Indicates an error encoding the RPC request. - @remarks This is typically due to some sort of unexpected input value. - - See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details. - */ + /// Indicates an error encoding the RPC request. + /// + /// This is typically due to some sort of unexpected input value. + /// + /// See the `NSUnderlyingError` value in the `NSError.userInfo` dictionary for details. case RPCRequestEncodingError = 1 - /** @var FIRAuthInternalErrorCodeJSONSerializationError - @brief Indicates an error serializing an RPC request. - @remarks This is typically due to some sort of unexpected input value. - - If an @c NSJSONSerialization.isValidJSONObject: check fails, the error will contain no - @c NSUnderlyingError key in the @c NSError.userInfo dictionary. If an error was - encountered calling @c NSJSONSerialization.dataWithJSONObject:options:error:, the - resulting error will be associated with the @c NSUnderlyingError key in the - @c NSError.userInfo dictionary. - */ + /// Indicates an error serializing an RPC request. + /// + /// This is typically due to some sort of unexpected input value. + /// + /// If an `JSONSerialization.isValidJSONObject` check fails, the error will contain no + /// `NSUnderlyingError` key in the `NSError.userInfo` dictionary. If an error was + /// encountered calling `NSJSONSerialization.dataWithJSONObject`, the + /// resulting error will be associated with the `NSUnderlyingError` key in the + /// `NSError.userInfo` dictionary. case JSONSerializationError = 2 - /** @var FIRAuthInternalErrorCodeUnexpectedErrorResponse - @brief Indicates an HTTP error occurred and the data returned either couldn't be deserialized - or couldn't be decoded. - @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details - about the HTTP error which occurred. - - If the response could be deserialized as JSON then the @c NSError.userInfo dictionary will - contain a value for the key @c FIRAuthErrorUserInfoDeserializedResponseKey which is the - deserialized response value. - - If the response could not be deserialized as JSON then the @c NSError.userInfo dictionary - will contain values for the @c NSUnderlyingErrorKey and @c FIRAuthErrorUserInfoDataKey - keys. - */ + /// Indicates an HTTP error occurred and the data returned either couldn't be deserialized + /// or couldn't be decoded. + /// + /// See the `NSUnderlyingError` value in the `NSError.userInfo` dictionary for details + /// about the HTTP error which occurred. + /// + /// If the response could be deserialized as JSON then the `NSError.userInfo` dictionary will + /// contain a value for the key `AuthErrorUserInfoDeserializedResponseKey` which is the + /// deserialized response value. + /// + /// If the response could not be deserialized as JSON then the `NSError.userInfo` dictionary + /// will contain values for the `NSUnderlyingErrorKey` and `AuthErrorUserInfoDataKey` keys. case unexpectedErrorResponse = 3 - /** @var FIRAuthInternalErrorCodeUnexpectedResponse - @brief Indicates the HTTP response indicated the request was a successes, but the response - contains something other than a JSON-encoded dictionary, or the data type of the response - indicated it is different from the type of response we expected. - @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary. - If this key is present in the dictionary, it may contain an error from - @c NSJSONSerialization error (indicating the response received was of the wrong data - type). - - See the @c FIRAuthErrorUserInfoDeserializedResponseKey value in the @c NSError.userInfo - dictionary. If the response could be deserialized, it's deserialized representation will - be associated with this key. If the @c NSUnderlyingError value in the @c NSError.userInfo - dictionary is @c nil, this indicates the JSON didn't represent a dictionary. - */ + /// Indicates the HTTP response indicated the request was a successes, but the response + /// contains something other than a JSON-encoded dictionary, or the data type of the response + /// indicated it is different from the type of response we expected. + /// + /// See the `NSUnderlyingError` value in the `NSError.userInfo` dictionary. + /// If this key is present in the dictionary, it may contain an error from + /// `NSJSONSerialization` error (indicating the response received was of the wrong data type). + /// + /// See the `AuthErrorUserInfoDeserializedResponseKey` value in the `NSError.userInfo` + /// dictionary. If the response could be deserialized, it's deserialized representation will + /// be associated with this key. If the @c NSUnderlyingError value in the @c NSError.userInfo + /// dictionary is @c nil, this indicates the JSON didn't represent a dictionary. case unexpectedResponse = 4 - /** @var FIRAuthInternalErrorCodeRPCResponseDecodingError - @brief Indicates an error decoding the RPC response. - This is typically due to some sort of unexpected response value from the server. - @remarks See the @c NSUnderlyingError value in the @c NSError.userInfo dictionary for details. - - See the @c FIRErrorUserInfoDecodedResponseKey value in the @c NSError.userInfo dictionary. - The deserialized representation of the response will be associated with this key. - */ + /// Indicates an error decoding the RPC response. + /// + /// This is typically due to some sort of unexpected response value from the server. + /// + /// See the `NSUnderlyingError` value in the `NSError.userInfo` dictionary for details. + /// + /// See the `userInfoDeserializedResponseKey` value in the `NSError.userInfo` dictionary. + /// The deserialized representation of the response will be associated with this key. case RPCResponseDecodingError = 5 } diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift index 856b69a949b..1b6b573995b 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthUIDelegate.swift @@ -17,31 +17,26 @@ import Foundation import UIKit - /** @protocol AuthUIDelegate - @brief A protocol to handle user interface interactions for Firebase Auth. - This protocol is available on iOS, macOS Catalyst, and tvOS only. - */ + /// A protocol to handle user interface interactions for Firebase Auth. + /// + /// This protocol is available on iOS, macOS Catalyst, and tvOS only. @objc(FIRAuthUIDelegate) public protocol AuthUIDelegate: NSObjectProtocol { - /** @fn presentViewController:animated:completion: - @brief If implemented, this method will be invoked when Firebase Auth needs to display a view - controller. - @param viewControllerToPresent The view controller to be presented. - @param flag Decides whether the view controller presentation should be animated or not. - @param completion The block to execute after the presentation finishes. This block has no return - value and takes no parameters. - */ + /// If implemented, this method will be invoked when Firebase Auth needs to display a view + /// controller. + /// - Parameter viewControllerToPresent: The view controller to be presented. + /// - Parameter flag: Decides whether the view controller presentation should be animated. + /// - Parameter completion: The block to execute after the presentation finishes. + /// This block has no return value and takes no parameters. @objc(presentViewController:animated:completion:) func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) - /** @fn dismissViewControllerAnimated:completion: - @brief If implemented, this method will be invoked when Firebase Auth needs to display a view - controller. - @param flag Decides whether removing the view controller should be animated or not. - @param completion The block to execute after the presentation finishes. This block has no return - value and takes no parameters. - */ + /// If implemented, this method will be invoked when Firebase Auth needs to display a view + /// controller. + /// - Parameter flag: Decides whether removing the view controller should be animated or not. + /// - Parameter completion: The block to execute after the presentation finishes. + /// This block has no return value and takes no parameters. @objc(dismissViewControllerAnimated:completion:) func dismiss(animated flag: Bool, completion: (() -> Void)?) } diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift index 784d097324a..20de31ca968 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift @@ -19,19 +19,16 @@ import UIKit import WebKit - /** @class AuthURLPresenter - @brief A Class responsible for presenting URL via SFSafariViewController or WKWebView. - */ + /// A Class responsible for presenting URL via SFSafariViewController or WKWebView. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AuthURLPresenter: NSObject, SFSafariViewControllerDelegate, AuthWebViewControllerDelegate { - /** @fn - @brief Presents an URL to interact with user. - @param url The URL to present. - @param uiDelegate The UI delegate to present view controller. - @param completion A block to be called either synchronously if the presentation fails to start, - or asynchronously in future on an unspecified thread once the presentation finishes. - */ + /// Presents an URL to interact with user. + /// - Parameter url: The URL to present. + /// - Parameter uiDelegate: The UI delegate to present view controller. + /// - Parameter completion: A block to be called either synchronously if the presentation fails + /// to start, or asynchronously in future on an unspecified thread once the presentation + /// finishes. func present(_ url: URL, uiDelegate: AuthUIDelegate?, callbackMatcher: @escaping (URL?) -> Bool, @@ -74,11 +71,9 @@ } } - /** @fn canHandleURL: - @brief Determines if a URL was produced by the currently presented URL. - @param url The URL to handle. - @return Whether the URL could be handled or not. - */ + /// Determines if a URL was produced by the currently presented URL. + /// - Parameter url: The URL to handle. + /// - Returns: Whether the URL could be handled or not. func canHandle(url: URL) -> Bool { if isPresenting, let callbackMatcher = callbackMatcher, @@ -119,44 +114,33 @@ } } - /** @var_isPresenting - @brief Whether or not some web-based content is being presented. - Accesses to this property are serialized on the global Auth work queue - and thus this variable should not be read or written outside of the work queue. - */ + /// Whether or not some web-based content is being presented. + /// + /// Accesses to this property are serialized on the global Auth work queue + /// and thus this variable should not be read or written outside of the work queue. private var isPresenting: Bool = false - /** @var callbackMatcher - @brief The callback URL matcher for the current presentation, if one is active. - */ + /// The callback URL matcher for the current presentation, if one is active. private var callbackMatcher: ((URL) -> Bool)? - /** @var safariViewController - @brief The SFSafariViewController used for the current presentation, if any. - */ + /// The SFSafariViewController used for the current presentation, if any. private var safariViewController: SFSafariViewController? - /** @var webViewController - @brief The FIRAuthWebViewController used for the current presentation, if any. - */ + /// The `AuthWebViewController` used for the current presentation, if any. private var webViewController: AuthWebViewController? - /** @var uiDelegate - @brief The UIDelegate used to present the SFSafariViewController. - */ + /// The UIDelegate used to present the SFSafariViewController. var uiDelegate: AuthUIDelegate? - /** @var completion - @brief The completion handler for the current presentation, if one is active. - Accesses to this variable are serialized on the global Auth work queue - and thus this variable should not be read or written outside of the work queue. - @remarks This variable is also used as a flag to indicate a presentation is active. - */ + /// The completion handler for the current presentation, if one is active. + /// + /// Accesses to this variable are serialized on the global Auth work queue + /// and thus this variable should not be read or written outside of the work queue. + /// + /// This variable is also used as a flag to indicate a presentation is active. var completion: ((URL?, Error?) -> Void)? - /** @var fakeUIDelegate - @brief Test-only option to validate the calls to the uiDelegate. - */ + /// Test-only option to validate the calls to the uiDelegate. var fakeUIDelegate: AuthUIDelegate? // MARK: Private methods diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift index 7814fc74cd7..47456070935 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebUtils.swift @@ -77,10 +77,8 @@ class AuthWebUtils: NSObject { return false } - /** @fn extractDomain:urlString - @brief Strips url of scheme and path string to extract domain name - @param urlString URL string for domain - */ + /// Strips url of scheme and path string to extract domain name + /// - Parameter urlString: URL string for domain static func extractDomain(urlString: String) -> String? { var domain = urlString diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift index 474f020a152..8950052112e 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift @@ -17,9 +17,7 @@ import UIKit import WebKit - /** @class AuthWebView - @brief A class responsible for creating a WKWebView for use within Firebase Auth. - */ + /// A class responsible for creating a WKWebView for use within Firebase Auth. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AuthWebView: UIView { lazy var webView: WKWebView = createWebView() diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift index 6fb56a4060d..f476c0e06f5 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthWebViewController.swift @@ -18,38 +18,29 @@ import UIKit import WebKit - /** @protocol AuthWebViewControllerDelegate - @brief Defines a delegate for AuthWebViewController - */ + /// Defines a delegate for AuthWebViewController @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) protocol AuthWebViewControllerDelegate: AnyObject { - /** @fn webViewControllerDidCancel: - @brief Notifies the delegate that the web view controller is being cancelled by the user. - @param webViewController The web view controller in question. - */ + /// Notifies the delegate that the web view controller is being cancelled by the user. + /// - Parameter webViewController: The web view controller in question. func webViewControllerDidCancel(_ controller: AuthWebViewController) - /** @fn webViewController:canHandleURL: - @brief Determines if a URL should be handled by the delegate. - @param URL The URL to handle. - @return Whether the URL could be handled or not. - */ + /// Determines if a URL should be handled by the delegate. + /// - Parameter url: The URL to handle. + /// - Returns: Whether the URL could be handled or not. func webViewController(_ controller: AuthWebViewController, canHandle url: URL) -> Bool - /** @fn webViewController:didFailWithError: - @brief Notifies the delegate that the web view controller failed to load a page. - @param webViewController The web view controller in question. - @param error The error that has occurred. - */ + /// Notifies the delegate that the web view controller failed to load a page. + /// - Parameter webViewController: The web view controller in question. + /// - Parameter error: The error that has occurred. func webViewController(_ controller: AuthWebViewController, didFailWithError error: Error) - /** @fn presentURL:UIDelegate:callbackMatcher:completion: - @brief Presents an URL to interact with user. - @param url The URL to present. - @param uiDelegate The UI delegate to present view controller. - @param completion A block to be called either synchronously if the presentation fails to start, - or asynchronously in future on an unspecified thread once the presentation finishes. - */ + /// Presents an URL to interact with user. + /// - Parameter url: The URL to present. + /// - Parameter uiDelegate: The UI delegate to present view controller. + /// - Parameter completion: A block to be called either synchronously if the presentation fails + /// to start, or asynchronously in future on an unspecified thread once the presentation + /// finishes. func present(_ url: URL, uiDelegate: AuthUIDelegate?, callbackMatcher: @escaping (URL?) -> Bool, diff --git a/FirebaseAuth/Tests/Unit/AuthBackendRPCImplentationTests.swift b/FirebaseAuth/Tests/Unit/AuthBackendRPCImplentationTests.swift index dc43d57168b..8a425c82599 100644 --- a/FirebaseAuth/Tests/Unit/AuthBackendRPCImplentationTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthBackendRPCImplentationTests.swift @@ -235,7 +235,7 @@ class AuthBackendRPCImplementationTests: RPCBaseTests { expected. We are expecting to receive an @c NSError with the code @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded response in the @c NSError.userInfo dictionary associated with the key - @c FIRAuthErrorUserInfoDecodedResponseKey. + `userInfoDeserializedResponseKey`. */ func testNonDictionarySuccessResponse() async throws { // We are responding with a JSON-encoded string value representing an array - which is From 0bb0dcf76cde5a5e924599bba9d942fff7d4ec62 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 7 Feb 2024 11:15:28 -0800 Subject: [PATCH 027/104] Migrate a few more retry steps (#12371) --- .github/workflows/abtesting.yml | 29 +++++++++++++------ .github/workflows/auth.yml | 48 ++++++++++++++++++++++--------- .github/workflows/crashlytics.yml | 30 +++++++++++++------ 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/.github/workflows/abtesting.yml b/.github/workflows/abtesting.yml index c8ee5c3cde9..84e2719bde0 100644 --- a/.github/workflows/abtesting.yml +++ b/.github/workflows/abtesting.yml @@ -38,10 +38,13 @@ jobs: run: scripts/setup_bundler.sh - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Build and test - run: | - scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseABTesting.podspec \ - --platforms=${{ matrix.target }} + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=${{ matrix.target }} spm: # Don't run on private repo unless it is a PR. @@ -65,8 +68,13 @@ jobs: run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - - name: Unit Tests - run: scripts/third_party/travis/retry.sh ./scripts/build.sh ABTestingUnit ${{ matrix.target }} spm + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/build.sh ABTestingUnit ${{ matrix.target }} spm catalyst: # Don't run on private repo unless it is a PR. @@ -81,8 +89,13 @@ jobs: - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: scripts/setup_bundler.sh - - name: Setup project and Build for Catalyst - run: scripts/test_catalyst.sh FirebaseABTesting test FirebaseABTesting-Unit-unit + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/test_catalyst.sh FirebaseABTesting test FirebaseABTesting-Unit-unit quickstart: # Don't run on private repo unless it is a PR. diff --git a/.github/workflows/auth.yml b/.github/workflows/auth.yml index 43efa7a522c..ab8bbc1ef6f 100644 --- a/.github/workflows/auth.yml +++ b/.github/workflows/auth.yml @@ -44,10 +44,13 @@ jobs: run: scripts/configure_test_keychain.sh - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Build and test - run: | - scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb ${{ matrix.podspec }} --platforms=${{ matrix.target }} \ - ${{ matrix.tests }} + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/pod_lib_lint.rb ${{ matrix.podspec }} --platforms=${{ matrix.target }} ${{ matrix.tests }} integration-tests: # Don't run on private repo unless it is a PR. @@ -84,8 +87,13 @@ jobs: FirebaseAuth/Tests/Sample/SwiftApiTests/Credentials.swift "$plist_secret" - name: Xcode run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer - - name: BuildAndTest # can be replaced with pod lib lint with CocoaPods 1.10 - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh Auth iOS) + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: ([ -z $plist_secret ] || scripts/build.sh Auth iOS) spm: # Don't run on private repo unless it is a PR. @@ -111,8 +119,13 @@ jobs: run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - - name: Unit Tests - run: scripts/third_party/travis/retry.sh ./scripts/build.sh AuthUnit ${{ matrix.target }} ${{ matrix.test }} + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/third_party/travis/retry.sh ./scripts/build.sh AuthUnit ${{ matrix.target }} ${{ matrix.test }} catalyst: # Don't run on private repo unless it is a PR. @@ -126,9 +139,13 @@ jobs: - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: scripts/setup_bundler.sh - - name: Setup project and Build for Catalyst - # Only build the unit tests on Catalyst. Their keychain reliance causes several failures. - run: scripts/test_catalyst.sh FirebaseAuth build FirebaseAuth-Unit-unit + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/test_catalyst.sh FirebaseAuth build FirebaseAuth-Unit-unit quickstart: # Don't run on private repo unless it is a PR. @@ -195,5 +212,10 @@ jobs: run: scripts/setup_bundler.sh - name: Configure test keychain run: scripts/configure_test_keychain.sh - - name: PodLibLint Auth Cron - run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseAuth.podspec --platforms=${{ matrix.target }} ${{ matrix.flags }} + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseAuth.podspec --platforms=${{ matrix.target }} ${{ matrix.flags }} diff --git a/.github/workflows/crashlytics.yml b/.github/workflows/crashlytics.yml index e41daed1ced..ad4f618f40e 100644 --- a/.github/workflows/crashlytics.yml +++ b/.github/workflows/crashlytics.yml @@ -41,10 +41,13 @@ jobs: run: scripts/setup_bundler.sh - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Build and test - run: | - scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseCrashlytics.podspec --platforms=${{ matrix.target }} \ - ${{ matrix.tests }} + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/pod_lib_lint.rb FirebaseCrashlytics.podspec --platforms=${{ matrix.target }} ${{ matrix.tests }} spm: # Don't run on private repo unless it is a PR. @@ -68,9 +71,13 @@ jobs: run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - - name: Unit Tests - run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseCrashlyticsUnit ${{ matrix.target }} spm - + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseCrashlyticsUnit ${{ matrix.target }} spm catalyst: # Don't run on private repo unless it is a PR. @@ -85,8 +92,13 @@ jobs: - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: scripts/setup_bundler.sh - - name: Setup project and Build for Catalyst - run: scripts/test_catalyst.sh FirebaseCrashlytics test FirebaseCrashlytics-Unit-unit + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/test_catalyst.sh FirebaseCrashlytics test FirebaseCrashlytics-Unit-unit quickstart: # Don't run on private repo unless it is a PR. From 3ff2b270eb36081614e887517b9660286d76b2ae Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 7 Feb 2024 15:23:36 -0800 Subject: [PATCH 028/104] Update Roadmap (#12373) --- ROADMAP.md | 104 +--------------------------------------------- SwiftDashboard.md | 70 ------------------------------- 2 files changed, 1 insertion(+), 173 deletions(-) delete mode 100644 SwiftDashboard.md diff --git a/ROADMAP.md b/ROADMAP.md index 5d60f4db0d6..ded212a3e67 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,109 +11,7 @@ contributing to the Firebase iOS SDK. ## Modernization - More Swifty -As we go into 2022, it's a top priority for the Firebase team to improve -usability and functionality for Swift developers. We welcome the community's -input and contribution as we work through this. - -See the [Project Dashboard](SwiftDashboard.md). - -Please upvote existing feature requests, add new feature requests, and send PRs. -* [Example Feature Request](https://github.com/firebase/firebase-ios-sdk/issues/8827) -* [Example Pull Request](https://github.com/firebase/firebase-ios-sdk/pull/6568) - -See [Contributing.md](Contributing.md) for full details about contributing -code to the Firebase repo. - -Thanks in large part to community contributions, we already have several Swift -improvements: -* Analytics - * Enabling [SwiftUI Screen tracking](https://github.com/firebase/firebase-ios-sdk/blob/main/FirebaseAnalyticsSwift/CHANGELOG.md) - automated view logging for SwiftUI apps -* Firestore and RTDB - * Codable Support ([Firestore](https://github.com/firebase/firebase-ios-sdk/pull/3198), - [Database](https://github.com/firebase/firebase-ios-sdk/tree/main/FirebaseDatabaseSwift/Sources/Codable)) - eliminated manual data processing - * [Property wrappers](https://github.com/firebase/firebase-ios-sdk/pull/8408) for Firestore collections dramatically simplified client coding -* Storage - * Eliminated impossible states, provided new and improved async API usage via - [Result type](https://github.com/firebase/firebase-ios-sdk/blob/main/FirebaseStorage/CHANGELOG.md) - and [async/await](https://github.com/firebase/firebase-ios-sdk/blob/main/FirebaseStorage/CHANGELOG.md) - additions -* ML Model Downloader - * Full [SDK implementation in Swift](https://github.com/firebase/firebase-ios-sdk/tree/main/FirebaseMLModelDownloader/Sources) -* In App Messaging - * Vastly simplified usage from SwiftUI with - [SwiftUI modifiers](https://github.com/firebase/firebase-ios-sdk/pull/7496) to show messages and - [preview helpers](https://github.com/firebase/firebase-ios-sdk/pull/8351) - -### Phase 1 - Address Low Hanging Fruit for all Firebase Products -* Swift API tests -* async/await API evaluation, tests, and augmentation -* Fix non-Swifty APIs -* Fill API gaps -* Better Swift Error Handling -* Property Wrappers (Not necessarily low hanging, but can be high value) -* Identify larger projects for future phases - -### APIs - -Continue to evolve the Firebase API surface to be more -Swift-friendly. This is generally done with Swift specific extension libraries. - -[FirebaseFirestoreSwift](Firestore/Swift) is a larger library that adds -Codable support for Firestore. - -Add more such APIs to improve the Firebase Swift API. - -More examples in the -[feature requests](https://github.com/firebase/firebase-ios-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22Swift+API%22). - -### SwiftUI - -Firebase should be better integrated with SwiftUI apps. See SwiftUI related -[issues](https://github.com/firebase/firebase-ios-sdk/issues?q=is%3Aissue+is%3Aopen++label%3ASwiftUI). - -### Swift Async/Await - -Evaluate impact on Firebase APIs of the -[Swift Async/await proposal](https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md). -For example, Objective-C callback APIs that return a value do not get an -async/await API automatically generated and an explicit function may need to be -added. See these -[Firebase Storage examples](https://github.com/firebase/firebase-ios-sdk/blob/main/FirebaseStorage/Sources/AsyncAwait.swift). - -### Combine - -Firebase has community support for Combine (Thanks!). See -[Combine Readme](FirebaseCombineSwift/README.md) for usage and project details. - -## More complete Apple platform support - -Continue to expand the range and quality of Firebase support across -all Apple platforms. - -Expand the -[current non-iOS platform support](README.md#community-supported-efforts) -from community supported to officially supported. - -Fill in the missing pieces of the support matrix, which is -primarily *watchOS* for several libraries. - -## Getting Started - -### Quickstarts - -Modernize the [Swift Quickstarts](https://github.com/firebase/quickstart-ios). -Continue the work done in 2020 and 2021 that used better Swift style, SwiftUI, -Swift Package Manager, async/await APIs, and multi-platform support for -[Analytics](https://github.com/firebase/quickstart-ios/tree/master/analytics), -[ABTesting](https://github.com/firebase/quickstart-ios/tree/master/abtesting), -[Auth](https://github.com/firebase/quickstart-ios/tree/master/authentication), -[Database](https://github.com/firebase/quickstart-ios/tree/master/database), -[Functions](https://github.com/firebase/quickstart-ios/tree/master/functions), -[Performance](https://github.com/firebase/quickstart-ios/tree/master/performance), -and -[RemoteConfig](https://github.com/firebase/quickstart-ios/tree/master/config). +We're continuing a long term journey to migrate from Objective-C to Swift. ## Product Improvements diff --git a/SwiftDashboard.md b/SwiftDashboard.md deleted file mode 100644 index 0484b6dcd91..00000000000 --- a/SwiftDashboard.md +++ /dev/null @@ -1,70 +0,0 @@ -# Firebase Swift Modernization Dashboard - -This dashboard summarizes the status of Firebase's [2022 Swift Modernization Project](ROADMAP.md). -Please upvote or create a [feature request](https://github.com/firebase/firebase-ios-sdk/issues) -to help prioritize any particular cell(s). - -This dashboard is intended to track an initial full Swift review of Firebase along with addressing low-hanging fruit. We would expect it to identify additional follow up -tasks for additional Swift improvements. - -| | An | ApC | ApD | Aut | Cor | Crs | DB | DL | Fst | Fn | IAM | Ins | Msg | MLM | Prf | RC | Str | -| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| **Swift Library** | ✅ | ❌ |❌ | ❌ | n/a | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | -| **Single Module** | ❌ | ✅ |✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | -| **API Tests** | ✅ | ✅ |❌ | ✅ | ✅ | ❌ | ✅ | ❌ | 1 | ✅ | 1 | ✅ | ✅ | 1 | ❌ | ✅ | ✅ | -| **async/await** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | -| **Swift Errors** | ✅ | ✅ | ✅ | 2 | ✅ | 5 | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | 3 | -| **Codable** | n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | ✅ | ✅ | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | -| **SwiftUI Lifecycle** | ❌ | n/a | n/a | ❌ | n/a | n/a | n/a | ❌ | n/a | n/a | n/a | n/a | ❌ | n/a | ❌ | n/a | n/a | -| **SwiftUI Interop** | ✅ | n/a | ❌ | ❌ | n/a | ❌ | ❌ | n/a | ✅ | n/a | ✅ | n/a | n/a | n/a | ❌ | n/a | n/a | -| **Property Wrappers** | n/a | n/a | n/a | ❌ | n/a | n/a | ❌ | n/a | 4 | n/a | n/a | n/a | n/a | n/a | n/a | ✅ | n/a | -| **Swift Doc Scrub** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅| - -### Other Projects -- Tooling to surface full list of automatically generated Swift API from Objective-C and validate. -- Improve singleton naming scheme. Move singletons into a Firebase namespace, like `Firebase.auth()`, `Firebase.storage()`, etc. -- Swift Generics. Update APIs that are using weakly typed information to use proper generics. - -## Notes -1. Tests exist. Coverage to be confirmed. -2. `NS_ERROR_ENUM` used but a larger audit is still needed for more localized errors. -3. Still needs to unify Objective-C and Swift errors. -4. One property wrapper added in [#8614](https://github.com/firebase/firebase-ios-sdk/pull/8614). More to go. -5. `record(Error)` API should be expanded to collect Swift Errors as well as NSErrors. - -## Rows (Swift Capabilities) -* **Swift Library**: SDK includes public APIs written in Swift, either in the main product library or a Swift-specific extension. -* **Single Module**: Public API surface in a single module. -* **API Tests**: Tests exist for all Swift APIs. Integration tests are preferred, but compile-only tests are acceptable. -* **async/await**:API tests include tests for all auto-generated async/await APIs. Implementations are added for -asynchronous APIs that don't have auto-generated counterparts like -[these](https://github.com/firebase/firebase-ios-sdk/blob/main/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift) -for Storage. -* **Swift Errors**: Swift Error Codes are available instead of NSErrors. -* **Codable**: Codable is implemented where appropriate. -* **SwiftUI Lifecycle**: Dependencies on the AppDelegate Lifecycle are migrated to the Multicast AppDelegate. -* **SwiftUI Interop**: Update APIs that include UIViewControllers (or implementations that depend on them) to work with SwiftUI. This will overlap with -Property Wrappers and likely the SwiftUI lifecycle bits, but an audit and improvements could likely be made. The existing FIAM and Analytics View modifier -APIs would fit into this category. -* **Property Wrappers**: Property wrappers are used to improve the API. -* **Swift Doc Scrub**: Review and update to change Objective-C types and call examples to Swift. In addition to updating the documentation content, we -should also investigate using DocC to format the docs. - -## Columns (Firebase Products) -* An - Analytics -* ApC - App Check -* ApD - App Distribution -* Aut - Auth -* Cor - Core -* Crs - Crashlytics -* DB - Real-time Database -* DL - Dynamic Links -* Fst - Firestore -* Fn - Functions -* IAM - In App Messaging -* Ins - Installations -* Msg - Messaging -* MLM - MLModel Downloader -* Prf - Performance -* RC - Remote Config -* Str - Storage From cd1ee8d2a4be4ca406477a8f7d1b1f8515fe0a37 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 7 Feb 2024 19:34:54 -0500 Subject: [PATCH 029/104] Remove placeholder token constant from Firebase App Check (#12374) --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index f1ccdab2b6d..3c6001ecdf1 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -48,8 +48,6 @@ static id _providerFactory; -static NSString *const kDummyFACTokenValue = @"eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ=="; - @interface FIRAppCheck () @property(class, nullable) id providerFactory; From 7b9125a2e99529e48a2e19dd6892c950e3dda58d Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 8 Feb 2024 16:41:30 -0500 Subject: [PATCH 030/104] Update links to `main` branches (#12375) --- Firestore/Source/Public/FirebaseFirestore/FIRTimestamp.h | 2 +- Firestore/core/include/firebase/firestore/timestamp.h | 2 +- Firestore/core/test/unit/FSTGoogleTestTests.mm | 2 +- scripts/update_xcode_target.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRTimestamp.h b/Firestore/Source/Public/FirebaseFirestore/FIRTimestamp.h index 8ba13a3a128..375f697338a 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRTimestamp.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRTimestamp.h @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN * 9999-12-31T23:59:59.999999999Z. By restricting to that range, we ensure that we can convert to * and from RFC 3339 date strings. * - * @see https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto for the + * @see https://github.com/google/protobuf/blob/main/src/google/protobuf/timestamp.proto for the * reference timestamp definition. */ NS_SWIFT_NAME(Timestamp) diff --git a/Firestore/core/include/firebase/firestore/timestamp.h b/Firestore/core/include/firebase/firestore/timestamp.h index 286da1149f1..0ab818532f8 100644 --- a/Firestore/core/include/firebase/firestore/timestamp.h +++ b/Firestore/core/include/firebase/firestore/timestamp.h @@ -38,7 +38,7 @@ namespace firebase { * from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. * * @see - * https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto + * https://github.com/google/protobuf/blob/main/src/google/protobuf/timestamp.proto */ class Timestamp { public: diff --git a/Firestore/core/test/unit/FSTGoogleTestTests.mm b/Firestore/core/test/unit/FSTGoogleTestTests.mm index 244875a1614..f0120857d76 100644 --- a/Firestore/core/test/unit/FSTGoogleTestTests.mm +++ b/Firestore/core/test/unit/FSTGoogleTestTests.mm @@ -139,7 +139,7 @@ @interface GoogleTests : XCTestCase * These members are then joined with a ":" as googletest requires. * * @see - * https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md + * https://github.com/google/googletest/blob/main/docs/advanced.md */ NSString* CreateTestFiltersFromTestsToRun(NSSet* testsToRun) { NSMutableString* result = [[NSMutableString alloc] init]; diff --git a/scripts/update_xcode_target.rb b/scripts/update_xcode_target.rb index f4eb2d661bb..f9f58387f90 100755 --- a/scripts/update_xcode_target.rb +++ b/scripts/update_xcode_target.rb @@ -15,7 +15,7 @@ # limitations under the License. # Script to add a file to an Xcode target. -# Adapted from https://github.com/firebase/quickstart-ios/blob/master/scripts/info_script.rb +# Adapted from https://github.com/firebase/quickstart-ios/blob/main/scripts/info_script.rb require 'xcodeproj' project_path = ARGV[0] From d049a9d47f32f273cfc536e40b60d83dcb6c1242 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 9 Feb 2024 15:24:05 -0800 Subject: [PATCH 031/104] [auth-swift] Reimplement AuthTestingSupport in Swift (#12377) --- .../AuthProvider/PhoneAuthProvider.swift | 2 +- FirebaseAuthTestingSupport.podspec | 21 +++------- .../Auth/Sources/FIRPhoneAuthProviderFake.m | 32 --------------- ...iderFake.h => PhoneAuthProviderFake.swift} | 39 +++++++++---------- .../Tests/PhoneAuthProviderFakeTests.swift | 30 ++++++++++---- 5 files changed, 49 insertions(+), 75 deletions(-) delete mode 100644 FirebaseTestingSupport/Auth/Sources/FIRPhoneAuthProviderFake.m rename FirebaseTestingSupport/Auth/Sources/{Public/FirebaseAuthTestingSupport/FIRPhoneAuthProviderFake.h => PhoneAuthProviderFake.swift} (50%) diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift index 8a12d7cb5cf..61a78271347 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift @@ -478,7 +478,7 @@ import Foundation private let callbackScheme: String private let usingClientIDScheme: Bool - private init(auth: Auth) { + init(auth: Auth) { self.auth = auth if let clientID = auth.app?.options.clientID { let reverseClientIDScheme = clientID.components(separatedBy: ".").reversed() diff --git a/FirebaseAuthTestingSupport.podspec b/FirebaseAuthTestingSupport.podspec index 2bec66c180a..f581d15d40e 100644 --- a/FirebaseAuthTestingSupport.podspec +++ b/FirebaseAuthTestingSupport.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuthTestingSupport' - s.version = '1.0.0' + s.version = '2.0.0' s.summary = 'Firebase SDKs testing support types and utilities.' s.description = <<-DESC @@ -17,10 +17,10 @@ Pod::Spec.new do |s| :tag => 'CocoaPods-' + s.version.to_s } - ios_deployment_target = '11.0' + ios_deployment_target = '13.0' osx_deployment_target = '10.13' - tvos_deployment_target = '12.0' - watchos_deployment_target = '6.0' + tvos_deployment_target = '13.0' + watchos_deployment_target = '7.0' s.swift_version = '5.3' @@ -36,19 +36,10 @@ Pod::Spec.new do |s| base_dir = 'FirebaseTestingSupport/Auth/' s.source_files = [ - base_dir + 'Sources/**/*.{m,mm,h}', + base_dir + 'Sources/**/*.swift', ] - s.public_header_files = base_dir + '**/*.h' - - s.dependency 'FirebaseAuth', '~> 10.0' - - s.pod_target_xcconfig = { - 'GCC_C_LANGUAGE_STANDARD' => 'c99', - 'OTHER_CFLAGS' => '-fno-autolink', - 'HEADER_SEARCH_PATHS' => - '"${PODS_TARGET_SRCROOT}" ' - } + s.dependency 'FirebaseAuth', '~> 10.22' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } diff --git a/FirebaseTestingSupport/Auth/Sources/FIRPhoneAuthProviderFake.m b/FirebaseTestingSupport/Auth/Sources/FIRPhoneAuthProviderFake.m deleted file mode 100644 index 6a93e51ad27..00000000000 --- a/FirebaseTestingSupport/Auth/Sources/FIRPhoneAuthProviderFake.m +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021 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. - -#import "FirebaseTestingSupport/Auth/Sources/Public/FirebaseAuthTestingSupport/FIRPhoneAuthProviderFake.h" - -@implementation FIRPhoneAuthProviderFake - -- (instancetype)init { - // The object is partially initialized. Make sure the methods used during testing are overridden. - return self; -} - -- (void)verifyPhoneNumber:(NSString *)phoneNumber - UIDelegate:(id)UIDelegate - completion:(FIRVerificationResultCallback)completion { - if (self.verifyPhoneNumberHandler) { - self.verifyPhoneNumberHandler(completion); - } -} - -@end diff --git a/FirebaseTestingSupport/Auth/Sources/Public/FirebaseAuthTestingSupport/FIRPhoneAuthProviderFake.h b/FirebaseTestingSupport/Auth/Sources/PhoneAuthProviderFake.swift similarity index 50% rename from FirebaseTestingSupport/Auth/Sources/Public/FirebaseAuthTestingSupport/FIRPhoneAuthProviderFake.h rename to FirebaseTestingSupport/Auth/Sources/PhoneAuthProviderFake.swift index 3cded28cff8..fa6e9d9aa4f 100644 --- a/FirebaseTestingSupport/Auth/Sources/Public/FirebaseAuthTestingSupport/FIRPhoneAuthProviderFake.h +++ b/FirebaseTestingSupport/Auth/Sources/PhoneAuthProviderFake.swift @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// 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. @@ -12,24 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -#import - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^FIRVerifyPhoneNumberHandler)(FIRVerificationResultCallback completion); +@testable import FirebaseAuth +import Foundation /// A fake object to replace a real `AuthAPNSTokenManager` in tests. -NS_SWIFT_NAME(PhoneAuthProviderFake) -@interface FIRPhoneAuthProviderFake : FIRPhoneAuthProvider - -- (instancetype)init; - -/// The block to be called each time when `verifyPhoneNumber(_:uiDelegate:completion:)` method is -/// called. -@property(nonatomic, nullable, copy) FIRVerifyPhoneNumberHandler verifyPhoneNumberHandler; - -// TODO: Implement other handlers as needed. - -@end - -NS_ASSUME_NONNULL_END +public class PhoneAuthProviderFake: PhoneAuthProvider { + override init(auth: Auth) { + super.init(auth: auth) + } + + var verifyPhoneNumberHandler: (((String?, Error?) -> Void) -> Void)? + + override public func verifyPhoneNumber(_ phoneNumber: String, + uiDelegate: AuthUIDelegate? = nil, + completion: ((_: String?, _: Error?) -> Void)?) { + if let verifyPhoneNumberHandler, + let completion { + verifyPhoneNumberHandler(completion) + } + } +} diff --git a/FirebaseTestingSupport/Auth/Tests/PhoneAuthProviderFakeTests.swift b/FirebaseTestingSupport/Auth/Tests/PhoneAuthProviderFakeTests.swift index 388cd178f41..7cb52267c6e 100644 --- a/FirebaseTestingSupport/Auth/Tests/PhoneAuthProviderFakeTests.swift +++ b/FirebaseTestingSupport/Auth/Tests/PhoneAuthProviderFakeTests.swift @@ -12,31 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. +@testable import FirebaseAuth @testable import FirebaseAuthTestingSupport +import FirebaseCore import Foundation import XCTest class PhoneAuthProviderFakeTests: XCTestCase { + var auth: Auth! + static var testNum = 0 + override func setUp() { + super.setUp() + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = "TEST_API_KEY" + options.projectID = "myProjectID" + PhoneAuthProviderFakeTests.testNum = PhoneAuthProviderFakeTests.testNum + 1 + let name = "test-name\(PhoneAuthProviderFakeTests.testNum)" + FirebaseApp.configure(name: name, options: options) + auth = Auth( + app: FirebaseApp.app(name: name)! + ) + } + func testPhoneAuthProviderFakeConstructor() throws { - let fakePhoneAuthProvider = PhoneAuthProviderFake() + let fakePhoneAuthProvider = PhoneAuthProviderFake(auth: auth) XCTAssertNotNil(fakePhoneAuthProvider) - XCTAssertTrue(fakePhoneAuthProvider.isKind(of: PhoneAuthProvider.self)) } func testVerifyPhoneNumberHandler() { - let fakePhoneAuthProvider = PhoneAuthProviderFake() + let fakePhoneAuthProvider = PhoneAuthProviderFake(auth: auth) let handlerExpectation = expectation(description: "Handler called") fakePhoneAuthProvider.verifyPhoneNumberHandler = { completion in handlerExpectation.fulfill() - - completion(nil, nil) + completion("test-id", nil) } let completionExpectation = expectation(description: "Completion called") - fakePhoneAuthProvider.verifyPhoneNumber("", uiDelegate: nil) { verficationID, error in + fakePhoneAuthProvider.verifyPhoneNumber("", uiDelegate: nil) { verificationID, error in completionExpectation.fulfill() - XCTAssertNil(verficationID) + XCTAssertEqual(verificationID, "test-id") XCTAssertNil(error) } From 998e04be93cc334cbeff96f432b56df46055021c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 10 Feb 2024 17:16:56 -0800 Subject: [PATCH 032/104] [auth-swift] Include Swift-generated ObjC Auth types via Firebase.h (#12379) --- CoreOnly/Sources/Firebase.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CoreOnly/Sources/Firebase.h b/CoreOnly/Sources/Firebase.h index 8a7420d42c3..41a52e48e5f 100755 --- a/CoreOnly/Sources/Firebase.h +++ b/CoreOnly/Sources/Firebase.h @@ -32,6 +32,8 @@ #if __has_include() #import + #import + #import #endif #if __has_include() From 2c76938316df35db9f8f5aa69bfeacfac40a8a07 Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:41:56 -0500 Subject: [PATCH 033/104] Bug fix for flaky behaviour when using Map in arrayRemove (#12378) --- Firestore/CHANGELOG.md | 3 + Firestore/core/src/model/value_util.cc | 114 ++++++++---------- Firestore/core/src/model/value_util.h | 4 + .../core/test/unit/model/value_util_test.cc | 18 +++ 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index c05338877fd..afa0f58da9a 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) + # 10.21.0 - Add an error when trying to build Firestore's binary SPM distribution for visionOS (#12279). See Firestore's 10.12.0 release note for a supported diff --git a/Firestore/core/src/model/value_util.cc b/Firestore/core/src/model/value_util.cc index 012c36955a7..61c4a8c865f 100644 --- a/Firestore/core/src/model/value_util.cc +++ b/Firestore/core/src/model/value_util.cc @@ -109,19 +109,22 @@ void SortFields(google_firestore_v1_ArrayValue& value) { } } +void SortFields(google_firestore_v1_MapValue& value) { + std::sort(value.fields, value.fields + value.fields_count, + [](const google_firestore_v1_MapValue_FieldsEntry& lhs, + const google_firestore_v1_MapValue_FieldsEntry& rhs) { + return nanopb::MakeStringView(lhs.key) < + nanopb::MakeStringView(rhs.key); + }); + + for (pb_size_t i = 0; i < value.fields_count; ++i) { + SortFields(value.fields[i].value); + } +} + void SortFields(google_firestore_v1_Value& value) { if (IsMap(value)) { - google_firestore_v1_MapValue& map_value = value.map_value; - std::sort(map_value.fields, map_value.fields + map_value.fields_count, - [](const google_firestore_v1_MapValue_FieldsEntry& lhs, - const google_firestore_v1_MapValue_FieldsEntry& rhs) { - return nanopb::MakeStringView(lhs.key) < - nanopb::MakeStringView(rhs.key); - }); - - for (pb_size_t i = 0; i < map_value.fields_count; ++i) { - SortFields(map_value.fields[i].value); - } + SortFields(value.map_value); } else if (IsArray(value)) { SortFields(value.array_value); } @@ -223,30 +226,31 @@ ComparisonResult CompareArrays(const google_firestore_v1_Value& left, right.array_value.values_count); } -ComparisonResult CompareObjects(const google_firestore_v1_Value& left, - const google_firestore_v1_Value& right) { - google_firestore_v1_MapValue left_map = left.map_value; - google_firestore_v1_MapValue right_map = right.map_value; +ComparisonResult CompareMaps(const google_firestore_v1_MapValue& left, + const google_firestore_v1_MapValue& right) { + // Sort the given MapValues + auto left_map = DeepClone(left); + auto right_map = DeepClone(right); + SortFields(*left_map); + SortFields(*right_map); - // Porting Note: MapValues in iOS are always kept in sorted order. We - // therefore do no need to sort them before comparing. - for (pb_size_t i = 0; i < left_map.fields_count && i < right_map.fields_count; - ++i) { - ComparisonResult key_cmp = - util::Compare(nanopb::MakeStringView(left_map.fields[i].key), - nanopb::MakeStringView(right_map.fields[i].key)); + for (pb_size_t i = 0; + i < left_map->fields_count && i < right_map->fields_count; ++i) { + const ComparisonResult key_cmp = + util::Compare(nanopb::MakeStringView(left_map->fields[i].key), + nanopb::MakeStringView(right_map->fields[i].key)); if (key_cmp != ComparisonResult::Same) { return key_cmp; } - ComparisonResult value_cmp = - Compare(left_map.fields[i].value, right.map_value.fields[i].value); + const ComparisonResult value_cmp = + Compare(left_map->fields[i].value, right_map->fields[i].value); if (value_cmp != ComparisonResult::Same) { return value_cmp; } } - return util::Compare(left_map.fields_count, right_map.fields_count); + return util::Compare(left_map->fields_count, right_map->fields_count); } ComparisonResult Compare(const google_firestore_v1_Value& left, @@ -291,7 +295,7 @@ ComparisonResult Compare(const google_firestore_v1_Value& left, return CompareArrays(left, right); case TypeOrder::kMap: - return CompareObjects(left, right); + return CompareMaps(left.map_value, right.map_value); case TypeOrder::kMaxValue: return util::ComparisonResult::Same; @@ -366,26 +370,12 @@ bool ArrayEquals(const google_firestore_v1_ArrayValue& left, return true; } -bool ObjectEquals(const google_firestore_v1_MapValue& left, - const google_firestore_v1_MapValue& right) { +bool MapValueEquals(const google_firestore_v1_MapValue& left, + const google_firestore_v1_MapValue& right) { if (left.fields_count != right.fields_count) { return false; } - - // Porting Note: MapValues in iOS are always kept in sorted order. We - // therefore do no need to sort them before comparing. - for (size_t i = 0; i < right.fields_count; ++i) { - if (nanopb::MakeStringView(left.fields[i].key) != - nanopb::MakeStringView(right.fields[i].key)) { - return false; - } - - if (left.fields[i].value != right.fields[i].value) { - return false; - } - } - - return true; + return CompareMaps(left, right) == ComparisonResult::Same; } bool Equals(const google_firestore_v1_Value& lhs, @@ -436,10 +426,10 @@ bool Equals(const google_firestore_v1_Value& lhs, return ArrayEquals(lhs.array_value, rhs.array_value); case TypeOrder::kMap: - return ObjectEquals(lhs.map_value, rhs.map_value); + return MapValueEquals(lhs.map_value, rhs.map_value); case TypeOrder::kMaxValue: - return ObjectEquals(lhs.map_value, rhs.map_value); + return MapValueEquals(lhs.map_value, rhs.map_value); default: HARD_FAIL("Invalid type value: %s", left_type); @@ -794,27 +784,11 @@ Message DeepClone( break; case google_firestore_v1_Value_array_value_tag: - target->array_value.values_count = source.array_value.values_count; - target->array_value.values = nanopb::MakeArray( - source.array_value.values_count); - for (pb_size_t i = 0; i < source.array_value.values_count; ++i) { - target->array_value.values[i] = - *DeepClone(source.array_value.values[i]).release(); - } + target->array_value = *DeepClone(source.array_value).release(); break; case google_firestore_v1_Value_map_value_tag: - target->map_value.fields_count = source.map_value.fields_count; - target->map_value.fields = - nanopb::MakeArray( - source.map_value.fields_count); - for (pb_size_t i = 0; i < source.map_value.fields_count; ++i) { - target->map_value.fields[i].key = - nanopb::MakeBytesArray(source.map_value.fields[i].key->bytes, - source.map_value.fields[i].key->size); - target->map_value.fields[i].value = - *DeepClone(source.map_value.fields[i].value).release(); - } + target->map_value = *DeepClone(source.map_value).release(); break; } return target; @@ -832,6 +806,20 @@ Message DeepClone( return target; } +Message DeepClone( + const google_firestore_v1_MapValue& source) { + Message target{source}; + target->fields_count = source.fields_count; + target->fields = nanopb::MakeArray( + source.fields_count); + for (pb_size_t i = 0; i < source.fields_count; ++i) { + target->fields[i].key = nanopb::MakeBytesArray(source.fields[i].key->bytes, + source.fields[i].key->size); + target->fields[i].value = *DeepClone(source.fields[i].value).release(); + } + return target; +} + } // namespace model } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/model/value_util.h b/Firestore/core/src/model/value_util.h index c39fc8da39b..91e26a21ebb 100644 --- a/Firestore/core/src/model/value_util.h +++ b/Firestore/core/src/model/value_util.h @@ -183,6 +183,10 @@ nanopb::Message DeepClone( nanopb::Message DeepClone( const google_firestore_v1_ArrayValue& source); +/** Creates a copy of the contents of the MapValue proto. */ +nanopb::Message DeepClone( + const google_firestore_v1_MapValue& source); + /** Returns true if `value` is a INTEGER_VALUE. */ inline bool IsInteger(const absl::optional& value) { return value && diff --git a/Firestore/core/test/unit/model/value_util_test.cc b/Firestore/core/test/unit/model/value_util_test.cc index 50ee1b1add2..d4db43dfe20 100644 --- a/Firestore/core/test/unit/model/value_util_test.cc +++ b/Firestore/core/test/unit/model/value_util_test.cc @@ -545,6 +545,24 @@ TEST_F(ValueUtilTest, DeepClone) { VerifyDeepClone(Map("a", Array("b", Map("c", GeoPoint(30, 60))))); } +TEST_F(ValueUtilTest, CompareMaps) { + auto left_1 = Map("a", 7, "b", 0); + auto right_1 = Map("a", 7, "b", 0); + EXPECT_EQ(model::Compare(*left_1, *right_1), ComparisonResult::Same); + + auto left_2 = Map("a", 3, "b", 5); + auto right_2 = Map("b", 5, "a", 3); + EXPECT_EQ(model::Compare(*left_2, *right_2), ComparisonResult::Same); + + auto left_3 = Map("a", 8, "b", 10, "c", 5); + auto right_3 = Map("a", 8, "b", 10); + EXPECT_EQ(model::Compare(*left_3, *right_3), ComparisonResult::Descending); + + auto left_4 = Map("a", 7, "b", 0); + auto right_4 = Map("a", 7, "b", 10); + EXPECT_EQ(model::Compare(*left_4, *right_4), ComparisonResult::Ascending); +} + } // namespace } // namespace model From ab0d0854a3682f14c0ee26859469cb4b1636d5e1 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 14 Feb 2024 10:03:34 -0500 Subject: [PATCH 034/104] Make `AuthInterop` and `AppCheckInterop` properties optional in Storage (#12387) --- FirebaseStorage/Sources/Storage.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseStorage/Sources/Storage.swift b/FirebaseStorage/Sources/Storage.swift index c4590bd28e7..79d18dfa4f6 100644 --- a/FirebaseStorage/Sources/Storage.swift +++ b/FirebaseStorage/Sources/Storage.swift @@ -306,8 +306,8 @@ import FirebaseCore private static func initFetcherServiceForApp(_ app: FirebaseApp, _ bucket: String, - _ auth: AuthInterop, - _ appCheck: AppCheckInterop) + _ auth: AuthInterop?, + _ appCheck: AppCheckInterop?) -> GTMSessionFetcherService { objc_sync_enter(fetcherServiceLock) defer { objc_sync_exit(fetcherServiceLock) } @@ -334,8 +334,8 @@ import FirebaseCore return fetcherService! } - private let auth: AuthInterop - private let appCheck: AppCheckInterop + private let auth: AuthInterop? + private let appCheck: AppCheckInterop? private let storageBucket: String private var usesEmulator: Bool = false var host: String From ca77625a3263da3a1b680b09c16a558dd2874e5f Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:22:45 -0500 Subject: [PATCH 035/104] Revert "[Release Tooling] Stop including 'Info.plist's in static xcframeworks (#12243)" (#12396) --- FirebaseCore/CHANGELOG.md | 4 +++- .../Sources/ZipBuilder/CarthageUtils.swift | 3 +++ .../Sources/ZipBuilder/FrameworkBuilder.swift | 17 +++++++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index e406249fa6d..2b9caf3f31f 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,6 +1,8 @@ # Unreleased - [Swift Package Manager] Firebase now enforces a Swift 5.7.1 minimum version, which is aligned with the Xcode 14.1 minimum. (#12350) +- Revert Firebase 10.20.0 change that removed `Info.plist` files from + static xcframeworks (#12390). # Firebase 10.21.0 - Firebase now requires at least CocoaPods version 1.12.0 to enable privacy @@ -9,7 +11,7 @@ # Firebase 10.20.0 - The following change only applies to those using a binary distribution of a Firebase SDK(s): In preparation for supporting Privacy Manifests, each - platform framework directory within a static xcframewok no longer contains + platform framework directory within a static xcframework no longer contains an `Info.plist` file (#12243). # Firebase 10.14.0 diff --git a/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift b/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift index bb17e0e1fa8..1e934c25382 100644 --- a/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift +++ b/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift @@ -241,6 +241,9 @@ extension CarthageUtils { } catch { fatalError("Couldn't copy dummy library for Firebase framework in Carthage. \(error)") } + + // Write the Info.plist. + generatePlistContents(forName: "Firebase", withVersion: version, to: frameworkDir) } static func generatePlistContents(forName name: String, diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index 3f4e7172e9a..09b86c133d7 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -380,6 +380,10 @@ struct FrameworkBuilder { } umbrellaHeader = umbrellaHeaderURL.lastPathComponent } + // Add an Info.plist. Required by Carthage and SPM binary xcframeworks. + CarthageUtils.generatePlistContents(forName: frameworkName, + withVersion: podInfo.version, + to: frameworkDir) // TODO: copy PrivateHeaders directory as well if it exists. SDWebImage is an example pod. @@ -590,8 +594,8 @@ struct FrameworkBuilder { /// Groups slices for each platform into a minimal set of frameworks. /// - Parameter withName: The framework name. /// - Parameter isCarthage: Name the temp directory differently for Carthage. - /// - Parameter fromFolder: The almost complete framework folder. Includes - /// Headers, and Resources. + /// - Parameter fromFolder: The almost complete framework folder. Includes Headers, Info.plist, + /// and Resources. /// - Parameter slicedFrameworks: All the frameworks sliced by platform. /// - Parameter moduleMapContents: Module map contents for all frameworks in this pod. private func groupFrameworks(withName framework: String, @@ -684,6 +688,15 @@ struct FrameworkBuilder { fatalError("Could not create framework directory needed to build \(framework): \(error)") } + // Info.plist from `fromFolder` + do { + let infoPlistSrc = fromFolder.appendingPathComponent("Info.plist").resolvingSymlinksInPath() + let infoPlistDst = platformFrameworkDir.appendingPathComponent("Info.plist") + try fileManager.copyItem(at: infoPlistSrc, to: infoPlistDst) + } catch { + fatalError("Could not create framework directory needed to build \(framework): \(error)") + } + // Copy the binary to the right location. let binaryName = frameworkPath.lastPathComponent.replacingOccurrences(of: ".framework", with: "") From 4a8f84baf08059f4114223d8489041c4c9b0f609 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 16 Feb 2024 08:34:09 -0800 Subject: [PATCH 036/104] [FIAM] Fix an objc_retain crash (#12395) --- FirebaseInAppMessaging/CHANGELOG.md | 3 +++ .../Sources/Flows/FIRIAMMessageClientCache.m | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/FirebaseInAppMessaging/CHANGELOG.md b/FirebaseInAppMessaging/CHANGELOG.md index e8825c0ee6a..82a46fddf1e 100644 --- a/FirebaseInAppMessaging/CHANGELOG.md +++ b/FirebaseInAppMessaging/CHANGELOG.md @@ -1,3 +1,6 @@ +# 10.22.0 +- [fixed] Fixed an `objc_retain` crash. (#12393) + # 10.17.0 - [deprecated] All of the public API from `FirebaseInAppMessagingSwift` can now be accessed through the `FirebaseInAppMessaging` module. Therefore, diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m index 7c4e4bcc3e1..37f24531edd 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m @@ -126,7 +126,9 @@ - (void)setupAnalyticsEventListening { } - (BOOL)hasTestMessage { - return self.testMessages.count > 0; + @synchronized(self) { + return self.testMessages.count > 0; + } } - (nullable FIRIAMMessageDefinition *)nextOnAppLaunchDisplayMsg { @@ -135,7 +137,7 @@ - (nullable FIRIAMMessageDefinition *)nextOnAppLaunchDisplayMsg { - (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg { @synchronized(self) { - // always first check test message which always have higher prirority + // always first check test message which always have higher priority if (self.testMessages.count > 0) { FIRIAMMessageDefinition *testMessage = self.testMessages[0]; // always remove test message right away when being fetched for display From 3de7e6c29dbc7c5c30eb6b702e9f50280830f99b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 16 Feb 2024 09:25:07 -0800 Subject: [PATCH 037/104] Disable flaky Storage ObjC quickstart on Xcode 15.2 (#12399) --- .github/workflows/storage.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/storage.yml b/.github/workflows/storage.yml index 4585647f128..d5ae8073ddf 100644 --- a/.github/workflows/storage.yml +++ b/.github/workflows/storage.yml @@ -112,12 +112,17 @@ jobs: quickstart: # Don't run on private repo unless it is a PR. if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' + # TODO: See #12399 and restore Objective-C testing for Xcode 15 if GHA is fixed. strategy: matrix: include: - os: macos-12 xcode: Xcode_14.2 - - os: macos-13 + - swift: swift + os: macos-13 + xcode: Xcode_15.2 + - swift: swift + os: macos-13 xcode: Xcode_15.2 env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} @@ -134,10 +139,8 @@ jobs: quickstart-ios/storage/GoogleService-Info.plist "$plist_secret" - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Test objc quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true swift) + - name: Test quickstart + run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true ${{ matrix.swift }}) quickstart-ftl-cron-only: # Don't run on private repo. From b4d4586aaab7208b6c7ecbaf80429aac0351a2dc Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 20 Feb 2024 17:24:46 -0800 Subject: [PATCH 038/104] [auth-swift] Consistent structure for FirebaseAuthInterop (#12384) --- .github/workflows/storage.yml | 4 +-- CoreOnly/Sources/Firebase.h | 9 ++++-- FirebaseAuth.podspec | 2 +- FirebaseAuth/Interop/CMakeLists.txt | 2 +- .../FirebaseAuthInterop}/FIRAuthInterop.h | 0 FirebaseAuth/Sources/Swift/Auth/Auth.swift | 2 +- .../Sources/Swift/Auth/AuthComponent.swift | 31 +++++++++---------- FirebaseAuth/Sources/Swift/User/User.swift | 12 ++----- .../Sources/Swift/User/UserInfo.swift | 2 +- .../Sources/Swift/User/UserInfoImpl.swift | 27 +++++++++------- FirebaseAuth/Tests/Unit/SwiftAPI.swift | 8 ++--- FirebaseAuthInterop.podspec | 4 +-- FirebaseDatabase/Sources/Api/FIRDatabase.m | 2 +- .../Sources/Api/FIRDatabaseComponent.m | 2 +- .../FIRDatabaseConnectionContextProvider.m | 2 +- FirebaseDatabase/Tests/Helpers/FTestHelpers.m | 2 +- .../Tests/Integration/FIRAuthTests.m | 2 +- FirebaseStorage.podspec | 8 ++--- .../FIRStorageIntegrationTests.m | 3 +- Firestore/Source/API/FSTFirestoreComponent.mm | 2 +- ...irebase_auth_credentials_provider_apple.mm | 2 +- ...firebase_auth_credentials_provider_test.mm | 2 +- .../ClientApp.xcodeproj/project.pbxproj | 30 +++++++++--------- IntegrationTesting/ClientApp/Podfile | 4 +-- .../Shared-iOS12+/objc-module-import-test.m | 1 - .../Shared-iOS12+/swift-import-test.swift | 1 - .../Shared-iOS13+/objc-header-import-test.m | 12 +++++++ .../Shared-iOS13+/objc-module-import-test.m | 1 + .../objcxx-header-import-test.mm | 13 ++++++++ .../Shared-iOS13+/swift-import-test.swift | 1 + Package.swift | 2 +- SharedTestUtilities/FIRAuthInteropFake.h | 2 +- SharedTestUtilities/FIRAuthInteropFake.m | 2 +- 33 files changed, 111 insertions(+), 88 deletions(-) rename FirebaseAuth/Interop/{ => Public/FirebaseAuthInterop}/FIRAuthInterop.h (100%) diff --git a/.github/workflows/storage.yml b/.github/workflows/storage.yml index 4585647f128..2ff4e8dd919 100644 --- a/.github/workflows/storage.yml +++ b/.github/workflows/storage.yml @@ -134,8 +134,8 @@ jobs: quickstart-ios/storage/GoogleService-Info.plist "$plist_secret" - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Test objc quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true) + # - name: Test objc quickstart + # run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true) - name: Test swift quickstart run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true swift) diff --git a/CoreOnly/Sources/Firebase.h b/CoreOnly/Sources/Firebase.h index 41a52e48e5f..37d5f9e495c 100755 --- a/CoreOnly/Sources/Firebase.h +++ b/CoreOnly/Sources/Firebase.h @@ -32,8 +32,13 @@ #if __has_include() #import - #import - #import + #if __has_include("FirebaseAuth-umbrella.h") + #if __has_include() + #import + #endif + #import + #import + #endif #endif #if __has_include() diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index b625a9f4dc6..22b4e9603d6 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -53,7 +53,7 @@ supports email and password accounts, as well as several 3rd party authenticatio } s.framework = 'Security' s.ios.framework = 'SafariServices' - s.dependency 'FirebaseAuthInterop', '~> 10.9' + s.dependency 'FirebaseAuthInterop', '~> 10.22' s.dependency 'FirebaseAppCheckInterop', '~> 10.17' s.dependency 'FirebaseCore', '~> 10.0' s.dependency 'FirebaseCoreExtension', '~> 10.0' diff --git a/FirebaseAuth/Interop/CMakeLists.txt b/FirebaseAuth/Interop/CMakeLists.txt index e38b5b5aa87..f5107b1e7b6 100644 --- a/FirebaseAuth/Interop/CMakeLists.txt +++ b/FirebaseAuth/Interop/CMakeLists.txt @@ -16,7 +16,7 @@ if(NOT APPLE) return() endif() -file(GLOB headers Public/*.h) +file(GLOB headers Public/FirebaseAuthInterop/*.h) firebase_ios_generate_dummy_source(FirebaseAuthInterop sources) firebase_ios_add_framework( diff --git a/FirebaseAuth/Interop/FIRAuthInterop.h b/FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h similarity index 100% rename from FirebaseAuth/Interop/FIRAuthInterop.h rename to FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index a67ebe77c2a..8c9e92a7bd6 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -168,7 +168,7 @@ extension Auth: AuthInterop { /// - Parameter app: The app for which to retrieve the associated `Auth` instance. /// - Returns: The `Auth` instance associated with the given app. @objc open class func auth(app: FirebaseApp) -> Auth { - return ComponentType.instance(for: AuthProvider.self, in: app.container).auth() + return ComponentType.instance(for: AuthInterop.self, in: app.container) as! Auth } /// Gets the `FirebaseApp` object that this auth object is connected to. diff --git a/FirebaseAuth/Sources/Swift/Auth/AuthComponent.swift b/FirebaseAuth/Sources/Swift/Auth/AuthComponent.swift index 277fcce76b1..aa73a11fbc4 100644 --- a/FirebaseAuth/Sources/Swift/Auth/AuthComponent.swift +++ b/FirebaseAuth/Sources/Swift/Auth/AuthComponent.swift @@ -12,19 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation + import FirebaseAppCheckInterop +import FirebaseAuthInterop import FirebaseCore import FirebaseCoreExtension -import Foundation - -@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) -@objc(FIRAuthProvider) protocol AuthProvider { - @objc func auth() -> Auth -} @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRAuthComponent) -class AuthComponent: NSObject, Library, AuthProvider, ComponentLifecycleMaintainer { +class AuthComponent: NSObject, Library, ComponentLifecycleMaintainer { // MARK: - Private Variables /// The app associated with all Auth instances in this container. @@ -47,17 +44,17 @@ class AuthComponent: NSObject, Library, AuthProvider, ComponentLifecycleMaintain // MARK: - Library conformance static func componentsToRegister() -> [Component] { + let authCreationBlock: ComponentCreationBlock = { container, isCacheable in + guard let app = container.app else { return nil } + isCacheable.pointee = true + return Auth(app: app) + } let appCheckInterop = Dependency(with: AppCheckInterop.self, isRequired: false) - return [Component(AuthProvider.self, - instantiationTiming: .alwaysEager, - dependencies: [appCheckInterop]) { container, isCacheable in - guard let app = container.app else { return nil } - isCacheable.pointee = true - let newComponent = AuthComponent(app: app) - // Set up instances early enough so User on keychain will be decoded. - newComponent.auth() - return newComponent - }] + let authInterop = Component(AuthInterop.self, + instantiationTiming: .alwaysEager, + dependencies: [appCheckInterop], + creationBlock: authCreationBlock) + return [authInterop] } // MARK: - AuthProvider conformance diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index cf6ec906231..f0ea312e752 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -1010,15 +1010,12 @@ extension User: NSSecureCoding {} guard let requestConfiguration = self.auth?.requestConfiguration else { fatalError("Auth Internal Error: Unexpected nil requestConfiguration.") } - guard let uid = self.uid else { - fatalError("Auth Internal Error: uid is nil.") - } - let request = DeleteAccountRequest(localID: uid, accessToken: accessToken, + let request = DeleteAccountRequest(localID: self.uid, accessToken: accessToken, requestConfiguration: requestConfiguration) Task { do { let _ = try await AuthBackend.call(with: request) - try self.auth?.signOutByForce(withUserID: uid) + try self.auth?.signOutByForce(withUserID: self.uid) User.callInMainThreadWithError(callback: completion, error: nil) } catch { User.callInMainThreadWithError(callback: completion, error: error) @@ -1174,7 +1171,7 @@ extension User: NSSecureCoding {} } /// The provider's user ID for the user. - @objc open var uid: String? + @objc open var uid: String /// The name of the user. @objc open var displayName: String? @@ -1783,9 +1780,6 @@ extension User: NSSecureCoding {} /// Signs out this user if the user or the token is invalid. /// - Parameter error: The error from the server. private func signOutIfTokenIsInvalid(withError error: Error) { - guard let uid else { - return - } let code = (error as NSError).code if code == AuthErrorCode.userNotFound.rawValue || code == AuthErrorCode.userDisabled.rawValue || diff --git a/FirebaseAuth/Sources/Swift/User/UserInfo.swift b/FirebaseAuth/Sources/Swift/User/UserInfo.swift index 9ce14dcb26a..93e8066abe4 100644 --- a/FirebaseAuth/Sources/Swift/User/UserInfo.swift +++ b/FirebaseAuth/Sources/Swift/User/UserInfo.swift @@ -20,7 +20,7 @@ import Foundation var providerID: String { get } /// The provider's user ID for the user. - var uid: String? { get } + var uid: String { get } /// The name of the user. var displayName: String? { get } diff --git a/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift b/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift index 5e68a2d2cb7..97eb927cdaa 100644 --- a/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift +++ b/FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift @@ -23,12 +23,13 @@ extension UserInfoImpl: NSSecureCoding {} /// - Returns: A new instance of `UserInfo` using data from the getAccountInfo endpoint. class func userInfo(withGetAccountInfoResponseProviderUserInfo providerUserInfo: GetAccountInfoResponseProviderUserInfo) -> UserInfoImpl { - guard let providerID = providerUserInfo.providerID else { + guard let providerID = providerUserInfo.providerID, + let uid = providerUserInfo.federatedID else { // This was a crash in ObjC implementation. Should providerID be not nullable? - fatalError("Missing providerID from GetAccountInfoResponseProviderUserInfo") + fatalError("Missing providerID or uid from GetAccountInfoResponseProviderUserInfo") } return UserInfoImpl(withProviderID: providerID, - userID: providerUserInfo.federatedID, + userID: uid, displayName: providerUserInfo.displayName, photoURL: providerUserInfo.photoURL, email: providerUserInfo.email, @@ -43,7 +44,7 @@ extension UserInfoImpl: NSSecureCoding {} /// - Parameter email: The user's email address. /// - Parameter phoneNumber: The user's phone number. private init(withProviderID providerID: String, - userID: String?, + userID: String, displayName: String?, photoURL: URL?, email: String?, @@ -57,7 +58,7 @@ extension UserInfoImpl: NSSecureCoding {} } var providerID: String - var uid: String? + var uid: String var displayName: String? var photoURL: URL? var email: String? @@ -86,15 +87,17 @@ extension UserInfoImpl: NSSecureCoding {} } required convenience init?(coder: NSCoder) { - guard let providerID = coder.decodeObject(of: [NSString.self], - forKey: UserInfoImpl.kProviderIDCodingKey) as? String + guard let providerID = coder.decodeObject( + of: [NSString.self], + forKey: UserInfoImpl.kProviderIDCodingKey + ) as? String, + let userID = coder.decodeObject( + of: [NSString.self], + forKey: UserInfoImpl.kUserIDCodingKey + ) as? String else { return nil } - let uid = coder.decodeObject( - of: [NSString.self], - forKey: UserInfoImpl.kUserIDCodingKey - ) as? String let displayName = coder.decodeObject( of: [NSString.self], forKey: UserInfoImpl.kDisplayNameCodingKey @@ -112,7 +115,7 @@ extension UserInfoImpl: NSSecureCoding {} forKey: UserInfoImpl.kPhoneNumberCodingKey ) as? String self.init(withProviderID: providerID, - userID: uid, + userID: userID, displayName: displayName, photoURL: photoURL, email: email, diff --git a/FirebaseAuth/Tests/Unit/SwiftAPI.swift b/FirebaseAuth/Tests/Unit/SwiftAPI.swift index 6e43a499abb..d620e9c0ea7 100644 --- a/FirebaseAuth/Tests/Unit/SwiftAPI.swift +++ b/FirebaseAuth/Tests/Unit/SwiftAPI.swift @@ -614,8 +614,8 @@ class AuthAPI_hOnlyTests: XCTestCase { changeRequest.commitChanges { _ in } let _: String = user.providerID - if let _: String = user.uid, - let _: String = user.displayName, + let _: String = user.uid + if let _: String = user.displayName, let _: URL = user.photoURL, let _: String = user.email, let _: String = user.phoneNumber {} @@ -671,8 +671,8 @@ class AuthAPI_hOnlyTests: XCTestCase { func userInfoProperties(userInfo: UserInfo) { let _: String = userInfo.providerID - if let _: String = userInfo.uid, - let _: String = userInfo.displayName, + let _: String = userInfo.uid + if let _: String = userInfo.displayName, let _: URL = userInfo.photoURL, let _: String = userInfo.email, let _: String = userInfo.phoneNumber {} diff --git a/FirebaseAuthInterop.podspec b/FirebaseAuthInterop.podspec index 65d84553e7b..e24aaca8632 100644 --- a/FirebaseAuthInterop.podspec +++ b/FirebaseAuthInterop.podspec @@ -25,6 +25,6 @@ Pod::Spec.new do |s| s.tvos.deployment_target = '12.0' s.watchos.deployment_target = '6.0' - s.source_files = 'FirebaseAuth/Interop/*.[hm]' - s.public_header_files = 'FirebaseAuth/Interop/*.h' + s.source_files = 'FirebaseAuth/Interop/**/*.[hm]' + s.public_header_files = 'FirebaseAuth/Interop/Public/FirebaseAuthInterop/*.h' end diff --git a/FirebaseDatabase/Sources/Api/FIRDatabase.m b/FirebaseDatabase/Sources/Api/FIRDatabase.m index f90e318ab8f..355de35b85d 100644 --- a/FirebaseDatabase/Sources/Api/FIRDatabase.m +++ b/FirebaseDatabase/Sources/Api/FIRDatabase.m @@ -16,7 +16,7 @@ #import -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseDatabase/Sources/Api/FIRDatabaseComponent.h" diff --git a/FirebaseDatabase/Sources/Api/FIRDatabaseComponent.m b/FirebaseDatabase/Sources/Api/FIRDatabaseComponent.m index 6a65b8e91f3..57a6c1e73d9 100644 --- a/FirebaseDatabase/Sources/Api/FIRDatabaseComponent.m +++ b/FirebaseDatabase/Sources/Api/FIRDatabaseComponent.m @@ -20,7 +20,7 @@ #import "FirebaseDatabase/Sources/Core/FRepoManager.h" #import "FirebaseDatabase/Sources/FIRDatabaseConfig_Private.h" -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import diff --git a/FirebaseDatabase/Sources/Login/FIRDatabaseConnectionContextProvider.m b/FirebaseDatabase/Sources/Login/FIRDatabaseConnectionContextProvider.m index 358a26d1e70..213886dfd3e 100644 --- a/FirebaseDatabase/Sources/Login/FIRDatabaseConnectionContextProvider.m +++ b/FirebaseDatabase/Sources/Login/FIRDatabaseConnectionContextProvider.m @@ -18,7 +18,7 @@ #import "FirebaseCore/Extension/FirebaseCoreInternal.h" -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #import "FirebaseDatabase/Sources/Api/Private/FIRDatabaseQuery_Private.h" #import "FirebaseDatabase/Sources/Utilities/FUtilities.h" diff --git a/FirebaseDatabase/Tests/Helpers/FTestHelpers.m b/FirebaseDatabase/Tests/Helpers/FTestHelpers.m index 26ba73453c9..26fe0141595 100644 --- a/FirebaseDatabase/Tests/Helpers/FTestHelpers.m +++ b/FirebaseDatabase/Tests/Helpers/FTestHelpers.m @@ -16,7 +16,7 @@ #import "FirebaseDatabase/Tests/Helpers/FTestHelpers.h" -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseDatabase/Sources/Api/Private/FIRDatabase_Private.h" diff --git a/FirebaseDatabase/Tests/Integration/FIRAuthTests.m b/FirebaseDatabase/Tests/Integration/FIRAuthTests.m index 1bdbe2bca2e..c342d052e93 100644 --- a/FirebaseDatabase/Tests/Integration/FIRAuthTests.m +++ b/FirebaseDatabase/Tests/Integration/FIRAuthTests.m @@ -16,7 +16,7 @@ #import -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseDatabase/Sources/FIRDatabaseConfig_Private.h" diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index a9ea3bd4127..3b548b63497 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -17,10 +17,10 @@ Firebase Storage provides robust, secure file uploads and downloads from Firebas } s.social_media_url = 'https://twitter.com/Firebase' - ios_deployment_target = '11.0' - osx_deployment_target = '10.13' - tvos_deployment_target = '12.0' - watchos_deployment_target = '6.0' + ios_deployment_target = '13.0' + osx_deployment_target = '10.15' + tvos_deployment_target = '13.0' + watchos_deployment_target = '7.0' s.ios.deployment_target = ios_deployment_target s.osx.deployment_target = osx_deployment_target diff --git a/FirebaseStorage/Tests/ObjCIntegration/FIRStorageIntegrationTests.m b/FirebaseStorage/Tests/ObjCIntegration/FIRStorageIntegrationTests.m index bb9236d4c7a..bad1bdfa821 100644 --- a/FirebaseStorage/Tests/ObjCIntegration/FIRStorageIntegrationTests.m +++ b/FirebaseStorage/Tests/ObjCIntegration/FIRStorageIntegrationTests.m @@ -14,10 +14,9 @@ #import +@import FirebaseAuth; @import FirebaseStorage; -#import - #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseStorage/Tests/ObjCIntegration/Credentials.h" diff --git a/Firestore/Source/API/FSTFirestoreComponent.mm b/Firestore/Source/API/FSTFirestoreComponent.mm index 286576610b0..39285e138d9 100644 --- a/Firestore/Source/API/FSTFirestoreComponent.mm +++ b/Firestore/Source/API/FSTFirestoreComponent.mm @@ -22,7 +22,7 @@ #include #include -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #import "FirebaseCore/Extension/FIRAppInternal.h" #import "FirebaseCore/Extension/FIRComponent.h" #import "FirebaseCore/Extension/FIRComponentContainer.h" diff --git a/Firestore/core/src/credentials/firebase_auth_credentials_provider_apple.mm b/Firestore/core/src/credentials/firebase_auth_credentials_provider_apple.mm index c4452025e87..0760e524cb9 100644 --- a/Firestore/core/src/credentials/firebase_auth_credentials_provider_apple.mm +++ b/Firestore/core/src/credentials/firebase_auth_credentials_provider_apple.mm @@ -18,7 +18,7 @@ #import "FirebaseCore/Extension/FIRAppInternal.h" -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #include "Firestore/core/src/util/error_apple.h" #include "Firestore/core/src/util/hard_assert.h" diff --git a/Firestore/core/test/unit/credentials/firebase_auth_credentials_provider_test.mm b/Firestore/core/test/unit/credentials/firebase_auth_credentials_provider_test.mm index c1e190f439e..4c93cd51163 100644 --- a/Firestore/core/test/unit/credentials/firebase_auth_credentials_provider_test.mm +++ b/Firestore/core/test/unit/credentials/firebase_auth_credentials_provider_test.mm @@ -20,7 +20,7 @@ #include // NOLINT(build/c++11) #include -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" #include "Firestore/core/src/util/statusor.h" #include "Firestore/core/src/util/string_apple.h" diff --git a/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj b/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj index 2f426d10b8d..4fbcd304dab 100644 --- a/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj +++ b/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + DE305B702B7BE0B5000595B3 /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = DE305B6F2B7BE0B5000595B3 /* FirebaseStorage */; }; DE99626B2B44C96C0038ED6B /* objc-module-import-test.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9962682B44C96B0038ED6B /* objc-module-import-test.m */; }; DE99626C2B44C96C0038ED6B /* objc-module-import-test.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9962682B44C96B0038ED6B /* objc-module-import-test.m */; }; DE99626D2B44C96C0038ED6B /* objcxx-header-import-test.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE9962692B44C96B0038ED6B /* objcxx-header-import-test.mm */; }; @@ -50,7 +51,6 @@ EA7DF5AF29EF3328005664A7 /* FirebasePerformance in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, tvos, ); productRef = EA7DF5AE29EF3328005664A7 /* FirebasePerformance */; }; EA7DF5B129EF3328005664A7 /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5B029EF3328005664A7 /* FirebaseRemoteConfig */; }; EA7DF5B329EF3328005664A7 /* FirebaseRemoteConfigSwift in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5B229EF3328005664A7 /* FirebaseRemoteConfigSwift */; }; - EA7DF5B529EF3328005664A7 /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5B429EF3328005664A7 /* FirebaseStorage */; }; EA7DF5B729EF3328005664A7 /* FirebaseStorageCombine-Community in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5B629EF3328005664A7 /* FirebaseStorageCombine-Community */; }; EAA0A99A2AD8495000C28FCD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAA0A9992AD8495000C28FCD /* Preview Assets.xcassets */; }; EAA0A9A52AD849E600C28FCD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1269B329EDF98800D79E66 /* AppDelegate.swift */; }; @@ -119,7 +119,6 @@ EA7DF5A129EF3327005664A7 /* FirebaseFunctions in Frameworks */, EA7DF58D29EF3326005664A7 /* FirebaseAppDistribution-Beta in Frameworks */, EA7DF5AF29EF3328005664A7 /* FirebasePerformance in Frameworks */, - EA7DF5B529EF3328005664A7 /* FirebaseStorage in Frameworks */, EA7DF5A729EF3327005664A7 /* FirebaseInAppMessagingSwift-Beta in Frameworks */, EA7DF59929EF3326005664A7 /* FirebaseDynamicLinks in Frameworks */, EA7DF59D29EF3326005664A7 /* FirebaseFirestoreCombine-Community in Frameworks */, @@ -149,6 +148,7 @@ EABBCF6F2B45B46500232BAF /* FirebaseAuthCombine-Community in Frameworks */, EABBCF6D2B45B44100232BAF /* FirebaseAuth in Frameworks */, EAA0A9C32AD84E5600C28FCD /* FirebaseInAppMessagingSwift-Beta in Frameworks */, + DE305B702B7BE0B5000595B3 /* FirebaseStorage in Frameworks */, EAA0A9C12AD84E5600C28FCD /* FirebaseInAppMessaging-Beta in Frameworks */, EAA0A9C52AD84E5D00C28FCD /* FirebaseAnalytics in Frameworks */, ); @@ -157,6 +157,13 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + DE305B6E2B7BE0B5000595B3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; EA1269A729EDF98800D79E66 = { isa = PBXGroup; children = ( @@ -169,7 +176,7 @@ EAA0A9902AD8494F00C28FCD /* ClientApp-CocoaPods-iOS13 */, EAA0A9B22AD84E0800C28FCD /* ClientApp-iOS13 */, EA1269B129EDF98800D79E66 /* Products */, - EABBCF6B2B45B44100232BAF /* Frameworks */, + DE305B6E2B7BE0B5000595B3 /* Frameworks */, ); sourceTree = ""; }; @@ -291,13 +298,6 @@ path = "Preview Content"; sourceTree = ""; }; - EABBCF6B2B45B44100232BAF /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -337,7 +337,6 @@ EA7DF5AE29EF3328005664A7 /* FirebasePerformance */, EA7DF5B029EF3328005664A7 /* FirebaseRemoteConfig */, EA7DF5B229EF3328005664A7 /* FirebaseRemoteConfigSwift */, - EA7DF5B429EF3328005664A7 /* FirebaseStorage */, EA7DF5B629EF3328005664A7 /* FirebaseStorageCombine-Community */, EA0BC0FE29F06D5B005B8AEE /* FirebaseAnalyticsOnDeviceConversion */, ); @@ -399,6 +398,7 @@ EAA0A9C62AD84E5D00C28FCD /* FirebaseAnalyticsSwift */, EABBCF6C2B45B44100232BAF /* FirebaseAuth */, EABBCF6E2B45B46500232BAF /* FirebaseAuthCombine-Community */, + DE305B6F2B7BE0B5000595B3 /* FirebaseStorage */, ); productName = "ClientApp-iOS13"; productReference = EAA0A9B12AD84E0800C28FCD /* ClientApp-iOS13.app */; @@ -1026,6 +1026,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + DE305B6F2B7BE0B5000595B3 /* FirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseStorage; + }; EA0BC0FE29F06D5B005B8AEE /* FirebaseAnalyticsOnDeviceConversion */ = { isa = XCSwiftPackageProductDependency; productName = FirebaseAnalyticsOnDeviceConversion; @@ -1118,10 +1122,6 @@ isa = XCSwiftPackageProductDependency; productName = FirebaseRemoteConfigSwift; }; - EA7DF5B429EF3328005664A7 /* FirebaseStorage */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseStorage; - }; EA7DF5B629EF3328005664A7 /* FirebaseStorageCombine-Community */ = { isa = XCSwiftPackageProductDependency; productName = "FirebaseStorageCombine-Community"; diff --git a/IntegrationTesting/ClientApp/Podfile b/IntegrationTesting/ClientApp/Podfile index c127c55238c..644b709fefa 100644 --- a/IntegrationTesting/ClientApp/Podfile +++ b/IntegrationTesting/ClientApp/Podfile @@ -28,7 +28,6 @@ target 'ClientApp-CocoaPods' do pod 'FirebaseInAppMessaging', :path => '../../' pod 'FirebaseMessaging', :path => '../../' pod 'FirebasePerformance', :path => '../../' - pod 'FirebaseStorage', :path => '../../' pod 'FirebaseMLModelDownloader', :path => '../../' pod 'Firebase', :path => '../../' end @@ -41,7 +40,8 @@ target 'ClientApp-CocoaPods-iOS13' do pod 'FirebaseAnalytics' # Binary pods don't work with `:path`. pod 'FirebaseAnalyticsSwift', :path => '../../' # Requires iOS 13.0+ pod 'FirebaseAuth', :path => '../../' # Requires iOS 13.0+ + pod 'FirebaseAuthInterop', :path => '../../' pod 'FirebaseInAppMessaging', :path => '../../' pod 'FirebaseInAppMessagingSwift', :path => '../../' # Requires iOS 13.0+ - + pod 'FirebaseStorage', :path => '../../' end diff --git a/IntegrationTesting/ClientApp/Shared-iOS12+/objc-module-import-test.m b/IntegrationTesting/ClientApp/Shared-iOS12+/objc-module-import-test.m index 4339ad22252..2d7baf881a7 100644 --- a/IntegrationTesting/ClientApp/Shared-iOS12+/objc-module-import-test.m +++ b/IntegrationTesting/ClientApp/Shared-iOS12+/objc-module-import-test.m @@ -39,4 +39,3 @@ @import FirebaseInAppMessaging; #endif @import FirebaseRemoteConfig; -@import FirebaseStorage; diff --git a/IntegrationTesting/ClientApp/Shared-iOS12+/swift-import-test.swift b/IntegrationTesting/ClientApp/Shared-iOS12+/swift-import-test.swift index 0c5dc24395f..35eacac276f 100644 --- a/IntegrationTesting/ClientApp/Shared-iOS12+/swift-import-test.swift +++ b/IntegrationTesting/ClientApp/Shared-iOS12+/swift-import-test.swift @@ -50,7 +50,6 @@ import FirebaseMLModelDownloader #endif import FirebaseRemoteConfig import FirebaseRemoteConfigSwift -import FirebaseStorage #if SWIFT_PACKAGE import FirebaseStorageCombineSwift #endif // SWIFT_PACKAGE diff --git a/IntegrationTesting/ClientApp/Shared-iOS13+/objc-header-import-test.m b/IntegrationTesting/ClientApp/Shared-iOS13+/objc-header-import-test.m index c0a1b20a26e..cea08137cea 100644 --- a/IntegrationTesting/ClientApp/Shared-iOS13+/objc-header-import-test.m +++ b/IntegrationTesting/ClientApp/Shared-iOS13+/objc-header-import-test.m @@ -31,3 +31,15 @@ #import #import "FirebaseInAppMessaging/FirebaseInAppMessaging.h" #endif +#ifdef COCOAPODS +#import "FirebaseStorage/FIRStorageTypedefs.h" + +@interface TestImports : NSObject +@end + +@implementation TestImports +- (FIRAuth *)testImports { + return [FIRAuth auth]; +} +@end +#endif diff --git a/IntegrationTesting/ClientApp/Shared-iOS13+/objc-module-import-test.m b/IntegrationTesting/ClientApp/Shared-iOS13+/objc-module-import-test.m index 908313f9651..7aea74bd865 100644 --- a/IntegrationTesting/ClientApp/Shared-iOS13+/objc-module-import-test.m +++ b/IntegrationTesting/ClientApp/Shared-iOS13+/objc-module-import-test.m @@ -25,3 +25,4 @@ #if (TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || TARGET_OS_TV @import FirebaseInAppMessaging; #endif +@import FirebaseStorage; diff --git a/IntegrationTesting/ClientApp/Shared-iOS13+/objcxx-header-import-test.mm b/IntegrationTesting/ClientApp/Shared-iOS13+/objcxx-header-import-test.mm index 13415ff9e52..0f073b2e12a 100644 --- a/IntegrationTesting/ClientApp/Shared-iOS13+/objcxx-header-import-test.mm +++ b/IntegrationTesting/ClientApp/Shared-iOS13+/objcxx-header-import-test.mm @@ -31,3 +31,16 @@ #import #import "FirebaseInAppMessaging/FirebaseInAppMessaging.h" #endif + +#ifdef COCOAPODS +#import "FirebaseStorage/FIRStorageTypedefs.h" + +@interface TestImportsCxx : NSObject +@end + +@implementation TestImportsCxx +- (FIRAuth *)testImports { + return [FIRAuth auth]; +} +@end +#endif diff --git a/IntegrationTesting/ClientApp/Shared-iOS13+/swift-import-test.swift b/IntegrationTesting/ClientApp/Shared-iOS13+/swift-import-test.swift index 2007967ea16..df63dbf94b6 100644 --- a/IntegrationTesting/ClientApp/Shared-iOS13+/swift-import-test.swift +++ b/IntegrationTesting/ClientApp/Shared-iOS13+/swift-import-test.swift @@ -22,3 +22,4 @@ import FirebaseAuth import FirebaseInAppMessaging import FirebaseInAppMessagingSwift #endif +import FirebaseStorage diff --git a/Package.swift b/Package.swift index 6bc64e96642..ded86578798 100644 --- a/Package.swift +++ b/Package.swift @@ -457,7 +457,7 @@ let package = Package( exclude: [ "CMakeLists.txt", ], - publicHeadersPath: ".", + publicHeadersPath: "Public", cSettings: [ .headerSearchPath("../../"), ] diff --git a/SharedTestUtilities/FIRAuthInteropFake.h b/SharedTestUtilities/FIRAuthInteropFake.h index dd5d0d82ab1..ef245e20eda 100644 --- a/SharedTestUtilities/FIRAuthInteropFake.h +++ b/SharedTestUtilities/FIRAuthInteropFake.h @@ -16,7 +16,7 @@ #import -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" NS_ASSUME_NONNULL_BEGIN diff --git a/SharedTestUtilities/FIRAuthInteropFake.m b/SharedTestUtilities/FIRAuthInteropFake.m index 2b66411bd88..ca7aeab94e1 100644 --- a/SharedTestUtilities/FIRAuthInteropFake.m +++ b/SharedTestUtilities/FIRAuthInteropFake.m @@ -16,7 +16,7 @@ #import "SharedTestUtilities/FIRAuthInteropFake.h" -#import "FirebaseAuth/Interop/FIRAuthInterop.h" +#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h" NS_ASSUME_NONNULL_BEGIN From 1ed6d66b2010539bf21abf66b942dcf89384bd75 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 21 Feb 2024 12:15:45 -0500 Subject: [PATCH 039/104] Make `instanceForProtocol:inContainer:` return `nullable T` (#12391) --- FirebaseCore/Extension/FIRComponentType.h | 3 ++- FirebaseCore/Sources/FIRComponentType.m | 3 ++- FirebaseStorage/Sources/Storage.swift | 12 ++++++++---- .../Tests/Unit/StorageComponentTests.swift | 6 +++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/FirebaseCore/Extension/FIRComponentType.h b/FirebaseCore/Extension/FIRComponentType.h index 6f2aca7b863..c69085d1983 100644 --- a/FirebaseCore/Extension/FIRComponentType.h +++ b/FirebaseCore/Extension/FIRComponentType.h @@ -27,7 +27,8 @@ NS_SWIFT_NAME(ComponentType) /// Do not use directly. A factory method to retrieve an instance that provides a specific /// functionality. -+ (T)instanceForProtocol:(Protocol *)protocol inContainer:(FIRComponentContainer *)container; ++ (nullable T)instanceForProtocol:(Protocol *)protocol + inContainer:(FIRComponentContainer *)container; @end diff --git a/FirebaseCore/Sources/FIRComponentType.m b/FirebaseCore/Sources/FIRComponentType.m index c9cd2ad2c2c..2204fd65b32 100644 --- a/FirebaseCore/Sources/FIRComponentType.m +++ b/FirebaseCore/Sources/FIRComponentType.m @@ -20,7 +20,8 @@ @implementation FIRComponentType -+ (id)instanceForProtocol:(Protocol *)protocol inContainer:(FIRComponentContainer *)container { ++ (nullable id)instanceForProtocol:(Protocol *)protocol + inContainer:(FIRComponentContainer *)container { // Forward the call to the container. return [container instanceForProtocol:protocol]; } diff --git a/FirebaseStorage/Sources/Storage.swift b/FirebaseStorage/Sources/Storage.swift index 79d18dfa4f6..16792fd1d80 100644 --- a/FirebaseStorage/Sources/Storage.swift +++ b/FirebaseStorage/Sources/Storage.swift @@ -61,8 +61,10 @@ import FirebaseCore /// - Parameter app: The custom `FirebaseApp` used for initialization. /// - Returns: A `Storage` instance, configured with the custom `FirebaseApp`. @objc(storageForApp:) open class func storage(app: FirebaseApp) -> Storage { - let provider = ComponentType.instance(for: StorageProvider.self, - in: app.container) + guard let provider = ComponentType.instance(for: StorageProvider.self, + in: app.container) else { + fatalError("No \(StorageProvider.self) instance found for Firebase app: \(app.name)") + } return provider.storage(for: Storage.bucket(for: app)) } @@ -75,8 +77,10 @@ import FirebaseCore /// URL. @objc(storageForApp:URL:) open class func storage(app: FirebaseApp, url: String) -> Storage { - let provider = ComponentType.instance(for: StorageProvider.self, - in: app.container) + guard let provider = ComponentType.instance(for: StorageProvider.self, + in: app.container) else { + fatalError("No \(StorageProvider.self) instance found for Firebase app: \(app.name)") + } return provider.storage(for: Storage.bucket(for: app, urlString: url)) } diff --git a/FirebaseStorage/Tests/Unit/StorageComponentTests.swift b/FirebaseStorage/Tests/Unit/StorageComponentTests.swift index dce5d75fe0d..a7851fb9479 100644 --- a/FirebaseStorage/Tests/Unit/StorageComponentTests.swift +++ b/FirebaseStorage/Tests/Unit/StorageComponentTests.swift @@ -70,14 +70,14 @@ class StorageComponentTests: StorageTestHelpers { in: container) XCTAssertNotNil(provider) - let storage1 = provider.storage(for: "randomBucket") - let storage2 = provider.storage(for: "randomBucket") + let storage1 = provider?.storage(for: "randomBucket") + let storage2 = provider?.storage(for: "randomBucket") XCTAssertNotNil(storage1) // Ensure they're the same instance. XCTAssert(storage1 === storage2) - let storage3 = provider.storage(for: "differentBucket") + let storage3 = provider?.storage(for: "differentBucket") XCTAssertNotNil(storage3) XCTAssert(storage1 !== storage3) From 46f04216beb688042135cf42c13f1f01ca77ff90 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 22 Feb 2024 09:28:13 -0800 Subject: [PATCH 040/104] [Binary] Add missing values to Info.plist generator (#12413) --- ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift b/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift index 1e934c25382..5b87998bb37 100644 --- a/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift +++ b/ReleaseTooling/Sources/ZipBuilder/CarthageUtils.swift @@ -250,10 +250,15 @@ extension CarthageUtils { withVersion version: String, to location: URL) { let ver = version.components(separatedBy: "-")[0] // remove any version suffix. + + // TODO(paulb777): Does MinimumOSVersion or anything else need + // to be adapted for other platforms? let plist: [String: String] = ["CFBundleIdentifier": "com.firebase.Firebase-\(name)", "CFBundleInfoDictionaryVersion": "6.0", "CFBundlePackageType": "FMWK", "CFBundleVersion": ver, + "CFBundleShortVersionString": ver, + "MinimumOSVersion": Platform.iOS.minimumVersion, "DTSDKName": "iphonesimulator11.2", "CFBundleExecutable": name, "CFBundleName": name] From 134e7f2c39ff4cde9f850897f44d493ad93e6011 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 22 Feb 2024 09:28:45 -0800 Subject: [PATCH 041/104] [binary] Use Xcode built Info.plist's (#12414) --- .../Sources/ZipBuilder/FrameworkBuilder.swift | 83 ++++++++++--------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index 09b86c133d7..cc09b1d1f41 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -380,10 +380,6 @@ struct FrameworkBuilder { } umbrellaHeader = umbrellaHeaderURL.lastPathComponent } - // Add an Info.plist. Required by Carthage and SPM binary xcframeworks. - CarthageUtils.generatePlistContents(forName: frameworkName, - withVersion: podInfo.version, - to: frameworkDir) // TODO: copy PrivateHeaders directory as well if it exists. SDWebImage is an example pod. @@ -639,33 +635,7 @@ struct FrameworkBuilder { "\(framework): \(error)") } - // Move any privacy manifest-containing resource bundles into the - // platform framework. - try? fileManager.contentsOfDirectory( - at: frameworkPath.deletingLastPathComponent(), - includingPropertiesForKeys: nil - ) - .filter { $0.pathExtension == "bundle" } - // TODO(ncooke3): Once the zip is built with Xcode 15, the following - // `filter` can be removed. The following block exists to preserve - // how resources (e.g. like FIAM's) are packaged for use in Xcode 14. - .filter { bundleURL in - let dirEnum = fileManager.enumerator(atPath: bundleURL.path) - var containsPrivacyManifest = false - while let relativeFilePath = dirEnum?.nextObject() as? String { - if relativeFilePath.hasSuffix("PrivacyInfo.xcprivacy") { - containsPrivacyManifest = true - break - } - } - return containsPrivacyManifest - } - // Bundles are moved rather than copied to prevent them from being - // packaged in a `Resources` directory at the root of the xcframework. - .forEach { try! fileManager.moveItem( - at: $0, - to: platformFrameworkDir.appendingPathComponent($0.lastPathComponent) - ) } + processPrivacyManifests(fileManager, frameworkPath, platformFrameworkDir) // Headers from slice do { @@ -688,25 +658,23 @@ struct FrameworkBuilder { fatalError("Could not create framework directory needed to build \(framework): \(error)") } - // Info.plist from `fromFolder` - do { - let infoPlistSrc = fromFolder.appendingPathComponent("Info.plist").resolvingSymlinksInPath() - let infoPlistDst = platformFrameworkDir.appendingPathComponent("Info.plist") - try fileManager.copyItem(at: infoPlistSrc, to: infoPlistDst) - } catch { - fatalError("Could not create framework directory needed to build \(framework): \(error)") - } - - // Copy the binary to the right location. + // Copy the binary and Info.plist to the right location. let binaryName = frameworkPath.lastPathComponent.replacingOccurrences(of: ".framework", with: "") let fatBinary = frameworkPath.appendingPathComponent(binaryName).resolvingSymlinksInPath() + let infoPlist = frameworkPath.appendingPathComponent("Info.plist").resolvingSymlinksInPath() + let infoPlistDestination = platformFrameworkDir.appendingPathComponent("Info.plist") let fatBinaryDestination = platformFrameworkDir.appendingPathComponent(framework) do { try fileManager.copyItem(at: fatBinary, to: fatBinaryDestination) } catch { fatalError("Could not copy fat binary to framework directory for \(framework): \(error)") } + do { + try fileManager.copyItem(at: infoPlist, to: infoPlistDestination) + } catch { + // The Catalyst and macos Info.plist's are in another location. Ignore failure. + } // Use the appropriate moduleMaps packageModuleMaps(inFrameworks: [frameworkPath], @@ -719,6 +687,39 @@ struct FrameworkBuilder { return frameworksBuilt } + /// Process privacy manifests. + /// + /// Move any privacy manifest-containing resource bundles into the platform framework. + func processPrivacyManifests(_ fileManager: FileManager, + _ frameworkPath: URL, + _ platformFrameworkDir: URL) { + try? fileManager.contentsOfDirectory( + at: frameworkPath.deletingLastPathComponent(), + includingPropertiesForKeys: nil + ) + .filter { $0.pathExtension == "bundle" } + // TODO(ncooke3): Once the zip is built with Xcode 15, the following + // `filter` can be removed. The following block exists to preserve + // how resources (e.g. like FIAM's) are packaged for use in Xcode 14. + .filter { bundleURL in + let dirEnum = fileManager.enumerator(atPath: bundleURL.path) + var containsPrivacyManifest = false + while let relativeFilePath = dirEnum?.nextObject() as? String { + if relativeFilePath.hasSuffix("PrivacyInfo.xcprivacy") { + containsPrivacyManifest = true + break + } + } + return containsPrivacyManifest + } + // Bundles are moved rather than copied to prevent them from being + // packaged in a `Resources` directory at the root of the xcframework. + .forEach { try! fileManager.moveItem( + at: $0, + to: platformFrameworkDir.appendingPathComponent($0.lastPathComponent) + ) } + } + /// Package the built frameworks into an XCFramework. /// - Parameter withName: The framework name. /// - Parameter frameworks: The grouped frameworks. From d26431241102b0eb980e1a2d121826cac1e7ecaa Mon Sep 17 00:00:00 2001 From: Evan Cooper Date: Thu, 22 Feb 2024 13:55:58 -0500 Subject: [PATCH 042/104] Add FirebaseSource to ReadDecodable extensions of DocumentReference (#12401) --- .../Codable/DocumentReference+ReadDecodable.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Firestore/Swift/Source/Codable/DocumentReference+ReadDecodable.swift b/Firestore/Swift/Source/Codable/DocumentReference+ReadDecodable.swift index cc6f6f6a6b6..2d3af7a9dfa 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+ReadDecodable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+ReadDecodable.swift @@ -48,14 +48,18 @@ public extension DocumentReference { /// not yet been set to their final value are returned from the snapshot. /// - decoder: The decoder to use to convert the document. Defaults to use /// the default decoder. + /// - source: Indicates whether the results should be fetched from the cache only + /// (`Source.cache`), the server only (`Source.server`), or to attempt the + /// server and fall back to the cache (`Source.default`). /// - completion: The closure to call when the document snapshot has been /// fetched and decoded. func getDocument(as type: T.Type, with serverTimestampBehavior: ServerTimestampBehavior = .none, decoder: Firestore.Decoder = .init(), + source: FirestoreSource = .default, completion: @escaping (Result) -> Void) { - getDocument { snapshot, error in + getDocument(source: source) { snapshot, error in guard let snapshot = snapshot else { /** * Force unwrapping here is fine since this logic corresponds to the auto-synthesized @@ -101,13 +105,17 @@ public extension DocumentReference { /// snapshot. /// - decoder: The decoder to use to convert the document. Defaults to use /// the default decoder. + /// - source: Indicates whether the results should be fetched from the cache only + /// (`Source.cache`), the server only (`Source.server`), or to attempt the + /// server and fall back to the cache (`Source.default`). /// - Returns: This instance of the supplied `Decodable` type `T`. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func getDocument(as type: T.Type, with serverTimestampBehavior: ServerTimestampBehavior = .none, - decoder: Firestore.Decoder = .init()) async throws -> T { - let snapshot = try await getDocument() + decoder: Firestore.Decoder = .init(), + source: FirestoreSource = .default) async throws -> T { + let snapshot = try await getDocument(source: source) return try snapshot.data(as: T.self, with: serverTimestampBehavior, decoder: decoder) From 0b9af70877cfd8505af4e0888fcd6d916c0deedc Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 22 Feb 2024 16:36:59 -0800 Subject: [PATCH 043/104] Enable nanopb version with privacy manifest (#12412) --- .github/workflows/zip.yml | 2 +- FirebaseAnalytics.podspec | 2 +- FirebaseCrashlytics.podspec | 2 +- FirebaseFirestoreInternal.podspec | 2 +- FirebaseInAppMessaging.podspec | 2 +- FirebaseMessaging.podspec | 2 +- FirebasePerformance.podspec | 2 +- FirebaseSessions.podspec | 2 +- GoogleAppMeasurement.podspec | 2 +- Package.swift | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 2bebb1f7da2..45578d33761 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -584,7 +584,7 @@ jobs: - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: - name: quickstart_artifacts_ihappmessaging + name: quickstart_artifacts_inappmessaging path: quickstart-ios/ quickstart_framework_messaging: diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index 82b191480c4..83363a2c0f3 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -32,7 +32,7 @@ Pod::Spec.new do |s| s.dependency 'GoogleUtilities/MethodSwizzler', '~> 7.11' s.dependency 'GoogleUtilities/NSData+zlib', '~> 7.11' s.dependency 'GoogleUtilities/Network', '~> 7.11' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.default_subspecs = 'AdIdSupport' diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index ab12afd74e5..b2303029d24 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -61,7 +61,7 @@ Pod::Spec.new do |s| s.dependency 'PromisesObjC', '~> 2.1' s.dependency 'GoogleDataTransport', '~> 9.2' s.dependency 'GoogleUtilities/Environment', '~> 7.8' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.libraries = 'c++', 'z' s.ios.frameworks = 'Security', 'SystemConfiguration' diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index cbe2eabeaaf..39a7e85ecf9 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -102,7 +102,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.dependency 'gRPC-C++', '~> 1.49.1' s.dependency 'leveldb-library', '~> 1.22' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.ios.frameworks = 'SystemConfiguration', 'UIKit' s.osx.frameworks = 'SystemConfiguration' diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index 29cb090198e..5f8e245690e 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -84,7 +84,7 @@ See more product details at https://firebase.google.com/products/in-app-messagin s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'FirebaseABTesting', '~> 10.0' s.dependency 'GoogleUtilities/Environment', '~> 7.8' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 625576dc9c7..cffc83756e8 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -65,7 +65,7 @@ device, and it is completely free. s.dependency 'GoogleUtilities/Environment', '~> 7.8' s.dependency 'GoogleUtilities/UserDefaults', '~> 7.8' s.dependency 'GoogleDataTransport', '~> 9.3' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } diff --git a/FirebasePerformance.podspec b/FirebasePerformance.podspec index e2a543d0dd2..971eb5f2c34 100644 --- a/FirebasePerformance.podspec +++ b/FirebasePerformance.podspec @@ -67,7 +67,7 @@ Firebase Performance library to measure performance of Mobile and Web Apps. s.dependency 'GoogleUtilities/Environment', '~> 7.8' s.dependency 'GoogleUtilities/ISASwizzler', '~> 7.8' s.dependency 'GoogleUtilities/MethodSwizzler', '~> 7.8' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.test_spec 'unit' do |unit_tests| unit_tests.platforms = {:ios => ios_deployment_target, :tvos => tvos_deployment_target} diff --git a/FirebaseSessions.podspec b/FirebaseSessions.podspec index 271158893a2..c44ee45d889 100644 --- a/FirebaseSessions.podspec +++ b/FirebaseSessions.podspec @@ -44,7 +44,7 @@ Pod::Spec.new do |s| s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'GoogleDataTransport', '~> 9.2' s.dependency 'GoogleUtilities/Environment', '~> 7.10' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.dependency 'PromisesSwift', '~> 2.1' s.pod_target_xcconfig = { diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index e924e6250ef..88bd7a533ad 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -32,7 +32,7 @@ Pod::Spec.new do |s| s.dependency 'GoogleUtilities/MethodSwizzler', '~> 7.11' s.dependency 'GoogleUtilities/NSData+zlib', '~> 7.11' s.dependency 'GoogleUtilities/Network', '~> 7.11' - s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0' + s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' s.default_subspecs = 'AdIdSupport' diff --git a/Package.swift b/Package.swift index c9850b123dc..eaf1ce91704 100644 --- a/Package.swift +++ b/Package.swift @@ -162,7 +162,7 @@ let package = Package( ), .package( url: "https://github.com/firebase/nanopb.git", - "2.30909.0" ..< "2.30910.0" + "2.30909.0" ..< "2.30911.0" ), abseilDependency(), grpcDependency(), From 57926cafa253a33643943e63a16bd4465ab6f0fb Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 23 Feb 2024 10:34:54 -0800 Subject: [PATCH 044/104] Fix GoogleUtilities zip file structure (#12418) --- ReleaseTooling/Sources/Utils/FileManager+Utils.swift | 10 ++++++++++ .../Sources/ZipBuilder/ResourcesManager.swift | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ReleaseTooling/Sources/Utils/FileManager+Utils.swift b/ReleaseTooling/Sources/Utils/FileManager+Utils.swift index 14af5f0406a..1a657fd5155 100644 --- a/ReleaseTooling/Sources/Utils/FileManager+Utils.swift +++ b/ReleaseTooling/Sources/Utils/FileManager+Utils.swift @@ -28,6 +28,9 @@ public extension FileManager { /// All folders with a `.bundle` extension. case bundles + /// All folders with a `.bundle` extension excluding privacy manifest bundles. + case nonPrivacyBundles + /// A directory with an optional name. If name is `nil`, all directories will be matched. case directories(name: String?) @@ -186,6 +189,13 @@ public extension FileManager { if fileURL.pathExtension == "bundle" { matches.append(fileURL) } + case .nonPrivacyBundles: + // The only thing of interest is the path extension being ".bundle", but not a privacy + // bundle. + if fileURL.pathExtension == "bundle", + !fileURL.lastPathComponent.hasSuffix("_Privacy.bundle") { + matches.append(fileURL) + } case .headers: if fileURL.pathExtension == "h" { matches.append(fileURL) diff --git a/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift b/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift index fd2a6ad6a00..48d1a964db8 100644 --- a/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift +++ b/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift @@ -30,7 +30,7 @@ extension ResourcesManager { static func directoryContainsResources(_ dir: URL) throws -> Bool { // First search for any .bundle files. let fileManager = FileManager.default - let bundles = try fileManager.recursivelySearch(for: .bundles, in: dir) + let bundles = try fileManager.recursivelySearch(for: .nonPrivacyBundles, in: dir) // Stop searching if there were any bundles found. if !bundles.isEmpty { return true } @@ -168,7 +168,7 @@ extension ResourcesManager { to resourceDir: URL, keepOriginal: Bool = false) throws -> [URL] { let fileManager = FileManager.default - let allBundles = try fileManager.recursivelySearch(for: .bundles, in: dir) + let allBundles = try fileManager.recursivelySearch(for: .nonPrivacyBundles, in: dir) // If no bundles are found, return an empty array since nothing was done (but there wasn't an // error). From 30dd2baf60d84adee4d72d22b85bcfae5ef11df7 Mon Sep 17 00:00:00 2001 From: themiswang Date: Fri, 23 Feb 2024 13:37:14 -0500 Subject: [PATCH 045/104] add optional (#12419) --- FirebaseSessions/Tests/TestApp/Shared/MockSubscriberSDK.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseSessions/Tests/TestApp/Shared/MockSubscriberSDK.swift b/FirebaseSessions/Tests/TestApp/Shared/MockSubscriberSDK.swift index e3ef501dd59..c7427286501 100644 --- a/FirebaseSessions/Tests/TestApp/Shared/MockSubscriberSDK.swift +++ b/FirebaseSessions/Tests/TestApp/Shared/MockSubscriberSDK.swift @@ -44,7 +44,7 @@ protocol MockSubscriberSDKProtocol { let sessions = ComponentType.instance(for: SessionsProvider.self, in: app.container) - sessions.register(subscriber: self) + sessions?.register(subscriber: self) } // MARK: - Library Conformance From dd5d5a9782b570641dd87568b886b6cefce05e6e Mon Sep 17 00:00:00 2001 From: Pragati Date: Fri, 23 Feb 2024 11:58:28 -0800 Subject: [PATCH 046/104] add game center to AuthMenu --- .../AuthenticationExample/Models/AuthMenu.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index b0abf158c96..711c60a0680 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -24,6 +24,7 @@ enum AuthMenu: String { case gitHub = "github.com" case yahoo = "yahoo.com" case facebook = "facebook.com" + case gameCenter = "gc.apple.com" case emailPassword = "password" case passwordless = "emailLink" case phoneNumber = "phone" @@ -54,6 +55,8 @@ enum AuthMenu: String { return "Yahoo" case .facebook: return "Facebook" + case .gameCenter: + return "Game Center" case .emailPassword: return "Email & Password Login" case .passwordless: @@ -91,6 +94,8 @@ enum AuthMenu: String { self = .yahoo case "Facebook": self = .facebook + case "Game Center": + self = .gameCenter case "Email & Password Login": self = .emailPassword case "Email Link/Passwordless": @@ -114,7 +119,7 @@ enum AuthMenu: String { extension AuthMenu: DataSourceProvidable { private static var providers: [AuthMenu] { - [.google, .apple, .twitter, .microsoft, .gitHub, .yahoo, .facebook] + [.google, .apple, .twitter, .microsoft, .gitHub, .yahoo, .facebook, .gameCenter] } static var settingsSection: Section { From 3539c2b6917cf8a4cdce71a3323a65329ddd2fe7 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 23 Feb 2024 15:08:29 -0800 Subject: [PATCH 047/104] Privacy Manifests for named Firebase SDKs (#12407) Co-authored-by: Nick Cooke --- .github/workflows/zip.yml | 2 + Crashlytics/Resources/PrivacyInfo.xcprivacy | 66 +++++++++++++++++++ FirebaseABTesting.podspec | 3 + .../Sources/Resources/PrivacyInfo.xcprivacy | 18 +++++ FirebaseAuth.podspec | 3 + .../Sources/Resources/PrivacyInfo.xcprivacy | 50 ++++++++++++++ FirebaseCore.podspec | 4 ++ .../Extension/Resources/PrivacyInfo.xcprivacy | 18 +++++ .../Sources/Resources/PrivacyInfo.xcprivacy | 26 ++++++++ .../Sources/Resources/PrivacyInfo.xcprivacy | 26 ++++++++ FirebaseCoreExtension.podspec | 4 ++ FirebaseCoreInternal.podspec | 4 ++ FirebaseCrashlytics.podspec | 4 ++ FirebaseDynamicLinks.podspec | 3 + .../Sources/Resources/PrivacyInfo.xcprivacy | 46 +++++++++++++ FirebaseFirestore.podspec | 3 + FirebaseFirestoreInternal.podspec | 4 ++ FirebaseInstallations.podspec | 3 + .../Library/Resources/PrivacyInfo.xcprivacy | 30 +++++++++ FirebaseMessaging.podspec | 3 + .../Sources/Resources/PrivacyInfo.xcprivacy | 54 +++++++++++++++ FirebaseRemoteConfig.podspec | 3 + .../Swift/Resources/PrivacyInfo.xcprivacy | 38 +++++++++++ .../Source/Resources/PrivacyInfo.xcprivacy | 30 +++++++++ .../Source/Resources/PrivacyInfo.xcprivacy | 30 +++++++++ Package.swift | 18 ++++- 26 files changed, 490 insertions(+), 3 deletions(-) create mode 100644 Crashlytics/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseABTesting/Sources/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseAuth/Sources/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseCore/Internal/Sources/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseCore/Sources/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseDynamicLinks/Sources/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseInstallations/Source/Library/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseMessaging/Sources/Resources/PrivacyInfo.xcprivacy create mode 100644 FirebaseRemoteConfig/Swift/Resources/PrivacyInfo.xcprivacy create mode 100644 Firestore/Source/Resources/PrivacyInfo.xcprivacy create mode 100644 Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 45578d33761..57c0dafc369 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -7,6 +7,8 @@ on: - '.github/workflows/zip.yml' - 'scripts/build_non_firebase_sdks.sh' - 'Gemfile*' + # DELETE BEFORE pushing + - 'FirebaseCore.podspec' # Don't run based on any markdown only changes. - '!ReleaseTooling/*.md' schedule: diff --git a/Crashlytics/Resources/PrivacyInfo.xcprivacy b/Crashlytics/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..730bb05c863 --- /dev/null +++ b/Crashlytics/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,66 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCrashData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + + diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index 2321afaf72f..60e3584c1f0 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -43,6 +43,9 @@ Firebase Cloud Messaging and Firebase Remote Config in your app. 'Interop/Analytics/Public/*.h', 'FirebaseCore/Extension/*.h', ] + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseABTesting/Sources/Resources/PrivacyInfo.xcprivacy' + } s.requires_arc = base_dir + '*.m' s.public_header_files = base_dir + 'Public/FirebaseABTesting/*.h' s.pod_target_xcconfig = { diff --git a/FirebaseABTesting/Sources/Resources/PrivacyInfo.xcprivacy b/FirebaseABTesting/Sources/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..c89c88f62f5 --- /dev/null +++ b/FirebaseABTesting/Sources/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,18 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyAccessedAPITypes + + + + + diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index 63dc3f0fcc0..5333ffc3b4b 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -41,6 +41,9 @@ supports email and password accounts, as well as several 3rd party authenticatio 'FirebaseAuth/Interop/*.h', ] s.public_header_files = source + 'Public/FirebaseAuth/*.h' + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseAuth/Sources/Resources/PrivacyInfo.xcprivacy' + } s.preserve_paths = [ 'FirebaseAuth/README.md', 'FirebaseAuth/CHANGELOG.md' diff --git a/FirebaseAuth/Sources/Resources/PrivacyInfo.xcprivacy b/FirebaseAuth/Sources/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..123ccfcd6f7 --- /dev/null +++ b/FirebaseAuth/Sources/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,50 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeUserID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + + diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index f629afae4cd..5320d32e4ef 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -36,6 +36,10 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration 'FirebaseCore/Extension/*.h' ] + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseCore/Sources/Resources/PrivacyInfo.xcprivacy' + } + s.swift_version = '5.3' s.public_header_files = 'FirebaseCore/Sources/Public/FirebaseCore/*.h' diff --git a/FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy b/FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..c89c88f62f5 --- /dev/null +++ b/FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,18 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyAccessedAPITypes + + + + + diff --git a/FirebaseCore/Internal/Sources/Resources/PrivacyInfo.xcprivacy b/FirebaseCore/Internal/Sources/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..3fb515ffdd3 --- /dev/null +++ b/FirebaseCore/Internal/Sources/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,26 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + + + diff --git a/FirebaseCore/Sources/Resources/PrivacyInfo.xcprivacy b/FirebaseCore/Sources/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..0244f2f54b0 --- /dev/null +++ b/FirebaseCore/Sources/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,26 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + + diff --git a/FirebaseCoreExtension.podspec b/FirebaseCoreExtension.podspec index a5e6a1344d8..9f8aff68ce6 100644 --- a/FirebaseCoreExtension.podspec +++ b/FirebaseCoreExtension.podspec @@ -30,5 +30,9 @@ Pod::Spec.new do |s| s.source_files = 'FirebaseCore/Extension/*.[hm]' s.public_header_files = 'FirebaseCore/Extension/*.h' + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy' + } + s.dependency 'FirebaseCore', '~> 10.0' end diff --git a/FirebaseCoreInternal.podspec b/FirebaseCoreInternal.podspec index bae9c0dc743..330587c824d 100644 --- a/FirebaseCoreInternal.podspec +++ b/FirebaseCoreInternal.podspec @@ -32,6 +32,10 @@ Pod::Spec.new do |s| 'FirebaseCore/Internal/Sources/**/*.swift' ] + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseCore/Internal/Sources/Resources/PrivacyInfo.xcprivacy' + } + s.swift_version = '5.3' s.dependency 'GoogleUtilities/NSData+zlib', '~> 7.8' diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index b2303029d24..e4b5cebea5c 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -36,6 +36,10 @@ Pod::Spec.new do |s| 'Interop/Analytics/Public/*.h', ] + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'Crashlytics/Resources/PrivacyInfo.xcprivacy' + } + s.public_header_files = [ 'Crashlytics/Crashlytics/Public/FirebaseCrashlytics/*.h' ] diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index d81f919e856..d0c2cf282b8 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -29,6 +29,9 @@ Firebase Dynamic Links are deep links that enhance user experience and increase 'FirebaseCore/Extension/*.h', ] s.public_header_files = 'FirebaseDynamicLinks/Sources/Public/FirebaseDynamicLinks/*.h' + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseDynamicLinks/Sources/Resources/PrivacyInfo.xcprivacy' + } s.frameworks = 'QuartzCore' s.weak_framework = 'WebKit' s.dependency 'FirebaseCore', '~> 10.0' diff --git a/FirebaseDynamicLinks/Sources/Resources/PrivacyInfo.xcprivacy b/FirebaseDynamicLinks/Sources/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..0fccd3b0f69 --- /dev/null +++ b/FirebaseDynamicLinks/Sources/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,46 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDataTypes + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + + + diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 0f80b831868..53d0f8a0877 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -31,6 +31,9 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, 'FirebaseFirestoreInternal/**/*.[mh]', 'Firestore/Swift/Source/**/*.swift', ] + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy' + } s.dependency 'FirebaseCore', '~> 10.0' s.dependency 'FirebaseCoreExtension', '~> 10.0' diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index 39a7e85ecf9..139ef417636 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -87,6 +87,10 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, 'Firestore/core/src/util/secure_random_openssl.cc' ] + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'Firestore/Source/Resources/PrivacyInfo.xcprivacy' + } + s.dependency 'FirebaseAppCheckInterop', '~> 10.17' s.dependency 'FirebaseCore', '~> 10.0' diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index 7ba214b8d73..815ac860204 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -40,6 +40,9 @@ Pod::Spec.new do |s| s.public_header_files = [ base_dir + 'Library/Public/FirebaseInstallations/*.h', ] + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseInstallations/Source/Library/Resources/PrivacyInfo.xcprivacy' + } s.framework = 'Security' s.dependency 'FirebaseCore', '~> 10.0' diff --git a/FirebaseInstallations/Source/Library/Resources/PrivacyInfo.xcprivacy b/FirebaseInstallations/Source/Library/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..1e83fa67acd --- /dev/null +++ b/FirebaseInstallations/Source/Library/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,30 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + + + + diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index cffc83756e8..0cee992e8b3 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -45,6 +45,9 @@ device, and it is completely free. 'FirebaseInstallations/Source/Library/Private/*.h', ] s.public_header_files = base_dir + 'Sources/Public/FirebaseMessaging/*.h' + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseMessaging/Sources/Resources/PrivacyInfo.xcprivacy' + } s.library = 'sqlite3' s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', diff --git a/FirebaseMessaging/Sources/Resources/PrivacyInfo.xcprivacy b/FirebaseMessaging/Sources/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..3b29f79eeaf --- /dev/null +++ b/FirebaseMessaging/Sources/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,54 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDataTypes + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyAccessedAPITypes + + + + + diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index a19552b9463..1efbc8ff746 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -43,6 +43,9 @@ app update. 'FirebaseRemoteConfig/Swift/**/*.swift', ] s.public_header_files = base_dir + 'Public/FirebaseRemoteConfig/*.h' + s.resource_bundles = { + "#{s.module_name}_Privacy" => 'FirebaseRemoteConfig/Swift/Resources/PrivacyInfo.xcprivacy' + } s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' diff --git a/FirebaseRemoteConfig/Swift/Resources/PrivacyInfo.xcprivacy b/FirebaseRemoteConfig/Swift/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..719a06f4937 --- /dev/null +++ b/FirebaseRemoteConfig/Swift/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,38 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + + + diff --git a/Firestore/Source/Resources/PrivacyInfo.xcprivacy b/Firestore/Source/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..1e83fa67acd --- /dev/null +++ b/Firestore/Source/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,30 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + + + + diff --git a/Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy b/Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000000..1e83fa67acd --- /dev/null +++ b/Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,30 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + + + + diff --git a/Package.swift b/Package.swift index eaf1ce91704..5309b8ddbfd 100644 --- a/Package.swift +++ b/Package.swift @@ -199,6 +199,7 @@ let package = Package( .product(name: "GULLogger", package: "GoogleUtilities"), ], path: "FirebaseCore/Sources", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "Public", cSettings: [ .headerSearchPath("../.."), @@ -232,6 +233,7 @@ let package = Package( .target( name: "FirebaseCoreExtension", path: "FirebaseCore/Extension", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: ".", cSettings: [ .headerSearchPath("../../"), @@ -246,7 +248,8 @@ let package = Package( dependencies: [ .product(name: "GULNSData", package: "GoogleUtilities"), ], - path: "FirebaseCore/Internal/Sources" + path: "FirebaseCore/Internal/Sources", + resources: [.process("Resources/PrivacyInfo.xcprivacy")] ), .testTarget( name: "FirebaseCoreInternalTests", @@ -260,6 +263,7 @@ let package = Package( name: "FirebaseABTesting", dependencies: ["FirebaseCore"], path: "FirebaseABTesting/Sources", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "Public", cSettings: [ .headerSearchPath("../../"), @@ -431,6 +435,7 @@ let package = Package( .product(name: "RecaptchaInterop", package: "interop-ios-for-google-sdks"), ], path: "FirebaseAuth/Sources", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "Public", cSettings: [ .headerSearchPath("../../"), @@ -516,6 +521,7 @@ let package = Package( "Shared/", "third_party/libunwind/dwarf.h", ], + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "Crashlytics/Public", cSettings: [ .headerSearchPath(".."), @@ -648,6 +654,7 @@ let package = Package( name: "FirebaseDynamicLinks", dependencies: ["FirebaseCore"], path: "FirebaseDynamicLinks/Sources", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "Public", cSettings: [ .headerSearchPath("../../"), @@ -794,6 +801,7 @@ let package = Package( .product(name: "GULUserDefaults", package: "GoogleUtilities"), ], path: "FirebaseInstallations/Source/Library", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "Public", cSettings: [ .headerSearchPath("../../../"), @@ -839,6 +847,7 @@ let package = Package( .product(name: "nanopb", package: "nanopb"), ], path: "FirebaseMessaging/Sources", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], publicHeadersPath: "Public", cSettings: [ .headerSearchPath("../../"), @@ -993,7 +1002,8 @@ let package = Package( "FirebaseRemoteConfigInternal", "FirebaseSharedSwift", ], - path: "FirebaseRemoteConfig/Swift" + path: "FirebaseRemoteConfig/Swift", + resources: [.process("Resources/PrivacyInfo.xcprivacy")] ), .target( name: "FirebaseRemoteConfigSwift", @@ -1465,7 +1475,8 @@ func firestoreTargets() -> [Target] { ], sources: [ "Swift/Source/", - ] + ], + resources: [.process("Source/Resources/PrivacyInfo.xcprivacy")] ), ] } @@ -1513,6 +1524,7 @@ func firestoreTargets() -> [Target] { "FirebaseSharedSwift", ], path: "Firestore/Swift/Source", + resources: [.process("Resources/PrivacyInfo.xcprivacy")], linkerSettings: [ .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS, .tvOS])), .linkedFramework("UIKit", .when(platforms: [.iOS, .tvOS])), From e46346686f164c1d37252c48773f797984e1a6c6 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 26 Feb 2024 10:31:14 -0800 Subject: [PATCH 048/104] Fix metadata.md generation (#12423) --- .../Sources/Utils/FileManager+Utils.swift | 18 +++++++----------- .../Sources/ZipBuilder/ResourcesManager.swift | 4 ++-- .../Sources/ZipBuilder/ZipBuilder.swift | 6 +++++- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ReleaseTooling/Sources/Utils/FileManager+Utils.swift b/ReleaseTooling/Sources/Utils/FileManager+Utils.swift index 1a657fd5155..5f25c4e5c83 100644 --- a/ReleaseTooling/Sources/Utils/FileManager+Utils.swift +++ b/ReleaseTooling/Sources/Utils/FileManager+Utils.swift @@ -28,9 +28,6 @@ public extension FileManager { /// All folders with a `.bundle` extension. case bundles - /// All folders with a `.bundle` extension excluding privacy manifest bundles. - case nonPrivacyBundles - /// A directory with an optional name. If name is `nil`, all directories will be matched. case directories(name: String?) @@ -162,7 +159,13 @@ public extension FileManager { // Recursively search using the enumerator, adding any matches to the array. var matches: [URL] = [] var foundXcframework = false // Ignore .frameworks after finding an xcframework. - while let fileURL = dirEnumerator.nextObject() as? URL { + for case let fileURL as URL in dirEnumerator { + // Never mess with Privacy.bundles + if fileURL.lastPathComponent.hasSuffix("_Privacy.bundle") { + dirEnumerator.skipDescendants() + continue + } + switch type { case .allFiles: // Skip directories, include everything else. @@ -189,13 +192,6 @@ public extension FileManager { if fileURL.pathExtension == "bundle" { matches.append(fileURL) } - case .nonPrivacyBundles: - // The only thing of interest is the path extension being ".bundle", but not a privacy - // bundle. - if fileURL.pathExtension == "bundle", - !fileURL.lastPathComponent.hasSuffix("_Privacy.bundle") { - matches.append(fileURL) - } case .headers: if fileURL.pathExtension == "h" { matches.append(fileURL) diff --git a/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift b/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift index 48d1a964db8..fd2a6ad6a00 100644 --- a/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift +++ b/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift @@ -30,7 +30,7 @@ extension ResourcesManager { static func directoryContainsResources(_ dir: URL) throws -> Bool { // First search for any .bundle files. let fileManager = FileManager.default - let bundles = try fileManager.recursivelySearch(for: .nonPrivacyBundles, in: dir) + let bundles = try fileManager.recursivelySearch(for: .bundles, in: dir) // Stop searching if there were any bundles found. if !bundles.isEmpty { return true } @@ -168,7 +168,7 @@ extension ResourcesManager { to resourceDir: URL, keepOriginal: Bool = false) throws -> [URL] { let fileManager = FileManager.default - let allBundles = try fileManager.recursivelySearch(for: .nonPrivacyBundles, in: dir) + let allBundles = try fileManager.recursivelySearch(for: .bundles, in: dir) // If no bundles are found, return an empty array since nothing was done (but there wasn't an // error). diff --git a/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift index aeca4a52dc5..ea46bd64085 100644 --- a/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift @@ -243,7 +243,11 @@ struct ZipBuilder { podInfo: podInfo) carthageGoogleUtilitiesFrameworks += cdFrameworks } - if resourceContents != nil { + let fileManager = FileManager.default + if let resourceContents, + let contents = try? fileManager.contentsOfDirectory(at: resourceContents, + includingPropertiesForKeys: nil), + !contents.isEmpty { resources[podName] = resourceContents } } else if podsBuilt[podName] == nil { From 7147ecc36d588136cc1cf2d29f7e497b34317afa Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 26 Feb 2024 10:31:27 -0800 Subject: [PATCH 049/104] Fix FirebaseSessions release mode crash (#12425) --- Crashlytics/CHANGELOG.md | 3 ++- .../Sources/SessionStartEvent.swift | 13 +----------- .../SourcesObjC/NanoPB/FIRSESNanoPBHelpers.h | 3 +++ .../SourcesObjC/NanoPB/FIRSESNanoPBHelpers.m | 20 +++++++++++++++++++ Package.swift | 2 ++ 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index f040eecef96..52d43c7359d 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,5 +1,6 @@ -# Unreleased +# 10.22.0 - [changed] Removed calls to statfs in the Crashlytics SDK to comply with Apple Privacy Manifests. This change removes support for collecting Disk Space Free in Crashlytics reports. +- [fixed] Fixed FirebaseSessions crash on startup that occurs in release mode in Xcode 15.3 and other build configurations. (#11403) # 10.16.0 - [fixed] Fixed a memory leak regression when generating session events (#11725). diff --git a/FirebaseSessions/Sources/SessionStartEvent.swift b/FirebaseSessions/Sources/SessionStartEvent.swift index c107849a662..5a8d0d0148d 100644 --- a/FirebaseSessions/Sources/SessionStartEvent.swift +++ b/FirebaseSessions/Sources/SessionStartEvent.swift @@ -150,18 +150,7 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject { // MARK: - GDTCOREventDataObject func transportBytes() -> Data { - var fields = firebase_appquality_sessions_SessionEvent_fields - var error: NSError? - let data = FIRSESEncodeProto(&fields.0, &proto, &error) - if error != nil { - Logger - .logError("Session Event failed to encode as proto with error: \(error.debugDescription)") - } - guard let data = data else { - Logger.logError("Session Event generated nil transportBytes. Returning empty data.") - return Data() - } - return data + return FIRSESTransportBytes(&proto) } // MARK: - Data Conversion diff --git a/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.h b/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.h index 8956ff022a6..e07f0e344ed 100644 --- a/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.h +++ b/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.h @@ -91,6 +91,9 @@ pb_size_t FIRSESGetAppleApplicationInfoTag(void); /// private method in GULAppEnvironmentUtil. NSString* _Nullable FIRSESGetSysctlEntry(const char* sysctlKey); +/// C function to bridge from Swift to do nanopb bytes transfer. +NSData* FIRSESTransportBytes(const void* _Nonnull proto); + NS_ASSUME_NONNULL_END #endif /* FIRSESNanoPBHelpers_h */ diff --git a/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.m b/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.m index 22cb8a61b2f..6440ec4d886 100644 --- a/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.m +++ b/FirebaseSessions/SourcesObjC/NanoPB/FIRSESNanoPBHelpers.m @@ -21,6 +21,8 @@ #import "FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.h" +@import FirebaseCoreExtension; + #import #import #import @@ -182,4 +184,22 @@ pb_size_t FIRSESGetAppleApplicationInfoTag(void) { } } +NSData *FIRSESTransportBytes(const void *_Nonnull proto) { + const pb_field_t *fields = firebase_appquality_sessions_SessionEvent_fields; + NSError *error; + NSData *data = FIRSESEncodeProto(fields, proto, &error); + if (error != nil) { + FIRLogError( + @"FirebaseSessions", @"I-SES000001", @"%@", + [NSString stringWithFormat:@"Session Event failed to encode as proto with error: %@", + error.debugDescription]); + } + if (data == nil) { + data = [NSData data]; + FIRLogError(@"FirebaseSessions", @"I-SES000002", + @"Session Event generated nil transportBytes. Returning empty data."); + } + return data; +} + NS_ASSUME_NONNULL_END diff --git a/Package.swift b/Package.swift index 5309b8ddbfd..9670eff963b 100644 --- a/Package.swift +++ b/Package.swift @@ -1076,6 +1076,8 @@ let package = Package( .target( name: "FirebaseSessionsObjC", dependencies: [ + "FirebaseCore", + "FirebaseCoreExtension", .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "nanopb", package: "nanopb"), ], From cb2f6e901ad6b2dc4215f8ecc8b7a154211d9939 Mon Sep 17 00:00:00 2001 From: themiswang Date: Mon, 26 Feb 2024 15:10:21 -0500 Subject: [PATCH 050/104] Sending authentication token for crashlytics and session (#12383) --- Crashlytics/CHANGELOG.md | 4 ++ .../Controllers/FIRCLSReportUploader.h | 1 + .../Controllers/FIRCLSReportUploader.m | 7 +- .../Models/FIRCLSInstallIdentifierModel.h | 2 +- .../Models/FIRCLSInstallIdentifierModel.m | 34 ++++++--- .../Models/Record/FIRCLSReportAdapter.h | 3 +- .../Models/Record/FIRCLSReportAdapter.m | 6 +- .../Protogen/nanopb/crashlytics.nanopb.c | 3 +- .../Protogen/nanopb/crashlytics.nanopb.h | 4 +- .../UnitTests/FIRCLSContextManagerTests.m | 9 ++- .../FIRCLSInstallIdentifierModelTests.m | 69 +++++++++++++------ .../UnitTests/FIRCLSReportAdapterTests.m | 4 +- .../UnitTests/Mocks/FIRMockInstallations.h | 3 + .../UnitTests/Mocks/FIRMockInstallations.m | 17 +++++ .../Development/DevEventConsoleLogger.swift | 2 + .../Sources/FirebaseSessions.swift | 4 ++ .../Sources/FirebaseSessionsError.swift | 2 + .../Installations+InstallationsProtocol.swift | 54 +++++++++++++-- .../Sources/SessionCoordinator.swift | 6 +- .../Sources/SessionStartEvent.swift | 7 ++ .../Settings/SettingsDownloadClient.swift | 4 +- .../Protogen/nanopb/sessions.nanopb.c | 3 +- .../Protogen/nanopb/sessions.nanopb.h | 4 +- .../Mocks/MockInstallationsProtocol.swift | 21 +++++- .../Tests/Unit/SessionCoordinatorTests.swift | 16 +++++ 25 files changed, 233 insertions(+), 56 deletions(-) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 52d43c7359d..6f16b3d7425 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased + +- [fixed] Force validation or rotation of FIDs for FirebaseSessions. + # 10.22.0 - [changed] Removed calls to statfs in the Crashlytics SDK to comply with Apple Privacy Manifests. This change removes support for collecting Disk Space Free in Crashlytics reports. - [fixed] Fixed FirebaseSessions crash on startup that occurs in release mode in Xcode 15.3 and other build configurations. (#11403) diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h b/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h index 6f134b55296..3723a50e464 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h @@ -28,6 +28,7 @@ @property(nonatomic, readonly) NSOperationQueue *operationQueue; @property(nonatomic, readonly) FIRCLSFileManager *fileManager; @property(nonatomic, copy) NSString *fiid; +@property(nonatomic, copy) NSString *authToken; - (void)prepareAndSubmitReport:(FIRCLSInternalReport *)report dataCollectionToken:(FIRCLSDataCollectionToken *)dataCollectionToken diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.m b/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.m index caf1bae252d..0de639c6e86 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.m +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.m @@ -95,8 +95,10 @@ - (void)prepareAndSubmitReport:(FIRCLSInternalReport *)report // urgent mode. Since urgent mode happens when the app is in a crash loop, // we can safely assume users aren't rotating their FIID, so this can be skipped. if (!urgent) { - [self.installIDModel regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull newFIID) { + [self.installIDModel regenerateInstallIDIfNeededWithBlock:^( + NSString *_Nonnull newFIID, NSString *_Nonnull authToken) { self.fiid = [newFIID copy]; + self.authToken = [authToken copy]; }]; } else { FIRCLSWarningLog( @@ -186,7 +188,8 @@ - (void)uploadPackagedReportAtPath:(NSString *)path FIRCLSReportAdapter *adapter = [[FIRCLSReportAdapter alloc] initWithPath:path googleAppId:self.googleAppID installIDModel:self.installIDModel - fiid:self.fiid]; + fiid:self.fiid + authToken:self.authToken]; GDTCOREvent *event = [self.googleTransport eventForTransport]; event.dataObject = adapter; diff --git a/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.h b/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.h index fd33a4b6959..4e0f4832162 100644 --- a/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.h +++ b/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.h @@ -44,7 +44,7 @@ NS_ASSUME_NONNULL_BEGIN * - Concern 2: Whatever the FIID is, we should send it with the Crash report so we're in sync with * Sessions and other Firebase SDKs */ -- (BOOL)regenerateInstallIDIfNeededWithBlock:(void (^)(NSString *fiid))block; +- (BOOL)regenerateInstallIDIfNeededWithBlock:(void (^)(NSString *fiid, NSString *authToken))block; @end diff --git a/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.m b/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.m index 9cc936bb50e..fbdcc21f80b 100644 --- a/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.m +++ b/Crashlytics/Crashlytics/Models/FIRCLSInstallIdentifierModel.m @@ -98,33 +98,45 @@ - (NSString *)generateInstallationUUID { #pragma mark Privacy Shield -- (BOOL)regenerateInstallIDIfNeededWithBlock:(void (^)(NSString *fiid))block { +- (BOOL)regenerateInstallIDIfNeededWithBlock:(void (^)(NSString *fiid, NSString *authToken))block { BOOL __block didRotate = false; + NSString __block *authTokenComplete = @""; + NSString __block *currentIIDComplete = @""; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + // Installations Completions run async, so wait a reasonable amount of time for it to finish. + dispatch_group_t workingGroup = dispatch_group_create(); - // This runs Completion async, so wait a reasonable amount of time for it to finish. + dispatch_group_enter(workingGroup); [self.installations - installationIDWithCompletion:^(NSString *_Nullable currentIID, NSError *_Nullable error) { - // Provide the IID to the callback. For this case we don't care - // if the FIID is null because it's the best we can do - we just want - // to send up the same FIID that is sent by other SDKs (eg. the Sessions SDK). - block(currentIID); + authTokenWithCompletion:^(FIRInstallationsAuthTokenResult *_Nullable tokenResult, + NSError *_Nullable error) { + authTokenComplete = tokenResult.authToken; + dispatch_group_leave(workingGroup); + }]; + dispatch_group_enter(workingGroup); + [self.installations + installationIDWithCompletion:^(NSString *_Nullable currentIID, NSError *_Nullable error) { + currentIIDComplete = currentIID; didRotate = [self rotateCrashlyticsInstallUUIDWithIID:currentIID error:error]; if (didRotate) { FIRCLSInfoLog(@"Rotated Crashlytics Install UUID because Firebase Install ID changed"); } - dispatch_semaphore_signal(semaphore); + dispatch_group_leave(workingGroup); }]; - intptr_t result = dispatch_semaphore_wait( - semaphore, dispatch_time(DISPATCH_TIME_NOW, FIRCLSInstallationsWaitTime)); + intptr_t result = dispatch_group_wait( + workingGroup, dispatch_time(DISPATCH_TIME_NOW, FIRCLSInstallationsWaitTime)); + if (result != 0) { FIRCLSErrorLog(@"Crashlytics timed out while checking for Firebase Installation ID"); } + // Provide the IID to the callback. For this case we don't care + // if the FIID is null because it's the best we can do - we just want + // to send up the same FIID that is sent by other SDKs (eg. the Sessions SDK). + block(currentIIDComplete, authTokenComplete); return didRotate; } diff --git a/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.h b/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.h index 24afcdeff91..4db047cf58f 100644 --- a/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.h +++ b/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.h @@ -35,5 +35,6 @@ - (instancetype)initWithPath:(NSString *)folderPath googleAppId:(NSString *)googleAppID installIDModel:(FIRCLSInstallIdentifierModel *)installIDModel - fiid:(NSString *)fiid; + fiid:(NSString *)fiid + authToken:(NSString *)authToken; @end diff --git a/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.m b/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.m index 3d52bbeb844..2cbcdb9ef23 100644 --- a/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.m +++ b/Crashlytics/Crashlytics/Models/Record/FIRCLSReportAdapter.m @@ -30,6 +30,7 @@ @interface FIRCLSReportAdapter () @property(nonatomic, strong) FIRCLSInstallIdentifierModel *installIDModel; @property(nonatomic, copy) NSString *fiid; +@property(nonatomic, copy) NSString *authToken; @end @@ -38,13 +39,15 @@ @implementation FIRCLSReportAdapter - (instancetype)initWithPath:(NSString *)folderPath googleAppId:(NSString *)googleAppID installIDModel:(FIRCLSInstallIdentifierModel *)installIDModel - fiid:(NSString *)fiid { + fiid:(NSString *)fiid + authToken:(NSString *)authToken { self = [super init]; if (self) { _folderPath = folderPath; _googleAppID = googleAppID; _installIDModel = installIDModel; _fiid = [fiid copy]; + _authToken = [authToken copy]; [self loadMetaDataFile]; @@ -156,6 +159,7 @@ - (google_crashlytics_Report)protoReport { report.installation_uuid = FIRCLSEncodeString(self.installIDModel.installID); report.firebase_installation_id = FIRCLSEncodeString(self.fiid); report.app_quality_session_id = FIRCLSEncodeString(self.identity.app_quality_session_id); + report.firebase_authentication_token = FIRCLSEncodeString(self.authToken); report.build_version = FIRCLSEncodeString(self.application.build_version); report.display_version = FIRCLSEncodeString(self.application.display_version); report.apple_payload = [self protoFilesPayload]; diff --git a/Crashlytics/Protogen/nanopb/crashlytics.nanopb.c b/Crashlytics/Protogen/nanopb/crashlytics.nanopb.c index cd9c87d50bd..cb88acc139f 100644 --- a/Crashlytics/Protogen/nanopb/crashlytics.nanopb.c +++ b/Crashlytics/Protogen/nanopb/crashlytics.nanopb.c @@ -26,7 +26,7 @@ -const pb_field_t google_crashlytics_Report_fields[10] = { +const pb_field_t google_crashlytics_Report_fields[11] = { PB_FIELD( 1, BYTES , SINGULAR, POINTER , FIRST, google_crashlytics_Report, sdk_version, sdk_version, 0), PB_FIELD( 3, BYTES , SINGULAR, POINTER , OTHER, google_crashlytics_Report, gmp_app_id, sdk_version, 0), PB_FIELD( 4, UENUM , SINGULAR, STATIC , OTHER, google_crashlytics_Report, platform, gmp_app_id, 0), @@ -36,6 +36,7 @@ const pb_field_t google_crashlytics_Report_fields[10] = { PB_FIELD( 10, MESSAGE , SINGULAR, STATIC , OTHER, google_crashlytics_Report, apple_payload, display_version, &google_crashlytics_FilesPayload_fields), PB_FIELD( 16, BYTES , SINGULAR, POINTER , OTHER, google_crashlytics_Report, firebase_installation_id, apple_payload, 0), PB_FIELD( 17, BYTES , SINGULAR, POINTER , OTHER, google_crashlytics_Report, app_quality_session_id, firebase_installation_id, 0), + PB_FIELD( 19, BYTES , SINGULAR, POINTER , OTHER, google_crashlytics_Report, firebase_authentication_token, app_quality_session_id, 0), PB_LAST_FIELD }; diff --git a/Crashlytics/Protogen/nanopb/crashlytics.nanopb.h b/Crashlytics/Protogen/nanopb/crashlytics.nanopb.h index ed9838ad47d..4ad4f89a120 100644 --- a/Crashlytics/Protogen/nanopb/crashlytics.nanopb.h +++ b/Crashlytics/Protogen/nanopb/crashlytics.nanopb.h @@ -61,6 +61,7 @@ typedef struct _google_crashlytics_Report { google_crashlytics_FilesPayload apple_payload; pb_bytes_array_t *firebase_installation_id; pb_bytes_array_t *app_quality_session_id; + pb_bytes_array_t *firebase_authentication_token; /* @@protoc_insertion_point(struct:google_crashlytics_Report) */ } google_crashlytics_Report; @@ -84,12 +85,13 @@ typedef struct _google_crashlytics_Report { #define google_crashlytics_Report_installation_uuid_tag 5 #define google_crashlytics_Report_firebase_installation_id_tag 16 #define google_crashlytics_Report_app_quality_session_id_tag 17 +#define google_crashlytics_Report_firebase_authentication_token 19 #define google_crashlytics_Report_build_version_tag 6 #define google_crashlytics_Report_display_version_tag 7 #define google_crashlytics_Report_apple_payload_tag 10 /* Struct field encoding specification for nanopb */ -extern const pb_field_t google_crashlytics_Report_fields[10]; +extern const pb_field_t google_crashlytics_Report_fields[11]; extern const pb_field_t google_crashlytics_FilesPayload_fields[2]; extern const pb_field_t google_crashlytics_FilesPayload_File_fields[3]; diff --git a/Crashlytics/UnitTests/FIRCLSContextManagerTests.m b/Crashlytics/UnitTests/FIRCLSContextManagerTests.m index 249b85f4991..887eb442bf0 100644 --- a/Crashlytics/UnitTests/FIRCLSContextManagerTests.m +++ b/Crashlytics/UnitTests/FIRCLSContextManagerTests.m @@ -75,7 +75,8 @@ - (void)test_notSettingSessionID_protoHasNilSessionID { FIRCLSReportAdapter *adapter = [[FIRCLSReportAdapter alloc] initWithPath:self.report.path googleAppId:@"TestGoogleAppID" installIDModel:self.installIDModel - fiid:@"TestFIID"]; + fiid:@"TestFIID" + authToken:@"TestAuthToken"]; XCTAssertEqualObjects(adapter.identity.app_quality_session_id, @""); } @@ -92,7 +93,8 @@ - (void)test_settingSessionIDMultipleTimes_protoHasLastSessionID { FIRCLSReportAdapter *adapter = [[FIRCLSReportAdapter alloc] initWithPath:self.report.path googleAppId:@"TestGoogleAppID" installIDModel:self.installIDModel - fiid:@"TestFIID"]; + fiid:@"TestFIID" + authToken:@"TestAuthToken"]; NSLog(@"reportPath: %@", self.report.path); XCTAssertEqualObjects(adapter.identity.app_quality_session_id, TestContextSessionID2); @@ -110,7 +112,8 @@ - (void)test_settingSessionIDOutOfOrder_protoHasLastSessionID { FIRCLSReportAdapter *adapter = [[FIRCLSReportAdapter alloc] initWithPath:self.report.path googleAppId:@"TestGoogleAppID" installIDModel:self.installIDModel - fiid:@"TestFIID"]; + fiid:@"TestFIID" + authToken:@"TestAuthToken"]; NSLog(@"reportPath: %@", self.report.path); XCTAssertEqualObjects(adapter.identity.app_quality_session_id, TestContextSessionID2); diff --git a/Crashlytics/UnitTests/FIRCLSInstallIdentifierModelTests.m b/Crashlytics/UnitTests/FIRCLSInstallIdentifierModelTests.m index b95b65b3ff6..6ccbc00b932 100644 --- a/Crashlytics/UnitTests/FIRCLSInstallIdentifierModelTests.m +++ b/Crashlytics/UnitTests/FIRCLSInstallIdentifierModelTests.m @@ -66,10 +66,13 @@ - (void)testCreateUUIDAndRotate { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; sleep(1); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); XCTAssertFalse(didRotate); XCTAssertEqualObjects([_defaults objectForKey:FABInstallationUUIDKey], model.installID); XCTAssertNil([_defaults objectForKey:FABInstallationADIDKey]); @@ -85,10 +88,13 @@ - (void)testCreateUUIDAndErrorGettingInstanceID { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertFalse(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); XCTAssertEqualObjects([_defaults objectForKey:FABInstallationUUIDKey], model.installID); XCTAssertNil([_defaults objectForKey:FABInstallationADIDKey]); XCTAssertEqualObjects(nil, [_defaults objectForKey:FIRCLSInstallationIIDHashKey]); @@ -135,10 +141,13 @@ - (void)testIIDChanges { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertTrue(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); // Test that the UUID changed. XCTAssertNotEqualObjects(model.installID, @"old_uuid"); XCTAssertEqualObjects([_defaults objectForKey:FABInstallationUUIDKey], model.installID); @@ -158,10 +167,13 @@ - (void)testIIDDoesntChange { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertFalse(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); // Test that the UUID changed. XCTAssertEqualObjects(model.installID, @"test_uuid"); XCTAssertEqualObjects([_defaults objectForKey:FABInstallationUUIDKey], model.installID); @@ -180,10 +192,13 @@ - (void)testUUIDSetButNeverIIDNilIID { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertFalse(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); // Test that the UUID did not change. The FIID can be nil if // there's no FIID cached, so we can't say whether to regenerate XCTAssertEqualObjects(model.installID, @"old_uuid"); @@ -202,10 +217,13 @@ - (void)testUUIDSetButNeverIIDWithIID { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertFalse(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); // Test that the UUID did not change. The FIID can be nil if // there's no FIID cached, so we can't say whether to regenerate XCTAssertEqualObjects(model.installID, @"old_uuid"); @@ -226,10 +244,14 @@ - (void)testADIDWasSetButNeverIID { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertFalse(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); + // Test that the UUID didn't change. XCTAssertEqualObjects(model.installID, @"test_uuid"); XCTAssertEqualObjects([_defaults objectForKey:FABInstallationUUIDKey], model.installID); @@ -248,10 +270,13 @@ - (void)testADIDWasSetAndIIDBecomesSet { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertFalse(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); // Test that the UUID didn't change. XCTAssertEqualObjects(model.installID, @"test_uuid"); XCTAssertEqualObjects([_defaults objectForKey:FABInstallationUUIDKey], model.installID); @@ -272,8 +297,9 @@ - (void)testADIDAndIIDWereSet { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertFalse(didRotate); // Test that the UUID didn't change. @@ -297,10 +323,13 @@ - (void)testADIDAndIIDWereSet2 { [[FIRCLSInstallIdentifierModel alloc] initWithInstallations:iid]; XCTAssertNotNil(model.installID); - BOOL didRotate = [model regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid){ - }]; + BOOL didRotate = [model + regenerateInstallIDIfNeededWithBlock:^(NSString *_Nonnull fiid, NSString *_Nonnull authToken){ + }]; XCTAssertTrue(didRotate); + XCTAssertTrue(iid.authTokenFinished); + XCTAssertTrue(iid.installationIDFinished); // Test that the UUID change. XCTAssertNotEqualObjects(model.installID, @"test_uuid"); XCTAssertEqualObjects([_defaults objectForKey:FABInstallationUUIDKey], model.installID); diff --git a/Crashlytics/UnitTests/FIRCLSReportAdapterTests.m b/Crashlytics/UnitTests/FIRCLSReportAdapterTests.m index 1298298ac9c..ae53871f043 100644 --- a/Crashlytics/UnitTests/FIRCLSReportAdapterTests.m +++ b/Crashlytics/UnitTests/FIRCLSReportAdapterTests.m @@ -32,6 +32,7 @@ @interface FIRCLSReportAdapterTests : XCTestCase @end static NSString *const TestFIID = @"TEST_FIID"; +static NSString *const TestAuthToken = @"TEST_AUTH_TOKEN"; @implementation FIRCLSReportAdapterTests @@ -46,7 +47,8 @@ - (FIRCLSReportAdapter *)constructAdapterWithPath:(NSString *)path return [[FIRCLSReportAdapter alloc] initWithPath:path googleAppId:googleAppID installIDModel:installIDModel - fiid:TestFIID]; + fiid:TestFIID + authToken:TestAuthToken]; } /// Attempt sending a proto report to the reporting endpoint diff --git a/Crashlytics/UnitTests/Mocks/FIRMockInstallations.h b/Crashlytics/UnitTests/Mocks/FIRMockInstallations.h index f697c9a6654..6d3ab54d1be 100644 --- a/Crashlytics/UnitTests/Mocks/FIRMockInstallations.h +++ b/Crashlytics/UnitTests/Mocks/FIRMockInstallations.h @@ -16,6 +16,9 @@ @interface FIRMockInstallations : FIRInstallations +@property(nonatomic) BOOL authTokenFinished; +@property(nonatomic) BOOL installationIDFinished; + - (instancetype)initWithFID:(NSString *)installationID; - (instancetype)initWithError:(NSError *)error; diff --git a/Crashlytics/UnitTests/Mocks/FIRMockInstallations.m b/Crashlytics/UnitTests/Mocks/FIRMockInstallations.m index 25dda90e724..7d546a6ca8c 100644 --- a/Crashlytics/UnitTests/Mocks/FIRMockInstallations.m +++ b/Crashlytics/UnitTests/Mocks/FIRMockInstallations.m @@ -21,11 +21,24 @@ @interface FIRMockInstallationsImpl : NSObject @property(nonatomic, copy) NSString *installationID; @property(nonatomic, strong) NSError *error; +// the init function is not public for the token result, use as a placeholder to mock the token +// completion block +@property(nonatomic, strong) FIRInstallationsAuthTokenResult *tokenResult; + +@property(nonatomic) BOOL authTokenFinished; +@property(nonatomic) BOOL installationIDFinished; + @end @implementation FIRMockInstallationsImpl +- (void)authTokenWithCompletion:(FIRInstallationsTokenHandler)completion { + self.authTokenFinished = true; + completion(self.tokenResult, self.error); +} + - (void)installationIDWithCompletion:(FIRInstallationsIDHandler)completion { + self.installationIDFinished = true; completion(self.installationID, self.error); } @@ -41,6 +54,8 @@ - (instancetype)initWithFID:(NSString *)installationID { FIRMockInstallationsImpl *mock = [[FIRMockInstallationsImpl alloc] init]; mock.installationID = [installationID copy]; mock.error = nil; + mock.authTokenFinished = false; + mock.installationIDFinished = false; self = (id)mock; return self; } @@ -49,6 +64,8 @@ - (instancetype)initWithError:(NSError *)error { FIRMockInstallationsImpl *mock = [[FIRMockInstallationsImpl alloc] init]; mock.installationID = nil; mock.error = error; + mock.authTokenFinished = false; + mock.installationIDFinished = false; self = (id)mock; return self; } diff --git a/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift b/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift index 84e31d63eff..7d32a7b63db 100644 --- a/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift +++ b/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift @@ -42,6 +42,8 @@ class DevEventConsoleLogger: EventGDTLoggerProtocol { session_index: \(proto.session_data.session_index) event_timestamp_us: \(proto.session_data.event_timestamp_us) firebase_installation_id: \(proto.session_data.firebase_installation_id.description) + firebase_autheticationtion_token: + \(proto.session_data.firebase_authentication_token.description) data_collection_status crashlytics: \(proto.session_data.data_collection_status.crashlytics) performance: \(proto.session_data.data_collection_status.performance) diff --git a/FirebaseSessions/Sources/FirebaseSessions.swift b/FirebaseSessions/Sources/FirebaseSessions.swift index 9c8f7871f22..269cfb9061c 100644 --- a/FirebaseSessions/Sources/FirebaseSessions.swift +++ b/FirebaseSessions/Sources/FirebaseSessions.swift @@ -118,6 +118,10 @@ private enum GoogleDataTransportConfig { .logDebug( "Data Collection is disabled for all subscribers. Skipping this Session Event" ) + case .SessionInstallationsTimeOutError: + Logger.logError( + "Error getting Firebase Installation ID due to timeout. Skipping this Session Event" + ) } } } diff --git a/FirebaseSessions/Sources/FirebaseSessionsError.swift b/FirebaseSessions/Sources/FirebaseSessionsError.swift index 0fae03aa31a..12ed1fb139b 100644 --- a/FirebaseSessions/Sources/FirebaseSessionsError.swift +++ b/FirebaseSessions/Sources/FirebaseSessionsError.swift @@ -20,6 +20,8 @@ enum FirebaseSessionsError: Error { case SessionSamplingError /// Firebase Installation ID related error case SessionInstallationsError(Error) + /// Firebase Installation ID related timeout error + case SessionInstallationsTimeOutError /// Error from the GoogleDataTransport SDK case DataTransportError(Error) /// Sessions SDK is disabled via settings error diff --git a/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift b/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift index 47d7a4f1ed5..17ab5569694 100644 --- a/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift +++ b/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift @@ -16,19 +16,63 @@ import Foundation @_implementationOnly import FirebaseInstallations - protocol InstallationsProtocol { - func installationID(completion: @escaping (Result) -> Void) + var installationsWaitTimeInSecond: Int { get } + + /// Override Installation function for testing + func authToken(completion: @escaping (InstallationsAuthTokenResult?, Error?) -> Void) + + /// Override Installation function for testing + func installationID(completion: @escaping (String?, Error?) -> Void) + + /// Return a tuple: (installationID, authenticationToken) for success result + func installationID(completion: @escaping (Result<(String, String), Error>) -> Void) } -extension Installations: InstallationsProtocol { - func installationID(completion: @escaping (Result) -> Void) { +extension InstallationsProtocol { + var installationsWaitTimeInSecond: Int { + return 10 + } + + func installationID(completion: @escaping (Result<(String, String), Error>) -> Void) { + var authTokenComplete = "" + var intallationComplete: String? + var errorComplete: Error? + + let workingGroup = DispatchGroup() + + workingGroup.enter() + authToken { (authTokenResult: InstallationsAuthTokenResult?, error: Error?) in + authTokenComplete = authTokenResult?.authToken ?? "" + workingGroup.leave() + } + + workingGroup.enter() installationID { (installationID: String?, error: Error?) in if let installationID = installationID { - completion(.success(installationID)) + intallationComplete = installationID } else if let error = error { + errorComplete = error + } + workingGroup.leave() + } + + // adding timeout for 10 seconds + let result = workingGroup + .wait(timeout: .now() + DispatchTimeInterval.seconds(installationsWaitTimeInSecond)) + + switch result { + case .timedOut: + completion(.failure(FirebaseSessionsError.SessionInstallationsTimeOutError)) + return + default: + if let installationID = intallationComplete { + completion(.success((installationID, authTokenComplete))) + } else if let error = errorComplete { completion(.failure(error)) } } } } + +extension Installations: InstallationsProtocol {} diff --git a/FirebaseSessions/Sources/SessionCoordinator.swift b/FirebaseSessions/Sources/SessionCoordinator.swift index 801eafb9e51..3d4cec1b072 100644 --- a/FirebaseSessions/Sources/SessionCoordinator.swift +++ b/FirebaseSessions/Sources/SessionCoordinator.swift @@ -67,11 +67,13 @@ class SessionCoordinator: SessionCoordinatorProtocol { -> Void) { installations.installationID { result in switch result { - case let .success(fiid): - event.setInstallationID(installationId: fiid) + case let .success(installationsInfo): + event.setInstallationID(installationId: installationsInfo.0) + event.setAuthenticationToken(authenticationToken: installationsInfo.1) callback(.success(())) case let .failure(error): event.setInstallationID(installationId: "") + event.setAuthenticationToken(authenticationToken: "") callback(.failure(FirebaseSessionsError.SessionInstallationsError(error))) } } diff --git a/FirebaseSessions/Sources/SessionStartEvent.swift b/FirebaseSessions/Sources/SessionStartEvent.swift index 5a8d0d0148d..eb1560a86f5 100644 --- a/FirebaseSessions/Sources/SessionStartEvent.swift +++ b/FirebaseSessions/Sources/SessionStartEvent.swift @@ -91,6 +91,7 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject { proto.application_info.session_sdk_version, proto.session_data.session_id, proto.session_data.firebase_installation_id, + proto.session_data.firebase_authentication_token, proto.session_data.first_session_id, ] for pointer in garbage { @@ -104,6 +105,12 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject { nanopb_free(oldID) } + func setAuthenticationToken(authenticationToken: String) { + let oldToken = proto.session_data.firebase_authentication_token + proto.session_data.firebase_authentication_token = makeProtoString(authenticationToken) + nanopb_free(oldToken) + } + func setSamplingRate(samplingRate: Double) { proto.session_data.data_collection_status.session_sampling_rate = samplingRate } diff --git a/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift b/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift index c944291857a..aaea90941dc 100644 --- a/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift +++ b/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift @@ -53,8 +53,8 @@ class SettingsDownloader: SettingsDownloadClient { installations.installationID { result in switch result { - case let .success(fiid): - let request = self.buildRequest(url: validURL, fiid: fiid) + case let .success(installationsInfo): + let request = self.buildRequest(url: validURL, fiid: installationsInfo.0) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { diff --git a/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.c b/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.c index 3673f9d6ccb..f89596ab1df 100644 --- a/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.c +++ b/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.c @@ -39,13 +39,14 @@ const pb_field_t firebase_appquality_sessions_NetworkConnectionInfo_fields[3] = PB_LAST_FIELD }; -const pb_field_t firebase_appquality_sessions_SessionInfo_fields[7] = { +const pb_field_t firebase_appquality_sessions_SessionInfo_fields[8] = { PB_FIELD( 1, BYTES , SINGULAR, POINTER , FIRST, firebase_appquality_sessions_SessionInfo, session_id, session_id, 0), PB_FIELD( 3, BYTES , SINGULAR, POINTER , OTHER, firebase_appquality_sessions_SessionInfo, firebase_installation_id, session_id, 0), PB_FIELD( 4, INT64 , SINGULAR, STATIC , OTHER, firebase_appquality_sessions_SessionInfo, event_timestamp_us, firebase_installation_id, 0), PB_FIELD( 6, MESSAGE , SINGULAR, STATIC , OTHER, firebase_appquality_sessions_SessionInfo, data_collection_status, event_timestamp_us, &firebase_appquality_sessions_DataCollectionStatus_fields), PB_FIELD( 7, BYTES , SINGULAR, POINTER , OTHER, firebase_appquality_sessions_SessionInfo, first_session_id, data_collection_status, 0), PB_FIELD( 8, INT32 , SINGULAR, STATIC , OTHER, firebase_appquality_sessions_SessionInfo, session_index, first_session_id, 0), + PB_FIELD( 9, BYTES , SINGULAR, POINTER , OTHER, firebase_appquality_sessions_SessionInfo, firebase_authentication_token, session_index, 0), PB_LAST_FIELD }; diff --git a/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.h b/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.h index c8cfeb8edb3..5b604e93870 100644 --- a/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.h +++ b/FirebaseSessions/SourcesObjC/Protogen/nanopb/sessions.nanopb.h @@ -161,6 +161,7 @@ typedef struct _firebase_appquality_sessions_SessionInfo { firebase_appquality_sessions_DataCollectionStatus data_collection_status; pb_bytes_array_t *first_session_id; int32_t session_index; + pb_bytes_array_t *firebase_authentication_token; /* @@protoc_insertion_point(struct:firebase_appquality_sessions_SessionInfo) */ } firebase_appquality_sessions_SessionInfo; @@ -224,6 +225,7 @@ typedef struct _firebase_appquality_sessions_SessionEvent { #define firebase_appquality_sessions_SessionInfo_firebase_installation_id_tag 3 #define firebase_appquality_sessions_SessionInfo_event_timestamp_us_tag 4 #define firebase_appquality_sessions_SessionInfo_data_collection_status_tag 6 +#define firebase_appquality_sessions_SessionInfo_firebase_authentication_token_tag 9 #define firebase_appquality_sessions_ApplicationInfo_android_app_info_tag 5 #define firebase_appquality_sessions_ApplicationInfo_apple_app_info_tag 6 #define firebase_appquality_sessions_ApplicationInfo_app_id_tag 1 @@ -240,7 +242,7 @@ typedef struct _firebase_appquality_sessions_SessionEvent { /* Struct field encoding specification for nanopb */ extern const pb_field_t firebase_appquality_sessions_SessionEvent_fields[4]; extern const pb_field_t firebase_appquality_sessions_NetworkConnectionInfo_fields[3]; -extern const pb_field_t firebase_appquality_sessions_SessionInfo_fields[7]; +extern const pb_field_t firebase_appquality_sessions_SessionInfo_fields[8]; extern const pb_field_t firebase_appquality_sessions_DataCollectionStatus_fields[4]; extern const pb_field_t firebase_appquality_sessions_ApplicationInfo_fields[10]; extern const pb_field_t firebase_appquality_sessions_AndroidApplicationInfo_fields[3]; diff --git a/FirebaseSessions/Tests/Unit/Mocks/MockInstallationsProtocol.swift b/FirebaseSessions/Tests/Unit/Mocks/MockInstallationsProtocol.swift index 0abfdaf2446..10f9392776a 100644 --- a/FirebaseSessions/Tests/Unit/Mocks/MockInstallationsProtocol.swift +++ b/FirebaseSessions/Tests/Unit/Mocks/MockInstallationsProtocol.swift @@ -19,9 +19,24 @@ class MockInstallationsProtocol: InstallationsProtocol { static let testInstallationId = "testInstallationId" - var result: Result = .success(testInstallationId) + static let testAuthToken = "testAuthToken" + var result: Result<(String, String), Error> = .success((testInstallationId, testAuthToken)) + var installationIdFinished = false + var authTokenFinished = false - func installationID(completion: @escaping (Result) -> Void) { - completion(result) + func installationID(completion: @escaping (String?, Error?) -> Void) { + installationIdFinished = true + switch result { + case let .success(success): + completion(success.0, nil) + case let .failure(failure): + completion(nil, failure) + } + } + + func authToken(completion: @escaping (InstallationsAuthTokenResult?, Error?) -> Void) { + Thread.sleep(forTimeInterval: 0.1) + authTokenFinished = true + completion(nil, nil) } } diff --git a/FirebaseSessions/Tests/Unit/SessionCoordinatorTests.swift b/FirebaseSessions/Tests/Unit/SessionCoordinatorTests.swift index 7086a9bc135..ba9b10186b6 100644 --- a/FirebaseSessions/Tests/Unit/SessionCoordinatorTests.swift +++ b/FirebaseSessions/Tests/Unit/SessionCoordinatorTests.swift @@ -34,6 +34,11 @@ class SessionCoordinatorTests: XCTestCase { ) } + override func tearDown() { + installations.authTokenFinished = false + installations.installationIdFinished = false + } + var defaultSessionInfo: SessionInfo { return SessionInfo( sessionId: "test_session_id", @@ -61,6 +66,9 @@ class SessionCoordinatorTests: XCTestCase { fieldName: "installation_id" ) + XCTAssertTrue(installations.authTokenFinished) + XCTAssertTrue(installations.installationIdFinished) + // We should have logged successfully XCTAssertEqual(fireLogger.loggedEvent, event) XCTAssert(resultSuccess) @@ -82,6 +90,9 @@ class SessionCoordinatorTests: XCTestCase { } } + XCTAssertTrue(installations.authTokenFinished) + XCTAssertTrue(installations.installationIdFinished) + // Make sure we've set the Installation ID assertEqualProtoString( event.proto.session_data.firebase_installation_id, @@ -110,6 +121,8 @@ class SessionCoordinatorTests: XCTestCase { } } + XCTAssertTrue(installations.authTokenFinished) + XCTAssertTrue(installations.installationIdFinished) // We should have logged the event, but with a failed result XCTAssertNotNil(fireLogger.loggedEvent) XCTAssertFalse(resultSuccess) @@ -138,6 +151,9 @@ class SessionCoordinatorTests: XCTestCase { } } + XCTAssertTrue(installations.authTokenFinished) + XCTAssertTrue(installations.installationIdFinished) + // Make sure we've set the Installation ID to empty because the FIID // fetch failed assertEqualProtoString( From cc45d17b6ae404e54f6ef270ba6aa3e941e60641 Mon Sep 17 00:00:00 2001 From: themiswang Date: Mon, 26 Feb 2024 22:36:17 -0500 Subject: [PATCH 051/104] Fix release notes (#12431) --- Crashlytics/CHANGELOG.md | 5 +---- .../Sources/Development/DevEventConsoleLogger.swift | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 6f16b3d7425..4c449c579b5 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,8 +1,5 @@ -# Unreleased - -- [fixed] Force validation or rotation of FIDs for FirebaseSessions. - # 10.22.0 +- [fixed] Force validation or rotation of FIDs for FirebaseSessions. - [changed] Removed calls to statfs in the Crashlytics SDK to comply with Apple Privacy Manifests. This change removes support for collecting Disk Space Free in Crashlytics reports. - [fixed] Fixed FirebaseSessions crash on startup that occurs in release mode in Xcode 15.3 and other build configurations. (#11403) diff --git a/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift b/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift index 7d32a7b63db..488fca7efc6 100644 --- a/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift +++ b/FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift @@ -42,7 +42,7 @@ class DevEventConsoleLogger: EventGDTLoggerProtocol { session_index: \(proto.session_data.session_index) event_timestamp_us: \(proto.session_data.event_timestamp_us) firebase_installation_id: \(proto.session_data.firebase_installation_id.description) - firebase_autheticationtion_token: + firebase_authentication_token: \(proto.session_data.firebase_authentication_token.description) data_collection_status crashlytics: \(proto.session_data.data_collection_status.crashlytics) From 432f720414084594b5c3d684614122078c6c70e9 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 27 Feb 2024 07:51:27 -0800 Subject: [PATCH 052/104] Remove temporary GHA trigger (#12434) --- .github/workflows/zip.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 57c0dafc369..45578d33761 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -7,8 +7,6 @@ on: - '.github/workflows/zip.yml' - 'scripts/build_non_firebase_sdks.sh' - 'Gemfile*' - # DELETE BEFORE pushing - - 'FirebaseCore.podspec' # Don't run based on any markdown only changes. - '!ReleaseTooling/*.md' schedule: From a0a60e0d35e4557d33af53891e48bd03933cc1f9 Mon Sep 17 00:00:00 2001 From: tsunghung <78230356+tsunghung@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:50:47 -0800 Subject: [PATCH 053/104] Analytics 10.22.0 (#12436) --- GoogleAppMeasurement.podspec | 2 +- GoogleAppMeasurementOnDeviceConversion.podspec | 2 +- Package.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index 88bd7a533ad..0e8a6ba88ed 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/a95ed962e7ccb437/GoogleAppMeasurement-10.21.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/07d18a8bd138c027/GoogleAppMeasurement-10.22.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index 7fa66feaf3f..a9570b7335c 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/4ab453c686c6aac4/GoogleAppMeasurementOnDeviceConversion-10.20.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/4d6277bc4a9e003b/GoogleAppMeasurementOnDeviceConversion-10.22.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/Package.swift b/Package.swift index 9670eff963b..1c183156c6b 100644 --- a/Package.swift +++ b/Package.swift @@ -314,8 +314,8 @@ let package = Package( ), .binaryTarget( name: "FirebaseAnalytics", - url: "https://dl.google.com/firebase/ios/swiftpm/10.21.0/FirebaseAnalytics.zip", - checksum: "4d2c6daf6fd6f4d9e3071ed0051d4651648f44a01712a5949a36f169b1a2bd61" + url: "https://dl.google.com/firebase/ios/swiftpm/10.22.0/FirebaseAnalytics.zip", + checksum: "685f19cc58ab447b290c3c8aedf731fa44b3cbbbee8ebb98dcc96a316efc24f0" ), .target( name: "FirebaseAnalyticsSwiftTarget", @@ -1324,7 +1324,7 @@ func googleAppMeasurementDependency() -> Package.Dependency { return .package(url: appMeasurementURL, branch: "main") } - return .package(url: appMeasurementURL, exact: "10.21.0") + return .package(url: appMeasurementURL, exact: "10.22.0") } func abseilDependency() -> Package.Dependency { From 1e592921b205aa073d0fde00375eddcca119335e Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 27 Feb 2024 20:05:45 -0500 Subject: [PATCH 054/104] [Release Tooling] Rename gRPC-C++ framework to grpcpp (#12437) --- ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift | 2 ++ ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index cc09b1d1f41..2fc5c337b2e 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -283,6 +283,8 @@ struct FrameworkBuilder { /// - Returns: The corresponding framework/module name. private static func frameworkBuildName(_ framework: String) -> String { switch framework { + case "gRPC-C++": + return "grpcpp" case "PromisesObjC": return "FBLPromises" case "Protobuf": diff --git a/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift index 5dbdc636546..059ba8459b4 100755 --- a/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift @@ -51,7 +51,7 @@ struct ModuleMapBuilder { content += """ link framework "BoringSSL-GRPC" link framework "gRPC-Core" - link framework "gRPC-C++" + link framework "grpcpp" """ } From 1f699458b8a97b70d2bd75288b8efda9a099ee8a Mon Sep 17 00:00:00 2001 From: tsunghung <78230356+tsunghung@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:38:26 -0800 Subject: [PATCH 055/104] Analytics 10.22.0 (#12438) --- FirebaseAnalytics.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index 83363a2c0f3..6af10397ec1 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/573d1b06cde0fa35/FirebaseAnalytics-10.21.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/d50db472e27feddf/FirebaseAnalytics-10.22.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' From 4065853eef0c5c5c09f59107aec14e0affeb8659 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 28 Feb 2024 10:17:03 -0500 Subject: [PATCH 056/104] [Release Tooling] Validation workaround for Xcode 15.3b3 (#12439) --- .../Sources/ZipBuilder/FrameworkBuilder.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index 2fc5c337b2e..b6704a28fc6 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -673,7 +673,20 @@ struct FrameworkBuilder { fatalError("Could not copy fat binary to framework directory for \(framework): \(error)") } do { - try fileManager.copyItem(at: infoPlist, to: infoPlistDestination) + // The minimum OS version is set to 100.0 to work around b/327020913. + // TODO(ncooke3): Revert this logic once b/327020913 is fixed. + var plistDictionary = try PropertyListSerialization.propertyList( + from: Data(contentsOf: infoPlist), format: nil + ) as! [AnyHashable: Any] + plistDictionary["MinimumOSVersion"] = "100.0" + + let updatedPlistData = try PropertyListSerialization.data( + fromPropertyList: plistDictionary, + format: .binary, + options: 0 + ) + + try updatedPlistData.write(to: infoPlistDestination) } catch { // The Catalyst and macos Info.plist's are in another location. Ignore failure. } From 1f7d83acbcd417f45c69a6641e23ccd6051e62bb Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 28 Feb 2024 11:17:54 -0500 Subject: [PATCH 057/104] Add CHANGELOG entry for `gRPC-C++` framework rename (#12442) --- Firestore/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index afa0f58da9a..ea60df47a1f 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased - [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) +- [Zip Distribution] Renamed `gRPC-C++.xcframework` to `grpcc.xcframework`, + matching the module name, to work around an issue introduced in Xcode 15.3 + with `+` characters in framework names. (#12437) + - Please ensure that `gRPC-C++.xcframework` is removed when upgrading. # 10.21.0 - Add an error when trying to build Firestore's binary SPM distribution for From 1aa946639a6c9721974242f2f01b729f190ae561 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 28 Feb 2024 11:21:27 -0500 Subject: [PATCH 058/104] Update CHANGELOG entries to 10.22.0 (#12443) --- FirebaseCore/CHANGELOG.md | 2 +- Firestore/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index 2b9caf3f31f..d861b70ad7b 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# Firebase 10.22.0 - [Swift Package Manager] Firebase now enforces a Swift 5.7.1 minimum version, which is aligned with the Xcode 14.1 minimum. (#12350) - Revert Firebase 10.20.0 change that removed `Info.plist` files from diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index ea60df47a1f..03e4949a61f 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# 10.22.0 - [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) - [Zip Distribution] Renamed `gRPC-C++.xcframework` to `grpcc.xcframework`, matching the module name, to work around an issue introduced in Xcode 15.3 From 0047cef1b07924be2ec238d224d0f6a37614d2af Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:23:38 -0500 Subject: [PATCH 059/104] [Release] Additional changelog updates for 10.22.0 (#12444) --- FirebaseCore/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index d861b70ad7b..66acb8b1bf2 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -3,6 +3,14 @@ which is aligned with the Xcode 14.1 minimum. (#12350) - Revert Firebase 10.20.0 change that removed `Info.plist` files from static xcframeworks (#12390). +- Added privacy manifests for Firebase SDKs named in + https://developer.apple.com/support/third-party-SDK-requirements/. Please + review https://firebase.google.com/docs/ios/app-store-data-collection for + updated guidance on interpreting Firebase's privacy manifests and completing + app Privacy Nutrition Labels. (#11490) +- Fixed validation issues in Xcode 15.3 that affected binary distributions + including Analytics, Firestore (SwiftPM binary distribution), and the + Firebase zip distribution. (#12441) # Firebase 10.21.0 - Firebase now requires at least CocoaPods version 1.12.0 to enable privacy From 8fdad7fde25d2f9b2b2c4f873bf1615601a432ed Mon Sep 17 00:00:00 2001 From: tsunghung <78230356+tsunghung@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:49:00 -0800 Subject: [PATCH 060/104] Analytics 10.22.0 (#12446) --- FirebaseAnalytics.podspec | 2 +- GoogleAppMeasurement.podspec | 2 +- GoogleAppMeasurementOnDeviceConversion.podspec | 2 +- Package.swift | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index 6af10397ec1..6b4b7412480 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/d50db472e27feddf/FirebaseAnalytics-10.22.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/d9e6824c98c32455/FirebaseAnalytics-10.22.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index 0e8a6ba88ed..df2097af8c9 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/07d18a8bd138c027/GoogleAppMeasurement-10.22.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/a92666f1e01923b8/GoogleAppMeasurement-10.22.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index a9570b7335c..bd77bd0edde 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/4d6277bc4a9e003b/GoogleAppMeasurementOnDeviceConversion-10.22.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/a0551aa6065a8330/GoogleAppMeasurementOnDeviceConversion-10.22.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/Package.swift b/Package.swift index 1c183156c6b..6eb274b6950 100644 --- a/Package.swift +++ b/Package.swift @@ -314,8 +314,8 @@ let package = Package( ), .binaryTarget( name: "FirebaseAnalytics", - url: "https://dl.google.com/firebase/ios/swiftpm/10.22.0/FirebaseAnalytics.zip", - checksum: "685f19cc58ab447b290c3c8aedf731fa44b3cbbbee8ebb98dcc96a316efc24f0" + url: "https://dl.google.com/firebase/ios/swiftpm/10.22.0/FirebaseAnalytics-rc1.zip", + checksum: "78ce96a59962a8946f5df8dd57c7d0d83fd3cf0e14f3efab65c8c65fbdf03a8a" ), .target( name: "FirebaseAnalyticsSwiftTarget", From 0bc3adda16a022f237a1ee2f0bac5552dfce5359 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:08:44 -0500 Subject: [PATCH 061/104] [Release Tooling] Update zip integration instructions for privacy manifest support (#12449) --- FirebaseCore/CHANGELOG.md | 5 +++++ ReleaseTooling/Template/README.md | 21 +++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index 66acb8b1bf2..e9c1443cedb 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -11,6 +11,11 @@ - Fixed validation issues in Xcode 15.3 that affected binary distributions including Analytics, Firestore (SwiftPM binary distribution), and the Firebase zip distribution. (#12441) +- [Zip Distribution] The manual integration instructions found in the + `Firebase.zip` have been updated for Xcode 15 users. The updated instructions + call for embedding SDKs dragged in from the `Firebase.zip`. This will enable + Xcode's tooling to detect privacy manifests bundled within the xcframework. + # Firebase 10.21.0 - Firebase now requires at least CocoaPods version 1.12.0 to enable privacy diff --git a/ReleaseTooling/Template/README.md b/ReleaseTooling/Template/README.md index 6b5f261311d..e6943b7b4f7 100644 --- a/ReleaseTooling/Template/README.md +++ b/ReleaseTooling/Template/README.md @@ -34,17 +34,18 @@ To integrate a Firebase SDK with your app: box that appears, make sure the target you want this framework to be added to has a checkmark next to it, and that you've selected "Copy items if needed." - > ⚠ Do not add the Firebase frameworks to the **Embed Frameworks** Xcode build - > phase. The Firebase frameworks are not embedded dynamic frameworks, but are - > [static frameworks](https://www.raywenderlich.com/65964/create-a-framework-for-ios) - > which cannot be embedded into your application's bundle. +7. If using Xcode 15, embed each framework that was dragged in. Navigate to the + target's _General_ settings and find _Frameworks, Libraries, & Embedded + Content_. For each framework dragged in from the `Firebase.zip`, select + **Embed & Sign**. This step will enable privacy manifests to be picked up by + Xcode's tooling. -7. If the SDK has resources, go into the Resources folders, which will be in +8. If the SDK has resources, go into the Resources folders, which will be in the SDK folder. Drag all of those resources into the Project Navigator, just like the frameworks, again making sure that the target you want to add these resources to has a checkmark next to it, and that you've selected "Copy items if needed". -8. Add the `-ObjC` flag to **Other Linker Settings**: +9. Add the `-ObjC` flag to **Other Linker Settings**: a. In your project settings, open the **Settings** panel for your target. @@ -53,13 +54,13 @@ To integrate a Firebase SDK with your app: c. Double-click the setting, click the '+' button, and add `-ObjC` -9. Drag the `Firebase.h` header in this directory into your project. This will +10. Drag the `Firebase.h` header in this directory into your project. This will allow you to `#import "Firebase.h"` and start using any Firebase SDK that you have. -10. Drag `module.modulemap` into your project and update the +11. Drag `module.modulemap` into your project and update the "User Header Search Paths" in your project's Build Settings to include the directory that contains the added module map. -11. If your app does not include any Swift implementation, you may need to add +12. If your app does not include any Swift implementation, you may need to add a dummy Swift file to the app to prevent Swift system library missing symbol linker errors. See https://forums.swift.org/t/using-binary-swift-sdks-from-non-swift-apps/55989. @@ -67,7 +68,7 @@ To integrate a Firebase SDK with your app: > ⚠ If prompted with the option to create a corresponding bridging header > for the new Swift file, select **Don't create**. -12. You're done! Compile your target and start using Firebase. +13. You're done! Build your target and start using Firebase. If you want to add another SDK, repeat the steps above with the xcframeworks for the new SDK. You only need to add each framework once, so if you've already From d8e9044fbbb1795ef0bcc0f0e75c1657e86cea58 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:58:42 -0500 Subject: [PATCH 062/104] [Release Tooling] Recommend -lc++ linker flag for manual integration (#12451) --- ReleaseTooling/Template/README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/ReleaseTooling/Template/README.md b/ReleaseTooling/Template/README.md index e6943b7b4f7..162363e5be7 100644 --- a/ReleaseTooling/Template/README.md +++ b/ReleaseTooling/Template/README.md @@ -54,13 +54,22 @@ To integrate a Firebase SDK with your app: c. Double-click the setting, click the '+' button, and add `-ObjC` -10. Drag the `Firebase.h` header in this directory into your project. This will +10. Add the `-lc++` flag to **Other Linker Settings**: + + a. In your project settings, open the **Settings** panel for your target. + + b. Go to the Build Settings tab and find the **Other Linker Flags** setting + in the **Linking** section. + + c. Double-click the setting, click the '+' button, and add `-lc++` + +11. Drag the `Firebase.h` header in this directory into your project. This will allow you to `#import "Firebase.h"` and start using any Firebase SDK that you have. -11. Drag `module.modulemap` into your project and update the +12. Drag `module.modulemap` into your project and update the "User Header Search Paths" in your project's Build Settings to include the directory that contains the added module map. -12. If your app does not include any Swift implementation, you may need to add +13. If your app does not include any Swift implementation, you may need to add a dummy Swift file to the app to prevent Swift system library missing symbol linker errors. See https://forums.swift.org/t/using-binary-swift-sdks-from-non-swift-apps/55989. @@ -68,7 +77,7 @@ To integrate a Firebase SDK with your app: > ⚠ If prompted with the option to create a corresponding bridging header > for the new Swift file, select **Don't create**. -13. You're done! Build your target and start using Firebase. +14. You're done! Build your target and start using Firebase. If you want to add another SDK, repeat the steps above with the xcframeworks for the new SDK. You only need to add each framework once, so if you've already From bced7730d20c3537ae1417ee626e0b16a82e9295 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 28 Feb 2024 19:37:52 -0500 Subject: [PATCH 063/104] Add CHANGELOG entry for renamed frameworks (#12450) --- FirebaseCore/CHANGELOG.md | 10 +++++++++- Firestore/CHANGELOG.md | 4 ---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index e9c1443cedb..e383f958ccd 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -15,7 +15,15 @@ `Firebase.zip` have been updated for Xcode 15 users. The updated instructions call for embedding SDKs dragged in from the `Firebase.zip`. This will enable Xcode's tooling to detect privacy manifests bundled within the xcframework. - +- [Zip Distribution] Several xcframeworks have been renamed to resolve the above + Xcode 15.3 validation issues. Please ensure that the following renamed + xcframeworks are removed from your project when upgrading (#12437, #12447): + - `abseil.xcframework` to `absl.xcframework` + - `BoringSSL-gRPC.xcframework` to `openssl_grpc.xcframework` + - `gRPC-Core.xcframework` to `grpc.xcframework` + - `gRPC-C++.xcframework` to `grpcc.xcframework` + - `leveldb-library.xcframework` to `leveldb.xcframework` + - `PromisesSwift.xcframework` to `Promises.xcframework` # Firebase 10.21.0 - Firebase now requires at least CocoaPods version 1.12.0 to enable privacy diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 03e4949a61f..60d99030d03 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,9 +1,5 @@ # 10.22.0 - [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) -- [Zip Distribution] Renamed `gRPC-C++.xcframework` to `grpcc.xcframework`, - matching the module name, to work around an issue introduced in Xcode 15.3 - with `+` characters in framework names. (#12437) - - Please ensure that `gRPC-C++.xcframework` is removed when upgrading. # 10.21.0 - Add an error when trying to build Firestore's binary SPM distribution for From 2f3094367acfcb7afdcee8ad45cfc4c3b2158d16 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 28 Feb 2024 20:17:05 -0500 Subject: [PATCH 064/104] Fix framework names in changelog entry (#12452) --- FirebaseCore/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index e383f958ccd..10aeb1cc605 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -19,9 +19,9 @@ Xcode 15.3 validation issues. Please ensure that the following renamed xcframeworks are removed from your project when upgrading (#12437, #12447): - `abseil.xcframework` to `absl.xcframework` - - `BoringSSL-gRPC.xcframework` to `openssl_grpc.xcframework` + - `BoringSSL-GRPC.xcframework` to `openssl_grpc.xcframework` - `gRPC-Core.xcframework` to `grpc.xcframework` - - `gRPC-C++.xcframework` to `grpcc.xcframework` + - `gRPC-C++.xcframework` to `grpcpp.xcframework` - `leveldb-library.xcframework` to `leveldb.xcframework` - `PromisesSwift.xcframework` to `Promises.xcframework` From 41050f0cec0e1806258c141f0a1a8bae99a9b0ec Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 28 Feb 2024 22:59:37 -0500 Subject: [PATCH 065/104] [Release Tooling] Rename frameworks to match modules (#12447) Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> --- .../Sources/ZipBuilder/FrameworkBuilder.swift | 10 ++++++++++ .../Sources/ZipBuilder/ModuleMapBuilder.swift | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index b6704a28fc6..c78385ec186 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -283,10 +283,20 @@ struct FrameworkBuilder { /// - Returns: The corresponding framework/module name. private static func frameworkBuildName(_ framework: String) -> String { switch framework { + case "abseil": + return "absl" + case "BoringSSL-GRPC": + return "openssl_grpc" + case "gRPC-Core": + return "grpc" case "gRPC-C++": return "grpcpp" + case "leveldb-library": + return "leveldb" case "PromisesObjC": return "FBLPromises" + case "PromisesSwift": + return "Promises" case "Protobuf": return "protobuf" default: diff --git a/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift index 059ba8459b4..dc781d6a0d4 100755 --- a/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift @@ -49,8 +49,8 @@ struct ModuleMapBuilder { if module == "FirebaseFirestoreInternal" { content += """ - link framework "BoringSSL-GRPC" - link framework "gRPC-Core" + link framework "openssl_grpc" + link framework "grpc" link framework "grpcpp" """ } From 12345ff2f150cbd976187db4a7a7d63d702305a3 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:07:46 -0500 Subject: [PATCH 066/104] [Release Tooling] Prefer xml Info.plist (#12448) Co-authored-by: Andrew Heard --- ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index c78385ec186..7c763834411 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -692,7 +692,7 @@ struct FrameworkBuilder { let updatedPlistData = try PropertyListSerialization.data( fromPropertyList: plistDictionary, - format: .binary, + format: .xml, options: 0 ) From fe09d61a539e11fdbe24f269bba10144b6145fe2 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 29 Feb 2024 16:08:41 +0000 Subject: [PATCH 067/104] [Release] Add Firestore 10.22.0 binary (#12456) --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 6eb274b6950..05ca701bd7f 100644 --- a/Package.swift +++ b/Package.swift @@ -1494,8 +1494,8 @@ func firestoreTargets() -> [Target] { } else { return .binaryTarget( name: "FirebaseFirestoreInternal", - url: "https://dl.google.com/firebase/ios/bin/firestore/10.21.0/FirebaseFirestoreInternal.zip", - checksum: "50d864ef4e7e090ea1388926674d7095ae5a83ac429f788c3d6e3497e7a5b175" + url: "https://dl.google.com/firebase/ios/bin/firestore/10.22.0/FirebaseFirestoreInternal.zip", + checksum: "35c02539c6bbd43ec1c3b58f894984e48ff8c560db0c799fb8f6377b59c96099" ) } }() From 86ae5deb1229c66e0c7d94571fa0685cdcd7b5cc Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 4 Mar 2024 20:32:21 +0000 Subject: [PATCH 068/104] [Release] Carthage updates for 10.22.0 (#12465) --- ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json | 1 + .../CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json | 1 + 19 files changed, 19 insertions(+) diff --git a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json index eff8f0425c6..606d9c06920 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseABTesting-3c0e5c9ddc52bccd.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseABTesting-8dad3d6af34cb26c.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseABTesting-0ad6c6c2f729706c.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseABTesting-2823ac22562f1fbe.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseABTesting-e87c686cee02758a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseABTesting-6a65ab8b888172af.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseABTesting-197f0cb4125363b6.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json index 20fadca4a21..8d3f22bb190 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/Google-Mobile-Ads-SDK-c8bc252ed3323212.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/Google-Mobile-Ads-SDK-5f8bb98bb2467b85.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/Google-Mobile-Ads-SDK-23be5a73a2ce3dcc.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/Google-Mobile-Ads-SDK-bf8077d30296e04a.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/Google-Mobile-Ads-SDK-8b0d1ce3d1162b67.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/Google-Mobile-Ads-SDK-046511c3fd0189eb.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/Google-Mobile-Ads-SDK-50008c143ad8f268.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json index 67115c7bd7f..b28bbd0ea7f 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAnalytics-6f8b70c8ee2efc85.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAnalytics-4d7ca295e8b44c0c.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAnalytics-620570dc24ce7d7b.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAnalytics-a121058bc5824bfa.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAnalytics-95669fcf109f74a2.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAnalytics-c0db6cb0e858e397.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAnalytics-e8ebe991b5743f71.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json index 33081e98162..e4dbe4f3dfc 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAnalyticsOnDeviceConversion-37cf6277991d7d75.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAnalyticsOnDeviceConversion-d3913995b7344202.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAnalyticsOnDeviceConversion-202ed30074984af7.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAnalyticsOnDeviceConversion-4b5874979659af63.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAnalyticsOnDeviceConversion-091f5252d693a9f9.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAnalyticsOnDeviceConversion-7bbb73d46383a042.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAnalyticsOnDeviceConversion-eca2f83d40e0278d.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json index 08e59ee59b7..a1e400157fd 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAppCheck-b0ead84a126d24d4.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAppCheck-8f7dfe411eeaccdf.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAppCheck-a458ebf606a7b451.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAppCheck-2b52807979acf863.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAppCheck-d19e46a728b1ac4f.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAppCheck-8339fde989fe8f24.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAppCheck-3ce0f074bfcd2596.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json index 05008847a77..d96e5583946 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAppDistribution-45b5c85bba08a85b.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAppDistribution-264a5e036b72a526.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAppDistribution-e08ef26e391c7b0b.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAppDistribution-139211bb5dd3dbc3.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAppDistribution-cefc3327ddfceda6.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAppDistribution-7931e42d39575534.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAppDistribution-79dc2b1348d9aee9.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json index b8540b8da37..aef6428aef4 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseAuth-2165e27f89d4959e.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseAuth-222a2417c3c21b41.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseAuth-da6796caf834f09f.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAuth-529e82147fbbd402.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAuth-e43e66353617f093.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAuth-8a9591e6daa7e207.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAuth-7e18a510d0a5b02e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json index 750c9ad6d44..f321ad7f10a 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseCrashlytics-054718c61ef054f9.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseCrashlytics-029c76d79754388c.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseCrashlytics-0f5ccfdbf0de85f7.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseCrashlytics-47c05619edb8ae9b.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseCrashlytics-d29d3285a7d9fa1d.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseCrashlytics-165beb64483b4278.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseCrashlytics-53604573442e756b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json index 71042ee6c80..c1a787120f4 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseDatabase-8b7048f7890bb665.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseDatabase-a7f5c6d032473b01.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseDatabase-a05cb524bec955b2.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseDatabase-f5156c8169b6358f.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseDatabase-5b22f689cb66d83a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseDatabase-e1a9d1f0c4222cf7.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseDatabase-aea9249d81841ee1.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json index 4cac083f472..b099f6ead0d 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseDynamicLinks-bfdce6ac5d591ab3.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseDynamicLinks-693c6213bc87f8c0.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseDynamicLinks-ad0ac7b8fdf4c1b5.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseDynamicLinks-c17c59949b7cc573.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseDynamicLinks-7cf4ae5e96882ca8.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseDynamicLinks-c3bdeb37651a5d5d.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseDynamicLinks-bcb5df6ec32f6684.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json index 5ed8fc07b0d..b853a2b2d5e 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseFirestore-4c3d1568e379a98c.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseFirestore-88b0aaac6fe277fe.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseFirestore-dcf15ce0975bfa3c.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseFirestore-e4570e4863fe2044.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseFirestore-73ba0700b1aa6d6a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseFirestore-02eb8da05f81fca5.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseFirestore-46fa68ddf287f76e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json index fa18832e784..448dfb89814 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseFunctions-b949cfeca4e7a80f.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseFunctions-23d6ba97d95db62c.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseFunctions-b77aca8c98dba58d.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseFunctions-d98d21836c2f2130.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseFunctions-47189f2c99cdf806.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseFunctions-17c4b760141e38ad.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseFunctions-688a38b567392fcf.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json index d605a90f573..4125b2318ca 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/GoogleSignIn-c887dbc6bd07c787.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/GoogleSignIn-e55954e1a3ca9ee8.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/GoogleSignIn-82fc8f5e20a9345b.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/GoogleSignIn-a16b78c06ef8f77c.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/GoogleSignIn-a5b49807be66100b.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/GoogleSignIn-0d2e746eb3ff9f92.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/GoogleSignIn-5cb2a2f1f74efd5e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json index bedb6a1e462..7de061a8fd2 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseInAppMessaging-f29d7b7839cda915.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseInAppMessaging-c6a82f2dccc9a092.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseInAppMessaging-940786963f9ac384.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseInAppMessaging-fbb53083384bea1e.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseInAppMessaging-91e5426eade46bca.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseInAppMessaging-10801bd111df59de.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseInAppMessaging-91d4dd9878a06b7e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json index e9f09a5570e..d81b3861de8 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseMLModelDownloader-ee2af587027e74d3.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseMLModelDownloader-e45969e88bf879cd.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseMLModelDownloader-d779b84cfdf214f3.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseMLModelDownloader-b3bffe302a074d0e.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseMLModelDownloader-559cb113c0cfd8f2.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseMLModelDownloader-9c909894999c92e4.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseMLModelDownloader-9abf9b0e24bfb921.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json index b8ce415798e..dcd884a8317 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseMessaging-289a04c85f7e771d.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseMessaging-236bb6f578c05ed1.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseMessaging-4a481ad8d3446844.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseMessaging-812bc4f1c2d27e93.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseMessaging-59ef1cc63c660712.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseMessaging-76c02a69e3fe1008.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseMessaging-439a17dcc8b8172b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json index 42bc4ea2a3e..4ab36e1a255 100644 --- a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebasePerformance-7a7398acc615dbb6.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebasePerformance-6494eb8091be4e03.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebasePerformance-4b6c574e0645b449.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebasePerformance-2a39f03d02fcbc5f.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebasePerformance-36ac6dfb99caa11b.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebasePerformance-f9f5be8ffad5cbb0.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebasePerformance-0ffe559f7554d8a5.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json index 6e28e9f7a9b..d9bdcfcdc9a 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseRemoteConfig-45a7f541a654884c.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseRemoteConfig-44d640335ebdfea7.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseRemoteConfig-933eae5291c343cc.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseRemoteConfig-be4764f1b3e07c4f.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseRemoteConfig-edd1b427b8bbe782.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseRemoteConfig-10b62ee5663aaab3.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseRemoteConfig-2237eb5fcd4a4525.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json index 56dffe7b95e..857c97f7e05 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json @@ -14,6 +14,7 @@ "10.2.0": "https://dl.google.com/dl/firebase/ios/carthage/10.2.0/FirebaseStorage-347e6a3402706596.zip", "10.20.0": "https://dl.google.com/dl/firebase/ios/carthage/10.20.0/FirebaseStorage-fac47c17aae220e0.zip", "10.21.0": "https://dl.google.com/dl/firebase/ios/carthage/10.21.0/FirebaseStorage-6f3adc4f2b871f04.zip", + "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseStorage-e3b2849afc9f0f95.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseStorage-ac463d14593d10a8.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseStorage-fdf8479115660ce6.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseStorage-04f255ea8c3a7420.zip", From 2e7ff6a892f868c46665febb8708e5b4708c347b Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 5 Mar 2024 02:32:26 +0000 Subject: [PATCH 069/104] [Release] Update versions for Release 10.23.0 (#12467) --- Firebase.podspec | 48 +++++++++---------- FirebaseABTesting.podspec | 2 +- FirebaseAnalytics.podspec | 6 +-- FirebaseAnalyticsOnDeviceConversion.podspec | 4 +- FirebaseAppCheck.podspec | 2 +- FirebaseAppCheckInterop.podspec | 2 +- FirebaseAppDistribution.podspec | 2 +- FirebaseAuth.podspec | 2 +- FirebaseAuthInterop.podspec | 2 +- FirebaseCore.podspec | 2 +- FirebaseCoreExtension.podspec | 2 +- FirebaseCoreInternal.podspec | 2 +- FirebaseCrashlytics.podspec | 2 +- FirebaseDatabase.podspec | 2 +- FirebaseDynamicLinks.podspec | 2 +- FirebaseFirestore.podspec | 2 +- FirebaseFirestoreInternal.podspec | 2 +- FirebaseFunctions.podspec | 2 +- FirebaseInAppMessaging.podspec | 2 +- FirebaseInstallations.podspec | 2 +- FirebaseMLModelDownloader.podspec | 2 +- FirebaseMessaging.podspec | 2 +- FirebaseMessagingInterop.podspec | 2 +- FirebasePerformance.podspec | 2 +- FirebaseRemoteConfig.podspec | 2 +- FirebaseSessions.podspec | 2 +- FirebaseSharedSwift.podspec | 2 +- FirebaseStorage.podspec | 2 +- GoogleAppMeasurement.podspec | 4 +- ...leAppMeasurementOnDeviceConversion.podspec | 2 +- Package.swift | 2 +- .../FirebaseManifest/FirebaseManifest.swift | 2 +- 32 files changed, 59 insertions(+), 59 deletions(-) diff --git a/Firebase.podspec b/Firebase.podspec index c7e3709a691..a56eeced656 100644 --- a/Firebase.podspec +++ b/Firebase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Firebase' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase' s.description = <<-DESC @@ -36,14 +36,14 @@ Simplify your app development, grow your user base, and monetize more effectivel ss.ios.deployment_target = '10.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' - ss.ios.dependency 'FirebaseAnalytics', '~> 10.22.0' - ss.osx.dependency 'FirebaseAnalytics', '~> 10.22.0' - ss.tvos.dependency 'FirebaseAnalytics', '~> 10.22.0' + ss.ios.dependency 'FirebaseAnalytics', '~> 10.23.0' + ss.osx.dependency 'FirebaseAnalytics', '~> 10.23.0' + ss.tvos.dependency 'FirebaseAnalytics', '~> 10.23.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'CoreOnly' do |ss| - ss.dependency 'FirebaseCore', '10.22.0' + ss.dependency 'FirebaseCore', '10.23.0' ss.source_files = 'CoreOnly/Sources/Firebase.h' ss.preserve_paths = 'CoreOnly/Sources/module.modulemap' if ENV['FIREBASE_POD_REPO_FOR_DEV_POD'] then @@ -79,13 +79,13 @@ Simplify your app development, grow your user base, and monetize more effectivel ss.ios.deployment_target = '10.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' - ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 10.22.0' + ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 10.23.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'ABTesting' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseABTesting', '~> 10.22.0' + ss.dependency 'FirebaseABTesting', '~> 10.23.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -95,13 +95,13 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'AppDistribution' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseAppDistribution', '~> 10.22.0-beta' + ss.ios.dependency 'FirebaseAppDistribution', '~> 10.23.0-beta' ss.ios.deployment_target = '11.0' end s.subspec 'AppCheck' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAppCheck', '~> 10.22.0' + ss.dependency 'FirebaseAppCheck', '~> 10.23.0' ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' @@ -110,7 +110,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Auth' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAuth', '~> 10.22.0' + ss.dependency 'FirebaseAuth', '~> 10.23.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -120,7 +120,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Crashlytics' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseCrashlytics', '~> 10.22.0' + ss.dependency 'FirebaseCrashlytics', '~> 10.23.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -130,7 +130,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Database' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseDatabase', '~> 10.22.0' + ss.dependency 'FirebaseDatabase', '~> 10.23.0' # Standard platforms PLUS watchOS 7. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -140,13 +140,13 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'DynamicLinks' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseDynamicLinks', '~> 10.22.0' + ss.ios.dependency 'FirebaseDynamicLinks', '~> 10.23.0' ss.ios.deployment_target = '11.0' end s.subspec 'Firestore' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFirestore', '~> 10.22.0' + ss.dependency 'FirebaseFirestore', '~> 10.23.0' ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' @@ -154,7 +154,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Functions' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFunctions', '~> 10.22.0' + ss.dependency 'FirebaseFunctions', '~> 10.23.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -164,20 +164,20 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'InAppMessaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseInAppMessaging', '~> 10.22.0-beta' - ss.tvos.dependency 'FirebaseInAppMessaging', '~> 10.22.0-beta' + ss.ios.dependency 'FirebaseInAppMessaging', '~> 10.23.0-beta' + ss.tvos.dependency 'FirebaseInAppMessaging', '~> 10.23.0-beta' ss.ios.deployment_target = '11.0' ss.tvos.deployment_target = '12.0' end s.subspec 'Installations' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseInstallations', '~> 10.22.0' + ss.dependency 'FirebaseInstallations', '~> 10.23.0' end s.subspec 'Messaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMessaging', '~> 10.22.0' + ss.dependency 'FirebaseMessaging', '~> 10.23.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -187,7 +187,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'MLModelDownloader' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMLModelDownloader', '~> 10.22.0-beta' + ss.dependency 'FirebaseMLModelDownloader', '~> 10.23.0-beta' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -197,15 +197,15 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Performance' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebasePerformance', '~> 10.22.0' - ss.tvos.dependency 'FirebasePerformance', '~> 10.22.0' + ss.ios.dependency 'FirebasePerformance', '~> 10.23.0' + ss.tvos.dependency 'FirebasePerformance', '~> 10.23.0' ss.ios.deployment_target = '11.0' ss.tvos.deployment_target = '12.0' end s.subspec 'RemoteConfig' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseRemoteConfig', '~> 10.22.0' + ss.dependency 'FirebaseRemoteConfig', '~> 10.23.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -215,7 +215,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Storage' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseStorage', '~> 10.22.0' + ss.dependency 'FirebaseStorage', '~> 10.23.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index 60e3584c1f0..dbd855d6486 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseABTesting' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase ABTesting' s.description = <<-DESC diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index 6b4b7412480..0259cc52c58 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalytics' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Analytics for iOS' s.description = <<-DESC @@ -37,12 +37,12 @@ Pod::Spec.new do |s| s.default_subspecs = 'AdIdSupport' s.subspec 'AdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement', '10.22.0' + ss.dependency 'GoogleAppMeasurement', '10.23.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end s.subspec 'WithoutAdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.22.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.23.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end diff --git a/FirebaseAnalyticsOnDeviceConversion.podspec b/FirebaseAnalyticsOnDeviceConversion.podspec index 3dddf28a777..e4ec770b8e5 100644 --- a/FirebaseAnalyticsOnDeviceConversion.podspec +++ b/FirebaseAnalyticsOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsOnDeviceConversion' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'On device conversion measurement plugin for FirebaseAnalytics. Not intended for direct use.' s.description = <<-DESC @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.cocoapods_version = '>= 1.12.0' - s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.22.0' + s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.23.0' s.static_framework = true diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 232d4b29318..da7607cd677 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheck' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase App Check SDK.' s.description = <<-DESC diff --git a/FirebaseAppCheckInterop.podspec b/FirebaseAppCheckInterop.podspec index f1a238848b6..bc5111f6ae8 100644 --- a/FirebaseAppCheckInterop.podspec +++ b/FirebaseAppCheckInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheckInterop' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Interfaces that allow other Firebase SDKs to use AppCheck functionality.' s.description = <<-DESC diff --git a/FirebaseAppDistribution.podspec b/FirebaseAppDistribution.podspec index ccf859c21bb..11bbbacd781 100644 --- a/FirebaseAppDistribution.podspec +++ b/FirebaseAppDistribution.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppDistribution' - s.version = '10.22.0-beta' + s.version = '10.23.0-beta' s.summary = 'App Distribution for Firebase iOS SDK.' s.description = <<-DESC diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index 5333ffc3b4b..73f6666d777 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuth' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Apple platform client for Firebase Authentication' s.description = <<-DESC diff --git a/FirebaseAuthInterop.podspec b/FirebaseAuthInterop.podspec index 65d84553e7b..87990ae36dc 100644 --- a/FirebaseAuthInterop.podspec +++ b/FirebaseAuthInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuthInterop' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Auth functionality.' s.description = <<-DESC diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index 5320d32e4ef..4957df4fa6e 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCore' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Core' s.description = <<-DESC diff --git a/FirebaseCoreExtension.podspec b/FirebaseCoreExtension.podspec index 9f8aff68ce6..5c2e7e8a4d0 100644 --- a/FirebaseCoreExtension.podspec +++ b/FirebaseCoreExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreExtension' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Extended FirebaseCore APIs for Firebase product SDKs' s.description = <<-DESC diff --git a/FirebaseCoreInternal.podspec b/FirebaseCoreInternal.podspec index 330587c824d..1990db42d28 100644 --- a/FirebaseCoreInternal.podspec +++ b/FirebaseCoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreInternal' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'APIs for internal FirebaseCore usage.' s.description = <<-DESC diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index e4b5cebea5c..108645728e4 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCrashlytics' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Best and lightest-weight crash reporting for mobile, desktop and tvOS.' s.description = 'Firebase Crashlytics helps you track, prioritize, and fix stability issues that erode app quality.' s.homepage = 'https://firebase.google.com/' diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index ab9551fac82..3aa94748bda 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDatabase' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Realtime Database' s.description = <<-DESC diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index d0c2cf282b8..aaa43aec721 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDynamicLinks' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Dynamic Links' s.description = <<-DESC diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 53d0f8a0877..b7ea45b827c 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestore' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC Google Cloud Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development. diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index 139ef417636..f6f3235d733 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestoreInternal' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index 75f42c569ff..9c74c320285 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFunctions' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Cloud Functions for Firebase' s.description = <<-DESC diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index 5f8e245690e..f0122b8ec7c 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessaging' - s.version = '10.22.0-beta' + s.version = '10.23.0-beta' s.summary = 'Firebase In-App Messaging for iOS' s.description = <<-DESC diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index 815ac860204..e8cb5c352b8 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInstallations' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Installations' s.description = <<-DESC diff --git a/FirebaseMLModelDownloader.podspec b/FirebaseMLModelDownloader.podspec index 317b3ddb3b6..7d17f6a05ac 100644 --- a/FirebaseMLModelDownloader.podspec +++ b/FirebaseMLModelDownloader.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMLModelDownloader' - s.version = '10.22.0-beta' + s.version = '10.23.0-beta' s.summary = 'Firebase ML Model Downloader' s.description = <<-DESC diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 0cee992e8b3..15195fc96f1 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessaging' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Messaging' s.description = <<-DESC diff --git a/FirebaseMessagingInterop.podspec b/FirebaseMessagingInterop.podspec index f93f2b9dec7..6e8989948cd 100644 --- a/FirebaseMessagingInterop.podspec +++ b/FirebaseMessagingInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessagingInterop' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Messaging functionality.' s.description = <<-DESC diff --git a/FirebasePerformance.podspec b/FirebasePerformance.podspec index 971eb5f2c34..c8d89bb17f5 100644 --- a/FirebasePerformance.podspec +++ b/FirebasePerformance.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebasePerformance' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Performance' s.description = <<-DESC diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 1efbc8ff746..efef50fef50 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfig' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Remote Config' s.description = <<-DESC diff --git a/FirebaseSessions.podspec b/FirebaseSessions.podspec index c44ee45d889..9ff30a18019 100644 --- a/FirebaseSessions.podspec +++ b/FirebaseSessions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSessions' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Sessions' s.description = <<-DESC diff --git a/FirebaseSharedSwift.podspec b/FirebaseSharedSwift.podspec index 9412356fc06..c451e29e1d7 100644 --- a/FirebaseSharedSwift.podspec +++ b/FirebaseSharedSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSharedSwift' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Shared Swift Extensions for Firebase' s.description = <<-DESC diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index a9ea3bd4127..51fbc91dd24 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseStorage' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Firebase Storage' s.description = <<-DESC diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index df2097af8c9..2167fa94cb5 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurement' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = 'Shared measurement methods for Google libraries. Not intended for direct use.' s.description = <<-DESC @@ -37,7 +37,7 @@ Pod::Spec.new do |s| s.default_subspecs = 'AdIdSupport' s.subspec 'AdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.22.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.23.0' ss.vendored_frameworks = 'Frameworks/GoogleAppMeasurementIdentitySupport.xcframework' end diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index bd77bd0edde..f56e61e7aca 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurementOnDeviceConversion' - s.version = '10.22.0' + s.version = '10.23.0' s.summary = <<-SUMMARY On device conversion measurement plugin for Google App Measurement. Not intended for direct use. diff --git a/Package.swift b/Package.swift index 05ca701bd7f..c95d29bfd51 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ import class Foundation.ProcessInfo import PackageDescription -let firebaseVersion = "10.22.0" +let firebaseVersion = "10.23.0" let package = Package( name: "Firebase", diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index 093aacae131..5bd4f3dd5e2 100755 --- a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift +++ b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift @@ -21,7 +21,7 @@ import Foundation /// The version and releasing fields of the non-Firebase pods should be reviewed every release. /// The array should be ordered so that any pod's dependencies precede it in the list. public let shared = Manifest( - version: "10.22.0", + version: "10.23.0", pods: [ Pod("FirebaseSharedSwift"), Pod("FirebaseCoreInternal"), From 42fea7d4894b5a530324a459586dd817321ba755 Mon Sep 17 00:00:00 2001 From: tsunghung <78230356+tsunghung@users.noreply.github.com> Date: Tue, 5 Mar 2024 07:32:18 -0800 Subject: [PATCH 070/104] Add API tests for hashed email and phone number (#12469) --- FirebaseAnalyticsSwift/Tests/ObjCAPI/ObjCAPITests.m | 2 ++ FirebaseAnalyticsSwift/Tests/SwiftUnit/AnalyticsAPITests.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/FirebaseAnalyticsSwift/Tests/ObjCAPI/ObjCAPITests.m b/FirebaseAnalyticsSwift/Tests/ObjCAPI/ObjCAPITests.m index a0be6eee954..6392674a812 100644 --- a/FirebaseAnalyticsSwift/Tests/ObjCAPI/ObjCAPITests.m +++ b/FirebaseAnalyticsSwift/Tests/ObjCAPI/ObjCAPITests.m @@ -60,6 +60,8 @@ - (void)consentTests:(NSURL *)url { - (void)onDeviceConversionTests:(NSURL *)url { [FIRAnalytics initiateOnDeviceConversionMeasurementWithEmailAddress:@"a@.a.com"]; [FIRAnalytics initiateOnDeviceConversionMeasurementWithPhoneNumber:@"+15555555555"]; + [FIRAnalytics initiateOnDeviceConversionMeasurementWithHashedEmailAddress:[NSData data]]; + [FIRAnalytics initiateOnDeviceConversionMeasurementWithHashedPhoneNumber:[NSData data]]; } - (NSArray *)eventNames { diff --git a/FirebaseAnalyticsSwift/Tests/SwiftUnit/AnalyticsAPITests.swift b/FirebaseAnalyticsSwift/Tests/SwiftUnit/AnalyticsAPITests.swift index 0b0542f8417..1d6a454f771 100644 --- a/FirebaseAnalyticsSwift/Tests/SwiftUnit/AnalyticsAPITests.swift +++ b/FirebaseAnalyticsSwift/Tests/SwiftUnit/AnalyticsAPITests.swift @@ -73,6 +73,8 @@ final class AnalyticsAPITests { Analytics.initiateOnDeviceConversionMeasurement(emailAddress: "test@gmail.com") Analytics.initiateOnDeviceConversionMeasurement(phoneNumber: "+15555555555") + Analytics.initiateOnDeviceConversionMeasurement(hashedEmailAddress: Data()) + Analytics.initiateOnDeviceConversionMeasurement(hashedPhoneNumber: Data()) // MARK: - EventNames From 28007226650d76ee191597a3c42e4d1fb08640b6 Mon Sep 17 00:00:00 2001 From: themiswang Date: Tue, 5 Mar 2024 13:41:41 -0500 Subject: [PATCH 071/104] Update upload-symbols to 13.7 (#12471) --- Crashlytics/CHANGELOG.md | 3 +++ Crashlytics/upload-symbols | Bin 742656 -> 759536 bytes 2 files changed, 3 insertions(+) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 4c449c579b5..2c06843fcea 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [added] Updated upload-symbols to 13.7 with VisionPro build phase support. (#12306) + # 10.22.0 - [fixed] Force validation or rotation of FIDs for FirebaseSessions. - [changed] Removed calls to statfs in the Crashlytics SDK to comply with Apple Privacy Manifests. This change removes support for collecting Disk Space Free in Crashlytics reports. diff --git a/Crashlytics/upload-symbols b/Crashlytics/upload-symbols index ace10d505b61e96229910f9c823dee9f435ed6f0..481428e6200d238fdd5e7950e98bb0caaf2d946a 100755 GIT binary patch literal 759536 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

QiGj*Q(c9877aeRWId3*s33sgXUt2kB@++Y;3Zn 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
NF^XEoiLXf5@w^Y&RKV%79ruO!hKKhV{ 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�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

7MRFLs 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

  • a6q28H0^>`w&d%EKVS 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

    ZS~;Pa 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�Lmb|>bRdkr)Ab6b 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 z�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 From 89526bf44a74f0cc1e53a18de74090e01f1828db Mon Sep 17 00:00:00 2001 From: tsunghung <78230356+tsunghung@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:22:54 -0800 Subject: [PATCH 072/104] Analytics 10.22.1 (#12482) --- FirebaseAnalyticsOnDeviceConversion.podspec | 4 ++-- GoogleAppMeasurementOnDeviceConversion.podspec | 4 ++-- Package.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseAnalyticsOnDeviceConversion.podspec b/FirebaseAnalyticsOnDeviceConversion.podspec index e4ec770b8e5..52d1135775e 100644 --- a/FirebaseAnalyticsOnDeviceConversion.podspec +++ b/FirebaseAnalyticsOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsOnDeviceConversion' - s.version = '10.23.0' + s.version = '10.22.1' s.summary = 'On device conversion measurement plugin for FirebaseAnalytics. Not intended for direct use.' s.description = <<-DESC @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.cocoapods_version = '>= 1.12.0' - s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.23.0' + s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.22.1' s.static_framework = true diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index f56e61e7aca..d2a11cd4683 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurementOnDeviceConversion' - s.version = '10.23.0' + s.version = '10.22.1' s.summary = <<-SUMMARY On device conversion measurement plugin for Google App Measurement. Not intended for direct use. @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/a0551aa6065a8330/GoogleAppMeasurementOnDeviceConversion-10.22.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/c696679c30b9561e/GoogleAppMeasurementOnDeviceConversion-10.22.1.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/Package.swift b/Package.swift index c95d29bfd51..ae854aa3f10 100644 --- a/Package.swift +++ b/Package.swift @@ -1324,7 +1324,7 @@ func googleAppMeasurementDependency() -> Package.Dependency { return .package(url: appMeasurementURL, branch: "main") } - return .package(url: appMeasurementURL, exact: "10.22.0") + return .package(url: appMeasurementURL, exact: "10.22.1") } func abseilDependency() -> Package.Dependency { From 5797426f043e2f40b1249feb76f83bcae70801da Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 7 Mar 2024 01:44:20 +0000 Subject: [PATCH 073/104] Upgrade `clang-format` to v18 (#12483) --- CONTRIBUTING.md | 2 +- FirebaseDatabase/Tests/Integration/FData.m | 7 ++++--- .../Tests/Integration/FIRDatabaseQueryTests.m | 4 +--- FirebaseDatabase/Tests/Integration/FOrderByTests.m | 4 +--- Firestore/core/src/immutable/keys_view.h | 4 ++-- Firestore/core/src/util/hashing.h | 8 ++++---- README.md | 2 +- scripts/setup_check.sh | 2 +- scripts/style.sh | 4 ++-- 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2023549978c..b62328f4dde 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,7 +132,7 @@ To develop Firebase software, **install**: To install [clang-format] and [mint] using [Homebrew]: ```console - brew install clang-format@17 + brew install clang-format@18 brew install mint ``` diff --git a/FirebaseDatabase/Tests/Integration/FData.m b/FirebaseDatabase/Tests/Integration/FData.m index 012603b8ca0..2fa04319f9f 100644 --- a/FirebaseDatabase/Tests/Integration/FData.m +++ b/FirebaseDatabase/Tests/Integration/FData.m @@ -314,9 +314,10 @@ - (void)testWriteLeafNodeOverwriteAtParentMultipleTimesVerifyExpectedEvents { ios - sdk / firebase - ios - sdk / Example / Database / Tests / Helpers / FEventTester - .m : 123 because it was raised inside test case -[FEventTester(null)] which has no - associated XCTestRun object.This may happen when test cases are - constructed and invoked independently of standard XCTest infrastructure, + .m : 123 because it was raised inside test case - + [FEventTester( + null)] which has no associated XCTestRun object.This may happen when test cases + are constructed and invoked independently of standard XCTest infrastructure, or when the test has already finished ." - Expected http://localhost:9000/-M8IJYWb68MuqQKKz2IY/a aa (0) to match " "http://localhost:9000/-M8IJYWb68MuqQKKz2IY/a (null) (4)' from " diff --git a/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m b/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m index a9945b50e49..4163cd15ae6 100644 --- a/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m +++ b/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m @@ -4192,9 +4192,7 @@ - (void)testGetForParentReturnsCorrectValue { [ref getDataWithCompletionBlock:^(NSError* err, FIRDataSnapshot* snapshot) { XCTAssertNil(err); - XCTAssertEqualObjects( - [snapshot value], - @{@"a" : @1}); + XCTAssertEqualObjects([snapshot value], @{@"a" : @1}); done = YES; }]; } diff --git a/FirebaseDatabase/Tests/Integration/FOrderByTests.m b/FirebaseDatabase/Tests/Integration/FOrderByTests.m index 86642d36e65..e3734f8d915 100644 --- a/FirebaseDatabase/Tests/Integration/FOrderByTests.m +++ b/FirebaseDatabase/Tests/Integration/FOrderByTests.m @@ -235,9 +235,7 @@ - (void)testFiresChildMovedEvents { moved = YES; XCTAssertEqualObjects(snapshot.key, @"greg", @""); XCTAssertEqualObjects(prevName, @"rob", @""); - XCTAssertEqualObjects( - snapshot.value, - @{@"nuggets" : @57}, @""); + XCTAssertEqualObjects(snapshot.value, @{@"nuggets" : @57}, @""); }]; [ref setValue:initial]; diff --git a/Firestore/core/src/immutable/keys_view.h b/Firestore/core/src/immutable/keys_view.h index fad8510cd59..c866ba5ef6c 100644 --- a/Firestore/core/src/immutable/keys_view.h +++ b/Firestore/core/src/immutable/keys_view.h @@ -47,8 +47,8 @@ auto KeysView(const Range& range) -> KeysRange { } template -auto KeysViewFrom(const Range& range, const K& key) - -> KeysRange { +auto KeysViewFrom(const Range& range, + const K& key) -> KeysRange { auto keys_begin = util::make_iterator_first(range.lower_bound(key)); auto keys_end = util::make_iterator_first(std::end(range)); return util::make_range(keys_begin, keys_end); diff --git a/Firestore/core/src/util/hashing.h b/Firestore/core/src/util/hashing.h index 7286c44cc32..33375f03a63 100644 --- a/Firestore/core/src/util/hashing.h +++ b/Firestore/core/src/util/hashing.h @@ -190,8 +190,8 @@ auto RankedInvokeHash(const Range& range, HashChoice<3>) * value can itself be hashed. */ template -auto RankedInvokeHash(const absl::optional& option, HashChoice<4>) - -> decltype(InvokeHash(*option)) { +auto RankedInvokeHash(const absl::optional& option, + HashChoice<4>) -> decltype(InvokeHash(*option)) { return option ? InvokeHash(*option) : -1171; } @@ -202,8 +202,8 @@ size_t RankedInvokeHash(K value, HashChoice<5>) { } template -auto RankedInvokeHash(const std::unique_ptr& ptr, HashChoice<6>) - -> decltype(InvokeHash(*ptr)) { +auto RankedInvokeHash(const std::unique_ptr& ptr, + HashChoice<6>) -> decltype(InvokeHash(*ptr)) { return ptr ? InvokeHash(*ptr) : 23631; } diff --git a/README.md b/README.md index 7ab83d9c446..6e0742ddb29 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ GitHub Actions will verify that any code changes are done in a style-compliant way. Install `clang-format` and `mint`: ```console -brew install clang-format@17 +brew install clang-format@18 brew install mint ``` diff --git a/scripts/setup_check.sh b/scripts/setup_check.sh index 057abc47c0b..eb323eb0473 100755 --- a/scripts/setup_check.sh +++ b/scripts/setup_check.sh @@ -35,7 +35,7 @@ fi # install clang-format brew update -brew install clang-format@17 +brew install clang-format@18 # mint installs tools from Mintfile on demand. brew install mint diff --git a/scripts/style.sh b/scripts/style.sh index 58feb1d3da8..e8fcf12f37a 100755 --- a/scripts/style.sh +++ b/scripts/style.sh @@ -56,7 +56,7 @@ version="${version/ (*)/}" version="${version/.*/}" case "$version" in - 17) + 18) ;; google3-trunk) echo "Please use a publicly released clang-format; a recent LLVM release" @@ -65,7 +65,7 @@ case "$version" in exit 1 ;; *) - echo "Please upgrade to clang-format version 17." + echo "Please upgrade to clang-format version 18." echo "If it's installed via homebrew you can run:" echo "brew upgrade clang-format" exit 1 From 33cf55b444005a697c4b2acdc512974547a3e0f5 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:53:56 -0500 Subject: [PATCH 074/104] [Release] Add patch note for 10.22.1 (#12490) --- FirebaseCore/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index 10aeb1cc605..79ec5d5f636 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,3 +1,9 @@ +# Firebase 10.22.1 +- [Swift Package Manager / CocoaPods] Fix app validation issues on Xcode 15.3 + for those using the `FirebaseAnalyticsOnDeviceConversion` SDK. This issue was + caused by embedding an incomplete `Info.plist` from a dependency of the SDK. + (#12441) + # Firebase 10.22.0 - [Swift Package Manager] Firebase now enforces a Swift 5.7.1 minimum version, which is aligned with the Xcode 14.1 minimum. (#12350) From 48a77e345196c2123dcfdd58b7166c16604e63ed Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 8 Mar 2024 07:28:24 -0800 Subject: [PATCH 075/104] Restore 10.23.0 versions to main after 10.22.1 (#12497) --- FirebaseAnalyticsOnDeviceConversion.podspec | 4 ++-- GoogleAppMeasurementOnDeviceConversion.podspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FirebaseAnalyticsOnDeviceConversion.podspec b/FirebaseAnalyticsOnDeviceConversion.podspec index 52d1135775e..e4ec770b8e5 100644 --- a/FirebaseAnalyticsOnDeviceConversion.podspec +++ b/FirebaseAnalyticsOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsOnDeviceConversion' - s.version = '10.22.1' + s.version = '10.23.0' s.summary = 'On device conversion measurement plugin for FirebaseAnalytics. Not intended for direct use.' s.description = <<-DESC @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.cocoapods_version = '>= 1.12.0' - s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.22.1' + s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.23.0' s.static_framework = true diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index d2a11cd4683..717e14fb888 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurementOnDeviceConversion' - s.version = '10.22.1' + s.version = '10.23.0' s.summary = <<-SUMMARY On device conversion measurement plugin for Google App Measurement. Not intended for direct use. From 6ae867d07e49cf47ad7beefe951218557c39a8b8 Mon Sep 17 00:00:00 2001 From: Jon Simantov Date: Fri, 8 Mar 2024 13:30:52 -0800 Subject: [PATCH 076/104] Patch abseil-cpp to ignore deprecated errors in new Xcode. (#12498) --- cmake/external/abseil-cpp.cmake | 3 +++ cmake/external/abseil-cpp.patch.txt | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 cmake/external/abseil-cpp.patch.txt diff --git a/cmake/external/abseil-cpp.cmake b/cmake/external/abseil-cpp.cmake index 8546dbfb875..7fde8522ccf 100644 --- a/cmake/external/abseil-cpp.cmake +++ b/cmake/external/abseil-cpp.cmake @@ -14,6 +14,7 @@ include(ExternalProject) +# Note: When updating to 20230802.0 or later, remove the PATCH_COMMAND below. set(version 20220623.0) ExternalProject_Add( @@ -31,4 +32,6 @@ ExternalProject_Add( INSTALL_COMMAND "" TEST_COMMAND "" HTTP_HEADER "${EXTERNAL_PROJECT_HTTP_HEADER}" + + PATCH_COMMAND patch -Np1 -i ${CMAKE_CURRENT_LIST_DIR}/abseil-cpp.patch.txt ) diff --git a/cmake/external/abseil-cpp.patch.txt b/cmake/external/abseil-cpp.patch.txt new file mode 100644 index 00000000000..5c906321a38 --- /dev/null +++ b/cmake/external/abseil-cpp.patch.txt @@ -0,0 +1,27 @@ +diff --git a/absl/meta/type_traits.h b/absl/meta/type_traits.h +index d886cb30..c2a2d15e 100644 +--- a/absl/meta/type_traits.h ++++ b/absl/meta/type_traits.h +@@ -35,6 +35,12 @@ + #ifndef ABSL_META_TYPE_TRAITS_H_ + #define ABSL_META_TYPE_TRAITS_H_ + ++// Added by firebase-ios-sdk/cmake/external/abseil-cpp.patch.txt ++#if __clang__ ++#pragma clang diagnostic push ++#pragma clang diagnostic ignored "-Wdeprecated" ++#endif // __clang__ ++ + #include + #include + #include +@@ -794,4 +800,9 @@ using swap_internal::StdSwapIsUnconstrained; + ABSL_NAMESPACE_END + } // namespace absl + ++// Added by firebase-ios-sdk/cmake/external/abseil-cpp.patch.txt ++#if __clang__ ++#pragma clang diagnostic pop ++#endif // __clang__ ++ + #endif // ABSL_META_TYPE_TRAITS_H_ From d715ee896924bbc6969e2e9b19170f21df7a65aa Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 8 Mar 2024 18:17:30 -0800 Subject: [PATCH 077/104] Fix CI breakage from recent merge (#12504) --- cmake/external/abseil-cpp.patch.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/external/abseil-cpp.patch.txt b/cmake/external/abseil-cpp.patch.txt index 5c906321a38..1624ee06fab 100644 --- a/cmake/external/abseil-cpp.patch.txt +++ b/cmake/external/abseil-cpp.patch.txt @@ -5,7 +5,7 @@ index d886cb30..c2a2d15e 100644 @@ -35,6 +35,12 @@ #ifndef ABSL_META_TYPE_TRAITS_H_ #define ABSL_META_TYPE_TRAITS_H_ - + +// Added by firebase-ios-sdk/cmake/external/abseil-cpp.patch.txt +#if __clang__ +#pragma clang diagnostic push @@ -18,7 +18,7 @@ index d886cb30..c2a2d15e 100644 @@ -794,4 +800,9 @@ using swap_internal::StdSwapIsUnconstrained; ABSL_NAMESPACE_END } // namespace absl - + +// Added by firebase-ios-sdk/cmake/external/abseil-cpp.patch.txt +#if __clang__ +#pragma clang diagnostic pop From 01c7d950dbea30720dfb560f45d28d8611c654e2 Mon Sep 17 00:00:00 2001 From: hs7 Date: Mon, 11 Mar 2024 23:53:01 +0900 Subject: [PATCH 078/104] Resolved warning no rule to process file 'PrivacyInfo.xcprivacy' of type 'text.xml' with CocoaPods (#12513) --- FirebaseMessaging.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 15195fc96f1..6e3cbcde4e6 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -37,7 +37,7 @@ device, and it is completely free. base_dir = "FirebaseMessaging/" s.source_files = [ - base_dir + 'Sources/**/*', + base_dir + 'Sources/**/*.{c,m,h}', base_dir + 'Sources/Protogen/nanopb/*.h', base_dir + 'Interop/*.h', 'Interop/Analytics/Public/*.h', From 34667bc0bfc6c853f45d822ccaa0f864d443e588 Mon Sep 17 00:00:00 2001 From: wu-hui <53845758+wu-hui@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:12:09 -0400 Subject: [PATCH 079/104] Upgrade grpc to 1.62 for cocoapods (#12398) --- FirebaseFirestoreInternal.podspec | 5 +++-- Firestore/core/test/unit/nanopb/message_test.cc | 7 ------- Firestore/core/test/unit/util/to_string_test.cc | 5 +++-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index f6f3235d733..4717dcf1cf7 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -94,7 +94,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.dependency 'FirebaseAppCheckInterop', '~> 10.17' s.dependency 'FirebaseCore', '~> 10.0' - abseil_version = '~> 1.20220623.0' + abseil_version = '~> 1.20240116.1' s.dependency 'abseil/algorithm', abseil_version s.dependency 'abseil/base', abseil_version s.dependency 'abseil/container/flat_hash_map', abseil_version @@ -104,7 +104,8 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.dependency 'abseil/time', abseil_version s.dependency 'abseil/types', abseil_version - s.dependency 'gRPC-C++', '~> 1.49.1' + s.dependency 'gRPC-Core', '~> 1.62.0' + s.dependency 'gRPC-C++', '~> 1.62.0' s.dependency 'leveldb-library', '~> 1.22' s.dependency 'nanopb', '>= 2.30908.0', '< 2.30911.0' diff --git a/Firestore/core/test/unit/nanopb/message_test.cc b/Firestore/core/test/unit/nanopb/message_test.cc index 78489edbbad..77549e80857 100644 --- a/Firestore/core/test/unit/nanopb/message_test.cc +++ b/Firestore/core/test/unit/nanopb/message_test.cc @@ -25,7 +25,6 @@ #include "Firestore/core/src/nanopb/writer.h" #include "Firestore/core/src/remote/grpc_nanopb.h" #include "Firestore/core/test/unit/testutil/status_testing.h" -#include "grpcpp/impl/codegen/grpc_library.h" #include "grpcpp/support/byte_buffer.h" #include "gtest/gtest.h" @@ -60,12 +59,6 @@ class MessageTest : public testing::Test { grpc::ByteBuffer BadProto() const { return {}; } - - private: - // Note: gRPC slice will crash upon destruction if gRPC library hasn't been - // initialized, which is normally done by inheriting from this class (which - // does initialization in its constructor). - grpc::GrpcLibraryCodegen grpc_initializer_; }; #if !__clang_analyzer__ diff --git a/Firestore/core/test/unit/util/to_string_test.cc b/Firestore/core/test/unit/util/to_string_test.cc index a5b38d177c9..9f4a6f58a37 100644 --- a/Firestore/core/test/unit/util/to_string_test.cc +++ b/Firestore/core/test/unit/util/to_string_test.cc @@ -49,8 +49,9 @@ TEST(ToStringTest, SimpleTypes) { EXPECT_EQ(ToString(nullptr), "null"); - void* ptr = reinterpret_cast(0xBAAAAAAD); - EXPECT_EQ(ToString(ptr), "baaaaaad"); + // TODO(b/326402002): Below no longer passes after abseil upgrade + // to 1.20240116.1 void* ptr = reinterpret_cast(0xBAAAAAAD); + // EXPECT_EQ(ToString(ptr), "baaaaaad"); } TEST(ToStringTest, CustomToString) { From 07b76691759cb99b3890a1162dde8f499e2ade79 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:53:11 -0400 Subject: [PATCH 080/104] Snapshot listener source from cache (#12370) --- Firestore/CHANGELOG.md | 3 + .../Firestore.xcodeproj/project.pbxproj | 22 + .../Example/Tests/SpecTests/FSTSpecTests.mm | 19 +- .../Tests/SpecTests/FSTSyncEngineTestDriver.h | 3 +- .../SpecTests/FSTSyncEngineTestDriver.mm | 4 +- .../json/listen_source_spec_test.json | 5895 +++++++++++++++++ .../Tests/Util/FSTIntegrationTestCase.h | 5 + .../Tests/Util/FSTIntegrationTestCase.mm | 16 + Firestore/Source/API/FIRDocumentReference.mm | 9 + Firestore/Source/API/FIRQuery.mm | 9 + .../Source/API/FIRSnapshotListenOptions.mm | 63 + Firestore/Source/API/converters.h | 4 + Firestore/Source/API/converters.mm | 12 + .../FirebaseFirestore/FIRDocumentReference.h | 17 + .../Public/FirebaseFirestore/FIRQuery.h | 16 + .../FIRSnapshotListenOptions.h | 83 + .../FirebaseFirestore/FirebaseFirestore.h | 1 + Firestore/Swift/Tests/BridgingHeader.h | 1 + .../SnapshotListenerSourceTests.swift | 684 ++ Firestore/core/src/api/listen_source.h | 37 + Firestore/core/src/core/event_manager.cc | 65 +- Firestore/core/src/core/event_manager.h | 25 +- Firestore/core/src/core/listen_options.h | 51 +- Firestore/core/src/core/query_listener.cc | 5 + Firestore/core/src/core/query_listener.h | 4 + Firestore/core/src/core/sync_engine.cc | 40 +- Firestore/core/src/core/sync_engine.h | 38 +- .../core/test/unit/core/event_manager_test.cc | 44 +- 28 files changed, 7138 insertions(+), 37 deletions(-) create mode 100644 Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json create mode 100644 Firestore/Source/API/FIRSnapshotListenOptions.mm create mode 100644 Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h create mode 100644 Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift create mode 100644 Firestore/core/src/api/listen_source.h diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 60d99030d03..ceded5649ef 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [feature] Enable snapshot listener option to retrieve data from local cache only. (#12370) + # 10.22.0 - [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index b0aa3693dd8..99f967e3ccd 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ 1CC56DCA513B98CE39A6ED45 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; }; 1CC9BABDD52B2A1E37E2698D /* mutation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C8522DE226C467C54E6788D8 /* mutation_test.cc */; }; 1CEEB0E7FBBB974224BBA557 /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; + 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */; }; 1D618761796DE311A1707AA2 /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; }; 1D71CA6BBA1E3433F243188E /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; }; 1D76DDBE57A4D66C64C00B65 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; @@ -238,6 +239,7 @@ 21E588CF29C72813D8A7A0A1 /* FSTExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = B8BFD9B37D1029D238BDD71E /* FSTExceptionCatcher.m */; }; 21E66B6A4A00786C3E934EB1 /* query_engine_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B8A853940305237AFDA8050B /* query_engine_test.cc */; }; 224496E752E42E220F809FAC /* resource.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1C3F7302BF4AE6CBC00ECDD0 /* resource.pb.cc */; }; + 2252357505C92A067DAC38B0 /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 226574601C3F6D14DF14C16B /* recovery_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 9C1AFCC9E616EC33D6E169CF /* recovery_spec_test.json */; }; 227CFA0B2A01884C277E4F1D /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; 229D1A9381F698D71F229471 /* string_win_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 79507DF8378D3C42F5B36268 /* string_win_test.cc */; }; @@ -508,6 +510,7 @@ 5150E9F256E6E82D6F3CB3F1 /* bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F7FC06E0A47D393DE1759AE1 /* bundle_cache_test.cc */; }; 518BF03D57FBAD7C632D18F8 /* FIRQueryUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */; }; 51A483DE202CC3E9FCD8FF6E /* Validation_BloomFilterTest_MD5_5000_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = B0520A41251254B3C24024A3 /* Validation_BloomFilterTest_MD5_5000_01_membership_test_result.json */; }; + 5250AE69A391E7A3310E013B /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 52967C3DD7896BFA48840488 /* byte_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5342CDDB137B4E93E2E85CCA /* byte_string_test.cc */; }; 529AB59F636060FEA21BD4FF /* garbage_collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = AAED89D7690E194EF3BA1132 /* garbage_collection_spec_test.json */; }; 5360D52DCAD1069B1E4B0B9D /* testing_hooks_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A002425BC4FC4E805F4175B6 /* testing_hooks_test.cc */; }; @@ -816,6 +819,7 @@ 6F67601562343B63B8996F7A /* FSTTestingHooks.mm in Sources */ = {isa = PBXBuildFile; fileRef = D85AC18C55650ED230A71B82 /* FSTTestingHooks.mm */; }; 6F914209F46E6552B5A79570 /* async_queue_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */; }; 6FAC16B7FBD3B40D11A6A816 /* target.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7D20B89AAC00B5BCE7 /* target.pb.cc */; }; + 6FB40B88ACB4CFB34917319C /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 6FC85C48CF8235BA1845E1C8 /* FSTUserDataReaderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D9892F204959C50613F16C8 /* FSTUserDataReaderTests.mm */; }; 6FCC64A1937E286E76C294D0 /* logic_utils_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 28B45B2104E2DAFBBF86DBB7 /* logic_utils_test.cc */; }; 6FD2369F24E884A9D767DD80 /* FIRDocumentSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04B202154AA00B64F25 /* FIRDocumentSnapshotTests.mm */; }; @@ -1050,6 +1054,7 @@ 9C86EEDEA131BFD50255EEF1 /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; 9CC32ACF397022BB7DF11B52 /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = D22D4C211AC32E4F8B4883DA /* Validation_BloomFilterTest_MD5_500_0001_bloom_filter_proto.json */; }; 9CE07BAAD3D3BC5F069D38FE /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; }; + 9CFF379C7404F7CE6B26AF29 /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; 9D71628E38D9F64C965DF29E /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; 9E1997789F19BF2E9029012E /* FIRCompositeIndexQueryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 65AF0AB593C3AD81A1F1A57E /* FIRCompositeIndexQueryTests.mm */; }; 9E656F4FE92E8BFB7F625283 /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; }; @@ -1059,6 +1064,7 @@ 9F9244225BE2EC88AA0CE4EF /* sorted_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */; }; A05BC6BDA2ABE405009211A9 /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; }; A06FBB7367CDD496887B86F8 /* leveldb_opener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 75860CD13AF47EB1EA39EC2F /* leveldb_opener_test.cc */; }; + A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */; }; A0C6C658DFEE58314586907B /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; }; A0D61250F959BC52CEFF9467 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = C8FB22BCB9F454DA44BA80C8 /* Validation_BloomFilterTest_MD5_50000_01_membership_test_result.json */; }; A0E1C7F5C7093A498F65C5CF /* memory_bundle_cache_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB4AB1388538CD3CB19EB028 /* memory_bundle_cache_test.cc */; }; @@ -1068,6 +1074,7 @@ A17DBC8F24127DA8A381F865 /* testutil.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352820A3B3BD003E0143 /* testutil.cc */; }; A186FECD0257B92FDB0E83B8 /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B96CC29E9946508F022859C /* Validation_BloomFilterTest_MD5_50000_0001_membership_test_result.json */; }; A192648233110B7B8BD65528 /* field_transform_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7515B47C92ABEEC66864B55C /* field_transform_test.cc */; }; + A1A466F55A1ED0AC5EE449BF /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; A1F57CC739211F64F2E9232D /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; }; A215078DBFBB5A4F4DADE8A9 /* leveldb_index_manager_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 166CE73C03AB4366AAC5201C /* leveldb_index_manager_test.cc */; }; A21819C437C3C80450D7EEEE /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; }; @@ -1156,11 +1163,13 @@ AFCA3C24AA751B5B2D3E6FEF /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 0D964D4936953635AC7E0834 /* Validation_BloomFilterTest_MD5_1_01_bloom_filter_proto.json */; }; AFE84E7B0C356CD2A113E56E /* status_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA33F964042646FDDAF9F9 /* status_testing.cc */; }; AFF7D2CF35B51656E4744164 /* bloom_filter_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2E6F09AD1EE0A6A452E9A08 /* bloom_filter_test.cc */; }; + B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */; }; B03F286F3AEC3781C386C646 /* FIRNumericTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */; }; B04E4FE20930384DF3A402F9 /* aggregate_query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AF924C79F49F793992A84879 /* aggregate_query_test.cc */; }; B0B779769926304268200015 /* query_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 731541602214AFFA0037F4DC /* query_spec_test.json */; }; B0D10C3451EDFB016A6EAF03 /* writer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = BC3C788D290A935C353CEAA1 /* writer_test.cc */; }; B0E745EAC5F37CA61F868F38 /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B3E4A77493524333133C5DC /* Validation_BloomFilterTest_MD5_50000_1_bloom_filter_proto.json */; }; + B104B69726EF6A5B41DAFB17 /* listen_source_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */; }; B15D17049414E2F5AE72C9C6 /* memory_local_store_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F6CA0C5638AB6627CB5B4CF4 /* memory_local_store_test.cc */; }; B188D7EC9A100F365DB02490 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json in Resources */ = {isa = PBXBuildFile; fileRef = DD990FD89C165F4064B4F608 /* Validation_BloomFilterTest_MD5_500_01_membership_test_result.json */; }; B192F30DECA8C28007F9B1D0 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; }; @@ -1740,6 +1749,8 @@ 4B59C0A7B2A4548496ED4E7D /* Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_1_0001_bloom_filter_proto.json; sourceTree = ""; }; 4BD051DBE754950FEAC7A446 /* Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json; path = bloom_filter_golden_test_data/Validation_BloomFilterTest_MD5_500_01_bloom_filter_proto.json; sourceTree = ""; }; 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_apple_benchmark.mm; sourceTree = ""; }; + 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnapshotListenerSourceTests.swift; sourceTree = ""; }; + 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */ = {isa = PBXFileReference; includeInIndex = 1; path = listen_source_spec_test.json; sourceTree = ""; }; 4F5B96F3ABCD2CA901DB1CD4 /* bundle_builder.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = bundle_builder.cc; sourceTree = ""; }; 526D755F65AC676234F57125 /* target_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = target_test.cc; sourceTree = ""; }; 52756B7624904C36FBB56000 /* fake_target_metadata_provider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = fake_target_metadata_provider.h; sourceTree = ""; }; @@ -2221,6 +2232,7 @@ 3355BE9391CC4857AF0BDAE3 /* DatabaseTests.swift */, 62E54B832A9E910A003347C8 /* IndexingTests.swift */, 621D620928F9CE7400D2FA26 /* QueryIntegrationTests.swift */, + 4D65F6E69993611D47DC8E7C /* SnapshotListenerSourceTests.swift */, ); path = Integration; sourceTree = ""; @@ -2968,6 +2980,7 @@ 8C7278B604B8799F074F4E8C /* index_spec_test.json */, 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */, 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */, + 4D9E51DA7A275D8B1CAEAEB2 /* listen_source_spec_test.json */, 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */, 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */, 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */, @@ -3378,6 +3391,7 @@ BFBE4732E93E38317B110778 /* index_spec_test.json in Resources */, 546877D72248206A005E3DE0 /* limbo_spec_test.json in Resources */, 546877D82248206A005E3DE0 /* limit_spec_test.json in Resources */, + 6FB40B88ACB4CFB34917319C /* listen_source_spec_test.json in Resources */, 546877D92248206A005E3DE0 /* listen_spec_test.json in Resources */, 546877DA2248206A005E3DE0 /* offline_spec_test.json in Resources */, 546877DB2248206A005E3DE0 /* orderby_spec_test.json in Resources */, @@ -3436,6 +3450,7 @@ 604B75044D6BEC2B7515EA1B /* index_spec_test.json in Resources */, 54ACB6CB224C11F400172E69 /* limbo_spec_test.json in Resources */, 54ACB6CC224C11F400172E69 /* limit_spec_test.json in Resources */, + 2252357505C92A067DAC38B0 /* listen_source_spec_test.json in Resources */, 54ACB6CD224C11F400172E69 /* listen_spec_test.json in Resources */, 54ACB6CE224C11F400172E69 /* offline_spec_test.json in Resources */, 54ACB6CF224C11F400172E69 /* orderby_spec_test.json in Resources */, @@ -3484,6 +3499,7 @@ 77C36312F8025EC73991D7DA /* index_spec_test.json in Resources */, F08DA55D31E44CB5B9170CCE /* limbo_spec_test.json in Resources */, 15A5F95DA733FD89A1E4147D /* limit_spec_test.json in Resources */, + 5250AE69A391E7A3310E013B /* listen_source_spec_test.json in Resources */, D73BBA4AB42940AB187169E3 /* listen_spec_test.json in Resources */, C15F5F1E7427738F20C2D789 /* offline_spec_test.json in Resources */, 4781186C01D33E67E07F0D0D /* orderby_spec_test.json in Resources */, @@ -3532,6 +3548,7 @@ 6156C6A837D78D49ED8B8812 /* index_spec_test.json in Resources */, 85BC2AB572A400114BF59255 /* limbo_spec_test.json in Resources */, 9F41D724D9947A89201495AD /* limit_spec_test.json in Resources */, + B104B69726EF6A5B41DAFB17 /* listen_source_spec_test.json in Resources */, 3CFFA6F016231446367E3A69 /* listen_spec_test.json in Resources */, A0C6C658DFEE58314586907B /* offline_spec_test.json in Resources */, D98430EA4FAA357D855FA50F /* orderby_spec_test.json in Resources */, @@ -3599,6 +3616,7 @@ 3783E25DFF9E5C0896D34FEF /* index_spec_test.json in Resources */, 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */, 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */, + 9CFF379C7404F7CE6B26AF29 /* listen_source_spec_test.json in Resources */, 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */, 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */, 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */, @@ -3665,6 +3683,7 @@ E04607A1E2964684184E8AEA /* index_spec_test.json in Resources */, 2AD8EE91928AE68DF268BEDA /* limbo_spec_test.json in Resources */, BC5AC8890974E0821431267E /* limit_spec_test.json in Resources */, + A1A466F55A1ED0AC5EE449BF /* listen_source_spec_test.json in Resources */, 5B89B1BA0AD400D9BF581420 /* listen_spec_test.json in Resources */, F660788F69B4336AC6CD2720 /* offline_spec_test.json in Resources */, 4F5714D37B6D119CB07ED8AE /* orderby_spec_test.json in Resources */, @@ -4572,6 +4591,7 @@ 3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */, 62E54B862A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620C28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, + 1CFBD4563960D8A20C4679A3 /* SnapshotListenerSourceTests.swift in Sources */, 4D42E5C756229C08560DD731 /* XCTestCase+Await.mm in Sources */, 09BE8C01EC33D1FD82262D5D /* aggregate_query_test.cc in Sources */, 0EC3921AE220410F7394729B /* aggregation_result.pb.cc in Sources */, @@ -4812,6 +4832,7 @@ 5E89B1A5A5430713C79C4854 /* FirestoreEncoderTests.swift in Sources */, 62E54B852A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620B28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, + A0BC30D482B0ABD1A3A24CDC /* SnapshotListenerSourceTests.swift in Sources */, 736C4E82689F1CA1859C4A3F /* XCTestCase+Await.mm in Sources */, 412BE974741729A6683C386F /* aggregate_query_test.cc in Sources */, DF983A9C1FBF758AF3AF110D /* aggregation_result.pb.cc in Sources */, @@ -5299,6 +5320,7 @@ 6F45846C159D3C063DBD3CBE /* FirestoreEncoderTests.swift in Sources */, 62E54B842A9E910B003347C8 /* IndexingTests.swift in Sources */, 621D620A28F9CE7400D2FA26 /* QueryIntegrationTests.swift in Sources */, + B00F8D1819EE20C45B660940 /* SnapshotListenerSourceTests.swift in Sources */, 5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */, B04E4FE20930384DF3A402F9 /* aggregate_query_test.cc in Sources */, 1A3D8028303B45FCBB21CAD3 /* aggregation_result.pb.cc in Sources */, diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 622b3b527d7..f2b8ca2e4be 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -41,6 +41,7 @@ #include "Firestore/core/src/bundle/bundle_reader.h" #include "Firestore/core/src/bundle/bundle_serializer.h" #include "Firestore/core/src/core/field_filter.h" +#import "Firestore/core/src/core/listen_options.h" #include "Firestore/core/src/credentials/user.h" #include "Firestore/core/src/local/persistence.h" #include "Firestore/core/src/local/target_data.h" @@ -79,10 +80,12 @@ using firebase::firestore::Error; using firebase::firestore::google_firestore_v1_ArrayValue; using firebase::firestore::google_firestore_v1_Value; +using firebase::firestore::api::ListenSource; using firebase::firestore::api::LoadBundleTask; using firebase::firestore::bundle::BundleReader; using firebase::firestore::bundle::BundleSerializer; using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ListenOptions; using firebase::firestore::core::Query; using firebase::firestore::credentials::User; using firebase::firestore::local::Persistence; @@ -385,11 +388,25 @@ - (DocumentViewChange)parseChange:(NSDictionary *)jsonDoc ofType:(DocumentViewCh return DocumentViewChange{std::move(doc), type}; } +- (ListenOptions)parseOptions:(NSDictionary *)optionsSpec { + ListenOptions options = ListenOptions::FromIncludeMetadataChanges(true); + + if (optionsSpec != nil) { + ListenSource source = + [optionsSpec[@"source"] isEqual:@"cache"] ? ListenSource::Cache : ListenSource::Default; + // include_metadata_changes are default to true in spec tests + options = ListenOptions::FromOptions(true, source); + } + + return options; +} + #pragma mark - Methods for doing the steps of the spec test. - (void)doListen:(NSDictionary *)listenSpec { Query query = [self parseQuery:listenSpec[@"query"]]; - TargetId actualID = [self.driver addUserListenerWithQuery:std::move(query)]; + ListenOptions options = [self parseOptions:listenSpec[@"options"]]; + TargetId actualID = [self.driver addUserListenerWithQuery:std::move(query) options:options]; TargetId expectedID = [listenSpec[@"targetId"] intValue]; XCTAssertEqual(actualID, expectedID, @"targetID assigned to listen"); diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index c99836f8406..978ae28a4e5 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -146,9 +146,10 @@ typedef std:: * Resulting events are captured and made available via the capturedEventsSinceLastCall method. * * @param query A valid query to execute against the backend. + * @param options A listen option to configure snapshot listener. * @return The target ID assigned by the system to track the query. */ -- (model::TargetId)addUserListenerWithQuery:(core::Query)query; +- (model::TargetId)addUserListenerWithQuery:(core::Query)query options:(core::ListenOptions)options; /** * Removes a listener from the FSTSyncEngine as if the user had removed a listener corresponding diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index 41fe884c70f..7fe5dc5d91e 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -476,10 +476,8 @@ - (FSTOutstandingWrite *)receiveWriteError:(int)errorCode return result; } -- (TargetId)addUserListenerWithQuery:(Query)query { - // TODO(dimond): Allow customizing listen options in spec tests +- (TargetId)addUserListenerWithQuery:(Query)query options:(ListenOptions)options { // TODO(dimond): Change spec tests to verify isFromCache on snapshots - ListenOptions options = ListenOptions::FromIncludeMetadataChanges(true); auto listener = QueryListener::Create( query, options, [self, query](const StatusOr &maybe_snapshot) { FSTQueryEvent *event = [[FSTQueryEvent alloc] init]; diff --git a/Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json new file mode 100644 index 00000000000..1912afc320f --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/listen_source_spec_test.json @@ -0,0 +1,5895 @@ +{ + "Clients can have multiple listeners with different sources": { + "describeName": "Listens source options:", + "itName": "Clients can have multiple listeners with different sources", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + }, + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + }, + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Contents of query are cleared when listen is removed.": { + "comment": "Explicitly tests eager GC behavior", + "describeName": "Listens source options:", + "itName": "Contents of query are cleared when listen is removed.", + "tags": [ + "eager-gc" + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "key": "a" + } + ] + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "writeAck": { + "version": 1000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Documents are cleared when listen is removed.": { + "describeName": "Listens source options:", + "itName": "Documents are cleared when listen is removed.", + "tags": [ + "eager-gc" + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "matches": true + } + ] + }, + { + "userSet": [ + "collection/b", + { + "matches": true + } + ] + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "matches": true + }, + "version": 0 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "matches": true + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "writeAck": { + "version": 1000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/b" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "userSet": [ + "collection/b", + { + "matches": false + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "matches": true + }, + "version": 0 + } + ] + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "writeAck": { + "version": 3000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/b" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userUnlisten": [ + 4, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Doesn't include unknown documents in cached result": { + "describeName": "Listens source options:", + "itName": "Doesn't include unknown documents in cached result", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": true + }, + "steps": [ + { + "userSet": [ + "collection/exists", + { + "key": "a" + } + ] + }, + { + "userPatch": [ + "collection/unknown", + { + "key": "b" + } + ] + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/exists", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Doesn't raise 'hasPendingWrites' for deletes": { + "describeName": "Listens source options:", + "itName": "Doesn't raise 'hasPendingWrites' for deletes", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userDelete": "collection/a", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ] + } + ] + }, + { + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + } + ] + }, + "Empty initial snapshot is raised from cache": { + "describeName": "Listens source options:", + "itName": "Empty initial snapshot is raised from cache", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Empty-due-to-delete initial snapshot is raised from cache": { + "describeName": "Listens source options:", + "itName": "Empty-due-to-delete initial snapshot is raised from cache", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userDelete": "collection/a" + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Listeners with different source shares watch changes between primary and secondary clients": { + "describeName": "Listens source options:", + "itName": "Listeners with different source shares watch changes between primary and secondary clients", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 3, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Local mutations notifies listeners sourced from cache in all tabs": { + "describeName": "Listens source options:", + "itName": "Local mutations notifies listeners sourced from cache in all tabs", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userSet": [ + "collection/a", + { + "key": "a" + } + ], + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "a" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Mirror queries being listened from different sources while listening to server in primary tab": { + "describeName": "Listens source options:", + "itName": "Mirror queries being listened from different sources while listening to server in primary tab", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Mirror queries being listened in different clients sourced from cache ": { + "describeName": "Listens source options:", + "itName": "Mirror queries being listened in different clients sourced from cache ", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userUnlisten": [ + 4, + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "userSet": [ + "collection/c", + { + "sort": -1 + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "sort": -1 + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Mirror queries being listened in the same secondary client sourced from cache": { + "describeName": "Listens source options:", + "itName": "Mirror queries being listened in the same secondary client sourced from cache", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 4 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "userUnlisten": [ + 4, + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "userSet": [ + "collection/c", + { + "sort": -1 + } + ], + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "sort": -1 + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Mirror queries from different sources while listening to server in secondary tab": { + "describeName": "Listens source options:", + "itName": "Mirror queries from different sources while listening to server in secondary tab", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + }, + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "sort", + "desc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": -1 + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "sort", + "asc" + ] + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "sort": 1 + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Newer deleted docs from bundles should delete cached docs": { + "describeName": "Listens source options:", + "itName": "Newer deleted docs from bundles should delete cached docs", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.003000000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":158}}155{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.003000000Z\",\"exists\":false}}", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ] + } + ] + } + ] + }, + "Newer docs from bundles should keep not raise snapshot if there are unacknowledged writes": { + "describeName": "Listens source options:", + "itName": "Newer docs from bundles should keep not raise snapshot if there are unacknowledged writes", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-250" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 250 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userPatch": [ + "collection/a", + { + "value": "patched" + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "value": "patched" + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.001001000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":388}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.001001000Z\",\"exists\":true}}228{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.000250000Z\",\"updateTime\":\"1970-01-01T00:00:00.001001000Z\",\"fields\":{\"value\":{\"stringValue\":\"fromBundle\"}}}}" + } + ] + }, + "Newer docs from bundles should overwrite cache": { + "describeName": "Listens source options:", + "itName": "Newer docs from bundles should overwrite cache", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.003000000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":379}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.003000000Z\",\"exists\":true}}219{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.001999000Z\",\"updateTime\":\"1970-01-01T00:00:00.002999000Z\",\"fields\":{\"value\":{\"stringValue\":\"b\"}}}}", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "b" + }, + "version": 2999 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes": { + "describeName": "Listens source options:", + "itName": "Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-250" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 250 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 250 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userPatch": [ + "collection/a", + { + "value": "patched" + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "value": "patched" + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "writeAck": { + "version": 1000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.000500000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":379}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.000500000Z\",\"exists\":true}}219{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.000250000Z\",\"updateTime\":\"1970-01-01T00:00:00.000500000Z\",\"fields\":{\"value\":{\"stringValue\":\"b\"}}}}" + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.001001000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":388}}154{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.001001000Z\",\"exists\":true}}228{\"document\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"createTime\":\"1970-01-01T00:00:00.000250000Z\",\"updateTime\":\"1970-01-01T00:00:00.001001000Z\",\"fields\":{\"value\":{\"stringValue\":\"fromBundle\"}}}}", + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "fromBundle" + }, + "version": 1001 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Older deleted docs from bundles should do nothing": { + "describeName": "Listens source options:", + "itName": "Older deleted docs from bundles should do nothing", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "value": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "loadBundle": "127{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":\"1970-01-01T00:00:00.000999000Z\",\"version\":1,\"totalDocuments\":1,\"totalBytes\":158}}155{\"documentMetadata\":{\"name\":\"projects/test-project/databases/(default)/documents/collection/a\",\"readTime\":\"1970-01-01T00:00:00.000999000Z\",\"exists\":false}}" + } + ] + }, + "Primary client should not invoke watch request while all clients are listening to cache": { + "describeName": "Listens source options:", + "itName": "Primary client should not invoke watch request while all clients are listening to cache", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "Query is executed by primary client even if primary client only has listeners sourced from cache": { + "describeName": "Listens source options:", + "itName": "Query is executed by primary client even if primary client only has listeners sourced from cache", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Query only raises events in participating clients": { + "describeName": "Listens source options:", + "itName": "Query only raises events in participating clients", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 4, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 3, + "drainQueue": true + }, + { + "clientIndex": 3, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 3, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Un-listen to listeners from different source": { + "describeName": "Listens source options:", + "itName": "Un-listen to listeners from different source", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useEagerGCForMemory": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "userSet": [ + "collection/b", + { + "key": "b" + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "key": "b" + }, + "version": 0 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "onSnapshotsInSync fires for multiple listeners": { + "describeName": "Listens source options:", + "itName": "onSnapshotsInSync fires for multiple listeners", + "tags": [ + ], + "config": { + "numClients": 1, + "useEagerGCForMemory": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": { + "options": { + "source": "cache" + }, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "v": 2 + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedSnapshotsInSyncEvents": 1 + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 3 + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "v": 3 + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedSnapshotsInSyncEvents": 3 + }, + { + "removeSnapshotsInSyncListener": true + }, + { + "userSet": [ + "collection/a", + { + "v": 4 + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true, + "modified": [ + { + "createTime": 0, + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": true + }, + "value": { + "v": 4 + }, + "version": 0 + } + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedSnapshotsInSyncEvents": 2 + } + ] + } +} diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h index abd965beee3..a63fd66cd1b 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -35,6 +35,7 @@ @class FIRQuery; @class FIRWriteBatch; @class FSTEventAccumulator; +@class FIRTransaction; NS_ASSUME_NONNULL_BEGIN @@ -113,6 +114,10 @@ extern "C" { - (FIRDocumentReference *)addDocumentRef:(FIRCollectionReference *)ref data:(NSDictionary *)data; +- (void)runTransaction:(FIRFirestore *)db + block:(id _Nullable (^)(FIRTransaction *, NSError **error))block + completion:(nullable void (^)(id _Nullable result, NSError *_Nullable error))completion; + - (void)mergeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data; - (void)mergeDocumentRef:(FIRDocumentReference *)ref diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index 6633ed03748..f42d10bcb01 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -569,6 +569,22 @@ - (FIRDocumentReference *)addDocumentRef:(FIRCollectionReference *)ref return doc; } +- (void)runTransaction:(FIRFirestore *)db + block:(id _Nullable (^)(FIRTransaction *, NSError **error))block + completion: + (nullable void (^)(id _Nullable result, NSError *_Nullable error))completion { + XCTestExpectation *expectation = [self expectationWithDescription:@"runTransaction"]; + [db runTransactionWithOptions:nil + block:block + completion:^(id _Nullable result, NSError *_Nullable error) { + if (completion) { + completion(result, error); + } + [expectation fulfill]; + }]; + [self awaitExpectation:expectation]; +} + - (void)mergeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data { XCTestExpectation *expectation = [self expectationWithDescription:@"setDataWithMerge"]; [ref setData:data merge:YES completion:[self completionForExpectation:expectation]]; diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index b6f326ee8d9..a50a2c7df71 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -27,6 +27,7 @@ #import "Firestore/Source/API/FIRFirestoreSource+Internal.h" #import "Firestore/Source/API/FIRListenerRegistration+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" +#import "Firestore/Source/API/converters.h" #include "Firestore/core/src/api/collection_reference.h" #include "Firestore/core/src/api/document_reference.h" @@ -50,6 +51,7 @@ using firebase::firestore::api::DocumentSnapshotListener; using firebase::firestore::api::Firestore; using firebase::firestore::api::ListenerRegistration; +using firebase::firestore::api::MakeListenSource; using firebase::firestore::api::MakeSource; using firebase::firestore::api::Source; using firebase::firestore::core::EventListener; @@ -212,6 +214,13 @@ - (void)getDocumentWithSource:(FIRFirestoreSource)source return [self addSnapshotListenerInternalWithOptions:options listener:listener]; } +- (id)addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener { + ListenOptions listenOptions = + ListenOptions::FromOptions(options.includeMetadataChanges, MakeListenSource(options.source)); + return [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; +} + - (id)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions listener:(FIRDocumentSnapshotBlock) listener { diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index 98518183049..d4185488341 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -37,6 +37,7 @@ #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/API/FSTUserDataReader.h" +#import "Firestore/Source/API/converters.h" #include "Firestore/core/src/api/query_core.h" #include "Firestore/core/src/api/query_listener_registration.h" @@ -69,6 +70,7 @@ using firebase::firestore::google_firestore_v1_Value; using firebase::firestore::google_firestore_v1_Value_fields; using firebase::firestore::api::Firestore; +using firebase::firestore::api::MakeListenSource; using firebase::firestore::api::Query; using firebase::firestore::api::QueryListenerRegistration; using firebase::firestore::api::QuerySnapshot; @@ -191,6 +193,13 @@ - (void)getDocumentsWithSource:(FIRFirestoreSource)publicSource return [self addSnapshotListenerInternalWithOptions:options listener:listener]; } +- (id)addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener { + ListenOptions listenOptions = + ListenOptions::FromOptions(options.includeMetadataChanges, MakeListenSource(options.source)); + return [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; +} + - (id)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions listener: (FIRQuerySnapshotBlock)listener { diff --git a/Firestore/Source/API/FIRSnapshotListenOptions.mm b/Firestore/Source/API/FIRSnapshotListenOptions.mm new file mode 100644 index 00000000000..9e22d686428 --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotListenOptions.mm @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#import "FIRSnapshotListenOptions.h" + +#import + +#include +#include + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRSnapshotListenOptions + +- (instancetype)initPrivate:(FIRListenSource)source + includeMetadataChanges:(BOOL)includeMetadataChanges { + self = [self init]; + if (self) { + _source = source; + _includeMetadataChanges = includeMetadataChanges; + } + return self; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _source = FIRListenSourceDefault; + _includeMetadataChanges = NO; + } + return self; +} + +- (FIRSnapshotListenOptions *)optionsWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { + FIRSnapshotListenOptions *newOptions = + [[FIRSnapshotListenOptions alloc] initPrivate:self.source + includeMetadataChanges:includeMetadataChanges]; + return newOptions; +} + +- (FIRSnapshotListenOptions *)optionsWithSource:(FIRListenSource)source { + FIRSnapshotListenOptions *newOptions = + [[FIRSnapshotListenOptions alloc] initPrivate:source + includeMetadataChanges:self.includeMetadataChanges]; + return newOptions; +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/API/converters.h b/Firestore/Source/API/converters.h index f5d0e4545b8..e777ba81a2c 100644 --- a/Firestore/Source/API/converters.h +++ b/Firestore/Source/API/converters.h @@ -24,6 +24,8 @@ #import #include +#import "FIRSnapshotListenOptions.h" +#import "Firestore/core/src/api/listen_source.h" @class FIRGeoPoint; @class FIRTimestamp; @@ -62,6 +64,8 @@ FIRTimestamp* MakeFIRTimestamp(const Timestamp& timestamp); FIRDocumentReference* MakeFIRDocumentReference(const model::DocumentKey& document_key, std::shared_ptr firestore); +ListenSource MakeListenSource(const FIRListenSource& source); + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/Source/API/converters.mm b/Firestore/Source/API/converters.mm index 251d3e578bf..8bfd5a07090 100644 --- a/Firestore/Source/API/converters.mm +++ b/Firestore/Source/API/converters.mm @@ -25,6 +25,7 @@ #include "Firestore/core/include/firebase/firestore/geo_point.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/api/firestore.h" +#import "Firestore/core/src/api/listen_source.h" #include "Firestore/core/src/model/document_key.h" NS_ASSUME_NONNULL_BEGIN @@ -61,6 +62,17 @@ Timestamp MakeTimestamp(NSDate* date) { return [[FIRDocumentReference alloc] initWithKey:key firestore:std::move(firestore)]; } +ListenSource MakeListenSource(const FIRListenSource& source) { + switch (source) { + case FIRListenSourceDefault: + return ListenSource::Default; + case FIRListenSourceCache: + return ListenSource::Cache; + default: + return ListenSource::Default; + } +} + } // namespace api } // namespace firestore } // namespace firebase diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h index 70dbd0d02ad..b6f87450cb3 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRDocumentReference.h @@ -18,6 +18,7 @@ #import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" +#import "FIRSnapshotListenOptions.h" @class FIRCollectionReference; @class FIRDocumentSnapshot; @@ -270,6 +271,22 @@ addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:)); // clang-format on +/** + * Attaches a listener for `DocumentSnapshot` events. + * + * @param options Sets snapshot listener options, including whether metadata-only changes should + * trigger snapshot events, the source to listen to, the executor to use to call the + * listener, or the activity to scope the listener to. + * @param listener The listener to attach. + * + * @return A `ListenerRegistration` that can be used to remove this listener. + */ +- (id) + addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(void (^)(FIRDocumentSnapshot *_Nullable snapshot, + NSError *_Nullable error))listener + NS_SWIFT_NAME(addSnapshotListener(options:listener:)); + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h index bde102c212a..c75952876a2 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRQuery.h @@ -18,6 +18,7 @@ #import "FIRFirestoreSource.h" #import "FIRListenerRegistration.h" +#import "FIRSnapshotListenOptions.h" @class FIRAggregateQuery; @class FIRAggregateField; @@ -104,6 +105,21 @@ NS_SWIFT_NAME(Query) NSError *_Nullable error))listener NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:)); +/** + * Attaches a listener for `QuerySnapshot` events. + * @param options Sets snapshot listener options, including whether metadata-only changes should + * trigger snapshot events, the source to listen to, the executor to use to call the + * listener, or the activity to scope the listener to. + * @param listener The listener to attach. + * + * @return A `ListenerRegistration` that can be used to remove this listener. + */ +- (id) + addSnapshotListenerWithOptions:(FIRSnapshotListenOptions *)options + listener:(void (^)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))listener + NS_SWIFT_NAME(addSnapshotListener(options:listener:)); + #pragma mark - Filtering Data /** * Creates and returns a new Query with the additional filter. diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h new file mode 100644 index 00000000000..13d9903abef --- /dev/null +++ b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h @@ -0,0 +1,83 @@ +/* + * 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. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The source the snapshot listener retrieves data from. + */ +typedef NS_ENUM(NSUInteger, FIRListenSource) { + /** + * The default behavior. The listener attempts to return initial snapshot from cache and retrieve + * up-to-date snapshots from the Firestore server. Snapshot events will be triggered on local + * mutations and server-side updates. + */ + FIRListenSourceDefault, + /** + * The listener retrieves data and listens to updates from the local Firestore cache without + * attempting to send the query to the server. If some documents gets updated as a result from + * other queries, they will be picked up by listeners using the cache. + * + * Note that the data might be stale if the cache hasn't synchronized with recent server-side + * changes. + */ + FIRListenSourceCache +} NS_SWIFT_NAME(ListenSource); + +/** + * Options to configure the behavior of `Firestore.addSnapshotListenerWithOptions()`. Instances + * of this class control settings like whether metadata-only changes trigger events and the + * preferred data source. + */ +NS_SWIFT_NAME(SnapshotListenOptions) +@interface FIRSnapshotListenOptions : NSObject + +/** The source the snapshot listener retrieves data from. */ +@property(nonatomic, readonly) FIRListenSource source; +/** Indicates whether metadata-only changes should trigger snapshot events. */ +@property(nonatomic, readonly) BOOL includeMetadataChanges; + +/** + * Creates and returns a new `SnapshotListenOptions` object with all properties initialized to their + * default values. + * + * @return The created `SnapshotListenOptions` object. + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Creates and returns a new `SnapshotListenOptions` object with with all properties of the current + * `SnapshotListenOptions` object plus the new property specifying whether metadata-only changes + * should trigger snapshot events + * + * @return The created `SnapshotListenOptions` object. + */ +- (FIRSnapshotListenOptions *)optionsWithIncludeMetadataChanges:(BOOL)includeMetadataChanges; + +/** + * Creates and returns a new `SnapshotListenOptions` object with with all properties of the current + * `SnapshotListenOptions` object plus the new property specifying the source that the snapshot + * listener listens to. + * + * @return The created `SnapshotListenOptions` object. + */ +- (FIRSnapshotListenOptions *)optionsWithSource:(FIRListenSource)source; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h b/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h index 6746aa489e8..9a0574a8207 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h +++ b/Firestore/Source/Public/FirebaseFirestore/FirebaseFirestore.h @@ -34,6 +34,7 @@ #import "FIRLocalCacheSettings.h" #import "FIRQuery.h" #import "FIRQuerySnapshot.h" +#import "FIRSnapshotListenOptions.h" #import "FIRSnapshotMetadata.h" #import "FIRTimestamp.h" #import "FIRTransaction.h" diff --git a/Firestore/Swift/Tests/BridgingHeader.h b/Firestore/Swift/Tests/BridgingHeader.h index 5bfed83adf4..58165c82944 100644 --- a/Firestore/Swift/Tests/BridgingHeader.h +++ b/Firestore/Swift/Tests/BridgingHeader.h @@ -18,6 +18,7 @@ #define FIRESTORE_SWIFT_TESTS_BRIDGINGHEADER_H_ #import "Firestore/Example/Tests/API/FSTAPIHelpers.h" +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTExceptionCatcher.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" diff --git a/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift b/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift new file mode 100644 index 00000000000..22c69679224 --- /dev/null +++ b/Firestore/Swift/Tests/Integration/SnapshotListenerSourceTests.swift @@ -0,0 +1,684 @@ +/* + * 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. + */ + +import FirebaseFirestore +import FirebaseFirestoreSwift +import Foundation + +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) +class SnapshotListenerSourceTests: FSTIntegrationTestCase { + func assertQuerySnapshotDataEquals(_ snapshot: Any, + _ expectedData: [[String: Any]]) throws { + let extractedData = FIRQuerySnapshotGetData(snapshot as! QuerySnapshot) + guard extractedData.count == expectedData.count else { + XCTFail( + "Result count mismatch: Expected \(expectedData.count), got \(extractedData.count)" + ) + return + } + for index in 0 ..< extractedData.count { + XCTAssertTrue(areDictionariesEqual(extractedData[index], expectedData[index])) + } + } + + // TODO(swift testing): update the function to be able to check other value types as well. + func areDictionariesEqual(_ dict1: [String: Any], _ dict2: [String: Any]) -> Bool { + guard dict1.count == dict2.count + else { return false } // Check if the number of elements matches + + for (key, value1) in dict1 { + guard let value2 = dict2[key] else { return false } + + // Value Checks (Assuming consistent types after the type check) + if let str1 = value1 as? String, let str2 = value2 as? String { + if str1 != str2 { return false } + } else if let int1 = value1 as? Int, let int2 = value2 as? Int { + if int1 != int2 { return false } + } else { + // Handle other potential types or return false for mismatch + return false + } + } + return true + } + + func testCanRaiseSnapshotFromCacheForQuery() throws { + let collRef = collectionRef(withDocuments: ["a": ["k": "a"]]) + readDocumentSet(forRef: collRef) // populate the cache. + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a"]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testCanRaiseSnapshotFromCacheForDocumentReference() throws { + let docRef = documentRef() + docRef.setData(["k": "a"]) + readDocument(forRef: docRef) // populate the cache. + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = docRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let docSnap = eventAccumulator.awaitEvent(withName: "snapshot") as! DocumentSnapshot + XCTAssertEqual(docSnap.data() as! [String: String], ["k": "a"]) + XCTAssertEqual(docSnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testListenToCacheShouldNotBeAffectedByOnlineStatusChange() throws { + let collRef = collectionRef(withDocuments: ["a": ["k": "a"]]) + readDocumentSet(forRef: collRef) // populate the cache. + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + .withIncludeMetadataChanges(true) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a"]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + disableNetwork() + enableNetwork() + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testMultipleListenersSourcedFromCacheCanWorkIndependently() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + + let query = collRef.whereField("sort", isGreaterThan: 0).order(by: "sort") + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration1 = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + let registration2 = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + var expected = [["k": "b", "sort": 1]] + var querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Do a local mutation + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Detach one listener, and do a local mutation. The other listener + // should not be affected. + registration1.remove() + addDocumentRef(collRef, data: ["k": "d", "sort": 3]) + + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2], ["k": "d", "sort": 3]] + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration2.remove() + } + + // Two queries that mapped to the same target ID are referred to as + // "mirror queries". An example for a mirror query is a limitToLast() + // query and a limit() query that share the same backend Target ID. + // Since limitToLast() queries are sent to the backend with a modified + // orderBy() clause, they can map to the same target representation as + // limit() query, even if both queries appear separate to the user. + func testListenUnlistenRelistenToMirrorQueriesFromCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + "c": ["k": "c", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + let options = SnapshotListenOptions().withSource(ListenSource.cache) + + // Setup a `limit` query. + let limit = collRef.order(by: "sort", descending: false).limit(to: 2) + let limitAccumulator = FSTEventAccumulator.init(forTest: self) + var limitRegistration = limit.addSnapshotListener( + options: options, + listener: limitAccumulator.valueEventHandler + ) + // Setup a mirroring `limitToLast` query. + let limitToLast = collRef.order(by: "sort", descending: true).limit(toLast: 2) + let limitToLastAccumulator = FSTEventAccumulator + .init(forTest: self) + var limitToLastRegistration = limitToLast.addSnapshotListener( + options: options, + listener: limitToLastAccumulator.valueEventHandler + ) + + // Verify both queries get expected result. + var querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": 0], ["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + querySnap = limitToLastAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1], ["k": "a", "sort": 0]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Un-listen then re-listen to the limit query. + limitRegistration.remove() + limitRegistration = limit.addSnapshotListener( + options: options, + listener: limitAccumulator.valueEventHandler + ) + querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, + [["k": "a", "sort": 0], ["k": "b", "sort": 1]] + ) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Add a document that would change the result set. + addDocumentRef(collRef, data: ["k": "d", "sort": -1]) + + // Verify both queries get expected result. + querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "d", "sort": -1], ["k": "a", "sort": 0]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + querySnap = limitToLastAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": 0], ["k": "d", "sort": -1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + + // Un-listen to limitToLast, update a doc, then re-listen to limitToLast + limitToLastRegistration.remove() + updateDocumentRef(collRef.document("a"), data: ["k": "a", "sort": -2]) + limitToLastRegistration = limitToLast.addSnapshotListener( + options: options, + listener: limitToLastAccumulator.valueEventHandler + ) + + // Verify both queries get expected result. + querySnap = limitAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": -2], ["k": "d", "sort": -1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + querySnap = limitToLastAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "d", "sort": -1], ["k": "a", "sort": -2]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + // We listened to LimitToLast query after the doc update. + XCTAssertEqual(querySnap.metadata.hasPendingWrites, false) + } + + func testCanListenToDefaultSourceFirstAndThenCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isGreaterThanOrEqualTo: 1).order(by: "sort") + + // Listen to the query with default options, which will also populates the cache + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + + var querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Listen to the same query from cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + // The metadata is sync with server due to the default listener + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testCanListenToCacheSourceFirstAndThenDefault() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + var querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + // Cache is empty + try assertQuerySnapshotDataEquals(querySnap, []) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Listen to the same query from server + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Default listener updates the cache, whish triggers cache listener to raise snapshot. + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + // The metadata is sync with server due to the default listener + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testWillNotGetMetadataOnlyUpdatesIfListeningToCacheOnly() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + let options = SnapshotListenOptions().withSource(ListenSource.cache) + + let registration = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + var querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Do a local mutation + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "b", "sort": 1], ["k": "c", "sort": 2]]) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + XCTAssertEqual(querySnap.metadata?.hasPendingWrites, true) + + // As we are not listening to server, the listener will not get notified + // when local mutation is acknowledged by server. + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testWillHaveSynceMetadataUpdatesWhenListeningToBothCacheAndDefaultSource() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the cache + let cacheAccumulator = FSTEventAccumulator.init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + .withIncludeMetadataChanges(true) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + var querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + var expected = [["k": "b", "sort": 1]] + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + // Listen to the same query from server + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener( + includeMetadataChanges: true, + listener: defaultAccumulator.valueEventHandler + ) + + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + // First snapshot will be raised from cache. + XCTAssertEqual(querySnap.metadata.isFromCache, true) + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + // Second snapshot will be raised from server result + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // As listening to metadata changes, the cache listener also gets triggered and synced + // with default listener. + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + // The metadata is sync with server due to the default listener + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Do a local mutation + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + // snapshot gets triggered by local mutation + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.hasPendingWrites, true) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Local mutation gets acknowledged by the server + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + XCTAssertEqual(querySnap.metadata.hasPendingWrites, false) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + XCTAssertEqual(querySnap.metadata.hasPendingWrites, false) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testCanUnlistenToDefaultSourceWhileStillListeningToCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the query with both source options + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + defaultAccumulator.awaitEvent(withName: "snapshot") + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + cacheAccumulator.awaitEvent(withName: "snapshot") + + // Un-listen to the default listener. + defaultRegistration.remove() + + // Add a document and verify listener to cache works as expected + addDocumentRef(collRef, data: ["k": "c", "sort": -1]) + defaultAccumulator.assertNoAdditionalEvents() + + let querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, + [["k": "c", "sort": -1], ["k": "b", "sort": 1]] + ) + + cacheAccumulator.assertNoAdditionalEvents() + cacheRegistration.remove() + } + + func testCanUnlistenToCacheSourceWhileStillListeningToServer() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isNotEqualTo: 0).order(by: "sort") + + // Listen to the query with both source options + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + defaultAccumulator.awaitEvent(withName: "snapshot") + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + cacheAccumulator.awaitEvent(withName: "snapshot") + + // Un-listen to cache. + cacheRegistration.remove() + + // Add a document and verify listener to server works as expected. + addDocumentRef(collRef, data: ["k": "c", "sort": -1]) + cacheAccumulator.assertNoAdditionalEvents() + + let querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, + [["k": "c", "sort": -1], ["k": "b", "sort": 1]] + ) + + defaultAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + } + + func testCanListenUnlistenRelistenToSameQueryWithDifferentSourceOptions() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isGreaterThan: 0).order(by: "sort") + + // Listen to the query with default options, which will also populates the cache + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + var defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + var querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + var expected = [["k": "b", "sort": 1]] + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Listen to the same query from cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + var cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Un-listen to the default listener, add a doc and re-listen. + defaultRegistration.remove() + addDocumentRef(collRef, data: ["k": "c", "sort": 2]) + + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Un-listen to cache, update a doc, then re-listen to cache. + cacheRegistration.remove() + updateDocumentRef(collRef.document("b"), data: ["k": "b", "sort": 3]) + + expected = [["k": "c", "sort": 2], ["k": "b", "sort": 3]] + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, expected + ) + + cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals( + querySnap, expected + ) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } + + func testCanListenToCompositeIndexQueriesFromCache() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + readDocumentSet(forRef: collRef) // populate the cache. + + let query = collRef.whereField("k", isLessThanOrEqualTo: "a") + .whereField("sort", isGreaterThanOrEqualTo: 0) + + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = query.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + let querySnap = eventAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, [["k": "a", "sort": 0]]) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testCanRaiseInitialSnapshotFromCachedEmptyResults() throws { + let collRef = collectionRef() + + // Populate the cache with empty query result. + var querySnap = readDocumentSet(forRef: collRef) + try assertQuerySnapshotDataEquals(querySnap, []) + + // Add a snapshot listener whose first event should be raised from cache. + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + + querySnap = eventAccumulator.awaitEvent(withName: "initial event") as! QuerySnapshot + try assertQuerySnapshotDataEquals(querySnap, []) + XCTAssertEqual(querySnap.metadata.isFromCache, true) + + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testWillNotBeTriggeredByTransactionsWhileListeningToCache() throws { + let collRef = collectionRef() + + // Add a snapshot listener whose first event should be raised from cache. + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let registration = collRef.addSnapshotListener( + options: options, + listener: eventAccumulator.valueEventHandler + ) + let querySnap = eventAccumulator.awaitEvent(withName: "initial event") + try assertQuerySnapshotDataEquals(querySnap, []) + + let docRef = documentRef() + // Use a transaction to perform a write without triggering any local events. + runTransaction(docRef.firestore, block: { transaction, errorPointer -> Any? in + transaction.updateData(["K": "a"], forDocument: docRef) + return nil + }) + + // There should be no events raised + eventAccumulator.assertNoAdditionalEvents() + registration.remove() + } + + func testSharesServerSideUpdatesWhenListeningToBothCacheAndDefault() throws { + let collRef = collectionRef(withDocuments: [ + "a": ["k": "a", "sort": 0], + "b": ["k": "b", "sort": 1], + ]) + let query = collRef.whereField("sort", isGreaterThan: 0).order(by: "sort") + + // Listen to the query with default options, which will also populates the cache + let defaultAccumulator = FSTEventAccumulator.init(forTest: self) + let defaultRegistration = query.addSnapshotListener(defaultAccumulator.valueEventHandler) + var querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + var expected = [["k": "b", "sort": 1]] + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Listen to the same query from cache + let cacheAccumulator = FSTEventAccumulator + .init(forTest: self) + let options = SnapshotListenOptions().withSource(ListenSource.cache) + let cacheRegistration = query.addSnapshotListener( + options: options, + listener: cacheAccumulator.valueEventHandler + ) + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + + // Use a transaction to mock server side updates + let docRef = collRef.document() + runTransaction(docRef.firestore, block: { transaction, errorPointer -> Any? in + transaction.setData(["k": "c", "sort": 2], forDocument: docRef) + return nil + }) + + // Default listener receives the server update + querySnap = defaultAccumulator.awaitEvent(withName: "snapshot") + expected = [["k": "b", "sort": 1], ["k": "c", "sort": 2]] + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + // Cache listener raises snapshot as well + querySnap = cacheAccumulator.awaitEvent(withName: "snapshot") + try assertQuerySnapshotDataEquals(querySnap, expected) + XCTAssertEqual(querySnap.metadata.isFromCache, false) + + defaultAccumulator.assertNoAdditionalEvents() + cacheAccumulator.assertNoAdditionalEvents() + defaultRegistration.remove() + cacheRegistration.remove() + } +} diff --git a/Firestore/core/src/api/listen_source.h b/Firestore/core/src/api/listen_source.h new file mode 100644 index 00000000000..2053a92df39 --- /dev/null +++ b/Firestore/core/src/api/listen_source.h @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_API_LISTEN_SOURCE_H_ +#define FIRESTORE_CORE_SRC_API_LISTEN_SOURCE_H_ + +namespace firebase { +namespace firestore { +namespace api { + +/** + * An enum that configures the snapshot listener data source. Using this enum, + * specify whether snapshot events are triggered by local cache changes + * only, or from both local cache and watch changes(which is the default). + * + * See `FIRFirestoreListenSource` for more details. + */ +enum class ListenSource { Default, Cache }; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_API_LISTEN_SOURCE_H_ diff --git a/Firestore/core/src/core/event_manager.cc b/Firestore/core/src/core/event_manager.cc index 283a8341076..d5c3f3542b9 100644 --- a/Firestore/core/src/core/event_manager.cc +++ b/Firestore/core/src/core/event_manager.cc @@ -37,11 +37,27 @@ EventManager::EventManager(QueryEventSource* query_event_source) model::TargetId EventManager::AddQueryListener( std::shared_ptr listener) { const Query& query = listener->query(); + ListenerSetupAction listener_action = + ListenerSetupAction::NoSetupActionRequired; auto inserted = queries_.emplace(query, QueryListenersInfo{}); + // If successfully inserted, it means we haven't listened to this query + // before. bool first_listen = inserted.second; QueryListenersInfo& query_info = inserted.first->second; + if (first_listen) { + listener_action = listener->listens_to_remote_store() + ? ListenerSetupAction:: + InitializeLocalListenAndRequireWatchConnection + : ListenerSetupAction::InitializeLocalListenOnly; + } else if (!query_info.has_remote_listeners() && + listener->listens_to_remote_store()) { + // Query has been listening to local cache, and tries to add a new listener + // sourced from watch. + listener_action = ListenerSetupAction::RequireWatchConnectionOnly; + } + query_info.listeners.push_back(listener); bool raised_event = listener->OnOnlineStateChanged(online_state_); @@ -56,8 +72,20 @@ model::TargetId EventManager::AddQueryListener( } } - if (first_listen) { - query_info.target_id = query_event_source_->Listen(query); + switch (listener_action) { + case ListenerSetupAction::InitializeLocalListenAndRequireWatchConnection: + query_info.target_id = query_event_source_->Listen( + query, /** should_listen_to_remote= */ true); + break; + case ListenerSetupAction::InitializeLocalListenOnly: + query_info.target_id = query_event_source_->Listen( + query, /** should_listen_to_remote= */ false); + break; + case ListenerSetupAction::RequireWatchConnectionOnly: + query_event_source_->ListenToRemoteStore(query); + break; + default: + break; } return query_info.target_id; } @@ -65,18 +93,41 @@ model::TargetId EventManager::AddQueryListener( void EventManager::RemoveQueryListener( std::shared_ptr listener) { const Query& query = listener->query(); - bool last_listen = false; + ListenerRemovalAction listener_action = + ListenerRemovalAction::NoRemovalActionRequired; auto found_iter = queries_.find(query); if (found_iter != queries_.end()) { QueryListenersInfo& query_info = found_iter->second; query_info.Erase(listener); - last_listen = query_info.listeners.empty(); + + if (query_info.listeners.empty()) { + listener_action = + listener->listens_to_remote_store() + ? ListenerRemovalAction:: + TerminateLocalListenAndRequireWatchDisconnection + : ListenerRemovalAction::TerminateLocalListenOnly; + } else if (!query_info.has_remote_listeners() && + listener->listens_to_remote_store()) { + // The removed listener is the last one that sourced from watch. + listener_action = ListenerRemovalAction::RequireWatchDisconnectionOnly; + } } - if (last_listen) { - queries_.erase(found_iter); - query_event_source_->StopListening(query); + switch (listener_action) { + case ListenerRemovalAction:: + TerminateLocalListenAndRequireWatchDisconnection: + queries_.erase(found_iter); + return query_event_source_->StopListening( + query, /** should_stop_remote_listening= */ true); + case ListenerRemovalAction::TerminateLocalListenOnly: + queries_.erase(found_iter); + return query_event_source_->StopListening( + query, /** should_stop_remote_listening= */ false); + case ListenerRemovalAction::RequireWatchDisconnectionOnly: + return query_event_source_->StopListeningToRemoteStoreOnly(query); + default: + return; } } diff --git a/Firestore/core/src/core/event_manager.h b/Firestore/core/src/core/event_manager.h index ba4f2ee56e9..9ee783a85bd 100644 --- a/Firestore/core/src/core/event_manager.h +++ b/Firestore/core/src/core/event_manager.h @@ -23,6 +23,7 @@ #include #include "Firestore/core/src/core/query.h" +#include "Firestore/core/src/core/query_listener.h" #include "Firestore/core/src/core/sync_engine_callback.h" #include "Firestore/core/src/core/view_snapshot.h" #include "Firestore/core/src/model/model_fwd.h" @@ -35,7 +36,6 @@ namespace firestore { namespace core { class QueryEventSource; -class QueryListener; /** * EventManager is responsible for mapping queries to query event listeners. @@ -97,12 +97,35 @@ class EventManager : public SyncEngineCallback { snapshot_ = snapshot; } + bool has_remote_listeners() { + for (const auto& listener : listeners) { + if (listener->listens_to_remote_store()) { + return true; + } + } + return false; + } + private: // Other members are public in this struct, ensure that any reads are // copies by requiring reads to go through a const getter. absl::optional snapshot_; }; + enum ListenerSetupAction { + InitializeLocalListenAndRequireWatchConnection = 0, + InitializeLocalListenOnly = 1, + RequireWatchConnectionOnly = 2, + NoSetupActionRequired = 3 + }; + + enum ListenerRemovalAction { + TerminateLocalListenAndRequireWatchDisconnection = 0, + TerminateLocalListenOnly = 1, + RequireWatchDisconnectionOnly = 2, + NoRemovalActionRequired = 3 + }; + QueryEventSource* query_event_source_ = nullptr; model::OnlineState online_state_ = model::OnlineState::Unknown; std::unordered_map queries_; diff --git a/Firestore/core/src/core/listen_options.h b/Firestore/core/src/core/listen_options.h index cf76d872878..2499b75e224 100644 --- a/Firestore/core/src/core/listen_options.h +++ b/Firestore/core/src/core/listen_options.h @@ -17,10 +17,14 @@ #ifndef FIRESTORE_CORE_SRC_CORE_LISTEN_OPTIONS_H_ #define FIRESTORE_CORE_SRC_CORE_LISTEN_OPTIONS_H_ +#include +#include "Firestore/core/src/api/listen_source.h" namespace firebase { namespace firestore { namespace core { +using api::ListenSource; + class ListenOptions { public: ListenOptions() = default; @@ -44,14 +48,36 @@ class ListenOptions { } /** - * Creates a default ListenOptions, with metadata changes and - * wait_for_sync_when_online disabled. + * Creates a new ListenOptions. + * + * @param include_query_metadata_changes Raise events when only metadata of + * the query changes. + * @param include_document_metadata_changes Raise events when only metadata of + * documents changes. + * @param wait_for_sync_when_online Wait for a sync with the server when + * online, but still raise events while offline. + * @param source sets the source a snapshot listener listens to. + */ + ListenOptions(bool include_query_metadata_changes, + bool include_document_metadata_changes, + bool wait_for_sync_when_online, + ListenSource source) + : include_query_metadata_changes_(include_query_metadata_changes), + include_document_metadata_changes_(include_document_metadata_changes), + wait_for_sync_when_online_(wait_for_sync_when_online), + source_(std::move(source)) { + } + + /** + * Creates a default ListenOptions, with metadata changes, + * wait_for_sync_when_online disabled, and listen source set to default. */ static ListenOptions DefaultOptions() { return ListenOptions( /*include_query_metadata_changes=*/false, /*include_document_metadata_changes=*/false, - /*wait_for_sync_when_online=*/false); + /*wait_for_sync_when_online=*/false, + /*source=*/ListenSource::Default); } /** @@ -63,7 +89,19 @@ class ListenOptions { return ListenOptions( /*include_query_metadata_changes=*/include_metadata_changes, /*include_document_metadata_changes=*/include_metadata_changes, - /*wait_for_sync_when_online=*/false); + /*wait_for_sync_when_online=*/false, + /*source=*/ListenSource::Default); + } + + /** + * Creates a ListenOptions which sets the source snapshot listener listens to. + */ + static ListenOptions FromOptions(bool include_metadata_changes, + ListenSource source) { + return ListenOptions( + /*include_query_metadata_changes=*/include_metadata_changes, + /*include_document_metadata_changes=*/include_metadata_changes, + /*wait_for_sync_when_online=*/false, std::move(source)); } bool include_query_metadata_changes() const { @@ -78,10 +116,15 @@ class ListenOptions { return wait_for_sync_when_online_; } + ListenSource source() const { + return source_; + } + private: bool include_query_metadata_changes_ = false; bool include_document_metadata_changes_ = false; bool wait_for_sync_when_online_ = false; + ListenSource source_ = ListenSource::Default; }; } // namespace core diff --git a/Firestore/core/src/core/query_listener.cc b/Firestore/core/src/core/query_listener.cc index 3e0cbb2cceb..c0ec38e752a 100644 --- a/Firestore/core/src/core/query_listener.cc +++ b/Firestore/core/src/core/query_listener.cc @@ -136,6 +136,11 @@ bool QueryListener::ShouldRaiseInitialEvent(const ViewSnapshot& snapshot, return true; } + // Always raise first event if listening to cache + if (!listens_to_remote_store()) { + return true; + } + // NOTE: We consider OnlineState::Unknown as online (it should become Offline // or Online if we wait long enough). bool maybe_online = online_state != OnlineState::Offline; diff --git a/Firestore/core/src/core/query_listener.h b/Firestore/core/src/core/query_listener.h index eec5ada73b8..6b934a0de59 100644 --- a/Firestore/core/src/core/query_listener.h +++ b/Firestore/core/src/core/query_listener.h @@ -63,6 +63,10 @@ class QueryListener { return query_; } + bool listens_to_remote_store() const { + return options_.source() != ListenSource::Cache; + } + /** The last received view snapshot. */ const absl::optional& snapshot() const { return snapshot_; diff --git a/Firestore/core/src/core/sync_engine.cc b/Firestore/core/src/core/sync_engine.cc index 24a4c3f7803..77223cb1fed 100644 --- a/Firestore/core/src/core/sync_engine.cc +++ b/Firestore/core/src/core/sync_engine.cc @@ -104,7 +104,7 @@ void SyncEngine::AssertCallbackExists(absl::string_view source) { "Tried to call '%s' before callback was registered.", source); } -TargetId SyncEngine::Listen(Query query) { +TargetId SyncEngine::Listen(Query query, bool should_listen_to_remote) { AssertCallbackExists("Listen"); HARD_ASSERT(query_views_by_query_.find(query) == query_views_by_query_.end(), @@ -121,7 +121,9 @@ TargetId SyncEngine::Listen(Query query) { snapshots.push_back(std::move(view_snapshot)); sync_engine_callback_->OnViewSnapshots(std::move(snapshots)); - remote_store_->Listen(std::move(target_data)); + if (should_listen_to_remote) { + remote_store_->Listen(std::move(target_data)); + } return target_id; } @@ -161,22 +163,48 @@ ViewSnapshot SyncEngine::InitializeViewAndComputeSnapshot( return view_change.snapshot().value(); } -void SyncEngine::StopListening(const Query& query) { +void SyncEngine::ListenToRemoteStore(Query query) { + AssertCallbackExists("ListenToRemoteStore"); + TargetData target_data = local_store_->AllocateTarget(query.ToTarget()); + remote_store_->Listen(std::move(target_data)); +} + +void SyncEngine::StopListening(const Query& query, + bool should_stop_remote_listening) { AssertCallbackExists("StopListening"); + StopListeningAndReleaseTarget(query, /** last_listen= */ true, + should_stop_remote_listening); +} + +void SyncEngine::StopListeningToRemoteStoreOnly(const Query& query) { + AssertCallbackExists("StopListeningToRemoteStoreOnly"); + StopListeningAndReleaseTarget(query, /** last_listen= */ false, + /** should_stop_remote_listening= */ true); +} +void SyncEngine::StopListeningAndReleaseTarget( + const Query& query, bool last_listen, bool should_stop_remote_listening) { auto query_view = query_views_by_query_[query]; HARD_ASSERT(query_view, "Trying to stop listening to a query not found"); - query_views_by_query_.erase(query); + if (last_listen) { + query_views_by_query_.erase(query); + } + // One target could have multiple queries mapped to it. TargetId target_id = query_view->target_id(); auto& queries = queries_by_target_[target_id]; queries.erase(std::remove(queries.begin(), queries.end(), query), queries.end()); - if (queries.empty()) { - local_store_->ReleaseTarget(target_id); + if (!queries.empty()) return; + + if (should_stop_remote_listening) { remote_store_->StopListening(target_id); + } + + if (last_listen) { + local_store_->ReleaseTarget(target_id); RemoveAndCleanupTarget(target_id, Status::OK()); } } diff --git a/Firestore/core/src/core/sync_engine.h b/Firestore/core/src/core/sync_engine.h index e8bfc028d02..bcf930fdd0c 100644 --- a/Firestore/core/src/core/sync_engine.h +++ b/Firestore/core/src/core/sync_engine.h @@ -70,16 +70,33 @@ class QueryEventSource { /** * Initiates a new listen. The LocalStore will be queried for initial data - * and the listen will be sent to the `RemoteStore` to get remote data. The - * registered SyncEngineCallback will be notified of resulting view + * and the listen will be sent to the RemoteStore if the query is listening to + * watch. The registered SyncEngineCallback will be notified of resulting view * snapshots and/or listen errors. * * @return the target ID assigned to the query. */ - virtual model::TargetId Listen(Query query) = 0; + virtual model::TargetId Listen(Query query, bool should_listen_to_remote) = 0; - /** Stops listening to a query previously listened to via `Listen`. */ - virtual void StopListening(const Query& query) = 0; + /** + * Sends the listen to the RemoteStore to get remote data. Invoked when a + * Query starts listening to the remote store, while already listening to the + * cache. + */ + virtual void ListenToRemoteStore(Query query) = 0; + + /** + * Stops listening to a query previously listened to via `Listen`. Un-listen + * to remote store if there is a watch connection established and stayed open. + */ + virtual void StopListening(const Query& query, + bool should_stop_remote_listening) = 0; + + /** + * Stops listening to a query from watch. Invoked when a Query stops listening + * to the remote store, while still listening to the cache. + */ + virtual void StopListeningToRemoteStoreOnly(const Query& query) = 0; }; /** @@ -107,8 +124,12 @@ class SyncEngine : public remote::RemoteStoreCallback, public QueryEventSource { void SetCallback(SyncEngineCallback* callback) override { sync_engine_callback_ = callback; } - model::TargetId Listen(Query query) override; - void StopListening(const Query& query) override; + model::TargetId Listen(Query query, + bool should_listen_to_remote = true) override; + void ListenToRemoteStore(Query query) override; + void StopListening(const Query& query, + bool should_stop_remote_listening = true) override; + void StopListeningToRemoteStoreOnly(const Query& query) override; /** * Initiates the write of local mutation batch which involves adding the @@ -244,6 +265,9 @@ class SyncEngine : public remote::RemoteStoreCallback, public QueryEventSource { nanopb::ByteString resume_token); void RemoveAndCleanupTarget(model::TargetId target_id, util::Status status); + void StopListeningAndReleaseTarget(const Query& query, + bool should_stop_remote_listening, + bool last_listen); void RemoveLimboTarget(const model::DocumentKey& key); diff --git a/Firestore/core/test/unit/core/event_manager_test.cc b/Firestore/core/test/unit/core/event_manager_test.cc index 9229dae919e..5d48a9aae0c 100644 --- a/Firestore/core/test/unit/core/event_manager_test.cc +++ b/Firestore/core/test/unit/core/event_manager_test.cc @@ -57,11 +57,21 @@ std::shared_ptr NoopQueryListener(core::Query query) { NoopViewSnapshotHandler()); } +std::shared_ptr NoopQueryCacheListener(core::Query query) { + return QueryListener::Create( + std::move(query), + ListenOptions::FromOptions(/** include_metadata_changes= */ false, + ListenSource::Cache), + NoopViewSnapshotHandler()); +} + class MockEventSource : public core::QueryEventSource { public: MOCK_METHOD1(SetCallback, void(core::SyncEngineCallback*)); - MOCK_METHOD1(Listen, model::TargetId(core::Query)); - MOCK_METHOD1(StopListening, void(const core::Query&)); + MOCK_METHOD2(Listen, model::TargetId(core::Query, bool)); + MOCK_METHOD1(ListenToRemoteStore, void(core::Query)); + MOCK_METHOD2(StopListening, void(const core::Query&, bool)); + MOCK_METHOD1(StopListeningToRemoteStoreOnly, void(const core::Query&)); }; TEST(EventManagerTest, HandlesManyListnersPerQuery) { @@ -73,14 +83,34 @@ TEST(EventManagerTest, HandlesManyListnersPerQuery) { EXPECT_CALL(mock_event_source, SetCallback(_)); EventManager event_manager(&mock_event_source); - EXPECT_CALL(mock_event_source, Listen(query)); + EXPECT_CALL(mock_event_source, Listen(query, true)); + event_manager.AddQueryListener(listener1); + + // Expecting no activity from mock_event_source. + event_manager.AddQueryListener(listener2); + event_manager.RemoveQueryListener(listener2); + + EXPECT_CALL(mock_event_source, StopListening(query, true)); + event_manager.RemoveQueryListener(listener1); +} + +TEST(EventManagerTest, HandlesManyCacheListnersPerQuery) { + core::Query query = Query("foo/bar"); + auto listener1 = NoopQueryCacheListener(query); + auto listener2 = NoopQueryCacheListener(query); + + StrictMock mock_event_source; + EXPECT_CALL(mock_event_source, SetCallback(_)); + EventManager event_manager(&mock_event_source); + + EXPECT_CALL(mock_event_source, Listen(query, false)); event_manager.AddQueryListener(listener1); // Expecting no activity from mock_event_source. event_manager.AddQueryListener(listener2); event_manager.RemoveQueryListener(listener2); - EXPECT_CALL(mock_event_source, StopListening(query)); + EXPECT_CALL(mock_event_source, StopListening(query, false)); event_manager.RemoveQueryListener(listener1); } @@ -91,7 +121,7 @@ TEST(EventManagerTest, HandlesUnlistenOnUnknownListenerGracefully) { MockEventSource mock_event_source; EventManager event_manager(&mock_event_source); - EXPECT_CALL(mock_event_source, StopListening(_)).Times(0); + EXPECT_CALL(mock_event_source, StopListening(_, true)).Times(0); event_manager.RemoveQueryListener(listener); } @@ -128,10 +158,10 @@ TEST(EventManagerTest, NotifiesListenersInTheRightOrder) { MockEventSource mock_event_source; EventManager event_manager(&mock_event_source); - EXPECT_CALL(mock_event_source, Listen(query1)); + EXPECT_CALL(mock_event_source, Listen(query1, true)); event_manager.AddQueryListener(listener1); - EXPECT_CALL(mock_event_source, Listen(query2)); + EXPECT_CALL(mock_event_source, Listen(query2, true)); event_manager.AddQueryListener(listener2); event_manager.AddQueryListener(listener3); From 90c33e404f515834fe4c753cc940fe22deed77b5 Mon Sep 17 00:00:00 2001 From: themiswang Date: Mon, 11 Mar 2024 16:13:04 -0400 Subject: [PATCH 081/104] [Rollouts] Feature rollouts merge to main (#12410) --- CoreOnly/Tests/FirebasePodTest/Podfile | 1 + Crashlytics/CHANGELOG.md | 1 + .../Components/FIRCLSUserLogging.h | 4 +- .../Components/FIRCLSUserLogging.m | 16 +- .../FIRCLSRolloutsPersistenceManager.h | 30 + .../FIRCLSRolloutsPersistenceManager.m | 67 ++ Crashlytics/Crashlytics/FIRCrashlytics.m | 51 +- .../Crashlytics/Handlers/FIRCLSException.h | 5 +- .../Crashlytics/Handlers/FIRCLSException.mm | 41 +- .../Crashlytics/Models/FIRCLSInternalReport.h | 1 + .../Crashlytics/Models/FIRCLSInternalReport.m | 1 + .../CrashlyticsRemoteConfigManager.swift | 141 +++ .../Rollouts/EncodedRolloutAssignment.swift | 44 + .../Rollouts/StringToHexConverter.swift | 38 + Crashlytics/UnitTests/FIRCLSFileTests.m | 31 + Crashlytics/UnitTests/FIRCLSLoggingTests.m | 10 +- .../FIRCLSRolloutsPersistenceManagerTests.m | 70 ++ .../UnitTests/FIRRecordExceptionModelTests.m | 2 +- .../CrashlyticsRemoteConfigManagerTests.swift | 136 +++ Example/watchOSSample/Podfile | 1 + FirebaseCrashlytics.podspec | 6 +- FirebaseRemoteConfig.podspec | 4 +- FirebaseRemoteConfig/CHANGELOG.md | 3 + .../Interop/RemoteConfigConstants.swift | 21 + .../Interop/RemoteConfigInterop.swift | 21 + .../Interop/RolloutAssignment.swift | 47 + .../Interop/RolloutsStateSubscriber.swift | 20 + .../Sources/FIRRemoteConfig.m | 106 +- .../Sources/FIRRemoteConfigComponent.h | 8 +- .../Sources/FIRRemoteConfigComponent.m | 49 +- .../Sources/Private/FIRRemoteConfig_Private.h | 4 + .../Sources/Private/RCNConfigSettings.h | 9 +- .../Sources/RCNConfigConstants.h | 12 +- .../Sources/RCNConfigContent.h | 5 + .../Sources/RCNConfigContent.m | 112 +- .../Sources/RCNConfigDBManager.h | 14 +- .../Sources/RCNConfigDBManager.m | 122 +- .../Sources/RCNConfigDefines.h | 2 + FirebaseRemoteConfig/Sources/RCNConfigFetch.m | 5 +- .../Sources/RCNConfigSettings.m | 11 +- FirebaseRemoteConfig/Sources/RCNConstants3P.m | 1 + .../Sources/RCNUserDefaultsManager.h | 4 +- .../Sources/RCNUserDefaultsManager.m | 19 +- .../project.pbxproj | 1059 +++++++++++++++++ .../FeatureRolloutsTestApp/ContentView.swift | 29 + .../FeatureRolloutsTestApp.entitlements | 10 + .../ContentView.swift | 27 + .../ContentView.swift | 26 + .../ContentView.swift | 25 + .../Tests/FeatureRolloutsTestApp/Podfile | 52 + .../Shared/CrashButtonView.swift | 62 + .../Shared/FeatureRolloutsTestAppApp.swift | 31 + .../Shared/RemoteConfigButtonView.swift | 51 + FirebaseRemoteConfig/Tests/Sample/Podfile | 1 + .../RemoteConfigSampleApp/ViewController.m | 9 +- .../SwiftUnit/RemoteConfigInteropTests.swift | 65 + .../Tests/Unit/FIRRemoteConfigComponentTest.m | 43 +- .../Tests/Unit/RCNConfigContentTest.m | 209 +++- .../Tests/Unit/RCNConfigDBManagerTest.m | 165 ++- .../Tests/Unit/RCNConfigTest.m | 33 +- .../Tests/Unit/RCNInstanceIDTest.m | 4 +- .../Tests/Unit/RCNRemoteConfigTest.m | 65 +- .../Tests/Unit/RCNThrottlingTests.m | 23 +- .../Tests/Unit/RCNUserDefaultsManagerTests.m | 27 +- .../generate_featureRolloutsTestApp.sh | 58 + FirebaseRemoteConfigInterop.podspec | 34 + ...ebaseRemoteConfigSwift_APIBuildTests.swift | 1 + FirebaseSessions/Tests/TestApp/Podfile | 1 + IntegrationTesting/ClientApp/Podfile | 1 + .../Podfile | 1 + Package.swift | 48 +- .../FirebaseManifest/FirebaseManifest.swift | 1 + scripts/localize_podfile.swift | 1 + 73 files changed, 3281 insertions(+), 177 deletions(-) create mode 100644 Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h create mode 100644 Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m create mode 100644 Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift create mode 100644 Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift create mode 100644 Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift create mode 100644 Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m create mode 100644 Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift create mode 100644 FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift create mode 100644 FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift create mode 100644 FirebaseRemoteConfig/Interop/RolloutAssignment.swift create mode 100644 FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift create mode 100644 FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift create mode 100644 FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift create mode 100755 FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh create mode 100644 FirebaseRemoteConfigInterop.podspec diff --git a/CoreOnly/Tests/FirebasePodTest/Podfile b/CoreOnly/Tests/FirebasePodTest/Podfile index dfe7a0fa557..1e7dacbdb17 100644 --- a/CoreOnly/Tests/FirebasePodTest/Podfile +++ b/CoreOnly/Tests/FirebasePodTest/Podfile @@ -33,6 +33,7 @@ target 'FirebasePodTest' do pod 'FirebaseAppCheckInterop', :path => '../../../' pod 'FirebaseAuthInterop', :path => '../../../' pod 'FirebaseMessagingInterop', :path => '../../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../../' pod 'FirebaseCoreInternal', :path => '../../../' pod 'FirebaseCoreExtension', :path => '../../../' pod 'FirebaseSessions', :path => '../../../' diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 2c06843fcea..62f507a7c01 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased - [added] Updated upload-symbols to 13.7 with VisionPro build phase support. (#12306) +- [changed] Added support for Crashlytics to report metadata about Remote Config keys and values. # 10.22.0 - [fixed] Force validation or rotation of FIDs for FirebaseSessions. diff --git a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h index e0cadd483fb..0b2aa3922f8 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h +++ b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h @@ -81,7 +81,9 @@ void FIRCLSUserLoggingRecordUserKeysAndValues(NSDictionary* keysAndValues); void FIRCLSUserLoggingRecordInternalKeyValue(NSString* key, id value); void FIRCLSUserLoggingWriteInternalKeyValue(NSString* key, NSString* value); -void FIRCLSUserLoggingRecordError(NSError* error, NSDictionary* additionalUserInfo); +void FIRCLSUserLoggingRecordError(NSError* error, + NSDictionary* additionalUserInfo, + NSString* rolloutsInfoJSON); NSDictionary* FIRCLSUserLoggingGetCompactedKVEntries(FIRCLSUserLoggingKVStorage* storage, bool decodeHex); diff --git a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m index 31b4deef1e9..4da93b43450 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m +++ b/Crashlytics/Crashlytics/Components/FIRCLSUserLogging.m @@ -355,7 +355,8 @@ static void FIRCLSUserLoggingWriteError(FIRCLSFile *file, NSError *error, NSDictionary *additionalUserInfo, NSArray *addresses, - uint64_t timestamp) { + uint64_t timestamp, + NSString *rolloutsInfoJSON) { FIRCLSFileWriteSectionStart(file, "error"); FIRCLSFileWriteHashStart(file); FIRCLSFileWriteHashEntryHexEncodedString(file, "domain", [[error domain] UTF8String]); @@ -374,12 +375,20 @@ static void FIRCLSUserLoggingWriteError(FIRCLSFile *file, FIRCLSUserLoggingRecordErrorUserInfo(file, "info", [error userInfo]); FIRCLSUserLoggingRecordErrorUserInfo(file, "extra_info", additionalUserInfo); + // rollouts + if (rolloutsInfoJSON) { + FIRCLSFileWriteHashKey(file, "rollouts"); + FIRCLSFileWriteStringUnquoted(file, [rolloutsInfoJSON UTF8String]); + FIRCLSFileWriteHashEnd(file); + } + FIRCLSFileWriteHashEnd(file); FIRCLSFileWriteSectionEnd(file); } void FIRCLSUserLoggingRecordError(NSError *error, - NSDictionary *additionalUserInfo) { + NSDictionary *additionalUserInfo, + NSString *rolloutsInfoJSON) { if (!error) { return; } @@ -396,7 +405,8 @@ void FIRCLSUserLoggingRecordError(NSError *error, FIRCLSUserLoggingWriteAndCheckABFiles( &_firclsContext.readonly->logging.errorStorage, &_firclsContext.writable->logging.activeErrorLogPath, ^(FIRCLSFile *file) { - FIRCLSUserLoggingWriteError(file, error, additionalUserInfo, addresses, timestamp); + FIRCLSUserLoggingWriteError(file, error, additionalUserInfo, addresses, timestamp, + rolloutsInfoJSON); }); } diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h new file mode 100644 index 00000000000..bda6eabbf5e --- /dev/null +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h @@ -0,0 +1,30 @@ +// 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. + +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + +@interface FIRCLSRolloutsPersistenceManager : NSObject + +- (instancetype _Nullable)initWithFileManager:(FIRCLSFileManager *_Nonnull)fileManager; +- (instancetype _Nonnull)init NS_UNAVAILABLE; ++ (instancetype _Nonnull)new NS_UNAVAILABLE; + +- (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts + reportID:(NSString *_Nonnull)reportID; +- (void)debugLogWithMessage:(NSString *_Nonnull)message; +@end diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m new file mode 100644 index 00000000000..3e7867dab76 --- /dev/null +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m @@ -0,0 +1,67 @@ +// 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. + +#import +#include "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h" +#include "Crashlytics/Crashlytics/Components/FIRCLSUserLogging.h" +#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h" + +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + +@interface FIRCLSRolloutsPersistenceManager : NSObject +@property(nonatomic, readonly) FIRCLSFileManager *fileManager; +@end + +@implementation FIRCLSRolloutsPersistenceManager +- (instancetype)initWithFileManager:(FIRCLSFileManager *)fileManager { + self = [super init]; + if (!self) { + return nil; + } + _fileManager = fileManager; + return self; +} + +- (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts + reportID:(NSString *_Nonnull)reportID { + NSString *rolloutsPath = [[[_fileManager activePath] stringByAppendingPathComponent:reportID] + stringByAppendingPathComponent:FIRCLSReportRolloutsFile]; + if (![_fileManager fileExistsAtPath:rolloutsPath]) { + if (![_fileManager createFileAtPath:rolloutsPath contents:nil attributes:nil]) { + FIRCLSDebugLog(@"Could not create rollouts.clsrecord file. Error was code: %d - message: %s", + errno, strerror(errno)); + } + } + + NSFileHandle *rolloutsFile = [NSFileHandle fileHandleForUpdatingAtPath:rolloutsPath]; + + dispatch_sync(FIRCLSGetLoggingQueue(), ^{ + [rolloutsFile seekToEndOfFile]; + [rolloutsFile writeData:rollouts]; + NSData *newLineData = [@"\n" dataUsingEncoding:NSUTF8StringEncoding]; + [rolloutsFile writeData:newLineData]; + }); +} + +- (void)debugLogWithMessage:(NSString *_Nonnull)message { + FIRCLSDebugLog(message); +} + +@end diff --git a/Crashlytics/Crashlytics/FIRCrashlytics.m b/Crashlytics/Crashlytics/FIRCrashlytics.m index 4d112cddad4..85502b2a9d9 100644 --- a/Crashlytics/Crashlytics/FIRCrashlytics.m +++ b/Crashlytics/Crashlytics/FIRCrashlytics.m @@ -31,6 +31,7 @@ #import "Crashlytics/Crashlytics/Helpers/FIRCLSDefines.h" #include "Crashlytics/Crashlytics/Helpers/FIRCLSProfiling.h" #include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSExecutionIdentifierModel.h" #import "Crashlytics/Crashlytics/Models/FIRCLSFileManager.h" #import "Crashlytics/Crashlytics/Models/FIRCLSSettings.h" #import "Crashlytics/Crashlytics/Settings/Models/FIRCLSApplicationIdentifierModel.h" @@ -47,6 +48,7 @@ #import "Crashlytics/Crashlytics/Controllers/FIRCLSNotificationManager.h" #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportManager.h" #import "Crashlytics/Crashlytics/Controllers/FIRCLSReportUploader.h" +#import "Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h" #import "Crashlytics/Crashlytics/Private/FIRCLSExistingReportManager_Private.h" #import "Crashlytics/Crashlytics/Private/FIRCLSOnDemandModel_Private.h" #import "Crashlytics/Crashlytics/Private/FIRExceptionModel_Private.h" @@ -58,6 +60,12 @@ #import @import FirebaseSessions; +@import FirebaseRemoteConfigInterop; +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods #if TARGET_OS_IPHONE #import @@ -76,7 +84,10 @@ @protocol FIRCrashlyticsInstanceProvider @end -@interface FIRCrashlytics () +@interface FIRCrashlytics () @property(nonatomic) BOOL didPreviouslyCrash; @property(nonatomic, copy) NSString *googleAppID; @@ -91,6 +102,8 @@ @interface FIRCrashlytics () )analytics - sessions:(id)sessions { + sessions:(id)sessions + remoteConfig:(id)remoteConfig { self = [super init]; if (self) { @@ -189,8 +203,19 @@ - (instancetype)initWithApp:(FIRApp *)app }] catch:^void(NSError *error) { FIRCLSErrorLog(@"Crash reporting failed to initialize with error: %@", error); }]; - } + // RemoteConfig subscription should be made after session report directory created. + if (remoteConfig) { + FIRCLSDebugLog(@"Registering RemoteConfig SDK subscription for rollouts data"); + + FIRCLSRolloutsPersistenceManager *persistenceManager = + [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:_fileManager]; + _remoteConfigManager = + [[FIRCLSRemoteConfigManager alloc] initWithRemoteConfig:remoteConfig + persistenceDelegate:persistenceManager]; + [remoteConfig registerRolloutsStateSubscriber:self for:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]; + } + } return self; } @@ -215,6 +240,7 @@ + (void)load { id analytics = FIR_COMPONENT(FIRAnalyticsInterop, container); id sessions = FIR_COMPONENT(FIRSessionsProvider, container); + id remoteConfig = FIR_COMPONENT(FIRRemoteConfigInterop, container); FIRInstallations *installations = [FIRInstallations installationsWithApp:container.app]; @@ -224,7 +250,8 @@ + (void)load { appInfo:NSBundle.mainBundle.infoDictionary installations:installations analytics:analytics - sessions:sessions]; + sessions:sessions + remoteConfig:remoteConfig]; }; FIRComponent *component = @@ -377,11 +404,13 @@ - (void)recordError:(NSError *)error { } - (void)recordError:(NSError *)error userInfo:(NSDictionary *)userInfo { - FIRCLSUserLoggingRecordError(error, userInfo); + NSString *rolloutsInfoJSON = [_remoteConfigManager getRolloutAssignmentsEncodedJsonString]; + FIRCLSUserLoggingRecordError(error, userInfo, rolloutsInfoJSON); } - (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel { - FIRCLSExceptionRecordModel(exceptionModel); + NSString *rolloutsInfoJSON = [_remoteConfigManager getRolloutAssignmentsEncodedJsonString]; + FIRCLSExceptionRecordModel(exceptionModel, rolloutsInfoJSON); } - (void)recordOnDemandExceptionModel:(FIRExceptionModel *)exceptionModel { @@ -407,4 +436,14 @@ - (FIRSessionsSubscriberName)sessionsSubscriberName { return FIRSessionsSubscriberNameCrashlytics; } +#pragma mark - FIRRolloutsStateSubscriber +- (void)rolloutsStateDidChange:(FIRRolloutsState *_Nonnull)rolloutsState { + if (!_remoteConfigManager) { + FIRCLSDebugLog(@"rolloutsStateDidChange gets called without init the rc manager."); + return; + } + NSString *currentReportID = _managerData.executionIDModel.executionID; + [_remoteConfigManager updateRolloutsStateWithRolloutsState:rolloutsState + reportID:currentReportID]; +} @end diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSException.h b/Crashlytics/Crashlytics/Handlers/FIRCLSException.h index ae53b916f8b..65aae9bfd32 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSException.h +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSException.h @@ -60,7 +60,7 @@ void FIRCLSExceptionRaiseTestObjCException(void) __attribute((noreturn)); void FIRCLSExceptionRaiseTestCppException(void) __attribute((noreturn)); #ifdef __OBJC__ -void FIRCLSExceptionRecordModel(FIRExceptionModel* exceptionModel); +void FIRCLSExceptionRecordModel(FIRExceptionModel* exceptionModel, NSString* rolloutsInfoJSON); NSString* FIRCLSExceptionRecordOnDemandModel(FIRExceptionModel* exceptionModel, int previousRecordedOnDemandExceptions, int previousDroppedOnDemandExceptions); @@ -68,7 +68,8 @@ void FIRCLSExceptionRecordNSException(NSException* exception); void FIRCLSExceptionRecord(FIRCLSExceptionType type, const char* name, const char* reason, - NSArray* frames); + NSArray* frames, + NSString* rolloutsInfoJSON); NSString* FIRCLSExceptionRecordOnDemand(FIRCLSExceptionType type, const char* name, const char* reason, diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm b/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm index 798a4548ded..b92cd9848dd 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSException.mm @@ -82,11 +82,11 @@ void FIRCLSExceptionInitialize(FIRCLSExceptionReadOnlyContext *roContext, rwContext->customExceptionCount = 0; } -void FIRCLSExceptionRecordModel(FIRExceptionModel *exceptionModel) { +void FIRCLSExceptionRecordModel(FIRExceptionModel *exceptionModel, NSString *rolloutsInfoJSON) { const char *name = [[exceptionModel.name copy] UTF8String]; const char *reason = [[exceptionModel.reason copy] UTF8String] ?: ""; - - FIRCLSExceptionRecord(FIRCLSExceptionTypeCustom, name, reason, [exceptionModel.stackTrace copy]); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCustom, name, reason, [exceptionModel.stackTrace copy], + rolloutsInfoJSON); } NSString *FIRCLSExceptionRecordOnDemandModel(FIRExceptionModel *exceptionModel, @@ -122,7 +122,7 @@ void FIRCLSExceptionRecordNSException(NSException *exception) { } FIRCLSExceptionRecord(FIRCLSExceptionTypeObjectiveC, [name UTF8String], [reason UTF8String], - frames); + frames, nil); } static void FIRCLSExceptionRecordFrame(FIRCLSFile *file, FIRStackFrame *frame) { @@ -175,7 +175,8 @@ void FIRCLSExceptionWrite(FIRCLSFile *file, FIRCLSExceptionType type, const char *name, const char *reason, - NSArray *frames) { + NSArray *frames, + NSString *rolloutsInfoJSON) { FIRCLSFileWriteSectionStart(file, "exception"); FIRCLSFileWriteHashStart(file); @@ -196,6 +197,12 @@ void FIRCLSExceptionWrite(FIRCLSFile *file, FIRCLSFileWriteArrayEnd(file); } + if (rolloutsInfoJSON) { + FIRCLSFileWriteHashKey(file, "rollouts"); + FIRCLSFileWriteStringUnquoted(file, [rolloutsInfoJSON UTF8String]); + FIRCLSFileWriteHashEnd(file); + } + FIRCLSFileWriteHashEnd(file); FIRCLSFileWriteSectionEnd(file); @@ -204,7 +211,8 @@ void FIRCLSExceptionWrite(FIRCLSFile *file, void FIRCLSExceptionRecord(FIRCLSExceptionType type, const char *name, const char *reason, - NSArray *frames) { + NSArray *frames, + NSString *rolloutsInfoJSON) { if (!FIRCLSContextIsInitialized()) { return; } @@ -224,7 +232,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, return; } - FIRCLSExceptionWrite(&file, type, name, reason, frames); + FIRCLSExceptionWrite(&file, type, name, reason, frames, nil); // We only want to do this work if we have the expectation that we'll actually crash FIRCLSHandler(&file, mach_thread_self(), NULL); @@ -235,7 +243,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, FIRCLSUserLoggingWriteAndCheckABFiles( &_firclsContext.readonly->logging.customExceptionStorage, &_firclsContext.writable->logging.activeCustomExceptionPath, ^(FIRCLSFile *file) { - FIRCLSExceptionWrite(file, type, name, reason, frames); + FIRCLSExceptionWrite(file, type, name, reason, frames, rolloutsInfoJSON); }); } @@ -271,6 +279,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, // Create new report and copy into it the current state of custom keys and log and the sdk.log, // binary_images.clsrecord, and metadata.clsrecord files. + // Also copy rollouts.clsrecord if applicable. NSError *error = nil; BOOL copied = [fileManager.underlyingFileManager copyItemAtPath:currentReportPath toPath:newReportPath @@ -343,7 +352,7 @@ void FIRCLSExceptionRecord(FIRCLSExceptionType type, FIRCLSSDKLog("Unable to open log file for on demand custom exception\n"); return nil; } - FIRCLSExceptionWrite(&file, type, name, reason, frames); + FIRCLSExceptionWrite(&file, type, name, reason, frames, nil); FIRCLSHandler(&file, mach_thread_self(), NULL); FIRCLSFileClose(&file); @@ -397,19 +406,21 @@ static void FIRCLSCatchAndRecordActiveException(std::type_info *typeInfo) { #endif } } catch (const char *exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "const char *", exc, nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "const char *", exc, nil, nil); } catch (const std::string &exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::string", exc.c_str(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::string", exc.c_str(), nil, nil); } catch (const std::exception &exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc.what(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc.what(), nil, + nil); } catch (const std::exception *exc) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc->what(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), exc->what(), nil, + nil); } catch (const std::bad_alloc &exc) { // it is especially important to avoid demangling in this case, because the expetation at this // point is that all allocations could fail - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::bad_alloc", exc.what(), nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, "std::bad_alloc", exc.what(), nil, nil); } catch (...) { - FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), "", nil); + FIRCLSExceptionRecord(FIRCLSExceptionTypeCpp, FIRCLSExceptionDemangle(name), "", nil, nil); } } diff --git a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h index 6303962c667..624c1990ae7 100644 --- a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h +++ b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h @@ -36,6 +36,7 @@ extern NSString *const FIRCLSReportInternalIncrementalKVFile; extern NSString *const FIRCLSReportInternalCompactedKVFile; extern NSString *const FIRCLSReportUserIncrementalKVFile; extern NSString *const FIRCLSReportUserCompactedKVFile; +extern NSString *const FIRCLSReportRolloutsFile; @class FIRCLSFileManager; diff --git a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m index 61daf92f3e8..35160d1cbc1 100644 --- a/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m +++ b/Crashlytics/Crashlytics/Models/FIRCLSInternalReport.m @@ -41,6 +41,7 @@ NSString *const FIRCLSReportInternalCompactedKVFile = @"internal_compacted_kv.clsrecord"; NSString *const FIRCLSReportUserIncrementalKVFile = @"user_incremental_kv.clsrecord"; NSString *const FIRCLSReportUserCompactedKVFile = @"user_compacted_kv.clsrecord"; +NSString *const FIRCLSReportRolloutsFile = @"rollouts.clsrecord"; @interface FIRCLSInternalReport () { NSString *_identifier; diff --git a/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift new file mode 100644 index 00000000000..d6d5cb16b82 --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/CrashlyticsRemoteConfigManager.swift @@ -0,0 +1,141 @@ +// Copyright 2023 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. + +import FirebaseRemoteConfigInterop +import Foundation + +@objc(FIRCLSPersistenceLog) +public protocol CrashlyticsPersistenceLog { + func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) + func debugLog(message: String) +} + +@objc(FIRCLSRemoteConfigManager) +public class CrashlyticsRemoteConfigManager: NSObject { + public static let maxRolloutAssignments = 128 + public static let maxParameterValueLength = 256 + + private let lock = NSLock() + private var _rolloutAssignment: [RolloutAssignment] = [] + + var remoteConfig: RemoteConfigInterop + var persistenceDelegate: CrashlyticsPersistenceLog + + @objc public var rolloutAssignment: [RolloutAssignment] { + lock.lock() + defer { lock.unlock() } + let copy = _rolloutAssignment + return copy + } + + @objc public init(remoteConfig: RemoteConfigInterop, + persistenceDelegate: CrashlyticsPersistenceLog) { + self.remoteConfig = remoteConfig + self.persistenceDelegate = persistenceDelegate + } + + @objc public func updateRolloutsState(rolloutsState: RolloutsState, reportID: String) { + lock.lock() + _rolloutAssignment = normalizeRolloutAssignment(assignments: Array(rolloutsState.assignments)) + lock.unlock() + + // Writring to persistence + if let rolloutsData = + getRolloutsStateEncodedJsonData() { + persistenceDelegate.updateRolloutsStateToPersistence( + rollouts: rolloutsData, + reportID: reportID + ) + } + } + + /// Return string format: [{RolloutAssignment1}, {RolloutAssignment2}, {RolloutAssignment3}...] + /// This will get inserted into each clsrcord for non-fatal events. + /// Return a string type because later `FIRCLSFileWriteStringUnquoted` takes string as input + @objc public func getRolloutAssignmentsEncodedJsonString() -> String? { + let encodeData = getRolloutAssignmentsEncodedJsonData() + if let data = encodeData { + return String(data: data, encoding: .utf8) + } + + let debugInfo = encodeData?.debugDescription ?? "nil" + persistenceDelegate.debugLog(message: String( + format: "Failed to serialize rollouts: %@", + arguments: [debugInfo] + )) + + return nil + } +} + +private extension CrashlyticsRemoteConfigManager { + func normalizeRolloutAssignment(assignments: [RolloutAssignment]) -> [RolloutAssignment] { + var validatedAssignments = assignments + if assignments.count > CrashlyticsRemoteConfigManager.maxRolloutAssignments { + persistenceDelegate + .debugLog( + message: "Rollouts excess the maximum number of assignments can pass to Crashlytics" + ) + validatedAssignments = + Array(assignments[.. CrashlyticsRemoteConfigManager.maxParameterValueLength { + debugPrint( + "Rollouts excess the maximum length of parameter value can pass to Crashlytics", + assignment.parameterValue + ) + let upperBound = String.Index( + utf16Offset: CrashlyticsRemoteConfigManager.maxParameterValueLength, + in: assignment.parameterValue + ) + let slicedParameterValue = assignment.parameterValue[.. Data? { + let contentEncodedRolloutAssignments = rolloutAssignment.map { assignment in + EncodedRolloutAssignment(assignment: assignment) + } + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.outputFormatting = .sortedKeys + let encodeData = try? encoder.encode(contentEncodedRolloutAssignments) + return encodeData + } + + /// Return string format: {"rollouts": [{RolloutAssignment1}, {RolloutAssignment2}, + /// {RolloutAssignment3}...]} + /// This will get stored in the separate rollouts.clsrecord + /// Return a data type because later `[NSFileHandler writeData:]` takes data as input + func getRolloutsStateEncodedJsonData() -> Data? { + let contentEncodedRolloutAssignments = rolloutAssignment.map { assignment in + EncodedRolloutAssignment(assignment: assignment) + } + + let state = EncodedRolloutsState(assignments: contentEncodedRolloutAssignments) + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let encodeData = try? encoder.encode(state) + return encodeData + } +} diff --git a/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift b/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift new file mode 100644 index 00000000000..725b63050ec --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/EncodedRolloutAssignment.swift @@ -0,0 +1,44 @@ +// 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. + +import FirebaseRemoteConfigInterop +import Foundation + +@objc(FIRCLSEncodedRolloutsState) +class EncodedRolloutsState: NSObject, Codable { + @objc public private(set) var rollouts: [EncodedRolloutAssignment] + + @objc public init(assignments: [EncodedRolloutAssignment]) { + rollouts = assignments + super.init() + } +} + +@objc(FIRCLSEncodedRolloutAssignment) +class EncodedRolloutAssignment: NSObject, Codable { + @objc public private(set) var rolloutId: String + @objc public private(set) var variantId: String + @objc public private(set) var templateVersion: Int64 + @objc public private(set) var parameterKey: String + @objc public private(set) var parameterValue: String + + public init(assignment: RolloutAssignment) { + rolloutId = FileUtility.stringToHexConverter(for: assignment.rolloutId) + variantId = FileUtility.stringToHexConverter(for: assignment.variantId) + templateVersion = assignment.templateVersion + parameterKey = FileUtility.stringToHexConverter(for: assignment.parameterKey) + parameterValue = FileUtility.stringToHexConverter(for: assignment.parameterValue) + super.init() + } +} diff --git a/Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift b/Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift new file mode 100644 index 00000000000..9d4365db927 --- /dev/null +++ b/Crashlytics/Crashlytics/Rollouts/StringToHexConverter.swift @@ -0,0 +1,38 @@ +// 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. + +import Foundation + +// This is a swift rewrite for the logic in FIRCLSFile for the function FIRCLSFileHexEncodeString() +@objc(FIRCLSwiftFileUtility) +public class FileUtility: NSObject { + @objc public static func stringToHexConverter(for string: String) -> String { + let hexMap = "0123456789abcdef" + + var processedString = "" + let utf8Array = string.utf8.map { UInt8($0) } + for c in utf8Array { + let index1 = String.Index( + utf16Offset: Int(c >> 4), + in: hexMap + ) + let index2 = String.Index( + utf16Offset: Int(c & 0x0F), + in: hexMap + ) + processedString = processedString + String(hexMap[index1]) + String(hexMap[index2]) + } + return processedString + } +} diff --git a/Crashlytics/UnitTests/FIRCLSFileTests.m b/Crashlytics/UnitTests/FIRCLSFileTests.m index 85ff6c36a57..b8895f68a67 100644 --- a/Crashlytics/UnitTests/FIRCLSFileTests.m +++ b/Crashlytics/UnitTests/FIRCLSFileTests.m @@ -14,6 +14,12 @@ #include "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h" +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + #import @interface FIRCLSFileTests : XCTestCase @@ -169,6 +175,31 @@ - (void)hexEncodingStringWithFile:(FIRCLSFile *)file buffered ? @"" : @"un"); } +// This is the test to compare FIRCLSwiftFileUtility.stringToHexConverter(for:) and +// FIRCLSFileWriteHexEncodedString return the same hex encoding value +- (void)testHexEncodingStringObjcAndSwiftResultsSame { + NSString *testedValueString = @"是themis的测试数据,输入中文"; + + FIRCLSFile *unbufferedFile = &_unbufferedFile; + FIRCLSFileWriteHashStart(unbufferedFile); + FIRCLSFileWriteHashEntryHexEncodedString(unbufferedFile, "hex", [testedValueString UTF8String]); + FIRCLSFileWriteHashEnd(unbufferedFile); + NSString *contentsFromObjcHexEncoding = [self contentsOfFileAtPath:self.unbufferedPath]; + + FIRCLSFile *bufferedFile = &_bufferedFile; + NSString *encodedValue = [FIRCLSwiftFileUtility stringToHexConverterFor:testedValueString]; + FIRCLSFileWriteHashStart(bufferedFile); + FIRCLSFileWriteHashKey(bufferedFile, "hex"); + FIRCLSFileWriteStringUnquoted(bufferedFile, "\""); + FIRCLSFileWriteStringUnquoted(bufferedFile, [encodedValue UTF8String]); + FIRCLSFileWriteStringUnquoted(bufferedFile, "\""); + FIRCLSFileWriteHashEnd(bufferedFile); + FIRCLSFileFlushWriteBuffer(bufferedFile); + NSString *contentsFromSwiftHexEncoding = [self contentsOfFileAtPath:self.bufferedPath]; + + XCTAssertTrue([contentsFromObjcHexEncoding isEqualToString:contentsFromSwiftHexEncoding]); +} + #pragma mark - - (void)testHexEncodingLongString { diff --git a/Crashlytics/UnitTests/FIRCLSLoggingTests.m b/Crashlytics/UnitTests/FIRCLSLoggingTests.m index a5c72f5c73c..b79341fe06e 100644 --- a/Crashlytics/UnitTests/FIRCLSLoggingTests.m +++ b/Crashlytics/UnitTests/FIRCLSLoggingTests.m @@ -365,7 +365,7 @@ - (void)testLoggedError { code:-1 userInfo:@{@"key1" : @"value", @"key2" : @"value2"}]; - FIRCLSUserLoggingRecordError(error, @{@"additional" : @"key"}); + FIRCLSUserLoggingRecordError(error, @{@"additional" : @"key"}, nil); NSArray* errors = [self errorAContents]; @@ -405,7 +405,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { userInfo:@{@"key1" : @"value", @"key2" : @"value2"}]; for (size_t i = 0; i < _firclsContext.readonly->logging.errorStorage.maxEntries; ++i) { - FIRCLSUserLoggingRecordError(error, nil); + FIRCLSUserLoggingRecordError(error, nil, nil); } NSArray* errors = [self errorAContents]; @@ -414,7 +414,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { // at this point, if we log one more, we should expect a roll over to the next file - FIRCLSUserLoggingRecordError(error, nil); + FIRCLSUserLoggingRecordError(error, nil, nil); XCTAssertEqual([[self errorAContents] count], 8, @""); XCTAssertEqual([[self errorBContents] count], 1, @""); @@ -422,7 +422,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { // and our next entry should continue into the B file - FIRCLSUserLoggingRecordError(error, nil); + FIRCLSUserLoggingRecordError(error, nil, nil); XCTAssertEqual([[self errorAContents] count], 8, @""); XCTAssertEqual([[self errorBContents] count], 2, @""); @@ -432,7 +432,7 @@ - (void)testWritingMaximumNumberOfLoggedErrors { - (void)testLoggedErrorWithNullsInAdditionalInfo { NSError* error = [NSError errorWithDomain:@"Domain" code:-1 userInfo:nil]; - FIRCLSUserLoggingRecordError(error, @{@"null-key" : [NSNull null]}); + FIRCLSUserLoggingRecordError(error, @{@"null-key" : [NSNull null]}, nil); NSArray* errors = [self errorAContents]; diff --git a/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m b/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m new file mode 100644 index 00000000000..aec030d7538 --- /dev/null +++ b/Crashlytics/UnitTests/FIRCLSRolloutsPersistenceManagerTests.m @@ -0,0 +1,70 @@ +// Copyright 2024 Google +// +// 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. + +#import +#import + +#import "Crashlytics/Crashlytics/Components/FIRCLSContext.h" +#import "Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.h" +#import "Crashlytics/Crashlytics/Models/FIRCLSInternalReport.h" +#import "Crashlytics/UnitTests/Mocks/FIRCLSTempMockFileManager.h" +#if SWIFT_PACKAGE +@import FirebaseCrashlyticsSwift; +#else // Swift Package Manager +#import +#endif // CocoaPods + +NSString *reportId = @"1234567"; + +@interface FIRCLSRolloutsPersistenceManagerTests : XCTestCase +@property(nonatomic, strong) FIRCLSTempMockFileManager *fileManager; +@property(nonatomic, strong) FIRCLSRolloutsPersistenceManager *rolloutsPersistenceManager; +@end + +@implementation FIRCLSRolloutsPersistenceManagerTests +- (void)setUp { + [super setUp]; + FIRCLSContextBaseInit(); + self.fileManager = [[FIRCLSTempMockFileManager alloc] init]; + [self.fileManager createReportDirectories]; + [self.fileManager setupNewPathForExecutionIdentifier:reportId]; + + self.rolloutsPersistenceManager = + [[FIRCLSRolloutsPersistenceManager alloc] initWithFileManager:self.fileManager]; +} + +- (void)tearDown { + [self.fileManager removeItemAtPath:_fileManager.rootPath]; + FIRCLSContextBaseDeinit(); + [super tearDown]; +} + +- (void)testUpdateRolloutsStateToPersistenceWithRollouts { + NSString *encodedStateString = + @"{rollouts:[{\"parameter_key\":\"6d795f66656174757265\",\"parameter_value\":" + @"\"e8bf99e698af7468656d6973e79a84e6b58be8af95e695b0e68daeefbc8ce8be93e585a5e4b8ade69687\"," + @"\"rollout_id\":\"726f6c6c6f75745f31\",\"template_version\":1,\"variant_id\":" + @"\"636f6e74726f6c\"}]}"; + + NSData *data = [encodedStateString dataUsingEncoding:NSUTF8StringEncoding]; + NSString *rolloutsFilePath = + [[[self.fileManager activePath] stringByAppendingPathComponent:reportId] + stringByAppendingPathComponent:FIRCLSReportRolloutsFile]; + + [self.rolloutsPersistenceManager updateRolloutsStateToPersistenceWithRollouts:data + reportID:reportId]; + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:rolloutsFilePath]); +} + +@end diff --git a/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m b/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m index 1908fea71db..e564614f8ae 100644 --- a/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m +++ b/Crashlytics/UnitTests/FIRRecordExceptionModelTests.m @@ -75,7 +75,7 @@ - (void)testWrittenCLSRecordFile { FIRExceptionModel *exceptionModel = [FIRExceptionModel exceptionModelWithName:name reason:reason]; exceptionModel.stackTrace = stackTrace; - FIRCLSExceptionRecordModel(exceptionModel); + FIRCLSExceptionRecordModel(exceptionModel, nil); NSData *data = [NSData dataWithContentsOfFile:[self.reportPath diff --git a/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift new file mode 100644 index 00000000000..6c2e070e47f --- /dev/null +++ b/Crashlytics/UnitTestsSwift/CrashlyticsRemoteConfigManagerTests.swift @@ -0,0 +1,136 @@ +// Copyright 2023 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. +#if SWIFT_PACKAGE + @testable import FirebaseCrashlyticsSwift +#else + @testable import FirebaseCrashlytics +#endif +import FirebaseRemoteConfigInterop +import XCTest + +class RemoteConfigConfigMock: RemoteConfigInterop { + func registerRolloutsStateSubscriber(_ subscriber: FirebaseRemoteConfigInterop + .RolloutsStateSubscriber, + for namespace: String) {} +} + +class PersistanceManagerMock: CrashlyticsPersistenceLog { + func updateRolloutsStateToPersistence(rollouts: Data, reportID: String) {} + func debugLog(message: String) {} +} + +final class CrashlyticsRemoteConfigManagerTests: XCTestCase { + let rollouts: RolloutsState = { + let assignment1 = RolloutAssignment( + rolloutId: "rollout_1", + variantId: "control", + templateVersion: 1, + parameterKey: "my_feature", + parameterValue: "false" + ) + let assignment2 = RolloutAssignment( + rolloutId: "rollout_2", + variantId: "enabled", + templateVersion: 1, + parameterKey: "themis_big_feature", + parameterValue: "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ) + let rollouts = RolloutsState(assignmentList: [assignment1, assignment2]) + return rollouts + }() + + let singleRollout: RolloutsState = { + let assignment1 = RolloutAssignment( + rolloutId: "rollout_1", + variantId: "control", + templateVersion: 1, + parameterKey: "my_feature", + parameterValue: "这是themis的测试数据,输入中文" // check unicode + ) + let rollouts = RolloutsState(assignmentList: [assignment1]) + return rollouts + }() + + let rcInterop = RemoteConfigConfigMock() + + func testRemoteConfigManagerProperlyProcessRolloutsState() throws { + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: rollouts, reportID: "12R") + XCTAssertEqual(rcManager.rolloutAssignment.count, 2) + + for assignment in rollouts.assignments { + if assignment.parameterKey == "themis_big_feature" { + XCTAssertEqual( + assignment.parameterValue.count, + CrashlyticsRemoteConfigManager.maxParameterValueLength + ) + } + } + } + + func testRemoteConfigManagerGenerateEncodedRolloutAssignmentsJson() throws { + let expectedString = + "[{\"parameter_key\":\"6d795f66656174757265\",\"parameter_value\":\"e8bf99e698af7468656d6973e79a84e6b58be8af95e695b0e68daeefbc8ce8be93e585a5e4b8ade69687\",\"rollout_id\":\"726f6c6c6f75745f31\",\"template_version\":1,\"variant_id\":\"636f6e74726f6c\"}]" + + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: singleRollout, reportID: "456") + + let string = rcManager.getRolloutAssignmentsEncodedJsonString() + XCTAssertEqual(string, expectedString) + } + + func testMultiThreadsUpdateRolloutAssignments() throws { + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + DispatchQueue.main.async { [weak self] in + if let singleRollout = self?.singleRollout { + rcManager.updateRolloutsState(rolloutsState: singleRollout, reportID: "456") + XCTAssertEqual(rcManager.rolloutAssignment.count, 1) + } + } + + DispatchQueue.main.async { [weak self] in + if let rollouts = self?.rollouts { + rcManager.updateRolloutsState(rolloutsState: rollouts, reportID: "456") + XCTAssertEqual(rcManager.rolloutAssignment.count, 2) + } + } + } + + func testMultiThreadsReadAndWriteRolloutAssignments() throws { + let rcManager = CrashlyticsRemoteConfigManager( + remoteConfig: rcInterop, + persistenceDelegate: PersistanceManagerMock() + ) + rcManager.updateRolloutsState(rolloutsState: singleRollout, reportID: "456") + + DispatchQueue.main.async { [weak self] in + if let rollouts = self?.rollouts { + let oldAssignments = rcManager.rolloutAssignment + rcManager.updateRolloutsState(rolloutsState: rollouts, reportID: "456") + XCTAssertEqual(rcManager.rolloutAssignment.count, 2) + XCTAssertEqual(oldAssignments.count, 1) + } + } + XCTAssertEqual(rcManager.rolloutAssignment.count, 1) + } +} diff --git a/Example/watchOSSample/Podfile b/Example/watchOSSample/Podfile index 5dd5e804c13..2f862708597 100644 --- a/Example/watchOSSample/Podfile +++ b/Example/watchOSSample/Podfile @@ -19,6 +19,7 @@ target 'SampleWatchAppWatchKitExtension' do pod 'FirebaseDatabase', :path => '../../' pod 'FirebaseAppCheckInterop', :path => '../../' pod 'FirebaseAuthInterop', :path => '../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../' pod 'Firebase/Messaging', :path => '../../' pod 'Firebase/Storage', :path => '../../' diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index 108645728e4..e67e886b399 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| s.prefix_header_file = false s.source_files = [ - 'Crashlytics/Crashlytics/**/*.{c,h,m,mm}', + 'Crashlytics/Crashlytics/**/*.{c,h,m,mm,swift}', 'Crashlytics/Protogen/**/*.{c,h,m,mm}', 'Crashlytics/Shared/**/*.{c,h,m,mm}', 'Crashlytics/third_party/**/*.{c,h,m,mm}', @@ -62,6 +62,7 @@ Pod::Spec.new do |s| s.dependency 'FirebaseCore', '~> 10.5' s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'FirebaseSessions', '~> 10.5' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.23' s.dependency 'PromisesObjC', '~> 2.1' s.dependency 'GoogleDataTransport', '~> 9.2' s.dependency 'GoogleUtilities/Environment', '~> 7.8' @@ -119,7 +120,8 @@ Pod::Spec.new do |s| :tvos => tvos_deployment_target } unit_tests.source_files = 'Crashlytics/UnitTests/*.[mh]', - 'Crashlytics/UnitTests/*/*.[mh]' + 'Crashlytics/UnitTests/*/*.[mh]', + 'Crashlytics/UnitTestsSwift/*.swift' unit_tests.resources = 'Crashlytics/UnitTests/Data/*', 'Crashlytics/UnitTests/*.clsrecord', 'Crashlytics/UnitTests/FIRCLSMachO/machO_data/*' diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index efef50fef50..9767a218a60 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -56,6 +56,7 @@ app update. s.dependency 'FirebaseInstallations', '~> 10.0' s.dependency 'GoogleUtilities/Environment', '~> 7.8' s.dependency 'GoogleUtilities/NSData+zlib', '~> 7.8' + s.dependency 'FirebaseRemoteConfigInterop', '~> 10.23' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } @@ -80,7 +81,8 @@ app update. 'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.m', 'FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m', 'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h', - 'FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m' + 'FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m', + 'FirebaseRemoteConfig/Tests/SwiftUnit/*.swift' # Supply plist custom plist testing. unit_tests.resources = 'FirebaseRemoteConfig/Tests/Unit/Defaults-testInfo.plist', diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index 47e9aff902e..e56c9da1f2d 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [changed] Add support for other Firebase products to integrate with Remote Config. + # 10.17.0 - [feature] The `FirebaseRemoteConfig` module now contains Firebase Remote Config's Swift-only APIs that were previously only available via the diff --git a/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift b/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift new file mode 100644 index 00000000000..f9a10e409b7 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RemoteConfigConstants.swift @@ -0,0 +1,21 @@ +// 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. + +import Foundation + +@objc(FIRRemoteConfigConstants) +public final class RemoteConfigConstants: NSObject { + @objc(FIRNamespaceGoogleMobilePlatform) public static let NamespaceGoogleMobilePlatform = + "firebase" +} diff --git a/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift b/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift new file mode 100644 index 00000000000..b7988efa389 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RemoteConfigInterop.swift @@ -0,0 +1,21 @@ +// Copyright 2023 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. + +import Foundation + +@objc(FIRRemoteConfigInterop) +public protocol RemoteConfigInterop { + func registerRolloutsStateSubscriber(_ subscriber: RolloutsStateSubscriber, + for namespace: String) +} diff --git a/FirebaseRemoteConfig/Interop/RolloutAssignment.swift b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift new file mode 100644 index 00000000000..715412bb4f1 --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RolloutAssignment.swift @@ -0,0 +1,47 @@ +// Copyright 2023 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. + +import Foundation + +@objc(FIRRolloutAssignment) +public class RolloutAssignment: NSObject { + @objc public var rolloutId: String + @objc public var variantId: String + @objc public var templateVersion: Int64 + @objc public var parameterKey: String + @objc public var parameterValue: String + + @objc public init(rolloutId: String, variantId: String, templateVersion: Int64, + parameterKey: String, + parameterValue: String) { + self.rolloutId = rolloutId + self.variantId = variantId + self.templateVersion = templateVersion + self.parameterKey = parameterKey + self.parameterValue = parameterValue + super.init() + } +} + +@objc(FIRRolloutsState) +public class RolloutsState: NSObject { + @objc public var assignments: Set = Set() + + @objc public init(assignmentList: [RolloutAssignment]) { + for assignment in assignmentList { + assignments.insert(assignment) + } + super.init() + } +} diff --git a/FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift b/FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift new file mode 100644 index 00000000000..88e5ba8772d --- /dev/null +++ b/FirebaseRemoteConfig/Interop/RolloutsStateSubscriber.swift @@ -0,0 +1,20 @@ +// Copyright 2023 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. + +import Foundation + +@objc(FIRRolloutsStateSubscriber) +public protocol RolloutsStateSubscriber { + func rolloutsStateDidChange(_ rolloutsState: RolloutsState) +} diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 4035d558707..561ada50693 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -45,6 +45,8 @@ /// Notification when config is successfully activated const NSNotificationName FIRRemoteConfigActivateNotification = @"FIRRemoteConfigActivateNotification"; +static NSNotificationName FIRRolloutsStateDidChangeNotificationName = + @"FIRRolloutsStateDidChangeNotification"; /// Listener for the get methods. typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull); @@ -79,8 +81,9 @@ @implementation FIRRemoteConfig { *RCInstances; + (nonnull FIRRemoteConfig *)remoteConfigWithApp:(FIRApp *_Nonnull)firebaseApp { - return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform - app:firebaseApp]; + return [FIRRemoteConfig + remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:firebaseApp]; } + (nonnull FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *_Nonnull)firebaseNamespace { @@ -116,8 +119,9 @@ + (FIRRemoteConfig *)remoteConfig { @"initializer in SwiftUI."]; } - return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + return [FIRRemoteConfig + remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; } /// Singleton instance of serial queue for queuing all incoming RC calls. @@ -329,10 +333,20 @@ - (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completi // New config has been activated at this point FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); [strongSelf->_configContent activatePersonalization]; + // Update last active template version number in setting and userDefaults. + [strongSelf->_settings updateLastActiveTemplateVersion]; + // Update activeRolloutMetadata + [strongSelf->_configContent activateRolloutMetadata:^(BOOL success) { + if (success) { + [self notifyRolloutsStateChange:strongSelf->_configContent.activeRolloutMetadata + versionNumber:strongSelf->_settings.lastActiveTemplateVersion]; + } + }]; + // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; - if ([namespace isEqualToString:FIRNamespaceGoogleMobilePlatform]) { + if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) { dispatch_async(dispatch_get_main_queue(), ^{ [self notifyConfigHasActivated]; }); @@ -377,6 +391,17 @@ - (NSString *)fullyQualifiedNamespace:(NSString *)namespace { return fullyQualifiedNamespace; } +- (FIRRemoteConfigValue *)defaultValueForFullyQualifiedNamespace:(NSString *)namespace + key:(NSString *)key { + FIRRemoteConfigValue *value = self->_configContent.defaultConfig[namespace][key]; + if (!value) { + value = [[FIRRemoteConfigValue alloc] + initWithData:[NSData data] + source:(FIRRemoteConfigSource)FIRRemoteConfigSourceStatic]; + } + return value; +} + #pragma mark - Get Config Result - (FIRRemoteConfigValue *)objectForKeyedSubscript:(NSString *)key { @@ -402,13 +427,7 @@ - (FIRRemoteConfigValue *)configValueForKey:(NSString *)key { config:[self->_configContent getConfigAndMetadataForNamespace:FQNamespace]]; return; } - value = self->_configContent.defaultConfig[FQNamespace][key]; - if (value) { - return; - } - - value = [[FIRRemoteConfigValue alloc] initWithData:[NSData data] - source:FIRRemoteConfigSourceStatic]; + value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key]; }); return value; } @@ -613,4 +632,67 @@ - (FIRConfigUpdateListenerRegistration *)addOnConfigUpdateListener: return [self->_configRealtime addConfigUpdateListener:listener]; } +#pragma mark - Rollout + +- (void)addRemoteConfigInteropSubscriber:(id)subscriber { + [[NSNotificationCenter defaultCenter] + addObserverForName:FIRRolloutsStateDidChangeNotificationName + object:self + queue:nil + usingBlock:^(NSNotification *_Nonnull notification) { + FIRRolloutsState *rolloutsState = + notification.userInfo[FIRRolloutsStateDidChangeNotificationName]; + [subscriber rolloutsStateDidChange:rolloutsState]; + }]; + // Send active rollout metadata stored in persistence while app launched if there is activeConfig + NSString *fullyQualifiedNamespace = [self fullyQualifiedNamespace:_FIRNamespace]; + NSDictionary *activeConfig = self->_configContent.activeConfig; + if (activeConfig[fullyQualifiedNamespace] && activeConfig[fullyQualifiedNamespace].count > 0) { + [self notifyRolloutsStateChange:self->_configContent.activeRolloutMetadata + versionNumber:self->_settings.lastActiveTemplateVersion]; + } +} + +- (void)notifyRolloutsStateChange:(NSArray *)rolloutMetadata + versionNumber:(NSString *)versionNumber { + NSArray *rolloutsAssignments = + [self rolloutsAssignmentsWith:rolloutMetadata versionNumber:versionNumber]; + FIRRolloutsState *rolloutsState = + [[FIRRolloutsState alloc] initWithAssignmentList:rolloutsAssignments]; + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", + @"Send rollouts state notification with name %@ to RemoteConfigInterop.", + FIRRolloutsStateDidChangeNotificationName); + [[NSNotificationCenter defaultCenter] + postNotificationName:FIRRolloutsStateDidChangeNotificationName + object:self + userInfo:@{FIRRolloutsStateDidChangeNotificationName : rolloutsState}]; +} + +- (NSArray *)rolloutsAssignmentsWith: + (NSArray *)rolloutMetadata + versionNumber:(NSString *)versionNumber { + NSMutableArray *rolloutsAssignments = [[NSMutableArray alloc] init]; + NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace]; + for (NSDictionary *metadata in rolloutMetadata) { + NSString *rolloutId = metadata[RCNFetchResponseKeyRolloutID]; + NSString *variantID = metadata[RCNFetchResponseKeyVariantID]; + NSArray *affectedParameterKeys = metadata[RCNFetchResponseKeyAffectedParameterKeys]; + if (rolloutId && variantID && affectedParameterKeys) { + for (NSString *key in affectedParameterKeys) { + FIRRemoteConfigValue *value = self->_configContent.activeConfig[FQNamespace][key]; + if (!value) { + value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key]; + } + FIRRolloutAssignment *assignment = + [[FIRRolloutAssignment alloc] initWithRolloutId:rolloutId + variantId:variantID + templateVersion:[versionNumber longLongValue] + parameterKey:key + parameterValue:value.stringValue]; + [rolloutsAssignments addObject:assignment]; + } + } + } + return rolloutsAssignments; +} @end diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h index f015ea14974..e8dda531a01 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h @@ -17,6 +17,7 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; @class FIRApp; @class FIRRemoteConfig; @@ -37,7 +38,8 @@ NS_ASSUME_NONNULL_BEGIN /// A concrete implementation for FIRRemoteConfigInterop to create Remote Config instances and /// register with Core's component system. -@interface FIRRemoteConfigComponent : NSObject +@interface FIRRemoteConfigComponent + : NSObject /// The FIRApp that instances will be set up with. @property(nonatomic, weak, readonly) FIRApp *app; @@ -45,6 +47,10 @@ NS_ASSUME_NONNULL_BEGIN /// Cached instances of Remote Config objects. @property(nonatomic, strong) NSMutableDictionary *instances; +/// Clear all the component instances from the singleton which created previously, this is for +/// testing only ++ (void)clearAllComponentInstances; + /// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. - (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace; diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m index e0adc7bccbf..81055451ae4 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m @@ -24,6 +24,31 @@ @implementation FIRRemoteConfigComponent +// Because Component now need to register two protocols (provider and interop), we need a way to +// return the same component instance for both registered protocol, this singleton pattern allow us +// to return the same component object for both registration callback. +static NSMutableDictionary *_componentInstances = nil; + ++ (FIRRemoteConfigComponent *)getComponentForApp:(FIRApp *)app { + @synchronized(_componentInstances) { + // need to init the dictionary first + if (!_componentInstances) { + _componentInstances = [[NSMutableDictionary alloc] init]; + } + if (![_componentInstances objectForKey:app.name]) { + _componentInstances[app.name] = [[self alloc] initWithApp:app]; + } + return _componentInstances[app.name]; + } + return nil; +} + ++ (void)clearAllComponentInstances { + @synchronized(_componentInstances) { + [_componentInstances removeAllObjects]; + } +} + /// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. - (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace { if (!remoteConfigNamespace) { @@ -102,9 +127,29 @@ + (void)load { creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { // Cache the component so instances of Remote Config are cached. *isCacheable = YES; - return [[FIRRemoteConfigComponent alloc] initWithApp:container.app]; + return [FIRRemoteConfigComponent getComponentForApp:container.app]; + }]; + + // Unlike provider needs to setup a hard dependency on remote config, interop allows an optional + // dependency on RC + FIRComponent *rcInterop = [FIRComponent + componentWithProtocol:@protocol(FIRRemoteConfigInterop) + instantiationTiming:FIRInstantiationTimingAlwaysEager + dependencies:@[] + creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { + // Cache the component so instances of Remote Config are cached. + *isCacheable = YES; + return [FIRRemoteConfigComponent getComponentForApp:container.app]; }]; - return @[ rcProvider ]; + return @[ rcProvider, rcInterop ]; +} + +#pragma mark - Remote Config Interop Protocol + +- (void)registerRolloutsStateSubscriber:(id)subscriber + for:(NSString * _Nonnull)namespace { + FIRRemoteConfig *instance = [self remoteConfigForNamespace:namespace]; + [instance addRemoteConfigInteropSubscriber:subscriber]; } @end diff --git a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h index ef7def6fd9d..4420dcb2679 100644 --- a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h +++ b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h @@ -23,6 +23,7 @@ @class RCNConfigFetch; @class RCNConfigRealtime; @protocol FIRAnalyticsInterop; +@protocol FIRRolloutsStateSubscriber; NS_ASSUME_NONNULL_BEGIN @@ -78,6 +79,9 @@ NS_ASSUME_NONNULL_BEGIN configContent:(RCNConfigContent *)configContent analytics:(nullable id)analytics; +/// Register RolloutsStateSubcriber to FIRRemoteConfig instance +- (void)addRemoteConfigInteropSubscriber:(id _Nonnull)subscriber; + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h index 987f3a98225..36fb8e7435f 100644 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -79,8 +79,10 @@ @property(nonatomic, readwrite, assign) NSString *lastETag; /// The timestamp of the last eTag update. @property(nonatomic, readwrite, assign) NSTimeInterval lastETagUpdateTime; -// Last fetched template version. -@property(nonatomic, readwrite, assign) NSString *lastTemplateVersion; +/// Last fetched template version. +@property(nonatomic, readwrite, assign) NSString *lastFetchedTemplateVersion; +/// Last active template version. +@property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion; #pragma mark Throttling properties @@ -134,6 +136,9 @@ /// indicates a server issue. - (void)updateRealtimeExponentialBackoffTime; +/// Update last active template version from last fetched template version. +- (void)updateLastActiveTemplateVersion; + /// Returns the difference between the Realtime backoff end time and the current time in a /// NSTimeInterval format. - (NSTimeInterval)getRealtimeBackoffInterval; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h index db0e0213ae1..51d248c4106 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -37,6 +37,14 @@ static NSString *const RCNFetchResponseKeyEntries = @"entries"; static NSString *const RCNFetchResponseKeyExperimentDescriptions = @"experimentDescriptions"; /// Key that includes data for Personalization metadata. static NSString *const RCNFetchResponseKeyPersonalizationMetadata = @"personalizationMetadata"; +/// Key that includes data for Rollout metadata. +static NSString *const RCNFetchResponseKeyRolloutMetadata = @"rolloutMetadata"; +/// Key that indicates rollout id in Rollout metadata. +static NSString *const RCNFetchResponseKeyRolloutID = @"rolloutId"; +/// Key that indicates variant id in Rollout metadata. +static NSString *const RCNFetchResponseKeyVariantID = @"variantId"; +/// Key that indicates affected parameter keys in Rollout Metadata. +static NSString *const RCNFetchResponseKeyAffectedParameterKeys = @"affectedParameterKeys"; /// Error key. static NSString *const RCNFetchResponseKeyError = @"error"; /// Error code. @@ -58,5 +66,7 @@ static NSString *const RCNFetchResponseKeyStateNoTemplate = @"NO_TEMPLATE"; static NSString *const RCNFetchResponseKeyStateNoChange = @"NO_CHANGE"; /// Template found, but evaluates to empty (e.g. all keys omitted). static NSString *const RCNFetchResponseKeyStateEmptyConfig = @"EMPTY_CONFIG"; -/// Template Version key +/// Fetched Template Version key static NSString *const RCNFetchResponseKeyTemplateVersion = @"templateVersion"; +/// Active Template Version key +static NSString *const RCNActiveKeyTemplateVersion = @"activeTemplateVersion"; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index 34d0895243a..e8410074b30 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -39,6 +39,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { @property(nonatomic, readonly, copy) NSDictionary *activeConfig; /// Local default config that is provided by external users; @property(nonatomic, readonly, copy) NSDictionary *defaultConfig; +/// Active Rollout metadata that is currently used. +@property(nonatomic, readonly, copy) NSArray *activeRolloutMetadata; - (instancetype)init NS_UNAVAILABLE; @@ -65,6 +67,9 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { /// Gets the active config and Personalization metadata. - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; +/// Sets the fetched rollout metadata to active with a success completion handler. +- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler; + /// Returns the updated parameters between fetched and active config. - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 4f55a2e9274..1c266734c40 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -38,6 +38,10 @@ @implementation RCNConfigContent { /// Pending Personalization metadata that is latest data from server that might or might not be /// applied. NSDictionary *_fetchedPersonalization; + /// Active Rollout metadata that is currently used. + NSArray *_activeRolloutMetadata; + /// Pending Rollout metadata that is latest data from server that might or might not be applied. + NSArray *_fetchedRolloutMetadata; /// DBManager RCNConfigDBManager *_DBManager; /// Current bundle identifier; @@ -80,6 +84,8 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager { _defaultConfig = [[NSMutableDictionary alloc] init]; _activePersonalization = [[NSDictionary alloc] init]; _fetchedPersonalization = [[NSDictionary alloc] init]; + _activeRolloutMetadata = [[NSArray alloc] init]; + _fetchedRolloutMetadata = [[NSArray alloc] init]; _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; if (!_bundleIdentifier) { FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038", @@ -115,25 +121,30 @@ - (void)loadConfigFromMainTable { _isDatabaseLoadAlreadyInitiated = true; dispatch_group_enter(_dispatch_group); - [_DBManager - loadMainWithBundleIdentifier:_bundleIdentifier - completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { - self->_fetchedConfig = [fetchedConfig mutableCopy]; - self->_activeConfig = [activeConfig mutableCopy]; - self->_defaultConfig = [defaultConfig mutableCopy]; - dispatch_group_leave(self->_dispatch_group); - }]; + [_DBManager loadMainWithBundleIdentifier:_bundleIdentifier + completionHandler:^( + BOOL success, NSDictionary *fetchedConfig, NSDictionary *activeConfig, + NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) { + self->_fetchedConfig = [fetchedConfig mutableCopy]; + self->_activeConfig = [activeConfig mutableCopy]; + self->_defaultConfig = [defaultConfig mutableCopy]; + self->_fetchedRolloutMetadata = + [rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata] copy]; + self->_activeRolloutMetadata = + [rolloutMetadata[@RCNRolloutTableKeyActiveMetadata] copy]; + dispatch_group_leave(self->_dispatch_group); + }]; // TODO(karenzeng): Refactor personalization to be returned in loadMainWithBundleIdentifier above dispatch_group_enter(_dispatch_group); - [_DBManager loadPersonalizationWithCompletionHandler:^( - BOOL success, NSDictionary *fetchedPersonalization, - NSDictionary *activePersonalization, NSDictionary *defaultConfig) { - self->_fetchedPersonalization = [fetchedPersonalization copy]; - self->_activePersonalization = [activePersonalization copy]; - dispatch_group_leave(self->_dispatch_group); - }]; + [_DBManager + loadPersonalizationWithCompletionHandler:^( + BOOL success, NSDictionary *fetchedPersonalization, NSDictionary *activePersonalization, + NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) { + self->_fetchedPersonalization = [fetchedPersonalization copy]; + self->_activePersonalization = [activePersonalization copy]; + dispatch_group_leave(self->_dispatch_group); + }]; } /// Update the current config result to main table. @@ -269,6 +280,7 @@ - (void)updateConfigContentWithResponse:(NSDictionary *)response [self handleUpdateStateForConfigNamespace:currentNamespace withEntries:response[RCNFetchResponseKeyEntries]]; [self handleUpdatePersonalization:response[RCNFetchResponseKeyPersonalizationMetadata]]; + [self handleUpdateRolloutFetchedMetadata:response[RCNFetchResponseKeyRolloutMetadata]]; return; } } @@ -279,6 +291,15 @@ - (void)activatePersonalization { fromSource:RCNDBSourceActive]; } +- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler { + _activeRolloutMetadata = _fetchedRolloutMetadata; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata + value:_activeRolloutMetadata + completionHandler:^(BOOL success, NSDictionary *result) { + completionHandler(success); + }]; +} + #pragma mark State handling - (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace { if (!_fetchedConfig[currentNamespace]) { @@ -342,6 +363,16 @@ - (void)handleUpdatePersonalization:(NSDictionary *)metadata { [_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched]; } +- (void)handleUpdateRolloutFetchedMetadata:(NSArray *)metadata { + if (!metadata) { + metadata = [[NSArray alloc] init]; + } + _fetchedRolloutMetadata = metadata; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:metadata + completionHandler:nil]; +} + #pragma mark - getter/setter - (NSDictionary *)fetchedConfig { /// If this is the first time reading the fetchedConfig, we might still be reading it from the @@ -369,6 +400,11 @@ - (NSDictionary *)activePersonalization { return _activePersonalization; } +- (NSArray *)activeRolloutMetadata { + [self checkAndWaitForInitialDatabaseLoad]; + return _activeRolloutMetadata; +} + - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace { /// If this is the first time reading the active metadata, we might still be reading it from the /// database. @@ -411,6 +447,8 @@ - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace _activeConfig[FIRNamespace] ? _activeConfig[FIRNamespace] : [[NSDictionary alloc] init]; NSDictionary *fetchedP13n = _fetchedPersonalization; NSDictionary *activeP13n = _activePersonalization; + NSArray *fetchedRolloutMetadata = _fetchedRolloutMetadata; + NSArray *activeRolloutMetadata = _activeRolloutMetadata; // add new/updated params for (NSString *key in [fetchedConfig allKeys]) { @@ -439,8 +477,50 @@ - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace } } + NSDictionary *fetchedRollouts = + [self getParameterKeyToRolloutMetadata:fetchedRolloutMetadata]; + NSDictionary *activeRollouts = + [self getParameterKeyToRolloutMetadata:activeRolloutMetadata]; + + // add params with new/updated rollout metadata + for (NSString *key in [fetchedRollouts allKeys]) { + if (activeRollouts[key] == nil || + ![activeRollouts[key] isEqualToDictionary:fetchedRollouts[key]]) { + [updatedKeys addObject:key]; + } + } + // add params with deleted rollout metadata + for (NSString *key in [activeRollouts allKeys]) { + if (fetchedRollouts[key] == nil) { + [updatedKeys addObject:key]; + } + } + configUpdate = [[FIRRemoteConfigUpdate alloc] initWithUpdatedKeys:updatedKeys]; return configUpdate; } +- (NSDictionary *)getParameterKeyToRolloutMetadata: + (NSArray *)rolloutMetadata { + NSMutableDictionary *result = + [[NSMutableDictionary alloc] init]; + for (NSDictionary *metadata in rolloutMetadata) { + NSString *rolloutId = metadata[RCNFetchResponseKeyRolloutID]; + NSString *variantId = metadata[RCNFetchResponseKeyVariantID]; + NSArray *affectedKeys = metadata[RCNFetchResponseKeyAffectedParameterKeys]; + if (rolloutId && variantId && affectedKeys) { + for (NSString *key in affectedKeys) { + if (result[key]) { + NSMutableDictionary *rolloutIdToVariantId = result[key]; + [rolloutIdToVariantId setValue:variantId forKey:rolloutId]; + } else { + NSMutableDictionary *rolloutIdToVariantId = [@{rolloutId : variantId} mutableCopy]; + [result setValue:rolloutIdToVariantId forKey:key]; + } + } + } + } + return [result copy]; +} + @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h index 39c3e213b73..fba094624ca 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h @@ -53,10 +53,12 @@ typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result); /// @param fetchedConfig Return fetchedConfig loaded from DB /// @param activeConfig Return activeConfig loaded from DB /// @param defaultConfig Return defaultConfig loaded from DB +/// @param rolloutMetadata Return fetched and active RolloutMetadata loaded from DB typedef void (^RCNDBLoadCompletion)(BOOL success, NSDictionary *fetchedConfig, NSDictionary *activeConfig, - NSDictionary *defaultConfig); + NSDictionary *defaultConfig, + NSDictionary *rolloutMetadata); /// Returns the current version of the Remote Config database. + (NSString *)remoteConfigPathForDatabase; @@ -78,7 +80,6 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Load Personalization from table. /// @param handler The callback when reading from DB is complete. - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler; - /// Insert a record in metadata table. /// @param columnNameToValue The column name and its value to be inserted in metadata table. /// @param handler The callback. @@ -110,6 +111,15 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Insert or update the data in Personalization config. - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)metadata fromSource:(RCNDBSource)source; +/// Insert rollout metadata in rollout table. +/// @param key Key indicating whether rollout metadata is fetched or active and defined in +/// RCNConfigDefines.h. +/// @param metadataList The metadata info for each rollout entry . +/// @param handler The callback. +- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)metadataList + completionHandler:(RCNDBCompletion)handler; + /// Clear the record of given namespace and package name /// before updating the table. - (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index 6550760c16b..5b21306a85a 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -31,6 +31,7 @@ #define RCNTableNameInternalMetadata "internal_metadata" #define RCNTableNameExperiment "experiment" #define RCNTableNamePersonalization "personalization" +#define RCNTableNameRollout "rollout" static BOOL gIsNewDatabase; /// SQLite file name in versions 0, 1 and 2. @@ -284,11 +285,14 @@ - (BOOL)createTableSchema { "create TABLE IF NOT EXISTS " RCNTableNamePersonalization " (_id INTEGER PRIMARY KEY, key INTEGER, value BLOB)"; + static const char *createTableRollout = "create TABLE IF NOT EXISTS " RCNTableNameRollout + " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; + return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] && [self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] && [self executeQuery:createTableInternalMetadata] && [self executeQuery:createTableExperiment] && - [self executeQuery:createTablePersonalization]; + [self executeQuery:createTablePersonalization] && [self executeQuery:createTableRollout]; } - (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path { @@ -618,6 +622,52 @@ - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue return YES; } +- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)metadataList + completionHandler:(RCNDBCompletion)handler { + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [self insertOrUpdateRolloutTableWithKey:key value:metadataList]; + if (handler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertOrUpdateRolloutTableWithKey:(NSString *)key + value:(NSArray *)arrayValue { + RCN_MUST_NOT_BE_MAIN_THREAD(); + NSError *error; + NSData *dataValue = [NSJSONSerialization dataWithJSONObject:arrayValue + options:NSJSONWritingPrettyPrinted + error:&error]; + const char *SQL = + "INSERT OR REPLACE INTO " RCNTableNameRollout + " (_id, key, value) values ((SELECT _id from " RCNTableNameRollout " WHERE key = ?), ?, ?)"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + if (![self bindStringToStatement:statement index:1 string:key]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (![self bindStringToStatement:statement index:2 string:key]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + #pragma mark - update - (void)updateMetadataWithOption:(RCNUpdateOption)option @@ -852,7 +902,6 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { - (NSMutableArray *)loadExperimentTableFromKey:(NSString *)key { RCN_MUST_NOT_BE_MAIN_THREAD(); - NSMutableArray *results = [[NSMutableArray alloc] init]; const char *SQL = "SELECT value FROM " RCNTableNameExperiment " WHERE key = ?"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { @@ -861,12 +910,49 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { NSArray *params = @[ key ]; [self bindStringsToStatement:statement stringArray:params]; - NSData *experimentData; + NSMutableArray *results = [self loadValuesFromStatement:statement]; + return results; +} + +- (NSArray *)loadRolloutTableFromKey:(NSString *)key { + RCN_MUST_NOT_BE_MAIN_THREAD(); + const char *SQL = "SELECT value FROM " RCNTableNameRollout " WHERE key = ?"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + NSArray *params = @[ key ]; + [self bindStringsToStatement:statement stringArray:params]; + NSMutableArray *results = [self loadValuesFromStatement:statement]; + // There should be only one entry in this table. + if (results.count != 1) { + return nil; + } + NSArray *rollout; + // Convert from NSData to NSArray + if (results[0]) { + NSError *error; + rollout = [NSJSONSerialization JSONObjectWithData:results[0] options:0 error:&error]; + if (!rollout) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011", + @"Failed to convert NSData to NSAarry for Rollout Metadata with error %@.", + error); + } + } + if (!rollout) { + rollout = [[NSArray alloc] init]; + } + return rollout; +} + +- (NSMutableArray *)loadValuesFromStatement:(sqlite3_stmt *)statement { + NSMutableArray *results = [[NSMutableArray alloc] init]; + NSData *value; while (sqlite3_step(statement) == SQLITE_ROW) { - experimentData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) - length:sqlite3_column_bytes(statement, 0)]; - if (experimentData) { - [results addObject:experimentData]; + value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) + length:sqlite3_column_bytes(statement, 0)]; + if (value) { + [results addObject:value]; } } @@ -880,7 +966,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil); + handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil, nil); }); return; } @@ -913,7 +999,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(YES, fetchedPersonalization, activePersonalization, nil); + handler(YES, fetchedPersonalization, activePersonalization, nil, nil); }); } }); @@ -987,7 +1073,7 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new]); + handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new], [NSDictionary new]); }); return; } @@ -1000,12 +1086,26 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier __block NSDictionary *defaultConfig = [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier fromSource:RCNDBSourceDefault]; + + __block NSArray *fetchedRolloutMetadata = + [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyFetchedMetadata]; + __block NSArray *activeRolloutMetadata = + [strongSelf loadRolloutTableFromKey:@RCNRolloutTableKeyActiveMetadata]; + if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ fetchedConfig = fetchedConfig ? fetchedConfig : [[NSDictionary alloc] init]; activeConfig = activeConfig ? activeConfig : [[NSDictionary alloc] init]; defaultConfig = defaultConfig ? defaultConfig : [[NSDictionary alloc] init]; - handler(YES, fetchedConfig, activeConfig, defaultConfig); + fetchedRolloutMetadata = + fetchedRolloutMetadata ? fetchedRolloutMetadata : [[NSArray alloc] init]; + activeRolloutMetadata = + activeRolloutMetadata ? activeRolloutMetadata : [[NSArray alloc] init]; + NSDictionary *rolloutMetadata = @{ + @RCNRolloutTableKeyActiveMetadata : [activeRolloutMetadata copy], + @RCNRolloutTableKeyFetchedMetadata : [fetchedRolloutMetadata copy] + }; + handler(YES, fetchedConfig, activeConfig, defaultConfig, rolloutMetadata); }); } }); diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h index cf08f738105..1e95373541b 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h @@ -31,5 +31,7 @@ #define RCNExperimentTableKeyPayload "experiment_payload" #define RCNExperimentTableKeyMetadata "experiment_metadata" #define RCNExperimentTableKeyActivePayload "experiment_active_payload" +#define RCNRolloutTableKeyActiveMetadata "active_rollout_metadata" +#define RCNRolloutTableKeyFetchedMetadata "fetched_rollout_metadata" #endif diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index c3a0f16ddd8..dbc4b9bec56 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -25,6 +25,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNDevice.h" +@import FirebaseRemoteConfigInterop; #ifdef RCN_STAGING_SERVER static NSString *const kServerURLDomain = @@ -105,7 +106,7 @@ - (instancetype)initWithContent:(RCNConfigContent *)content _content = content; _fetchSession = [self newFetchSession]; _options = options; - _templateVersionNumber = [self->_settings lastTemplateVersion]; + _templateVersionNumber = [self->_settings lastFetchedTemplateVersion]; } return self; } @@ -572,7 +573,7 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; - if ([namespace isEqualToString:FIRNamespaceGoogleMobilePlatform]) { + if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) { [strongSelf->_experiment updateExperimentsWithResponse: fetchedConfig[RCNFetchResponseKeyExperimentDescriptions]]; } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 0b3e3ad1164..e85a63f4873 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -110,7 +110,8 @@ - (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager } _isFetchInProgress = NO; - _lastTemplateVersion = [_userDefaultsManager lastTemplateVersion]; + _lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion]; + _lastActiveTemplateVersion = [_userDefaultsManager lastActiveTemplateVersion]; _realtimeExponentialBackoffRetryInterval = [_userDefaultsManager currentRealtimeThrottlingRetryIntervalSeconds]; _realtimeExponentialBackoffThrottleEndTime = [_userDefaultsManager realtimeThrottleEndTime]; @@ -292,7 +293,8 @@ - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; // Note: We expect the googleAppID to always be available. _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID); - [_userDefaultsManager setLastTemplateVersion:templateVersion]; + _lastFetchedTemplateVersion = templateVersion; + [_userDefaultsManager setLastFetchedTemplateVersion:templateVersion]; } [self updateMetadataTable]; @@ -377,6 +379,11 @@ - (void)updateMetadataTable { [_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:nil]; } +- (void)updateLastActiveTemplateVersion { + _lastActiveTemplateVersion = _lastFetchedTemplateVersion; + [_userDefaultsManager setLastActiveTemplateVersion:_lastActiveTemplateVersion]; +} + #pragma mark - fetch request /// Returns a fetch request with the latest device and config change. diff --git a/FirebaseRemoteConfig/Sources/RCNConstants3P.m b/FirebaseRemoteConfig/Sources/RCNConstants3P.m index 6bd5d78d094..e64295be62c 100644 --- a/FirebaseRemoteConfig/Sources/RCNConstants3P.m +++ b/FirebaseRemoteConfig/Sources/RCNConstants3P.m @@ -17,4 +17,5 @@ #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" /// Firebase Remote Config service default namespace. +/// TODO(doudounan): Change to use this namespace defined in RemoteConfigInterop. NSString *const FIRNamespaceGoogleMobilePlatform = @"firebase"; diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h index acbcd5842f4..b235f217d81 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -44,7 +44,9 @@ NS_ASSUME_NONNULL_BEGIN /// Realtime retry count. @property(nonatomic, assign) int realtimeRetryCount; /// Last fetched template version. -@property(nonatomic, assign) NSString *lastTemplateVersion; +@property(nonatomic, assign) NSString *lastFetchedTemplateVersion; +/// Last active template version. +@property(nonatomic, assign) NSString *lastActiveTemplateVersion; /// Designated initializer. - (instancetype)initWithAppName:(NSString *)appName diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m index 29ec2e87a06..880a2157fe1 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m @@ -111,7 +111,7 @@ - (void)setLastETag:(NSString *)lastETag { } } -- (NSString *)lastTemplateVersion { +- (NSString *)lastFetchedTemplateVersion { NSDictionary *userDefaults = [self instanceUserDefaults]; if ([userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion]) { return [userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion]; @@ -120,12 +120,27 @@ - (NSString *)lastTemplateVersion { return @"0"; } -- (void)setLastTemplateVersion:(NSString *)templateVersion { +- (void)setLastFetchedTemplateVersion:(NSString *)templateVersion { if (templateVersion) { [self setInstanceUserDefaultsValue:templateVersion forKey:RCNFetchResponseKeyTemplateVersion]; } } +- (NSString *)lastActiveTemplateVersion { + NSDictionary *userDefaults = [self instanceUserDefaults]; + if ([userDefaults objectForKey:RCNActiveKeyTemplateVersion]) { + return [userDefaults objectForKey:RCNActiveKeyTemplateVersion]; + } + + return @"0"; +} + +- (void)setLastActiveTemplateVersion:(NSString *)templateVersion { + if (templateVersion) { + [self setInstanceUserDefaultsValue:templateVersion forKey:RCNActiveKeyTemplateVersion]; + } +} + - (NSTimeInterval)lastETagUpdateTime { NSNumber *lastETagUpdateTime = [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime]; diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..d7f955d7c07 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp.xcodeproj/project.pbxproj @@ -0,0 +1,1059 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 61A5654706089C41A7398CF3 /* Pods_FeatureRolloutsTestApp_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FE5945D035CAAB3297D2CAC /* Pods_FeatureRolloutsTestApp_iOS.framework */; }; + 848D345C8969AF72BCC0E2E4 /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1729B0ED2CACB9C5A62A6F8C /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework */; }; + 951D70152B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */; }; + 951D70162B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */; }; + AD11C57C978D52894BFDC47F /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E60230146BDE14D306856CB /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework */; }; + C427C4A32B4603F60088A488 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C427C4A52B4603F60088A488 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A42B4603F60088A488 /* ContentView.swift */; }; + C49C486C2B4704D900BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C49C48702B4704F300BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C49C487A2B4704F500BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */; }; + C49C48832B47074400BC1456 /* FirebaseCrashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C48822B47074400BC1456 /* FirebaseCrashlytics.framework */; }; + C49C48872B47075600BC1456 /* FirebaseRemoteConfig.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C48862B47075600BC1456 /* FirebaseRemoteConfig.framework */; }; + C49C488B2B47075C00BC1456 /* FirebaseCrashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C488A2B47075C00BC1456 /* FirebaseCrashlytics.framework */; }; + C49C488F2B47076200BC1456 /* FirebaseRemoteConfig.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49C488E2B47076200BC1456 /* FirebaseRemoteConfig.framework */; }; + C49C48952B47207200BC1456 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C48942B47207200BC1456 /* ContentView.swift */; }; + C49C48992B4720AE00BC1456 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C48982B4720AE00BC1456 /* ContentView.swift */; }; + C49C489C2B4720DD00BC1456 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C489B2B4720DD00BC1456 /* ContentView.swift */; }; + C49C489E2B4722C100BC1456 /* CrashButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C489D2B4722C100BC1456 /* CrashButtonView.swift */; }; + C49C489F2B47233000BC1456 /* CrashButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C489D2B4722C100BC1456 /* CrashButtonView.swift */; }; + C49C48A12B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + C49C48A22B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + C49C48A32B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + C49C48A42B47261000BC1456 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C49C48A02B47261000BC1456 /* GoogleService-Info.plist */; }; + F07A9478976524A8264259F0 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5472955122D0CE0A8A3CE4D5 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + C49C48852B47074400BC1456 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48892B47075600BC1456 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + C49C488D2B47075C00BC1456 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 025F972344BB0B489CC052D6 /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig"; sourceTree = ""; }; + 10710CAF870FA7E8D1ABF94C /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig"; sourceTree = ""; }; + 1729B0ED2CACB9C5A62A6F8C /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2842D338F32EE531C752262E /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig"; sourceTree = ""; }; + 2D15DD53784CDDE94D00AB02 /* Pods-FeatureRolloutsTestApp_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS.release.xcconfig"; sourceTree = ""; }; + 2FE5945D035CAAB3297D2CAC /* Pods_FeatureRolloutsTestApp_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 30BB126FCB8D5F53B5795500 /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig"; sourceTree = ""; }; + 4E60230146BDE14D306856CB /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5472955122D0CE0A8A3CE4D5 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6D30A6D1F2CE622B6D5D563F /* Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig"; sourceTree = ""; }; + 8BA72854B19D7A9D9BE15E1D /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig"; sourceTree = ""; }; + 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteConfigButtonView.swift; sourceTree = ""; }; + AF260B513E38B2528E7B13CC /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig"; path = "Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig"; sourceTree = ""; }; + C427C49F2B4603F60088A488 /* FeatureRolloutsTestApp_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureRolloutsTestAppApp.swift; sourceTree = ""; }; + C427C4A42B4603F60088A488 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C427C4A82B4603F80088A488 /* FeatureRolloutsTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FeatureRolloutsTestApp.entitlements; sourceTree = ""; }; + C49C48412B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_Crashlytics_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48772B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_RemoteConfig_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48812B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48822B47074400BC1456 /* FirebaseCrashlytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseCrashlytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48862B47075600BC1456 /* FirebaseRemoteConfig.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseRemoteConfig.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C488A2B47075C00BC1456 /* FirebaseCrashlytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseCrashlytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C488E2B47076200BC1456 /* FirebaseRemoteConfig.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseRemoteConfig.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C49C48942B47207200BC1456 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C49C48982B4720AE00BC1456 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C49C489B2B4720DD00BC1456 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C49C489D2B4722C100BC1456 /* CrashButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashButtonView.swift; sourceTree = ""; }; + C49C48A02B47261000BC1456 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C427C49C2B4603F60088A488 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 61A5654706089C41A7398CF3 /* Pods_FeatureRolloutsTestApp_iOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C483E2B460FC600BC1456 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F07A9478976524A8264259F0 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework in Frameworks */, + C49C48832B47074400BC1456 /* FirebaseCrashlytics.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48722B4704F300BC1456 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AD11C57C978D52894BFDC47F /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework in Frameworks */, + C49C48872B47075600BC1456 /* FirebaseRemoteConfig.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C487C2B4704F500BC1456 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C488F2B47076200BC1456 /* FirebaseRemoteConfig.framework in Frameworks */, + 848D345C8969AF72BCC0E2E4 /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework in Frameworks */, + C49C488B2B47075C00BC1456 /* FirebaseCrashlytics.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 29E7B4F9D5112B2AFBA1C6F8 /* Pods */ = { + isa = PBXGroup; + children = ( + 2842D338F32EE531C752262E /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig */, + 10710CAF870FA7E8D1ABF94C /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig */, + 025F972344BB0B489CC052D6 /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig */, + 8BA72854B19D7A9D9BE15E1D /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig */, + AF260B513E38B2528E7B13CC /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig */, + 30BB126FCB8D5F53B5795500 /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig */, + 6D30A6D1F2CE622B6D5D563F /* Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig */, + 2D15DD53784CDDE94D00AB02 /* Pods-FeatureRolloutsTestApp_iOS.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 4D9F4C8E7175D4479AD28BAC /* Frameworks */ = { + isa = PBXGroup; + children = ( + C49C488E2B47076200BC1456 /* FirebaseRemoteConfig.framework */, + C49C488A2B47075C00BC1456 /* FirebaseCrashlytics.framework */, + C49C48862B47075600BC1456 /* FirebaseRemoteConfig.framework */, + C49C48822B47074400BC1456 /* FirebaseCrashlytics.framework */, + 1729B0ED2CACB9C5A62A6F8C /* Pods_FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.framework */, + 5472955122D0CE0A8A3CE4D5 /* Pods_FeatureRolloutsTestApp_Crashlytics_iOS.framework */, + 4E60230146BDE14D306856CB /* Pods_FeatureRolloutsTestApp_RemoteConfig_iOS.framework */, + 2FE5945D035CAAB3297D2CAC /* Pods_FeatureRolloutsTestApp_iOS.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + C427C4962B4603F60088A488 = { + isa = PBXGroup; + children = ( + C49C489A2B4720C700BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */, + C49C48972B47208B00BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */, + C49C48932B47205400BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */, + C427C4A12B4603F60088A488 /* FeatureRolloutsTestApp */, + C49C486B2B47048000BC1456 /* Shared */, + C427C4A02B4603F60088A488 /* Products */, + 29E7B4F9D5112B2AFBA1C6F8 /* Pods */, + 4D9F4C8E7175D4479AD28BAC /* Frameworks */, + ); + sourceTree = ""; + }; + C427C4A02B4603F60088A488 /* Products */ = { + isa = PBXGroup; + children = ( + C427C49F2B4603F60088A488 /* FeatureRolloutsTestApp_iOS.app */, + C49C48412B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS.app */, + C49C48772B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS.app */, + C49C48812B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app */, + ); + name = Products; + sourceTree = ""; + }; + C427C4A12B4603F60088A488 /* FeatureRolloutsTestApp */ = { + isa = PBXGroup; + children = ( + C427C4A42B4603F60088A488 /* ContentView.swift */, + C427C4A82B4603F80088A488 /* FeatureRolloutsTestApp.entitlements */, + ); + path = FeatureRolloutsTestApp; + sourceTree = ""; + }; + C49C486B2B47048000BC1456 /* Shared */ = { + isa = PBXGroup; + children = ( + C49C48A02B47261000BC1456 /* GoogleService-Info.plist */, + C427C4A22B4603F60088A488 /* FeatureRolloutsTestAppApp.swift */, + 951D70142B71AD9A00BE7EED /* RemoteConfigButtonView.swift */, + C49C489D2B4722C100BC1456 /* CrashButtonView.swift */, + ); + path = Shared; + sourceTree = ""; + }; + C49C48932B47205400BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */ = { + isa = PBXGroup; + children = ( + C49C48942B47207200BC1456 /* ContentView.swift */, + ); + path = FeatureRolloutsTestApp_Crashlytics_iOS; + sourceTree = ""; + }; + C49C48972B47208B00BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */ = { + isa = PBXGroup; + children = ( + C49C48982B4720AE00BC1456 /* ContentView.swift */, + ); + path = FeatureRolloutsTestApp_RemoteConfig_iOS; + sourceTree = ""; + }; + C49C489A2B4720C700BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */ = { + isa = PBXGroup; + children = ( + C49C489B2B4720DD00BC1456 /* ContentView.swift */, + ); + path = FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C427C49E2B4603F60088A488 /* FeatureRolloutsTestApp_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C427C4C42B4603F80088A488 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_iOS" */; + buildPhases = ( + 1C5C78EEA66C3693218AA186 /* [CP] Check Pods Manifest.lock */, + C427C49B2B4603F60088A488 /* Sources */, + C427C49C2B4603F60088A488 /* Frameworks */, + C427C49D2B4603F60088A488 /* Resources */, + 3FCED6F36D70B363DD56F3FB /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_iOS; + productName = FeatureRolloutsTestApp; + productReference = C427C49F2B4603F60088A488 /* FeatureRolloutsTestApp_iOS.app */; + productType = "com.apple.product-type.application"; + }; + C49C48402B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C49C48622B460FC800BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_Crashlytics_iOS" */; + buildPhases = ( + E894A1751ED4EBA12174B475 /* [CP] Check Pods Manifest.lock */, + C49C483D2B460FC600BC1456 /* Sources */, + C49C483E2B460FC600BC1456 /* Frameworks */, + C49C483F2B460FC600BC1456 /* Resources */, + 8809A9DD2155751AF47F697B /* [CP] Embed Pods Frameworks */, + C49C48852B47074400BC1456 /* Embed Frameworks */, + C49C48A72B47285600BC1456 /* Crashlytics run script */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_Crashlytics_iOS; + productName = FeatureRolloutsTestApp_Crashlytics_iOS; + productReference = C49C48412B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS.app */; + productType = "com.apple.product-type.application"; + }; + C49C486E2B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C49C48742B4704F300BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_RemoteConfig_iOS" */; + buildPhases = ( + 2E2005BA5C63DC9C5F6B0B5C /* [CP] Check Pods Manifest.lock */, + C49C486F2B4704F300BC1456 /* Sources */, + C49C48722B4704F300BC1456 /* Frameworks */, + C49C48732B4704F300BC1456 /* Resources */, + AE72CFC82C05D96F24F22349 /* [CP] Embed Pods Frameworks */, + C49C48892B47075600BC1456 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_RemoteConfig_iOS; + productName = FeatureRolloutsTestApp_Crashlytics_iOS; + productReference = C49C48772B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS.app */; + productType = "com.apple.product-type.application"; + }; + C49C48782B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = C49C487E2B4704F500BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS" */; + buildPhases = ( + 1D1A7736C1202CF5AE3E74DF /* [CP] Check Pods Manifest.lock */, + C49C48792B4704F500BC1456 /* Sources */, + C49C487C2B4704F500BC1456 /* Frameworks */, + C49C487D2B4704F500BC1456 /* Resources */, + 1AFC789ACC0369540ADCC334 /* [CP] Embed Pods Frameworks */, + C49C488D2B47075C00BC1456 /* Embed Frameworks */, + C49C48A52B47279000BC1456 /* Crashlytics run script */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS; + productName = FeatureRolloutsTestApp_Crashlytics_iOS; + productReference = C49C48812B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C427C4972B4603F60088A488 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + C427C49E2B4603F60088A488 = { + CreatedOnToolsVersion = 15.0; + }; + C49C48402B460FC600BC1456 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = C427C49A2B4603F60088A488 /* Build configuration list for PBXProject "FeatureRolloutsTestApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C427C4962B4603F60088A488; + productRefGroup = C427C4A02B4603F60088A488 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C49C48782B4704F500BC1456 /* FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS */, + C49C48402B460FC600BC1456 /* FeatureRolloutsTestApp_Crashlytics_iOS */, + C49C486E2B4704F300BC1456 /* FeatureRolloutsTestApp_RemoteConfig_iOS */, + C427C49E2B4603F60088A488 /* FeatureRolloutsTestApp_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + C427C49D2B4603F60088A488 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A12B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C483F2B460FC600BC1456 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A22B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48732B4704F300BC1456 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A32B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C487D2B4704F500BC1456 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48A42B47261000BC1456 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1AFC789ACC0369540ADCC334 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1C5C78EEA66C3693218AA186 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 1D1A7736C1202CF5AE3E74DF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 2E2005BA5C63DC9C5F6B0B5C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3FCED6F36D70B363DD56F3FB /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_iOS/Pods-FeatureRolloutsTestApp_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8809A9DD2155751AF47F697B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_Crashlytics_iOS/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + AE72CFC82C05D96F24F22349 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS/Pods-FeatureRolloutsTestApp_RemoteConfig_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C49C48A52B47279000BC1456 /* Crashlytics run script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${BUILD_NAME}", + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "Crashlytics run script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n${PODS_ROOT}/../../../../Crashlytics/run\n"; + }; + C49C48A72B47285600BC1456 /* Crashlytics run script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${BUILD_DIR%Build/*}SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run", + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "Crashlytics run script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n${PODS_ROOT}/../../../../Crashlytics/run\n"; + }; + E894A1751ED4EBA12174B475 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FeatureRolloutsTestApp_Crashlytics_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C427C49B2B4603F60088A488 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C427C4A52B4603F60088A488 /* ContentView.swift in Sources */, + C427C4A32B4603F60088A488 /* FeatureRolloutsTestAppApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C483D2B460FC600BC1456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C489F2B47233000BC1456 /* CrashButtonView.swift in Sources */, + C49C486C2B4704D900BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */, + C49C48952B47207200BC1456 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C486F2B4704F300BC1456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C49C48702B4704F300BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */, + 951D70162B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */, + C49C48992B4720AE00BC1456 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C49C48792B4704F500BC1456 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 951D70152B71AD9B00BE7EED /* RemoteConfigButtonView.swift in Sources */, + C49C487A2B4704F500BC1456 /* FeatureRolloutsTestAppApp.swift in Sources */, + C49C489E2B4722C100BC1456 /* CrashButtonView.swift in Sources */, + C49C489C2B4720DD00BC1456 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C427C4C22B4603F80088A488 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + C427C4C32B4603F80088A488 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + C427C4C52B4603F80088A488 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6D30A6D1F2CE622B6D5D563F /* Pods-FeatureRolloutsTestApp_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C427C4C62B4603F80088A488 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D15DD53784CDDE94D00AB02 /* Pods-FeatureRolloutsTestApp_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + C49C48632B460FC800BC1456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 025F972344BB0B489CC052D6 /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C49C48642B460FC800BC1456 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8BA72854B19D7A9D9BE15E1D /* Pods-FeatureRolloutsTestApp_Crashlytics_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C49C48752B4704F300BC1456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF260B513E38B2528E7B13CC /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C49C48762B4704F300BC1456 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 30BB126FCB8D5F53B5795500 /* Pods-FeatureRolloutsTestApp_RemoteConfig_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C49C487F2B4704F500BC1456 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2842D338F32EE531C752262E /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C49C48802B4704F500BC1456 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 10710CAF870FA7E8D1ABF94C /* Pods-FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.firebase.config.featureRolloutsTestApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C427C49A2B4603F60088A488 /* Build configuration list for PBXProject "FeatureRolloutsTestApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C427C4C22B4603F80088A488 /* Debug */, + C427C4C32B4603F80088A488 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C427C4C42B4603F80088A488 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C427C4C52B4603F80088A488 /* Debug */, + C427C4C62B4603F80088A488 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C49C48622B460FC800BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_Crashlytics_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C49C48632B460FC800BC1456 /* Debug */, + C49C48642B460FC800BC1456 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C49C48742B4704F300BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_RemoteConfig_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C49C48752B4704F300BC1456 /* Debug */, + C49C48762B4704F300BC1456 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C49C487E2B4704F500BC1456 /* Build configuration list for PBXNativeTarget "FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C49C487F2B4704F500BC1456 /* Debug */, + C49C48802B4704F500BC1456 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = C427C4972B4603F60088A488 /* Project object */; +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift new file mode 100644 index 00000000000..cd875f49230 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/ContentView.swift @@ -0,0 +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. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements new file mode 100644 index 00000000000..f2ef3ae0265 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp/FeatureRolloutsTestApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift new file mode 100644 index 00000000000..ac68e43b8a8 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS/ContentView.swift @@ -0,0 +1,27 @@ +// +// 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. +// + +import Foundation +import SwiftUI + +struct ContentView: View { + var body: some View { + CrashButtonView() + .padding() + RemoteConfigButtonView() + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift new file mode 100644 index 00000000000..acb951d35a1 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_Crashlytics_iOS/ContentView.swift @@ -0,0 +1,26 @@ +// +// 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. +// + +import FirebaseCrashlytics +import Foundation +import SwiftUI + +struct ContentView: View { + var body: some View { + CrashButtonView() + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift new file mode 100644 index 00000000000..51e437b030a --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/FeatureRolloutsTestApp_RemoteConfig_iOS/ContentView.swift @@ -0,0 +1,25 @@ +// +// 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. +// + +import Foundation +import SwiftUI + +struct ContentView: View { + var body: some View { + RemoteConfigButtonView() + .padding() + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile new file mode 100644 index 00000000000..975c45eaa98 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Podfile @@ -0,0 +1,52 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +def shared_pods + pod 'FirebaseCore', :path => '../../../' + pod 'FirebaseInstallations', :path => '../../../' + pod 'FirebaseCoreInternal', :path => '../../../' + pod 'FirebaseCoreExtension', :path => '../../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../../' + pod 'FirebasePerformance', :path => '../../../' +end + +target 'FeatureRolloutsTestApp_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods +end + +target 'FeatureRolloutsTestApp_Crashlytics_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods + pod 'FirebaseCrashlytics', :path => '../../../' +end + +target 'FeatureRolloutsTestApp_RemoteConfig_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods + pod 'FirebaseRemoteConfig', :path => '../../../' +end + +target 'FeatureRolloutsTestApp_CrashlyticsRemoteConfig_iOS' do + platform :ios, '11.0' + + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + shared_pods + pod 'FirebaseCrashlytics', :path => '../../../' + pod 'FirebaseRemoteConfig', :path => '../../../' +end + diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift new file mode 100644 index 00000000000..4fb004e196c --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/CrashButtonView.swift @@ -0,0 +1,62 @@ +// +// 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. +// + +import FirebaseCrashlytics +import Foundation +import SwiftUI + +struct CrashButtonView: View { + var body: some View { + var counter = 0 + + NavigationView { + VStack( + alignment: .leading, + spacing: 10 + ) { + Button(action: { + Crashlytics.crashlytics().setUserID("ThisIsABot") + }) { + Text("Set User Id") + } + + Button(action: { + assertionFailure("Throw a Crash") + }) { + Text("Crash") + } + + Button(action: { + Crashlytics.crashlytics().record(error: NSError( + domain: "This is a test non-fatal", + code: 400 + )) + }) { + Text("Record Non-fatal event") + } + + Button(action: { + Crashlytics.crashlytics().setCustomValue(counter, forKey: "counter " + String(counter)) + let i = counter + counter = i + 1 + }) { + Text("Set custom key") + } + } + .navigationTitle("Crashlytics Example") + } + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift new file mode 100644 index 00000000000..b00e9bc6e6b --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/FeatureRolloutsTestAppApp.swift @@ -0,0 +1,31 @@ +// +// 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. +// + +import FirebaseCore +import SwiftUI + +@main +struct FeatureRolloutsTestAppApp: App { + init() { + FirebaseApp.configure() + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift new file mode 100644 index 00000000000..1391ad16e55 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared/RemoteConfigButtonView.swift @@ -0,0 +1,51 @@ +// +// 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. +// + +import FirebaseRemoteConfig +import Foundation +import SwiftUI + +struct RemoteConfigButtonView: View { + @State private var turnOnRealTimeRC = false + let rc = RemoteConfig.remoteConfig() + @RemoteConfigProperty(key: "ios_rollouts", fallback: "unfetched") var iosRollouts: String + + var body: some View { + NavigationView { + VStack( + alignment: .leading, + spacing: 10 + ) { + Button(action: { + rc.fetch() + }) { + Text("Fetch") + } + Button(action: { + rc.activate() + }) { + Text("Activate") + } + Text(iosRollouts) + Toggle("Turn on RealTime RC", isOn: $turnOnRealTimeRC).toggleStyle(.button).tint(.mint) + .onChange(of: self.turnOnRealTimeRC, perform: { value in + rc.addOnConfigUpdateListener { u, e in rc.activate() } + }) + } + .navigationTitle("Remote Config Example") + } + } +} diff --git a/FirebaseRemoteConfig/Tests/Sample/Podfile b/FirebaseRemoteConfig/Tests/Sample/Podfile index 961df70b58e..bfff53e6fea 100644 --- a/FirebaseRemoteConfig/Tests/Sample/Podfile +++ b/FirebaseRemoteConfig/Tests/Sample/Podfile @@ -14,6 +14,7 @@ target 'RemoteConfigSampleApp' do pod 'FirebaseInstallations', :path => '../../../' pod 'FirebaseRemoteConfig', :path => '../../../' pod 'FirebaseABTesting', :path => '../../..' + pod 'FirebaseRemoteConfigInterop', :path => '../../..' # Pods for RemoteConfigSampleApp diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m index 57c766a9035..e57cc930ea2 100644 --- a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m @@ -21,6 +21,7 @@ #import #import "../../../Sources/Private/FIRRemoteConfig_Private.h" #import "FRCLog.h" +@import FirebaseRemoteConfigInterop; static NSString *const FIRPerfNamespace = @"fireperf"; static NSString *const FIRDefaultFIRAppName = @"__FIRAPP_DEFAULT"; @@ -81,7 +82,8 @@ - (void)viewDidLoad { // TODO(mandard): Add support for deleting and adding namespaces in the app. self.namespacePickerData = - [[NSArray alloc] initWithObjects:FIRNamespaceGoogleMobilePlatform, FIRPerfNamespace, nil]; + [[NSArray alloc] initWithObjects:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform, + FIRPerfNamespace, nil]; self.appPickerData = [[NSArray alloc] initWithObjects:FIRDefaultFIRAppName, FIRSecondFIRAppName, nil]; self.RCInstances = [[NSMutableDictionary alloc] init]; @@ -91,7 +93,8 @@ - (void)viewDidLoad { if (!self.RCInstances[namespaceString]) { self.RCInstances[namespaceString] = [[NSMutableDictionary alloc] init]; } - if ([namespaceString isEqualToString:FIRNamespaceGoogleMobilePlatform] && + if ([namespaceString + isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform] && [appString isEqualToString:FIRDefaultFIRAppName]) { self.RCInstances[namespaceString][appString] = [FIRRemoteConfig remoteConfig]; } else { @@ -120,7 +123,7 @@ - (void)viewDidLoad { [alert addAction:defaultAction]; // Add realtime listener for firebase namespace - [self.RCInstances[FIRNamespaceGoogleMobilePlatform][FIRDefaultFIRAppName] + [self.RCInstances[FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform][FIRDefaultFIRAppName] addOnConfigUpdateListener:^(FIRRemoteConfigUpdate *_Nullable update, NSError *_Nullable error) { if (error != nil) { diff --git a/FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift b/FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift new file mode 100644 index 00000000000..d4610a03d65 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftUnit/RemoteConfigInteropTests.swift @@ -0,0 +1,65 @@ +// Copyright 2023 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. + +import FirebaseRemoteConfigInterop +import XCTest + +class MockRCInterop: RemoteConfigInterop { + weak var subscriber: FirebaseRemoteConfigInterop.RolloutsStateSubscriber? + func registerRolloutsStateSubscriber(_ subscriber: FirebaseRemoteConfigInterop + .RolloutsStateSubscriber, + for namespace: String) { + self.subscriber = subscriber + } +} + +class MockRolloutSubscriber: RolloutsStateSubscriber { + var isSubscriberCalled = false + var rolloutsState: RolloutsState? + func rolloutsStateDidChange(_ rolloutsState: FirebaseRemoteConfigInterop.RolloutsState) { + isSubscriberCalled = true + self.rolloutsState = rolloutsState + } +} + +final class RemoteConfigInteropTests: XCTestCase { + let rollouts: RolloutsState = { + let assignment1 = RolloutAssignment( + rolloutId: "rollout_1", + variantId: "control", + templateVersion: 1, + parameterKey: "my_feature", + parameterValue: "false" + ) + let assignment2 = RolloutAssignment( + rolloutId: "rollout_2", + variantId: "enabled", + templateVersion: 123, + parameterKey: "themis_big_feature", + parameterValue: "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ) + let rollouts = RolloutsState(assignmentList: [assignment1, assignment2]) + return rollouts + }() + + func testRemoteConfigIntegration() throws { + let rcSubscriber = MockRolloutSubscriber() + let rcInterop = MockRCInterop() + rcInterop.registerRolloutsStateSubscriber(rcSubscriber, for: "namespace") + rcInterop.subscriber?.rolloutsStateDidChange(rollouts) + + XCTAssertTrue(rcSubscriber.isSubscriberCalled) + XCTAssertEqual(rcSubscriber.rolloutsState?.assignments.count, 2) + } +} diff --git a/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m b/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m index 077702b7b19..52d56bb3852 100644 --- a/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m @@ -20,6 +20,7 @@ #import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h" #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; @interface FIRRemoteConfigComponentTest : XCTestCase @end @@ -31,6 +32,7 @@ - (void)tearDown { // Clear out any apps that were called with `configure`. [FIRApp resetApps]; + [FIRRemoteConfigComponent clearAllComponentInstances]; } - (void)testRCInstanceCreationAndCaching { @@ -92,7 +94,8 @@ - (void)testInitialization { } - (void)testRegistersAsLibrary { - XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 1); + // Now component has two register, one is provider and another one is Interop + XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 2); // Configure a test FIRApp for fetching an instance of the FIRRemoteConfigProvider. NSString *appName = [self generatedTestAppName]; @@ -101,12 +104,50 @@ - (void)testRegistersAsLibrary { // Attempt to fetch the component and verify it's a valid instance. id provider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + id interop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container); XCTAssertNotNil(provider); + XCTAssertNotNil(interop); // Ensure that the instance that comes from the container is cached. id sameProvider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + id sameInterop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container); XCTAssertNotNil(sameProvider); + XCTAssertNotNil(sameInterop); XCTAssertEqual(provider, sameProvider); + XCTAssertEqual(interop, sameInterop); + + // Dynamic typing, both prototols are refering to the same component instance + id providerID = provider; + id interopID = interop; + XCTAssertEqualObjects(providerID, interopID); +} + +- (void)testTwoAppsCreateTwoComponents { + NSString *appName = [self generatedTestAppName]; + [FIRApp configureWithName:appName options:[self fakeOptions]]; + FIRApp *app = [FIRApp appNamed:appName]; + + [FIRApp configureWithOptions:[self fakeOptions]]; + FIRApp *defaultApp = [FIRApp defaultApp]; + XCTAssertNotNil(defaultApp); + XCTAssertNotEqualObjects(app, defaultApp); + + id provider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + id interop = FIR_COMPONENT(FIRRemoteConfigInterop, app.container); + id defaultAppProvider = + FIR_COMPONENT(FIRRemoteConfigProvider, defaultApp.container); + id defaultAppInterop = + FIR_COMPONENT(FIRRemoteConfigInterop, defaultApp.container); + + id providerID = provider; + id interopID = interop; + id defaultAppProviderID = defaultAppProvider; + id defaultAppInteropID = defaultAppInterop; + + XCTAssertEqualObjects(providerID, interopID); + XCTAssertEqualObjects(defaultAppProviderID, defaultAppInteropID); + // Check two apps get their own component to register + XCTAssertNotEqualObjects(interopID, defaultAppInteropID); } - (void)testThrowsWithEmptyGoogleAppID { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index d4f33bf0f71..e02f22f2454 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -24,6 +24,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; @interface RCNConfigContent (Testing) - (BOOL)checkAndWaitForInitialDatabaseLoad; @@ -44,7 +45,7 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justSmallDelay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.isLoadMainCompleted = YES; - handler(YES, nil, nil, nil); + handler(YES, nil, nil, nil, nil); }); } - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { @@ -53,7 +54,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justOtherSmallDelay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.isLoadPersonalizationCompleted = YES; - handler(YES, nil, nil, nil); + handler(YES, nil, nil, nil, nil); }); } @end @@ -62,6 +63,7 @@ @interface RCNConfigContentTest : XCTestCase { NSTimeInterval _expectationTimeout; RCNConfigContent *_configContent; NSString *namespaceApp1, *namespaceApp2; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -70,11 +72,12 @@ @implementation RCNConfigContentTest - (void)setUp { [super setUp]; _expectationTimeout = 1.0; + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; namespaceApp1 = [NSString - stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName]; + stringWithFormat:@"%@:%@", _namespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName]; namespaceApp2 = [NSString - stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName]; + stringWithFormat:@"%@:%@", _namespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName]; _configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; @@ -129,14 +132,14 @@ - (void)testUpdateConfigContentWithResponse { NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; NSDictionary *fetchedConfig = _configContent.fetchedConfig; - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"] stringValue], @"value1"); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"] stringValue], @"value2"); } @@ -147,20 +150,20 @@ - (void)testUpdateConfigContentWithStatusUpdateWithDifferentKeys { NSDictionary *entries = @{@"key1" : @"value1"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; configToSet = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; entries = @{@"key2" : @"value2", @"key3" : @"value3"}; [configToSet setValue:entries forKey:@"entries"]; [_configContent updateConfigContentWithResponse:configToSet - forNamespace:FIRNamespaceGoogleMobilePlatform]; + forNamespace:_namespaceGoogleMobilePlatform]; NSDictionary *fetchedConfig = _configContent.fetchedConfig; - XCTAssertNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + XCTAssertNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key1"]); + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key2"] stringValue], @"value2"); - XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"]); - XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"] stringValue], + XCTAssertNotNil(fetchedConfig[_namespaceGoogleMobilePlatform][@"key3"]); + XCTAssertEqualObjects([fetchedConfig[_namespaceGoogleMobilePlatform][@"key3"] stringValue], @"value3"); } @@ -332,7 +335,9 @@ - (void)testConfigUpdate_noChange_emptyResponse { // populate fetched config NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // active config is the same as fetched config @@ -365,7 +370,8 @@ - (void)testConfigUpdate_paramAdded_returnsNewKey { // fetch response has new param NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1", newParam : @"value2"} - p13nMetadata:nil]; + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -391,7 +397,9 @@ - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey { // fetch response contains updated value NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -417,7 +425,9 @@ - (void)testConfigUpdate_paramDeleted_returnsDeletedKey { // fetch response does not contain existing param NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{newParam : value1} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -437,7 +447,8 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { // popuate fetched config NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{existingParam : value1} - p13nMetadata:@{existingParam : oldMetadata}]; + p13nMetadata:@{existingParam : oldMetadata} + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content @@ -461,6 +472,148 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); } +- (void)testConfigUpdate_rolloutMetadataUpdated_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key1 = @"key1"; + NSString *key2 = @"kety2"; + NSString *value = @"value"; + NSString *rolloutId1 = @"1"; + NSString *rolloutId2 = @"2"; + NSString *variantId1 = @"A"; + NSString *variantId2 = @"B"; + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + } ]; + // Update rolltou metadata + NSArray *updatedRolloutMetadata = @[ + @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId2, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + }, + @{ + RCNFetchResponseKeyRolloutID : rolloutId2, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key2 ] + }, + ]; + // Populate fetched config + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key1 : value} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key1 : rcValue}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + // New fetch response has updated rollout metadata + [fetchResponse setValue:updatedRolloutMetadata forKey:RCNFetchResponseKeyRolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + + XCTAssertTrue([update updatedKeys].count == 2); + XCTAssertTrue([[update updatedKeys] containsObject:key1]); + XCTAssertTrue([[update updatedKeys] containsObject:key2]); +} + +- (void)testConfigUpdate_rolloutMetadataDeleted_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key1 = @"key1"; + NSString *key2 = @"key2"; + NSString *value = @"value"; + NSString *rolloutId1 = @"1"; + NSString *variantId1 = @"A"; + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1, key2 ] + } ]; + // Remove key2 from rollout metadata + NSArray *updatedRolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key1 ] + } ]; + // Populate fetched config + NSMutableDictionary *fetchResponse = + [self createFetchResponseWithConfigEntries:@{key1 : value, key2 : value} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key1 : rcValue, key2 : rcValue}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + // New fetch response has updated rollout metadata + [fetchResponse setValue:updatedRolloutMetadata forKey:RCNFetchResponseKeyRolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:key2]); +} + +- (void)testConfigUpdate_rolloutMetadataDeletedAll_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key = @"key"; + NSString *value = @"value"; + NSString *rolloutId1 = @"1"; + NSString *variantId1 = @"A"; + NSArray *rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : rolloutId1, + RCNFetchResponseKeyVariantID : variantId1, + RCNFetchResponseKeyAffectedParameterKeys : @[ key ] + } ]; + // Populate fetched config + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key : value} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + [_configContent activateRolloutMetadata:nil]; + XCTAssertEqualObjects(rolloutMetadata, _configContent.activeRolloutMetadata); + FIRRemoteConfigValue *rcValue = + [[FIRRemoteConfigValue alloc] initWithData:[value dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key : rcValue}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + + // New fetch response has updated rollout metadata + NSMutableDictionary *updateFetchResponse = + [self createFetchResponseWithConfigEntries:@{key : value} + p13nMetadata:nil + rolloutMetadata:nil]; + [_configContent updateConfigContentWithResponse:updateFetchResponse forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + [_configContent activateRolloutMetadata:nil]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:key]); + XCTAssertTrue(_configContent.activeRolloutMetadata.count == 0); +} + - (void)testConfigUpdate_valueSourceChanged_returnsKey { NSString *namespace = @"test_namespace"; NSString *existingParam = @"key1"; @@ -477,7 +630,9 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { // fetch response contains same key->value NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{existingParam : value1} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -489,14 +644,18 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { #pragma mark - Test Helpers - (NSMutableDictionary *)createFetchResponseWithConfigEntries:(NSDictionary *)config - p13nMetadata:(NSDictionary *)metadata { + p13nMetadata:(NSDictionary *)p13nMetadata + rolloutMetadata:(NSArray *)rolloutMetadata { NSMutableDictionary *fetchResponse = [[NSMutableDictionary alloc] initWithObjectsAndKeys:RCNFetchResponseKeyStateUpdate, RCNFetchResponseKeyState, nil]; if (config) { [fetchResponse setValue:config forKey:RCNFetchResponseKeyEntries]; } - if (metadata) { - [fetchResponse setValue:metadata forKey:RCNFetchResponseKeyPersonalizationMetadata]; + if (p13nMetadata) { + [fetchResponse setValue:p13nMetadata forKey:RCNFetchResponseKeyPersonalizationMetadata]; + } + if (rolloutMetadata) { + [fetchResponse setValue:rolloutMetadata forKey:RCNFetchResponseKeyRolloutMetadata]; } return fetchResponse; } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m index 23705be1abf..773af690935 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m @@ -83,8 +83,8 @@ - (void)testV1NamespaceMigrationToV2Namespace { BOOL loadSuccess, NSDictionary *> *fetchedConfig, NSDictionary *> *activeConfig, - NSDictionary *> - *defaultConfig) { + NSDictionary *> *defaultConfig, + NSDictionary *unusedRolloutMetadata) { XCTAssertTrue(loadSuccess); NSString *fullyQualifiedNamespace = [NSString stringWithFormat:@"%@:%@", namespace_p, kFIRDefaultAppName]; @@ -125,18 +125,19 @@ - (void)testWriteAndLoadMainTableResult { XCTAssertTrue(success); if (count == 100) { // check DB read correctly - [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier - completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, - NSDictionary *defaultConfig) { - NSMutableDictionary *res = [fetchedConfig mutableCopy]; - XCTAssertTrue(success); - FIRRemoteConfigValue *value = res[namespace_p][@"key100"]; - XCTAssertEqualObjects(value.stringValue, @"value100"); - if (success) { - [loadConfigContentExpectation fulfill]; - } - }]; + [self->_DBManager + loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { + NSMutableDictionary *res = [fetchedConfig mutableCopy]; + XCTAssertTrue(success); + FIRRemoteConfigValue *value = res[namespace_p][@"key100"]; + XCTAssertEqualObjects(value.stringValue, @"value100"); + if (success) { + [loadConfigContentExpectation fulfill]; + } + }]; } }; NSString *value = [NSString stringWithFormat:@"value%d", i]; @@ -382,7 +383,8 @@ - (void)testDeleteParamAndLoadMainTable { [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { NSMutableDictionary *res = [activeConfig mutableCopy]; XCTAssertTrue(success); FIRRemoteConfigValue *value = res[namespaceToDelete][@"keyToDelete"]; @@ -403,7 +405,8 @@ - (void)testDeleteParamAndLoadMainTable { [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSDictionary *activeConfig, NSDictionary *defaultConfig, + NSDictionary *unusedRolloutMetadata) { NSMutableDictionary *res = [activeConfig mutableCopy]; XCTAssertTrue(success); FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"]; @@ -587,6 +590,136 @@ - (void)testWriteAndLoadMetadataMultipleTimes { [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; } +- (void)testWriteAndLoadFetchedAndActiveRollout { + XCTestExpectation *writeAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Write and load rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *fetchedRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"2", + @"variant_id" : @"1", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + NSArray *activeRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"3", + @"variant_id" : @"a", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBLoadCompletion loadCompletion = ^( + BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(fetchedRollout, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + XCTAssertEqualObjects(activeRollout, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + + [writeAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:loadCompletion]; + }; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:fetchedRollout + completionHandler:nil]; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata + value:activeRollout + completionHandler:writeRolloutCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +- (void)testUpdateAndLoadRollout { + XCTestExpectation *updateAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Update and load rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *fetchedRollout = @[ @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + } ]; + + NSArray *updatedFetchedRollout = @[ + @{ + @"rollout_id" : @"1", + @"variant_id" : @"B", + @"affected_parameter_keys" : @[ @"key_1", @"key_2" ] + }, + @{ + @"rollout_id" : @"2", + @"variant_id" : @"1", + @"affected_parameter_keys" : @[ @"key_1", @"key_3" ] + } + ]; + + RCNDBCompletion writeRolloutCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBLoadCompletion loadCompletion = + ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(updatedFetchedRollout, + rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + + [updateAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:loadCompletion]; + }; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:fetchedRollout + completionHandler:nil]; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:updatedFetchedRollout + completionHandler:writeRolloutCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} +- (void)testLoadEmptyRollout { + XCTestExpectation *updateAndLoadFetchedRolloutExpectation = + [self expectationWithDescription:@"Load empty rollout in database successfully"]; + + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSArray *emptyResult = [[NSArray alloc] init]; + + RCNDBLoadCompletion loadCompletion = + ^(BOOL success, NSDictionary *unusedFetchedConfig, NSDictionary *unusedActiveConfig, + NSDictionary *unusedDefaultConfig, NSDictionary *rolloutMetadata) { + XCTAssertTrue(success); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata]); + XCTAssertNotNil(rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + XCTAssertEqualObjects(emptyResult, rolloutMetadata[@RCNRolloutTableKeyActiveMetadata]); + + [updateAndLoadFetchedRolloutExpectation fulfill]; + }; + [self->_DBManager loadMainWithBundleIdentifier:bundleIdentifier completionHandler:loadCompletion]; + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + - (void)testUpdateAndloadLastFetchStatus { XCTestExpectation *updateAndLoadMetadataExpectation = [self expectationWithDescription:@"Update and load last fetch status in database successfully."]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m index 2a5bd7c67c9..9acb62e0717 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m @@ -23,6 +23,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" +@import FirebaseRemoteConfigInterop; static NSString *const RCNFakeSenderID = @"855865492447"; static NSString *const RCNFakeToken = @"ctToAh17Exk:" @@ -48,6 +49,7 @@ @interface RCNConfigTest : XCTestCase { RCNConfigExperiment *_experiment; RCNConfigFetch *_configFetch; dispatch_queue_t _queue; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -66,9 +68,10 @@ - (void)setUp { experiment:_experiment queue:_queue]; _configFetch = OCMPartialMock(fetcher); + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; // Fake a response with a default namespace and a custom namespace. NSDictionary *namespaceToConfig = @{ - FIRNamespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"}, + _namespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"}, FIRNamespaceGooglePlayPlatform : @{@"playerID" : @"36", @"gameLevel" : @"87"}, }; _response = @@ -149,19 +152,19 @@ - (void)testFetchAllConfigsSuccessfully { XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; XCTAssertEqual(self->_settings.expirationInSeconds, 43200, @@ -200,11 +203,11 @@ - (void)testFetchConfigInCachedResults { NSDictionary *result = self->_configContent.fetchedConfig; XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; @@ -246,19 +249,19 @@ - (void)testFetchFailedWithCachedResult { XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; @@ -340,19 +343,19 @@ - (void)testFetchThrottledWithStaledCachedResult { NSDictionary *result = self->_configContent.fetchedConfig; XCTAssertNotNil(result); [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key1" value:@"value1"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGoogleMobilePlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"key2" value:@"value2"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"playerID" value:@"36"]; [self checkConfigResult:result - withNamespace:FIRNamespaceGooglePlayPlatform + withNamespace:_namespaceGoogleMobilePlatform key:@"gameLevel" value:@"87"]; XCTAssertEqual( diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m index 5d1b28fb61d..cbbcd0a91bd 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNInstanceIDTest.m @@ -29,6 +29,7 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" +@import FirebaseRemoteConfigInterop; @interface RCNConfigFetch (ForTest) - (instancetype)initWithContent:(RCNConfigContent *)content @@ -136,7 +137,8 @@ - (void)setUpConfigMock { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; + ; break; case RCNTestRCInstanceDefault: default: diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index e1a23b5a695..e02b8ecaabf 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -18,6 +18,7 @@ #import #import +#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h" #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" @@ -31,6 +32,9 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; + +@protocol FIRRolloutsStateSubscriber; @interface RCNConfigFetch (ForTest) - (instancetype)initWithContent:(RCNConfigContent *)content @@ -130,6 +134,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSTimeInterval _checkCompletionTimeout; NSMutableArray *_configInstances; NSMutableArray *> *_entries; + NSArray *_rolloutMetadata; NSMutableArray *> *_response; NSMutableArray *_responseData; NSMutableArray *_URLResponse; @@ -145,6 +150,7 @@ @interface RCNRemoteConfigTest : XCTestCase { NSString *_fullyQualifiedNamespace; RCNConfigSettings *_settings; dispatch_queue_t _queue; + NSString *_namespaceGoogleMobilePlatform; } @end @@ -180,6 +186,7 @@ - (void)setUp { _URLResponse = [[NSMutableArray alloc] initWithCapacity:3]; _configFetch = [[NSMutableArray alloc] initWithCapacity:3]; _configRealtime = [[NSMutableArray alloc] initWithCapacity:3]; + _namespaceGoogleMobilePlatform = FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform; // Populate the default, second app, second namespace instances. for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { @@ -204,7 +211,7 @@ - (void)setUp { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -259,7 +266,17 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, updateCompletionHandler:nil]; }); - _response[i] = @{@"state" : @"UPDATE", @"entries" : _entries[i]}; + _rolloutMetadata = @[ @{ + RCNFetchResponseKeyRolloutID : @"1", + RCNFetchResponseKeyVariantID : @"0", + RCNFetchResponseKeyAffectedParameterKeys : @[ _entries[i].allKeys[0] ] + } ]; + + _response[i] = @{ + @"state" : @"UPDATE", + @"entries" : _entries[i], + RCNFetchResponseKeyRolloutMetadata : _rolloutMetadata + }; _responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil]; @@ -286,6 +303,7 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, - (void)tearDown { [_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath]; + [FIRRemoteConfigComponent clearAllComponentInstances]; [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:_userDefaultsSuiteName]; [_DBManagerMock stopMocking]; _DBManagerMock = nil; @@ -594,7 +612,7 @@ - (void)testFetchConfigsFailed { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -707,7 +725,7 @@ - (void)testFetchConfigsFailedErrorNoNetwork { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -911,7 +929,7 @@ - (void)testActivateOnFetchNoChangeStatus { case RCNTestRCInstanceSecondApp: currentAppName = RCNTestsSecondFIRAppName; currentOptions = [self secondAppOptions]; - currentNamespace = FIRNamespaceGoogleMobilePlatform; + currentNamespace = _namespaceGoogleMobilePlatform; break; case RCNTestRCInstanceDefault: default: @@ -1782,6 +1800,43 @@ - (void)testRealtimeStreamRequestBody { XCTAssertTrue([strData containsString:@"appInstanceId:'iid'"]); } +- (void)testFetchAndActivateRolloutsNotifyInterop { + id mockNotificationCenter = [OCMockObject mockForClass:[NSNotificationCenter class]]; + [[mockNotificationCenter expect] postNotificationName:@"RolloutsStateDidChangeNotification" + object:[OCMArg any] + userInfo:[OCMArg any]]; + id mockSubscriber = [OCMockObject mockForProtocol:@protocol(FIRRolloutsStateSubscriber)]; + [[mockSubscriber expect] rolloutsStateDidChange:[OCMArg any]]; + + XCTestExpectation *expectation = [self + expectationWithDescription:[NSString + stringWithFormat:@"Test rollout update send notification"]]; + + XCTAssertEqual(_configInstances[RCNTestRCInstanceDefault].lastFetchStatus, + FIRRemoteConfigFetchStatusNoFetchYet); + + FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = + ^void(FIRRemoteConfigFetchAndActivateStatus status, NSError *error) { + XCTAssertEqual(status, FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote); + XCTAssertNil(error); + + XCTAssertEqual(self->_configInstances[RCNTestRCInstanceDefault].lastFetchStatus, + FIRRemoteConfigFetchStatusSuccess); + XCTAssertNotNil(self->_configInstances[RCNTestRCInstanceDefault].lastFetchTime); + XCTAssertGreaterThan( + self->_configInstances[RCNTestRCInstanceDefault].lastFetchTime.timeIntervalSince1970, 0, + @"last fetch time interval should be set."); + [expectation fulfill]; + }; + + [_configInstances[RCNTestRCInstanceDefault] + fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion]; + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + #pragma mark - Test Helpers - (FIROptions *)firstAppOptions { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m index 8721463feb8..5429c61df1f 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m @@ -25,6 +25,7 @@ #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" #import "FirebaseCore/Extension/FirebaseCoreInternal.h" +@import FirebaseRemoteConfigInterop; @interface RCNThrottlingTests : XCTestCase { RCNConfigContent *_configContentMock; @@ -53,20 +54,22 @@ - (void)setUp { RCNConfigDBManager *DBManager = [[RCNConfigDBManager alloc] init]; _configContentMock = OCMClassMock([RCNConfigContent class]); - _settings = [[RCNConfigSettings alloc] initWithDatabaseManager:DBManager - namespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + _settings = [[RCNConfigSettings alloc] + initWithDatabaseManager:DBManager + namespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; _experimentMock = OCMClassMock([RCNConfigExperiment class]); dispatch_queue_t _queue = dispatch_queue_create( "com.google.GoogleConfigService.FIRRemoteConfigTest", DISPATCH_QUEUE_SERIAL); - _configFetch = [[RCNConfigFetch alloc] initWithContent:_configContentMock - DBManager:DBManager - settings:_settings - experiment:_experimentMock - queue:_queue - namespace:FIRNamespaceGoogleMobilePlatform - app:[FIRApp defaultApp]]; + _configFetch = [[RCNConfigFetch alloc] + initWithContent:_configContentMock + DBManager:DBManager + settings:_settings + experiment:_experimentMock + queue:_queue + namespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; } - (void)mockFetchResponseWithStatusCode:(NSInteger)statusCode { diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m index 0c3135e2edd..5f915d73632 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -129,8 +129,17 @@ - (void)testUserDefaultsTemplateVersionWriteAndRead { [[RCNUserDefaultsManager alloc] initWithAppName:AppName bundleID:[NSBundle mainBundle].bundleIdentifier namespace:FQNamespace1]; - [manager setLastTemplateVersion:@"1"]; - XCTAssertEqual([manager lastTemplateVersion], @"1"); + [manager setLastFetchedTemplateVersion:@"1"]; + XCTAssertEqual([manager lastFetchedTemplateVersion], @"1"); +} + +- (void)testUserDefaultsActiveTemplateVersionWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:AppName + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:FQNamespace1]; + [manager setLastActiveTemplateVersion:@"1"]; + XCTAssertEqual([manager lastActiveTemplateVersion], @"1"); } - (void)testUserDefaultsRealtimeThrottleEndTimeWriteAndRead { @@ -229,10 +238,16 @@ - (void)testUserDefaultsForMultipleNamespaces { XCTAssertEqual([manager2 realtimeRetryCount], 2); /// Fetch template version. - [manager1 setLastTemplateVersion:@"1"]; - [manager2 setLastTemplateVersion:@"2"]; - XCTAssertEqualObjects([manager1 lastTemplateVersion], @"1"); - XCTAssertEqualObjects([manager2 lastTemplateVersion], @"2"); + [manager1 setLastFetchedTemplateVersion:@"1"]; + [manager2 setLastFetchedTemplateVersion:@"2"]; + XCTAssertEqualObjects([manager1 lastFetchedTemplateVersion], @"1"); + XCTAssertEqualObjects([manager2 lastFetchedTemplateVersion], @"2"); + + /// Active template version. + [manager1 setLastActiveTemplateVersion:@"1"]; + [manager2 setLastActiveTemplateVersion:@"2"]; + XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1"); + XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2"); } - (void)testUserDefaultsReset { diff --git a/FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh b/FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh new file mode 100755 index 00000000000..1667fc0fe5a --- /dev/null +++ b/FirebaseRemoteConfig/generate_featureRolloutsTestApp.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Copyright 2022 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. +# + +readonly DIR="$( git rev-parse --show-toplevel )" + +# +# This script attempts to copy the Google Services file from google3. If you are not a Google Employee, it will fail, so we'd recommend you create your own Firebase App and place the Google Services file in Tests/TestApp/Shared +# + +echoColor() { + COLOR='\033[0;35m' + NC='\033[0m' + printf "${COLOR}$1${NC}\n" +} + +echoRed() { + COLOR='\033[0;31m' + NC='\033[0m' + printf "${COLOR}$1${NC}\n" +} + +echoColor "Generating Firebase Remote Config Feature Rolouts Test App" +echoColor "Copying GoogleService-Info.plist from google3. Checking gcert status" +if gcertstatus; then + G3Path="/google/src/files/head/depot/google3/third_party/firebase/ios/Secrets/RemoteConfig/FeatureRollouts/GoogleService-Info.plist" + Dest="$DIR/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp/Shared" + cp $G3Path $Dest + echoColor "Copied $G3Path to $Dest" +else + echoRed "gcert token is not valid. If you are a Google Employee, run 'gcert', and then repeat this command. Non-Google employees will need to download a GoogleService-Info.plist and place it in $DIR/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp" +fi + + +echoColor "Running 'pod install'" +cd $DIR/FirebaseRemoteConfig/Tests/FeatureRolloutsTestApp +pod install + +# Upon a `pod install`, Crashlytics will copy these files at the root directory +# due to a funky interaction with its cocoapod. This line deletes these extra +# copies of the files as they should only live in Crashlytics/ +rm -f $DIR/run $DIR/upload-symbols + +open *.xcworkspace + diff --git a/FirebaseRemoteConfigInterop.podspec b/FirebaseRemoteConfigInterop.podspec new file mode 100644 index 00000000000..86b86b24e6a --- /dev/null +++ b/FirebaseRemoteConfigInterop.podspec @@ -0,0 +1,34 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseRemoteConfigInterop' + s.version = '10.23.0' + s.summary = 'Interfaces that allow other Firebase SDKs to use Remote Config functionality.' + + s.description = <<-DESC + Not for public use. + A set of protocols that other Firebase SDKs can use to interoperate with FirebaseRemoetConfig in a safe + and reliable manner. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache-2.0', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + # NOTE that these should not be used externally, this is for Firebase pods to depend on each + # other. + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => 'CocoaPods-' + s.version.to_s + } + + s.swift_version = '5.3' + s.cocoapods_version = '>= 1.12.0' + s.prefix_header_file = false + + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.13' + s.tvos.deployment_target = '12.0' + s.watchos.deployment_target = '6.0' + + s.source_files = 'FirebaseRemoteConfig/Interop/*.swift' +end diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift index b695e375887..892f14ec834 100644 --- a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift +++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/FirebaseRemoteConfigSwift_APIBuildTests.swift @@ -16,6 +16,7 @@ import XCTest import FirebaseCore import FirebaseRemoteConfig +import FirebaseRemoteConfigInterop import FirebaseRemoteConfigSwift final class FirebaseRemoteConfigSwift_APIBuildTests: XCTestCase { diff --git a/FirebaseSessions/Tests/TestApp/Podfile b/FirebaseSessions/Tests/TestApp/Podfile index 67c05149217..4bea966bc4f 100644 --- a/FirebaseSessions/Tests/TestApp/Podfile +++ b/FirebaseSessions/Tests/TestApp/Podfile @@ -7,6 +7,7 @@ def shared_pods pod 'FirebaseCoreInternal', :path => '../../../' pod 'FirebaseCoreExtension', :path => '../../../' pod 'FirebaseSessions', :path => '../../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../../' end target 'AppQualityDevApp_iOS' do diff --git a/IntegrationTesting/ClientApp/Podfile b/IntegrationTesting/ClientApp/Podfile index 7efd2cdd1ea..2a72d478739 100644 --- a/IntegrationTesting/ClientApp/Podfile +++ b/IntegrationTesting/ClientApp/Podfile @@ -17,6 +17,7 @@ target 'ClientApp-CocoaPods' do pod 'FirebaseAppCheck', :path => '../../' pod 'FirebaseRemoteConfig', :path => '../../' pod 'FirebaseRemoteConfigSwift', :path => '../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../' pod 'FirebaseAppDistribution', :path => '../../' pod 'FirebaseAuth', :path => '../../' pod 'FirebaseCrashlytics', :path => '../../' diff --git a/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile b/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile index 03bfe2e2d04..e45e8cd4908 100644 --- a/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile +++ b/IntegrationTesting/CocoapodsIntegrationTest/TestEnvironments/Cocoapods_multiprojects_frameworks/Podfile @@ -25,6 +25,7 @@ target 'CocoapodsIntegrationTest' do pod 'FirebaseInstallations', :path => '../../' pod 'FirebaseMessaging', :path => '../../' pod 'FirebaseMessagingInterop', :path => '../../' + pod 'FirebaseRemoteConfigInterop', :path => '../../' pod 'FirebasePerformance', :path => '../../' pod 'FirebaseStorage', :path => '../../' end diff --git a/Package.swift b/Package.swift index ae854aa3f10..cd16df6ca5f 100644 --- a/Package.swift +++ b/Package.swift @@ -497,11 +497,17 @@ let package = Package( ), .target( name: "FirebaseCrashlytics", - dependencies: ["FirebaseCore", "FirebaseInstallations", "FirebaseSessions", - .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), - .product(name: "GULEnvironment", package: "GoogleUtilities"), - .product(name: "FBLPromises", package: "Promises"), - .product(name: "nanopb", package: "nanopb")], + dependencies: [ + "FirebaseCore", + "FirebaseInstallations", + "FirebaseSessions", + "FirebaseRemoteConfigInterop", + "FirebaseCrashlyticsSwift", + .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), + .product(name: "GULEnvironment", package: "GoogleUtilities"), + .product(name: "FBLPromises", package: "Promises"), + .product(name: "nanopb", package: "nanopb"), + ], path: "Crashlytics", exclude: [ "run", @@ -514,6 +520,7 @@ let package = Package( "upload-symbols", "CrashlyticsInputFiles.xcfilelist", "third_party/libunwind/LICENSE", + "Crashlytics/Rollouts/", ], sources: [ "Crashlytics/", @@ -543,6 +550,19 @@ let package = Package( .linkedFramework("SystemConfiguration", .when(platforms: [.iOS, .macOS, .tvOS])), ] ), + .target( + name: "FirebaseCrashlyticsSwift", + dependencies: ["FirebaseRemoteConfigInterop"], + path: "Crashlytics", + sources: [ + "Crashlytics/Rollouts/", + ] + ), + .testTarget( + name: "FirebaseCrashlyticsSwiftUnit", + dependencies: ["FirebaseCrashlyticsSwift"], + path: "Crashlytics/UnitTestsSwift/" + ), .testTarget( name: "FirebaseCrashlyticsUnit", dependencies: ["FirebaseCrashlytics", .product(name: "OCMock", package: "ocmock")], @@ -967,6 +987,7 @@ let package = Package( "FirebaseCore", "FirebaseABTesting", "FirebaseInstallations", + "FirebaseRemoteConfigInterop", .product(name: "GULNSData", package: "GoogleUtilities"), ], path: "FirebaseRemoteConfig/Sources", @@ -996,6 +1017,14 @@ let package = Package( .headerSearchPath("../../.."), ] ), + .testTarget( + name: "RemoteConfigSwiftUnit", + dependencies: ["FirebaseRemoteConfigInternal"], + path: "FirebaseRemoteConfig/Tests/SwiftUnit", + cSettings: [ + .headerSearchPath("../../.."), + ] + ), .target( name: "FirebaseRemoteConfig", dependencies: [ @@ -1039,6 +1068,15 @@ let package = Package( .headerSearchPath("../../../"), ] ), + // Internal headers only for consuming from other SDK. + .target( + name: "FirebaseRemoteConfigInterop", + path: "FirebaseRemoteConfig/Interop", + publicHeadersPath: ".", + cSettings: [ + .headerSearchPath("../../"), + ] + ), // MARK: - Firebase Sessions diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index 5bd4f3dd5e2..8575cf37e36 100755 --- a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift +++ b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift @@ -32,6 +32,7 @@ public let shared = Manifest( Pod("FirebaseMessagingInterop"), Pod("FirebaseInstallations"), Pod("FirebaseSessions"), + Pod("FirebaseRemoteConfigInterop"), Pod("GoogleAppMeasurement", isClosedSource: true), Pod("GoogleAppMeasurementOnDeviceConversion", isClosedSource: true, platforms: ["ios"]), Pod("FirebaseAnalytics", isClosedSource: true, zip: true), diff --git a/scripts/localize_podfile.swift b/scripts/localize_podfile.swift index f07cb3124a0..8b60a2cbdd5 100755 --- a/scripts/localize_podfile.swift +++ b/scripts/localize_podfile.swift @@ -39,6 +39,7 @@ let implicitPods = [ "FirebaseAppCheckInterop", "FirebaseAuthInterop", "FirebaseMessagingInterop", "FirebaseCoreInternal", "FirebaseSessions", "FirebaseSharedSwift", + "FirebaseRemoteConfigInterop", ] let binaryPods = [ From b40cc2404bd07038ac18abe972f59111e5f8d21c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 11 Mar 2024 15:51:38 -0700 Subject: [PATCH 082/104] [spm] Update grpc to 1.62.3 (#12520) --- Firestore/CHANGELOG.md | 1 + Package.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index ceded5649ef..1a829eb0d68 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased - [feature] Enable snapshot listener option to retrieve data from local cache only. (#12370) +- [fixed] Update gRPC dependency to 1.62.* (#12098, #12021) # 10.22.0 - [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) diff --git a/Package.swift b/Package.swift index cd16df6ca5f..8a69c5c61b8 100644 --- a/Package.swift +++ b/Package.swift @@ -1373,7 +1373,7 @@ func abseilDependency() -> Package.Dependency { if ProcessInfo.processInfo.environment["FIREBASE_SOURCE_FIRESTORE"] != nil { packageInfo = ( "https://github.com/firebase/abseil-cpp-SwiftPM.git", - "0.20220623.0" ..< "0.20220624.0" + "0.20240116.1" ..< "0.20240117.0" ) } else { packageInfo = ( @@ -1391,7 +1391,7 @@ func grpcDependency() -> Package.Dependency { // If building Firestore from source, abseil will need to be built as source // as the headers in the binary version of abseil are unusable. if ProcessInfo.processInfo.environment["FIREBASE_SOURCE_FIRESTORE"] != nil { - packageInfo = ("https://github.com/grpc/grpc-ios.git", "1.49.1" ..< "1.50.0") + packageInfo = ("https://github.com/grpc/grpc-ios.git", "1.62.3" ..< "1.63.0") } else { packageInfo = ("https://github.com/google/grpc-binary.git", "1.49.1" ..< "1.50.0") } From 1de0529ed37beb6cbed9e98c4475496588e10857 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:22:04 -0400 Subject: [PATCH 083/104] [Release Tooling] Copy over macOS/macCatalyst plists (#12517) --- FirebaseCore/CHANGELOG.md | 3 +++ .../Sources/ZipBuilder/FrameworkBuilder.swift | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index 79ec5d5f636..4337e81de12 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- Fix validation issue for macOS and macCatalyst XCFrameworks. (#12505) + # Firebase 10.22.1 - [Swift Package Manager / CocoaPods] Fix app validation issues on Xcode 15.3 for those using the `FirebaseAnalyticsOnDeviceConversion` SDK. This issue was diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index 7c763834411..d322aa91ec4 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -674,7 +674,18 @@ struct FrameworkBuilder { let binaryName = frameworkPath.lastPathComponent.replacingOccurrences(of: ".framework", with: "") let fatBinary = frameworkPath.appendingPathComponent(binaryName).resolvingSymlinksInPath() - let infoPlist = frameworkPath.appendingPathComponent("Info.plist").resolvingSymlinksInPath() + let plistPathComponents = { + if platform == .catalyst || platform == .macOS { + // Frameworks for macOS and macCatalyst have a different directory + // structure so the framework-level `Info.plist` is found in a + // different spot. + return ["Versions", "A", "Resources", "Info.plist"] + } else { + return ["Info.plist"] + } + }() + let infoPlist = frameworkPath.appendingPathComponents(plistPathComponents) + .resolvingSymlinksInPath() let infoPlistDestination = platformFrameworkDir.appendingPathComponent("Info.plist") let fatBinaryDestination = platformFrameworkDir.appendingPathComponent(framework) do { @@ -698,7 +709,9 @@ struct FrameworkBuilder { try updatedPlistData.write(to: infoPlistDestination) } catch { - // The Catalyst and macos Info.plist's are in another location. Ignore failure. + fatalError( + "Could not copy framework-level plist to framework directory for \(framework): \(error)" + ) } // Use the appropriate moduleMaps From 5b3907f9913cd4e87323aa16c09d01e269e494b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 07:17:50 -0700 Subject: [PATCH 084/104] NOTICES Change (#12524) Co-authored-by: runner --- CoreOnly/NOTICES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CoreOnly/NOTICES b/CoreOnly/NOTICES index 659f6159e33..802eaf57d93 100644 --- a/CoreOnly/NOTICES +++ b/CoreOnly/NOTICES @@ -18,6 +18,7 @@ FirebaseMessaging FirebaseMessagingInterop FirebasePerformance FirebaseRemoteConfig +FirebaseRemoteConfigInterop FirebaseSessions FirebaseStorage GTMSessionFetcher @@ -253,6 +254,7 @@ record keeping.) 27287199 27287880 27287883 + 263291445 OpenSSL License --------------- From 370385c42aeec4810368089158ac1f3e9181a0ce Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 12 Mar 2024 09:50:06 -0700 Subject: [PATCH 085/104] Changelog updates for 10.23.0 (#12533) --- Crashlytics/CHANGELOG.md | 2 +- FirebaseCore/CHANGELOG.md | 2 +- FirebaseMessaging/CHANGELOG.md | 3 +++ FirebaseRemoteConfig/CHANGELOG.md | 2 +- Firestore/CHANGELOG.md | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index 62f507a7c01..ce0e9c89eea 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# 10.23.0 - [added] Updated upload-symbols to 13.7 with VisionPro build phase support. (#12306) - [changed] Added support for Crashlytics to report metadata about Remote Config keys and values. diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index 4337e81de12..c5a879602d9 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# Firebase 10.23.0 - Fix validation issue for macOS and macCatalyst XCFrameworks. (#12505) # Firebase 10.22.1 diff --git a/FirebaseMessaging/CHANGELOG.md b/FirebaseMessaging/CHANGELOG.md index e51c663ce33..8191c09c6b3 100644 --- a/FirebaseMessaging/CHANGELOG.md +++ b/FirebaseMessaging/CHANGELOG.md @@ -1,3 +1,6 @@ +# 10.23.0 +- [fixed] [CocoaPods] Fix "no rule" warning when running `pod install`. (#12511) + # 10.20.0 - [fixed] Fix 10.19.0 regression where the FCM registration token was nil at first app start after update from 10.19.0 or earlier. (#12245) diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index e56c9da1f2d..b2ba32e782c 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# 10.23.0 - [changed] Add support for other Firebase products to integrate with Remote Config. # 10.17.0 diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 1a829eb0d68..ee98034ed48 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# 10.23.0 - [feature] Enable snapshot listener option to retrieve data from local cache only. (#12370) - [fixed] Update gRPC dependency to 1.62.* (#12098, #12021) From 3e7aa393c12c360fc97df254e31f8dd42d5b0e7b Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:25:34 -0400 Subject: [PATCH 086/104] [Firestore] Improve logging in scripts/check_firestore_symbols.sh (#12535) --- scripts/check_firestore_symbols.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/check_firestore_symbols.sh b/scripts/check_firestore_symbols.sh index fe2d41b9b50..5fb2de75acc 100755 --- a/scripts/check_firestore_symbols.sh +++ b/scripts/check_firestore_symbols.sh @@ -153,7 +153,7 @@ nm ~/Library/Developer/Xcode/DerivedData/TestPkg-ObjC/Build/Products/Debug/TestP # return exit code 1, which will cause the set pipefail to terminate execution. # To avoid this, `|| true` ensures the exit code always indicates success. DIFF=$( - git diff --no-index \ + git diff --no-index --output-indicator-new="?" \ objc_symbols_without_linker_flag.txt \ objc_symbols_with_linker_flag.txt \ || true @@ -161,6 +161,9 @@ DIFF=$( if [[ -n "$DIFF" ]]; then echo "Failure: Unlinked Objective-C symbols have been detected:" echo "$DIFF" + echo -n "💡 To fix, follow the process shown in " + echo -n "https://github.com/firebase/firebase-ios-sdk/pull/12534 for the " + echo "above symbols that are prefixed with ?" exit 1 else echo "Success: No unlinked Objective-C symbols have been detected." From f7645725be44de731e712ac96d79fa5ba6c5fb90 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:00:41 -0400 Subject: [PATCH 087/104] [Firestore] Add unlinked symbol introduced in #12370 (#12534) --- Firestore/Source/API/FIRFirestore.mm | 4 +++- Firestore/Source/API/FIRSnapshotListenOptions.mm | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm index 29b56fd2a6b..f6dbf9dc059 100644 --- a/Firestore/Source/API/FIRFirestore.mm +++ b/Firestore/Source/API/FIRFirestore.mm @@ -567,12 +567,14 @@ - (void)terminateInternalWithCompletion:(nullable void (^)(NSError *_Nullable er #pragma mark - Force Link Unreferenced Symbols extern void FSTIncludeFSTFirestoreComponent(void); +extern void FSTIncludeFIRSnapshotListenOptions(void); -/// This method forces the linker to include all the Analytics categories without requiring app +/// This method forces the linker to include all Firestore symbols without requiring app /// developers to include the '-ObjC' linker flag in their projects. DO NOT CALL THIS METHOD. + (void)notCalled { NSAssert(NO, @"+notCalled should never be called"); FSTIncludeFSTFirestoreComponent(); + FSTIncludeFIRSnapshotListenOptions(); } @end diff --git a/Firestore/Source/API/FIRSnapshotListenOptions.mm b/Firestore/Source/API/FIRSnapshotListenOptions.mm index 9e22d686428..c1cb4216d43 100644 --- a/Firestore/Source/API/FIRSnapshotListenOptions.mm +++ b/Firestore/Source/API/FIRSnapshotListenOptions.mm @@ -58,6 +58,11 @@ - (FIRSnapshotListenOptions *)optionsWithSource:(FIRListenSource)source { return newOptions; } +/// This function forces the linker to include `FIRSnapshotListenOptions`. +/// See `+[FIRFirestore notCalled]`. +void FSTIncludeFIRSnapshotListenOptions(void) { +} + @end NS_ASSUME_NONNULL_END \ No newline at end of file From 77920e36c4a9d75cb87c5f5ba555ed61fbe6c144 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:10:16 -0400 Subject: [PATCH 088/104] [Firestore] Re-export public header added in #12370 (#12537) --- .../FirebaseFirestore/FIRSnapshotListenOptions.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 FirebaseFirestoreInternal/FirebaseFirestore/FIRSnapshotListenOptions.h diff --git a/FirebaseFirestoreInternal/FirebaseFirestore/FIRSnapshotListenOptions.h b/FirebaseFirestoreInternal/FirebaseFirestore/FIRSnapshotListenOptions.h new file mode 100644 index 00000000000..6d61e32c56b --- /dev/null +++ b/FirebaseFirestoreInternal/FirebaseFirestore/FIRSnapshotListenOptions.h @@ -0,0 +1,15 @@ +// 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. + +#import From 7bf243dc3e1bb40f66e40675a5e65a0b8da69e9a Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Tue, 12 Mar 2024 22:49:30 +0000 Subject: [PATCH 089/104] fix .plist line deletion --- .../SampleSwift/AuthenticationExample/SwiftApplication.plist | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist index 633b311e4b7..50378fe58b0 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SwiftApplication.plist @@ -24,6 +24,7 @@ CFBundleURLName CFBundleURLSchemes + CFBundleVersion From c8acb7625b7759fb239829caa282e3cf8fd49bbe Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:55:10 -0400 Subject: [PATCH 090/104] [Firestore] Bump dependency ranges for Firestore's binary SPM distro (#12538) --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 8a69c5c61b8..889a72f7788 100644 --- a/Package.swift +++ b/Package.swift @@ -1378,7 +1378,7 @@ func abseilDependency() -> Package.Dependency { } else { packageInfo = ( "https://github.com/google/abseil-cpp-binary.git", - "1.2022062300.0" ..< "1.2022062400.0" + "1.2024011601.0" ..< "1.2024011700.0" ) } @@ -1393,7 +1393,7 @@ func grpcDependency() -> Package.Dependency { if ProcessInfo.processInfo.environment["FIREBASE_SOURCE_FIRESTORE"] != nil { packageInfo = ("https://github.com/grpc/grpc-ios.git", "1.62.3" ..< "1.63.0") } else { - packageInfo = ("https://github.com/google/grpc-binary.git", "1.49.1" ..< "1.50.0") + packageInfo = ("https://github.com/google/grpc-binary.git", "1.62.1" ..< "1.63.0") } return .package(url: packageInfo.url, packageInfo.range) @@ -1532,8 +1532,8 @@ func firestoreTargets() -> [Target] { } else { return .binaryTarget( name: "FirebaseFirestoreInternal", - url: "https://dl.google.com/firebase/ios/bin/firestore/10.22.0/FirebaseFirestoreInternal.zip", - checksum: "35c02539c6bbd43ec1c3b58f894984e48ff8c560db0c799fb8f6377b59c96099" + url: "https://dl.google.com/firebase/ios/bin/firestore/10.23.0/FirebaseFirestoreInternal.zip", + checksum: "80050185438caedcf70a78737ed4da675445f18694b7617bf251962546bdec3b" ) } }() From 455e7d5cee32a1c325500a901523759c9ce88309 Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:56:18 -0400 Subject: [PATCH 091/104] Fix Firestore build warnings (#12536) --- Firestore/core/src/local/index_backfiller.cc | 7 ++++--- Firestore/core/src/local/index_backfiller.h | 10 ++++++---- Firestore/core/src/local/local_documents_view.cc | 2 +- Firestore/core/src/local/local_documents_view.h | 3 ++- Firestore/core/src/remote/bloom_filter.cc | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Firestore/core/src/local/index_backfiller.cc b/Firestore/core/src/local/index_backfiller.cc index b0be8be8fdf..1991031d4c6 100644 --- a/Firestore/core/src/local/index_backfiller.cc +++ b/Firestore/core/src/local/index_backfiller.cc @@ -13,6 +13,7 @@ // limitations under the License. #include +#include #include #include @@ -44,7 +45,7 @@ IndexBackfiller::IndexBackfiller() { max_documents_to_process_ = kMaxDocumentsToProcess; } -int IndexBackfiller::WriteIndexEntries(const LocalStore* local_store) { +size_t IndexBackfiller::WriteIndexEntries(const LocalStore* local_store) { IndexManager* index_manager = local_store->index_manager(); std::unordered_set processed_collection_groups; size_t documents_remaining = max_documents_to_process_; @@ -64,10 +65,10 @@ int IndexBackfiller::WriteIndexEntries(const LocalStore* local_store) { return max_documents_to_process_ - documents_remaining; } -int IndexBackfiller::WriteEntriesForCollectionGroup( +size_t IndexBackfiller::WriteEntriesForCollectionGroup( const LocalStore* local_store, const std::string& collection_group, - int documents_remaining_under_cap) const { + size_t documents_remaining_under_cap) const { IndexManager* index_manager = local_store->index_manager(); const auto* const local_documents_view = local_store->local_documents(); diff --git a/Firestore/core/src/local/index_backfiller.h b/Firestore/core/src/local/index_backfiller.h index 5ebd3aa8014..46c3ef1468d 100644 --- a/Firestore/core/src/local/index_backfiller.h +++ b/Firestore/core/src/local/index_backfiller.h @@ -15,6 +15,7 @@ #ifndef FIRESTORE_CORE_SRC_LOCAL_INDEX_BACKFILLER_H_ #define FIRESTORE_CORE_SRC_LOCAL_INDEX_BACKFILLER_H_ +#include #include namespace firebase { @@ -43,7 +44,7 @@ class IndexBackfiller { * Writes index entries until the cap is reached. Returns the number of * documents processed. */ - int WriteIndexEntries(const LocalStore* local_store); + size_t WriteIndexEntries(const LocalStore* local_store); private: friend class IndexBackfillerTest; @@ -53,9 +54,10 @@ class IndexBackfiller { * Writes entries for the provided collection group. Returns the number of * documents processed. */ - int WriteEntriesForCollectionGroup(const LocalStore* local_store, - const std::string& collection_group, - int documents_remaining_under_cap) const; + size_t WriteEntriesForCollectionGroup( + const LocalStore* local_store, + const std::string& collection_group, + size_t documents_remaining_under_cap) const; /** Returns the next offset based on the provided documents. */ model::IndexOffset GetNewOffset(const model::IndexOffset& existing_offset, diff --git a/Firestore/core/src/local/local_documents_view.cc b/Firestore/core/src/local/local_documents_view.cc index b4e2cd78807..d3812e42a5f 100644 --- a/Firestore/core/src/local/local_documents_view.cc +++ b/Firestore/core/src/local/local_documents_view.cc @@ -134,7 +134,7 @@ model::DocumentMap LocalDocumentsView::GetDocumentsMatchingCollectionGroupQuery( LocalWriteResult LocalDocumentsView::GetNextDocuments( const std::string& collection_group, const IndexOffset& offset, - int count) const { + size_t count) const { auto docs = remote_document_cache_->GetAll(collection_group, offset, count); auto overlays = count - docs.size() > 0 ? document_overlay_cache_->GetOverlays( diff --git a/Firestore/core/src/local/local_documents_view.h b/Firestore/core/src/local/local_documents_view.h index 7fc12b378fa..549656dc44e 100644 --- a/Firestore/core/src/local/local_documents_view.h +++ b/Firestore/core/src/local/local_documents_view.h @@ -17,6 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ #define FIRESTORE_CORE_SRC_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ +#include #include #include #include @@ -98,7 +99,7 @@ class LocalDocumentsView { */ local::LocalWriteResult GetNextDocuments(const std::string& collection_group, const model::IndexOffset& offset, - int count) const; + size_t count) const; /** * Similar to `GetDocuments`, but creates the local view from the given diff --git a/Firestore/core/src/remote/bloom_filter.cc b/Firestore/core/src/remote/bloom_filter.cc index ddf84b866e9..0e4c8e04f34 100644 --- a/Firestore/core/src/remote/bloom_filter.cc +++ b/Firestore/core/src/remote/bloom_filter.cc @@ -78,7 +78,7 @@ int32_t BloomFilter::GetBitIndex(const Hash& hash, int32_t hash_index) const { uint64_t bit_index = combined_hash % bit_count_uint64; HARD_ASSERT(bit_index <= INT32_MAX); - return bit_index; + return static_cast(bit_index); } bool BloomFilter::IsBitSet(int32_t index) const { From 9b5639c4019ec88e16cacf76f836209873050d95 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Tue, 12 Mar 2024 23:33:54 +0000 Subject: [PATCH 092/104] fix unit test --- .../AuthenticationExampleUITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift index 1dc9bddd718..67fcf65e0d1 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift @@ -38,7 +38,7 @@ class AuthenticationExampleUITests: XCTestCase { func testAuthOptions() { // There are 15 sign in methods, each with its own cell - XCTAssertEqual(app.tables.cells.count, 15) + XCTAssertEqual(app.tables.cells.count, 16) } func testAuthAnonymously() { From 0afde381071e310612d9c02a037a3b805468ee5c Mon Sep 17 00:00:00 2001 From: htcgh Date: Tue, 12 Mar 2024 16:37:30 -0700 Subject: [PATCH 093/104] Analytics 10.23.0 (#12539) --- FirebaseAnalytics.podspec | 2 +- GoogleAppMeasurement.podspec | 2 +- Package.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index 0259cc52c58..2e5ef18ec3a 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/d9e6824c98c32455/FirebaseAnalytics-10.22.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/dedc8d0f648c53b6/FirebaseAnalytics-10.23.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index 2167fa94cb5..2a3eec8af1e 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/a92666f1e01923b8/GoogleAppMeasurement-10.22.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/0019598badd9f3d1/GoogleAppMeasurement-10.23.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' diff --git a/Package.swift b/Package.swift index 889a72f7788..96fb48f9eea 100644 --- a/Package.swift +++ b/Package.swift @@ -314,8 +314,8 @@ let package = Package( ), .binaryTarget( name: "FirebaseAnalytics", - url: "https://dl.google.com/firebase/ios/swiftpm/10.22.0/FirebaseAnalytics-rc1.zip", - checksum: "78ce96a59962a8946f5df8dd57c7d0d83fd3cf0e14f3efab65c8c65fbdf03a8a" + url: "https://dl.google.com/firebase/ios/swiftpm/10.23.0/FirebaseAnalytics.zip", + checksum: "3fb2f7a91480cffd1dfe9c8f212626cf67860f2686f4ec74122df0e9b411ec53" ), .target( name: "FirebaseAnalyticsSwiftTarget", @@ -1362,7 +1362,7 @@ func googleAppMeasurementDependency() -> Package.Dependency { return .package(url: appMeasurementURL, branch: "main") } - return .package(url: appMeasurementURL, exact: "10.22.1") + return .package(url: appMeasurementURL, exact: "10.23.0") } func abseilDependency() -> Package.Dependency { From 135d4c7a0a22b416d43b002c4d8c2c5c4f8b8ca5 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:19:37 -0400 Subject: [PATCH 094/104] [Release] Update FirebaseFirestoreInternal checksum (#12545) --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 96fb48f9eea..92a9f259386 100644 --- a/Package.swift +++ b/Package.swift @@ -1533,7 +1533,7 @@ func firestoreTargets() -> [Target] { return .binaryTarget( name: "FirebaseFirestoreInternal", url: "https://dl.google.com/firebase/ios/bin/firestore/10.23.0/FirebaseFirestoreInternal.zip", - checksum: "80050185438caedcf70a78737ed4da675445f18694b7617bf251962546bdec3b" + checksum: "1948320535897e818554f7b2e6e1eff8f6e0f8d779979daed881187613334c13" ) } }() From fcf5ced6dae2d43fced2581e673cc3b59bdb8ffa Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:36:18 -0400 Subject: [PATCH 095/104] [Release] Re-spin FST with signed artifact (#12549) --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 92a9f259386..6e59b210092 100644 --- a/Package.swift +++ b/Package.swift @@ -1532,8 +1532,8 @@ func firestoreTargets() -> [Target] { } else { return .binaryTarget( name: "FirebaseFirestoreInternal", - url: "https://dl.google.com/firebase/ios/bin/firestore/10.23.0/FirebaseFirestoreInternal.zip", - checksum: "1948320535897e818554f7b2e6e1eff8f6e0f8d779979daed881187613334c13" + url: "https://dl.google.com/firebase/ios/bin/firestore/10.23.0/rc1/FirebaseFirestoreInternal.zip", + checksum: "777f89e6c453cca8dfdcc375304cd3f3059b55c4beab86dce3061c0ece1e0556" ) } }() From 7834c057fab0eead78cd9a673ba521e61de7eb30 Mon Sep 17 00:00:00 2001 From: Jana Date: Thu, 14 Mar 2024 09:01:34 -0400 Subject: [PATCH 096/104] Remove calls to fstat in crashlytics (#12531) --- Crashlytics/Crashlytics/Helpers/FIRCLSFile.m | 21 ++++++++++++++------ Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m | 15 +++++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Crashlytics/Crashlytics/Helpers/FIRCLSFile.m b/Crashlytics/Crashlytics/Helpers/FIRCLSFile.m index 05e7561c9a8..27caa6dcdf7 100644 --- a/Crashlytics/Crashlytics/Helpers/FIRCLSFile.m +++ b/Crashlytics/Crashlytics/Helpers/FIRCLSFile.m @@ -33,7 +33,8 @@ static const size_t FIRCLSStringBufferLength = 16; const size_t FIRCLSWriteBufferLength = 1000; -static bool FIRCLSFileInit(FIRCLSFile* file, int fdm, bool appendMode, bool bufferWrites); +static bool FIRCLSFileInit( + FIRCLSFile* file, const char* path, int fdm, bool appendMode, bool bufferWrites); static void FIRCLSFileWriteToFileDescriptorOrBuffer(FIRCLSFile* file, const char* string, @@ -55,7 +56,8 @@ static void FIRCLSFileWriteToFileDescriptorOrBuffer(FIRCLSFile* file, #define CLS_FILE_DEBUG_LOGGING 0 #pragma mark - File Structure -static bool FIRCLSFileInit(FIRCLSFile* file, int fd, bool appendMode, bool bufferWrites) { +static bool FIRCLSFileInit( + FIRCLSFile* file, const char* path, int fd, bool appendMode, bool bufferWrites) { if (!file) { FIRCLSSDKLog("Error: file is null\n"); return false; @@ -83,9 +85,16 @@ static bool FIRCLSFileInit(FIRCLSFile* file, int fd, bool appendMode, bool buffe file->writtenLength = 0; if (appendMode) { - struct stat fileStats; - fstat(fd, &fileStats); - off_t currentFileSize = fileStats.st_size; + NSError* attributesError; + NSString* objCPath = [NSString stringWithCString:path encoding:NSUTF8StringEncoding]; + NSDictionary* fileAttributes = + [[NSFileManager defaultManager] attributesOfItemAtPath:objCPath error:&attributesError]; + if (attributesError != nil) { + FIRCLSErrorLog(@"Failed to read filesize from %@ with error %@", objCPath, attributesError); + return false; + } + NSNumber* fileSizeNumber = [fileAttributes objectForKey:NSFileSize]; + long long currentFileSize = [fileSizeNumber longLongValue]; if (currentFileSize > 0) { file->writtenLength += currentFileSize; } @@ -133,7 +142,7 @@ bool FIRCLSFileInitWithPathMode(FIRCLSFile* file, } } - return FIRCLSFileInit(file, fd, appendMode, bufferWrites); + return FIRCLSFileInit(file, path, fd, appendMode, bufferWrites); } bool FIRCLSFileClose(FIRCLSFile* file) { diff --git a/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m b/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m index e2f17eb5547..477541c784d 100644 --- a/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m +++ b/Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.m @@ -42,8 +42,6 @@ static void FIRCLSMachOHeaderValues(FIRCLSMachOSliceRef slice, static bool FIRCLSMachOSliceIsValid(FIRCLSMachOSliceRef slice); bool FIRCLSMachOFileInitWithPath(FIRCLSMachOFileRef file, const char* path) { - struct stat statBuffer; - if (!file || !path) { return false; } @@ -58,16 +56,23 @@ bool FIRCLSMachOFileInitWithPath(FIRCLSMachOFileRef file, const char* path) { return false; } - if (fstat(file->fd, &statBuffer) == -1) { + NSError* attributesError; + NSString* objCPath = [NSString stringWithCString:path encoding:NSUTF8StringEncoding]; + NSDictionary* fileAttributes = + [[NSFileManager defaultManager] attributesOfItemAtPath:objCPath error:&attributesError]; + if (attributesError != nil) { close(file->fd); return false; } + NSNumber* fileSizeNumber = [fileAttributes objectForKey:NSFileSize]; + long long currentFileSize = [fileSizeNumber longLongValue]; + NSFileAttributeType fileType = [fileAttributes objectForKey:NSFileType]; // We need some minimum size for this to even be a possible mach-o file. I believe // its probably quite a bit bigger than this, but this at least covers something. // We also need it to be a regular file. - file->mappedSize = (size_t)statBuffer.st_size; - if (statBuffer.st_size < 16 || !(statBuffer.st_mode & S_IFREG)) { + file->mappedSize = (size_t)currentFileSize; + if (currentFileSize < 16 || ![fileType isEqualToString:NSFileTypeRegular]) { close(file->fd); return false; } From ca737359dbbcba511d77137f1271ef0b751b581a Mon Sep 17 00:00:00 2001 From: themiswang Date: Thu, 14 Mar 2024 14:05:44 -0400 Subject: [PATCH 097/104] fix unit tests (#12553) --- .../Tests/Unit/RCNRemoteConfigTest.m | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index e02b8ecaabf..38cef18cdb3 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -1801,16 +1801,10 @@ - (void)testRealtimeStreamRequestBody { } - (void)testFetchAndActivateRolloutsNotifyInterop { - id mockNotificationCenter = [OCMockObject mockForClass:[NSNotificationCenter class]]; - [[mockNotificationCenter expect] postNotificationName:@"RolloutsStateDidChangeNotification" - object:[OCMArg any] - userInfo:[OCMArg any]]; - id mockSubscriber = [OCMockObject mockForProtocol:@protocol(FIRRolloutsStateSubscriber)]; - [[mockSubscriber expect] rolloutsStateDidChange:[OCMArg any]]; - - XCTestExpectation *expectation = [self - expectationWithDescription:[NSString - stringWithFormat:@"Test rollout update send notification"]]; + XCTestExpectation *notificationExpectation = + [self expectationForNotification:@"FIRRolloutsStateDidChangeNotification" + object:nil + handler:nil]; XCTAssertEqual(_configInstances[RCNTestRCInstanceDefault].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); @@ -1826,15 +1820,12 @@ - (void)testFetchAndActivateRolloutsNotifyInterop { XCTAssertGreaterThan( self->_configInstances[RCNTestRCInstanceDefault].lastFetchTime.timeIntervalSince1970, 0, @"last fetch time interval should be set."); - [expectation fulfill]; + [notificationExpectation fulfill]; }; [_configInstances[RCNTestRCInstanceDefault] fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion]; - [self waitForExpectationsWithTimeout:_expectationTimeout - handler:^(NSError *error) { - XCTAssertNil(error); - }]; + [self waitForExpectations:@[ notificationExpectation ] timeout:_expectationTimeout]; } #pragma mark - Test Helpers From 939a521c835ede7d30c058724a8b02f66a860c01 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:02:06 -0400 Subject: [PATCH 098/104] [Release] Add release note for signed artifact changes (#12558) --- Firestore/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index ee98034ed48..9c2b16d7b47 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,6 +1,8 @@ # 10.23.0 - [feature] Enable snapshot listener option to retrieve data from local cache only. (#12370) - [fixed] Update gRPC dependency to 1.62.* (#12098, #12021) +- [feature] Firestore's binary Swift Package Manager distribution uses + XCFrameworks with code signatures (#12238). # 10.22.0 - [fixed] Fix the flaky offline behaviour when using `arrayRemove` on `Map` object. (#12378) From e159d943b873a02296947f1a84ad80353f80a1b2 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 15 Mar 2024 13:29:08 -0700 Subject: [PATCH 099/104] Fix typo (#12565) --- .../Public/FirebaseFirestore/FIRSnapshotListenOptions.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h index 13d9903abef..c1ef6a3a3a1 100644 --- a/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h +++ b/Firestore/Source/Public/FirebaseFirestore/FIRSnapshotListenOptions.h @@ -61,7 +61,7 @@ NS_SWIFT_NAME(SnapshotListenOptions) - (instancetype)init NS_DESIGNATED_INITIALIZER; /** - * Creates and returns a new `SnapshotListenOptions` object with with all properties of the current + * Creates and returns a new `SnapshotListenOptions` object with all properties of the current * `SnapshotListenOptions` object plus the new property specifying whether metadata-only changes * should trigger snapshot events * @@ -70,7 +70,7 @@ NS_SWIFT_NAME(SnapshotListenOptions) - (FIRSnapshotListenOptions *)optionsWithIncludeMetadataChanges:(BOOL)includeMetadataChanges; /** - * Creates and returns a new `SnapshotListenOptions` object with with all properties of the current + * Creates and returns a new `SnapshotListenOptions` object with all properties of the current * `SnapshotListenOptions` object plus the new property specifying the source that the snapshot * listener listens to. * From db885d7426285d735e3f92531b11a99a80ae2d7e Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 3 Apr 2024 13:27:04 -0700 Subject: [PATCH 100/104] style.sh changes --- .../ViewControllers/AuthViewController.swift | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index 80cf8fa35af..ea6e67821bc 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -@testable import FirebaseAuth // For Sign in with Facebook import FBSDKLoginKit -import FirebaseAuth +@testable import FirebaseAuth // [START auth_import] import FirebaseCore import GameKit @@ -117,7 +116,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .verifyClient: verifyClient() - + case .deleteApp: deleteApp() } @@ -413,7 +412,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { func verifyClient() { AppManager.shared.auth().tokenManager.getTokenInternal { token, error in - if(token == nil) { + if token == nil { print("Verify iOS Client failed.") return } @@ -422,33 +421,37 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { isSandbox: token?.type == .sandbox, requestConfiguration: AppManager.shared.auth().requestConfiguration ) - + Task { do { let verifyResponse = try await AuthBackend.call(with: request) - + guard let receipt = verifyResponse.receipt, let timeoutDate = verifyResponse.suggestedTimeOutDate else { print("Internal Auth Error: invalid VerifyClientResponse.") return } - + let timeout = timeoutDate.timeIntervalSinceNow do { - let credential = await AppManager.shared.auth().appCredentialManager.didStartVerification(withReceipt: receipt, timeout: timeout) - + let credential = await AppManager.shared.auth().appCredentialManager + .didStartVerification( + withReceipt: receipt, + timeout: timeout + ) + guard credential.secret != nil else { print("Failed to receive remote notification to verify App ID.") return } - + let testPhoneNumber = "+16509964692" let request = SendVerificationCodeRequest( phoneNumber: testPhoneNumber, codeIdentity: CodeIdentity.credential(credential), requestConfiguration: AppManager.shared.auth().requestConfiguration ) - + do { _ = try await AuthBackend.call(with: request) print("Verify iOS client succeeded") @@ -463,15 +466,14 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { } } - func deleteApp() { - AppManager.shared.app.delete({ success in + AppManager.shared.app.delete { success in if success { print("App deleted successfully.") } else { print("Failed to delete app.") } - }) + } } // MARK: - Private Helpers From 19b6c574fafabb91c8eff56404c356a786000f58 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 3 Apr 2024 22:35:35 -0700 Subject: [PATCH 101/104] remove authMenu test --- .../AuthenticationExampleUITests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift index 67fcf65e0d1..b363d4a986e 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift @@ -35,11 +35,11 @@ class AuthenticationExampleUITests: XCTestCase { // Verify that Auth Example app launched successfully XCTAssertTrue(app.navigationBars["Firebase Auth"].exists) } - - func testAuthOptions() { - // There are 15 sign in methods, each with its own cell - XCTAssertEqual(app.tables.cells.count, 16) - } +// TODO: Modify this test after code refactoring, current AuthMenu items aren't necessarily sign in methods +// func testAuthOptions() { +// // There are 16 sign in methods, each with its own cell +// XCTAssertEqual(app.tables.cells.count, 16) +// } func testAuthAnonymously() { app.staticTexts["Anonymous Authentication"].tap() From b55b1f1cd0cfeae07b3148ed0908fec8a676ea57 Mon Sep 17 00:00:00 2001 From: Pragati Date: Thu, 4 Apr 2024 11:01:04 -0700 Subject: [PATCH 102/104] style --- .../AuthenticationExampleUITests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift index b363d4a986e..b35893a1574 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift @@ -35,7 +35,8 @@ class AuthenticationExampleUITests: XCTestCase { // Verify that Auth Example app launched successfully XCTAssertTrue(app.navigationBars["Firebase Auth"].exists) } -// TODO: Modify this test after code refactoring, current AuthMenu items aren't necessarily sign in methods + + // TODO: Modify this test after code refactoring, current AuthMenu items aren't necessarily sign in methods // func testAuthOptions() { // // There are 16 sign in methods, each with its own cell // XCTAssertEqual(app.tables.cells.count, 16) From 723538653b96a05e3d4907f1458f2abb5e3b29e8 Mon Sep 17 00:00:00 2001 From: Pragati Date: Thu, 4 Apr 2024 14:30:33 -0700 Subject: [PATCH 103/104] comment out AuthMenu UI test --- .../AuthenticationExampleUITests.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift index 67fcf65e0d1..1a0fc7b02b8 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift @@ -36,10 +36,11 @@ class AuthenticationExampleUITests: XCTestCase { XCTAssertTrue(app.navigationBars["Firebase Auth"].exists) } - func testAuthOptions() { - // There are 15 sign in methods, each with its own cell - XCTAssertEqual(app.tables.cells.count, 16) - } + // TODO: Modify this test after code refactoring, current AuthMenu items aren't necessarily sign in methods + // func testAuthOptions() { + // // There are 16 sign in methods, each with its own cell + // XCTAssertEqual(app.tables.cells.count, 16) + // } func testAuthAnonymously() { app.staticTexts["Anonymous Authentication"].tap() From 1c331ebf5640def5997f8dfabf8923da1c0d1739 Mon Sep 17 00:00:00 2001 From: Pragati Date: Thu, 4 Apr 2024 15:42:55 -0700 Subject: [PATCH 104/104] cleanup --- .../Auth Provider Icons/Contents.json | 6 +++--- .../Game Center.imageset/Contents.json | 2 +- .../Game Center.imageset/game-center icon.png | Bin 27086 -> 0 bytes .../Game Center.imageset/gamecontroller.png | Bin 0 -> 764 bytes .../AccountLinkingViewController.swift | 7 ------- 5 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/game-center icon.png create mode 100644 FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/gamecontroller.png diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Contents.json b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Contents.json index da4a164c918..73c00596a7f 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Contents.json +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/Contents.json b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/Contents.json index ca69c269602..8cd53c205c6 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/Contents.json +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/Contents.json @@ -5,7 +5,7 @@ "scale" : "1x" }, { - "filename" : "game-center icon.png", + "filename" : "gamecontroller.png", "idiom" : "universal", "scale" : "2x" }, diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/game-center icon.png b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/game-center icon.png deleted file mode 100644 index 4c96fc2e25cc5b3d40de4c06935a9269d28aefdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27086 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 diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/gamecontroller.png b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Assets.xcassets/Auth Provider Icons/Game Center.imageset/gamecontroller.png new file mode 100644 index 0000000000000000000000000000000000000000..14b182a9c457c53d62bc0d28c34d4a7cda3a9a10 GIT binary patch literal 764 zcmeAS@N?(olHy`uVBq!ia0vp^vOp}v!3HFkAH8}DNHG=%xjQkeJ16rJ$Z<)H@J#dd zWzYh$IT#q*GZ|PwN`P1jh#44|7cep~18GK(*a9ZFtn312I9mv$Fh;wvfq{W3&C|s( z#DjP4)aZ`hK#}9UlDs?n3O(~?&V1&|>B?z(l-Wzu>*AE6nHsLVn*MP%8X|F1*w3zZ z37q2XbLuVY&ixA0#fvO?}=BVw{ zWN0ANc0gf8v-}5MyMxsgyyqWi*0gUbN?7-iWmlpg&snMckGi}6GKtIc99B5>O?rd8 z+)=i-HG!KgSc4xZ-C?``OLR}GSN{8TCWl=znrCicxx!Tu86jG?O`x7BP14lH>hG55 z7v=1H;jiwzEf2eSkMY`1o+nj~4=jBeUN5+J zAhlp!?FY$UCfui^%D%e(yLKqz+{6p+#q4ufo)mYp}P zP8?n;f3;3%b`fM?c!yu}RL}n{ocHK>5OR2iYxTrJLSLoHk@$ zw(JKJ&;7Dgt`vX&uWBov-deKKCS~15Lspw>d2jdorN4@Q{aJ{)TIS8QbB)yx!k&H# zn32jI(UtYfckxq8uJ@-(_ITxpn(TOyV!^$0&usPt!yk^{tD}6s<+oLCklN>Pg;{3( zU5n=LiOb!Vz5Q|RpwI8sd{$9fo6nXX+x@C0H=yQ$ZR5wf$`x-mi2nnod(=sl(`1!#c@dR6ung1ELU$6M8x5qUPl=3`X{an^LB{Ts55cElj literal 0 HcmV?d00001 diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift index 8752e8b0e6b..b6e00dbe309 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift @@ -241,7 +241,6 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate // Step 1: Ensure Game Center Authentication guard GKLocalPlayer.local.isAuthenticated else { print("Error: Player not authenticated with Game Center.") - // TODO: Handle the 'not authenticated' scenario (e.g., prompt the user) return } @@ -249,13 +248,11 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate GameCenterAuthProvider.getCredential { credential, error in if let error = error { print("Error getting Game Center credential: \(error.localizedDescription)") - // TODO: Handle the credential error return } guard let credential = credential else { print("Error: Missing Game Center credential") - // TODO: Handle the missing credential case return } @@ -263,12 +260,8 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate Auth.auth().currentUser?.link(with: credential) { authResult, error in if let error = error { print("Error linking Game Center to Firebase: \(error.localizedDescription)") - // TODO: Handle the linking error return } - - // Linking successful - print("Successfully linked Game Center to Firebase") } } }