diff --git a/.github/workflows/_spm.yml b/.github/workflows/_spm.yml index d4c8bf96c43..01f84ff11b2 100644 --- a/.github/workflows/_spm.yml +++ b/.github/workflows/_spm.yml @@ -58,6 +58,14 @@ on: required: false default: false + # Custom environment variables to inject into the jobs. + # Expected to be a JSON-formatted string. + # Example: '{"FIREBASE_APP_CHECK_BRANCH": "nc/target-split"}' + env_vars: + type: string + required: false + default: "{}" + outputs: cache_key: description: "The cache key for the Swift package resolution." @@ -72,6 +80,24 @@ jobs: cache_key: ${{ steps.generate_cache_key.outputs.cache_key }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Set Custom Environment Variables + run: | + python3 -c ' + import os, json + try: + env_vars = json.loads(os.environ.get("CUSTOM_ENV_VARS", "{}")) + if not isinstance(env_vars, dict): + raise ValueError("env_vars must be a JSON object") + with open(os.environ["GITHUB_ENV"], "a") as f: + for k, v in env_vars.items(): + f.write(f"{k}={v}\n") + except json.JSONDecodeError: + print("Warning: env_vars is not valid JSON. Skipping.") + except Exception as e: + print(f"Error setting env vars: {e}") + ' + env: + CUSTOM_ENV_VARS: ${{ inputs.env_vars }} - name: Xcode run: sudo xcode-select -s /Applications/Xcode_26.4.app/Contents/Developer - name: Generate Swift Package.resolved @@ -110,6 +136,24 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false + - name: Set Custom Environment Variables + run: | + python3 -c ' + import os, json + try: + env_vars = json.loads(os.environ.get("CUSTOM_ENV_VARS", "{}")) + if not isinstance(env_vars, dict): + raise ValueError("env_vars must be a JSON object") + with open(os.environ["GITHUB_ENV"], "a") as f: + for k, v in env_vars.items(): + f.write(f"{k}={v}\n") + except json.JSONDecodeError: + print("Warning: env_vars is not valid JSON. Skipping.") + except Exception as e: + print(f"Error setting env vars: {e}") + ' + env: + CUSTOM_ENV_VARS: ${{ inputs.env_vars }} - uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: .build diff --git a/.github/workflows/sdk.appcheck.yml b/.github/workflows/sdk.appcheck.yml index c47ff92a2c0..f2ac1424a40 100644 --- a/.github/workflows/sdk.appcheck.yml +++ b/.github/workflows/sdk.appcheck.yml @@ -30,6 +30,8 @@ jobs: uses: ./.github/workflows/_spm.yml with: target: ${{ matrix.target }} + env_vars: '{"FIREBASE_APP_CHECK_BRANCH": "nc/target-split"}' + catalyst: uses: ./.github/workflows/_catalyst.yml diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 5877abc1846..82e04f4d1f1 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -44,7 +44,7 @@ Pod::Spec.new do |s| s.osx.weak_framework = 'DeviceCheck' s.tvos.weak_framework = 'DeviceCheck' - s.dependency 'AppCheckCore', '~> 11.0' + s.dependency 'AppCheckCore', '~> 11.3' s.dependency 'FirebaseAppCheckInterop', '~> 12.15.0' s.dependency 'FirebaseCore', '~> 12.15.0' s.dependency 'GoogleUtilities/Environment', '~> 8.1' diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md new file mode 100644 index 00000000000..6d7f7fc272e --- /dev/null +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/E2E_TESTING.md @@ -0,0 +1,182 @@ +# E2E Testing with FIRAppCheckTestApp + +This document provides information on how to configure and run End-to-End (E2E) +tests for App Check providers using this sample app. + +## Configurability + +The app's behavior can be configured using environment variables passed during +test execution. + +### Environment Variables + +Starting with Xcode 13, you can pass environment variables directly to the +test runner by prefixing them with `TEST_RUNNER_`. The prefix is stripped when +it reaches the test process. + +- **`TEST_RUNNER_RECAPTCHA_SITE_KEY`**: The reCAPTCHA site key used + by the `AppCheckRecaptchaProvider`. + - **Access in Code**: Read via + `ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"]`. +- **`TEST_RUNNER_APP_CHECK_PROVIDER`**: Specifies which App Check provider + factory to use. + - **Supported Values**: `recaptcha` (default), `debug`. + - **Access in Code**: Read via + `ProcessInfo.processInfo.environment["APP_CHECK_PROVIDER"]`. + +### Manual Override + +For local debugging and manual testing, you can override the environment +variables by setting `manualProviderOverride` in `AppDelegate.swift`: + +```swift +let manualProviderOverride: String? = "debug" +``` + +## Running Tests + +The commands below should be run from the **repository root**. + +### Prerequisites +- Ensure you have a local checkout of the `app-check` repository if you are + developing it locally. Set `FIREBASE_APP_CHECK_LOCAL_PATH` to point to it. + +### Sample Commands + +#### Run tests with reCAPTCHA provider + +```bash +export TEST_RUNNER_RECAPTCHA_SITE_KEY="your_site_key_here" +export TEST_RUNNER_APP_CHECK_PROVIDER="recaptcha" +export FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" +SIM_ID=$(xcrun simctl list devices available | grep "iPhone" | grep -E -o '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -n 1) + +xcodebuild test \ + -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ + -scheme FIRAppCheckTestApp \ + -destination "platform=iOS Simulator,id=$SIM_ID" +``` + +#### Run tests with Debug provider + +```bash +export TEST_RUNNER_APP_CHECK_PROVIDER="debug" +export FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" +SIM_ID=$(xcrun simctl list devices available | grep "iPhone" | grep -E -o '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -n 1) + +xcodebuild test \ + -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ + -scheme FIRAppCheckTestApp \ + -destination "platform=iOS Simulator,id=$SIM_ID" +``` +*Note: The Debug provider might require you to register the generated debug token in the Firebase Console for the tests to pass if they interact with live services.* + +### Running and Testing in Xcode + +If you prefer to use the Xcode UI instead of `xcodebuild`, follow these steps +to configure the environment: + +#### 1. Resolve Local Dependency +If you are using a local checkout of the `app-check` repository, Xcode must be +launched from the terminal with the `FIREBASE_APP_CHECK_LOCAL_PATH` environment +variable set so that Swift Package Manager can resolve it correctly. + +Run the following command from the repository root: +```bash +open --env FIREBASE_APP_CHECK_LOCAL_PATH=/path/to/your/local/app-check FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace +``` + +#### 2. Configure Provider and Site Key +You have two options to configure the provider when running or testing in Xcode: + +**Option A: Via Manual Override in Code (Easiest for Running the App)** +If you just want to quickly run the app with a specific provider without +changing scheme settings: +1. Open `AppDelegate.swift`. +2. Locate `manualProviderOverride` in `application(_:didFinishLaunchingWithOptions:)`. +3. Set it to your desired provider: + ```swift + let manualProviderOverride: String? = "recaptcha" + ``` + *Note: Remember to revert this change before committing.* + +**Option B: Via Xcode Scheme (Recommended for Tests)** +This avoids modifying code and works for both running and testing. +1. In Xcode, go to **Product > Scheme > Edit Scheme...** (or press `⌘<`). +2. Select the **Run** or **Test** action in the left sidebar, depending on + what you are doing. +3. Go to the **Arguments** tab. +4. In the **Environment Variables** section, add: + * `APP_CHECK_PROVIDER`: Set to `recaptcha` or `debug`. + * `RECAPTCHA_SITE_KEY`: Set to your reCAPTCHA site key (required for + `recaptcha`). + +### Running and Testing with CocoaPods + +If you prefer to use the CocoaPods workflow instead of SPM: + +#### 0. Clean Up State (Optional but Recommended) +If you are switching from the SPM workflow or encounter issues, it is +recommended to clean up the CocoaPods state first: +```bash +pod deintegrate FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj +rm -rf FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace +rm -f FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile.lock +``` + +#### 1. Install Dependencies +To ensure a clean update and avoid conflicts with local development paths or +stale state, it is recommended to remove the existing `Pods` directory and +`Podfile.lock` before updating. + +Run the following command from the repository root: +```bash +rm -rf FirebaseAppCheck/Apps/FIRAppCheckTestApp/Pods +rm -f FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile.lock +FIREBASE_APP_CHECK_LOCAL_PATH="/path/to/your/local/app-check" pod update --repo-update --project-directory=FirebaseAppCheck/Apps/FIRAppCheckTestApp/ +``` + +#### 2. Open Workspace +Open the generated CocoaPods workspace instead of the project file: +```bash +open FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace +``` + +#### 3. Remove SPM Dependencies (If needed) +By default, the project file is configured for SPM. To avoid duplicate symbol +issues or conflicting resolutions when using CocoaPods: +1. In Xcode, select the project in the file navigator. +2. Select the project file at the top (not a target). +3. Go to the **Package Dependencies** tab. +4. Remove the `firebase-ios-sdk` or `app-check` package references if they + appear there. +5. Also, select the `FIRAppCheckTestApp` target, go to the **General** tab, + and scroll down to **Frameworks, Libraries, and Embedded Content**. +6. Remove any SPM-resolved frameworks from this list. + +#### 4. Configure and Run +You can configure the provider and site key either via the Xcode Scheme or by +passing environment variables to `xcodebuild`. + +**Via Xcode Scheme:** +Follow the instructions in **[Running and Testing in Xcode](#running-and-testing-in-xcode)**. + +**Via `xcodebuild` (Command Line):** +Run the following command from the repository root, replacing the site key with +your own: +```bash +export TEST_RUNNER_RECAPTCHA_SITE_KEY="your_site_key_here" +export TEST_RUNNER_APP_CHECK_PROVIDER="recaptcha" +SIM_ID=$(xcrun simctl list devices available | grep "iPhone" | grep -E -o '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -n 1) + +xcodebuild test \ + -workspace FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcworkspace \ + -scheme FIRAppCheckTestApp \ + -destination "platform=iOS Simulator,id=$SIM_ID" +``` +*(Note: See [Running Tests](#running-tests) for how to dynamically find a valid +simulator destination).* + +## Project Structure + +- **`FIRAppCheckTestAppTests`**: A hosted unit test target containing the test cases. It runs inside the app process to have access to the full app context. diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj index 4e71530ef0d..858db9c0aac 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/project.pbxproj @@ -7,45 +7,93 @@ objects = { /* Begin PBXBuildFile section */ + + 8F1202DD209F881D67D20BA8 /* FIRAppCheckTestAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DBE09F2A53CEC2B3E7600F4 /* FIRAppCheckTestAppTests.swift */; }; 9AC7C27C2541C7E500F5DD80 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC7C27B2541C7E500F5DD80 /* AppDelegate.swift */; }; 9AC7C27E2541C7E500F5DD80 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC7C27D2541C7E500F5DD80 /* SceneDelegate.swift */; }; 9AC7C2852541C7E600F5DD80 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9AC7C2842541C7E600F5DD80 /* Assets.xcassets */; }; 9AC7C2882541C7E600F5DD80 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9AC7C2862541C7E600F5DD80 /* LaunchScreen.storyboard */; }; - EAD122EF2DB97E10004D64C9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EAD122EE2DB97E10004D64C9 /* GoogleService-Info.plist */; }; + CB1E711C5CFEE1D2BFB9069D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5A8784D39A5706A47E89D31 /* Foundation.framework */; }; + EAA2C1AB2FAA966B008D663E /* RecaptchaEnterprise in Frameworks */ = {isa = PBXBuildFile; productRef = EAA2C1AA2FAA966B008D663E /* RecaptchaEnterprise */; }; + EAA2C1AD2FABCB69008D663E /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = EAA2C1AC2FABCB69008D663E /* FirebaseStorage */; }; + EABC1CB82FA9257600C35F73 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EABC1CB72FA9257600C35F73 /* GoogleService-Info.plist */; }; EAD122F12DB98BD0004D64C9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD122F02DB98BCC004D64C9 /* ContentView.swift */; }; EAD122F32DB9920D004D64C9 /* AppCheckTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD122F22DB99206004D64C9 /* AppCheckTestApp.swift */; }; EAD122F62DB9940E004D64C9 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = EAD122F52DB9940E004D64C9 /* FirebaseAppCheck */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 6394C6DF77B30A8569910AC4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9AC7C2702541C7E500F5DD80 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9AC7C2772541C7E500F5DD80; + remoteInfo = FIRAppCheckTestApp; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 2F8964B09A6746481927ACD1 /* FIRAppCheckTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FIRAppCheckTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9AC7C2782541C7E500F5DD80 /* FIRAppCheckTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FIRAppCheckTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9AC7C27B2541C7E500F5DD80 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9AC7C27D2541C7E500F5DD80 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 9AC7C2842541C7E600F5DD80 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9AC7C2872541C7E600F5DD80 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 9AC7C2892541C7E600F5DD80 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - EAD122EE2DB97E10004D64C9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 9DBE09F2A53CEC2B3E7600F4 /* FIRAppCheckTestAppTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FIRAppCheckTestAppTests.swift; path = FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift; sourceTree = ""; }; + A5A8784D39A5706A47E89D31 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + + EABC1CB72FA9257600C35F73 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; EAD122F02DB98BCC004D64C9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; EAD122F22DB99206004D64C9 /* AppCheckTestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCheckTestApp.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2D060508510F1138A9FC12DE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CB1E711C5CFEE1D2BFB9069D /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9AC7C2752541C7E500F5DD80 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( EAD122F62DB9940E004D64C9 /* FirebaseAppCheck in Frameworks */, + EAA2C1AB2FAA966B008D663E /* RecaptchaEnterprise in Frameworks */, + EAA2C1AD2FABCB69008D663E /* FirebaseStorage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4ABD2FB516E7A91E6E1527EA /* iOS */ = { + isa = PBXGroup; + children = ( + A5A8784D39A5706A47E89D31 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 671F07FAFFBA7B1FCBCABDBD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4ABD2FB516E7A91E6E1527EA /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; 9AC7C26F2541C7E500F5DD80 = { isa = PBXGroup; children = ( 9AC7C27A2541C7E500F5DD80 /* FIRAppCheckTestApp */, 9AC7C2792541C7E500F5DD80 /* Products */, + 671F07FAFFBA7B1FCBCABDBD /* Frameworks */, + 9DBE09F2A53CEC2B3E7600F4 /* FIRAppCheckTestAppTests.swift */, + ); sourceTree = ""; }; @@ -53,6 +101,7 @@ isa = PBXGroup; children = ( 9AC7C2782541C7E500F5DD80 /* FIRAppCheckTestApp.app */, + 2F8964B09A6746481927ACD1 /* FIRAppCheckTestAppTests.xctest */, ); name = Products; sourceTree = ""; @@ -60,7 +109,7 @@ 9AC7C27A2541C7E500F5DD80 /* FIRAppCheckTestApp */ = { isa = PBXGroup; children = ( - EAD122EE2DB97E10004D64C9 /* GoogleService-Info.plist */, + EABC1CB72FA9257600C35F73 /* GoogleService-Info.plist */, 9AC7C27B2541C7E500F5DD80 /* AppDelegate.swift */, 9AC7C27D2541C7E500F5DD80 /* SceneDelegate.swift */, EAD122F02DB98BCC004D64C9 /* ContentView.swift */, @@ -79,6 +128,7 @@ isa = PBXNativeTarget; buildConfigurationList = 9AC7C2A22541C7E600F5DD80 /* Build configuration list for PBXNativeTarget "FIRAppCheckTestApp" */; buildPhases = ( + 9AC7C2742541C7E500F5DD80 /* Sources */, 9AC7C2752541C7E500F5DD80 /* Frameworks */, 9AC7C2762541C7E500F5DD80 /* Resources */, @@ -92,6 +142,24 @@ productReference = 9AC7C2782541C7E500F5DD80 /* FIRAppCheckTestApp.app */; productType = "com.apple.product-type.application"; }; + DD8474378DDE4504369D6D2A /* FIRAppCheckTestAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3829FC5F4FEBE88FE48726F9 /* Build configuration list for PBXNativeTarget "FIRAppCheckTestAppTests" */; + buildPhases = ( + DF4B7F4A1910F5A1E4CA3F06 /* Sources */, + 2D060508510F1138A9FC12DE /* Frameworks */, + FD0227B5A3BEC9A7E811417E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 06C131FB9DE2B06EDC5A605D /* PBXTargetDependency */, + ); + name = FIRAppCheckTestAppTests; + productName = FIRAppCheckTestAppTests; + productReference = 2F8964B09A6746481927ACD1 /* FIRAppCheckTestAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -116,13 +184,15 @@ ); mainGroup = 9AC7C26F2541C7E500F5DD80; packageReferences = ( - EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "../../../../firebase-ios-sdk" */, + EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "firebase-ios-sdk" */, + EAA2C1A92FAA966B008D663E /* XCRemoteSwiftPackageReference "recaptcha-enterprise-mobile-sdk" */, ); productRefGroup = 9AC7C2792541C7E500F5DD80 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 9AC7C2772541C7E500F5DD80 /* FIRAppCheckTestApp */, + DD8474378DDE4504369D6D2A /* FIRAppCheckTestAppTests */, ); }; /* End PBXProject section */ @@ -133,13 +203,25 @@ buildActionMask = 2147483647; files = ( 9AC7C2882541C7E600F5DD80 /* LaunchScreen.storyboard in Resources */, - EAD122EF2DB97E10004D64C9 /* GoogleService-Info.plist in Resources */, 9AC7C2852541C7E600F5DD80 /* Assets.xcassets in Resources */, + EABC1CB82FA9257600C35F73 /* GoogleService-Info.plist in Resources */, + + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FD0227B5A3BEC9A7E811417E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 9AC7C2742541C7E500F5DD80 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -152,8 +234,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DF4B7F4A1910F5A1E4CA3F06 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8F1202DD209F881D67D20BA8 /* FIRAppCheckTestAppTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 06C131FB9DE2B06EDC5A605D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FIRAppCheckTestApp; + target = 9AC7C2772541C7E500F5DD80 /* FIRAppCheckTestApp */; + targetProxy = 6394C6DF77B30A8569910AC4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 9AC7C2862541C7E600F5DD80 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -290,6 +389,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = EQHXZ8M8AV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; INFOPLIST_FILE = FIRAppCheckTestApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -298,6 +398,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Firebase App Check Dev"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -311,6 +412,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = EQHXZ8M8AV; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; INFOPLIST_FILE = FIRAppCheckTestApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -319,14 +421,71 @@ PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Firebase App Check Dev"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; + B539FF780C152B469B6CBAD9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; + PRODUCT_NAME = FIRAppCheckTestAppTests; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FIRAppCheckTestApp.app/FIRAppCheckTestApp"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + F6B3E8BA0B5DC042AD569583 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.appcheck.testapp.dev; + PRODUCT_NAME = FIRAppCheckTestAppTests; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Firebase App Check Dev"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FIRAppCheckTestApp.app/FIRAppCheckTestApp"; + VALIDATE_PRODUCT = YES; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3829FC5F4FEBE88FE48726F9 /* Build configuration list for PBXNativeTarget "FIRAppCheckTestAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F6B3E8BA0B5DC042AD569583 /* Release */, + B539FF780C152B469B6CBAD9 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9AC7C2732541C7E500F5DD80 /* Build configuration list for PBXProject "FIRAppCheckTestApp" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -348,13 +507,34 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "../../../../firebase-ios-sdk" */ = { + EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCLocalSwiftPackageReference; relativePath = "../../../../firebase-ios-sdk"; }; /* End XCLocalSwiftPackageReference section */ +/* Begin XCRemoteSwiftPackageReference section */ + EAA2C1A92FAA966B008D663E /* XCRemoteSwiftPackageReference "recaptcha-enterprise-mobile-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 18.9.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ + EAA2C1AA2FAA966B008D663E /* RecaptchaEnterprise */ = { + isa = XCSwiftPackageProductDependency; + package = EAA2C1A92FAA966B008D663E /* XCRemoteSwiftPackageReference "recaptcha-enterprise-mobile-sdk" */; + productName = RecaptchaEnterprise; + }; + EAA2C1AC2FABCB69008D663E /* FirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + package = EAD122F42DB9940E004D64C9 /* XCLocalSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseStorage; + }; EAD122F52DB9940E004D64C9 /* FirebaseAppCheck */ = { isa = XCSwiftPackageProductDependency; productName = FirebaseAppCheck; diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme index 3c69bd4673b..70885fda1a9 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp.xcodeproj/xcshareddata/xcschemes/FIRAppCheckTestApp.xcscheme @@ -27,12 +27,13 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index d9b7a3ca49f..73f3bb01b6b 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -16,29 +16,74 @@ import UIKit +import AppCheckCore import FirebaseAppCheck import FirebaseCore +import FirebaseStorage class AppDelegate: UIResponder, UIApplicationDelegate { + private(set) static var shared: AppDelegate? + + override init() { + super.init() + AppDelegate.shared = self + } + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication .LaunchOptionsKey: Any]?) -> Bool { - let providerFactory = AppCheckDebugProviderFactory() - AppCheck.setAppCheckProviderFactory(providerFactory) + // Manual override for testing/debugging. + // Change this to explicitly set a provider, or leave nil to use environment variable. + let manualProviderOverride: String? = nil // e.g., "debug" or "recaptcha" - FirebaseApp.configure() + let options = setupAppCheck(overrideProvider: manualProviderOverride) - requestLimitedUseToken() + FirebaseApp.configure(options: options) - requestDeviceCheckToken() + return true + } - requestDebugToken() + private func setupAppCheck(overrideProvider: String?) -> FirebaseOptions { + // Note: If running via `xcodebuild test`, pass this with the `TEST_RUNNER_` prefix + // (e.g., `TEST_RUNNER_APP_CHECK_PROVIDER="debug"`). Xcode strips the prefix at runtime. + let providerType = overrideProvider ?? ProcessInfo.processInfo + .environment["APP_CHECK_PROVIDER"] ?? "debug" - if #available(iOS 14.0, *) { - requestAppAttestToken() + if overrideProvider == nil && ProcessInfo.processInfo.environment["APP_CHECK_PROVIDER"] == nil { + print("⚠️ Warning: APP_CHECK_PROVIDER environment variable is missing. Defaulting to 'debug'.") } - return true + print("Info: Using App Check provider: '\(providerType)'") + + guard let options = FirebaseOptions.defaultOptions() else { + fatalError( + "Failed to load default Firebase options. Ensure GoogleService-Info.plist is added to the project." + ) + } + + let providerFactory: AppCheckProviderFactory + switch providerType { + case "recaptcha": + guard let siteKey = ProcessInfo.processInfo.environment["RECAPTCHA_SITE_KEY"], + !siteKey.isEmpty else { + fatalError( + "Error: RECAPTCHA_SITE_KEY environment variable is missing or empty. E2E tests require this key." + ) + } + options.recaptchaSiteKey = siteKey + providerFactory = RecaptchaProviderFactory() + case "debug": + providerFactory = AppCheckDebugProviderFactory() + default: + print( + "Warning: Unknown APP_CHECK_PROVIDER '\(providerType)'. Falling back to Debug provider." + ) + providerFactory = AppCheckDebugProviderFactory() + } + + AppCheck.setAppCheckProviderFactory(providerFactory) + + return options } // MARK: UISceneSession Lifecycle @@ -70,12 +115,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } - DeviceCheckProvider(app: firebaseApp)?.getToken { token, error in - if let token { - print("DeviceCheck token: \(token.token), expiration date: \(token.expirationDate)") - } - - if let error { + Task { + do { + if let provider = DeviceCheckProvider(app: firebaseApp) { + let token = try await provider.getToken() + print("DeviceCheck token: \(token.token), expiration date: \(token.expirationDate)") + } + } catch { print("DeviceCheck error: \((error as NSError).userInfo)") } } @@ -89,12 +135,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if let debugProvider = AppCheckDebugProvider(app: firebaseApp) { print("Debug token: \(debugProvider.currentDebugToken())") - debugProvider.getToken { token, error in - if let token { + Task { + do { + let token = try await debugProvider.getToken() print("Debug FAC token: \(token.token), expiration date: \(token.expirationDate)") - } - - if let error { + } catch { print("Debug error: \(error)") } } @@ -103,19 +148,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: App Check API - func requestLimitedUseToken() { - AppCheck.appCheck().limitedUseToken { result, error in - if let result { - print("FAC limited-use token: \(result.token), expiration date: \(result.expirationDate)") - } + @discardableResult + func fetchAppCheckToken(forcingRefresh: Bool = false) async throws -> AppCheckToken { + let token = try await AppCheck.appCheck().token(forcingRefresh: forcingRefresh) - if let error { - print("Error: \(String(describing: error))") - } + let ttl = token.expirationDate.timeIntervalSinceNow + print("[NON-LIMITED USE] Token: \(token.token)") + print(" - Expiration date: \(token.expirationDate)") + print(" - TTL: \(Int(ttl)) seconds") + + try await readFromStorage() + + return token + } + + func readFromStorage() async throws { + print("Attempting to read from Cloud Storage...") + let storage = Storage.storage() + let storageRef = storage.reference() + // NOTE: This path corresponds to the security rules configured for the test project. + // The rules allow public read on '/cep/ping'. If these rules change, this test may fail. + let pingRef = storageRef.child("cep/ping") + + let data = try await pingRef.data(maxSize: 1 * 1024 * 1024) + + // This shouldn't be possible, but we want to know if it ever happens. + guard let string = String(data: data, encoding: .utf8) else { + fatalError( + "Unexpected state: data is not valid UTF-8. This shouldn't happen, but we want to know if it does." + ) } + + print("Storage content: \(string)") + } + + func requestLimitedUseToken() async throws -> String { + let result = try await AppCheck.appCheck().limitedUseToken() + print("[LIMITED USE] Token: \(result.token)") + print(" - Expiration date: \(result.expirationDate)") + return result.token } - @available(iOS 14.0, *) func requestAppAttestToken() { guard let firebaseApp = FirebaseApp.app() else { return @@ -126,12 +199,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } - appAttestProvider.getToken { token, error in - if let token { + Task { + do { + let token = try await appAttestProvider.getToken() print("App Attest FAC token: \(token.token), expiration date: \(token.expirationDate)") - } - - if let error { + } catch { print("App Attest error: \(error)") } } diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift new file mode 100644 index 00000000000..b0d2073b726 --- /dev/null +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestAppTests/FIRAppCheckTestAppTests.swift @@ -0,0 +1,52 @@ +// Copyright 2026 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 AppCheckCore +@testable import FIRAppCheckTestApp +import FirebaseAppCheck +import XCTest + +final class FIRAppCheckTestAppTests: XCTestCase { + var appDelegate: AppDelegate! + + @MainActor + override func setUp() async throws { + try await super.setUp() + appDelegate = try XCTUnwrap(AppDelegate.shared, "AppDelegate.shared is nil") + } + + func testTokenAcquisitionAndStorageAccess() async throws { + let token = try await appDelegate.fetchAppCheckToken() + XCTAssertGreaterThan(token.expirationDate, Date(), "Token should not be expired") + } + + func testLimitedUseTokenAcquisition() async throws { + let token = try await appDelegate.requestLimitedUseToken() + XCTAssertFalse(token.isEmpty, "Limited-use token should not be empty") + } + + func testCacheWorks() async throws { + let token1 = try await appDelegate.fetchAppCheckToken().token + let token2 = try await appDelegate.fetchAppCheckToken().token + + XCTAssertEqual(token1, token2, "Tokens should be identical (cached)") + } + + func testForceRefresh() async throws { + let token1 = try await appDelegate.fetchAppCheckToken().token + let token2 = try await appDelegate.fetchAppCheckToken(forcingRefresh: true).token + + XCTAssertNotEqual(token1, token2, "Tokens should be different after forced refresh") + } +} diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile index 4ba2cf4fe4c..9cecb04b6d7 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/Podfile @@ -11,4 +11,17 @@ target 'FIRAppCheckTestApp' do pod 'FirebaseAppCheck', :path => '../../../' pod 'FirebaseCore', :path => '../../../' + # FirebaseStorage is used for testing AppCheck protected access. + pod 'FirebaseStorage', :path => '../../../' + # RecaptchaEnterprise is needed for Firebase App Check reCAPTCHA provider. + pod 'RecaptchaEnterprise' + + # Use local AppCheckCore if the environment variable is set + if ENV['FIREBASE_APP_CHECK_LOCAL_PATH'] + pod 'AppCheckCore', :path => ENV['FIREBASE_APP_CHECK_LOCAL_PATH'] + end + + target 'FIRAppCheckTestAppTests' do + inherit! :search_paths + end end diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index 18297a91390..4fdc02bec39 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [added] Added reCAPTCHA provider. + # 12.14.0 - [added] Added `AppAttestProviderFactory` to simplify App Check setup when using the App Attest provider. (#16182) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h index 3f2fce59da5..bb2adb7f360 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h @@ -35,6 +35,9 @@ FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageCodeDebugToken; // FIRDeviceCheckProvider.m FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions; +// FIRRecaptchaProvider.m +FOUNDATION_EXPORT NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions; + void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...); GACAppCheckLogLevel FIRGetGACAppCheckLogLevel(void); diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m index 3c001b22326..83f3ca815f6 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.m @@ -35,6 +35,9 @@ // FIRDeviceCheckProvider.m NSString *const kFIRLoggerAppCheckMessageDeviceCheckProviderIncompleteFIROptions = @"I-FAA006001"; +// FIRRecaptchaProvider.m +NSString *const kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions = @"I-FAA007001"; + #pragma mark - Log functions void FIRAppCheckDebugLog(NSString *messageCode, NSString *message, ...) { diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h new file mode 100644 index 00000000000..92a1d3e409f --- /dev/null +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h @@ -0,0 +1,60 @@ +/* + * Copyright 2026 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 +#import "FIRAppCheckAvailability.h" +#import "FIRAppCheckProvider.h" + +@class FIRApp; + +NS_ASSUME_NONNULL_BEGIN + +/// App Check provider that verifies app integrity using +/// [reCAPTCHA Enterprise for iOS](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) +/// API. +NS_SWIFT_NAME(RecaptchaProvider) +@interface FIRRecaptchaProvider : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/// The default initializer. +/// @param app A `FirebaseApp` instance. +/// @return An instance of `RecaptchaProvider` if the provided +/// `FirebaseApp` instance contains all required parameters. +- (nullable instancetype)initWithApp:(FIRApp *)app; + +/* Jazzy doesn't generate documentation for protocol-inherited + * methods, so this is copied over from the protocol declaration. + */ +/// Returns a new Firebase App Check token. +/// @param handler The completion handler. Make sure to call the handler with either a token +/// or an error. +- (void)getTokenWithCompletion: + (void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error))handler + NS_SWIFT_NAME(getToken(completion:)); + +/// Returns a new Firebase App Check token. +/// When implementing this method for your custom provider, the token returned should be suitable +/// for consumption in a limited-use scenario. If you do not implement this method, the +/// getTokenWithCompletion will be invoked instead whenever a limited-use token is requested. +/// @param handler The completion handler. Make sure to call the handler with either a token +/// or an error. +- (void)getLimitedUseTokenWithCompletion: + (void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error))handler + NS_SWIFT_NAME(getLimitedUseToken(completion:)); + +@end +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h new file mode 100644 index 00000000000..9fc68b7d997 --- /dev/null +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h @@ -0,0 +1,31 @@ +/* + * Copyright 2026 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 +#import "FIRAppCheckProviderFactory.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An implementation of `AppCheckProviderFactory` that creates a new instance of +/// `AppCheckRecaptchaProvider` when requested. +NS_SWIFT_NAME(RecaptchaProviderFactory) +@interface FIRRecaptchaProviderFactory : NSObject + +/// Initializes a factory that will use the site key from Firebase app options. +- (instancetype)init; + +@end +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h index fbe5b9f8946..bfb29a5968e 100644 --- a/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h +++ b/FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FirebaseAppCheck.h @@ -31,3 +31,7 @@ // App Attest provider. #import "FIRAppAttestProvider.h" #import "FIRAppAttestProviderFactory.h" + +// Recaptcha provider +#import "FIRRecaptchaProvider.h" +#import "FIRRecaptchaProviderFactory.h" diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m new file mode 100644 index 00000000000..24c21115d5c --- /dev/null +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProvider.m @@ -0,0 +1,125 @@ +/* + * Copyright 2026 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 "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheckAvailability.h" + +#import + +#if SWIFT_PACKAGE +@import AppCheckRecaptchaProvider; +#elif __has_include() +#import +#elif __has_include("AppCheckCore-Swift.h") +// If frameworks are not available, fall back to importing the header as it +// should be findable from a header search path pointing to the build +// directory. See #12611 for more context. +#import "AppCheckCore-Swift.h" +#endif + +#import "FirebaseAppCheck/Sources/Core/FIRApp+AppCheck.h" + +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckToken+Internal.h" +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckValidator.h" +#import "FirebaseAppCheck/Sources/Core/FIRHeartbeatLogger+AppCheck.h" + +#import "FirebaseCore/Extension/FirebaseCoreInternal.h" + +@interface FIRRecaptchaProvider () + +@property(nonatomic, readonly) id recaptchaProvider; + +@end + +@implementation FIRRecaptchaProvider + +- (instancetype)initWithRecaptchaProvider:(id)recaptchaProvider { + self = [super init]; + if (self) { + _recaptchaProvider = recaptchaProvider; + } + return self; +} + +- (nullable instancetype)initWithApp:(FIRApp *)app { + NSString *siteKey = app.options.recaptchaSiteKey; + if (siteKey.length == 0) { + FIRLogError( + kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, + @"Cannot instantiate `%@` for app: %@. `FirebaseOptions.recaptchaSiteKey` is missing or " + @"empty. " + @"Please ensure you have added `RECAPTCHA_SITE_KEY` to your `GoogleService-Info.plist` " + @"or set `recaptchaSiteKey` on `FirebaseOptions` programmatically.", + NSStringFromClass([self class]), app.name); + return nil; + } + NSArray *missingOptionsFields = + [FIRAppCheckValidator tokenExchangeMissingFieldsInOptions:app.options]; + if (missingOptionsFields.count > 0) { + FIRLogError(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageRecaptchaProviderIncompleteFIROptions, + @"Cannot instantiate `%@` for app: %@. The following `FirebaseOptions` fields are " + @"missing: %@. " + @"Please ensure your `GoogleService-Info.plist` is complete or these fields are " + @"set on `FirebaseOptions` programmatically.", + NSStringFromClass([self class]), app.name, + [missingOptionsFields componentsJoinedByString:@", "]); + return nil; + } + + id heartbeatHook = [app.heartbeatLogger requestHook]; +#if TARGET_OS_IOS + GACRecaptchaProvider *recaptchaProvider = + [[GACRecaptchaProvider alloc] initWithSiteKey:siteKey + resourceName:app.resourceName + APIKey:app.options.APIKey + requestHooks:heartbeatHook ? @[ heartbeatHook ] : @[]]; + + return [self initWithRecaptchaProvider:recaptchaProvider]; +#else + return nil; +#endif +} + +#pragma mark - FIRAppCheckProvider + +- (void)getTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token, + NSError *_Nullable error))handler { + [self.recaptchaProvider getTokenWithCompletion:^(GACAppCheckToken *_Nullable internalToken, + NSError *_Nullable error) { + if (error) { + handler(nil, error); + return; + } + + handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); + }]; +} + +- (void)getLimitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable, + NSError *_Nullable))handler { + [self.recaptchaProvider getLimitedUseTokenWithCompletion:^( + GACAppCheckToken *_Nullable internalToken, NSError *_Nullable error) { + if (error) { + handler(nil, error); + return; + } + + handler([[FIRAppCheckToken alloc] initWithInternalToken:internalToken], nil); + }]; +} + +@end diff --git a/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m new file mode 100644 index 00000000000..273642adb37 --- /dev/null +++ b/FirebaseAppCheck/Sources/RecaptchaProvider/FIRRecaptchaProviderFactory.m @@ -0,0 +1,40 @@ +/* + * Copyright 2026 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 "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProviderFactory.h" + +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRAppCheck.h" +#import "FirebaseAppCheck/Sources/Public/FirebaseAppCheck/FIRRecaptchaProvider.h" + +#import "FirebaseAppCheck/Sources/Core/FIRAppCheckLogger.h" +#import "FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h" + +@interface FIRRecaptchaProviderFactory () + +@end + +@implementation FIRRecaptchaProviderFactory + +- (instancetype)init { + self = [super init]; + return self; +} + +- (nullable id)createProviderWithApp:(nonnull FIRApp *)app { + return [[FIRRecaptchaProvider alloc] initWithApp:app]; +} + +@end diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift new file mode 100644 index 00000000000..c4fc9588232 --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderFactoryTests.swift @@ -0,0 +1,47 @@ +// Copyright 2026 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 FirebaseAppCheck +import FirebaseCore +import XCTest + +final class RecaptchaProviderFactoryTests: XCTestCase { + override func setUp() { + super.setUp() + _ = registerMocksOnce + } + + func testCreateProviderWithApp_DefaultInit_UsesPlistSiteKey() throws { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + options.recaptchaSiteKey = "plist_site_key" + + let appName = "testCreateProviderWithApp_DefaultInit" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + let factory = RecaptchaProviderFactory() + + let createdProvider = factory.createProvider(with: app) + + XCTAssertTrue(createdProvider is RecaptchaProvider) + } +} diff --git a/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift new file mode 100644 index 00000000000..37c23037b4e --- /dev/null +++ b/FirebaseAppCheck/Tests/Unit/Swift/RecaptchaProviderTests.swift @@ -0,0 +1,259 @@ +// Copyright 2026 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 AppCheckCore +import AppCheckRecaptchaProvider +import FirebaseAppCheck +import FirebaseCore +import ObjectiveC +import RecaptchaInterop +import XCTest + +// These stub classes are needed to satisfy the reflection checks in +// AppCheckRecaptchaProvider.swift in the app-check repository. +// That class uses NSClassFromString to check if the Recaptcha Enterprise SDK +// is linked. By providing these stub classes with the expected Objective-C names +// using the runtime, we can run unit tests without crashing. + +final class StubRCAAction: NSObject, RCAActionProtocol { + static var login: RCAActionProtocol { fatalError("Not implemented") } + static var signup: RCAActionProtocol { fatalError("Not implemented") } + + var action: String + + required init(customAction: String) { + action = customAction + super.init() + } +} + +final class StubRCARecaptcha: NSObject, RCARecaptchaProtocol { + // Add a placeholder initializer to prevent inheriting init() from NSObject, + // which conflicts with the unavailable init in RCARecaptchaProtocol. + init(placeholder: Void) { + super.init() + } + + static func fetchClient(withSiteKey siteKey: String, + completion: @escaping (RCARecaptchaClientProtocol?, Error?) -> Void) { + // Do nothing. + } +} + +let registerMocksOnce: Void = { + let actionClass = objc_allocateClassPair(StubRCAAction.self, "RecaptchaEnterprise.RCAAction", 0) + if let actionClass = actionClass { + objc_registerClassPair(actionClass) + } + + let recaptchaClass = objc_allocateClassPair( + StubRCARecaptcha.self, + "RecaptchaEnterprise.RCARecaptcha", + 0 + ) + if let recaptchaClass = recaptchaClass { + objc_registerClassPair(recaptchaClass) + } +}() + +class FakeInternalProvider: NSObject, AppCheckCoreProvider { + var stubbedToken: AppCheckCoreToken? + var stubbedError: Error? + + @objc(getTokenWithCompletion:) + func getToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { + handler(stubbedToken, stubbedError) + } + + @objc(getLimitedUseTokenWithCompletion:) + func getLimitedUseToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) { + handler(stubbedToken, stubbedError) + } +} + +final class RecaptchaProviderTests: XCTestCase { + var provider: RecaptchaProvider! + var fakeInternalProvider: FakeInternalProvider! + + override func setUp() { + super.setUp() + _ = registerMocksOnce + fakeInternalProvider = FakeInternalProvider() + + guard let ProviderClass = NSClassFromString("FIRRecaptchaProvider") as? NSObject.Type + else { + XCTFail("Failed to get FIRRecaptchaProvider class") + return + } + + let providerInstance = ProviderClass.init() + providerInstance.setValue(fakeInternalProvider, forKey: "recaptchaProvider") + + guard let typedProvider = providerInstance as? RecaptchaProvider else { + XCTFail("Failed to cast provider instance to RecaptchaProvider") + return + } + + provider = typedProvider + } + + override func tearDown() { + provider = nil + fakeInternalProvider = nil + super.tearDown() + } + + func testInitWithValidApp() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + options.recaptchaSiteKey = "test_site_key" + + let appName = "testInitWithValidApp" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + XCTAssertNotNil(RecaptchaProvider(app: app)) + } + + func testInitWithIncompleteApp() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.projectID = "project_id" + options.recaptchaSiteKey = "test_site_key" + + let appName = "testInitWithIncompleteApp1" + let missingAPIKeyApp: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + missingAPIKeyApp = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + missingAPIKeyApp = FirebaseApp.app(name: appName)! + } + missingAPIKeyApp.isDataCollectionDefaultEnabled = false + + XCTAssertNil(RecaptchaProvider(app: missingAPIKeyApp)) + + options.projectID = nil + options.apiKey = "api_key" + options.recaptchaSiteKey = "test_site_key" + + let appName2 = "testInitWithIncompleteApp2" + let missingProjectIDApp: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName2) { + missingProjectIDApp = existingApp + } else { + FirebaseApp.configure(name: appName2, options: options) + missingProjectIDApp = FirebaseApp.app(name: appName2)! + } + missingProjectIDApp.isDataCollectionDefaultEnabled = false + XCTAssertNil(RecaptchaProvider(app: missingProjectIDApp)) + } + + func testInitWithMissingSiteKey() { + let options = FirebaseOptions(googleAppID: "1:123456789:ios:abc123", gcmSenderID: "sender_id") + options.apiKey = "api_key" + options.projectID = "project_id" + // options.recaptchaSiteKey is nil + + let appName = "testInitWithMissingSiteKey" + let app: FirebaseApp + if let existingApp = FirebaseApp.app(name: appName) { + app = existingApp + } else { + FirebaseApp.configure(name: appName, options: options) + app = FirebaseApp.app(name: appName)! + } + app.isDataCollectionDefaultEnabled = false + + XCTAssertNil(RecaptchaProvider(app: app)) + } + + func testGetTokenSuccess() { + let date = Date() + let validInternalToken = AppCheckCoreToken( + token: "valid_token", + expirationDate: date, + receivedAt: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getToken") + + provider.getToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetTokenAPIError() { + let expectedError = NSError(domain: "testGetTokenAPIError", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError + + let expectation = self.expectation(description: "getTokenError") + + provider.getToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetLimitedUseTokenSuccess() { + let date = Date() + let validInternalToken = AppCheckCoreToken( + token: "TEST_ValidToken", + expirationDate: date, + receivedAt: date + ) + fakeInternalProvider.stubbedToken = validInternalToken + + let expectation = self.expectation(description: "getLimitedUseToken") + + provider.getLimitedUseToken { token, error in + XCTAssertEqual(token?.token, validInternalToken.token) + XCTAssertEqual(token?.expirationDate, validInternalToken.expirationDate) + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testGetLimitedUseTokenProviderError() { + let expectedError = NSError(domain: "TEST_LimitedUseToken_Error", code: -1, userInfo: nil) + fakeInternalProvider.stubbedError = expectedError + + let expectation = self.expectation(description: "getLimitedUseTokenError") + + provider.getLimitedUseToken { token, error in + XCTAssertNil(token) + XCTAssertEqual(error as NSError?, expectedError) + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } +} diff --git a/FirebaseCore/Sources/FIROptions.m b/FirebaseCore/Sources/FIROptions.m index 95a3480706d..6e9a495fc51 100644 --- a/FirebaseCore/Sources/FIROptions.m +++ b/FirebaseCore/Sources/FIROptions.m @@ -29,6 +29,7 @@ NSString *const kFIRBundleID = @"BUNDLE_ID"; // The key to locate the project identifier in the plist file. NSString *const kFIRProjectID = @"PROJECT_ID"; +NSString *const kFIRRecaptchaSiteKey = @"RECAPTCHA_SITE_KEY"; NSString *const kFIRIsMeasurementEnabled = @"IS_MEASUREMENT_ENABLED"; NSString *const kFIRIsAnalyticsCollectionEnabled = @"FIREBASE_ANALYTICS_COLLECTION_ENABLED"; @@ -306,6 +307,15 @@ - (void)setStorageBucket:(NSString *)storageBucket { _optionsDictionary[kFIRStorageBucket] = [storageBucket copy]; } +- (NSString *)recaptchaSiteKey { + return self.optionsDictionary[kFIRRecaptchaSiteKey]; +} + +- (void)setRecaptchaSiteKey:(NSString *)recaptchaSiteKey { + [self checkEditingLocked]; + _optionsDictionary[kFIRRecaptchaSiteKey] = [recaptchaSiteKey copy]; +} + - (NSString *)bundleID { return self.optionsDictionary[kFIRBundleID]; } diff --git a/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h b/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h index 4e9f8853097..31d1f05932b 100644 --- a/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h +++ b/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h @@ -82,6 +82,11 @@ NS_SWIFT_NAME(FirebaseOptions) */ @property(nonatomic, copy, nullable) NSString *appGroupID; +/** + * The reCAPTCHA site key used by App Check. + */ +@property(nonatomic, copy, nullable) NSString *recaptchaSiteKey; + /** * Initializes a customized instance of FirebaseOptions from the file at the given plist file path. * This will read the file synchronously from disk. diff --git a/FirebaseCore/Tests/Unit/FIROptionsTest.m b/FirebaseCore/Tests/Unit/FIROptionsTest.m index a829bb7b2cc..ced922fc727 100644 --- a/FirebaseCore/Tests/Unit/FIROptionsTest.m +++ b/FirebaseCore/Tests/Unit/FIROptionsTest.m @@ -154,6 +154,19 @@ - (void)testInitCustomizedOptions { XCTAssertFalse(options.usingOptionsFromDefaultPlist); } +- (void)testRecaptchaSiteKey { + NSString *siteKey = @"placeholder_site_key"; + NSDictionary *optionsDictionary = @{@"RECAPTCHA_SITE_KEY" : siteKey}; + FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; + XCTAssertEqualObjects(options.recaptchaSiteKey, siteKey); +} + +- (void)testSetRecaptchaSiteKey { + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"app_id" GCMSenderID:@"sender_id"]; + options.recaptchaSiteKey = @"manual_site_key"; + XCTAssertEqualObjects(options.recaptchaSiteKey, @"manual_site_key"); +} + - (void)assertOptionsMatchDefaults:(FIROptions *)options andProjectID:(BOOL)matchProjectID { XCTAssertEqualObjects(options.googleAppID, kGoogleAppID); XCTAssertEqualObjects(options.APIKey, kAPIKey); diff --git a/Package.swift b/Package.swift index e4d84e1911f..dddf02bb77d 100644 --- a/Package.swift +++ b/Package.swift @@ -172,8 +172,7 @@ let package = Package( url: "https://github.com/google/interop-ios-for-google-sdks.git", "101.0.0" ..< "102.0.0" ), - .package(url: "https://github.com/google/app-check.git", - "11.0.1" ..< "12.0.0"), + appCheckDependency(), ], targets: [ .target( @@ -1263,6 +1262,7 @@ let package = Package( "FirebaseAppCheckInterop", "FirebaseCore", .product(name: "AppCheckCore", package: "app-check"), + .product(name: "AppCheckRecaptchaProvider", package: "app-check"), .product(name: "GULEnvironment", package: "GoogleUtilities"), .product(name: "GULUserDefaults", package: "GoogleUtilities"), ], @@ -1670,3 +1670,17 @@ func isFoundationModelsSupportedPlatformSwiftSetting() -> SwiftSetting { .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS]) ) } + +func appCheckDependency() -> Package.Dependency { + let appCheckURL = "https://github.com/google/app-check.git" + + if let localPath = Context.environment["FIREBASE_APP_CHECK_LOCAL_PATH"] { + return .package(path: localPath) + } + + if let branch = Context.environment["FIREBASE_APP_CHECK_BRANCH"] { + return .package(url: appCheckURL, branch: branch) + } + + return .package(url: appCheckURL, "11.3.0" ..< "12.0.0") +}