diff --git a/.github/workflows/abtesting.yml b/.github/workflows/abtesting.yml index a8701041b72..6edd5ef83b0 100644 --- a/.github/workflows/abtesting.yml +++ b/.github/workflows/abtesting.yml @@ -13,9 +13,10 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' - 'Gemfile*' schedule: - # Run every day at 1am(PST) - cron uses UTC times + # Run every day at 2am (PDT) / 5am (EDT) - cron uses UTC times - cron: '0 9 * * *' concurrency: @@ -40,29 +41,15 @@ jobs: product: FirebaseABTesting 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' - - env: + uses: ./.github/workflows/common_quickstart.yml + with: + product: ABTesting + is_legacy: true + setup_command: scripts/setup_quickstart.sh abtesting + plist_src_path: scripts/gha-encrypted/qs-database.plist.gpg + plist_dst_path: quickstart-ios/database/GoogleService-Info.plist + secrets: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Setup quickstart - env: - LEGACY: true - run: scripts/setup_quickstart.sh abtesting - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-abtesting.plist.gpg \ - quickstart-ios/abtesting/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - env: - LEGACY: true - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh ABTesting true) quickstart-ftl-cron-only: # Don't run on private repo. @@ -70,7 +57,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} runs-on: macos-15 steps: - uses: actions/checkout@v4 @@ -86,7 +72,7 @@ jobs: run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-abtesting.plist.gpg \ quickstart-ios/abtesting/GoogleService-Info.plist "$plist_secret" - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Build swift quickstart env: LEGACY: true @@ -116,7 +102,7 @@ jobs: - name: Setup Bundler run: scripts/setup_bundler.sh - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: PodLibLint ABTesting Cron run: | scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb \ diff --git a/.github/workflows/analytics.yml b/.github/workflows/analytics.yml index bb8d5c44141..6e835ee1eff 100644 --- a/.github/workflows/analytics.yml +++ b/.github/workflows/analytics.yml @@ -9,7 +9,7 @@ on: - '.github/workflows/analytics.yml' - 'Gemfile*' schedule: - # Run every day at 1am (PST) - cron uses UTC times + # Run every day at 2am (PDT) / 5am (EDT) - cron uses UTC times - cron: '0 9 * * *' concurrency: diff --git a/.github/workflows/appdistribution.yml b/.github/workflows/appdistribution.yml index 026e95f81ac..808e7ffe697 100644 --- a/.github/workflows/appdistribution.yml +++ b/.github/workflows/appdistribution.yml @@ -14,7 +14,7 @@ on: - '.github/workflows/common_catalyst.yml' - 'Gemfile*' schedule: - # Run every day at 1am (PST) - cron uses UTC times + # Run every day at 2am (PDT) / 5am (EDT) - cron uses UTC times - cron: '0 9 * * *' concurrency: @@ -54,6 +54,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Setup Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: PodLibLint App Distribution Cron diff --git a/.github/workflows/archiving.yml b/.github/workflows/archiving.yml index c70afe14b8e..c3b58ebbb69 100644 --- a/.github/workflows/archiving.yml +++ b/.github/workflows/archiving.yml @@ -6,7 +6,7 @@ on: paths: - '.github/workflows/archiving.yml' schedule: - # Run every day at 2am (PST) - cron uses UTC times + # Run every day at 3am (PDT) / 6am (EDT) - cron uses UTC times # This is set to 3 hours after zip workflow finishes so zip testing can run after. - cron: '0 10 * * *' @@ -27,6 +27,8 @@ jobs: pod: ["FirebaseAppDistribution", "FirebaseInAppMessaging", "FirebasePerformance"] steps: - uses: actions/checkout@v4 + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode_16.4.app - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126 with: cache_key: cron-${{ matrix.os }} @@ -50,6 +52,8 @@ jobs: pod: ["FirebaseABTesting", "FirebaseAuth", "FirebaseCore", "FirebaseCrashlytics", "FirebaseDatabase", "FirebaseFirestore", "FirebaseFunctions", "FirebaseMessaging", "FirebaseRemoteConfig", "FirebaseStorage"] steps: - uses: actions/checkout@v4 + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode_16.4.app - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126 with: cache_key: pods-${{ matrix.os }} diff --git a/.github/workflows/auth.yml b/.github/workflows/auth.yml index 03e8b2526f2..3f1d3142fe0 100644 --- a/.github/workflows/auth.yml +++ b/.github/workflows/auth.yml @@ -13,10 +13,11 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' - 'scripts/gha-encrypted/AuthSample/SwiftApplication.plist.gpg' - 'Gemfile*' schedule: - # Run every day at 1am (PST) - cron uses UTC times + # Run every day at 2am (PDT) / 5am (EDT) - cron uses UTC times - cron: '0 9 * * *' env: @@ -86,30 +87,22 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - timeout_minutes: 120 + timeout_minutes: 15 max_attempts: 3 - retry_on: error retry_wait_seconds: 120 command: ([ -z $plist_secret ] || scripts/build.sh Auth iOS ${{ matrix.scheme }}) 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' - - env: + uses: ./.github/workflows/common_quickstart.yml + with: + product: Authentication + is_legacy: false + setup_command: scripts/setup_quickstart.sh authentication + plist_src_path: scripts/gha-encrypted/qs-authentication.plist.gpg + plist_dst_path: quickstart-ios/authentication/GoogleService-Info.plist + run_tests: false + secrets: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup quickstart - run: scripts/setup_quickstart.sh authentication - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-auth.plist.gpg \ - quickstart-ios/authentication/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Authentication false) # TODO(@sunmou99): currently have issue with this job, will re-enable it once the issue resolved. # quickstart-ftl-cron-only: @@ -129,7 +122,7 @@ jobs: # - name: Setup quickstart # run: scripts/setup_quickstart.sh authentication # - name: Install Secret GoogleService-Info.plist - # run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-auth.plist.gpg \ + # run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-authentication.plist.gpg \ # quickstart-ios/authentication/GoogleService-Info.plist "$plist_secret" # - name: Build swift quickstart # run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_ftl.sh Authentication) @@ -153,17 +146,20 @@ jobs: '--use-static-frameworks' ] needs: pod_lib_lint + env: + plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Setup Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Configure test keychain run: scripts/configure_test_keychain.sh - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - timeout_minutes: 120 + timeout_minutes: 15 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/client_app.yml b/.github/workflows/client_app.yml index 150d879edca..12d64a91d67 100644 --- a/.github/workflows/client_app.yml +++ b/.github/workflows/client_app.yml @@ -13,7 +13,7 @@ on: - "IntegrationTesting/ClientApp/**" - "Gemfile*" schedule: - # Run every day at 12am (PST) - cron uses UTC times + # Run every day at 1am (PDT) / 4am (EDT) - cron uses UTC times - cron: "0 8 * * *" env: @@ -27,12 +27,16 @@ jobs: client-app-spm: if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' strategy: - # TODO: Add Xcode matrix when Xcode 16 is ubiquitous on CI runners. matrix: #TODO(ncooke3): Add multi-platform support: tvOS, macOS, catalyst platform: [iOS] scheme: [ClientApp] os: [macos-14, macos-15] + include: + - os: macos-14 + xcode: Xcode_16.2 + - os: macos-15 + xcode: Xcode_16.4 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -40,8 +44,8 @@ jobs: with: cache_key: ${{ matrix.os }} - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Build Client App –– ${{ matrix.platform }} + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Build Client App - ${{ matrix.platform }} run: scripts/third_party/travis/retry.sh ./scripts/build.sh ${{ matrix.scheme }} ${{ matrix.platform }} xcodebuild client-app-spm-source-firestore: @@ -50,12 +54,16 @@ jobs: FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 FIREBASE_SOURCE_FIRESTORE: 1 strategy: - # TODO: Add Xcode matrix when Xcode 16 is ubiquitous on CI runners. matrix: #TODO(ncooke3): Add multi-platform support: tvOS, macOS, catalyst platform: [iOS] scheme: [ClientApp] os: [macos-14, macos-15] + include: + - os: macos-14 + xcode: Xcode_16.2 + - os: macos-15 + xcode: Xcode_16.4 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -63,18 +71,22 @@ jobs: with: cache_key: ${{ matrix.os }} - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Build Client App –– ${{ matrix.platform }} + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Build Client App - ${{ matrix.platform }} run: scripts/third_party/travis/retry.sh ./scripts/build.sh ${{ matrix.scheme }} ${{ matrix.platform }} xcodebuild client-app-cocoapods: # 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' strategy: - # TODO: Add Xcode matrix when Xcode 16 is ubiquitous on CI runners. matrix: scheme: [ClientApp-CocoaPods] os: [macos-14, macos-15] + include: + - os: macos-14 + xcode: Xcode_16.2 + - os: macos-15 + xcode: Xcode_16.4 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -85,7 +97,7 @@ jobs: - name: Setup Bundler run: scripts/setup_bundler.sh - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Prereqs run: scripts/install_prereqs.sh ClientApp iOS xcodebuild - name: Build diff --git a/.github/workflows/cocoapods-integration.yml b/.github/workflows/cocoapods-integration.yml index 0a86f76e18b..8853b405a55 100644 --- a/.github/workflows/cocoapods-integration.yml +++ b/.github/workflows/cocoapods-integration.yml @@ -8,7 +8,7 @@ on: - '.github/workflows/cocoapods-integration.yml' - 'Gemfile*' schedule: - # Run every day at 2am (PST) - cron uses UTC times + # Run every day at 3am (PDT) / 6am (EDT) - cron uses UTC times - cron: '0 10 * * *' concurrency: @@ -26,6 +26,8 @@ jobs: - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126 with: cache_key: ${{ matrix.os }} + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Get realpath run: brew install coreutils - name: Build and test diff --git a/.github/workflows/combine.yml b/.github/workflows/combine.yml index 268d7c21d8c..37e6741ca04 100644 --- a/.github/workflows/combine.yml +++ b/.github/workflows/combine.yml @@ -42,7 +42,7 @@ on: # - 'Firestore/**' # (Disabled to avoid building Firestore in presubmits) schedule: - # Run every day at 11pm (PST) - cron uses UTC times + # Run every day at 12am (PDT) / 3am (EDT) - cron uses UTC times - cron: '0 7 * * *' concurrency: @@ -67,6 +67,9 @@ jobs: - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + - name: Install xcpretty run: gem install xcpretty @@ -88,6 +91,8 @@ jobs: with: cache_key: ${{ matrix.os }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Install xcpretty diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 1a82d76b36f..16ba348c402 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -67,7 +67,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Generate Swift Package.resolved id: swift_package_resolve run: swift package resolve @@ -104,10 +104,6 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Install visionOS, if needed. - if: matrix.platform == 'visionOS' - run: ls $(xcode-select -p)/Platforms/XROS.platform || \ - { xcodebuild -downloadPlatform visionOS } - name: Run setup command, if needed. if: inputs.setup_command != '' run: ${{ inputs.setup_command }} @@ -116,9 +112,8 @@ jobs: - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 if: contains(join(inputs.platforms), matrix.platform) || matrix.os == 'macos-14' with: - timeout_minutes: 120 + timeout_minutes: 15 max_attempts: 3 - retry_on: error retry_wait_seconds: 120 command: | ./scripts/build.sh \ diff --git a/.github/workflows/common_catalyst.yml b/.github/workflows/common_catalyst.yml index 6948e4e3197..dd235d3847d 100644 --- a/.github/workflows/common_catalyst.yml +++ b/.github/workflows/common_catalyst.yml @@ -38,12 +38,11 @@ jobs: - name: Setup Bundler run: scripts/setup_bundler.sh - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - timeout_minutes: 120 + timeout_minutes: 15 max_attempts: 3 - retry_on: error retry_wait_seconds: 120 command: | scripts/test_catalyst.sh \ diff --git a/.github/workflows/common_cocoapods.yml b/.github/workflows/common_cocoapods.yml index cf054ef7743..e672dabcaf9 100644 --- a/.github/workflows/common_cocoapods.yml +++ b/.github/workflows/common_cocoapods.yml @@ -139,9 +139,8 @@ jobs: - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 if: contains(join(inputs.platforms), matrix.platform) || matrix.os == 'macos-14' with: - timeout_minutes: 120 + timeout_minutes: 15 max_attempts: 3 - retry_on: error retry_wait_seconds: 120 command: | scripts/pod_lib_lint.rb ${{ inputs.product }}.podspec --platforms=${{ matrix.platform }} \ diff --git a/.github/workflows/common_quickstart.yml b/.github/workflows/common_quickstart.yml new file mode 100644 index 00000000000..a3f965796ea --- /dev/null +++ b/.github/workflows/common_quickstart.yml @@ -0,0 +1,114 @@ +name: common_quickstart + +permissions: + contents: read + +on: + workflow_call: + # Re-usable workflows do not automatically inherit the caller's secrets. + # + # This workflow decrypts encrypted files, so the calling workflow needs to + # pass the secret to the re-usable workflow. + # + # Example: + # + # quickstart: + # uses: ./.github/workflows/common_quickstart.yml + # with: + # # ... + # secrets: + # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + # + secrets: + plist_secret: + required: true + + inputs: + # The product to test be tested (e.g. `ABTesting`). + product: + type: string + required: true + + # Whether to test the legacy version of the quickstart. + is_legacy: + type: boolean + required: true + + # The path to the encrypted `GoogleService-Info.plist` file. + plist_src_path: + type: string + required: true + + # The destination path for the decrypted `GoogleService-Info.plist` file. + plist_dst_path: + type: string + required: true + + # The type of quickstart to test. + # + # Options: [swift, objc] + quickstart_type: + type: string + required: false + default: objc + + # Whether to run tests or just build. Defaults to true. + run_tests: + type: boolean + required: false + default: true + + # A command to execute before testing. + # + # Example: `scripts/setup_quickstart.sh functions` + setup_command: + type: string + required: false + default: "" + +jobs: + quickstart: + # Run on the main repo's scheduled jobs or pull requests and manual workflow invocations. + if: (github.repository == 'firebase/firebase-ios-sdk' && github.event_name == 'schedule') || contains(fromJSON('["pull_request", "workflow_dispatch"]'), github.event_name) + env: + plist_secret: ${{ secrets.plist_secret }} + LEGACY: ${{ inputs.is_legacy && true || '' }} + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + - name: Run setup command. + run: ${{ inputs.setup_command }} + - name: Install Secret GoogleService-Info.plist + run: | + scripts/decrypt_gha_secret.sh \ + ${{ inputs.plist_src_path }} \ + ${{ inputs.plist_dst_path }} \ + "$plist_secret" + - name: Build ${{ inputs.product }} Quickstart (${{ inputs.quickstart_type }} / ${{ inputs.is_legacy && 'Legacy' || 'Non-Legacy' }}) + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 + with: + timeout_minutes: 15 + max_attempts: 3 + retry_wait_seconds: 120 + command: | + scripts/test_quickstart.sh \ + ${{ inputs.product }} \ + ${{ inputs.run_tests }} \ + ${{ inputs.quickstart_type }} + # Failure sequence to upload artifact. + - id: lowercase_product + if: ${{ failure() }} + run: | + lowercase_product=$(echo "${{ inputs.product }}" | tr '[:upper:]' '[:lower:]') + echo "lowercase_product=$lowercase_product" >> $GITHUB_OUTPUT + - name: Remove data before upload. + if: ${{ failure() }} + run: scripts/remove_data.sh ${{ steps.lowercase_product.outputs.lowercase_product }} + - uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: quickstart_artifacts_${{ steps.lowercase_product.outputs.lowercase_product }} + path: quickstart-ios/ diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index f380a85947b..0c30320895a 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -14,7 +14,7 @@ on: - '.github/workflows/common_catalyst.yml' - 'Gemfile*' schedule: - # Run every day at 2am (PST) - cron uses UTC times + # Run every day at 3am (PDT) / 6am (EDT) - cron uses UTC times - cron: '0 10 * * *' concurrency: diff --git a/.github/workflows/core_extension.yml b/.github/workflows/core_extension.yml index 15b86f38538..b36caf5c85c 100644 --- a/.github/workflows/core_extension.yml +++ b/.github/workflows/core_extension.yml @@ -14,7 +14,7 @@ on: - '.github/workflows/common_cocoapods.yml' - 'Gemfile*' schedule: - # Run every day at 2am (PST) - cron uses UTC times + # Run every day at 3am (PDT) / 6am (EDT) - cron uses UTC times - cron: '0 10 * * *' jobs: @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: PodLibLint CoreInternal Cron diff --git a/.github/workflows/core_internal.yml b/.github/workflows/core_internal.yml index d1b8dd6cf73..f536f6c4656 100644 --- a/.github/workflows/core_internal.yml +++ b/.github/workflows/core_internal.yml @@ -15,7 +15,7 @@ on: - '.github/workflows/common_catalyst.yml' - 'Gemfile*' schedule: - # Run every day at 2am (PST) - cron uses UTC times + # Run every day at 3am (PDT) / 6am (EDT) - cron uses UTC times - cron: '0 10 * * *' jobs: @@ -54,7 +54,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: PodLibLint CoreInternal Cron diff --git a/.github/workflows/crashlytics.yml b/.github/workflows/crashlytics.yml index a32d474a61b..e88f692ebc3 100644 --- a/.github/workflows/crashlytics.yml +++ b/.github/workflows/crashlytics.yml @@ -13,10 +13,11 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' - 'Interop/Analytics/Public/*.h' - 'Gemfile*' schedule: - # Run every day at 10am (PST) - cron uses UTC times + # Run every day at 7pm (PDT) / 10pm (EDT) - cron uses UTC times - cron: '0 2 * * *' concurrency: @@ -42,34 +43,21 @@ jobs: buildonly_platforms: tvOS, macOS, watchOS 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' - - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Setup quickstart - run: scripts/setup_quickstart.sh crashlytics - env: - LEGACY: true - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-crashlytics.plist.gpg \ - quickstart-ios/crashlytics/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: | + uses: ./.github/workflows/common_quickstart.yml + with: + product: Crashlytics + is_legacy: true + quickstart_type: swift + setup_command: | + scripts/setup_quickstart.sh crashlytics mkdir quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics # Set the deployed pod location of run and upload-symbols with the development pod version. cp Crashlytics/run quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ cp Crashlytics/upload-symbols quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ - ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Crashlytics true swift) - env: - LEGACY: true + plist_src_path: scripts/gha-encrypted/qs-crashlytics.plist.gpg + plist_dst_path: quickstart-ios/crashlytics/GoogleService-Info.plist + secrets: + plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} quickstart-ftl-cron-only: # Don't run on private repo. @@ -77,7 +65,6 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} runs-on: macos-15 steps: - uses: actions/checkout@v4 @@ -86,7 +73,7 @@ jobs: with: python-version: '3.11' - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup quickstart run: scripts/setup_quickstart.sh crashlytics env: @@ -130,11 +117,10 @@ jobs: - name: Setup Bundler run: scripts/setup_bundler.sh - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - timeout_minutes: 120 + timeout_minutes: 15 max_attempts: 3 - retry_on: error retry_wait_seconds: 120 command: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseCrashlytics.podspec --platforms=${{ matrix.target }} ${{ matrix.flags }} diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 928c3bf28ad..a5a297b445f 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -16,10 +16,11 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' - 'Gemfile*' - 'scripts/run_database_emulator.sh' schedule: - # Run every day at 2am (PST) - cron uses UTC times + # Run every day at 3am (PDT) / 6am (EDT) - cron uses UTC times - cron: '0 10 * * *' concurrency: @@ -69,26 +70,20 @@ jobs: run: scripts/third_party/travis/retry.sh scripts/build.sh Database iOS integration 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' - env: + uses: ./.github/workflows/common_quickstart.yml + strategy: + matrix: + quickstart_type: [objc, swift] + with: + product: Database + is_legacy: false + setup_command: scripts/setup_quickstart.sh database + plist_src_path: scripts/gha-encrypted/qs-database.plist.gpg + plist_dst_path: quickstart-ios/database/GoogleService-Info.plist + quickstart_type: ${{ matrix.quickstart_type }} + run_tests: false + secrets: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup quickstart - run: scripts/setup_quickstart.sh database - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-database.plist.gpg \ - quickstart-ios/database/GoogleService-Info.plist "$plist_secret" - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Test objc quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false swift) database-cron-only: # Don't run on private repo. @@ -106,7 +101,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: PodLibLint database Cron diff --git a/.github/workflows/firebase_app_check.yml b/.github/workflows/firebase_app_check.yml index ff30db15a66..fcec95dad4e 100644 --- a/.github/workflows/firebase_app_check.yml +++ b/.github/workflows/firebase_app_check.yml @@ -14,7 +14,7 @@ on: - '.github/workflows/common_catalyst.yml' - 'Gemfile*' schedule: - # Run every day at 11pm (PST) - cron uses UTC times + # Run every day at 12am (PDT) / 3am (EDT) - cron uses UTC times - cron: '0 7 * * *' concurrency: @@ -58,7 +58,7 @@ jobs: with: cache_key: ${{ matrix.diagnostics }} - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - name: iOS Unit Tests @@ -84,7 +84,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: PodLibLint FirebaseAppCheck Cron diff --git a/.github/workflows/firebaseai.yml b/.github/workflows/firebaseai.yml index 7ad2a9dff29..16e3fdb08c5 100644 --- a/.github/workflows/firebaseai.yml +++ b/.github/workflows/firebaseai.yml @@ -13,7 +13,7 @@ on: # Do not run for documentation-only PRs. - '!**.md' schedule: - # Run every day at 11pm (PST) - cron uses UTC times + # Run every day at 12am (PDT) / 3am (EDT) - cron uses UTC times - cron: '0 7 * * *' workflow_dispatch: @@ -82,10 +82,17 @@ jobs: setup_command: scripts/update_vertexai_responses.sh quickstart: - runs-on: macos-15 + strategy: + matrix: + include: + - os: macos-15 + xcode: Xcode_16.4 + runs-on: ${{ matrix.os }} env: BRANCH_NAME: ${{ github.head_ref || github.ref_name || 'main' }} steps: - uses: actions/checkout@v4 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Build Quickstart run: scripts/quickstart_build_spm.sh FirebaseAI diff --git a/.github/workflows/firebasepod.yml b/.github/workflows/firebasepod.yml index 8f7fd3af5ef..de92ee23206 100644 --- a/.github/workflows/firebasepod.yml +++ b/.github/workflows/firebasepod.yml @@ -11,7 +11,7 @@ on: - '.github/workflows/firebasepod.yml' - 'Gemfile*' schedule: - # Run every day at 1am (PST) - cron uses UTC times + # Run every day at 2am (PDT) / 5am (EDT) - cron uses UTC times - cron: '0 9 * * *' concurrency: @@ -34,8 +34,13 @@ jobs: - name: Setup Bundler run: scripts/setup_bundler.sh - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Prereqs run: scripts/install_prereqs.sh FirebasePod iOS - name: Build run: scripts/build.sh FirebasePod iOS + - uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: firebasepod-xcodebuild-build.log + path: xcodebuild-build.log diff --git a/.github/workflows/firestore.yml b/.github/workflows/firestore.yml index 3e927287a2e..2a935f81fa0 100644 --- a/.github/workflows/firestore.yml +++ b/.github/workflows/firestore.yml @@ -18,7 +18,7 @@ on: workflow_dispatch: pull_request: schedule: - # Run every day at 12am (PST) - cron uses UTC times + # Run every day at 1am (PDT) / 4am (EDT) - cron uses UTC times - cron: '0 8 * * *' concurrency: @@ -375,6 +375,9 @@ jobs: - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + - name: Setup build run: scripts/install_prereqs.sh Firestore ${{ matrix.target }} xcodebuild @@ -578,7 +581,7 @@ jobs: with: cache_key: ${{ matrix.target }} - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - name: Build Test - Binary @@ -595,24 +598,16 @@ jobs: if: needs.*.result == 'failure' run: exit 1 - # Disable until FirebaseUI is updated to accept Firebase 9 and quickstart is updated to accept - # Firebase UI 12 + # TODO: Disable until FirebaseUI is updated to accept Firebase 9 and + # quickstart is updated to accept Firebase UI 12 # 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' - # env: + # uses: ./.github/workflows/common_quickstart.yml + # with: + # product: Firestore + # is_legacy: true + # setup_command: scripts/setup_quickstart.sh firestore + # plist_src_path: scripts/gha-encrypted/qs-firestore.plist.gpg + # plist_dst_path: quickstart-ios/firestore/GoogleService-Info.plist + # run_tests: false + # secrets: # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # runs-on: macos-14 - # needs: check - - # steps: - # - uses: actions/checkout@v4 - # - name: Setup quickstart - # run: scripts/setup_quickstart.sh firestore - # - name: Install Secret GoogleService-Info.plist - # run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ - # quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" - # - name: Test swift quickstart - # run: ([ -z $plist_secret ] || - # scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Firestore false) diff --git a/.github/workflows/functions.yml b/.github/workflows/functions.yml index c7a8b6e54ca..0563dea4059 100644 --- a/.github/workflows/functions.yml +++ b/.github/workflows/functions.yml @@ -20,7 +20,7 @@ on: - 'Gemfile*' schedule: - # Run every day at 1am (PST) - cron uses UTC times + # Run every day at 2am (PDT) / 5am (EDT) - cron uses UTC times - cron: '0 9 * * *' concurrency: @@ -50,40 +50,29 @@ jobs: with: target: FirebaseFunctionsUnit - # TODO: Move to macos-14 and Xcode 15. The legacy quickstart uses material which doesn't build on Xcode 15. + # TODO: The legacy quickstart uses material which doesn't build on Xcode 15. # 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' - # env: - # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # LEGACY: true - # # TODO: Move to macos-14 and Xcode 15. The legacy quickstart uses material which doesn't build on Xcode 15. - # runs-on: macos-12 - - # steps: - # - uses: actions/checkout@v4 - # - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - # - name: Setup quickstart - # run: scripts/setup_quickstart.sh functions - # - name: install secret googleservice-info.plist - # run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-functions.plist.gpg \ - # quickstart-ios/functions/GoogleService-Info.plist "$plist_secret" - # - name: Setup custom URL scheme - # run: sed -i '' 's/REVERSED_CLIENT_ID/com.googleusercontent.apps.1025801074639-6p6ebi8amuklcjrto20gvpe295smm8u6/' quickstart-ios/functions/LegacyFunctionsQuickstart/FunctionsExample/Info.plist - # - name: Test objc quickstart - # run: ([ -z $plist_secret ] || - # scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Functions true) - # - name: Test swift quickstart - # run: ([ -z $plist_secret ] || - # scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Functions true swift) + # uses: ./.github/workflows/common_quickstart.yml + # strategy: + # matrix: + # quickstart_type: [objc, swift] + # with: + # product: Functions + # is_legacy: true + # setup_command: | + # scripts/setup_quickstart.sh functions + # sed -i '' 's/REVERSED_CLIENT_ID/com.googleusercontent.apps.1025801074639-6p6ebi8amuklcjrto20gvpe295smm8u6/' quickstart-ios/functions/LegacyFunctionsQuickstart/FunctionsExample/Info.plist + # plist_src_path: scripts/gha-encrypted/qs-functions.plist.gpg + # plist_dst_path: quickstart-ios/functions/GoogleService-Info.plist + # quickstart_type: ${{ matrix.quickstart_type }} + # secrets: + # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} # quickstart-ftl-cron-only: # # Don't run on private repo # if: github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule' # env: # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} # LEGACY: true # # TODO: Move to macos-14 and Xcode 15. The legacy quickstart uses material which doesn't build on Xcode 15. # runs-on: macos-12 diff --git a/.github/workflows/generate_issues.yml b/.github/workflows/generate_issues.yml index 53473cabb05..b4e598c355c 100644 --- a/.github/workflows/generate_issues.yml +++ b/.github/workflows/generate_issues.yml @@ -7,7 +7,7 @@ on: - '.github/workflows/generate_issues.yml' - '.github/actions/testing_report_generation**' schedule: - # Run every day at 4am (PST) - cron uses UTC times + # Run every day at 5am (PDT) / 8am (EDT) - cron uses UTC times - cron: '0 12 * * *' permissions: diff --git a/.github/workflows/inappmessaging.yml b/.github/workflows/inappmessaging.yml index 6650fbb8fc9..61118a10d1f 100644 --- a/.github/workflows/inappmessaging.yml +++ b/.github/workflows/inappmessaging.yml @@ -12,9 +12,10 @@ on: - '.github/workflows/inappmessaging.yml' - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' + - '.github/workflows/common_quickstart.yml' - 'Gemfile*' schedule: - # Run every day at 10pm (PST) - cron uses UTC times + # Run every day at 11pm (PDT) / 2am (EDT) - cron uses UTC times - cron: '0 6 * * *' concurrency: @@ -46,7 +47,7 @@ jobs: # TODO(#8682): Reenable iPad after fixing Xcode 13 test failures. # platform: [iOS, iPad] platform: [iOS] - xcode: [Xcode_16.2] + xcode: [Xcode_16.4] steps: - uses: actions/checkout@v4 - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126 @@ -78,35 +79,23 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: PodLibLint InAppMessaging Cron run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseInAppMessaging.podspec --platforms=${{ matrix.platform }} ${{ matrix.flags }} 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' - - env: + uses: ./.github/workflows/common_quickstart.yml + strategy: + matrix: + quickstart_type: [objc, swift] + with: + product: InAppMessaging + is_legacy: false + quickstart_type: ${{ matrix.quickstart_type }} + setup_command: scripts/setup_quickstart.sh inappmessaging + plist_src_path: scripts/gha-encrypted/qs-inappmessaging.plist.gpg + plist_dst_path: quickstart-ios/inappmessaging/GoogleService-Info.plist + secrets: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-15 - - steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Setup quickstart - run: scripts/setup_quickstart.sh inappmessaging - - name: install secret googleservice-info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-inappmessaging.plist.gpg \ - quickstart-ios/inappmessaging/GoogleService-Info.plist "$plist_secret" - - name: Test objc quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true swift) diff --git a/.github/workflows/installations.yml b/.github/workflows/installations.yml index cf4f6795b18..54f95f0b67d 100644 --- a/.github/workflows/installations.yml +++ b/.github/workflows/installations.yml @@ -12,9 +12,10 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' - 'Gemfile*' schedule: - # Run every day at 10pm (PST) - cron uses UTC times + # Run every day at 11pm (PDT) / 2am (EDT) - cron uses UTC times - cron: '0 6 * * *' concurrency: @@ -48,23 +49,19 @@ jobs: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} 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' - - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Setup quickstart - run: scripts/setup_quickstart.sh installations - - name: Copy mock plist - run: cp quickstart-ios/mock-GoogleService-Info.plist quickstart-ios/installations/GoogleService-Info.plist - - name: Test objc quickstart - run: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Installations true - - name: Test swift quickstart - run: scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Installations true swift + uses: ./.github/workflows/common_quickstart.yml + strategy: + matrix: + quickstart_type: [objc, swift] + with: + product: Installations + is_legacy: false + setup_command: scripts/setup_quickstart.sh installations + plist_src_path: scripts/gha-encrypted/Installations/GoogleService-Info.plist.gpg + plist_dst_path: quickstart-ios/installations/GoogleService-Info.plist + quickstart_type: ${{ matrix.quickstart_type }} + secrets: + plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} quickstart-ftl-cron-only: # Don't run on private repo. @@ -78,7 +75,7 @@ jobs: with: python-version: '3.11' - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup quickstart run: scripts/setup_quickstart.sh installations - name: Copy mock plist @@ -112,7 +109,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Configure test keychain diff --git a/.github/workflows/messaging.yml b/.github/workflows/messaging.yml index 838ce5fe4d9..b1227c3e9eb 100644 --- a/.github/workflows/messaging.yml +++ b/.github/workflows/messaging.yml @@ -19,10 +19,11 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' # Rebuild on Ruby infrastructure changes - 'Gemfile*' schedule: - # Run every day at 10pm (PST) - cron uses UTC times + # Run every day at 11pm (PDT) / 2am (EDT) - cron uses UTC times - cron: '0 6 * * *' concurrency: @@ -80,40 +81,26 @@ jobs: run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/build.sh Messaging all) 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' - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + uses: ./.github/workflows/common_quickstart.yml strategy: matrix: - include: - - os: macos-15 - xcode: Xcode_16.2 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup quickstart - run: scripts/setup_quickstart.sh messaging - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-messaging.plist.gpg \ - quickstart-ios/messaging/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 Messaging false) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Messaging false swift) + quickstart_type: [objc, swift] + with: + product: Messaging + is_legacy: false + quickstart_type: ${{ matrix.quickstart_type }} + setup_command: scripts/setup_quickstart.sh messaging + plist_src_path: scripts/gha-encrypted/qs-messaging.plist.gpg + plist_dst_path: quickstart-ios/messaging/GoogleService-Info.plist + run_tests: false + secrets: + plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} quickstart-ftl-cron-only: # Don't run on private repo. if: github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule' env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} runs-on: macos-15 steps: - uses: actions/checkout@v4 @@ -122,7 +109,7 @@ jobs: with: python-version: '3.11' - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup quickstart run: scripts/setup_quickstart.sh messaging - name: Install Secret GoogleService-Info.plist @@ -150,7 +137,7 @@ jobs: os: [macos-14, macos-15] include: - os: macos-15 - xcode: Xcode_16.2 + xcode: Xcode_16.4 tests: --test-specs=unit - os: macos-14 xcode: Xcode_16.2 @@ -178,6 +165,8 @@ jobs: with: cache_key: sample${{ matrix.os }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Install Secret GoogleService-Info.plist @@ -186,8 +175,6 @@ jobs: FirebaseMessaging/Apps/Shared/GoogleService-Info.plist "$plist_secret" - name: Prereqs run: scripts/install_prereqs.sh MessagingSample iOS - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - name: Build run: ([ -z $plist_secret ] || scripts/build.sh MessagingSample iOS) @@ -212,7 +199,7 @@ jobs: - name: Prereqs run: scripts/install_prereqs.sh SwiftUISample iOS - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Build run: ([ -z $plist_secret ] || scripts/build.sh SwiftUISample iOS) @@ -237,7 +224,13 @@ jobs: - name: Prereqs run: scripts/install_prereqs.sh MessagingSampleStandaloneWatchApp watchOS - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Build run: ([ -z $plist_secret ] || scripts/build.sh MessagingSampleStandaloneWatchApp watchOS) + - name: Upload xcodebuild logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: xcodebuild-logs-${{ matrix.target }} + path: xcodebuild-*.log diff --git a/.github/workflows/mlmodeldownloader.yml b/.github/workflows/mlmodeldownloader.yml index 3eddabeadec..d4bb6f1a5c7 100644 --- a/.github/workflows/mlmodeldownloader.yml +++ b/.github/workflows/mlmodeldownloader.yml @@ -14,7 +14,7 @@ on: - '.github/workflows/common_catalyst.yml' - 'Gemfile*' schedule: - # Run every day at 11pm (PST) - cron uses UTC times + # Run every day at 12am (PDT) / 3am (EDT) - cron uses UTC times - cron: '0 7 * * *' concurrency: @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Configure test keychain @@ -83,7 +83,7 @@ jobs: cache_key: build-test${{ matrix.os }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Install GoogleService-Info.plist diff --git a/.github/workflows/notice_generation.yml b/.github/workflows/notice_generation.yml index a7a87cf4251..6d769c509d1 100644 --- a/.github/workflows/notice_generation.yml +++ b/.github/workflows/notice_generation.yml @@ -10,7 +10,7 @@ on: - '.github/workflows/notice_generation.yml' - '.github/actions/notices_generation**' schedule: - # Run every day at 2am (PST) - cron uses UTC times + # Run every day at 3am (PDT) / 6am (EDT) - cron uses UTC times - cron: '0 10 * * *' jobs: generate_a_notice: diff --git a/.github/workflows/performance-integration-tests.yml b/.github/workflows/performance-integration-tests.yml index 1092ba0df0f..2bae0f30b4a 100644 --- a/.github/workflows/performance-integration-tests.yml +++ b/.github/workflows/performance-integration-tests.yml @@ -12,9 +12,8 @@ on: # - https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule # - https://crontab.guru/ schedule: - # Runs every 4 hours. - # TODO: Validate when the timer starts after job is triggered. - - cron: '0 */4 * * *' + # Runs once a day. + - cron: '0 0 * * *' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} @@ -34,6 +33,8 @@ jobs: with: cache_key: integration - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Install xcpretty diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index b1073dfd11f..b0d01161259 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -19,10 +19,11 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' # Rebuild on Ruby infrastructure changes - 'Gemfile*' schedule: - # Run every day at 11pm (PST) - cron uses UTC times + # Run every day at 12am (PDT) / 3am (EDT) - cron uses UTC times # Specified in format 'minutes hours day month dayofweek' - cron: '0 7 * * *' @@ -60,7 +61,7 @@ jobs: cache_key: ${{ matrix.target }}${{ matrix.test }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: Install xcpretty @@ -76,35 +77,24 @@ jobs: #TODO: tests are not supported with Xcode 15 because the test spec depends on the iOS 8 GDCWebServer buildonly_platforms: iOS, tvOS + # TODO: The legacy ObjC quickstarts don't run with Xcode 15, re-able if we get these working. quickstart: - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' - - env: + uses: ./.github/workflows/common_quickstart.yml + with: + product: Performance + is_legacy: false + quickstart_type: swift + setup_command: scripts/setup_quickstart.sh performance + plist_src_path: scripts/gha-encrypted/qs-performance.plist.gpg + plist_dst_path: quickstart-ios/performance/GoogleService-Info.plist + secrets: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Setup quickstart - run: scripts/setup_quickstart.sh performance - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-performance.plist.gpg \ - quickstart-ios/performance/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true swift) - # TODO: The legacy ObjC quickstarts don't run with Xcode 15, re-able if we get these working. - # - name: Test objc quickstart - # run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true) quickstart-ftl-cron-only: if: github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule' env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} runs-on: macos-15 steps: - uses: actions/checkout@v4 @@ -113,7 +103,7 @@ jobs: with: python-version: '3.11' - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup quickstart run: scripts/setup_quickstart.sh performance - name: Install Secret GoogleService-Info.plist @@ -145,7 +135,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - name: PodLibLint Performance Cron diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 2cb87713f68..b87499a0fc4 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -7,9 +7,11 @@ on: pull_request: # closed will be triggered when a pull request is merged. This is to keep https://github.com/firebase/SpecsTesting up to date. types: [closed] + paths: + - '.github/workflows/prerelease.yml' workflow_dispatch: schedule: - # Run every day at 9pm (PST) - cron uses UTC times + # Run every day at 10pm (PDT) / 1am (EDT) - cron uses UTC times - cron: '0 5 * * *' env: @@ -35,6 +37,8 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Generate matrix id: generate_matrix run: | @@ -80,6 +84,7 @@ jobs: path: | *.podspec *.podspec.json + buildup_SpecsTesting_repo_FirebaseCore: needs: specs_checking # Don't run on private repo unless it is a PR. @@ -92,10 +97,17 @@ jobs: targeted_pod: FirebaseCore steps: - uses: actions/checkout@v4 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - uses: actions/download-artifact@v4.1.7 with: name: firebase-ios-sdk path: ${{ env.local_sdk_repo_dir }} + # Addresses flaky pushes due to missing git config on runner. + - name: Set git config + run: | + git config --global user.email "google-oss-bot@example.com" + git config --global user.name "google-oss-bot" - name: Update SpecsTesting repo run: | cd scripts/create_spec_repo/ @@ -127,10 +139,17 @@ jobs: targeted_pod: ${{ matrix.podspec }} steps: - uses: actions/checkout@v4 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - uses: actions/download-artifact@v4.1.7 with: name: firebase-ios-sdk path: ${{ env.local_sdk_repo_dir }} + # Addresses flaky pushes due to missing git config on runner. + - name: Set git config + run: | + git config --global user.email "google-oss-bot@example.com" + git config --global user.name "google-oss-bot" - name: Update SpecsTesting repo run: | [[ ${{ matrix.allowwarnings }} == true ]] && ALLOWWARNINGS=true @@ -163,6 +182,8 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Update SpecsTesting repo setup run: | # Update/create a nightly tag to the head of the main branch. @@ -194,175 +215,6 @@ jobs: pod repo add --silent "${local_repo}" https://"$botaccess"@github.com/Firebase/SpecsTesting.git BOT_TOKEN="${botaccess}" .build/debug/spec-repo-builder --sdk-repo $(pwd) --local-spec-repo-name "${local_repo}" --sdk-repo-name SpecsTesting --github-account Firebase --pod-sources 'https://${BOT_TOKEN}@github.com/Firebase/SpecsTesting' "https://github.com/firebase/SpecsDev.git" "https://github.com/firebase/SpecsStaging.git" "https://cdn.cocoapods.org/" "FirebaseFirestoreTestingSupport" "FirebaseAuthTestingSupport" "FirebaseCombineSwift" --keep-repo --include-pods "${updated_podspecs[@]}" - abtesting_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 == 'workflow_dispatch' - - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - env: - LEGACY: true - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh abtesting prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-abtesting.plist.gpg \ - quickstart-ios/abtesting/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - env: - LEGACY: true - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh ABTesting true) - - name: Remove data before upload - env: - LEGACY: true - if: ${{ failure() }} - run: scripts/remove_data.sh config - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_abtesting - path: quickstart-ios/ - - auth_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh Authentication prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-auth.plist.gpg \ - quickstart-ios/authentication/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Authentication false) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh authentication - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_auth - path: quickstart-ios/ - - crashlytics_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - env: - LEGACY: true - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh Crashlytics prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-crashlytics.plist.gpg \ - quickstart-ios/crashlytics/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - env: - LEGACY: true - run: | - mkdir -p quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics - # Set the deployed pod location of run and upload-symbols with the development pod version. - cp Crashlytics/run quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ - cp Crashlytics/upload-symbols quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ - ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Crashlytics true swift) - - name: Remove data before upload - env: - LEGACY: true - if: ${{ failure() }} - run: scripts/remove_data.sh crashlytics - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_crashlytics - path: quickstart-ios/ - - database_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh database prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-database.plist.gpg \ - quickstart-ios/database/GoogleService-Info.plist "$plist_secret" - - name: Test objc quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh database - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_database - path: quickstart-ios/ - - firestore_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh firestore prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ - quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Firestore false) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh firestore - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_firestore - path: quickstart-ios/ - # TODO: The functions quickstart uses Material which isn't supported by Xcode 15 #functions_quickstart: # Don't run on private repo unless it is a PR. @@ -370,16 +222,15 @@ jobs: # needs: buildup_SpecsTesting_repo # env: # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} # botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - # testing_repo_dir: "/tmp/test/" - # testing_repo: "firebase-ios-sdk" # LEGACY: true # # TODO: The functions quickstart uses Material which isn't supported by Xcode 15 # runs-on: macos-12 # steps: # - uses: actions/checkout@v4 # - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + # - name: Xcode + # run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer # - name: Setup testing repo and quickstart # run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh functions prerelease_testing # - name: install secret googleservice-info.plist @@ -402,159 +253,97 @@ jobs: # name: quickstart_artifacts_functions # path: quickstart-ios/ - inappmessaging_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh inappmessaging prerelease_testing - - name: install secret googleservice-info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-inappmessaging.plist.gpg \ - quickstart-ios/inappmessaging/GoogleService-Info.plist "$plist_secret" - - name: Test objc quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh inappmessaging - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_inappmessaging - path: quickstart-ios/ - - messaging_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh messaging prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-messaging.plist.gpg \ - quickstart-ios/messaging/GoogleService-Info.plist "$plist_secret" - - name: Test objc quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Messaging false) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Messaging false swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh messaging - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_messaging - path: quickstart-ios/ - - remoteconfig_quickstart: + 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 == 'workflow_dispatch' needs: buildup_SpecsTesting_repo + strategy: + matrix: + include: + - product: Performance + run_tests: true + swift: true + - product: Storage + is_legacy: true + run_tests: true + swift: true + - product: Config + run_tests: true + objc: true + - product: Messaging + run_tests: false + swift: true + objc: true + - product: InAppMessaging + run_tests: true + swift: true + objc: true + - product: Firestore + run_tests: false + - product: Database + run_tests: false + swift: true + objc: true + - product: Authentication + run_tests: false + objc: true + - product: Crashlytics + is_legacy: true + run_tests: true + objc: false + swift: true + setup_command: | + mkdir -p quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics + # Set the deployed pod location of run and upload-symbols with the development pod version. + cp Crashlytics/run quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ + cp Crashlytics/upload-symbols quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ + - product: ABTesting + is_legacy: true + run_tests: true + objc: true env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} + LEGACY: ${{ matrix.is_legacy && true || '' }} runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh config prerelease_testing + run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh ${{ matrix.product }} prerelease_testing - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-config.plist.gpg \ - quickstart-ios/config/GoogleService-Info.plist "$plist_secret" - - name: Test Swift Quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Config true) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh config - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} + run: | + scripts/decrypt_gha_secret.sh \ + scripts/gha-encrypted/qs-${{ matrix.product }}.plist.gpg \ + quickstart-ios/${{ matrix.product }}/GoogleService-Info.plist \ + "$plist_secret" + - name: Run setup command, if needed. + if: matrix.setup_command != '' + run: ${{ matrix.setup_command }} + - name: Build Swift quickstart + if: matrix.swift == true + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - name: quickstart_artifacts_config - path: quickstart-ios/ - - storage_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - LEGACY: true - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh storage prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-storage.plist.gpg \ - quickstart-ios/storage/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh storage - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} + timeout_minutes: 15 + max_attempts: 3 + retry_wait_seconds: 120 + command: scripts/test_quickstart.sh ${{ matrix.product }} ${{ matrix.run_tests }} swift + - name: Build Obj-C quickstart + if: matrix.objc == true + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - name: quickstart_artifacts_storage - path: quickstart-ios/ - - performance_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 == 'workflow_dispatch' - needs: buildup_SpecsTesting_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.PRERELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh Performance prerelease_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-performance.plist.gpg \ - quickstart-ios/performance/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true swift) - - name: Remove data before upload + timeout_minutes: 15 + max_attempts: 3 + retry_wait_seconds: 120 + command: scripts/test_quickstart.sh ${{ matrix.product }} ${{ matrix.run_tests }} + # Failure sequence to upload artifact. + - name: Remove data before upload. if: ${{ failure() }} - run: scripts/remove_data.sh performance + run: scripts/remove_data.sh ${{ matrix.product }} - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: - name: quickstart_artifacts_performance + name: quickstart_artifacts_${{ matrix.product }} path: quickstart-ios/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de6631fc6b4..32c40274ecc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,8 @@ name: release +permissions: + contents: read + on: pull_request: paths: @@ -8,7 +11,7 @@ on: - 'Gemfile*' workflow_dispatch: schedule: - # Run every day at 9pm (PST) - cron uses UTC times + # Run every day at 10pm (PDT) / 1am (EDT) - cron uses UTC times - cron: '0 5 * * *' env: @@ -86,6 +89,8 @@ jobs: targeted_pod: FirebaseCore steps: - uses: actions/checkout@v4 + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode_16.4.app - uses: actions/download-artifact@v4.1.7 with: name: firebase-ios-sdk @@ -119,10 +124,17 @@ jobs: targeted_pod: ${{ matrix.podspec }} steps: - uses: actions/checkout@v4 + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode_16.4.app - uses: actions/download-artifact@v4.1.7 with: name: firebase-ios-sdk path: ${{ env.local_sdk_repo_dir }} + # Addresses flaky pushes due to missing git config on runner. + - name: Set git config + run: | + git config --global user.email "google-oss-bot@example.com" + git config --global user.name "google-oss-bot" - name: Update SpecsReleasing repo run: | [[ ${{ matrix.allowwarnings }} == true ]] && ALLOWWARNINGS=true @@ -141,172 +153,96 @@ jobs: if: ${{ always() }} run: pod repo remove "${local_repo}" - abtesting_quickstart: + 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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - env: - LEGACY: true - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh abtesting nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-abtesting.plist.gpg \ - quickstart-ios/abtesting/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - env: - LEGACY: true - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh ABTesting true) - - name: Remove data before upload - env: - LEGACY: true - if: ${{ failure() }} - run: scripts/remove_data.sh config - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_abtesting - path: quickstart-ios/ - - auth_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' || github.event_name == 'workflow_dispatch' + if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'workflow_dispatch' needs: buildup_SpecsReleasing_repo + strategy: + matrix: + include: + - product: Performance + run_tests: true + swift: true + - product: Storage + run_tests: true + swift: true + is_legacy: true + - product: Config + run_tests: true + objc: true + - product: Messaging + run_tests: false + objc: true + swift: true + - product: InAppMessaging + run_tests: true + objc: true + swift: true + - product: Firestore + run_tests: false + objc: true + - product: Database + run_tests: false + objc: true + swift: true + - product: Authentication + run_tests: false + objc: true + - product: Crashlytics + run_tests: true + swift: true + is_legacy: true + setup_command: | + mkdir -p quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics + # Set the deployed pod location of run and upload-symbols with the development pod version. + cp Crashlytics/run quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ + cp Crashlytics/upload-symbols quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ + - product: ABTesting + run_tests: true + objc: true + is_legacy: true env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} botaccess: ${{ secrets.RELEASE_TESTING_PAT }} + LEGACY: ${{ matrix.is_legacy && true || '' }} runs-on: macos-15 steps: - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh Authentication nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-auth.plist.gpg \ - quickstart-ios/authentication/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Authentication false) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh authentication - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_auth - path: quickstart-ios/ - - crashlytics_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode_16.4.app - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Setup testing repo and quickstart - env: - LEGACY: true - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh Crashlytics nightly_release_testing + run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh ${{ matrix.product }} nightly_release_testing - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-crashlytics.plist.gpg \ - quickstart-ios/crashlytics/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - env: - LEGACY: true run: | - mkdir -p quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics - # Set the deployed pod location of run and upload-symbols with the development pod version. - cp Crashlytics/run quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ - cp Crashlytics/upload-symbols quickstart-ios/crashlytics/LegacyCrashlyticsQuickstart/Pods/FirebaseCrashlytics/ - ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Crashlytics true swift) - - name: Remove data before upload - env: - LEGACY: true - if: ${{ failure() }} - run: scripts/remove_data.sh crashlytics - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} + scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-${{ matrix.product }}.plist.gpg \ + quickstart-ios/${{ matrix.product }}/GoogleService-Info.plist "$plist_secret" + - name: Run setup command, if needed. + if: matrix.setup_command != '' + run: ${{ matrix.setup_command }} + - name: Build Obj-C quickstart + if: matrix.objc == true + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - name: quickstart_artifacts_crashlytics - path: quickstart-ios/ - - database_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh database nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-database.plist.gpg \ - quickstart-ios/database/GoogleService-Info.plist "$plist_secret" - - name: Test objc quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Database false swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh database - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} + timeout_minutes: 15 + max_attempts: 3 + retry_wait_seconds: 120 + command: scripts/test_quickstart.sh ${{ matrix.product }} ${{ matrix.run_tests }} + - name: Build Swift quickstart + if: matrix.swift == true + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - name: quickstart_artifacts_database - path: quickstart-ios/ - - firestore_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh firestore nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ - quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Firestore false) + timeout_minutes: 15 + max_attempts: 3 + retry_wait_seconds: 120 + command: scripts/test_quickstart.sh ${{ matrix.product }} ${{ matrix.run_tests }} swift - name: Remove data before upload if: ${{ failure() }} - run: scripts/remove_data.sh firestore + run: scripts/remove_data.sh ${{ matrix.product }} - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: - name: quickstart_artifacts_firestore + name: quickstart_artifacts_${{ matrix.product }} path: quickstart-ios/ # TODO: The functions quickstart uses Material which isn't supported by Xcode 15 @@ -316,10 +252,7 @@ jobs: # needs: buildup_SpecsReleasing_repo # env: # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} # botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - # testing_repo_dir: "/tmp/test/" - # testing_repo: "firebase-ios-sdk" # LEGACY: true # runs-on: macos-12 # steps: @@ -346,160 +279,3 @@ jobs: # with: # name: quickstart_artifacts_functions # path: quickstart-ios/ - - inappmessaging_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh inappmessaging nightly_release_testing - - name: install secret googleservice-info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-inappmessaging.plist.gpg \ - quickstart-ios/inappmessaging/GoogleService-Info.plist "$plist_secret" - - name: Test objc quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh InAppMessaging true swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh inappmessaging - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_inappmessaging - path: quickstart-ios/ - - messaging_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh messaging nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-messaging.plist.gpg \ - quickstart-ios/messaging/GoogleService-Info.plist "$plist_secret" - - name: Test objc quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Messaging false) - - name: Test swift quickstart - run: ([ -z $plist_secret ] || - scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Messaging false swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh messaging - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_messaging - path: quickstart-ios/ - - remoteconfig_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh config nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-config.plist.gpg \ - quickstart-ios/config/GoogleService-Info.plist "$plist_secret" - - name: Test Swift Quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Config true) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh config - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_config - path: quickstart-ios/ - - storage_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - LEGACY: true - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh storage nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-storage.plist.gpg \ - quickstart-ios/storage/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage true swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh storage - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_storage - path: quickstart-ios/ - - performance_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' || github.event_name == 'workflow_dispatch' - needs: buildup_SpecsReleasing_repo - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - botaccess: ${{ secrets.RELEASE_TESTING_PAT }} - testing_repo_dir: "/tmp/test/" - testing_repo: "firebase-ios-sdk" - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup testing repo and quickstart - run: BOT_TOKEN="${botaccess}" scripts/setup_quickstart.sh Performance nightly_release_testing - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-performance.plist.gpg \ - quickstart-ios/performance/GoogleService-Info.plist "$plist_secret" - - name: Test swift quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true swift) - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh performance - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_performance - path: quickstart-ios/ diff --git a/.github/workflows/remoteconfig.yml b/.github/workflows/remoteconfig.yml index 199fd669f41..5dc0984c517 100644 --- a/.github/workflows/remoteconfig.yml +++ b/.github/workflows/remoteconfig.yml @@ -13,11 +13,12 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' - 'Gemfile*' - 'scripts/generate_access_token.sh' - 'scripts/gha-encrypted/RemoteConfigSwiftAPI/**' schedule: - # Run every day at 12am (PST) - cron uses UTC times + # Run every day at 1am (PDT) / 4am (EDT) - cron uses UTC times - cron: '0 8 * * *' concurrency: @@ -72,7 +73,12 @@ jobs: - name: Xcode run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Fake Console API Tests - run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig ${{ matrix.target }} fakeconsole + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 + with: + timeout_minutes: 15 + max_attempts: 3 + retry_wait_seconds: 120 + command: scripts/build.sh RemoteConfig ${{ matrix.target }} fakeconsole - name: IntegrationTest if: matrix.target == 'iOS' # No retry to avoid exhausting AccessToken quota. @@ -84,24 +90,15 @@ jobs: product: FirebaseRemoteConfig 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' - env: + uses: ./.github/workflows/common_quickstart.yml + with: + product: Config + is_legacy: false + setup_command: scripts/setup_quickstart.sh config + plist_src_path: scripts/gha-encrypted/qs-config.plist.gpg + plist_dst_path: quickstart-ios/config/GoogleService-Info.plist + secrets: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - - name: Setup quickstart - run: scripts/setup_quickstart.sh config - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-config.plist.gpg \ - quickstart-ios/config/GoogleService-Info.plist "$plist_secret" - - name: Test Swift Quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Config true) # TODO(@sunmou99): currently have issue with this job, will re-enable it once the issue resolved. # quickstart-ftl-cron-only: @@ -109,7 +106,6 @@ jobs: # if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' # env: # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} # runs-on: macos-14 # steps: # - uses: actions/checkout@v4 @@ -157,9 +153,7 @@ jobs: strategy: matrix: target: [ios, tvos, macos] - flags: [ - '--skip-tests --use-static-frameworks' - ] + flags: ['--skip-tests --use-static-frameworks'] needs: pod_lib_lint steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/sessions-integration-tests.yml b/.github/workflows/sessions-integration-tests.yml index 4d11e4dc065..ccc8d9f15fd 100644 --- a/.github/workflows/sessions-integration-tests.yml +++ b/.github/workflows/sessions-integration-tests.yml @@ -31,7 +31,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Xcode - run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - uses: mikehardy/buildcache-action@c87cea0ccd718971d6cc39e672c4f26815b6c126 with: cache_key: sessions-integration-tests diff --git a/.github/workflows/sessions.yml b/.github/workflows/sessions.yml index d3ec5adb7f1..f0df72c44c2 100644 --- a/.github/workflows/sessions.yml +++ b/.github/workflows/sessions.yml @@ -15,7 +15,7 @@ on: - '.github/workflows/common_catalyst.yml' - 'Gemfile*' schedule: - # Run every day at 9am (PST) - cron uses UTC times + # Run every day at 6pm (PDT) / 9pm (EDT) - cron uses UTC times - cron: '0 1 * * *' concurrency: diff --git a/.github/workflows/shared-swift.yml b/.github/workflows/shared-swift.yml index b1292ebb253..fa13d6e9a4c 100644 --- a/.github/workflows/shared-swift.yml +++ b/.github/workflows/shared-swift.yml @@ -14,7 +14,7 @@ on: - 'Gemfile*' schedule: - # Run every day at 3am (PST) - cron uses UTC times + # Run every day at 4am (PDT) / 7am (EDT) - cron uses UTC times - cron: '0 11 * * *' concurrency: diff --git a/.github/workflows/spm.yml b/.github/workflows/spm.yml index 5b807927410..f4f8c05a539 100644 --- a/.github/workflows/spm.yml +++ b/.github/workflows/spm.yml @@ -12,7 +12,7 @@ on: - 'SwiftPM-PlatformExclude' - 'Gemfile*' schedule: - # Run every day at 12am (PST) - cron uses UTC times + # Run every day at 1am (PDT) / 4am (EDT) - cron uses UTC times - cron: '0 8 * * *' # This workflow builds and tests the Swift Package Manager. Only iOS runs on PRs @@ -81,9 +81,8 @@ jobs: run: FirebaseFunctions/Backend/start.sh synchronous - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - timeout_minutes: 120 + timeout_minutes: 15 max_attempts: 3 - retry_on: error retry_wait_seconds: 120 command: scripts/build.sh Firebase-Package iOS ${{ matrix.test }} @@ -98,7 +97,7 @@ jobs: - os: macos-14 xcode: Xcode_16.2 - os: macos-15 - xcode: Xcode_16.2 + xcode: Xcode_16.4 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -127,7 +126,7 @@ jobs: target: [tvOS, macOS, catalyst] include: - os: macos-15 - xcode: Xcode_16.2 + xcode: Xcode_16.4 - os: macos-14 xcode: Xcode_16.2 runs-on: ${{ matrix.os }} diff --git a/.github/workflows/storage.yml b/.github/workflows/storage.yml index 036037b5ca7..29449d9fe85 100644 --- a/.github/workflows/storage.yml +++ b/.github/workflows/storage.yml @@ -13,10 +13,11 @@ on: - '.github/workflows/common.yml' - '.github/workflows/common_cocoapods.yml' - '.github/workflows/common_catalyst.yml' + - '.github/workflows/common_quickstart.yml' # Rebuild on Ruby infrastructure changes. - 'Gemfile*' schedule: - # Run every day at 12am (PST) - cron uses UTC times + # Run every day at 1am (PDT) / 4am (EDT) - cron uses UTC times - cron: '0 8 * * *' concurrency: @@ -71,48 +72,30 @@ jobs: run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 with: - timeout_minutes: 120 + timeout_minutes: 15 max_attempts: 3 - retry_on: error retry_wait_seconds: 120 command: ([ -z $plist_secret ] || scripts/build.sh Storage${{ matrix.language }} all) 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-13 - # xcode: Xcode_14.2 # TODO: the legacy ObjC quickstart doesn't build with Xcode 15. - - swift: swift - os: macos-15 - xcode: Xcode_16.2 - env: + uses: ./.github/workflows/common_quickstart.yml + with: + product: Storage + quickstart_type: swift + is_legacy: true + setup_command: scripts/setup_quickstart.sh storage + plist_src_path: scripts/gha-encrypted/qs-storage.plist.gpg + plist_dst_path: quickstart-ios/storage/GoogleService-Info.plist + run_tests: false + secrets: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - LEGACY: true - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - - name: Setup quickstart - run: scripts/setup_quickstart.sh storage - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-storage.plist.gpg \ - quickstart-ios/storage/GoogleService-Info.plist "$plist_secret" - - name: Xcode - run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Test quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Storage false ${{ matrix.swift }}) quickstart-ftl-cron-only: # Don't run on private repo. if: github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule' env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} LEGACY: true runs-on: macos-15 steps: @@ -153,7 +136,7 @@ jobs: - os: macos-14 xcode: Xcode_16.2 - os: macos-15 - xcode: Xcode_16.2 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} needs: pod_lib_lint steps: diff --git a/.github/workflows/symbolcollision.yml b/.github/workflows/symbolcollision.yml index 00fcde3c3e9..298f7fd4f10 100644 --- a/.github/workflows/symbolcollision.yml +++ b/.github/workflows/symbolcollision.yml @@ -10,7 +10,7 @@ on: - 'SymbolCollisionTest/**' - 'Gemfile*' schedule: - # Run every day at 12am (PST) - cron uses UTC times + # Run every day at 1am (PDT) / 4am (EDT) - cron uses UTC times - cron: '0 8 * * *' concurrency: diff --git a/.github/workflows/watchos-sample.yml b/.github/workflows/watchos-sample.yml index 665dfbea350..b8589288863 100644 --- a/.github/workflows/watchos-sample.yml +++ b/.github/workflows/watchos-sample.yml @@ -18,7 +18,7 @@ on: # Rebuild on Ruby infrastructure changes - 'Gemfile*' schedule: - # Run every day at 10pm (PST) - cron uses UTC times + # Run every day at 11pm (PDT) / 2am (EDT) - cron uses UTC times - cron: '0 6 * * *' concurrency: diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 9879e942885..eba3c5b61b5 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -1,5 +1,9 @@ name: zip +permissions: + actions: read + contents: read + on: pull_request: paths: @@ -12,7 +16,7 @@ on: # Don't run based on any markdown only changes. - '!ReleaseTooling/*.md' schedule: - # Run every day at 8pm(PST) - cron uses UTC times + # Run every day at 9pm (PDT) / 12am (EDT) - cron uses UTC times - cron: '0 4 * * *' workflow_dispatch: @@ -21,6 +25,12 @@ on: description: 'Custom Podspec repos' required: true default: 'https://github.com/firebase/SpecsStaging.git' + zip_run_id: + # For example, in the below URL, `17335533279` is the run ID: + # - https://github.com/firebase/firebase-ios-sdk/actions/runs/17335533279 + description: 'Run ID of a previous successful zip workflow to use for quickstart testing' + required: false + default: '' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} @@ -29,7 +39,10 @@ concurrency: jobs: package-release: # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + if: | + github.repository == 'firebase/firebase-ios-sdk' && + contains(fromJSON('["schedule", "pull_request", "workflow_dispatch"]'), github.event_name) && + github.event.inputs.zip_run_id == '' runs-on: macos-14 steps: - uses: actions/checkout@v4 @@ -57,7 +70,10 @@ jobs: build: # 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' || github.event_name == 'workflow_dispatch' + if: | + github.repository == 'firebase/firebase-ios-sdk' && + contains(fromJSON('["schedule", "pull_request", "workflow_dispatch"]'), github.event_name) && + github.event.inputs.zip_run_id == '' runs-on: macos-14 steps: - uses: actions/checkout@v4 @@ -69,8 +85,6 @@ jobs: swift build -v package-head: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: build strategy: matrix: @@ -102,21 +116,17 @@ jobs: path: zip_output_dir quickstart_framework_abtesting: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "ABTesting" strategy: matrix: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] build-env: - os: macos-15 - xcode: Xcode_16.2 - # - os: macos-15 - # xcode: Xcode_16.4 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} steps: - uses: actions/checkout@v4 @@ -124,7 +134,11 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks @@ -132,8 +146,6 @@ jobs: mkdir -p "${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.build-env.xcode }}.app/Contents/Developer - name: Setup quickstart env: LEGACY: true @@ -163,12 +175,10 @@ jobs: path: quickstart-ios/ quickstart_framework_auth: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "Authentication" strategy: matrix: @@ -176,7 +186,7 @@ jobs: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-15 - xcode: Xcode_16.2 + xcode: Xcode_16.4 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -184,15 +194,17 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks run: | mkdir -p "${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 run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="FBSDKLoginKit FBSDKCoreKit FBSDKCoreKit_Basics FBAEMKit" scripts/setup_quickstart_framework.sh \ "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/* \ @@ -200,7 +212,7 @@ jobs: "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-auth.plist.gpg \ + run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-authentication.plist.gpg \ quickstart-ios/authentication/GoogleService-Info.plist "$plist_secret" - name: Test Swift Quickstart run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_framework.sh "${SDK}") @@ -214,21 +226,17 @@ jobs: path: quickstart-ios/ quickstart_framework_config: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "Config" strategy: matrix: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] build-env: - os: macos-15 - xcode: Xcode_16.2 - # - os: macos-15 - # xcode: Xcode_16.4 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} steps: - uses: actions/checkout@v4 @@ -236,15 +244,17 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks run: | mkdir -p "${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.build-env.xcode }}.app/Contents/Developer - name: Setup Swift Quickstart run: SAMPLE="$SDK" TARGET="${SDK}Example" scripts/setup_quickstart_framework.sh \ @@ -265,21 +275,17 @@ jobs: path: quickstart-ios/ quickstart_framework_crashlytics: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "Crashlytics" strategy: matrix: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] build-env: - os: macos-15 - xcode: Xcode_16.2 - # - os: macos-15 - # xcode: Xcode_16.4 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} steps: - uses: actions/checkout@v4 @@ -287,7 +293,11 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks @@ -295,8 +305,6 @@ jobs: mkdir -p "${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.build-env.xcode }}.app/Contents/Developer - name: Setup quickstart env: LEGACY: true @@ -339,12 +347,10 @@ jobs: path: quickstart-ios/ quickstart_framework_database: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "Database" strategy: matrix: @@ -358,7 +364,11 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks @@ -366,8 +376,6 @@ jobs: mkdir -p "${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 - name: Setup quickstart run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="FirebaseDatabaseUI" scripts/setup_quickstart_framework.sh \ "${HOME}"/ios_frameworks/Firebase/FirebaseDatabase/* \ @@ -391,21 +399,17 @@ jobs: path: quickstart-ios/ quickstart_framework_firestore: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "Firestore" strategy: matrix: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] build-env: - os: macos-15 - xcode: Xcode_16.2 - # - os: macos-15 - # xcode: Xcode_16.4 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} steps: - uses: actions/checkout@v4 @@ -413,7 +417,11 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks @@ -427,8 +435,12 @@ jobs: "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/* \ "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - - name: Xcode - run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer + - name: Upload build logs on failure + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: build_logs_firestore_${{ matrix.artifact }}_${{ matrix.build-env.os }} + path: sdk_zip/build_logs/ - name: Install Secret GoogleService-Info.plist run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" @@ -444,9 +456,8 @@ jobs: path: quickstart_artifacts_firestore.zip check_framework_firestore_symbols: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 runs-on: macos-14 @@ -458,6 +469,8 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: Firebase-actions-dir + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -475,21 +488,17 @@ jobs: "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/FirebaseFirestoreInternal.xcframework quickstart_framework_inappmessaging: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "InAppMessaging" strategy: matrix: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] build-env: - os: macos-15 - xcode: Xcode_16.2 - # - os: macos-15 - # xcode: Xcode_16.4 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} steps: - uses: actions/checkout@v4 @@ -497,7 +506,11 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks @@ -509,8 +522,6 @@ jobs: run: SAMPLE="$SDK" TARGET="${SDK}Example" scripts/setup_quickstart_framework.sh \ "${HOME}"/ios_frameworks/Firebase/FirebaseInAppMessaging/* \ "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - - name: Xcode - run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup swift quickstart run: SAMPLE="$SDK" TARGET="${SDK}ExampleSwift" scripts/setup_quickstart_framework.sh - name: Install Secret GoogleService-Info.plist @@ -530,21 +541,17 @@ jobs: path: quickstart-ios/ quickstart_framework_messaging: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "Messaging" strategy: matrix: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] build-env: - os: macos-15 - xcode: Xcode_16.2 - # - os: macos-15 - # xcode: Xcode_16.4 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} steps: - uses: actions/checkout@v4 @@ -552,7 +559,11 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks @@ -564,8 +575,6 @@ jobs: run: SAMPLE="$SDK" TARGET="${SDK}Example" scripts/setup_quickstart_framework.sh \ "${HOME}"/ios_frameworks/Firebase/FirebaseMessaging/* \ "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - - name: Xcode - run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup swift quickstart run: SAMPLE="$SDK" TARGET="${SDK}ExampleSwift" scripts/setup_quickstart_framework.sh - name: Install Secret GoogleService-Info.plist @@ -585,21 +594,17 @@ jobs: path: quickstart-ios/ quickstart_framework_storage: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: package-head + if: ${{ !cancelled() && (success() || github.event.inputs.zip_run_id != '') }} env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} SDK: "Storage" strategy: matrix: artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] build-env: - os: macos-15 - xcode: Xcode_16.2 - # - os: macos-15 - # xcode: Xcode_16.4 + xcode: Xcode_16.4 runs-on: ${{ matrix.build-env.os }} steps: - uses: actions/checkout@v4 @@ -607,7 +612,11 @@ jobs: uses: actions/download-artifact@v4.1.7 with: name: ${{ matrix.artifact }} + run-id: ${{ github.event.inputs.zip_run_id || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252 # v1 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: ./scripts/setup_bundler.sh - name: Move frameworks @@ -622,8 +631,6 @@ jobs: "${HOME}"/ios_frameworks/Firebase/FirebaseStorage/* \ "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - - name: Xcode - run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup swift quickstart env: LEGACY: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a460cbdd87..2679fbd217e 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@20 + brew install clang-format@21 brew install mint ``` diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index fa67c39d6a7..b4d4bbf8c5e 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,3 +1,10 @@ +# Unreleased +- [fixed] Add missing nanopb dependency to fix SwiftPM builds when building + dynamically linked libraries. (#15276) + +# 12.1.0 +- [fixed] Do not log using raw print in an internal class. (#15138) + # 12.0.0 - [fixed] Resolved compiler warnings related to constant definitions. (#15059) diff --git a/Firebase.podspec b/Firebase.podspec index 197ce7ab758..1a9364b4ad7 100644 --- a/Firebase.podspec +++ b/Firebase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Firebase' - s.version = '12.0.0' + s.version = '12.3.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 = '15.0' ss.osx.deployment_target = '10.15' ss.tvos.deployment_target = '15.0' - ss.ios.dependency 'FirebaseAnalytics', '~> 12.0.0' - ss.osx.dependency 'FirebaseAnalytics', '~> 12.0.0' - ss.tvos.dependency 'FirebaseAnalytics', '~> 12.0.0' + ss.ios.dependency 'FirebaseAnalytics', '~> 12.3.0' + ss.osx.dependency 'FirebaseAnalytics', '~> 12.3.0' + ss.tvos.dependency 'FirebaseAnalytics', '~> 12.3.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'CoreOnly' do |ss| - ss.dependency 'FirebaseCore', '~> 12.0.0' + ss.dependency 'FirebaseCore', '~> 12.3.0' ss.source_files = 'CoreOnly/Sources/Firebase.h' ss.preserve_paths = 'CoreOnly/Sources/module.modulemap' if ENV['FIREBASE_POD_REPO_FOR_DEV_POD'] then @@ -70,7 +70,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'ABTesting' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseABTesting', '~> 12.0.0' + ss.dependency 'FirebaseABTesting', '~> 12.3.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -80,13 +80,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', '~> 12.0.0-beta' + ss.ios.dependency 'FirebaseAppDistribution', '~> 12.3.0-beta' ss.ios.deployment_target = '15.0' end s.subspec 'AppCheck' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAppCheck', '~> 12.0.0' + ss.dependency 'FirebaseAppCheck', '~> 12.3.0' ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' ss.tvos.deployment_target = '15.0' @@ -95,7 +95,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', '~> 12.0.0' + ss.dependency 'FirebaseAuth', '~> 12.3.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -105,7 +105,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', '~> 12.0.0' + ss.dependency 'FirebaseCrashlytics', '~> 12.3.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -115,7 +115,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', '~> 12.0.0' + ss.dependency 'FirebaseDatabase', '~> 12.3.0' # Standard platforms PLUS watchOS 7. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -125,15 +125,15 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Firestore' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFirestore', '~> 12.0.0' + ss.dependency 'FirebaseFirestore', '~> 12.3.0' ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' - ss.ios.deployment_target = '15.0' + ss.tvos.deployment_target = '15.0' end s.subspec 'Functions' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFunctions', '~> 12.0.0' + ss.dependency 'FirebaseFunctions', '~> 12.3.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -143,20 +143,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', '~> 12.0.0-beta' - ss.tvos.dependency 'FirebaseInAppMessaging', '~> 12.0.0-beta' + ss.ios.dependency 'FirebaseInAppMessaging', '~> 12.3.0-beta' + ss.tvos.dependency 'FirebaseInAppMessaging', '~> 12.3.0-beta' ss.ios.deployment_target = '15.0' ss.tvos.deployment_target = '15.0' end s.subspec 'Installations' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseInstallations', '~> 12.0.0' + ss.dependency 'FirebaseInstallations', '~> 12.3.0' end s.subspec 'Messaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMessaging', '~> 12.0.0' + ss.dependency 'FirebaseMessaging', '~> 12.3.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -166,7 +166,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', '~> 12.0.0-beta' + ss.dependency 'FirebaseMLModelDownloader', '~> 12.3.0-beta' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -176,15 +176,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', '~> 12.0.0' - ss.tvos.dependency 'FirebasePerformance', '~> 12.0.0' + ss.ios.dependency 'FirebasePerformance', '~> 12.3.0' + ss.tvos.dependency 'FirebasePerformance', '~> 12.3.0' ss.ios.deployment_target = '15.0' ss.tvos.deployment_target = '15.0' end s.subspec 'RemoteConfig' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseRemoteConfig', '~> 12.0.0' + ss.dependency 'FirebaseRemoteConfig', '~> 12.3.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' @@ -194,7 +194,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', '~> 12.0.0' + ss.dependency 'FirebaseStorage', '~> 12.3.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '15.0' ss.osx.deployment_target = '10.15' diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index d1ce2aaa4b9..403e11741f8 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseABTesting' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase ABTesting' s.description = <<-DESC @@ -51,7 +51,7 @@ Firebase Cloud Messaging and Firebase Remote Config in your app. s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' } - s.dependency 'FirebaseCore', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } diff --git a/FirebaseAI.podspec b/FirebaseAI.podspec index a8ffa35789d..25a9e71dec5 100644 --- a/FirebaseAI.podspec +++ b/FirebaseAI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAI' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase AI SDK' s.description = <<-DESC @@ -43,10 +43,10 @@ Build AI-powered apps and features with the Gemini API using the Firebase AI SDK s.tvos.framework = 'UIKit' s.watchos.framework = 'WatchKit' - s.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' - s.dependency 'FirebaseAuthInterop', '~> 12.0.0' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseCoreExtension', '~> 12.0.0' + s.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' + s.dependency 'FirebaseAuthInterop', '~> 12.3.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseCoreExtension', '~> 12.3.0' s.test_spec 'unit' do |unit_tests| unit_tests_dir = 'FirebaseAI/Tests/Unit/' diff --git a/FirebaseAI/CHANGELOG.md b/FirebaseAI/CHANGELOG.md index 47b2627da67..4c2d482f8d5 100644 --- a/FirebaseAI/CHANGELOG.md +++ b/FirebaseAI/CHANGELOG.md @@ -1,5 +1,21 @@ +# 12.3.0 +- [fixed] Fixed a decoding error when generating images with the + `gemini-2.5-flash-image-preview` model using `generateContentStream` or + `sendMessageStream` with the Gemini Developer API. (#15262) + +# 12.2.0 +- [feature] Added support for returning thought summaries, which are synthesized + versions of a model's internal reasoning process. (#15096) +- [feature] Added support for limited-use tokens with Firebase App Check. + These limited-use tokens are required for an upcoming optional feature called + _replay protection_. We recommend + [enabling the usage of limited-use tokens](https://firebase.google.com/docs/ai-logic/app-check) + now so that when replay protection becomes available, you can enable it sooner + because more of your users will be on versions of your app that send limited-use tokens. + (#15099) + # 12.0.0 -- [added] Added support for Grounding with Google Search. (#15014) +- [feature] Added support for Grounding with Google Search. (#15014) - [removed] Removed `CountTokensResponse.totalBillableCharacters` which was deprecated in 11.15.0. Use `totalTokens` instead. (#15056) diff --git a/FirebaseAI/Sources/AILog.swift b/FirebaseAI/Sources/AILog.swift index 4019c2cd0ff..cae85a0ff0a 100644 --- a/FirebaseAI/Sources/AILog.swift +++ b/FirebaseAI/Sources/AILog.swift @@ -62,11 +62,13 @@ enum AILog { case decodedInvalidCitationPublicationDate = 3011 case generateContentResponseUnrecognizedContentModality = 3012 case decodedUnsupportedImagenPredictionType = 3013 + case decodedUnsupportedPartData = 3014 // SDK State Errors case generateContentResponseNoCandidates = 4000 case generateContentResponseNoText = 4001 case appCheckTokenFetchFailed = 4002 + case generateContentResponseEmptyCandidates = 4003 // SDK Debugging case loadRequestStreamResponseLine = 5000 diff --git a/FirebaseAI/Sources/Chat.swift b/FirebaseAI/Sources/Chat.swift index 1aa2c3490c7..80e908a8f57 100644 --- a/FirebaseAI/Sources/Chat.swift +++ b/FirebaseAI/Sources/Chat.swift @@ -45,6 +45,12 @@ public final class Chat: Sendable { } } + private func appendHistory(_ newElement: ModelContent) { + historyLock.withLock { + _history.append(newElement) + } + } + /// Sends a message using the existing history of this chat as context. If successful, the message /// and response will be added to the history. If unsuccessful, history will remain unchanged. /// - Parameter parts: The new content to send as a single chat message. @@ -82,7 +88,7 @@ public final class Chat: Sendable { // Append the request and successful result to history, then return the value. appendHistory(contentsOf: newContent) - history.append(toAdd) + appendHistory(toAdd) return result } @@ -134,38 +140,55 @@ public final class Chat: Sendable { // Aggregate the content to add it to the history before we finish. let aggregated = self.aggregatedChunks(aggregatedContent) - self.history.append(aggregated) + self.appendHistory(aggregated) continuation.finish() } } } private func aggregatedChunks(_ chunks: [ModelContent]) -> ModelContent { - var parts: [any Part] = [] + var parts: [InternalPart] = [] var combinedText = "" - for aggregate in chunks { - // Loop through all the parts, aggregating the text and adding the images. - for part in aggregate.parts { - switch part { - case let textPart as TextPart: - combinedText += textPart.text - - default: - // Don't combine it, just add to the content. If there's any text pending, add that as - // a part. + var combinedThoughts = "" + + func flush() { + if !combinedThoughts.isEmpty { + parts.append(InternalPart(.text(combinedThoughts), isThought: true, thoughtSignature: nil)) + combinedThoughts = "" + } + if !combinedText.isEmpty { + parts.append(InternalPart(.text(combinedText), isThought: nil, thoughtSignature: nil)) + combinedText = "" + } + } + + // Loop through all the parts, aggregating the text. + for part in chunks.flatMap({ $0.internalParts }) { + // Only text parts may be combined. + if case let .text(text) = part.data, part.thoughtSignature == nil { + // Thought summaries must not be combined with regular text. + if part.isThought ?? false { + // If we were combining regular text, flush it before handling "thoughts". if !combinedText.isEmpty { - parts.append(TextPart(combinedText)) - combinedText = "" + flush() } - - parts.append(part) + combinedThoughts += text + } else { + // If we were combining "thoughts", flush it before handling regular text. + if !combinedThoughts.isEmpty { + flush() + } + combinedText += text } + } else { + // This is a non-combinable part (not text), flush any pending text. + flush() + parts.append(part) } } - if !combinedText.isEmpty { - parts.append(TextPart(combinedText)) - } + // Flush any remaining text. + flush() return ModelContent(role: "model", parts: parts) } diff --git a/FirebaseAI/Sources/FirebaseAI.swift b/FirebaseAI/Sources/FirebaseAI.swift index 48f7183d4e6..7f05d8a0d7b 100644 --- a/FirebaseAI/Sources/FirebaseAI.swift +++ b/FirebaseAI/Sources/FirebaseAI.swift @@ -27,18 +27,29 @@ public final class FirebaseAI: Sendable { /// Creates an instance of `FirebaseAI`. /// - /// - Parameters: + /// - Parameters: /// - app: A custom `FirebaseApp` used for initialization; if not specified, uses the default /// ``FirebaseApp``. /// - backend: The backend API for the Firebase AI SDK; if not specified, uses the default /// ``Backend/googleAI()`` (Gemini Developer API). + /// - useLimitedUseAppCheckTokens: When sending tokens to the backend, this option enables + /// the usage of App Check's limited-use tokens instead of the standard cached tokens. Learn + /// more about [limited-use tokens](https://firebase.google.com/docs/ai-logic/app-check), + /// including their nuances, when to use them, and best practices for integrating them into + /// your app. + /// + /// _This flag is set to `false` by default._ + /// > Migrating to limited-use tokens sooner minimizes disruption when support for replay + /// > protection is added. /// - Returns: A `FirebaseAI` instance, configured with the custom `FirebaseApp`. public static func firebaseAI(app: FirebaseApp? = nil, - backend: Backend = .googleAI()) -> FirebaseAI { + backend: Backend = .googleAI(), + useLimitedUseAppCheckTokens: Bool = false) -> FirebaseAI { let instance = createInstance( app: app, location: backend.location, - apiConfig: backend.apiConfig + apiConfig: backend.apiConfig, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens ) // Verify that the `FirebaseAI` instance is always configured with the production endpoint since // this is the public API surface for creating an instance. @@ -156,7 +167,8 @@ public final class FirebaseAI: Sendable { ) static func createInstance(app: FirebaseApp?, location: String?, - apiConfig: APIConfig) -> FirebaseAI { + apiConfig: APIConfig, + useLimitedUseAppCheckTokens: Bool) -> FirebaseAI { guard let app = app ?? FirebaseApp.app() else { fatalError("No instance of the default Firebase app was found.") } @@ -166,16 +178,27 @@ public final class FirebaseAI: Sendable { // Unlock before the function returns. defer { os_unfair_lock_unlock(&instancesLock) } - let instanceKey = InstanceKey(appName: app.name, location: location, apiConfig: apiConfig) + let instanceKey = InstanceKey( + appName: app.name, + location: location, + apiConfig: apiConfig, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens + ) if let instance = instances[instanceKey] { return instance } - let newInstance = FirebaseAI(app: app, location: location, apiConfig: apiConfig) + let newInstance = FirebaseAI( + app: app, + location: location, + apiConfig: apiConfig, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens + ) instances[instanceKey] = newInstance return newInstance } - init(app: FirebaseApp, location: String?, apiConfig: APIConfig) { + init(app: FirebaseApp, location: String?, apiConfig: APIConfig, + useLimitedUseAppCheckTokens: Bool) { guard let projectID = app.options.projectID else { fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.") } @@ -191,7 +214,8 @@ public final class FirebaseAI: Sendable { projectID: projectID, apiKey: apiKey, firebaseAppID: app.options.googleAppID, - firebaseApp: app + firebaseApp: app, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens ) self.apiConfig = apiConfig self.location = location @@ -249,5 +273,6 @@ public final class FirebaseAI: Sendable { let appName: String let location: String? let apiConfig: APIConfig + let useLimitedUseAppCheckTokens: Bool } } diff --git a/FirebaseAI/Sources/FirebaseInfo.swift b/FirebaseAI/Sources/FirebaseInfo.swift index c1f27aa7fe3..6b10dec4e5f 100644 --- a/FirebaseAI/Sources/FirebaseInfo.swift +++ b/FirebaseAI/Sources/FirebaseInfo.swift @@ -27,6 +27,7 @@ struct FirebaseInfo: Sendable { let projectID: String let apiKey: String let firebaseAppID: String + let useLimitedUseAppCheckTokens: Bool let app: FirebaseApp init(appCheck: AppCheckInterop? = nil, @@ -34,12 +35,14 @@ struct FirebaseInfo: Sendable { projectID: String, apiKey: String, firebaseAppID: String, - firebaseApp: FirebaseApp) { + firebaseApp: FirebaseApp, + useLimitedUseAppCheckTokens: Bool) { self.appCheck = appCheck self.auth = auth self.projectID = projectID self.apiKey = apiKey self.firebaseAppID = firebaseAppID + self.useLimitedUseAppCheckTokens = useLimitedUseAppCheckTokens app = firebaseApp } } diff --git a/FirebaseAI/Sources/GenerateContentResponse.swift b/FirebaseAI/Sources/GenerateContentResponse.swift index b0e348d2192..015d5dae56c 100644 --- a/FirebaseAI/Sources/GenerateContentResponse.swift +++ b/FirebaseAI/Sources/GenerateContentResponse.swift @@ -57,30 +57,19 @@ public struct GenerateContentResponse: Sendable { public let usageMetadata: UsageMetadata? /// The response's content as text, if it exists. + /// + /// - Note: This does not include thought summaries; see ``thoughtSummary`` for more details. public var text: String? { - guard let candidate = candidates.first else { - AILog.error( - code: .generateContentResponseNoCandidates, - "Could not get text from a response that had no candidates." - ) - return nil - } - let textValues: [String] = candidate.content.parts.compactMap { part in - switch part { - case let textPart as TextPart: - return textPart.text - default: - return nil - } - } - guard textValues.count > 0 else { - AILog.error( - code: .generateContentResponseNoText, - "Could not get a text part from the first candidate." - ) - return nil - } - return textValues.joined(separator: " ") + return text(isThought: false) + } + + /// A summary of the model's thinking process, if available. + /// + /// - Important: Thought summaries are only available when `includeThoughts` is enabled in the + /// ``ThinkingConfig``. For more information, see the + /// [Thinking](https://firebase.google.com/docs/ai-logic/thinking) documentation. + public var thoughtSummary: String? { + return text(isThought: true) } /// Returns function calls found in any `Part`s of the first candidate of the response, if any. @@ -89,12 +78,10 @@ public struct GenerateContentResponse: Sendable { return [] } return candidate.content.parts.compactMap { part in - switch part { - case let functionCallPart as FunctionCallPart: - return functionCallPart - default: + guard let functionCallPart = part as? FunctionCallPart, !part.isThought else { return nil } + return functionCallPart } } @@ -107,7 +94,12 @@ public struct GenerateContentResponse: Sendable { """) return [] } - return candidate.content.parts.compactMap { $0 as? InlineDataPart } + return candidate.content.parts.compactMap { part in + guard let inlineDataPart = part as? InlineDataPart, !part.isThought else { + return nil + } + return inlineDataPart + } } /// Initializer for SwiftUI previews or tests. @@ -117,6 +109,30 @@ public struct GenerateContentResponse: Sendable { self.promptFeedback = promptFeedback self.usageMetadata = usageMetadata } + + func text(isThought: Bool) -> String? { + guard let candidate = candidates.first else { + AILog.error( + code: .generateContentResponseNoCandidates, + "Could not get text from a response that had no candidates." + ) + return nil + } + let textValues: [String] = candidate.content.parts.compactMap { part in + guard let textPart = part as? TextPart, part.isThought == isThought else { + return nil + } + return textPart.text + } + guard textValues.count > 0 else { + AILog.error( + code: .generateContentResponseNoText, + "Could not get a text part from the first candidate." + ) + return nil + } + return textValues.joined(separator: " ") + } } /// A struct representing a possible reply to a content generation prompt. Each content generation @@ -147,6 +163,12 @@ public struct Candidate: Sendable { self.citationMetadata = citationMetadata self.groundingMetadata = groundingMetadata } + + // Returns `true` if the candidate contains no information that a developer could use. + var isEmpty: Bool { + content.parts + .isEmpty && finishReason == nil && citationMetadata == nil && groundingMetadata == nil + } } /// A collection of source attributions for a piece of content. @@ -310,7 +332,7 @@ public struct PromptFeedback: Sendable { /// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) /// section within the Service Specific Terms). @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct GroundingMetadata: Sendable { +public struct GroundingMetadata: Sendable, Equatable, Hashable { /// A list of web search queries that the model performed to gather the grounding information. /// These can be used to allow users to explore the search results themselves. public let webSearchQueries: [String] @@ -327,7 +349,7 @@ public struct GroundingMetadata: Sendable { /// A struct representing the Google Search entry point. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) - public struct SearchEntryPoint: Sendable { + public struct SearchEntryPoint: Sendable, Equatable, Hashable { /// An HTML/CSS snippet that can be embedded in your app. /// /// To ensure proper rendering, it's recommended to display this content within a `WKWebView`. @@ -337,14 +359,14 @@ public struct GroundingMetadata: Sendable { /// Represents a chunk of retrieved data that supports a claim in the model's response. This is /// part of the grounding information provided when grounding is enabled. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) - public struct GroundingChunk: Sendable { + public struct GroundingChunk: Sendable, Equatable, Hashable { /// Contains details if the grounding chunk is from a web source. public let web: WebGroundingChunk? } /// A grounding chunk sourced from the web. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) - public struct WebGroundingChunk: Sendable { + public struct WebGroundingChunk: Sendable, Equatable, Hashable { /// The URI of the retrieved web page. public let uri: String? /// The title of the retrieved web page. @@ -358,7 +380,7 @@ public struct GroundingMetadata: Sendable { /// Provides information about how a specific segment of the model's response is supported by the /// retrieved grounding chunks. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) - public struct GroundingSupport: Sendable { + public struct GroundingSupport: Sendable, Equatable, Hashable { /// Specifies the segment of the model's response content that this grounding support pertains /// to. public let segment: Segment @@ -391,7 +413,7 @@ public struct GroundingMetadata: Sendable { /// Represents a specific segment within a ``ModelContent`` struct, often used to pinpoint the /// exact location of text or data that grounding information refers to. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct Segment: Sendable { +public struct Segment: Sendable, Equatable, Hashable { /// The zero-based index of the ``Part`` object within the `parts` array of its parent /// ``ModelContent`` object. This identifies which part of the content the segment belongs to. public let partIndex: Int @@ -509,15 +531,6 @@ extension Candidate: Decodable { finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason) - // The `content` may only be empty if a `finishReason` is included; if neither are included in - // the response then this is likely the `"content": {}` bug. - guard !content.parts.isEmpty || finishReason != nil else { - throw InvalidCandidateError.emptyContent(underlyingError: DecodingError.dataCorrupted(.init( - codingPath: [CodingKeys.content, CodingKeys.finishReason], - debugDescription: "Invalid Candidate: empty content and no finish reason" - ))) - } - citationMetadata = try container.decodeIfPresent( CitationMetadata.self, forKey: .citationMetadata diff --git a/FirebaseAI/Sources/GenerativeAIService.swift b/FirebaseAI/Sources/GenerativeAIService.swift index e1538af997f..8056d4172b8 100644 --- a/FirebaseAI/Sources/GenerativeAIService.swift +++ b/FirebaseAI/Sources/GenerativeAIService.swift @@ -177,7 +177,7 @@ struct GenerativeAIService { urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") if let appCheck = firebaseInfo.appCheck { - let tokenResult = await appCheck.getToken(forcingRefresh: false) + let tokenResult = try await fetchAppCheckToken(appCheck: appCheck) urlRequest.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck") if let error = tokenResult.error { AILog.error( @@ -207,6 +207,53 @@ struct GenerativeAIService { return urlRequest } + private func fetchAppCheckToken(appCheck: AppCheckInterop) async throws + -> FIRAppCheckTokenResultInterop { + if firebaseInfo.useLimitedUseAppCheckTokens { + if let token = await getLimitedUseAppCheckToken(appCheck: appCheck) { + return token + } + + let errorMessage = + "The provided App Check token provider doesn't implement getLimitedUseToken(), but requireLimitedUseTokens was enabled." + + #if Debug + fatalError(errorMessage) + #else + throw NSError( + domain: "\(Constants.baseErrorDomain).\(Self.self)", + code: AILog.MessageCode.appCheckTokenFetchFailed.rawValue, + userInfo: [NSLocalizedDescriptionKey: errorMessage] + ) + #endif + } + + return await appCheck.getToken(forcingRefresh: false) + } + + private func getLimitedUseAppCheckToken(appCheck: AppCheckInterop) async + -> FIRAppCheckTokenResultInterop? { + // At the moment, `await` doesn’t get along with Objective-C’s optional protocol methods. + await withCheckedContinuation { (continuation: CheckedContinuation< + FIRAppCheckTokenResultInterop?, + Never + >) in + guard + firebaseInfo.useLimitedUseAppCheckTokens, + // `getLimitedUseToken(completion:)` is an optional protocol method. Optional binding + // is performed to make sure `continuation` is called even if the method’s not implemented. + let limitedUseTokenClosure = appCheck.getLimitedUseToken + else { + return continuation.resume(returning: nil) + } + + limitedUseTokenClosure { tokenResult in + // The placeholder token should be used in the case of App Check error. + continuation.resume(returning: tokenResult) + } + } + } + private func httpResponse(urlResponse: URLResponse) throws -> HTTPURLResponse { // The following condition should always be true: "Whenever you make HTTP URL load requests, any // response objects you get back from the URLSession, NSURLConnection, or NSURLDownload class diff --git a/FirebaseAI/Sources/GenerativeModel.swift b/FirebaseAI/Sources/GenerativeModel.swift index 8d3f5e043a7..428e1fe6f26 100644 --- a/FirebaseAI/Sources/GenerativeModel.swift +++ b/FirebaseAI/Sources/GenerativeModel.swift @@ -174,6 +174,13 @@ public final class GenerativeModel: Sendable { throw GenerateContentError.responseStoppedEarly(reason: reason, response: response) } + // If all candidates are empty (contain no information that a developer could act on) then throw + if response.candidates.allSatisfy({ $0.isEmpty }) { + throw GenerateContentError.internalError(underlying: InvalidCandidateError.emptyContent( + underlyingError: Candidate.EmptyContentError() + )) + } + return response } @@ -223,6 +230,7 @@ public final class GenerativeModel: Sendable { let responseStream = generativeAIService.loadRequestStream(request: generateContentRequest) Task { do { + var didYieldResponse = false for try await response in responseStream { // Check the prompt feedback to see if the prompt was blocked. if response.promptFeedback?.blockReason != nil { @@ -237,9 +245,30 @@ public final class GenerativeModel: Sendable { ) } - continuation.yield(response) + // Skip returning the response if all candidates are empty (i.e., they contain no + // information that a developer could act on). + if response.candidates.allSatisfy({ $0.isEmpty }) { + AILog.log( + level: .debug, + code: .generateContentResponseEmptyCandidates, + "Skipped response with all empty candidates: \(response)" + ) + } else { + continuation.yield(response) + didYieldResponse = true + } + } + + // Throw an error if all responses were skipped due to empty content. + if didYieldResponse { + continuation.finish() + } else { + continuation.finish(throwing: GenerativeModel.generateContentError( + from: InvalidCandidateError.emptyContent( + underlyingError: Candidate.EmptyContentError() + ) + )) } - continuation.finish() } catch { continuation.finish(throwing: GenerativeModel.generateContentError(from: error)) return diff --git a/FirebaseAI/Sources/ModelContent.swift b/FirebaseAI/Sources/ModelContent.swift index 7d82bd76445..a0dfe6eb937 100644 --- a/FirebaseAI/Sources/ModelContent.swift +++ b/FirebaseAI/Sources/ModelContent.swift @@ -31,41 +31,73 @@ extension [ModelContent] { } } -/// A type describing data in media formats interpretable by an AI model. Each generative AI -/// request or response contains an `Array` of ``ModelContent``s, and each ``ModelContent`` value -/// may comprise multiple heterogeneous ``Part``s. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct ModelContent: Equatable, Sendable { - enum InternalPart: Equatable, Sendable { +struct InternalPart: Equatable, Sendable { + enum OneOfData: Equatable, Sendable { case text(String) - case inlineData(mimetype: String, Data) - case fileData(mimetype: String, uri: String) + case inlineData(InlineData) + case fileData(FileData) case functionCall(FunctionCall) case functionResponse(FunctionResponse) + + struct UnsupportedDataError: Error { + let decodingError: DecodingError + + var localizedDescription: String { + decodingError.localizedDescription + } + } } + let data: OneOfData? + + let isThought: Bool? + + let thoughtSignature: String? + + init(_ data: OneOfData, isThought: Bool?, thoughtSignature: String?) { + self.data = data + self.isThought = isThought + self.thoughtSignature = thoughtSignature + } +} + +/// A type describing data in media formats interpretable by an AI model. Each generative AI +/// request or response contains an `Array` of ``ModelContent``s, and each ``ModelContent`` value +/// may comprise multiple heterogeneous ``Part``s. +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +public struct ModelContent: Equatable, Sendable { /// The role of the entity creating the ``ModelContent``. For user-generated client requests, /// for example, the role is `user`. public let role: String? /// The data parts comprising this ``ModelContent`` value. public var parts: [any Part] { - var convertedParts = [any Part]() - for part in internalParts { - switch part { + return internalParts.compactMap { part -> (any Part)? in + switch part.data { case let .text(text): - convertedParts.append(TextPart(text)) - case let .inlineData(mimetype, data): - convertedParts.append(InlineDataPart(data: data, mimeType: mimetype)) - case let .fileData(mimetype, uri): - convertedParts.append(FileDataPart(uri: uri, mimeType: mimetype)) + return TextPart(text, isThought: part.isThought, thoughtSignature: part.thoughtSignature) + case let .inlineData(inlineData): + return InlineDataPart( + inlineData, isThought: part.isThought, thoughtSignature: part.thoughtSignature + ) + case let .fileData(fileData): + return FileDataPart( + fileData, isThought: part.isThought, thoughtSignature: part.thoughtSignature + ) case let .functionCall(functionCall): - convertedParts.append(FunctionCallPart(functionCall)) + return FunctionCallPart( + functionCall, isThought: part.isThought, thoughtSignature: part.thoughtSignature + ) case let .functionResponse(functionResponse): - convertedParts.append(FunctionResponsePart(functionResponse)) + return FunctionResponsePart( + functionResponse, isThought: part.isThought, thoughtSignature: part.thoughtSignature + ) + case .none: + // Filter out parts that contain missing or unrecognized data + return nil } } - return convertedParts } // TODO: Refactor this @@ -78,17 +110,35 @@ public struct ModelContent: Equatable, Sendable { for part in parts { switch part { case let textPart as TextPart: - convertedParts.append(.text(textPart.text)) + convertedParts.append(InternalPart( + .text(textPart.text), + isThought: textPart._isThought, + thoughtSignature: textPart.thoughtSignature + )) case let inlineDataPart as InlineDataPart: - let inlineData = inlineDataPart.inlineData - convertedParts.append(.inlineData(mimetype: inlineData.mimeType, inlineData.data)) + convertedParts.append(InternalPart( + .inlineData(inlineDataPart.inlineData), + isThought: inlineDataPart._isThought, + thoughtSignature: inlineDataPart.thoughtSignature + )) case let fileDataPart as FileDataPart: - let fileData = fileDataPart.fileData - convertedParts.append(.fileData(mimetype: fileData.mimeType, uri: fileData.fileURI)) + convertedParts.append(InternalPart( + .fileData(fileDataPart.fileData), + isThought: fileDataPart._isThought, + thoughtSignature: fileDataPart.thoughtSignature + )) case let functionCallPart as FunctionCallPart: - convertedParts.append(.functionCall(functionCallPart.functionCall)) + convertedParts.append(InternalPart( + .functionCall(functionCallPart.functionCall), + isThought: functionCallPart._isThought, + thoughtSignature: functionCallPart.thoughtSignature + )) case let functionResponsePart as FunctionResponsePart: - convertedParts.append(.functionResponse(functionResponsePart.functionResponse)) + convertedParts.append(InternalPart( + .functionResponse(functionResponsePart.functionResponse), + isThought: functionResponsePart._isThought, + thoughtSignature: functionResponsePart.thoughtSignature + )) default: fatalError() } @@ -102,6 +152,11 @@ public struct ModelContent: Equatable, Sendable { let content = parts.flatMap { $0.partsValue } self.init(role: role, parts: content) } + + init(role: String?, parts: [InternalPart]) { + self.role = role + internalParts = parts + } } // MARK: Codable Conformances @@ -121,7 +176,36 @@ extension ModelContent: Codable { } @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -extension ModelContent.InternalPart: Codable { +extension InternalPart: Codable { + enum CodingKeys: String, CodingKey { + case isThought = "thought" + case thoughtSignature + } + + public func encode(to encoder: Encoder) throws { + try data.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(isThought, forKey: .isThought) + try container.encodeIfPresent(thoughtSignature, forKey: .thoughtSignature) + } + + public init(from decoder: Decoder) throws { + do { + data = try OneOfData(from: decoder) + } catch let error as OneOfData.UnsupportedDataError { + AILog.error(code: .decodedUnsupportedPartData, error.localizedDescription) + data = nil + } catch { // Re-throw any other error types + throw error + } + let container = try decoder.container(keyedBy: CodingKeys.self) + isThought = try container.decodeIfPresent(Bool.self, forKey: .isThought) + thoughtSignature = try container.decodeIfPresent(String.self, forKey: .thoughtSignature) + } +} + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +extension InternalPart.OneOfData: Codable { enum CodingKeys: String, CodingKey { case text case inlineData @@ -135,10 +219,10 @@ extension ModelContent.InternalPart: Codable { switch self { case let .text(text): try container.encode(text, forKey: .text) - case let .inlineData(mimetype, bytes): - try container.encode(InlineData(data: bytes, mimeType: mimetype), forKey: .inlineData) - case let .fileData(mimetype: mimetype, url): - try container.encode(FileData(fileURI: url, mimeType: mimetype), forKey: .fileData) + case let .inlineData(inlineData): + try container.encode(inlineData, forKey: .inlineData) + case let .fileData(fileData): + try container.encode(fileData, forKey: .fileData) case let .functionCall(functionCall): try container.encode(functionCall, forKey: .functionCall) case let .functionResponse(functionResponse): @@ -151,20 +235,20 @@ extension ModelContent.InternalPart: Codable { if values.contains(.text) { self = try .text(values.decode(String.self, forKey: .text)) } else if values.contains(.inlineData) { - let inlineData = try values.decode(InlineData.self, forKey: .inlineData) - self = .inlineData(mimetype: inlineData.mimeType, inlineData.data) + self = try .inlineData(values.decode(InlineData.self, forKey: .inlineData)) } else if values.contains(.fileData) { - let fileData = try values.decode(FileData.self, forKey: .fileData) - self = .fileData(mimetype: fileData.mimeType, uri: fileData.fileURI) + self = try .fileData(values.decode(FileData.self, forKey: .fileData)) } else if values.contains(.functionCall) { self = try .functionCall(values.decode(FunctionCall.self, forKey: .functionCall)) } else if values.contains(.functionResponse) { self = try .functionResponse(values.decode(FunctionResponse.self, forKey: .functionResponse)) } else { let unexpectedKeys = values.allKeys.map { $0.stringValue } - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: values.codingPath, - debugDescription: "Unexpected Part type(s): \(unexpectedKeys)" + throw UnsupportedDataError(decodingError: DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: values.codingPath, + debugDescription: "Unexpected Part type(s): \(unexpectedKeys)" + ) )) } } diff --git a/FirebaseAI/Sources/Types/Internal/Errors/EmptyContentError.swift b/FirebaseAI/Sources/Types/Internal/Errors/EmptyContentError.swift new file mode 100644 index 00000000000..7c33a975c18 --- /dev/null +++ b/FirebaseAI/Sources/Types/Internal/Errors/EmptyContentError.swift @@ -0,0 +1,20 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +extension Candidate { + struct EmptyContentError: Error { + let localizedDescription = "Invalid Candidate: empty content and no finish reason" + } +} diff --git a/FirebaseAI/Sources/Types/Internal/Imagen/ImageGenerationParameters.swift b/FirebaseAI/Sources/Types/Internal/Imagen/ImageGenerationParameters.swift index 4189e5fbac7..aa1e1b085c8 100644 --- a/FirebaseAI/Sources/Types/Internal/Imagen/ImageGenerationParameters.swift +++ b/FirebaseAI/Sources/Types/Internal/Imagen/ImageGenerationParameters.swift @@ -23,6 +23,7 @@ struct ImageGenerationParameters { let outputOptions: ImageGenerationOutputOptions? let addWatermark: Bool? let includeResponsibleAIFilterReason: Bool? + let includeSafetyAttributes: Bool? } @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) @@ -42,6 +43,7 @@ extension ImageGenerationParameters: Encodable { case outputOptions case addWatermark case includeResponsibleAIFilterReason = "includeRaiReason" + case includeSafetyAttributes } func encode(to encoder: any Encoder) throws { @@ -58,5 +60,6 @@ extension ImageGenerationParameters: Encodable { includeResponsibleAIFilterReason, forKey: .includeResponsibleAIFilterReason ) + try container.encodeIfPresent(includeSafetyAttributes, forKey: .includeSafetyAttributes) } } diff --git a/FirebaseAI/Sources/Types/Internal/Imagen/ImagenSafetyAttributes.swift b/FirebaseAI/Sources/Types/Internal/Imagen/ImagenSafetyAttributes.swift new file mode 100644 index 00000000000..3dcb93d544a --- /dev/null +++ b/FirebaseAI/Sources/Types/Internal/Imagen/ImagenSafetyAttributes.swift @@ -0,0 +1,24 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +/// A `safetyAttributes` "prediction" from Imagen. +/// +/// This prediction is currently unused by the SDK and is only checked to be valid JSON. This type +/// is currently only used to avoid logging unsupported prediction types. +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +struct ImagenSafetyAttributes: Decodable { + let safetyAttributes: JSONObject +} diff --git a/FirebaseAI/Sources/Types/Internal/InternalPart.swift b/FirebaseAI/Sources/Types/Internal/InternalPart.swift index d543fb80f38..bb62dd4c0b5 100644 --- a/FirebaseAI/Sources/Types/Internal/InternalPart.swift +++ b/FirebaseAI/Sources/Types/Internal/InternalPart.swift @@ -67,6 +67,9 @@ struct FunctionResponse: Codable, Equatable, Sendable { struct ErrorPart: Part, Error { let error: Error + let isThought = false + let thoughtSignature: String? = nil + init(_ error: Error) { self.error = error } diff --git a/FirebaseAI/Sources/Types/Public/Imagen/ImagenGenerationResponse.swift b/FirebaseAI/Sources/Types/Public/Imagen/ImagenGenerationResponse.swift index f9816908c6d..9ed52c4d0e9 100644 --- a/FirebaseAI/Sources/Types/Public/Imagen/ImagenGenerationResponse.swift +++ b/FirebaseAI/Sources/Types/Public/Imagen/ImagenGenerationResponse.swift @@ -60,6 +60,8 @@ extension ImagenGenerationResponse: Decodable where T: Decodable { images.append(image) } else if let filteredReason = try? predictionsContainer.decode(RAIFilteredReason.self) { filteredReasons.append(filteredReason.raiFilteredReason) + } else if let _ = try? predictionsContainer.decode(ImagenSafetyAttributes.self) { + // Ignore SafetyAttributes "prediction" to avoid logging in `unsupportedPrediction` below. } else if let unsupportedPrediction = try? predictionsContainer.decode(JSONObject.self) { AILog.warning( code: .decodedUnsupportedImagenPredictionType, diff --git a/FirebaseAI/Sources/Types/Public/Imagen/ImagenModel.swift b/FirebaseAI/Sources/Types/Public/Imagen/ImagenModel.swift index e6f96df511a..729fad9f28d 100644 --- a/FirebaseAI/Sources/Types/Public/Imagen/ImagenModel.swift +++ b/FirebaseAI/Sources/Types/Public/Imagen/ImagenModel.swift @@ -159,7 +159,8 @@ public final class ImagenModel { ) }, addWatermark: generationConfig?.addWatermark, - includeResponsibleAIFilterReason: true + includeResponsibleAIFilterReason: true, + includeSafetyAttributes: true ) } } diff --git a/FirebaseAI/Sources/Types/Public/Part.swift b/FirebaseAI/Sources/Types/Public/Part.swift index 4890b725f4d..fb743d1025d 100644 --- a/FirebaseAI/Sources/Types/Public/Part.swift +++ b/FirebaseAI/Sources/Types/Public/Part.swift @@ -18,7 +18,14 @@ import Foundation /// /// Within a single value of ``Part``, different data types may not mix. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public protocol Part: PartsRepresentable, Codable, Sendable, Equatable {} +public protocol Part: PartsRepresentable, Codable, Sendable, Equatable { + /// Indicates whether this `Part` is a summary of the model's internal thinking process. + /// + /// When `includeThoughts` is set to `true` in ``ThinkingConfig``, the model may return one or + /// more "thought" parts that provide insight into how it reasoned through the prompt to arrive + /// at the final answer. These parts will have `isThought` set to `true`. + var isThought: Bool { get } +} /// A text part containing a string value. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) @@ -26,8 +33,20 @@ public struct TextPart: Part { /// Text value. public let text: String + public var isThought: Bool { _isThought ?? false } + + let thoughtSignature: String? + + let _isThought: Bool? + public init(_ text: String) { + self.init(text, isThought: nil, thoughtSignature: nil) + } + + init(_ text: String, isThought: Bool?, thoughtSignature: String?) { self.text = text + _isThought = isThought + self.thoughtSignature = thoughtSignature } } @@ -45,6 +64,7 @@ public struct TextPart: Part { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct InlineDataPart: Part { let inlineData: InlineData + let _isThought: Bool? /// The data provided in the inline data part. public var data: Data { inlineData.data } @@ -52,6 +72,10 @@ public struct InlineDataPart: Part { /// The IANA standard MIME type of the data. public var mimeType: String { inlineData.mimeType } + public var isThought: Bool { _isThought ?? false } + + let thoughtSignature: String? + /// Creates an inline data part from data and a MIME type. /// /// > Important: Supported input types depend on the model on the model being used; see [input @@ -67,11 +91,13 @@ public struct InlineDataPart: Part { /// requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) for /// supported values. public init(data: Data, mimeType: String) { - self.init(InlineData(data: data, mimeType: mimeType)) + self.init(InlineData(data: data, mimeType: mimeType), isThought: nil, thoughtSignature: nil) } - init(_ inlineData: InlineData) { + init(_ inlineData: InlineData, isThought: Bool?, thoughtSignature: String?) { self.inlineData = inlineData + _isThought = isThought + self.thoughtSignature = thoughtSignature } } @@ -79,9 +105,12 @@ public struct InlineDataPart: Part { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct FileDataPart: Part { let fileData: FileData + let _isThought: Bool? + let thoughtSignature: String? public var uri: String { fileData.fileURI } public var mimeType: String { fileData.mimeType } + public var isThought: Bool { _isThought ?? false } /// Constructs a new file data part. /// @@ -93,11 +122,13 @@ public struct FileDataPart: Part { /// requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) for /// supported values. public init(uri: String, mimeType: String) { - self.init(FileData(fileURI: uri, mimeType: mimeType)) + self.init(FileData(fileURI: uri, mimeType: mimeType), isThought: nil, thoughtSignature: nil) } - init(_ fileData: FileData) { + init(_ fileData: FileData, isThought: Bool?, thoughtSignature: String?) { self.fileData = fileData + _isThought = isThought + self.thoughtSignature = thoughtSignature } } @@ -105,6 +136,8 @@ public struct FileDataPart: Part { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct FunctionCallPart: Part { let functionCall: FunctionCall + let _isThought: Bool? + let thoughtSignature: String? /// The name of the function to call. public var name: String { functionCall.name } @@ -112,6 +145,8 @@ public struct FunctionCallPart: Part { /// The function parameters and values. public var args: JSONObject { functionCall.args } + public var isThought: Bool { _isThought ?? false } + /// Constructs a new function call part. /// /// > Note: A `FunctionCallPart` is typically received from the model, rather than created @@ -121,11 +156,13 @@ public struct FunctionCallPart: Part { /// - name: The name of the function to call. /// - args: The function parameters and values. public init(name: String, args: JSONObject) { - self.init(FunctionCall(name: name, args: args)) + self.init(FunctionCall(name: name, args: args), isThought: nil, thoughtSignature: nil) } - init(_ functionCall: FunctionCall) { + init(_ functionCall: FunctionCall, isThought: Bool?, thoughtSignature: String?) { self.functionCall = functionCall + _isThought = isThought + self.thoughtSignature = thoughtSignature } } @@ -137,6 +174,8 @@ public struct FunctionCallPart: Part { @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct FunctionResponsePart: Part { let functionResponse: FunctionResponse + let _isThought: Bool? + let thoughtSignature: String? /// The name of the function that was called. public var name: String { functionResponse.name } @@ -144,16 +183,22 @@ public struct FunctionResponsePart: Part { /// The function's response or return value. public var response: JSONObject { functionResponse.response } + public var isThought: Bool { _isThought ?? false } + /// Constructs a new `FunctionResponse`. /// /// - Parameters: /// - name: The name of the function that was called. /// - response: The function's response. public init(name: String, response: JSONObject) { - self.init(FunctionResponse(name: name, response: response)) + self.init( + FunctionResponse(name: name, response: response), isThought: nil, thoughtSignature: nil + ) } - init(_ functionResponse: FunctionResponse) { + init(_ functionResponse: FunctionResponse, isThought: Bool?, thoughtSignature: String?) { self.functionResponse = functionResponse + _isThought = isThought + self.thoughtSignature = thoughtSignature } } diff --git a/FirebaseAI/Sources/Types/Public/ThinkingConfig.swift b/FirebaseAI/Sources/Types/Public/ThinkingConfig.swift index c0e8f31465b..a339f8fa1d1 100644 --- a/FirebaseAI/Sources/Types/Public/ThinkingConfig.swift +++ b/FirebaseAI/Sources/Types/Public/ThinkingConfig.swift @@ -37,12 +37,24 @@ public struct ThinkingConfig: Sendable { /// feature or if the specified budget is not within the model's supported range. let thinkingBudget: Int? + /// Whether summaries of the model's "thoughts" are included in responses. + /// + /// When `includeThoughts` is set to `true`, the model will return a summary of its internal + /// thinking process alongside the final answer. This can provide valuable insight into how the + /// model arrived at its conclusion, which is particularly useful for complex or creative tasks. + /// + /// If you don't specify a value for `includeThoughts` (`nil`), the model will use its default + /// behavior (which is typically to not include thought summaries). + let includeThoughts: Bool? + /// Initializes a new `ThinkingConfig`. /// /// - Parameters: /// - thinkingBudget: The maximum number of tokens to be used for the model's thinking process. - public init(thinkingBudget: Int? = nil) { + /// - includeThoughts: If true, summaries of the model's "thoughts" are included in responses. + public init(thinkingBudget: Int? = nil, includeThoughts: Bool? = nil) { self.thinkingBudget = thinkingBudget + self.includeThoughts = includeThoughts } } diff --git a/FirebaseAI/Tests/TestApp/Sources/Constants.swift b/FirebaseAI/Tests/TestApp/Sources/Constants.swift index ef7d9e7c061..be5c0c06891 100644 --- a/FirebaseAI/Tests/TestApp/Sources/Constants.swift +++ b/FirebaseAI/Tests/TestApp/Sources/Constants.swift @@ -24,6 +24,7 @@ public enum ModelNames { public static let gemini2Flash = "gemini-2.0-flash-001" public static let gemini2FlashLite = "gemini-2.0-flash-lite-001" public static let gemini2FlashPreviewImageGeneration = "gemini-2.0-flash-preview-image-generation" + public static let gemini2_5_FlashImagePreview = "gemini-2.5-flash-image-preview" public static let gemini2_5_Flash = "gemini-2.5-flash" public static let gemini2_5_Pro = "gemini-2.5-pro" public static let gemma3_4B = "gemma-3-4b-it" diff --git a/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift b/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift index da77cea9df7..5b70223ece4 100644 --- a/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift +++ b/FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift @@ -50,13 +50,18 @@ struct GenerateContentIntegrationTests { @Test(arguments: [ (InstanceConfig.vertexAI_v1beta, ModelNames.gemini2FlashLite), (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2FlashLite), - (InstanceConfig.vertexAI_v1beta_staging, ModelNames.gemini2FlashLite), + (InstanceConfig.vertexAI_v1beta_global_appCheckLimitedUse, ModelNames.gemini2FlashLite), (InstanceConfig.googleAI_v1beta, ModelNames.gemini2FlashLite), + (InstanceConfig.googleAI_v1beta_appCheckLimitedUse, ModelNames.gemini2FlashLite), (InstanceConfig.googleAI_v1beta, ModelNames.gemma3_4B), - (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2FlashLite), - (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma3_4B), - (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2FlashLite), - (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma3_4B), + (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2FlashLite), + (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemma3_4B), + // Note: The following configs are commented out for easy one-off manual testing. + // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2FlashLite), + // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma3_4B), + // (InstanceConfig.vertexAI_v1beta_staging, ModelNames.gemini2FlashLite), + // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2FlashLite), + // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma3_4B), ]) func generateContent(_ config: InstanceConfig, modelName: String) async throws { let model = FirebaseAI.componentInstance(config).generativeModel( @@ -134,47 +139,84 @@ struct GenerateContentIntegrationTests { #expect(candidatesTokensDetails.tokenCount == usageMetadata.candidatesTokenCount) } - @Test(arguments: [ - (InstanceConfig.vertexAI_v1beta, ModelNames.gemini2_5_Flash, 0), - (InstanceConfig.vertexAI_v1beta, ModelNames.gemini2_5_Flash, 24576), - (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2_5_Pro, 128), - (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2_5_Pro, 32768), - (InstanceConfig.googleAI_v1beta, ModelNames.gemini2_5_Flash, 0), - (InstanceConfig.googleAI_v1beta, ModelNames.gemini2_5_Flash, 24576), - (InstanceConfig.googleAI_v1beta, ModelNames.gemini2_5_Pro, 128), - (InstanceConfig.googleAI_v1beta, ModelNames.gemini2_5_Pro, 32768), - (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2_5_Flash, 0), - (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2_5_Flash, 24576), - ]) + @Test( + arguments: [ + (.vertexAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig(thinkingBudget: 0)), + (.vertexAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig(thinkingBudget: 24576)), + (.vertexAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig( + thinkingBudget: 24576, includeThoughts: true + )), + (.vertexAI_v1beta_global, ModelNames.gemini2_5_Pro, ThinkingConfig(thinkingBudget: 128)), + (.vertexAI_v1beta_global, ModelNames.gemini2_5_Pro, ThinkingConfig(thinkingBudget: 32768)), + (.vertexAI_v1beta_global, ModelNames.gemini2_5_Pro, ThinkingConfig( + thinkingBudget: 32768, includeThoughts: true + )), + (.googleAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig(thinkingBudget: 0)), + (.googleAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig(thinkingBudget: 24576)), + (.googleAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig( + thinkingBudget: 24576, includeThoughts: true + )), + (.googleAI_v1beta, ModelNames.gemini2_5_Pro, ThinkingConfig(thinkingBudget: 128)), + (.googleAI_v1beta, ModelNames.gemini2_5_Pro, ThinkingConfig(thinkingBudget: 32768)), + (.googleAI_v1beta, ModelNames.gemini2_5_Pro, ThinkingConfig( + thinkingBudget: 32768, includeThoughts: true + )), + (.googleAI_v1beta_freeTier, ModelNames.gemini2_5_Flash, ThinkingConfig(thinkingBudget: 0)), + ( + .googleAI_v1beta_freeTier, + ModelNames.gemini2_5_Flash, + ThinkingConfig(thinkingBudget: 24576) + ), + (.googleAI_v1beta_freeTier, ModelNames.gemini2_5_Flash, ThinkingConfig( + thinkingBudget: 24576, includeThoughts: true + )), + // Note: The following configs are commented out for easy one-off manual testing. + // (.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2_5_Flash, ThinkingConfig( + // thinkingBudget: 0 + // )), + // (.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2_5_Flash, ThinkingConfig( + // thinkingBudget: 24576 + // )), + // (.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2_5_Flash, ThinkingConfig( + // thinkingBudget: 24576, includeThoughts: true + // )), + ] as [(InstanceConfig, String, ThinkingConfig)] + ) func generateContentThinking(_ config: InstanceConfig, modelName: String, - thinkingBudget: Int) async throws { + thinkingConfig: ThinkingConfig) async throws { let model = FirebaseAI.componentInstance(config).generativeModel( modelName: modelName, generationConfig: GenerationConfig( temperature: 0.0, topP: 0.0, topK: 1, - thinkingConfig: ThinkingConfig(thinkingBudget: thinkingBudget) + thinkingConfig: thinkingConfig ), safetySettings: safetySettings ) + let chat = model.startChat() let prompt = "Where is Google headquarters located? Answer with the city name only." - let response = try await model.generateContent(prompt) + let response = try await chat.sendMessage(prompt) let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines) #expect(text == "Mountain View") + let candidate = try #require(response.candidates.first) + let thoughtParts = candidate.content.parts.compactMap { $0.isThought ? $0 : nil } + #expect(thoughtParts.isEmpty != (thinkingConfig.includeThoughts ?? false)) + let usageMetadata = try #require(response.usageMetadata) #expect(usageMetadata.promptTokenCount.isEqual(to: 13, accuracy: tokenCountAccuracy)) #expect(usageMetadata.promptTokensDetails.count == 1) let promptTokensDetails = try #require(usageMetadata.promptTokensDetails.first) #expect(promptTokensDetails.modality == .text) #expect(promptTokensDetails.tokenCount == usageMetadata.promptTokenCount) - if thinkingBudget == 0 { - #expect(usageMetadata.thoughtsTokenCount == 0) - } else { + if let thinkingBudget = thinkingConfig.thinkingBudget, thinkingBudget > 0 { + #expect(usageMetadata.thoughtsTokenCount > 0) #expect(usageMetadata.thoughtsTokenCount <= thinkingBudget) + } else { + #expect(usageMetadata.thoughtsTokenCount == 0) } #expect(usageMetadata.candidatesTokenCount.isEqual(to: 3, accuracy: tokenCountAccuracy)) // The `candidatesTokensDetails` field is erroneously omitted when using the Google AI (Gemini @@ -195,22 +237,118 @@ struct GenerateContentIntegrationTests { )) } + @Test( + arguments: [ + (.vertexAI_v1beta_global, ModelNames.gemini2_5_Flash, ThinkingConfig(thinkingBudget: -1)), + (.vertexAI_v1beta_global, ModelNames.gemini2_5_Flash, ThinkingConfig( + thinkingBudget: -1, includeThoughts: true + )), + (.vertexAI_v1beta_global, ModelNames.gemini2_5_Pro, ThinkingConfig(thinkingBudget: -1)), + (.vertexAI_v1beta_global, ModelNames.gemini2_5_Pro, ThinkingConfig( + thinkingBudget: -1, includeThoughts: true + )), + (.googleAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig(thinkingBudget: -1)), + (.googleAI_v1beta, ModelNames.gemini2_5_Flash, ThinkingConfig( + thinkingBudget: -1, includeThoughts: true + )), + (.googleAI_v1beta, ModelNames.gemini2_5_Pro, ThinkingConfig(thinkingBudget: -1)), + (.googleAI_v1beta, ModelNames.gemini2_5_Pro, ThinkingConfig( + thinkingBudget: -1, includeThoughts: true + )), + ] as [(InstanceConfig, String, ThinkingConfig)] + ) + func generateContentThinkingFunctionCalling(_ config: InstanceConfig, modelName: String, + thinkingConfig: ThinkingConfig) async throws { + let getTemperatureDeclaration = FunctionDeclaration( + name: "getTemperature", + description: "Returns the current temperature in Celsius for the specified location", + parameters: [ + "city": .string(), + "region": .string(description: "The province or state"), + "country": .string(), + ] + ) + let model = FirebaseAI.componentInstance(config).generativeModel( + modelName: modelName, + generationConfig: GenerationConfig( + temperature: 0.0, + topP: 0.0, + topK: 1, + thinkingConfig: thinkingConfig + ), + safetySettings: safetySettings, + tools: [.functionDeclarations([getTemperatureDeclaration])], + systemInstruction: ModelContent(parts: """ + You are a weather bot that specializes in reporting outdoor temperatures in Celsius. + + Always use the `getTemperature` function to determine the current temperature in a location. + + Always respond in the format: + - Location: City, Province/State, Country + - Temperature: #C + """) + ) + let chat = model.startChat() + let prompt = "What is the current temperature in Waterloo, Ontario, Canada?" + + let response = try await chat.sendMessage(prompt) + + #expect(response.functionCalls.count == 1) + let temperatureFunctionCall = try #require(response.functionCalls.first) + try #require(temperatureFunctionCall.name == getTemperatureDeclaration.name) + #expect(temperatureFunctionCall.args == [ + "city": .string("Waterloo"), + "region": .string("Ontario"), + "country": .string("Canada"), + ]) + #expect(temperatureFunctionCall.isThought == false) + let thoughtSignature = try #require(temperatureFunctionCall.thoughtSignature) + #expect(!thoughtSignature.isEmpty) + + let temperatureFunctionResponse = FunctionResponsePart( + name: temperatureFunctionCall.name, + response: [ + "temperature": .number(25), + "units": .string("Celsius"), + ] + ) + + let response2 = try await chat.sendMessage(temperatureFunctionResponse) + + #expect(response2.functionCalls.isEmpty) + let finalText = try #require(response2.text).trimmingCharacters(in: .whitespacesAndNewlines) + #expect(finalText.contains("Waterloo")) + #expect(finalText.contains("25")) + } + @Test(arguments: [ - InstanceConfig.vertexAI_v1beta, - InstanceConfig.vertexAI_v1beta_global, - InstanceConfig.googleAI_v1beta, - InstanceConfig.googleAI_v1beta_staging, - InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, + (InstanceConfig.vertexAI_v1beta, ModelNames.gemini2FlashPreviewImageGeneration), + (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2FlashPreviewImageGeneration), + (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2_5_FlashImagePreview), + (InstanceConfig.googleAI_v1beta, ModelNames.gemini2FlashPreviewImageGeneration), + (InstanceConfig.googleAI_v1beta, ModelNames.gemini2_5_FlashImagePreview), + // Note: The following configs are commented out for easy one-off manual testing. + // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2FlashPreviewImageGeneration) + // (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2FlashPreviewImageGeneration), + // ( + // InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, + // ModelNames.gemini2FlashPreviewImageGeneration + // ), ]) - func generateImage(_ config: InstanceConfig) async throws { + func generateImage(_ config: InstanceConfig, modelName: String) async throws { let generationConfig = GenerationConfig( temperature: 0.0, topP: 0.0, topK: 1, responseModalities: [.text, .image] ) + let safetySettings = safetySettings.filter { + // HARM_CATEGORY_CIVIC_INTEGRITY is deprecated in Vertex AI but only rejected when using the + // 'gemini-2.0-flash-preview-image-generation' model. + $0.harmCategory != .civicIntegrity + } let model = FirebaseAI.componentInstance(config).generativeModel( - modelName: ModelNames.gemini2FlashPreviewImageGeneration, + modelName: modelName, generationConfig: generationConfig, safetySettings: safetySettings ) @@ -291,13 +429,18 @@ struct GenerateContentIntegrationTests { @Test(arguments: [ (InstanceConfig.vertexAI_v1beta, ModelNames.gemini2FlashLite), (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2FlashLite), - (InstanceConfig.vertexAI_v1beta_staging, ModelNames.gemini2FlashLite), + (InstanceConfig.vertexAI_v1beta_global_appCheckLimitedUse, ModelNames.gemini2FlashLite), (InstanceConfig.googleAI_v1beta, ModelNames.gemini2FlashLite), + (InstanceConfig.googleAI_v1beta_appCheckLimitedUse, ModelNames.gemini2FlashLite), (InstanceConfig.googleAI_v1beta, ModelNames.gemma3_4B), - (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2FlashLite), - (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma3_4B), - (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2FlashLite), - (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma3_4B), + (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2FlashLite), + (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemma3_4B), + // Note: The following configs are commented out for easy one-off manual testing. + // (InstanceConfig.vertexAI_v1beta_staging, ModelNames.gemini2FlashLite), + // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2FlashLite), + // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemma3_4B), + // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemini2FlashLite), + // (InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, ModelNames.gemma3_4B), ]) func generateContentStream(_ config: InstanceConfig, modelName: String) async throws { let expectedResponse = [ @@ -346,6 +489,73 @@ struct GenerateContentIntegrationTests { #expect(response == expectedResponse) } + @Test(arguments: [ + (InstanceConfig.vertexAI_v1beta, ModelNames.gemini2FlashPreviewImageGeneration), + (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2FlashPreviewImageGeneration), + (InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2_5_FlashImagePreview), + (InstanceConfig.googleAI_v1beta, ModelNames.gemini2FlashPreviewImageGeneration), + (InstanceConfig.googleAI_v1beta, ModelNames.gemini2_5_FlashImagePreview), + // Note: The following configs are commented out for easy one-off manual testing. + // (InstanceConfig.googleAI_v1beta_staging, ModelNames.gemini2FlashPreviewImageGeneration) + // (InstanceConfig.googleAI_v1beta_freeTier, ModelNames.gemini2FlashPreviewImageGeneration), + // ( + // InstanceConfig.googleAI_v1beta_freeTier_bypassProxy, + // ModelNames.gemini2FlashPreviewImageGeneration + // ), + ]) + func generateImageStreaming(_ config: InstanceConfig, modelName: String) async throws { + let generationConfig = GenerationConfig( + temperature: 0.0, + topP: 0.0, + topK: 1, + responseModalities: [.text, .image] + ) + let safetySettings = safetySettings.filter { + // HARM_CATEGORY_CIVIC_INTEGRITY is deprecated in Vertex AI but only rejected when using the + // 'gemini-2.0-flash-preview-image-generation' model. + $0.harmCategory != .civicIntegrity + } + let model = FirebaseAI.componentInstance(config).generativeModel( + modelName: modelName, + generationConfig: generationConfig, + safetySettings: safetySettings + ) + let prompt = "Generate an image of a cute cartoon kitten playing with a ball of yarn" + + let stream = try model.generateContentStream(prompt) + + var inlineDataParts = [InlineDataPart]() + for try await response in stream { + let candidate = try #require(response.candidates.first) + let inlineDataPart = candidate.content.parts.first { $0 is InlineDataPart } as? InlineDataPart + if let inlineDataPart { + inlineDataParts.append(inlineDataPart) + let inlineDataPartsViaAccessor = response.inlineDataParts + #expect(inlineDataPartsViaAccessor.count == 1) + #expect(inlineDataPartsViaAccessor == response.inlineDataParts) + } + let textPart = candidate.content.parts.first { $0 is TextPart } as? TextPart + #expect( + inlineDataPart != nil || textPart != nil || candidate.finishReason == .stop, + "No text or image found in the candidate" + ) + } + + #expect(inlineDataParts.count == 1) + let inlineDataPart = try #require(inlineDataParts.first) + #expect(inlineDataPart.mimeType == "image/png") + #expect(inlineDataPart.data.count > 0) + #if canImport(UIKit) + let uiImage = try #require(UIImage(data: inlineDataPart.data)) + // Gemini 2.0 Flash Experimental returns images sized to fit within a 1024x1024 pixel box but + // dimensions may vary depending on the aspect ratio. + #expect(uiImage.size.width <= 1024) + #expect(uiImage.size.width >= 500) + #expect(uiImage.size.height <= 1024) + #expect(uiImage.size.height >= 500) + #endif // canImport(UIKit) + } + // MARK: - App Check Tests @Test(arguments: InstanceConfig.appCheckNotConfiguredConfigs) diff --git a/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift b/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift index 21554d28250..df06f43c91f 100644 --- a/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift +++ b/FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift @@ -27,12 +27,20 @@ struct InstanceConfig: Equatable, Encodable { location: "global", apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1beta) ) + static let vertexAI_v1beta_global_appCheckLimitedUse = InstanceConfig( + location: "global", + useLimitedUseAppCheckTokens: true, + apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1beta) + ) static let vertexAI_v1beta_staging = InstanceConfig( apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyStaging), version: .v1beta) ) static let googleAI_v1beta = InstanceConfig( apiConfig: APIConfig(service: .googleAI(endpoint: .firebaseProxyProd), version: .v1beta) ) + static let googleAI_v1beta_appCheckLimitedUse = InstanceConfig( + apiConfig: APIConfig(service: .googleAI(endpoint: .firebaseProxyProd), version: .v1beta) + ) static let googleAI_v1beta_staging = InstanceConfig( apiConfig: APIConfig(service: .googleAI(endpoint: .firebaseProxyStaging), version: .v1beta) ) @@ -48,33 +56,54 @@ struct InstanceConfig: Equatable, Encodable { static let allConfigs = [ vertexAI_v1beta, vertexAI_v1beta_global, - vertexAI_v1beta_staging, + vertexAI_v1beta_global_appCheckLimitedUse, googleAI_v1beta, - googleAI_v1beta_staging, - googleAI_v1beta_freeTier_bypassProxy, + googleAI_v1beta_appCheckLimitedUse, + googleAI_v1beta_freeTier, + // Note: The following configs are commented out for easy one-off manual testing. + // vertexAI_v1beta_staging, + // googleAI_v1beta_staging, + // googleAI_v1beta_freeTier_bypassProxy, ] static let vertexAI_v1beta_appCheckNotConfigured = InstanceConfig( appName: FirebaseAppNames.appCheckNotConfigured, apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1beta) ) + static let vertexAI_v1beta_appCheckNotConfigured_limitedUseTokens = InstanceConfig( + appName: FirebaseAppNames.appCheckNotConfigured, + useLimitedUseAppCheckTokens: true, + apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1beta) + ) static let googleAI_v1beta_appCheckNotConfigured = InstanceConfig( appName: FirebaseAppNames.appCheckNotConfigured, apiConfig: APIConfig(service: .googleAI(endpoint: .firebaseProxyProd), version: .v1beta) ) + static let googleAI_v1beta_appCheckNotConfigured_limitedUseTokens = InstanceConfig( + appName: FirebaseAppNames.appCheckNotConfigured, + useLimitedUseAppCheckTokens: true, + apiConfig: APIConfig(service: .googleAI(endpoint: .firebaseProxyProd), version: .v1beta) + ) static let appCheckNotConfiguredConfigs = [ vertexAI_v1beta_appCheckNotConfigured, + vertexAI_v1beta_appCheckNotConfigured_limitedUseTokens, googleAI_v1beta_appCheckNotConfigured, + googleAI_v1beta_appCheckNotConfigured_limitedUseTokens, ] let appName: String? let location: String? + let useLimitedUseAppCheckTokens: Bool let apiConfig: APIConfig - init(appName: String? = nil, location: String? = nil, apiConfig: APIConfig) { + init(appName: String? = nil, + location: String? = nil, + useLimitedUseAppCheckTokens: Bool = false, + apiConfig: APIConfig) { self.appName = appName self.location = location + self.useLimitedUseAppCheckTokens = useLimitedUseAppCheckTokens self.apiConfig = apiConfig } @@ -108,8 +137,12 @@ extension InstanceConfig: CustomTestStringConvertible { " - Bypass Proxy" } let locationSuffix = location.map { " - \($0)" } ?? "" + let appCheckLimitedUseDesignator = useLimitedUseAppCheckTokens ? " - FAC Limited-Use" : "" - return "\(serviceName) (\(versionName))\(freeTierDesignator)\(endpointSuffix)\(locationSuffix)" + return """ + \(serviceName) (\(versionName))\(freeTierDesignator)\(endpointSuffix)\(locationSuffix)\ + \(appCheckLimitedUseDesignator) + """ } } @@ -121,7 +154,8 @@ extension FirebaseAI { return FirebaseAI.createInstance( app: instanceConfig.app, location: location, - apiConfig: instanceConfig.apiConfig + apiConfig: instanceConfig.apiConfig, + useLimitedUseAppCheckTokens: instanceConfig.useLimitedUseAppCheckTokens ) case .googleAI: assert( @@ -131,7 +165,8 @@ extension FirebaseAI { return FirebaseAI.createInstance( app: instanceConfig.app, location: nil, - apiConfig: instanceConfig.apiConfig + apiConfig: instanceConfig.apiConfig, + useLimitedUseAppCheckTokens: instanceConfig.useLimitedUseAppCheckTokens ) } } diff --git a/FirebaseAI/Tests/Unit/ChatTests.swift b/FirebaseAI/Tests/Unit/ChatTests.swift index 40373a47494..7ecebf42e28 100644 --- a/FirebaseAI/Tests/Unit/ChatTests.swift +++ b/FirebaseAI/Tests/Unit/ChatTests.swift @@ -68,7 +68,8 @@ final class ChatTests: XCTestCase { projectID: "my-project-id", apiKey: "API_KEY", firebaseAppID: "My app ID", - firebaseApp: app + firebaseApp: app, + useLimitedUseAppCheckTokens: false ), apiConfig: FirebaseAI.defaultVertexAIAPIConfig, tools: nil, @@ -94,4 +95,104 @@ final class ChatTests: XCTestCase { XCTAssertEqual(chat.history[1], assembledExpectation) #endif // os(watchOS) } + + func testSendMessage_unary_appendsHistory() async throws { + let expectedInput = "Test input" + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json", + subdirectory: "mock-responses/googleai" + ) + let model = GenerativeModel( + modelName: modelName, + modelResourceName: modelResourceName, + firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(), + apiConfig: FirebaseAI.defaultVertexAIAPIConfig, + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + let chat = model.startChat() + + // Pre-condition: History should be empty. + XCTAssertTrue(chat.history.isEmpty) + + let response = try await chat.sendMessage(expectedInput) + + XCTAssertNotNil(response.text) + let text = try XCTUnwrap(response.text) + XCTAssertFalse(text.isEmpty) + + // Post-condition: History should have the user's message and the model's response. + XCTAssertEqual(chat.history.count, 2) + let userInput = try XCTUnwrap(chat.history.first) + XCTAssertEqual(userInput.role, "user") + XCTAssertEqual(userInput.parts.count, 1) + let userInputText = try XCTUnwrap(userInput.parts.first as? TextPart) + XCTAssertEqual(userInputText.text, expectedInput) + + let modelResponse = try XCTUnwrap(chat.history.last) + XCTAssertEqual(modelResponse.role, "model") + XCTAssertEqual(modelResponse.parts.count, 1) + let modelResponseText = try XCTUnwrap(modelResponse.parts.first as? TextPart) + XCTAssertFalse(modelResponseText.text.isEmpty) + } + + func testSendMessageStream_error_doesNotAppendHistory() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "streaming-failure-finish-reason-safety", + withExtension: "txt", + subdirectory: "mock-responses/vertexai" + ) + let model = GenerativeModel( + modelName: modelName, + modelResourceName: modelResourceName, + firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(), + apiConfig: FirebaseAI.defaultVertexAIAPIConfig, + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + let chat = model.startChat() + let input = "Test input" + + // Pre-condition: History should be empty. + XCTAssertTrue(chat.history.isEmpty) + + do { + let stream = try chat.sendMessageStream(input) + for try await _ in stream { + // Consume the stream. + } + XCTFail("Should have thrown a responseStoppedEarly error.") + } catch let GenerateContentError.responseStoppedEarly(reason, _) { + XCTAssertEqual(reason, .safety) + } catch { + XCTFail("Unexpected error thrown: \(error)") + } + + // Post-condition: History should still be empty. + XCTAssertEqual(chat.history.count, 0) + } + + func testStartChat_withHistory_initializesCorrectly() async throws { + let history = [ + ModelContent(role: "user", parts: "Question 1"), + ModelContent(role: "model", parts: "Answer 1"), + ] + let model = GenerativeModel( + modelName: modelName, + modelResourceName: modelResourceName, + firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(), + apiConfig: FirebaseAI.defaultVertexAIAPIConfig, + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + + let chat = model.startChat(history: history) + + XCTAssertEqual(chat.history.count, 2) + XCTAssertEqual(chat.history, history) + } } diff --git a/FirebaseAI/Tests/Unit/Fakes/AppCheckInteropFake.swift b/FirebaseAI/Tests/Unit/Fakes/AppCheckInteropFake.swift index 62fc753ae68..81b6ac1bb7c 100644 --- a/FirebaseAI/Tests/Unit/Fakes/AppCheckInteropFake.swift +++ b/FirebaseAI/Tests/Unit/Fakes/AppCheckInteropFake.swift @@ -40,6 +40,10 @@ class AppCheckInteropFake: NSObject, AppCheckInterop { return AppCheckTokenResultInteropFake(token: token, error: error) } + func getLimitedUseToken() async -> any FIRAppCheckTokenResultInterop { + return AppCheckTokenResultInteropFake(token: "limited_use_\(token)", error: error) + } + func tokenDidChangeNotificationName() -> String { fatalError("\(#function) not implemented.") } @@ -52,9 +56,10 @@ class AppCheckInteropFake: NSObject, AppCheckInterop { fatalError("\(#function) not implemented.") } - private class AppCheckTokenResultInteropFake: NSObject, FIRAppCheckTokenResultInterop { - var token: String - var error: Error? + private class AppCheckTokenResultInteropFake: NSObject, FIRAppCheckTokenResultInterop, + @unchecked Sendable { + let token: String + let error: Error? init(token: String, error: Error?) { self.token = token diff --git a/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift b/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift index 103943e6f92..b1ee49da6a1 100644 --- a/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift +++ b/FirebaseAI/Tests/Unit/GenerativeModelGoogleAITests.swift @@ -262,6 +262,52 @@ final class GenerativeModelGoogleAITests: XCTestCase { ) } + func testGenerateContent_success_thinking_thoughtSummary() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-thinking-reply-thought-summary", + withExtension: "json", + subdirectory: googleAISubdirectory + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 2) + let thoughtPart = try XCTUnwrap(candidate.content.parts.first as? TextPart) + XCTAssertTrue(thoughtPart.isThought) + XCTAssertTrue(thoughtPart.text.hasPrefix("**Thinking About Google's Headquarters**")) + XCTAssertEqual(thoughtPart.text, response.thoughtSummary) + let textPart = try XCTUnwrap(candidate.content.parts.last as? TextPart) + XCTAssertFalse(textPart.isThought) + XCTAssertEqual(textPart.text, "Mountain View") + XCTAssertEqual(textPart.text, response.text) + } + + func testGenerateContent_success_thinking_functionCall_thoughtSummaryAndSignature() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-thinking-function-call-thought-summary-signature", + withExtension: "json", + subdirectory: googleAISubdirectory + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.finishReason, .stop) + XCTAssertEqual(candidate.content.parts.count, 2) + let thoughtPart = try XCTUnwrap(candidate.content.parts.first as? TextPart) + XCTAssertTrue(thoughtPart.isThought) + XCTAssertTrue(thoughtPart.text.hasPrefix("**Thinking Through the New Year's Eve Calculation**")) + let functionCallPart = try XCTUnwrap(candidate.content.parts.last as? FunctionCallPart) + XCTAssertFalse(functionCallPart.isThought) + XCTAssertEqual(functionCallPart.name, "now") + XCTAssertTrue(functionCallPart.args.isEmpty) + let thoughtSignature = try XCTUnwrap(functionCallPart.thoughtSignature) + XCTAssertTrue(thoughtSignature.hasPrefix("CtQOAVSoXO74PmYr9AFu")) + } + func testGenerateContent_failure_invalidAPIKey() async throws { let expectedStatusCode = 400 MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( @@ -397,6 +443,89 @@ final class GenerativeModelGoogleAITests: XCTestCase { XCTAssertNil(citation.publicationDate) } + func testGenerateContentStream_successWithThoughtSummary() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "streaming-success-thinking-reply-thought-summary", + withExtension: "txt", + subdirectory: googleAISubdirectory + ) + + var thoughtSummary = "" + var text = "" + let stream = try model.generateContentStream("Hi") + for try await response in stream { + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let textPart = try XCTUnwrap(candidate.content.parts.first as? TextPart) + if textPart.isThought { + let newThought = try XCTUnwrap(response.thoughtSummary) + XCTAssertEqual(textPart.text, newThought) + thoughtSummary.append(newThought) + } else { + let newText = try XCTUnwrap(response.text) + XCTAssertEqual(textPart.text, newText) + text.append(newText) + } + } + + XCTAssertTrue(thoughtSummary.hasPrefix("**Exploring Sky Color**")) + XCTAssertTrue(text.hasPrefix("The sky is blue because")) + } + + func testGenerateContentStream_success_thinking_functionCall_thoughtSummary_signature() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "streaming-success-thinking-function-call-thought-summary-signature", + withExtension: "txt", + subdirectory: googleAISubdirectory + ) + + var thoughtSummary = "" + var functionCalls: [FunctionCallPart] = [] + let stream = try model.generateContentStream("Hi") + for try await response in stream { + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + if part.isThought { + let textPart = try XCTUnwrap(part as? TextPart) + let newThought = try XCTUnwrap(response.thoughtSummary) + XCTAssertEqual(textPart.text, newThought) + thoughtSummary.append(newThought) + } else { + let functionCallPart = try XCTUnwrap(part as? FunctionCallPart) + XCTAssertEqual(response.functionCalls.count, 1) + let newFunctionCall = try XCTUnwrap(response.functionCalls.first) + XCTAssertEqual(functionCallPart, newFunctionCall) + functionCalls.append(newFunctionCall) + } + } + + XCTAssertTrue(thoughtSummary.hasPrefix("**Calculating the Days**")) + XCTAssertEqual(functionCalls.count, 1) + let functionCall = try XCTUnwrap(functionCalls.first) + XCTAssertEqual(functionCall.name, "now") + XCTAssertTrue(functionCall.args.isEmpty) + let thoughtSignature = try XCTUnwrap(functionCall.thoughtSignature) + XCTAssertTrue(thoughtSignature.hasPrefix("CiIBVKhc7vB+vaaq6rA")) + } + + func testGenerateContentStream_success_ignoresEmptyParts() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "streaming-success-empty-parts", + withExtension: "txt", + subdirectory: googleAISubdirectory + ) + + let stream = try model.generateContentStream("Hi") + for try await response in stream { + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertGreaterThan(candidate.content.parts.count, 0) + let text = response.text + let inlineData = response.inlineDataParts.first + XCTAssertTrue(text != nil || inlineData != nil, "Response did not contain text or data") + } + } + func testGenerateContentStream_failureInvalidAPIKey() async throws { MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( forResource: "unary-failure-api-key", diff --git a/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift b/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift index 6557735ccc4..2b7a60ec0a8 100644 --- a/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift +++ b/FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift @@ -434,6 +434,29 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTAssertEqual(text, "The sum of [1, 2, 3] is") } + func testGenerateContent_success_thinking_thoughtSummary() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-thinking-reply-thought-summary", + withExtension: "json", + subdirectory: vertexSubdirectory + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.finishReason, .stop) + XCTAssertEqual(candidate.content.parts.count, 2) + let thoughtPart = try XCTUnwrap(candidate.content.parts.first as? TextPart) + XCTAssertTrue(thoughtPart.isThought) + XCTAssertTrue(thoughtPart.text.hasPrefix("Right, someone needs the city where Google")) + XCTAssertEqual(response.thoughtSummary, thoughtPart.text) + let textPart = try XCTUnwrap(candidate.content.parts.last as? TextPart) + XCTAssertFalse(textPart.isThought) + XCTAssertEqual(textPart.text, "Mountain View") + XCTAssertEqual(response.text, textPart.text) + } + func testGenerateContent_success_image_invalidSafetyRatingsIgnored() async throws { MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( forResource: "unary-success-image-invalid-safety-ratings", @@ -454,6 +477,27 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTAssertGreaterThan(imagePart.data.count, 0) } + func testGenerateContent_success_image_emptyPartIgnored() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-empty-part", + withExtension: "json", + subdirectory: vertexSubdirectory + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 2) + let inlineDataParts = response.inlineDataParts + XCTAssertEqual(inlineDataParts.count, 1) + let imagePart = try XCTUnwrap(inlineDataParts.first) + XCTAssertEqual(imagePart.mimeType, "image/png") + XCTAssertGreaterThan(imagePart.data.count, 0) + let text = try XCTUnwrap(response.text) + XCTAssertTrue(text.starts(with: "I can certainly help you with that")) + } + func testGenerateContent_appCheck_validToken() async throws { let appCheckToken = "test-valid-token" model = GenerativeModel( @@ -478,6 +522,31 @@ final class GenerativeModelVertexAITests: XCTestCase { _ = try await model.generateContent(testPrompt) } + func testGenerateContent_appCheck_validToken_limitedUse() async throws { + let appCheckToken = "test-valid-token" + model = GenerativeModel( + modelName: testModelName, + modelResourceName: testModelResourceName, + firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo( + appCheck: AppCheckInteropFake(token: appCheckToken), + useLimitedUseAppCheckTokens: true + ), + apiConfig: apiConfig, + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + MockURLProtocol + .requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json", + subdirectory: vertexSubdirectory, + appCheckToken: "limited_use_\(appCheckToken)" + ) + + _ = try await model.generateContent(testPrompt) + } + func testGenerateContent_dataCollectionOff() async throws { let appCheckToken = "test-valid-token" model = GenerativeModel( @@ -738,12 +807,12 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTFail("Should throw GenerateContentError.internalError; no error thrown.") } catch let GenerateContentError .internalError(underlying: invalidCandidateError as InvalidCandidateError) { - guard case let .emptyContent(decodingError) = invalidCandidateError else { - XCTFail("Not an InvalidCandidateError.emptyContent error: \(invalidCandidateError)") + guard case let .emptyContent(underlyingError) = invalidCandidateError else { + XCTFail("Should be an InvalidCandidateError.emptyContent error: \(invalidCandidateError)") return } - _ = try XCTUnwrap(decodingError as? DecodingError, - "Not a DecodingError: \(decodingError)") + _ = try XCTUnwrap(underlyingError as? Candidate.EmptyContentError, + "Should be an empty content error: \(underlyingError)") } catch { XCTFail("Should throw GenerateContentError.internalError; error thrown: \(error)") } @@ -928,7 +997,7 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTAssertNotNil(responseError) let generateContentError = try XCTUnwrap(responseError as? GenerateContentError) guard case let .internalError(underlyingError) = generateContentError else { - XCTFail("Not an internal error: \(generateContentError)") + XCTFail("Should be an internal error: \(generateContentError)") return } XCTAssertEqual(underlyingError.localizedDescription, "Response was not an HTTP response.") @@ -956,12 +1025,12 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTAssertNotNil(responseError) let generateContentError = try XCTUnwrap(responseError as? GenerateContentError) guard case let .internalError(underlyingError) = generateContentError else { - XCTFail("Not an internal error: \(generateContentError)") + XCTFail("Should be an internal error: \(generateContentError)") return } let decodingError = try XCTUnwrap(underlyingError as? DecodingError) guard case let .dataCorrupted(context) = decodingError else { - XCTFail("Not a data corrupted error: \(decodingError)") + XCTFail("Should be a data corrupted error: \(decodingError)") return } XCTAssert(context.debugDescription.hasPrefix("Failed to decode GenerateContentResponse")) @@ -990,17 +1059,17 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTAssertNotNil(responseError) let generateContentError = try XCTUnwrap(responseError as? GenerateContentError) guard case let .internalError(underlyingError) = generateContentError else { - XCTFail("Not an internal error: \(generateContentError)") + XCTFail("Should be an internal error: \(generateContentError)") return } let invalidCandidateError = try XCTUnwrap(underlyingError as? InvalidCandidateError) guard case let .emptyContent(emptyContentUnderlyingError) = invalidCandidateError else { - XCTFail("Not an empty content error: \(invalidCandidateError)") + XCTFail("Should be an empty content error: \(invalidCandidateError)") return } _ = try XCTUnwrap( - emptyContentUnderlyingError as? DecodingError, - "Not a decoding error: \(emptyContentUnderlyingError)" + emptyContentUnderlyingError as? Candidate.EmptyContentError, + "Should be an empty content error: \(emptyContentUnderlyingError)" ) } @@ -1330,6 +1399,33 @@ final class GenerativeModelVertexAITests: XCTestCase { XCTAssertFalse(citations.contains { $0.license?.isEmpty ?? false }) } + func testGenerateContentStream_successWithThinking_thoughtSummary() async throws { + MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( + forResource: "streaming-success-thinking-reply-thought-summary", + withExtension: "txt", + subdirectory: vertexSubdirectory + ) + + var thoughtSummary = "" + var text = "" + let stream = try model.generateContentStream("Hi") + for try await response in stream { + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + let textPart = try XCTUnwrap(part as? TextPart) + if textPart.isThought { + let newThought = try XCTUnwrap(response.thoughtSummary) + thoughtSummary.append(newThought) + } else { + text.append(textPart.text) + } + } + + XCTAssertTrue(thoughtSummary.hasPrefix("**Understanding the Core Question**")) + XCTAssertTrue(text.hasPrefix("The sky is blue due to a phenomenon")) + } + func testGenerateContentStream_successWithInvalidSafetyRatingsIgnored() async throws { MockURLProtocol.requestHandler = try GenerativeModelTestUtil.httpRequestHandler( forResource: "streaming-success-image-invalid-safety-ratings", @@ -1488,7 +1584,7 @@ final class GenerativeModelVertexAITests: XCTestCase { } } catch let GenerateContentError.internalError(underlying as DecodingError) { guard case let .dataCorrupted(context) = underlying else { - XCTFail("Not a data corrupted error: \(underlying)") + XCTFail("Should be a data corrupted error: \(underlying)") return } XCTAssert(context.debugDescription.hasPrefix("Failed to decode GenerateContentResponse")) @@ -1516,11 +1612,11 @@ final class GenerativeModelVertexAITests: XCTestCase { } } catch let GenerateContentError.internalError(underlyingError as InvalidCandidateError) { guard case let .emptyContent(contentError) = underlyingError else { - XCTFail("Not an empty content error: \(underlyingError)") + XCTFail("Should be an empty content error: \(underlyingError)") return } - XCTAssert(contentError is DecodingError) + XCTAssert(contentError is Candidate.EmptyContentError) return } diff --git a/FirebaseAI/Tests/Unit/JSONValueTests.swift b/FirebaseAI/Tests/Unit/JSONValueTests.swift index 1ffe88eaf55..54ac3520e77 100644 --- a/FirebaseAI/Tests/Unit/JSONValueTests.swift +++ b/FirebaseAI/Tests/Unit/JSONValueTests.swift @@ -97,6 +97,48 @@ final class JSONValueTests: XCTestCase { XCTAssertEqual(json, "null") } + func testDecodeNestedObject() throws { + let nestedObject: JSONObject = [ + "nestedKey": .string("nestedValue"), + ] + let expectedObject: JSONObject = [ + "numberKey": .number(numberValue), + "objectKey": .object(nestedObject), + ] + let json = """ + { + "numberKey": \(numberValue), + "objectKey": { + "nestedKey": "nestedValue" + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + + func testDecodeNestedArray() throws { + let nestedArray: [JSONValue] = [.string("a"), .string("b")] + let expectedObject: JSONObject = [ + "numberKey": .number(numberValue), + "arrayKey": .array(nestedArray), + ] + let json = """ + { + "numberKey": \(numberValue), + "arrayKey": ["a", "b"] + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + func testEncodeNumber() throws { let jsonData = try encoder.encode(JSONValue.number(numberValue)) @@ -143,4 +185,30 @@ final class JSONValueTests: XCTestCase { let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) XCTAssertEqual(json, "[null,\(numberValueEncoded)]") } + + func testEncodeNestedObject() throws { + let nestedObject: JSONObject = [ + "nestedKey": .string("nestedValue"), + ] + let objectValue: JSONObject = [ + "numberKey": .number(numberValue), + "objectKey": .object(nestedObject), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + XCTAssertEqual(jsonObject, .object(objectValue)) + } + + func testEncodeNestedArray() throws { + let nestedArray: [JSONValue] = [.string("a"), .string("b")] + let objectValue: JSONObject = [ + "numberKey": .number(numberValue), + "arrayKey": .array(nestedArray), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + XCTAssertEqual(jsonObject, .object(objectValue)) + } } diff --git a/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift b/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift index e7531d1da9e..658db79a50e 100644 --- a/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift +++ b/FirebaseAI/Tests/Unit/PartsRepresentableTests.swift @@ -121,4 +121,55 @@ final class PartsRepresentableTests: XCTestCase { } } #endif + + func testMixedParts() throws { + let text = "This is a test" + let data = try XCTUnwrap("This is some data".data(using: .utf8)) + let inlineData = InlineDataPart(data: data, mimeType: "text/plain") + + let parts: [any PartsRepresentable] = [text, inlineData] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let dataPart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(dataPart, inlineData) + } + + #if canImport(UIKit) + func testMixedParts_withImage() throws { + let text = "This is a test" + let image = try XCTUnwrap(UIImage(systemName: "star")) + let parts: [any PartsRepresentable] = [text, image] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(imagePart.mimeType, "image/jpeg") + XCTAssertFalse(imagePart.data.isEmpty) + } + + #elseif canImport(AppKit) + func testMixedParts_withImage() throws { + let text = "This is a test" + let coreImage = CIImage(color: CIColor.blue) + .cropped(to: CGRect(origin: CGPoint.zero, size: CGSize(width: 16, height: 16))) + let rep = NSCIImageRep(ciImage: coreImage) + let image = NSImage(size: rep.size) + image.addRepresentation(rep) + + let parts: [any PartsRepresentable] = [text, image] + let modelContent = ModelContent(parts: parts) + + XCTAssertEqual(modelContent.parts.count, 2) + let textPart = try XCTUnwrap(modelContent.parts[0] as? TextPart) + XCTAssertEqual(textPart.text, text) + let imagePart = try XCTUnwrap(modelContent.parts[1] as? InlineDataPart) + XCTAssertEqual(imagePart.mimeType, "image/jpeg") + XCTAssertFalse(imagePart.data.isEmpty) + } + #endif } diff --git a/FirebaseAI/Tests/Unit/README.md b/FirebaseAI/Tests/Unit/README.md index 9463d595294..88019041f9f 100644 --- a/FirebaseAI/Tests/Unit/README.md +++ b/FirebaseAI/Tests/Unit/README.md @@ -1,3 +1,3 @@ See the Firebase AI SDK -[README](https://github.com/firebase/firebase-ios-sdk/tree/main/FirebaseVertexAI#unit-tests) +[README](https://github.com/firebase/firebase-ios-sdk/tree/main/FirebaseAI#unit-tests) for required setup instructions. diff --git a/FirebaseAI/Tests/Unit/SafetyTests.swift b/FirebaseAI/Tests/Unit/SafetyTests.swift new file mode 100644 index 00000000000..4a1e07e04e3 --- /dev/null +++ b/FirebaseAI/Tests/Unit/SafetyTests.swift @@ -0,0 +1,123 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest + +@testable import FirebaseAI + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +final class SafetyTests: XCTestCase { + let decoder = JSONDecoder() + let encoder = JSONEncoder() + + override func setUp() { + encoder.outputFormatting = .init( + arrayLiteral: .prettyPrinted, .sortedKeys, .withoutEscapingSlashes + ) + } + + // MARK: - SafetyRating Decoding + + func testDecodeSafetyRating_allFieldsPresent() throws { + let json = """ + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.1, + "severity": "HARM_SEVERITY_LOW", + "severityScore": 0.2, + "blocked": true + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + let rating = try decoder.decode(SafetyRating.self, from: jsonData) + + XCTAssertEqual(rating.category, .dangerousContent) + XCTAssertEqual(rating.probability, .negligible) + XCTAssertEqual(rating.probabilityScore, 0.1) + XCTAssertEqual(rating.severity, .low) + XCTAssertEqual(rating.severityScore, 0.2) + XCTAssertTrue(rating.blocked) + } + + func testDecodeSafetyRating_missingOptionalFields() throws { + let json = """ + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "LOW" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + let rating = try decoder.decode(SafetyRating.self, from: jsonData) + + XCTAssertEqual(rating.category, .harassment) + XCTAssertEqual(rating.probability, .low) + XCTAssertEqual(rating.probabilityScore, 0.0) + XCTAssertEqual(rating.severity, .unspecified) + XCTAssertEqual(rating.severityScore, 0.0) + XCTAssertFalse(rating.blocked) + } + + func testDecodeSafetyRating_unknownEnums() throws { + let json = """ + { + "category": "HARM_CATEGORY_UNKNOWN", + "probability": "UNKNOWN_PROBABILITY", + "severity": "UNKNOWN_SEVERITY" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + let rating = try decoder.decode(SafetyRating.self, from: jsonData) + + XCTAssertEqual(rating.category.rawValue, "HARM_CATEGORY_UNKNOWN") + XCTAssertEqual(rating.probability.rawValue, "UNKNOWN_PROBABILITY") + XCTAssertEqual(rating.severity.rawValue, "UNKNOWN_SEVERITY") + } + + // MARK: - SafetySetting Encoding + + func testEncodeSafetySetting_allFields() throws { + let setting = SafetySetting( + harmCategory: .hateSpeech, + threshold: .blockMediumAndAbove, + method: .severity + ) + let jsonData = try encoder.encode(setting) + let jsonString = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + + XCTAssertEqual(jsonString, """ + { + "category" : "HARM_CATEGORY_HATE_SPEECH", + "method" : "SEVERITY", + "threshold" : "BLOCK_MEDIUM_AND_ABOVE" + } + """) + } + + func testEncodeSafetySetting_nilMethod() throws { + let setting = SafetySetting( + harmCategory: .sexuallyExplicit, + threshold: .blockOnlyHigh + ) + let jsonData = try encoder.encode(setting) + let jsonString = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + + XCTAssertEqual(jsonString, """ + { + "category" : "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold" : "BLOCK_ONLY_HIGH" + } + """) + } +} diff --git a/FirebaseAI/Tests/Unit/TestUtilities/GenerativeModelTestUtil.swift b/FirebaseAI/Tests/Unit/TestUtilities/GenerativeModelTestUtil.swift index 9b00f0b0c87..ee4f47bc5b0 100644 --- a/FirebaseAI/Tests/Unit/TestUtilities/GenerativeModelTestUtil.swift +++ b/FirebaseAI/Tests/Unit/TestUtilities/GenerativeModelTestUtil.swift @@ -103,7 +103,8 @@ enum GenerativeModelTestUtil { static func testFirebaseInfo(appCheck: AppCheckInterop? = nil, auth: AuthInterop? = nil, - privateAppID: Bool = false) -> FirebaseInfo { + privateAppID: Bool = false, + useLimitedUseAppCheckTokens: Bool = false) -> FirebaseInfo { let app = FirebaseApp(instanceWithName: "testApp", options: FirebaseOptions(googleAppID: "ignore", gcmSenderID: "ignore")) @@ -114,7 +115,8 @@ enum GenerativeModelTestUtil { projectID: "my-project-id", apiKey: "API_KEY", firebaseAppID: "My app ID", - firebaseApp: app + firebaseApp: app, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens ) } } diff --git a/FirebaseAI/Tests/Unit/Types/Imagen/ImageGenerationParametersTests.swift b/FirebaseAI/Tests/Unit/Types/Imagen/ImageGenerationParametersTests.swift index 494feda9f7a..a96174f3b7d 100644 --- a/FirebaseAI/Tests/Unit/Types/Imagen/ImageGenerationParametersTests.swift +++ b/FirebaseAI/Tests/Unit/Types/Imagen/ImageGenerationParametersTests.swift @@ -34,7 +34,8 @@ final class ImageGenerationParametersTests: XCTestCase { personGeneration: nil, outputOptions: nil, addWatermark: nil, - includeResponsibleAIFilterReason: true + includeResponsibleAIFilterReason: true, + includeSafetyAttributes: true ) let parameters = ImagenModel.imageGenerationParameters( @@ -57,7 +58,8 @@ final class ImageGenerationParametersTests: XCTestCase { personGeneration: nil, outputOptions: nil, addWatermark: nil, - includeResponsibleAIFilterReason: true + includeResponsibleAIFilterReason: true, + includeSafetyAttributes: true ) let parameters = ImagenModel.imageGenerationParameters( @@ -95,7 +97,8 @@ final class ImageGenerationParametersTests: XCTestCase { compressionQuality: imageFormat.compressionQuality ), addWatermark: addWatermark, - includeResponsibleAIFilterReason: true + includeResponsibleAIFilterReason: true, + includeSafetyAttributes: true ) let parameters = ImagenModel.imageGenerationParameters( @@ -124,7 +127,8 @@ final class ImageGenerationParametersTests: XCTestCase { personGeneration: personFilterLevel.rawValue, outputOptions: nil, addWatermark: nil, - includeResponsibleAIFilterReason: true + includeResponsibleAIFilterReason: true, + includeSafetyAttributes: true ) let parameters = ImagenModel.imageGenerationParameters( @@ -170,7 +174,8 @@ final class ImageGenerationParametersTests: XCTestCase { compressionQuality: imageFormat.compressionQuality ), addWatermark: addWatermark, - includeResponsibleAIFilterReason: true + includeResponsibleAIFilterReason: true, + includeSafetyAttributes: true ) let parameters = ImagenModel.imageGenerationParameters( @@ -200,6 +205,7 @@ final class ImageGenerationParametersTests: XCTestCase { let outputOptions = ImageGenerationOutputOptions(mimeType: mimeType, compressionQuality: nil) let addWatermark = false let includeRAIReason = true + let includeSafetyAttributes = true let parameters = ImageGenerationParameters( sampleCount: sampleCount, storageURI: storageURI, @@ -209,7 +215,8 @@ final class ImageGenerationParametersTests: XCTestCase { personGeneration: personGeneration, outputOptions: outputOptions, addWatermark: addWatermark, - includeResponsibleAIFilterReason: includeRAIReason + includeResponsibleAIFilterReason: includeRAIReason, + includeSafetyAttributes: includeSafetyAttributes ) let jsonData = try encoder.encode(parameters) @@ -220,6 +227,7 @@ final class ImageGenerationParametersTests: XCTestCase { "addWatermark" : \(addWatermark), "aspectRatio" : "\(aspectRatio)", "includeRaiReason" : \(includeRAIReason), + "includeSafetyAttributes" : \(includeSafetyAttributes), "negativePrompt" : "\(negativePrompt)", "outputOptions" : { "mimeType" : "\(mimeType)" @@ -246,7 +254,8 @@ final class ImageGenerationParametersTests: XCTestCase { personGeneration: nil, outputOptions: nil, addWatermark: addWatermark, - includeResponsibleAIFilterReason: nil + includeResponsibleAIFilterReason: nil, + includeSafetyAttributes: nil ) let jsonData = try encoder.encode(parameters) @@ -272,7 +281,8 @@ final class ImageGenerationParametersTests: XCTestCase { personGeneration: nil, outputOptions: nil, addWatermark: nil, - includeResponsibleAIFilterReason: nil + includeResponsibleAIFilterReason: nil, + includeSafetyAttributes: nil ) let jsonData = try encoder.encode(parameters) diff --git a/FirebaseAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift b/FirebaseAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift index eb8b3df83ca..9a48ed7c8a2 100644 --- a/FirebaseAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift +++ b/FirebaseAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift @@ -25,6 +25,7 @@ final class ImagenGenerationRequestTests: XCTestCase { let aspectRatio = "16:9" let safetyFilterLevel = "block_low_and_above" let includeResponsibleAIFilterReason = true + let includeSafetyAttributes = true lazy var parameters = ImageGenerationParameters( sampleCount: sampleCount, storageURI: nil, @@ -34,7 +35,8 @@ final class ImagenGenerationRequestTests: XCTestCase { personGeneration: nil, outputOptions: nil, addWatermark: nil, - includeResponsibleAIFilterReason: includeResponsibleAIFilterReason + includeResponsibleAIFilterReason: includeResponsibleAIFilterReason, + includeSafetyAttributes: includeSafetyAttributes ) let apiConfig = FirebaseAI.defaultVertexAIAPIConfig @@ -108,6 +110,7 @@ final class ImagenGenerationRequestTests: XCTestCase { "parameters" : { "aspectRatio" : "\(aspectRatio)", "includeRaiReason" : \(includeResponsibleAIFilterReason), + "includeSafetyAttributes" : \(includeSafetyAttributes), "safetySetting" : "\(safetyFilterLevel)", "sampleCount" : \(sampleCount) } @@ -137,6 +140,7 @@ final class ImagenGenerationRequestTests: XCTestCase { "parameters" : { "aspectRatio" : "\(aspectRatio)", "includeRaiReason" : \(includeResponsibleAIFilterReason), + "includeSafetyAttributes" : \(includeSafetyAttributes), "safetySetting" : "\(safetyFilterLevel)", "sampleCount" : \(sampleCount) } diff --git a/FirebaseAI/Tests/Unit/Types/InternalPartTests.swift b/FirebaseAI/Tests/Unit/Types/InternalPartTests.swift new file mode 100644 index 00000000000..2cd5c5fee2a --- /dev/null +++ b/FirebaseAI/Tests/Unit/Types/InternalPartTests.swift @@ -0,0 +1,286 @@ +// 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. + +@testable import FirebaseAI +import XCTest + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +final class InternalPartTests: XCTestCase { + let decoder = JSONDecoder() + + func testDecodeTextPartWithThought() throws { + let json = """ + { + "text": "This is a thought.", + "thought": true + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertEqual(part.isThought, true) + guard case let .text(text) = part.data else { + XCTFail("Decoded part is not a text part.") + return + } + XCTAssertEqual(text, "This is a thought.") + } + + func testDecodeTextPartWithoutThought() throws { + let json = """ + { + "text": "This is not a thought." + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertNil(part.isThought) + guard case let .text(text) = part.data else { + XCTFail("Decoded part is not a text part.") + return + } + XCTAssertEqual(text, "This is not a thought.") + } + + func testDecodeInlineDataPartWithThought() throws { + let imageBase64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==" + let mimeType = "image/png" + let json = """ + { + "inlineData": { + "mimeType": "\(mimeType)", + "data": "\(imageBase64)" + }, + "thought": true + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertEqual(part.isThought, true) + guard case let .inlineData(inlineData) = part.data else { + XCTFail("Decoded part is not an inlineData part.") + return + } + XCTAssertEqual(inlineData.mimeType, mimeType) + XCTAssertEqual(inlineData.data, Data(base64Encoded: imageBase64)) + } + + func testDecodeInlineDataPartWithoutThought() throws { + let imageBase64 = "aGVsbG8=" + let mimeType = "image/png" + let json = """ + { + "inlineData": { + "mimeType": "\(mimeType)", + "data": "\(imageBase64)" + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertNil(part.isThought) + guard case let .inlineData(inlineData) = part.data else { + XCTFail("Decoded part is not an inlineData part.") + return + } + XCTAssertEqual(inlineData.mimeType, mimeType) + XCTAssertEqual(inlineData.data, Data(base64Encoded: imageBase64)) + } + + func testDecodeFileDataPartWithThought() throws { + let uri = "file:///path/to/file.mp3" + let mimeType = "audio/mpeg" + let json = """ + { + "fileData": { + "fileUri": "\(uri)", + "mimeType": "\(mimeType)" + }, + "thought": true + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertEqual(part.isThought, true) + guard case let .fileData(fileData) = part.data else { + XCTFail("Decoded part is not a fileData part.") + return + } + XCTAssertEqual(fileData.fileURI, uri) + XCTAssertEqual(fileData.mimeType, mimeType) + } + + func testDecodeFileDataPartWithoutThought() throws { + let uri = "file:///path/to/file.mp3" + let mimeType = "audio/mpeg" + let json = """ + { + "fileData": { + "fileUri": "\(uri)", + "mimeType": "\(mimeType)" + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertNil(part.isThought) + guard case let .fileData(fileData) = part.data else { + XCTFail("Decoded part is not a fileData part.") + return + } + XCTAssertEqual(fileData.fileURI, uri) + XCTAssertEqual(fileData.mimeType, mimeType) + } + + func testDecodeFunctionCallPartWithThoughtSignature() throws { + let functionName = "someFunction" + let expectedThoughtSignature = "some_signature" + let json = """ + { + "functionCall": { + "name": "\(functionName)", + "args": { + "arg1": "value1" + }, + }, + "thoughtSignature": "\(expectedThoughtSignature)" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + let thoughtSignature = try XCTUnwrap(part.thoughtSignature) + XCTAssertEqual(thoughtSignature, expectedThoughtSignature) + XCTAssertNil(part.isThought) + guard case let .functionCall(functionCall) = part.data else { + XCTFail("Decoded part is not a functionCall part.") + return + } + XCTAssertEqual(functionCall.name, functionName) + XCTAssertEqual(functionCall.args, ["arg1": .string("value1")]) + } + + func testDecodeFunctionCallPartWithoutThoughtSignature() throws { + let functionName = "someFunction" + let json = """ + { + "functionCall": { + "name": "\(functionName)", + "args": { + "arg1": "value1" + } + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertNil(part.isThought) + XCTAssertNil(part.thoughtSignature) + guard case let .functionCall(functionCall) = part.data else { + XCTFail("Decoded part is not a functionCall part.") + return + } + XCTAssertEqual(functionCall.name, functionName) + XCTAssertEqual(functionCall.args, ["arg1": .string("value1")]) + } + + func testDecodeFunctionCallPartWithoutArgs() throws { + let functionName = "someFunction" + let json = """ + { + "functionCall": { + "name": "\(functionName)" + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertNil(part.isThought) + XCTAssertNil(part.thoughtSignature) + guard case let .functionCall(functionCall) = part.data else { + XCTFail("Decoded part is not a functionCall part.") + return + } + XCTAssertEqual(functionCall.name, functionName) + XCTAssertEqual(functionCall.args, JSONObject()) + } + + func testDecodeFunctionResponsePartWithThought() throws { + let functionName = "someFunction" + let json = """ + { + "functionResponse": { + "name": "\(functionName)", + "response": { + "output": "someValue" + } + }, + "thought": true + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertEqual(part.isThought, true) + guard case let .functionResponse(functionResponse) = part.data else { + XCTFail("Decoded part is not a functionResponse part.") + return + } + XCTAssertEqual(functionResponse.name, functionName) + XCTAssertEqual(functionResponse.response, ["output": .string("someValue")]) + } + + func testDecodeFunctionResponsePartWithoutThought() throws { + let functionName = "someFunction" + let json = """ + { + "functionResponse": { + "name": "\(functionName)", + "response": { + "output": "someValue" + } + } + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let part = try decoder.decode(InternalPart.self, from: jsonData) + + XCTAssertNil(part.isThought) + guard case let .functionResponse(functionResponse) = part.data else { + XCTFail("Decoded part is not a functionResponse part.") + return + } + XCTAssertEqual(functionResponse.name, functionName) + XCTAssertEqual(functionResponse.response, ["output": .string("someValue")]) + } +} diff --git a/FirebaseAI/Tests/Unit/VertexComponentTests.swift b/FirebaseAI/Tests/Unit/VertexComponentTests.swift index 7202e01f4d6..702c6e50871 100644 --- a/FirebaseAI/Tests/Unit/VertexComponentTests.swift +++ b/FirebaseAI/Tests/Unit/VertexComponentTests.swift @@ -155,12 +155,14 @@ class VertexComponentTests: XCTestCase { let vertex1 = FirebaseAI.createInstance( app: VertexComponentTests.app, location: location, - apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1beta) + apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1beta), + useLimitedUseAppCheckTokens: false ) let vertex2 = FirebaseAI.createInstance( app: VertexComponentTests.app, location: location, - apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1) + apiConfig: APIConfig(service: .vertexAI(endpoint: .firebaseProxyProd), version: .v1), + useLimitedUseAppCheckTokens: false ) // Ensure they are different instances. @@ -181,7 +183,8 @@ class VertexComponentTests: XCTestCase { let vertex = FirebaseAI( app: app1, location: "transitory location", - apiConfig: FirebaseAI.defaultVertexAIAPIConfig + apiConfig: FirebaseAI.defaultVertexAIAPIConfig, + useLimitedUseAppCheckTokens: false ) weakVertex = vertex XCTAssertNotNil(weakVertex) @@ -208,7 +211,12 @@ class VertexComponentTests: XCTestCase { func testModelResourceName_developerAPI_generativeLanguage() throws { let app = try XCTUnwrap(VertexComponentTests.app) let apiConfig = APIConfig(service: .googleAI(endpoint: .googleAIBypassProxy), version: .v1beta) - let vertex = FirebaseAI.createInstance(app: app, location: nil, apiConfig: apiConfig) + let vertex = FirebaseAI.createInstance( + app: app, + location: nil, + apiConfig: apiConfig, + useLimitedUseAppCheckTokens: false + ) let model = "test-model-name" let modelResourceName = vertex.modelResourceName(modelName: model) @@ -222,7 +230,12 @@ class VertexComponentTests: XCTestCase { service: .googleAI(endpoint: .firebaseProxyStaging), version: .v1beta ) - let vertex = FirebaseAI.createInstance(app: app, location: nil, apiConfig: apiConfig) + let vertex = FirebaseAI.createInstance( + app: app, + location: nil, + apiConfig: apiConfig, + useLimitedUseAppCheckTokens: false + ) let model = "test-model-name" let projectID = vertex.firebaseInfo.projectID @@ -253,7 +266,12 @@ class VertexComponentTests: XCTestCase { service: .googleAI(endpoint: .firebaseProxyStaging), version: .v1beta ) - let vertex = FirebaseAI.createInstance(app: app, location: nil, apiConfig: apiConfig) + let vertex = FirebaseAI.createInstance( + app: app, + location: nil, + apiConfig: apiConfig, + useLimitedUseAppCheckTokens: false + ) let modelResourceName = vertex.modelResourceName(modelName: modelName) let expectedSystemInstruction = ModelContent(role: nil, parts: systemInstruction.parts) diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index d748235aea5..bba40f73c84 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalytics' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Analytics for iOS' s.description = <<-DESC @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/c3a136153af090b8/FirebaseAnalytics-12.0.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/20f7f19c421351ed/FirebaseAnalytics-12.2.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' @@ -26,8 +26,8 @@ Pod::Spec.new do |s| s.libraries = 'c++', 'sqlite3', 'z' s.frameworks = 'StoreKit' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseInstallations', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.1' s.dependency 'GoogleUtilities/MethodSwizzler', '~> 8.1' s.dependency 'GoogleUtilities/NSData+zlib', '~> 8.1' @@ -37,17 +37,17 @@ Pod::Spec.new do |s| s.default_subspecs = 'Default' s.subspec 'Default' do |ss| - ss.dependency 'GoogleAppMeasurement/Default', '12.0.0' + ss.dependency 'GoogleAppMeasurement/Default', '12.3.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end s.subspec 'Core' do |ss| - ss.dependency 'GoogleAppMeasurement/Core', '12.0.0' + ss.dependency 'GoogleAppMeasurement/Core', '12.3.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end s.subspec 'IdentitySupport' do |ss| - ss.dependency 'GoogleAppMeasurement/IdentitySupport', '12.0.0' + ss.dependency 'GoogleAppMeasurement/IdentitySupport', '12.3.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 51b36c2bedd..90d7c768ef9 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheck' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase App Check SDK.' s.description = <<-DESC @@ -45,8 +45,8 @@ Pod::Spec.new do |s| s.tvos.weak_framework = 'DeviceCheck' s.dependency 'AppCheckCore', '~> 11.0' - s.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' - s.dependency 'FirebaseCore', '~> 12.0.0' + s.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' + s.dependency 'FirebaseCore', '~> 12.3.0' s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' diff --git a/FirebaseAppCheck/Interop/Public/FirebaseAppCheckInterop/FIRAppCheckTokenResultInterop.h b/FirebaseAppCheck/Interop/Public/FirebaseAppCheckInterop/FIRAppCheckTokenResultInterop.h index cb86a1bffe8..f87820b9790 100644 --- a/FirebaseAppCheck/Interop/Public/FirebaseAppCheckInterop/FIRAppCheckTokenResultInterop.h +++ b/FirebaseAppCheck/Interop/Public/FirebaseAppCheckInterop/FIRAppCheckTokenResultInterop.h @@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN +NS_SWIFT_SENDABLE @protocol FIRAppCheckTokenResultInterop /// App Check token in the case of success or a dummy token in the case of a failure. diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h index 37b57b2adef..9cf6ddadec1 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckTokenResult.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN +NS_SWIFT_SENDABLE @interface FIRAppCheckTokenResult : NSObject - (instancetype)initWithToken:(NSString *)token error:(nullable NSError *)error; diff --git a/FirebaseAppCheckInterop.podspec b/FirebaseAppCheckInterop.podspec index 427530ab4fc..e1869c3ba33 100644 --- a/FirebaseAppCheckInterop.podspec +++ b/FirebaseAppCheckInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheckInterop' - s.version = '12.0.0' + s.version = '12.3.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 13abf86d57d..32c29999e8a 100644 --- a/FirebaseAppDistribution.podspec +++ b/FirebaseAppDistribution.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppDistribution' - s.version = '12.0.0-beta' + s.version = '12.3.0-beta' s.summary = 'App Distribution for Firebase iOS SDK.' s.description = <<-DESC @@ -30,10 +30,10 @@ iOS SDK for App Distribution for Firebase. ] s.public_header_files = base_dir + 'Public/FirebaseAppDistribution/*.h' - s.dependency 'FirebaseCore', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.1' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' - s.dependency 'FirebaseInstallations', '~> 12.0.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index 1f16ebcad86..5e47dc61b87 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuth' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Apple platform client for Firebase Authentication' s.description = <<-DESC @@ -55,10 +55,10 @@ supports email and password accounts, as well as several 3rd party authenticatio } s.framework = 'Security' s.ios.framework = 'SafariServices' - s.dependency 'FirebaseAuthInterop', '~> 12.0.0' - s.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseCoreExtension', '~> 12.0.0' + s.dependency 'FirebaseAuthInterop', '~> 12.3.0' + s.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseCoreExtension', '~> 12.3.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.1' s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GTMSessionFetcher/Core', '>= 3.4', '< 6.0' diff --git a/FirebaseAuth/CHANGELOG.md b/FirebaseAuth/CHANGELOG.md index 143e9dfedc6..2b725886b45 100644 --- a/FirebaseAuth/CHANGELOG.md +++ b/FirebaseAuth/CHANGELOG.md @@ -1,3 +1,10 @@ +# 12.2.0 +- [added] Added TOTP support for macOS. + +# 12.1.0 +- [fixed] Fix a formatting issue with generated TOTP URLs that prevented them + from working with the Google Authenticator app. (#15128) + # 12.0.0 - [removed] **Breaking Change**: Removed the following Dynamic Links related APIs: diff --git a/FirebaseAuth/README.md b/FirebaseAuth/README.md index 1dbb6f9cbc3..b055658e4c9 100644 --- a/FirebaseAuth/README.md +++ b/FirebaseAuth/README.md @@ -1,4 +1,4 @@ -# Firebase Auth for iOS +# Firebase Auth for iOS and macOS Firebase Auth enables apps to easily support multiple authentication options for their end users. diff --git a/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m b/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m index 4862a485884..85648f2fc3b 100644 --- a/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m +++ b/FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_OSX #import diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h index aae0db8ab6f..da05c8309ce 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h @@ -5,7 +5,7 @@ * 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/LICENSE2.0 + * 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, @@ -22,27 +22,26 @@ NS_ASSUME_NONNULL_BEGIN /** @typedef FIRMultiFactorSessionCallback @brief The callback that triggered when a developer calls `getSessionWithCompletion`. - This type is available on iOS only. + This type is available on iOS and macOS. @param session The multi factor session returned, if any. @param error The error which occurred, if any. */ typedef void (^FIRMultiFactorSessionCallback)(FIRMultiFactorSession *_Nullable session, NSError *_Nullable error) - NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.") - API_UNAVAILABLE(macos, tvos, watchos); + NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.") API_UNAVAILABLE(tvos, watchos); /** @brief The string identifier for using phone as a second factor. - This constant is available on iOS only. + This constant is available on iOS and macOS. */ extern NSString *const _Nonnull FIRPhoneMultiFactorID NS_SWIFT_NAME(PhoneMultiFactorID) - API_UNAVAILABLE(macos, tvos, watchos); + API_UNAVAILABLE(tvos, watchos); /** @brief The string identifier for using TOTP as a second factor. - This constant is available on iOS only. + This constant is available on iOS and macOS. */ extern NSString *const _Nonnull FIRTOTPMultiFactorID NS_SWIFT_NAME(TOTPMultiFactorID) - API_UNAVAILABLE(macos, tvos, watchos); + API_UNAVAILABLE(tvos, watchos); NS_ASSUME_NONNULL_END diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index d198f5418f5..fcb4bc33636 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -2341,6 +2341,29 @@ extension Auth: AuthInterop { } #endif + // MARK: IDP Initiated SAML Sign In + + public func signInWithSamlIdp(ProviderId providerId: String, + SpAcsUrl spAcsUrl: String, + SamlResp samlResp: String) async throws -> AuthDataResult { + let samlRespBody = "SAMLResponse=\(samlResp)&providerId=\(providerId)" + let request = SignInWithSamlIdpRequest( + requestUri: spAcsUrl, + postBody: samlRespBody, + returnSecureToken: true, + requestConfiguration: requestConfiguration + ) + let response = try await backend.call(with: request) + let user = try await completeSignIn( + withAccessToken: response.idToken, + accessTokenExpirationDate: response.expirationDate, + refreshToken: response.refreshToken, + anonymous: false + ) + try await updateCurrentUser(user) + return AuthDataResult(withUser: user, additionalUserInfo: nil) + } + // MARK: Internal properties /// Allow tests to swap in an alternate mainBundle, including ObjC unit tests via CocoaPods. diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift index 7a0c39340ae..55780c1ca89 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift @@ -99,9 +99,7 @@ final class AuthBackend: AuthBackendProtocol { } private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? { - #if !os(iOS) - return nil - #else + #if os(iOS) || os(macOS) if let mfaResponse = response as? AuthMFAResponse, mfaResponse.idToken == nil, let enrollments = mfaResponse.mfaInfo { @@ -124,7 +122,9 @@ final class AuthBackend: AuthBackendProtocol { } else { return nil } - #endif // !os(iOS) + #else + return nil + #endif // os(iOS) || os(macOS) } // Check whether or not the successful response is actually the special case phone diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpRequest.swift new file mode 100644 index 00000000000..c85044cbc2b --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpRequest.swift @@ -0,0 +1,55 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +final class SignInWithSamlIdpRequest: AuthRPCRequest { + typealias Response = SignInWithSamlIdpResponse + private let config: AuthRequestConfiguration + private let requestUri: String + private let postBody: String + private let returnSecureToken: Bool + + init(requestUri: String, + postBody: String, + returnSecureToken: Bool, + requestConfiguration: AuthRequestConfiguration) { + self.requestUri = requestUri + self.postBody = postBody + self.returnSecureToken = returnSecureToken + config = requestConfiguration + } + + func requestConfiguration() -> AuthRequestConfiguration { + return config + } + + func requestURL() -> URL { + var comps = URLComponents() + comps.scheme = "https" + comps.host = "identitytoolkit.googleapis.com" + comps.path = "/v1/accounts:signInWithIdp" + comps.queryItems = [URLQueryItem(name: "key", value: config.apiKey)] + return comps.url! + } + + var unencodedHTTPRequestBody: [String: AnyHashable]? { + let body: [String: AnyHashable] = [ + "requestUri": requestUri, + "postBody": postBody, + "returnSecureToken": returnSecureToken, + ] + return body + } +} diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpResponse.swift new file mode 100644 index 00000000000..72ca328ba21 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpResponse.swift @@ -0,0 +1,46 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +struct SignInWithSamlIdpResponse: AuthRPCResponse { + /// The user raw access token. + let idToken: String + /// Refresh token for the authenticated user. + let refreshToken: String + /// The provider Identifier + let providerId: String + /// The email id of user + let email: String + /// The calculated date and time when the token expires. + let expirationDate: Date + + init(dictionary: [String: AnyHashable]) throws { + guard + let email = dictionary["email"] as? String, + let expiration = dictionary["expiresIn"] as? String, + let idToken = dictionary["idToken"] as? String, + let providerId = dictionary["providerId"] as? String, + let refreshToken = dictionary["refreshToken"] as? String + else { + throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary) + } + self.idToken = idToken + self.refreshToken = refreshToken + self.providerId = providerId + self.email = email + let expiresInSec = TimeInterval(expiration) + expirationDate = Date().addingTimeInterval(expiresInSec ?? 3600) + } +} diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift index 17ec6b18731..e227117c981 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift @@ -14,7 +14,7 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) extension MultiFactor: NSSecureCoding {} @@ -22,7 +22,7 @@ import Foundation /// The interface defining the multi factor related properties and operations pertaining to a /// user. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactor) open class MultiFactor: NSObject { @objc open var enrolledFactors: [MultiFactorInfo] diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift index ff3edc94dd0..edfad7aaa58 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift @@ -14,12 +14,12 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The base class for asserting ownership of a second factor. This is equivalent to the /// AuthCredential class. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRMultiFactorAssertion) open class MultiFactorAssertion: NSObject { /// The second factor identifier for this opaque object asserting a second factor. @objc open var factorID: String diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift index 839e405fc05..a1b134a7265 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift @@ -16,12 +16,12 @@ import Foundation // TODO(Swift 6 Breaking): Make checked Sendable. -#if os(iOS) +#if os(iOS) || os(macOS) extension MultiFactorInfo: NSSecureCoding {} /// Safe public structure used to represent a second factor entity from a client perspective. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRMultiFactorInfo) open class MultiFactorInfo: NSObject, @unchecked Sendable { /// The multi-factor enrollment ID. @objc(UID) public let uid: String diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift index 223c1f9f5f5..374f86de88c 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift @@ -14,12 +14,12 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The subclass of base class `MultiFactorAssertion`, used to assert ownership of a phone /// second factor. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactorResolver) open class MultiFactorResolver: NSObject { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift index 33f7ef927ce..d22578a291a 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift @@ -14,7 +14,7 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// Opaque object that identifies the current session to enroll a second factor or to /// complete sign in when previously enrolled. @@ -23,7 +23,7 @@ import Foundation /// 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. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRMultiFactorSession) open class MultiFactorSession: NSObject { /// The ID token for an enroll flow. This has to be retrieved after recent authentication. diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift index 999809e4bd3..941b0c244f4 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift @@ -14,12 +14,12 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone /// second factor. /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @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 cd213c14196..74c50abfb96 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift @@ -14,14 +14,14 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// 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. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRPhoneMultiFactorGenerator) open class PhoneMultiFactorGenerator: NSObject { diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift index b847407c48a..4ec36505dac 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift @@ -16,13 +16,13 @@ import Foundation // TODO(Swift 6 Breaking): Make checked Sendable. -#if os(iOS) +#if os(iOS) || os(macOS) /// Extends the MultiFactorInfo class for phone number second factors. /// /// The identifier of this second factor is "phone". /// - /// This class is available on iOS only. + /// This class is available on iOS and macOS. @objc(FIRPhoneMultiFactorInfo) open class PhoneMultiFactorInfo: MultiFactorInfo, @unchecked Sendable { /// The string identifier for using phone as a second factor. diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift index b5b1c43f3d6..dd64e4319af 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift @@ -14,7 +14,7 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) enum SecretOrID { case secret(TOTPSecret) @@ -24,7 +24,7 @@ import Foundation /// 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. + /// This class is available on iOS and macOS. @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 bf3b07634ca..c24dedb3b22 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift @@ -14,13 +14,13 @@ import Foundation -#if os(iOS) +#if os(iOS) || os(macOS) /// 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. + /// This class is available on iOS and macOS. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @objc(FIRTOTPMultiFactorGenerator) open class TOTPMultiFactorGenerator: NSObject { /// Creates a TOTP secret as part of enrolling a TOTP second factor. Used for generating a diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift index dbb2eeb7042..b252f4fd8c6 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift @@ -17,13 +17,13 @@ import Foundation // TODO(Swift 6 Breaking): Make checked Sendable. Also, does this need // to be public? -#if os(iOS) +#if os(iOS) || os(macOS) /// 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. + /// This class is available on iOS and macOS. class TOTPMultiFactorInfo: MultiFactorInfo, @unchecked Sendable { /// Initialize the AuthProtoMFAEnrollment instance with proto. /// - Parameter proto: AuthProtoMFAEnrollment proto object. diff --git a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift index 9791b974e52..1f4b3f9e125 100644 --- a/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift +++ b/FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift @@ -19,13 +19,17 @@ import Foundation internal import GoogleUtilities_Environment #endif -#if os(iOS) - import UIKit +#if os(iOS) || os(macOS) + #if os(iOS) + import UIKit + #elseif os(macOS) + import AppKit + #endif /// 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. + /// This class is available on iOS and macOS. @objc(FIRTOTPSecret) open class TOTPSecret: NSObject { /// Returns the shared secret key/seed used to generate time-based one-time passwords. @objc open func sharedSecretKey() -> String { @@ -47,7 +51,7 @@ import Foundation return "" } return "otpauth://totp/\(issuer):\(accountName)?secret=\(secretKey)&issuer=\(issuer)" + - "&algorithm=%\(hashingAlgorithm)&digits=\(codeLength)" + "&algorithm=\(hashingAlgorithm)&digits=\(codeLength)" } /// Opens the specified QR Code URL in a password manager like iCloud Keychain. @@ -57,24 +61,33 @@ import Foundation @MainActor @objc(openInOTPAppWithQRCodeURL:) open func openInOTPApp(withQRCodeURL qrCodeURL: String) { if GULAppEnvironmentUtil.isAppExtension() { - // iOS App extensions should not call [UIApplication sharedApplication], even if - // UIApplication responds to it. + // App extensions should not call [UIApplication sharedApplication] or [NSWorkspace + // sharedWorkspace], even if they respond to it. return } - // Using reflection here to avoid build errors in extensions. - let sel = NSSelectorFromString("sharedApplication") - guard UIApplication.responds(to: sel), - let rawApplication = UIApplication.perform(sel), - let application = rawApplication.takeUnretainedValue() as? UIApplication else { - return - } - if let url = URL(string: qrCodeURL), application.canOpenURL(url) { - application.open(url, options: [:], completionHandler: nil) - } else { - AuthLog.logError(code: "I-AUT000019", - message: "URL: \(qrCodeURL) cannot be opened") - } + #if os(iOS) + // Using reflection here to avoid build errors in extensions. + let sel = NSSelectorFromString("sharedApplication") + guard UIApplication.responds(to: sel), + let rawApplication = UIApplication.perform(sel), + let application = rawApplication.takeUnretainedValue() as? UIApplication else { + return + } + if let url = URL(string: qrCodeURL), application.canOpenURL(url) { + application.open(url, options: [:], completionHandler: nil) + } else { + AuthLog.logError(code: "I-AUT000019", + message: "URL: \(qrCodeURL) cannot be opened") + } + #elseif os(macOS) + if let url = URL(string: qrCodeURL) { + NSWorkspace.shared.open(url) + } else { + AuthLog.logError(code: "I-AUT000019", + message: "URL: \(qrCodeURL) cannot be opened") + } + #endif } /// Shared secret key/seed used for enrolling in TOTP MFA and generating OTPs. diff --git a/FirebaseAuth/Sources/Swift/User/User.swift b/FirebaseAuth/Sources/Swift/User/User.swift index 4ef324e177c..ea006baa418 100644 --- a/FirebaseAuth/Sources/Swift/User/User.swift +++ b/FirebaseAuth/Sources/Swift/User/User.swift @@ -58,10 +58,10 @@ extension User: NSSecureCoding {} /// The tenant ID of the current user. `nil` if none is available. @objc public private(set) var tenantID: String? - #if os(iOS) + #if os(iOS) || os(macOS) /// Multi factor object associated with the user. /// - /// This property is available on iOS only. + /// This property is available on iOS and macOS. @objc public private(set) var multiFactor: MultiFactor #endif @@ -1066,7 +1066,7 @@ extension User: NSSecureCoding {} isEmailVerified = false metadata = UserMetadata(withCreationDate: nil, lastSignInDate: nil) tenantID = nil - #if os(iOS) + #if os(iOS) || os(macOS) multiFactor = MultiFactor(withMFAEnrollments: []) #endif uid = "" @@ -1297,7 +1297,7 @@ extension User: NSSecureCoding {} } } providerDataRaw = providerData - #if os(iOS) + #if os(iOS) || os(macOS) if let enrollments = user.mfaEnrollments { multiFactor = MultiFactor(withMFAEnrollments: enrollments) } @@ -1718,7 +1718,7 @@ extension User: NSSecureCoding {} coder.encode(auth.requestConfiguration.appID, forKey: kFirebaseAppIDCodingKey) } coder.encode(tokenService, forKey: kTokenServiceCodingKey) - #if os(iOS) + #if os(iOS) || os(macOS) coder.encode(multiFactor, forKey: kMultiFactorCodingKey) #endif } @@ -1747,7 +1747,7 @@ extension User: NSSecureCoding {} as? [String: UserInfoImpl] let metadata = coder.decodeObject(of: UserMetadata.self, forKey: kMetadataCodingKey) let tenantID = coder.decodeObject(of: NSString.self, forKey: kTenantIDCodingKey) as? String - #if os(iOS) + #if os(iOS) || os(macOS) let multiFactor = coder.decodeObject(of: MultiFactor.self, forKey: kMultiFactorCodingKey) #endif self.tokenService = tokenService @@ -1778,7 +1778,7 @@ extension User: NSSecureCoding {} backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer()) userProfileUpdate = UserProfileUpdate() - #if os(iOS) + #if os(iOS) || os(macOS) self.multiFactor = multiFactor ?? MultiFactor() super.init() multiFactor?.user = self diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift index 5c78b223ab4..17d76c810a7 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift @@ -568,7 +568,7 @@ class AuthErrorUtils { return error(code: .blockingCloudFunctionError, message: errorMessage) } - #if os(iOS) + #if os(iOS) || os(macOS) static func secondFactorRequiredError(pendingCredential: String?, hints: [MultiFactorInfo], auth: Auth) @@ -581,7 +581,7 @@ class AuthErrorUtils { return error(code: .secondFactorRequired, userInfo: userInfo) } - #endif // os(iOS) + #endif // os(iOS) || os(macOS) static func recaptchaSDKNotLinkedError() -> Error { // TODO(ObjC): point the link to GCIP doc once available. diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift index e7965767a8a..ed5a0cea2d3 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift @@ -50,10 +50,64 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Implementing this delegate method is needed when swizzling is disabled. // Without it, reCAPTCHA's login view controller will not dismiss. + // Without it, IdP Initiated SAML Sign In will not work. func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { for urlContext in URLContexts { let url = urlContext.url _ = Auth.auth().canHandle(url) + /// Handle IdP Initiated SAML deep link myapp://saml?resp= + if url.scheme?.lowercased() == "myapp", /// replace with your custom scheme + url.host?.lowercased() == "saml" { /// replace with your host + let spAcsUrl = + "https://iostemp-8a944.web.app/googleidp-saml/acs" /// replace with your SP ACS URL + if let rawQuery = url.query { + var respValue: String? + for pair in rawQuery.split(separator: "&", omittingEmptySubsequences: false) { + let parts = pair.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + if parts.count == 2, parts[0] == "resp" { + respValue = String(parts[1]) + break + } + } + if let resp = respValue { + let alert = UIAlertController( + title: "SAML Sign In", + message: "Enter Provider ID", + preferredStyle: .alert + ) + alert.addTextField { tf in + tf.placeholder = "Provider ID" + tf.text = "saml.provider" + tf.autocapitalizationType = .none + tf.autocorrectionType = .no + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in + let providerId = alert.textFields?.first?.text? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let requestUri = alert.textFields?.last?.text? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !providerId.isEmpty, !requestUri.isEmpty else { return } + Task { + do { + _ = try await AppManager.shared.auth().signInWithSamlIdp( + ProviderId: providerId, + SpAcsUrl: requestUri, + SamlResp: resp + ) + } catch { + print("IdP-initiated SAML sign-in failed with error:", error) + } + } + }) + var top = window?.rootViewController + while let presented = top?.presentedViewController { + top = presented + } + top?.present(alert, animated: true) + } + } + } } // URL not auth related; it should be handled separately. diff --git a/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift new file mode 100644 index 00000000000..5e8dc2c127c --- /dev/null +++ b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift @@ -0,0 +1,81 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if os(iOS) + + @testable import FirebaseAuth + import XCTest + + @available(iOS 15.0, macOS 12.0, tvOS 16.0, *) + class SignInWithSamlIdpTests: TestsBase { + func testSignInWithSamlFailureInvalidProvider() async throws { + try? await deleteCurrentUserAsync() + let invalidProvider = "saml.invalid" + let spAcsUrl = "https://example.com/saml-acs" + let samlResp = "samlResp" + do { + _ = try await Auth.auth().signInWithSamlIdp( + ProviderId: invalidProvider, + SpAcsUrl: spAcsUrl, + SamlResp: samlResp + ) + XCTFail("Expected failure for invalid provider ID") + } catch { + let ns = error as NSError + if let code = AuthErrorCode(rawValue: ns.code) { + XCTAssert([.operationNotAllowed].contains(code), + "Unexpected code: \(code)") + } else { + XCTFail("Unexpected error: \(error)") + } + let desc = (ns.userInfo[NSLocalizedDescriptionKey] as? String ?? "").uppercased() + XCTAssert( + desc.contains("THE IDENTITY PROVIDER CONFIGURATION IS NOT FOUND."), + "Expected backend invalid provider message, got: \(desc)" + ) + } + XCTAssertNil(Auth.auth().currentUser) + } + + func testSignInWithSamlFailureInvalidResponse() async throws { + try? await deleteCurrentUserAsync() + let providerId = "saml.googleidp" + let spAcsUrl = "https://example.com/saml-acs" + let invalidSamlResp = "invalid%25" + do { + _ = try await Auth.auth().signInWithSamlIdp( + ProviderId: providerId, + SpAcsUrl: spAcsUrl, + SamlResp: invalidSamlResp + ) + XCTFail("Expected failure for invalid SAMLResponse") + } catch { + let ns = error as NSError + if let code = AuthErrorCode(rawValue: ns.code) { + XCTAssert([.invalidCredential, .internalError].contains(code), + "Unexpected code: \(code)") + } else { + XCTFail("Unexpected error: \(error)") + } + let desc = (ns.userInfo[NSLocalizedDescriptionKey] as? String ?? "").uppercased() + XCTAssert( + desc.contains("UNABLE TO PARSE THE SAML TOKEN."), + "Expected backend invalid credential message, got: \(desc)" + ) + } + XCTAssertNil(Auth.auth().currentUser) + } + } + +#endif diff --git a/FirebaseAuth/Tests/Unit/AuthTests.swift b/FirebaseAuth/Tests/Unit/AuthTests.swift index 5ae1d522108..8e8fc2474f4 100644 --- a/FirebaseAuth/Tests/Unit/AuthTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthTests.swift @@ -2287,6 +2287,152 @@ class AuthTests: RPCBaseTests { } #endif + // MARK: SAML IdP sign-in + + #if os(iOS) + + static let kSamlProviderId = "saml.idp" + static let kSamlAcsUrl = "https://example.com/saml-acs-url" + static let kSamlResponse = "BASE64_SAML_ASSERTION" + static let kBadSamlResponse = "MALFORMED_OR_TAMPERED_SAML" + + func testSignInWithSamlIdpSuccess() throws { + let expectation = self.expectation(description: #function) + setFakeGetAccountProvider() + setFakeSecureTokenService() + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + XCTAssertEqual( + req.unencodedHTTPRequestBody?["requestUri"] as? String, + AuthTests.kSamlAcsUrl + ) + XCTAssertTrue( + (req.unencodedHTTPRequestBody?["postBody"] as? String)?.contains( + AuthTests.kSamlProviderId + ) ?? false + ) + XCTAssertTrue(req.unencodedHTTPRequestBody?["returnSecureToken"] as? Bool ?? false) + return try self.rpcIssuer.respond(withJSON: [ + "idToken": RPCBaseTests.kFakeAccessToken, + "refreshToken": self.kRefreshToken, + "email": self.kEmail, + "providerId": AuthTests.kSamlProviderId, + "expiresIn": "3600", + ]) + } + try auth.signOut() + Task { + do { + let result = try await self.auth.signInWithSamlIdp( + ProviderId: AuthTests.kSamlProviderId, + SpAcsUrl: AuthTests.kSamlAcsUrl, + SamlResp: AuthTests.kSamlResponse + ) + XCTAssertEqual(result.user.email, self.kEmail) + XCTAssertEqual(result.user.refreshToken, self.kRefreshToken) + XCTAssertFalse(result.user.isAnonymous) + expectation.fulfill() + } catch { + XCTFail("Unexpected error: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + func testSignInWithSamlIdpWithIncorrectUrl() throws { + let expectation = self.expectation(description: #function) + let kBadSamlAcsUrl = "https://example.com/saml-acs-incorrect-url" + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + let body = try XCTUnwrap(req.unencodedHTTPRequestBody) + XCTAssertEqual(body["requestUri"] as? String, kBadSamlAcsUrl) + return try self.rpcIssuer.respond(serverErrorMessage: "OPERATION_NOT_ALLOWED") + } + try auth.signOut() + Task { + do { + _ = try await self.auth.signInWithSamlIdp( + ProviderId: AuthTests.kSamlProviderId, + SpAcsUrl: kBadSamlAcsUrl, + SamlResp: AuthTests.kSamlResponse + ) + XCTFail("Expected OPERATION_NOT_ALLOWED") + } catch { + let ns = error as NSError + XCTAssertEqual(ns.code, AuthErrorCode.operationNotAllowed.rawValue) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + XCTAssertNil(auth.currentUser) + } + + func testSignInWithSamlIdpFailureInvalidProviderId() throws { + let expectation = self.expectation(description: #function) + let badProvider = "saml.non-existent-idp" + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + let body = try XCTUnwrap(req.unencodedHTTPRequestBody) + let postBody = try XCTUnwrap(body["postBody"] as? String) + XCTAssertTrue(postBody.contains("providerId=\(badProvider)")) + return try self.rpcIssuer.respond(serverErrorMessage: "OPERATION_NOT_ALLOWED") + } + try auth.signOut() + Task { + do { + _ = try await self.auth.signInWithSamlIdp( + ProviderId: badProvider, // wrong providerId + SpAcsUrl: AuthTests.kSamlAcsUrl, + SamlResp: AuthTests.kSamlResponse + ) + XCTFail("Expected OPERATION_NOT_ALLOWED") + } catch { + let ns = error as NSError + XCTAssertEqual(ns.code, AuthErrorCode.operationNotAllowed.rawValue) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + XCTAssertNil(auth.currentUser) + } + + func testSignInWithSamlIdpFailureInvalidPostBody() throws { + let expectation = self.expectation(description: #function) + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + let body = try XCTUnwrap(req.unencodedHTTPRequestBody) + XCTAssertEqual(body["requestUri"] as? String, AuthTests.kSamlAcsUrl) + let postBody = try XCTUnwrap(body["postBody"] as? String) + XCTAssertTrue(postBody.contains("SAMLResponse=\(AuthTests.kBadSamlResponse)")) + XCTAssertTrue(postBody.contains("providerId=\(AuthTests.kSamlProviderId)")) + XCTAssertTrue(body["returnSecureToken"] as? Bool ?? false) + return try self.rpcIssuer + .respond(underlyingErrorMessage: "INVALID_CREDENTIAL_OR_PROVIDER_ID") + } + try auth.signOut() + Task { + do { + _ = try await self.auth.signInWithSamlIdp( + ProviderId: AuthTests.kSamlProviderId, + SpAcsUrl: AuthTests.kSamlAcsUrl, + SamlResp: AuthTests.kBadSamlResponse + ) + XCTFail("Expected internalError but got success") + } catch { + let ns = error as NSError + XCTAssertEqual(ns.code, AuthErrorCode.internalError.rawValue) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + XCTAssertNil(auth.currentUser) + } + #endif + // MARK: Application Delegate tests. #if os(iOS) diff --git a/FirebaseAuth/Tests/Unit/ObjCAPITests.m b/FirebaseAuth/Tests/Unit/ObjCAPITests.m index 6784beb8548..d6a528d8930 100644 --- a/FirebaseAuth/Tests/Unit/ObjCAPITests.m +++ b/FirebaseAuth/Tests/Unit/ObjCAPITests.m @@ -372,7 +372,7 @@ - (void)FIRGoogleAuthProvider_h { accessToken:@"token"]; } -#if TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_OSX - (void)FIRMultiFactor_h:(FIRMultiFactor *)mf mfa:(FIRMultiFactorAssertion *)mfa { [mf getSessionWithCompletion:^(FIRMultiFactorSession *_Nullable credential, NSError *_Nullable error){ @@ -466,7 +466,9 @@ - (void)FIRPhoneAuthProvider_h:(FIRPhoneAuthCredential *)credential { - (void)phoneMultiFactorInfo:(FIRPhoneMultiFactorInfo *)info { __unused NSString *s = [info phoneNumber]; } +#endif +#if TARGET_OS_IOS || TARGET_OS_OSX - (void)FIRTOTPSecret_h:(FIRTOTPSecret *)secret { NSString *s = [secret sharedSecretKey]; s = [secret generateQRCodeURLWithAccountName:@"name" issuer:@"issuer"]; @@ -571,7 +573,7 @@ - (void)userProperties:(FIRUser *)user { b = [user isEmailVerified]; __unused NSArray *> *userInfo = [user providerData]; __unused FIRUserMetadata *meta = [user metadata]; -#if TARGET_OS_IOS +#if TARGET_OS_IOS || TARGET_OS_OSX __unused FIRMultiFactor *mf = [user multiFactor]; #endif NSString *s = [user refreshToken]; diff --git a/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m b/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m index 59fe0718b5e..62ab96666c5 100644 --- a/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m +++ b/FirebaseAuth/Tests/Unit/ObjCGlobalTests.m @@ -41,9 +41,11 @@ - (void)GlobalSymbolBuildTest { s = FIRGoogleAuthSignInMethod; #if TARGET_OS_IOS s = FIRPhoneMultiFactorID; - s = FIRTOTPMultiFactorID; s = FIRPhoneAuthProviderID; s = FIRPhoneAuthSignInMethod; +#endif +#if TARGET_OS_IOS || TARGET_OS_OSX + s = FIRTOTPMultiFactorID; #endif s = FIRTwitterAuthProviderID; s = FIRTwitterAuthSignInMethod; diff --git a/FirebaseAuth/Tests/Unit/SignInWithSamlIdpRequestTests.swift b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpRequestTests.swift new file mode 100644 index 00000000000..501dbf3a594 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpRequestTests.swift @@ -0,0 +1,113 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import FirebaseAuth +import FirebaseCore +import XCTest + +final class SignInWithSamlIdpRequestTests: XCTestCase { + let kAPIKey = "TEST_API_KEY" + let kAppID = "FAKE_APP_ID" + let kRequestUri = "https://example.web.app/sp-acs-url" + let kPostBody = "SAMLResponse=BASE64%2BSAFE&providerId=saml.provider" + let kComplexUri = "https://host/acs;param?p1=v1&p2=v2#frag" + let kRawPostBody = + "SAMLResponse=someResponse&providerId=saml.provider" + + var configuration: AuthRequestConfiguration! + + override func setUp() { + super.setUp() + configuration = AuthRequestConfiguration(apiKey: kAPIKey, appID: kAppID) + } + + override func tearDown() { + configuration = nil + super.tearDown() + } + + func testRequestURL() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + + let url = request.requestURL() + XCTAssertEqual(url.scheme, "https") + XCTAssertEqual(url.host, "identitytoolkit.googleapis.com") + XCTAssertEqual(url.path, "/v1/accounts:signInWithIdp") + + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.queryItems?.count, 1) + XCTAssertEqual(components?.queryItems?.first?.name, "key") + XCTAssertEqual(components?.queryItems?.first?.value, kAPIKey) + } + + func testRequestConfigurationPassed() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kPostBody, + returnSecureToken: false, + requestConfiguration: configuration + ) + + let returned = request.requestConfiguration() + XCTAssertEqual(returned.apiKey, kAPIKey) + XCTAssertIdentical(returned.auth, configuration.auth) + } + + func testUnencodedHTTPRequestBody() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + + guard let body = request.unencodedHTTPRequestBody else { + XCTFail("Body must not be nil") + return + } + + XCTAssertEqual(body.count, 3) + XCTAssertEqual(body["requestUri"] as? String, kRequestUri) + XCTAssertEqual(body["postBody"] as? String, kPostBody) + XCTAssertEqual(body["returnSecureToken"] as? Bool, true) + } + + func testUnencodedHTTPRequestPostBody() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kRawPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + + let body = request.unencodedHTTPRequestBody + XCTAssertEqual(body?["postBody"] as? String, kRawPostBody) + } + + func testUnencodedHTTPRequestBody_AllowsComplexRequestUri() throws { + let request = SignInWithSamlIdpRequest( + requestUri: kComplexUri, + postBody: kPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + let body = try XCTUnwrap(request.unencodedHTTPRequestBody) + XCTAssertEqual(body["requestUri"] as? String, kComplexUri) + } +} diff --git a/FirebaseAuth/Tests/Unit/SignInWithSamlIdpResponseTests.swift b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpResponseTests.swift new file mode 100644 index 00000000000..d3c6e2bb6d9 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpResponseTests.swift @@ -0,0 +1,85 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import FirebaseAuth +import XCTest + +final class SignInWithSamlIdpResponseTests: XCTestCase { + private func makeValidDictionary() -> [String: AnyHashable] { + return [ + "email": "user@example.com", + "expiresIn": "3600", + "idToken": "FAKE_ID_TOKEN", + "providerId": "saml.provider", + "refreshToken": "FAKE_REFRESH_TOKEN", + ] + } + + func testInitWithValidDictionaryAllRequiredFields() throws { + var dict = makeValidDictionary() + dict["email"] = "user1@example.com" + dict["idToken"] = "ID.TOKEN" + dict["providerId"] = "saml.myidp" + dict["refreshToken"] = "REFRESH.TOKEN" + let response = try SignInWithSamlIdpResponse(dictionary: dict) + XCTAssertEqual(response.email, "user1@example.com") + XCTAssertEqual(response.idToken, "ID.TOKEN") + XCTAssertEqual(response.providerId, "saml.myidp") + XCTAssertEqual(response.refreshToken, "REFRESH.TOKEN") + } + + func testInitMissingRequiredFields() { + struct Case { let name: String; let keyToRemove: String } + let cases: [Case] = [ + .init(name: "Missing email", keyToRemove: "email"), + .init(name: "Missing expiresIn", keyToRemove: "expiresIn"), + .init(name: "Missing idToken", keyToRemove: "idToken"), + .init(name: "Missing providerId", keyToRemove: "providerId"), + .init(name: "Missing refreshToken", keyToRemove: "refreshToken"), + ] + for c in cases { + var dict = makeValidDictionary() + dict.removeValue(forKey: c.keyToRemove) + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict), c.name) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + } + } + + func testInitIncorrectFieldTypes() { + var dict = makeValidDictionary() + dict["expiresIn"] = 3600 + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict)) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + dict = makeValidDictionary() + dict["idToken"] = 123 + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict)) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + dict = makeValidDictionary() + dict["email"] = NSNull() + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict)) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + } +} diff --git a/FirebaseAuth/Tests/Unit/SwiftAPI.swift b/FirebaseAuth/Tests/Unit/SwiftAPI.swift index db002ccd599..fc6c2f466da 100644 --- a/FirebaseAuth/Tests/Unit/SwiftAPI.swift +++ b/FirebaseAuth/Tests/Unit/SwiftAPI.swift @@ -548,7 +548,9 @@ class AuthAPI_hOnlyTests: XCTestCase { func phoneMultiFactorInfo(mfi: PhoneMultiFactorInfo) { let _: String = mfi.phoneNumber } + #endif + #if os(iOS) || os(macOS) func FIRTOTPSecret_h(session: MultiFactorSession) async throws { let obj = try await TOTPMultiFactorGenerator.generateSecret(with: session) _ = obj.sharedSecretKey() @@ -638,7 +640,7 @@ class AuthAPI_hOnlyTests: XCTestCase { let _: Bool = user.isEmailVerified let _: [UserInfo] = user.providerData let _: UserMetadata = user.metadata - #if os(iOS) + #if os(iOS) || os(macOS) let _: MultiFactor = user.multiFactor #endif if let _: String = user.refreshToken, diff --git a/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift b/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift index f0956d54aa3..8d863cb1a58 100644 --- a/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift +++ b/FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift @@ -38,9 +38,10 @@ class SwiftGlobalTests: XCTestCase { let _: String = GitHubAuthSignInMethod let _: String = GoogleAuthProviderID let _: String = GoogleAuthSignInMethod - #if os(iOS) - let _: String = PhoneMultiFactorID + #if os(iOS) || os(macOS) let _: String = TOTPMultiFactorID + #endif + #if os(iOS) let _: String = PhoneAuthProviderID let _: String = PhoneAuthSignInMethod #endif diff --git a/FirebaseAuth/Tests/Unit/UserTests.swift b/FirebaseAuth/Tests/Unit/UserTests.swift index c610e04a0bc..24f426d1bd8 100644 --- a/FirebaseAuth/Tests/Unit/UserTests.swift +++ b/FirebaseAuth/Tests/Unit/UserTests.swift @@ -135,6 +135,27 @@ class UserTests: RPCBaseTests { ]) #endif + var mfaInfo: [[AnyHashable: AnyHashable]] = [] + + #if os(iOS) + mfaInfo.append([ + "phoneInfo": kPhoneInfo, + "mfaEnrollmentId": kEnrollmentID, + "displayName": kDisplayName, + "enrolledAt": kEnrolledAt, + ]) + #endif + + #if os(iOS) || os(macOS) + mfaInfo.append([ + // In practice, this will be an empty dictionary. + "totpInfo": [AnyHashable: AnyHashable](), + "mfaEnrollmentId": kEnrollmentID, + "displayName": kDisplayName, + "enrolledAt": kEnrolledAt, + ]) + #endif + rpcIssuer?.fakeGetAccountProviderJSON = [[ kProviderUserInfoKey: providerUserInfos, kLocalIDKey: kLocalID, @@ -146,21 +167,7 @@ class UserTests: RPCBaseTests { "phoneNumber": kPhoneNumber, "createdAt": String(Int(kCreationDateTimeIntervalInSeconds) * 1000), // to nanoseconds "lastLoginAt": String(Int(kLastSignInDateTimeIntervalInSeconds) * 1000), - "mfaInfo": [ - [ - "phoneInfo": kPhoneInfo, - "mfaEnrollmentId": kEnrollmentID, - "displayName": kDisplayName, - "enrolledAt": kEnrolledAt, - ], - [ - // In practice, this will be an empty dictionary. - "totpInfo": [AnyHashable: AnyHashable](), - "mfaEnrollmentId": kEnrollmentID, - "displayName": kDisplayName, - "enrolledAt": kEnrolledAt, - ] as [AnyHashable: AnyHashable], - ], + "mfaInfo": mfaInfo, ]] let expectation = self.expectation(description: #function) @@ -247,9 +254,12 @@ class UserTests: RPCBaseTests { var encodedClasses = [User.self, NSDictionary.self, NSURL.self, SecureTokenService.self, UserInfoImpl.self, NSDate.self, UserMetadata.self, NSString.self, NSArray.self] - #if os(iOS) + #if os(iOS) || os(macOS) encodedClasses.append(MultiFactor.self) - encodedClasses.append(PhoneMultiFactorInfo.self) + encodedClasses.append(TOTPMultiFactorInfo.self) + #if os(iOS) + encodedClasses.append(PhoneMultiFactorInfo.self) + #endif #endif let unarchivedUser = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject( @@ -370,6 +380,17 @@ class UserTests: RPCBaseTests { XCTAssertEqual("\(date)", kEnrolledAtMatch) } #endif + + #if os(macOS) + // Verify TOTP MultiFactorInfo properties. + let enrolledFactors = try XCTUnwrap(user.multiFactor.enrolledFactors) + XCTAssertEqual(enrolledFactors.count, 1) + XCTAssertEqual(enrolledFactors[0].factorID, PhoneMultiFactorInfo.TOTPMultiFactorID) + XCTAssertEqual(enrolledFactors[0].uid, kEnrollmentID) + XCTAssertEqual(enrolledFactors[0].displayName, self.kDisplayName) + let date = try XCTUnwrap(enrolledFactors[0].enrollmentDate) + XCTAssertEqual("\(date)", kEnrolledAtMatch) + #endif } catch { XCTFail("Caught an error in \(#function): \(error)") } diff --git a/FirebaseAuthInterop.podspec b/FirebaseAuthInterop.podspec index c1f9df6ed18..305fb25050d 100644 --- a/FirebaseAuthInterop.podspec +++ b/FirebaseAuthInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuthInterop' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Auth functionality.' s.description = <<-DESC diff --git a/FirebaseCombineSwift.podspec b/FirebaseCombineSwift.podspec index ff78436f4d0..c427399b7c6 100644 --- a/FirebaseCombineSwift.podspec +++ b/FirebaseCombineSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCombineSwift' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Swift extensions with Combine support for Firebase' s.description = <<-DESC @@ -51,11 +51,11 @@ for internal testing only. It should not be published. s.osx.framework = 'AppKit' s.tvos.framework = 'UIKit' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseAuth', '~> 12.0.0' - s.dependency 'FirebaseFunctions', '~> 12.0.0' - s.dependency 'FirebaseFirestore', '~> 12.0.0' - s.dependency 'FirebaseStorage', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseAuth', '~> 12.3.0' + s.dependency 'FirebaseFunctions', '~> 12.3.0' + s.dependency 'FirebaseFirestore', '~> 12.3.0' + s.dependency 'FirebaseStorage', '~> 12.3.0' s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"', @@ -104,6 +104,6 @@ for internal testing only. It should not be published. int_tests.resources = 'FirebaseStorage/Tests/Integration/Resources/1mb.dat', 'FirebaseStorage/Tests/Integration/Resources/GoogleService-Info.plist', 'FirebaseStorage/Tests/Integration/Resources/HomeImprovement.numbers' - int_tests.dependency 'FirebaseAuth', '~> 12.0.0' + int_tests.dependency 'FirebaseAuth', '~> 12.3.0' end end diff --git a/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift b/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift index 4fd850da1e6..b7f8a790dff 100644 --- a/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if os(iOS) || targetEnvironment(macCatalyst) +#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS) import Combine import FirebaseAuth - @available(iOS 13.0, macCatalyst 13.0, *) - @available(macOS, unavailable) + @available(iOS 13.0, macCatalyst 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) public extension MultiFactor { @@ -111,4 +110,4 @@ } } -#endif // os(iOS) || targetEnvironment(macCatalyst) +#endif // os(iOS) || targetEnvironment(macCatalyst) || os(macOS) diff --git a/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift b/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift index b2ada743c66..a94e3b7cd82 100644 --- a/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if os(iOS) || targetEnvironment(macCatalyst) +#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS) import Combine import FirebaseAuth - @available(iOS 13.0, macCatalyst 13.0, *) - @available(macOS, unavailable) + @available(iOS 13.0, macCatalyst 13.0, macOS 10.15, *) @available(tvOS, unavailable) @available(watchOS, unavailable) public extension MultiFactorResolver { @@ -43,4 +42,4 @@ } } -#endif // os(iOS) || targetEnvironment(macCatalyst) +#endif // os(iOS) || targetEnvironment(macCatalyst) || os(macOS) diff --git a/FirebaseCombineSwift/Tests/Unit/Firestore/GetDocumentsTests.swift b/FirebaseCombineSwift/Tests/Unit/Firestore/GetDocumentsTests.swift index 0b1c539f5cb..a0eb8ea0a7b 100644 --- a/FirebaseCombineSwift/Tests/Unit/Firestore/GetDocumentsTests.swift +++ b/FirebaseCombineSwift/Tests/Unit/Firestore/GetDocumentsTests.swift @@ -20,7 +20,7 @@ import XCTest class GetDocumentsTests: XCTestCase { let expectationTimeout: TimeInterval = 2 - class MockQuery: QueryFake { + class MockQuery: QueryFake, @unchecked Sendable { var mockGetDocuments: () throws -> QuerySnapshot = { fatalError("You need to implement \(#function) in your mock.") } diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index 7ad1c4509cf..1cfd124c6f4 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCore' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Core' s.description = <<-DESC @@ -53,7 +53,7 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration # Remember to also update version in `cmake/external/GoogleUtilities.cmake` s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GoogleUtilities/Logger', '~> 8.1' - s.dependency 'FirebaseCoreInternal', '~> 12.0.0' + s.dependency 'FirebaseCoreInternal', '~> 12.3.0' s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => 'Firebase_VERSION=' + s.version.to_s, diff --git a/FirebaseCoreExtension.podspec b/FirebaseCoreExtension.podspec index 8171d0c5e31..21ee3852697 100644 --- a/FirebaseCoreExtension.podspec +++ b/FirebaseCoreExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreExtension' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Extended FirebaseCore APIs for Firebase product SDKs' s.description = <<-DESC @@ -34,5 +34,5 @@ Pod::Spec.new do |s| "#{s.module_name}_Privacy" => 'FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy' } - s.dependency 'FirebaseCore', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' end diff --git a/FirebaseCoreInternal.podspec b/FirebaseCoreInternal.podspec index 8def2d194ed..d07d9a7f190 100644 --- a/FirebaseCoreInternal.podspec +++ b/FirebaseCoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreInternal' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'APIs for internal FirebaseCore usage.' s.description = <<-DESC diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index 41679ea1fb0..3cbc3825050 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCrashlytics' - s.version = '12.0.0' + s.version = '12.3.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/' @@ -59,10 +59,10 @@ Pod::Spec.new do |s| cp -f ./Crashlytics/CrashlyticsInputFiles.xcfilelist ./CrashlyticsInputFiles.xcfilelist PREPARE_COMMAND_END - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseInstallations', '~> 12.0.0' - s.dependency 'FirebaseSessions', '~> 12.0.0' - s.dependency 'FirebaseRemoteConfigInterop', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' + s.dependency 'FirebaseSessions', '~> 12.3.0' + s.dependency 'FirebaseRemoteConfigInterop', '~> 12.3.0' s.dependency 'PromisesObjC', '~> 2.4' s.dependency 'GoogleDataTransport', '~> 10.1' s.dependency 'GoogleUtilities/Environment', '~> 8.1' diff --git a/FirebaseDataConnect/README.md b/FirebaseDataConnect/README.md new file mode 100644 index 00000000000..aa3562eb19e --- /dev/null +++ b/FirebaseDataConnect/README.md @@ -0,0 +1,5 @@ +# Firebase Data Connect Swift SDK + +**Connect your Swift & SwiftUI apps directly to a managed Google CloudSQL (PostgreSQL) database.** + +The SDK resides in a separate [git repo](https://github.com/firebase/data-connect-ios-sdk) diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index faa2503c774..c323f144338 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDatabase' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Realtime Database' s.description = <<-DESC @@ -48,9 +48,9 @@ Simplify your iOS development, grow your user base, and monetize more effectivel s.macos.frameworks = 'CFNetwork', 'Security', 'SystemConfiguration' s.watchos.frameworks = 'CFNetwork', 'Security', 'WatchKit' s.dependency 'leveldb-library', '~> 1.22' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' - s.dependency 'FirebaseSharedSwift', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' + s.dependency 'FirebaseSharedSwift', '~> 12.3.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' @@ -72,7 +72,7 @@ Simplify your iOS development, grow your user base, and monetize more effectivel 'SharedTestUtilities/FIRComponentTestUtilities.[mh]', 'SharedTestUtilities/FIROptionsMock.[mh]', ] - unit_tests.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' + unit_tests.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' unit_tests.dependency 'OCMock' unit_tests.resources = 'FirebaseDatabase/Tests/Resources/syncPointSpec.json', 'FirebaseDatabase/Tests/Resources/GoogleService-Info.plist' diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 72c0d6754e5..0e73ea1f60d 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestore' - s.version = '12.0.0' + s.version = '12.3.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. @@ -35,9 +35,9 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, "#{s.module_name}_Privacy" => 'Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy' } - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseCoreExtension', '~> 12.0.0' - s.dependency 'FirebaseFirestoreInternal', '~> 12.0.0' - s.dependency 'FirebaseSharedSwift', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseCoreExtension', '~> 12.3.0' + s.dependency 'FirebaseFirestoreInternal', '~> 12.3.0' + s.dependency 'FirebaseSharedSwift', '~> 12.3.0' end diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index 05c72462cd0..9dfd6e30ac2 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestoreInternal' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC @@ -91,8 +91,8 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, "#{s.module_name}_Privacy" => 'Firestore/Source/Resources/PrivacyInfo.xcprivacy' } - s.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' - s.dependency 'FirebaseCore', '~> 12.0.0' + s.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' + s.dependency 'FirebaseCore', '~> 12.3.0' abseil_version = '~> 1.20240722.0' s.dependency 'abseil/algorithm', abseil_version diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index f75ae1f809d..c6e04fbdfc1 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFunctions' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Cloud Functions for Firebase' s.description = <<-DESC @@ -35,12 +35,12 @@ Cloud Functions for Firebase. 'FirebaseFunctions/Sources/**/*.swift', ] - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseCoreExtension', '~> 12.0.0' - s.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' - s.dependency 'FirebaseAuthInterop', '~> 12.0.0' - s.dependency 'FirebaseMessagingInterop', '~> 12.0.0' - s.dependency 'FirebaseSharedSwift', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseCoreExtension', '~> 12.3.0' + s.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' + s.dependency 'FirebaseAuthInterop', '~> 12.3.0' + s.dependency 'FirebaseMessagingInterop', '~> 12.3.0' + s.dependency 'FirebaseSharedSwift', '~> 12.3.0' s.dependency 'GTMSessionFetcher/Core', '>= 3.4', '< 6.0' s.test_spec 'objc' do |objc_tests| diff --git a/FirebaseFunctions/Sources/Callable+Codable.swift b/FirebaseFunctions/Sources/Callable+Codable.swift index bcf5ce91a31..ca4790b3052 100644 --- a/FirebaseFunctions/Sources/Callable+Codable.swift +++ b/FirebaseFunctions/Sources/Callable+Codable.swift @@ -133,7 +133,6 @@ public struct Callable: Sendable { /// - Throws: An error if the callable fails to complete /// /// - Returns: The decoded `Response` value - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public func call(_ data: Request) async throws -> Response { let encoded = try encoder.encode(data) let result = try await callable.call(encoded) @@ -160,7 +159,6 @@ public struct Callable: Sendable { /// - Parameters: /// - data: Parameters to pass to the trigger. /// - Returns: The decoded `Response` value - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) public func callAsFunction(_ data: Request) async throws -> Response { return try await call(data) } @@ -174,7 +172,7 @@ private protocol StreamResponseProtocol {} /// /// This can be used as the generic `Response` parameter to ``Callable`` to receive both the /// yielded messages and final return value of the streaming callable function. -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +@available(macOS 12.0, watchOS 8.0, *) public enum StreamResponse: Decodable, Sendable, StreamResponseProtocol { diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 42a59a3e106..1662059662e 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -386,7 +386,6 @@ enum FunctionsConstants { return URL(string: "https://\(region)-\(projectID).cloudfunctions.net/\(name)") } - @available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) func callFunction(at url: URL, withObject data: Any?, options: HTTPSCallableOptions?, @@ -408,49 +407,6 @@ enum FunctionsConstants { } } - private func callFunction(url: URL, - withObject data: Any?, - options: HTTPSCallableOptions?, - timeout: TimeInterval, - context: FunctionsContext, - completion: @escaping @MainActor (Result) - -> Void) { - let fetcher: GTMSessionFetcher - do { - fetcher = try makeFetcher( - url: url, - data: data, - options: options, - timeout: timeout, - context: context - ) - } catch { - DispatchQueue.main.async { - completion(.failure(error)) - } - return - } - - fetcher.beginFetch { [self] data, error in - let result: Result - if let error { - result = .failure(processedError(fromResponseError: error, endpointURL: url)) - } else if let data { - do { - result = try .success(callableResult(fromResponseData: data, endpointURL: url)) - } catch { - result = .failure(error) - } - } else { - result = .failure(FunctionsError(.internal)) - } - - DispatchQueue.main.async { - completion(result) - } - } - } - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func stream(at url: URL, data: SendableWrapper?, @@ -556,7 +512,7 @@ enum FunctionsConstants { } } - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + @available(macOS 12.0, watchOS 8.0, *) private func callableStreamResult(fromResponseData data: Data, endpointURL url: URL) throws -> sending JSONStreamResponse { let data = try processedData(fromResponseData: data, endpointURL: url) diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index b74e5b93c4b..6286a169ef6 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -160,7 +160,6 @@ public final class HTTPSCallable: NSObject, Sendable { /// - 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, *) public func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult { try await functions .callFunction(at: url, withObject: data, options: options, timeout: timeoutInterval) diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index bacf989ed37..9a941b5f8e6 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -36,7 +36,6 @@ struct FunctionsContextProvider: Sendable { self.appCheck = appCheck } - @available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) func context(options: HTTPSCallableOptions?) async throws -> FunctionsContext { async let authToken = auth?.getToken(forcingRefresh: false) async let appCheckToken = getAppCheckToken(options: options) @@ -52,7 +51,6 @@ struct FunctionsContextProvider: Sendable { ) } - @available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) private func getAppCheckToken(options: HTTPSCallableOptions?) async -> String? { guard options?.requireLimitedUseAppCheckTokens != true, @@ -62,7 +60,6 @@ struct FunctionsContextProvider: Sendable { return tokenResult.token } - @available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) private func getLimitedUseAppCheckToken(options: HTTPSCallableOptions?) async -> String? { // At the moment, `await` doesn’t get along with Objective-C’s optional protocol methods. await withCheckedContinuation { (continuation: CheckedContinuation) in diff --git a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift index 8b342f11785..556ab119037 100644 --- a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift +++ b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift @@ -54,7 +54,6 @@ class MockFunctions: Functions, @unchecked Sendable { } } -@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *) class HTTPSCallableTests: XCTestCase { func testCallWithoutParametersSuccess() { // given diff --git a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift index 3032c700bc1..50b48f8c5f0 100644 --- a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift +++ b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift @@ -119,7 +119,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testDataAsync() async throws { let data = DataTestRequest( bool: true, @@ -174,7 +173,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testScalarAsync() async throws { let byName = functions.httpsCallable( "scalarTest", @@ -193,7 +191,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testScalarAsyncAlternateSignature() async throws { let byName: Callable = functions.httpsCallable("scalarTest") let byURL: Callable = functions.httpsCallable(emulatorURL("scalarTest")) @@ -241,7 +238,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testTokenAsync() async throws { // Recreate functions with a token. let functions = Functions( @@ -297,7 +293,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testFCMTokenAsync() async throws { let byName = functions.httpsCallable( "FCMTokenTest", @@ -342,7 +337,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testNullAsync() async throws { let byName = functions.httpsCallable( "nullTest", @@ -391,7 +385,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testMissingResultAsync() async { let byName = functions.httpsCallable( "missingResultTest", @@ -445,7 +438,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testUnhandledErrorAsync() async { let byName = functions.httpsCallable( "unhandledErrorTest", @@ -498,7 +490,6 @@ class IntegrationTests: XCTestCase { waitForExpectations(timeout: 5) } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testUnknownErrorAsync() async { let byName = functions.httpsCallable( "unknownErrorTest", @@ -553,7 +544,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testExplicitErrorAsync() async { let byName = functions.httpsCallable( "explicitErrorTest", @@ -608,7 +598,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testHttpErrorAsync() async { let byName = functions.httpsCallable( "httpErrorTest", @@ -661,7 +650,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testThrowErrorAsync() async { let byName = functions.httpsCallable( "throwTest", @@ -716,7 +704,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testTimeoutAsync() async { var byName = functions.httpsCallable( "timeoutTest", @@ -778,7 +765,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testCallAsFunctionAsync() async throws { let data = DataTestRequest( bool: true, @@ -841,7 +827,6 @@ class IntegrationTests: XCTestCase { } } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testInferredTyesAsync() async throws { let data = DataTestRequest( bool: true, diff --git a/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift b/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift index 6ab0fb11a81..9d8d013e04f 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift @@ -85,16 +85,13 @@ final class FunctionsAPITests: XCTestCase { } } - if #available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 7.0, *) { - // async/await is a Swift Concurrency feature available on iOS 13+ and macOS 10.15+ - Task { - do { - let data: Any? = nil - let result = try await callableRef.call(data) - _ = result.data - } catch { - // ... - } + Task { + do { + let data: Any? = nil + let result = try await callableRef.call(data) + _ = result.data + } catch { + // ... } } @@ -106,15 +103,12 @@ final class FunctionsAPITests: XCTestCase { } } - if #available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 7.0, *) { - // async/await is a Swift Concurrency feature available on iOS 13+ and macOS 10.15+ - Task { - do { - let result = try await callableRef.call() - _ = result.data - } catch { - // ... - } + Task { + do { + let result = try await callableRef.call() + _ = result.data + } catch { + // ... } } diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index 27e3f275853..3921a9d7c59 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessaging' - s.version = '12.0.0-beta' + s.version = '12.3.0-beta' s.summary = 'Firebase In-App Messaging for iOS' s.description = <<-DESC @@ -80,9 +80,9 @@ See more product details at https://firebase.google.com/products/in-app-messagin s.framework = 'UIKit' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseInstallations', '~> 12.0.0' - s.dependency 'FirebaseABTesting', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' + s.dependency 'FirebaseABTesting', '~> 12.3.0' s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' s.dependency 'nanopb', '~> 3.30910.0' diff --git a/FirebaseInAppMessaging/CHANGELOG.md b/FirebaseInAppMessaging/CHANGELOG.md index c2213ec15f6..64ca9d6955b 100644 --- a/FirebaseInAppMessaging/CHANGELOG.md +++ b/FirebaseInAppMessaging/CHANGELOG.md @@ -1,3 +1,8 @@ +# 12.1.0 +- [fixed] Fix Xcode 26 crash from missing `NSUserActivityTypeBrowsingWeb` + symbol. Note that this fix isn't in the 12.1.0 zip and Carthage + distributions, but will be included from 12.2.0 onwards. (#15159) + # 11.0.0 - [removed] **Breaking change**: The deprecated `FirebaseInAppMessagingSwift` module has been removed. See diff --git a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m index 840937c1be5..e80a4f29f48 100644 --- a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m +++ b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m @@ -175,8 +175,9 @@ - (BOOL)followURLWithContinueUserActivity:(NSURL *)url { FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", @"App delegate responds to application:continueUserActivity:restorationHandler:." "Simulating action url opening from a web browser."); - NSUserActivity *userActivity = - [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; + // Use string literal to ensure compatibility with Xcode 26 and iOS 18 + NSString *browsingWebType = @"NSUserActivityTypeBrowsingWeb"; + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:browsingWebType]; userActivity.webpageURL = url; BOOL handled = [self.appDelegate application:self.mainApplication continueUserActivity:userActivity diff --git a/FirebaseInAppMessaging/Tests/Unit/FIRIAMActionUrlFollowerTests.m b/FirebaseInAppMessaging/Tests/Unit/FIRIAMActionUrlFollowerTests.m index 9a0b56e26e5..6fe8e363402 100644 --- a/FirebaseInAppMessaging/Tests/Unit/FIRIAMActionUrlFollowerTests.m +++ b/FirebaseInAppMessaging/Tests/Unit/FIRIAMActionUrlFollowerTests.m @@ -84,8 +84,9 @@ - (void)testUniversalLinkHandlingReturnYES { continueUserActivity:[OCMArg checkWithBlock:^BOOL(id userActivity) { // verifying the type and url field for the userActivity object NSUserActivity *activity = (NSUserActivity *)userActivity; - return [activity.activityType - isEqualToString:NSUserActivityTypeBrowsingWeb] && + // Use string literal to ensure compatibility with Xcode 26 and iOS 18 + NSString *browsingWebType = @"NSUserActivityTypeBrowsingWeb"; + return [activity.activityType isEqualToString:browsingWebType] && [activity.webpageURL isEqual:url]; }] restorationHandler:[OCMArg any]]) diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index 6973f91be45..be91f3a0c72 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInstallations' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Installations' s.description = <<-DESC @@ -45,7 +45,7 @@ Pod::Spec.new do |s| } s.framework = 'Security' - s.dependency 'FirebaseCore', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' s.dependency 'PromisesObjC', '~> 2.4' s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' diff --git a/FirebaseMLModelDownloader.podspec b/FirebaseMLModelDownloader.podspec index a21110e4a04..7d8e926cbcf 100644 --- a/FirebaseMLModelDownloader.podspec +++ b/FirebaseMLModelDownloader.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMLModelDownloader' - s.version = '12.0.0-beta' + s.version = '12.3.0-beta' s.summary = 'Firebase ML Model Downloader' s.description = <<-DESC @@ -36,9 +36,9 @@ Pod::Spec.new do |s| ] s.framework = 'Foundation' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseCoreExtension', '~> 12.0.0' - s.dependency 'FirebaseInstallations', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseCoreExtension', '~> 12.3.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' s.dependency 'GoogleDataTransport', '~> 10.1' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' s.dependency 'SwiftProtobuf', '~> 1.19' diff --git a/FirebaseMLModelDownloader/Sources/proto/firebase_ml_log_sdk.pb.swift b/FirebaseMLModelDownloader/Sources/proto/firebase_ml_log_sdk.pb.swift index e7f83207e8f..ff94e2d8db6 100644 --- a/FirebaseMLModelDownloader/Sources/proto/firebase_ml_log_sdk.pb.swift +++ b/FirebaseMLModelDownloader/Sources/proto/firebase_ml_log_sdk.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: firebase_ml_log_sdk.proto @@ -21,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -enum EventName: SwiftProtobuf.Enum { +enum EventName: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unknownEvent // = 0 case modelDownload // = 100 @@ -66,25 +66,19 @@ enum EventName: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension EventName: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [EventName] = [ + static let allCases: [EventName] = [ .unknownEvent, .modelDownload, .modelUpdate, .remoteModelDeleteOnDevice, ] -} -#endif // swift(>=4.2) +} /// A list of error codes for various components of the system. For model downloading, the /// range of error codes is 100 to 199. -enum ErrorCode: SwiftProtobuf.Enum { +enum ErrorCode: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int /// No error at all. @@ -156,13 +150,8 @@ enum ErrorCode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension ErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [ErrorCode] = [ + static let allCases: [ErrorCode] = [ .noError, .uriExpired, .noNetworkConnection, @@ -173,12 +162,11 @@ extension ErrorCode: CaseIterable { .modelHashMismatch, .unknownError, ] -} -#endif // swift(>=4.2) +} /// Information about various parts of the system: app, Firebase, SDK. -struct SystemInfo { +struct SystemInfo: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -206,7 +194,7 @@ struct SystemInfo { } /// Information about models. -struct ModelInfo { +struct ModelInfo: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -229,7 +217,7 @@ struct ModelInfo { /// The model type is currently envisioned to be used mainly for model /// download/update. - enum ModelType: SwiftProtobuf.Enum { + enum ModelType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case typeUnknown // = 0 case custom // = 1 @@ -255,26 +243,20 @@ struct ModelInfo { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [ModelInfo.ModelType] = [ + .typeUnknown, + .custom, + ] + } init() {} } -#if swift(>=4.2) - -extension ModelInfo.ModelType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [ModelInfo.ModelType] = [ - .typeUnknown, - .custom, - ] -} - -#endif // swift(>=4.2) - /// Detailed information about a model. /// The message used to be named "CustomModelOptions". -struct ModelOptions { +struct ModelOptions: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -303,7 +285,7 @@ struct ModelOptions { /// result in multiple log entries. "download_status" in the log entry indicates /// during which stage it is logged. /// This message used to be named "CustomModelDownloadLogEvent". -struct ModelDownloadLogEvent { +struct ModelDownloadLogEvent: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -352,7 +334,7 @@ struct ModelDownloadLogEvent { /// or explicitly does not affect the later stages of the download. As a /// result, later stages (i.e. enum tag 3+) do not distinguish between explicit /// and implicit triggering. - enum DownloadStatus: SwiftProtobuf.Enum { + enum DownloadStatus: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unknownStatus // = 0 @@ -422,6 +404,20 @@ struct ModelDownloadLogEvent { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [ModelDownloadLogEvent.DownloadStatus] = [ + .unknownStatus, + .explicitlyRequested, + .implicitlyRequested, + .modelInfoRetrievalSucceeded, + .modelInfoRetrievalFailed, + .scheduled, + .downloading, + .succeeded, + .failed, + .updateAvailable, + ] + } init() {} @@ -429,28 +425,8 @@ struct ModelDownloadLogEvent { fileprivate var _options: ModelOptions? = nil } -#if swift(>=4.2) - -extension ModelDownloadLogEvent.DownloadStatus: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [ModelDownloadLogEvent.DownloadStatus] = [ - .unknownStatus, - .explicitlyRequested, - .implicitlyRequested, - .modelInfoRetrievalSucceeded, - .modelInfoRetrievalFailed, - .scheduled, - .downloading, - .succeeded, - .failed, - .updateAvailable, - ] -} - -#endif // swift(>=4.2) - /// Information about deleting a downloaded model on device. -struct DeleteModelLogEvent { +struct DeleteModelLogEvent: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -469,7 +445,7 @@ struct DeleteModelLogEvent { /// Main log event for FirebaseMl, that contains individual API events, like model /// download. /// NEXT ID: 44. -struct FirebaseMlLogEvent { +struct FirebaseMlLogEvent: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -520,37 +496,16 @@ struct FirebaseMlLogEvent { // MARK: - Code below here is support for the SwiftProtobuf runtime. extension EventName: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN_EVENT"), - 100: .same(proto: "MODEL_DOWNLOAD"), - 101: .same(proto: "MODEL_UPDATE"), - 252: .same(proto: "REMOTE_MODEL_DELETE_ON_DEVICE"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_EVENT\0\u{2}d\u{1}MODEL_DOWNLOAD\0\u{1}MODEL_UPDATE\0\u{2}W\u{2}REMOTE_MODEL_DELETE_ON_DEVICE\0") } extension ErrorCode: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NO_ERROR"), - 101: .same(proto: "URI_EXPIRED"), - 102: .same(proto: "NO_NETWORK_CONNECTION"), - 104: .same(proto: "DOWNLOAD_FAILED"), - 105: .same(proto: "MODEL_INFO_DOWNLOAD_UNSUCCESSFUL_HTTP_STATUS"), - 106: .same(proto: "MODEL_INFO_DOWNLOAD_NO_HASH"), - 107: .same(proto: "MODEL_INFO_DOWNLOAD_CONNECTION_FAILED"), - 116: .same(proto: "MODEL_HASH_MISMATCH"), - 9999: .same(proto: "UNKNOWN_ERROR"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0NO_ERROR\0\u{2}e\u{1}URI_EXPIRED\0\u{1}NO_NETWORK_CONNECTION\0\u{2}\u{2}DOWNLOAD_FAILED\0\u{1}MODEL_INFO_DOWNLOAD_UNSUCCESSFUL_HTTP_STATUS\0\u{1}MODEL_INFO_DOWNLOAD_NO_HASH\0\u{1}MODEL_INFO_DOWNLOAD_CONNECTION_FAILED\0\u{2}\u{9}MODEL_HASH_MISMATCH\0\u{2}[Z\u{2}UNKNOWN_ERROR\0") } extension SystemInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "SystemInfo" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "app_id"), - 2: .standard(proto: "app_version"), - 3: .standard(proto: "firebase_project_id"), - 4: .standard(proto: "ml_sdk_version"), - 7: .standard(proto: "api_key"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}app_id\0\u{3}app_version\0\u{3}firebase_project_id\0\u{3}ml_sdk_version\0\u{4}\u{3}api_key\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -600,12 +555,7 @@ extension SystemInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio extension ModelInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "ModelInfo" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .same(proto: "version"), - 5: .same(proto: "hash"), - 6: .standard(proto: "model_type"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}name\0\u{1}version\0\u{2}\u{3}hash\0\u{3}model_type\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -649,18 +599,12 @@ extension ModelInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation } extension ModelInfo.ModelType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "TYPE_UNKNOWN"), - 1: .same(proto: "CUSTOM"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0TYPE_UNKNOWN\0\u{1}CUSTOM\0") } extension ModelOptions: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "ModelOptions" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "model_info"), - 4: .standard(proto: "is_model_update_enabled"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}model_info\0\u{4}\u{3}is_model_update_enabled\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -676,9 +620,13 @@ extension ModelOptions: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } func traverse(visitor: inout V) throws { - if let v = self._modelInfo { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._modelInfo { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } + } }() if self.isModelUpdateEnabled != false { try visitor.visitSingularBoolField(value: self.isModelUpdateEnabled, fieldNumber: 4) } @@ -695,14 +643,7 @@ extension ModelOptions: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat extension ModelDownloadLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "ModelDownloadLogEvent" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "options"), - 2: .standard(proto: "rough_download_duration_ms"), - 3: .standard(proto: "error_code"), - 4: .standard(proto: "exact_download_duration_ms"), - 5: .standard(proto: "download_status"), - 6: .standard(proto: "download_failure_status"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}options\0\u{3}rough_download_duration_ms\0\u{3}error_code\0\u{3}exact_download_duration_ms\0\u{3}download_status\0\u{3}download_failure_status\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -722,9 +663,13 @@ extension ModelDownloadLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageIm } func traverse(visitor: inout V) throws { - if let v = self._options { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._options { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } + } }() if self.roughDownloadDurationMs != 0 { try visitor.visitSingularUInt64Field(value: self.roughDownloadDurationMs, fieldNumber: 2) } @@ -756,26 +701,12 @@ extension ModelDownloadLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageIm } extension ModelDownloadLogEvent.DownloadStatus: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN_STATUS"), - 1: .same(proto: "EXPLICITLY_REQUESTED"), - 2: .same(proto: "IMPLICITLY_REQUESTED"), - 3: .same(proto: "MODEL_INFO_RETRIEVAL_SUCCEEDED"), - 4: .same(proto: "MODEL_INFO_RETRIEVAL_FAILED"), - 5: .same(proto: "SCHEDULED"), - 6: .same(proto: "DOWNLOADING"), - 7: .same(proto: "SUCCEEDED"), - 8: .same(proto: "FAILED"), - 10: .same(proto: "UPDATE_AVAILABLE"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_STATUS\0\u{1}EXPLICITLY_REQUESTED\0\u{1}IMPLICITLY_REQUESTED\0\u{1}MODEL_INFO_RETRIEVAL_SUCCEEDED\0\u{1}MODEL_INFO_RETRIEVAL_FAILED\0\u{1}SCHEDULED\0\u{1}DOWNLOADING\0\u{1}SUCCEEDED\0\u{1}FAILED\0\u{2}\u{2}UPDATE_AVAILABLE\0") } extension DeleteModelLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "DeleteModelLogEvent" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "model_type"), - 2: .standard(proto: "is_successful"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}model_type\0\u{3}is_successful\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -810,12 +741,7 @@ extension DeleteModelLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl extension FirebaseMlLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "FirebaseMlLogEvent" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "system_info"), - 2: .standard(proto: "event_name"), - 3: .standard(proto: "model_download_log_event"), - 40: .standard(proto: "delete_model_log_event"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}system_info\0\u{3}event_name\0\u{3}model_download_log_event\0\u{4}%delete_model_log_event\0") fileprivate class _StorageClass { var _systemInfo: SystemInfo? = nil @@ -823,7 +749,11 @@ extension FirebaseMlLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImple var _modelDownloadLogEvent: ModelDownloadLogEvent? = nil var _deleteModelLogEvent: DeleteModelLogEvent? = nil - static let defaultInstance = _StorageClass() + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() private init() {} @@ -862,18 +792,22 @@ extension FirebaseMlLogEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImple func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._systemInfo { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._systemInfo { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } + } }() if _storage._eventName != .unknownEvent { try visitor.visitSingularEnumField(value: _storage._eventName, fieldNumber: 2) } - if let v = _storage._modelDownloadLogEvent { + try { if let v = _storage._modelDownloadLogEvent { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } - if let v = _storage._deleteModelLogEvent { + } }() + try { if let v = _storage._deleteModelLogEvent { try visitor.visitSingularMessageField(value: v, fieldNumber: 40) - } + } }() } try unknownFields.traverse(visitor: &visitor) } diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 627268c0891..de61f8e8549 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessaging' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Messaging' s.description = <<-DESC @@ -60,8 +60,8 @@ device, and it is completely free. s.tvos.framework = 'SystemConfiguration' s.osx.framework = 'SystemConfiguration' s.weak_framework = 'UserNotifications' - s.dependency 'FirebaseInstallations', '~> 12.0.0' - s.dependency 'FirebaseCore', '~> 12.0.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' + s.dependency 'FirebaseCore', '~> 12.3.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.1' s.dependency 'GoogleUtilities/Reachability', '~> 8.1' s.dependency 'GoogleUtilities/Environment', '~> 8.1' diff --git a/FirebaseMessaging/CHANGELOG.md b/FirebaseMessaging/CHANGELOG.md index 00b746df27a..f2756c5ce0a 100644 --- a/FirebaseMessaging/CHANGELOG.md +++ b/FirebaseMessaging/CHANGELOG.md @@ -1,3 +1,8 @@ +# 12.1.0 +- [fixed] Fix Xcode 26 crash from missing `NSUserActivityTypeBrowsingWeb` + symbol. Note that this fix isn't in the 12.1.0 zip and Carthage + distributions, but will be included from 12.2.0 onwards. (#15159) + # 11.14.0 - [fixed] Fix a potential SQL injection issue. (#14846). diff --git a/FirebaseMessaging/Sources/FIRMessaging.m b/FirebaseMessaging/Sources/FIRMessaging.m index e2bb09b098c..3eb83e94e5f 100644 --- a/FirebaseMessaging/Sources/FIRMessaging.m +++ b/FirebaseMessaging/Sources/FIRMessaging.m @@ -400,8 +400,9 @@ - (void)handleIncomingLinkIfNeededFromMessage:(NSDictionary *)message { // if they haven't implemented it. if ([NSUserActivity class] != nil && [appDelegate respondsToSelector:continueUserActivitySelector]) { - NSUserActivity *userActivity = - [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; + // Use string literal to ensure compatibility with Xcode 26 and iOS 18 + NSString *browsingWebType = @"NSUserActivityTypeBrowsingWeb"; + NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:browsingWebType]; userActivity.webpageURL = url; [appDelegate application:application continueUserActivity:userActivity diff --git a/FirebaseMessagingInterop.podspec b/FirebaseMessagingInterop.podspec index 495cc9207b1..4aa8021895b 100644 --- a/FirebaseMessagingInterop.podspec +++ b/FirebaseMessagingInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessagingInterop' - s.version = '12.0.0' + s.version = '12.3.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 50b110b2a37..ba627a2370b 100644 --- a/FirebasePerformance.podspec +++ b/FirebasePerformance.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebasePerformance' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Performance' s.description = <<-DESC @@ -58,10 +58,10 @@ Firebase Performance library to measure performance of Mobile and Web Apps. s.ios.framework = 'CoreTelephony' s.framework = 'QuartzCore' s.framework = 'SystemConfiguration' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseInstallations', '~> 12.0.0' - s.dependency 'FirebaseRemoteConfig', '~> 12.0.0' - s.dependency 'FirebaseSessions', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' + s.dependency 'FirebaseRemoteConfig', '~> 12.3.0' + s.dependency 'FirebaseSessions', '~> 12.3.0' s.dependency 'GoogleDataTransport', '~> 10.1' s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GoogleUtilities/MethodSwizzler', '~> 8.1' diff --git a/FirebasePerformance/CHANGELOG.md b/FirebasePerformance/CHANGELOG.md index 6ac0cee960b..14dbbbb3a16 100644 --- a/FirebasePerformance/CHANGELOG.md +++ b/FirebasePerformance/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [fixed] Add missing nanopb dependency to fix SwiftPM builds when building + dynamically linked libraries. (#15276) + # 11.6.0 - [fixed] Fix a crash related to registering for notifications when the app is between foreground or background states. (#13174) diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 413a164fa1a..d12f5f65acb 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfig' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Remote Config' s.description = <<-DESC @@ -49,13 +49,13 @@ app update. s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' } - s.dependency 'FirebaseABTesting', '~> 12.0.0' - s.dependency 'FirebaseSharedSwift', '~> 12.0.0' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseInstallations', '~> 12.0.0' + s.dependency 'FirebaseABTesting', '~> 12.3.0' + s.dependency 'FirebaseSharedSwift', '~> 12.3.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GoogleUtilities/NSData+zlib', '~> 8.1' - s.dependency 'FirebaseRemoteConfigInterop', '~> 12.0.0' + s.dependency 'FirebaseRemoteConfigInterop', '~> 12.3.0' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index f1b25939183..f5db5a0a572 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -1,3 +1,11 @@ +# Unreleased +- [fixed] Add missing GoogleUtilities dependency to fix SwiftPM builds when + building dynamically linked libraries. (#15276) + +# 12.2.0 +- [fixed] Fixed a race condition that could lead to a crash during network + session recreation. (#15087) + # 12.0.0 - [added] Improved how the SDK handles real-time requests when a Firebase project has exceeded its available quota for real-time services. diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 443265800b2..258eb362fe7 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -700,6 +700,8 @@ - (FIRRemoteConfigSettings *)configSettings { dispatch_sync(_queue, ^{ minimumFetchInterval = self->_settings.minimumFetchInterval; fetchTimeout = self->_settings.fetchTimeout; + // The NSURLSession needs to be recreated whenever the fetch timeout may be updated. + [_configFetch recreateNetworkSession]; }); FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000066", @"Successfully read configSettings. Minimum Fetch Interval:%f, " @@ -708,8 +710,6 @@ - (FIRRemoteConfigSettings *)configSettings { FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init]; settings.minimumFetchInterval = minimumFetchInterval; settings.fetchTimeout = fetchTimeout; - /// The NSURLSession needs to be recreated whenever the fetch timeout may be updated. - [_configFetch recreateNetworkSession]; return settings; } @@ -721,7 +721,7 @@ - (void)setConfigSettings:(FIRRemoteConfigSettings *)configSettings { self->_settings.minimumFetchInterval = configSettings.minimumFetchInterval; self->_settings.fetchTimeout = configSettings.fetchTimeout; - /// The NSURLSession needs to be recreated whenever the fetch timeout may be updated. + // The NSURLSession needs to be recreated whenever the fetch timeout may be updated. [self->_configFetch recreateNetworkSession]; FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000067", @"Successfully set configSettings. Minimum Fetch Interval:%f, " diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index 85e86f85e36..1df80385972 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -113,6 +113,7 @@ - (instancetype)initWithContent:(RCNConfigContent *)content } /// Force a new NSURLSession creation for updated config. +/// - Warning: This API is **not** thread-safe. - (void)recreateNetworkSession { if (_fetchSession) { [_fetchSession invalidateAndCancel]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index 9d4781930f1..a58bb92a76d 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -311,7 +311,6 @@ - (void)tearDown { _userDefaultsMock = nil; for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { [(id)_configInstances[i] stopMocking]; - // [(id)_configFetch[i] stopMocking]; } [_configInstances removeAllObjects]; [_configFetch removeAllObjects]; diff --git a/FirebaseRemoteConfigInterop.podspec b/FirebaseRemoteConfigInterop.podspec index ffa06365043..b1121de1930 100644 --- a/FirebaseRemoteConfigInterop.podspec +++ b/FirebaseRemoteConfigInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfigInterop' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Remote Config functionality.' s.description = <<-DESC diff --git a/FirebaseSessions.podspec b/FirebaseSessions.podspec index 56fb429ab3f..f003161ea12 100644 --- a/FirebaseSessions.podspec +++ b/FirebaseSessions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSessions' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Sessions' s.description = <<-DESC @@ -39,9 +39,9 @@ Pod::Spec.new do |s| base_dir + 'SourcesObjC/**/*.{c,h,m,mm}', ] - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseCoreExtension', '~> 12.0.0' - s.dependency 'FirebaseInstallations', '~> 12.0.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseCoreExtension', '~> 12.3.0' + s.dependency 'FirebaseInstallations', '~> 12.3.0' s.dependency 'GoogleDataTransport', '~> 10.1' s.dependency 'GoogleUtilities/Environment', '~> 8.1' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' diff --git a/FirebaseSessions/Sources/SessionInitiator.swift b/FirebaseSessions/Sources/SessionInitiator.swift index 9524fdbae73..2e3afeaa729 100644 --- a/FirebaseSessions/Sources/SessionInitiator.swift +++ b/FirebaseSessions/Sources/SessionInitiator.swift @@ -51,7 +51,7 @@ class SessionInitiator { let notificationCenter = NotificationCenter.default #if os(iOS) || os(tvOS) || os(visionOS) - // Change background update event listerner for iPadOS 26 multi-windowing supoort + // Change background update event listener for iPadOS 26 multi-windowing support if #available(iOS 26, *), GULAppEnvironmentUtil.appleDevicePlatform().contains("ipados") { notificationCenter.addObserver( self, diff --git a/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift b/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift index 18f9bcefbff..79a2e1ffa20 100644 --- a/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift +++ b/FirebaseSessions/Sources/Settings/SettingsCacheClient.swift @@ -129,7 +129,7 @@ final class SettingsCache: SettingsCacheClient { guard let duration = cacheContent[Self.flagCacheDuration] as? Double else { return Self.cacheDurationSecondsDefault } - print("Duration: \(duration)") + Logger.logDebug("[Settings] Cache duration: \(duration)") return duration } } diff --git a/FirebaseSharedSwift.podspec b/FirebaseSharedSwift.podspec index d91433da223..500846b7f4c 100644 --- a/FirebaseSharedSwift.podspec +++ b/FirebaseSharedSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSharedSwift' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Shared Swift Extensions for Firebase' s.description = <<-DESC diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index 9c9561b7335..130fabcb31b 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseStorage' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Firebase Storage' s.description = <<-DESC @@ -37,10 +37,10 @@ Firebase Storage provides robust, secure file uploads and downloads from Firebas 'FirebaseStorage/Typedefs/*.h', ] - s.dependency 'FirebaseAppCheckInterop', '~> 12.0.0' - s.dependency 'FirebaseAuthInterop', '~> 12.0.0' - s.dependency 'FirebaseCore', '~> 12.0.0' - s.dependency 'FirebaseCoreExtension', '~> 12.0.0' + s.dependency 'FirebaseAppCheckInterop', '~> 12.3.0' + s.dependency 'FirebaseAuthInterop', '~> 12.3.0' + s.dependency 'FirebaseCore', '~> 12.3.0' + s.dependency 'FirebaseCoreExtension', '~> 12.3.0' s.dependency 'GTMSessionFetcher/Core', '>= 3.4', '< 6.0' s.dependency 'GoogleUtilities/Environment', '~> 8.1' @@ -57,7 +57,7 @@ Firebase Storage provides robust, secure file uploads and downloads from Firebas objc_tests.requires_app_host = true objc_tests.resources = 'FirebaseStorage/Tests/Integration/Resources/1mb.dat', 'FirebaseStorage/Tests/Integration/Resources/GoogleService-Info.plist' - objc_tests.dependency 'FirebaseAuth', '~> 12.0.0' + objc_tests.dependency 'FirebaseAuth', '~> 12.3.0' objc_tests.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' } @@ -86,6 +86,6 @@ Firebase Storage provides robust, secure file uploads and downloads from Firebas int_tests.resources = 'FirebaseStorage/Tests/Integration/Resources/1mb.dat', 'FirebaseStorage/Tests/Integration/Resources/GoogleService-Info.plist', 'FirebaseStorage/Tests/Integration/Resources/HomeImprovement.numbers' - int_tests.dependency 'FirebaseAuth', '~> 12.0.0' + int_tests.dependency 'FirebaseAuth', '~> 12.3.0' end end diff --git a/FirebaseTestingSupport/Firestore/Sources/FIRQueryFake.mm b/FirebaseTestingSupport/Firestore/Sources/FIRQueryFake.mm index 8d8c181ca0d..002fbf67e8d 100644 --- a/FirebaseTestingSupport/Firestore/Sources/FIRQueryFake.mm +++ b/FirebaseTestingSupport/Firestore/Sources/FIRQueryFake.mm @@ -16,10 +16,13 @@ @implementation FIRQueryFake +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-designated-initializers" - (instancetype)init { // The object is partially initialized. Make sure the methods used during testing are overridden. return self; } +#pragma clang diagnostic pop - (void)getDocumentsWithCompletion:(FIRQuerySnapshotBlock)completion { if (self.getDocumentsHandler) { diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index bcb63c64c11..628e3afd826 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,3 +1,6 @@ +# 12.1.0 +- [fixed] Fixed accidental removal of `pod "Firebase/Firestore"` for tvOS in 12.0.0. + # 11.12.0 - [fixed] Fixed the `null` value handling in `isNotEqualTo` and `notIn` filters. diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index 4d26b5e6cfb..076e2641595 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurement' - s.version = '12.0.0' + s.version = '12.3.0' s.summary = 'Shared measurement methods for Google libraries. Not intended for direct use.' s.description = <<-DESC @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.authors = 'Google, Inc.' s.source = { - :http => 'https://dl.google.com/firebase/ios/analytics/dcba2ac84f595e1b/GoogleAppMeasurement-12.0.0.tar.gz' + :http => 'https://dl.google.com/firebase/ios/analytics/47d80ee1ff340179/GoogleAppMeasurement-12.2.0.tar.gz' } s.cocoapods_version = '>= 1.12.0' @@ -37,9 +37,9 @@ Pod::Spec.new do |s| s.default_subspecs = 'Default' s.subspec 'Default' do |ss| - ss.dependency 'GoogleAppMeasurement/Core', '12.0.0' - ss.dependency 'GoogleAppMeasurement/IdentitySupport', '12.0.0' - ss.ios.dependency 'GoogleAdsOnDeviceConversion', '2.1.0' + ss.dependency 'GoogleAppMeasurement/Core', '12.3.0' + ss.dependency 'GoogleAppMeasurement/IdentitySupport', '12.3.0' + ss.ios.dependency 'GoogleAdsOnDeviceConversion', '2.3.0' end s.subspec 'Core' do |ss| @@ -47,7 +47,7 @@ Pod::Spec.new do |s| end s.subspec 'IdentitySupport' do |ss| - ss.dependency 'GoogleAppMeasurement/Core', '12.0.0' + ss.dependency 'GoogleAppMeasurement/Core', '12.3.0' ss.vendored_frameworks = 'Frameworks/GoogleAppMeasurementIdentitySupport.xcframework' end end diff --git a/Package.swift b/Package.swift index 034bfc6d90a..8336778a177 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ import PackageDescription -let firebaseVersion = "12.0.0" +let firebaseVersion = "12.3.0" let package = Package( name: "Firebase", @@ -329,8 +329,8 @@ let package = Package( ), .binaryTarget( name: "FirebaseAnalytics", - url: "https://dl.google.com/firebase/ios/swiftpm/12.0.0/FirebaseAnalytics.zip", - checksum: "d5db7d1373be7c2595e34e6a50c943ca61ee36b806037da39c7b4d0253ce11db" + url: "https://dl.google.com/firebase/ios/swiftpm/12.2.0/FirebaseAnalytics.zip", + checksum: "f1b07dabcdf3f2b6c495af72baa55e40672a625b8a1b6c631fb43ec74a2ec1ca" ), .testTarget( name: "AnalyticsSwiftUnit", @@ -1003,6 +1003,7 @@ let package = Package( "FirebaseABTesting", "FirebaseInstallations", "FirebaseRemoteConfigInterop", + .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "GULNSData", package: "GoogleUtilities"), ], path: "FirebaseRemoteConfig/Sources", @@ -1100,6 +1101,13 @@ let package = Package( "FirebaseInstallations", "FirebaseCoreExtension", "FirebaseSessionsObjC", + // The `FirebaseSessions` target transitively depends on nanopb via the internal + // `FirebaseSessionsObjC` target. Not explicitly depending on nanopb leads to + // undefined symbol errors in Tuist based SPM builds. + // See the conversations in + // - https://github.com/firebase/firebase-ios-sdk/issues/15276 + // - https://github.com/firebase/firebase-ios-sdk/pull/15287 + .product(name: "nanopb", package: "nanopb"), .product(name: "Promises", package: "Promises"), .product(name: "GoogleDataTransport", package: "GoogleDataTransport"), .product(name: "GULEnvironment", package: "GoogleUtilities"), @@ -1384,7 +1392,7 @@ func googleAppMeasurementDependency() -> Package.Dependency { return .package(url: appMeasurementURL, branch: "main") } - return .package(url: appMeasurementURL, exact: "12.0.0") + return .package(url: appMeasurementURL, exact: "12.2.0") } func abseilDependency() -> Package.Dependency { diff --git a/README.md b/README.md index 6c5168f8bbe..15a560d92f7 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@20 +brew install clang-format@21 brew install mint ``` diff --git a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json index a2a0118d1fb..4dc290fc4b4 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseABTesting-76f42e76c5778188.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseABTesting-14bbd5283f79341d.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseABTesting-5436773ba2b9326e.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseABTesting-bd721c84362383a6.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseABTesting-bb0e44f97fd81c31.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseABTesting-328b9123860fa215.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/ABTesting-d0fdf10c43e985b1.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/ABTesting-d0fdf10c43e985b1.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/ABTesting-a71d17cadc209af9.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAIBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAIBinary.json index 4e7b9056455..493bacd3a80 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAIBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAIBinary.json @@ -1,5 +1,8 @@ { "11.13.0": "https://dl.google.com/dl/firebase/ios/carthage/11.13.0/FirebaseAI-b1e75ff6284775b1.zip", "11.14.0": "https://dl.google.com/dl/firebase/ios/carthage/11.14.0/FirebaseAI-0991ef5c3a83833a.zip", - "11.15.0": "https://dl.google.com/dl/firebase/ios/carthage/11.15.0/FirebaseAI-ba1237ee5b7a5baa.zip" + "11.15.0": "https://dl.google.com/dl/firebase/ios/carthage/11.15.0/FirebaseAI-ba1237ee5b7a5baa.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseAI-05a4568076093001.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseAI-1fa7d016c66b2331.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseAI-8fac222fb35cd84e.zip" } diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json index f7ff0a95f02..054ce256713 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAnalytics-74a530056782a0ed.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAnalytics-0929c5c36f6a3dd2.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAnalytics-fe649e5740ef72e9.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseAnalytics-70ead21957efa870.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseAnalytics-88dad74aa8ab040a.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseAnalytics-b37787f72cdbb950.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Analytics-2468c231ebeb7922.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Analytics-bc8101d420b896c5.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Analytics-d2b6a6b0242db786.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json index 2344d3aed80..d6682a9e7f3 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAppCheck-31041ca049010d8b.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAppCheck-68439fef0d9ee01c.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAppCheck-366c926c105319b0.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseAppCheck-b9f47f169bb6249c.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseAppCheck-072a1be1f8eb1177.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseAppCheck-dac2380c7e1b9898.zip", "8.0.0": "https://dl.google.com/dl/firebase/ios/carthage/8.0.0/FirebaseAppCheck-9ef1d217cf057203.zip", "8.1.0": "https://dl.google.com/dl/firebase/ios/carthage/8.1.0/FirebaseAppCheck-fc03215d9fe45d3a.zip", "8.10.0": "https://dl.google.com/dl/firebase/ios/carthage/8.10.0/FirebaseAppCheck-6ebe9e9539f06003.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json index b86174efba7..da411015d0a 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAppDistribution-908bdcdc87eee86b.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAppDistribution-e558ade73b5891d6.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAppDistribution-32e12df219d91736.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseAppDistribution-076c2c6efb7eb8dc.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseAppDistribution-370884f5f825f098.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseAppDistribution-042b04483c9241b6.zip", "6.31.0": "https://dl.google.com/dl/firebase/ios/carthage/6.31.0/FirebaseAppDistribution-07f6a2cf7f576a8a.zip", "6.32.0": "https://dl.google.com/dl/firebase/ios/carthage/6.32.0/FirebaseAppDistribution-a9c4f5db794508ca.zip", "6.33.0": "https://dl.google.com/dl/firebase/ios/carthage/6.33.0/FirebaseAppDistribution-448a96d2ade54581.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json index 596d5a8116a..087bb03cb96 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAuth-24605cbb83eadb6e.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAuth-f41dc3e6a1a923d7.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAuth-ab131b2e07abc902.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseAuth-e58b2b5f430bfbfe.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseAuth-2c17100b302eb080.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseAuth-9f0a14da6c12ea6d.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Auth-0fa76ba0f7956220.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Auth-5ddd2b4351012c7a.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Auth-5e248984d78d7284.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json index e86ca152e55..a2af858e249 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseCrashlytics-066561f91425ee41.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseCrashlytics-36f932dcd3db6874.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseCrashlytics-81aa29d9a106acb0.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseCrashlytics-d9a9be2a4e220017.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseCrashlytics-fbf241b0c59f3821.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseCrashlytics-623ce628d0404f39.zip", "6.15.0": "https://dl.google.com/dl/firebase/ios/carthage/6.15.0/FirebaseCrashlytics-1c6d22d5b73c84fd.zip", "6.16.0": "https://dl.google.com/dl/firebase/ios/carthage/6.16.0/FirebaseCrashlytics-938e5fd0e2eab3b3.zip", "6.17.0": "https://dl.google.com/dl/firebase/ios/carthage/6.17.0/FirebaseCrashlytics-fa09f0c8f31ed5d9.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json index 71718147ec6..8e9f4fa6ea5 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseDatabase-665f1d12ee1c5583.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseDatabase-8e544ced90fb6eb2.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseDatabase-8b970d6e0f67a415.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseDatabase-18b95ef28b89f3db.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseDatabase-d6c24e13e4b05437.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseDatabase-a87ae96a7eeb2535.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Database-1f7a820452722c7d.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Database-1f7a820452722c7d.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Database-59a12d87456b3e1c.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json index d482b503303..2e5d344364e 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseFirestore-b234cb861ecaaabe.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseFirestore-792558f0eddb9934.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseFirestore-30a2451150d46015.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseFirestore-635c7c2864cdd1a9.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseFirestore-6098779ef7b7b151.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseFirestore-8d65b82dc9d53ddf.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Firestore-68fc02c229d0cc69.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Firestore-87a804ab561d91db.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Firestore-ecb3eea7bde7e8e8.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json index ba156e8b3c3..f3cf5077781 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseFunctions-1b9c374ba8165fcb.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseFunctions-e85bcbe133482bbc.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseFunctions-1681244d37d89040.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseFunctions-2ac7bbbaf94c52e1.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseFunctions-f4a1c660d9a2ea75.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseFunctions-f3aa95160827b0af.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Functions-f4c426016dd41e38.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Functions-c6c44427c3034736.zip", "5.0.0": "https://dl.google.com/dl/firebase/ios/carthage/5.0.0/Functions-146f34c401bd459b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json index 945e0372292..44bb103f3ac 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/GoogleSignIn-e745ddfd77045287.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/GoogleSignIn-45d907510d5c840b.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/GoogleSignIn-d4359fb699843869.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/GoogleSignIn-b924d38d37bc920a.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/GoogleSignIn-01f98c11db934294.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/GoogleSignIn-31b2e32d1dadbaa8.zip", "6.0.0": "https://dl.google.com/dl/firebase/ios/carthage/6.0.0/GoogleSignIn-de9c5d5e8eb6d6ea.zip", "6.1.0": "https://dl.google.com/dl/firebase/ios/carthage/6.1.0/GoogleSignIn-8c82f2870573a793.zip", "6.10.0": "https://dl.google.com/dl/firebase/ios/carthage/6.10.0/GoogleSignIn-ff3aef61c4a55b05.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json index f7d04b01c31..cb089894be6 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseInAppMessaging-ee6bedaea672150b.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseInAppMessaging-af2b93ac9f853087.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseInAppMessaging-67bccdf31b1dc458.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseInAppMessaging-59f9fc54fc4eecb5.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseInAppMessaging-78a0d591fb574512.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseInAppMessaging-0ec7907b67ce2888.zip", "5.10.0": "https://dl.google.com/dl/firebase/ios/carthage/5.10.0/InAppMessaging-a7a3f933362f6e95.zip", "5.11.0": "https://dl.google.com/dl/firebase/ios/carthage/5.11.0/InAppMessaging-fa28ce1b88fbca93.zip", "5.12.0": "https://dl.google.com/dl/firebase/ios/carthage/5.12.0/InAppMessaging-fa28ce1b88fbca93.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json index 193446fc061..3d2e26797bd 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseMLModelDownloader-6c596e97794f0430.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseMLModelDownloader-373db3aced970d88.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseMLModelDownloader-2d08410294abf160.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseMLModelDownloader-1f183c6e3be7cab3.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseMLModelDownloader-3864d35f4429bc08.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseMLModelDownloader-6bfb3459ae557ef3.zip", "8.0.0": "https://dl.google.com/dl/firebase/ios/carthage/8.0.0/FirebaseMLModelDownloader-8f972757fb181320.zip", "8.1.0": "https://dl.google.com/dl/firebase/ios/carthage/8.1.0/FirebaseMLModelDownloader-058ad59fa6dc0111.zip", "8.10.0": "https://dl.google.com/dl/firebase/ios/carthage/8.10.0/FirebaseMLModelDownloader-286479a966d2fb37.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json index b48d93e0ea1..ba4b4663e5d 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseMessaging-9d0027ada8995751.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseMessaging-ffd97136b9f3cde5.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseMessaging-379bf3738f94ef44.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseMessaging-0f018ab3d7701839.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseMessaging-252cac88c87e9c55.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseMessaging-d1ab6eaf596d9b7d.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Messaging-a22ef2b5f2f30f82.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Messaging-94fa4e090c7e9185.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Messaging-2a00a1c64a19d176.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json index 1c75aa6bebf..5c16479a908 100644 --- a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebasePerformance-3b0d7bf17d771ece.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebasePerformance-e7063e87d9b3d1b7.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebasePerformance-9a2f8d3983650cea.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebasePerformance-0df8c67ab3cb665c.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebasePerformance-dec4dc5c3edadd9a.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebasePerformance-1913383f1952dce6.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Performance-d8693eb892bfa05b.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Performance-0a400f9460f7a71d.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Performance-f5b4002ab96523e4.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json index 0d5ccc12c74..ecc201dff62 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseRemoteConfig-68b7ad270036fef7.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseRemoteConfig-6e66634da4590f07.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseRemoteConfig-7455afe6f2231467.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseRemoteConfig-5894aa4820a265ae.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseRemoteConfig-bb5ba29a5f73cd24.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseRemoteConfig-3e803b148769baed.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/RemoteConfig-7e9635365ccd4a17.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/RemoteConfig-e7928fcb6311c439.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/RemoteConfig-9ab1ca5f360a1780.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json index 8868dc94ecc..836e19050a3 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json @@ -44,6 +44,9 @@ "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseStorage-1fb496e024a9eb57.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseStorage-f55cadd62f44b14f.zip", "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseStorage-5a28ee1b2244be55.zip", + "12.0.0": "https://dl.google.com/dl/firebase/ios/carthage/12.0.0/FirebaseStorage-21ed034a4fa51f2a.zip", + "12.1.0": "https://dl.google.com/dl/firebase/ios/carthage/12.1.0/FirebaseStorage-faeffdccd0d44a7c.zip", + "12.2.0": "https://dl.google.com/dl/firebase/ios/carthage/12.2.0/FirebaseStorage-20489713b94790a0.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Storage-6b3e77e1a7fdbc61.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Storage-4721c35d2b90a569.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Storage-821299369b9d0fb2.zip", diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index 4e63c277a8f..4b2b361b069 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: "12.0.0", + version: "12.3.0", pods: [ Pod("FirebaseSharedSwift"), Pod("FirebaseCoreInternal"), diff --git a/ReleaseTooling/Sources/FirebaseReleaser/InitializeRelease.swift b/ReleaseTooling/Sources/FirebaseReleaser/InitializeRelease.swift index 3e5c5190212..4674a765e55 100644 --- a/ReleaseTooling/Sources/FirebaseReleaser/InitializeRelease.swift +++ b/ReleaseTooling/Sources/FirebaseReleaser/InitializeRelease.swift @@ -50,29 +50,12 @@ enum InitializeRelease { updateFirebasePodspec(path: path, manifest: manifest) } else { updatePodspecVersion(pod: pod, version: version, path: path) - - // Pods dependencies to update to latest. - if pod.name.hasPrefix("GoogleAppMeasurement") || - pod.name == "FirebaseCore" || - pod.name == "FirebaseCoreExtension" || - pod.name == "FirebaseCoreInternal" || - pod.name == "FirebaseFirestoreInternal" || - pod.name == "FirebaseAI" { - updateDependenciesToLatest( - dependency: pod.name, - pods: manifest.pods, - version: version, - path: path - ) - } else if version.hasSuffix(".0.0") { - let patchlessVersion = String(version[.. String { + static func frameworkBuildName(_ framework: String) -> String { switch framework { case "abseil": return "absl" diff --git a/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift index dc781d6a0d4..a1ccd32243e 100755 --- a/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/ModuleMapBuilder.swift @@ -248,6 +248,11 @@ struct ModuleMapBuilder { installedPods[name]?.transitiveFrameworks = transitiveFrameworkDeps installedPods[name]?.transitiveLibraries = transitiveLibraryDeps - return ModuleMapContents(module: name, frameworks: myFrameworkDeps, libraries: myLibraryDeps) + let moduleName = FrameworkBuilder.frameworkBuildName(name) + return ModuleMapContents( + module: moduleName, + frameworks: myFrameworkDeps, + libraries: myLibraryDeps + ) } } diff --git a/ReleaseTooling/Sources/ZipBuilder/main.swift b/ReleaseTooling/Sources/ZipBuilder/main.swift index 4a56842b425..cc19e3fafe6 100644 --- a/ReleaseTooling/Sources/ZipBuilder/main.swift +++ b/ReleaseTooling/Sources/ZipBuilder/main.swift @@ -100,7 +100,7 @@ struct ZipBuilderTool: ParsableCommand { // MARK: - Platform Arguments /// The minimum iOS Version to build for. - @Option(default: "12.0", help: ArgumentHelp("The minimum supported iOS version.")) + @Option(default: "15.0", help: ArgumentHelp("The minimum supported iOS version.")) var minimumIOSVersion: String /// The minimum macOS Version to build for. @@ -108,7 +108,7 @@ struct ZipBuilderTool: ParsableCommand { var minimumMacOSVersion: String /// The minimum tvOS Version to build for. - @Option(default: "13.0", help: ArgumentHelp("The minimum supported tvOS version.")) + @Option(default: "15.0", help: ArgumentHelp("The minimum supported tvOS version.")) var minimumTVOSVersion: String /// The minimum watchOS Version to build for. diff --git a/ReleaseTooling/Template/README.md b/ReleaseTooling/Template/README.md index 2b02fa7177a..495301380e2 100644 --- a/ReleaseTooling/Template/README.md +++ b/ReleaseTooling/Template/README.md @@ -67,13 +67,26 @@ To integrate a Firebase SDK with your app: c. Double-click the setting, click the '+' button, and add `-lc++` -10. Drag the `Firebase.h` header in this directory into your project. This will +10. If you're using Firebase Analytics, disable + GoogleAdsOnDeviceConversion.xcframework for Mac Catalyst: + + a. In your project settings, open the **Settings** panel for your target. + + b. Go to the Build Phases tab and find the + **GoogleAdsOnDeviceConversion.xcframework** setting in the **Link Binary + With Libraries** section. + + c. Click on the filter icon button in the + **GoogleAdsOnDeviceConversion.xcframework** row and deselect the Mac Catalyst + checkbox. + +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. @@ -81,7 +94,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 diff --git a/scripts/build.sh b/scripts/build.sh index 80cc79f0bb5..2be500a8bbc 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -187,8 +187,11 @@ tvos_flags=( -destination 'platform=tvOS Simulator,name=Apple TV' ) visionos_flags=( + # As of Aug 15, 2025, the default OS "latest" was failing as it matched both + # the visionOS 26 beta and visionOS 2.5 (from Xcode 16.4) simulators; + # explicitly specifying OS=2.5 in destination as a workaround. -sdk 'xrsimulator' - -destination 'platform=visionOS Simulator,name=Apple Vision Pro' + -destination 'platform=visionOS Simulator,OS=2.5,name=Apple Vision Pro' ) catalyst_flags=( ARCHS=x86_64 VALID_ARCHS=x86_64 SUPPORTS_MACCATALYST=YES -sdk macosx @@ -489,12 +492,10 @@ case "$product-$platform-$method" in ../../../FirebaseRemoteConfig/Tests/Swift/AccessToken.json # Integration tests are only run on iOS to minimize flake failures. - # TODO(ncooke3): Remove -sdk and -destination flags and replace with "${xcb_flags[@]}" RunXcodebuild \ -workspace 'gen/FirebaseRemoteConfig/FirebaseRemoteConfig.xcworkspace' \ -scheme "FirebaseRemoteConfig-Unit-swift-api-tests" \ - -sdk 'iphonesimulator' \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.3.1' \ + "${xcb_flags[@]}" \ build \ test ;; @@ -521,6 +522,8 @@ case "$product-$platform-$method" in -scheme "FirebaseAITestApp-SPM" \ "${xcb_flags[@]}" \ -parallel-testing-enabled NO \ + -retry-tests-on-failure \ + -test-iterations 3 \ test ;; @@ -570,12 +573,10 @@ case "$product-$platform-$method" in if check_secrets; then # Integration tests are only run on iOS to minimize flake failures. - # TODO(ncooke3): Add back "${ios_flags[@]}". See #14657. RunXcodebuild \ -workspace 'gen/FirebaseStorage/FirebaseStorage.xcworkspace' \ -scheme "FirebaseStorage-Unit-integration" \ - -sdk 'iphonesimulator' \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.3.1' \ + "${ios_flags[@]}" \ "${xcb_flags[@]}" \ test fi @@ -591,12 +592,10 @@ case "$product-$platform-$method" in if check_secrets; then # Integration tests are only run on iOS to minimize flake failures. - # TODO(ncooke3): Add back "${ios_flags[@]}". See #14657. RunXcodebuild \ -workspace 'gen/FirebaseStorage/FirebaseStorage.xcworkspace' \ -scheme "FirebaseStorage-Unit-ObjCIntegration" \ - -sdk 'iphonesimulator' \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.3.1' \ + "${ios_flags[@]}" \ "${xcb_flags[@]}" \ test fi @@ -615,8 +614,8 @@ case "$product-$platform-$method" in RunXcodebuild \ -workspace 'gen/FirebaseCombineSwift/FirebaseCombineSwift.xcworkspace' \ -scheme "FirebaseCombineSwift-Unit-integration" \ - -sdk 'iphonesimulator' \ - -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.3.1' \ + "${ios_flags[@]}" \ + "${xcb_flags[@]}" \ test fi ;; diff --git a/scripts/gha-encrypted/qs-auth.plist.gpg b/scripts/gha-encrypted/qs-authentication.plist.gpg similarity index 100% rename from scripts/gha-encrypted/qs-auth.plist.gpg rename to scripts/gha-encrypted/qs-authentication.plist.gpg diff --git a/scripts/setup_check.sh b/scripts/setup_check.sh index 805596a9ae6..faf4715c688 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@20 +brew install clang-format@21 # mint installs tools from Mintfile on demand. brew install mint diff --git a/scripts/style.sh b/scripts/style.sh index 72a31312d72..c8c15f5155a 100755 --- a/scripts/style.sh +++ b/scripts/style.sh @@ -56,7 +56,7 @@ version="${version/ (*)/}" version="${version/.*/}" case "$version" in - 20) + 21) ;; 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 20." + echo "Please upgrade to clang-format version 21." echo "If it's installed via homebrew you can run:" echo "brew upgrade clang-format" exit 1