diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ace86e6bb..6ac0b5caf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,20 @@ permissions: jobs: build: - runs-on: macos-14 + runs-on: macos-15 env: CAKE_NAMES: ${{ inputs.names || 'Google.SignIn' }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup .NET (global.json) + - name: Toolchain versions + run: | + xcodebuild -version + swift --version + xcode-select -p + + - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: global-json-file: global.json diff --git a/.github/workflows/publish-github-packages.yml b/.github/workflows/publish-github-packages.yml index 3c6d437ee..154891050 100644 --- a/.github/workflows/publish-github-packages.yml +++ b/.github/workflows/publish-github-packages.yml @@ -17,18 +17,33 @@ permissions: jobs: publish: - runs-on: macos-14 + runs-on: macos-15 env: CAKE_NAMES: ${{ inputs.names || 'Google.SignIn' }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup .NET (global.json) + - name: Toolchain versions + run: | + xcodebuild -version + swift --version + xcode-select -p + + - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: global-json-file: global.json + - name: Configure GitHub Packages NuGet source (this repo owner) + run: | + dotnet nuget remove source "github-owner" >/dev/null 2>&1 || true + dotnet nuget add source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ + --name "github-owner" \ + --username "${{ github.actor }}" \ + --password "${{ secrets.GITHUB_TOKEN }}" \ + --store-password-in-clear-text + - name: Restore .NET workloads run: dotnet workload restore Xamarin.Google.sln diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 965f82b55..1db2f1784 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,31 @@ Most contributions fall into one of these buckets: * **Signing vs build-only**: device builds require provisioning; CI should validate “build-only” (no signing) where possible. * **URL scheme callbacks**: OAuth-style flows often require `Info.plist` URL scheme changes in addition to config files. +## Versioning notes (Cake vs .csproj) + +The Cake pipeline updates versions in binding `.csproj` files during the `externals` step: + +* `FileVersion` is always set to `artifact.NugetVersion` (from `components.cake`). +* `PackageVersion` is set to `artifact.NugetVersion` **unless** the project already specifies a pre-release suffix (e.g. `12.5.0.4-fork`). + +This allows forked builds to publish `-fork` (or similar) packages without changing the shared component version line in `components.cake`, while keeping deterministic, aligned build outputs. + +## Fork testing suffix policy (`-local` vs `-fork`) + +When validating a binding fix before upstream release: + +* Use `-local` for packages built on a developer machine and consumed from a local NuGet source. +* Use `-fork` for packages built by your fork CI and published to GitHub Packages. + +Suggested flow: + +1. Set a temporary prerelease `PackageVersion` (for example `12.5.0.4-fork` or `12.5.0.4-local`) in the affected project(s). +2. Build/publish from the matching channel (local machine for `-local`, GitHub Actions for `-fork`). +3. Consume that package from downstream repos to validate the fix. +4. Before opening an upstream PR, revert temporary prerelease versions/references back to the canonical version line expected upstream. + +Important: do not merge or submit upstream PRs with temporary fork-only or local-only package versions unless explicitly requested by maintainers. + ## Validation checklist (before requesting review) At minimum, validate the component(s) you touched: diff --git a/build.cake b/build.cake index 7f6163990..9f0c5b6ef 100644 --- a/build.cake +++ b/build.cake @@ -63,7 +63,8 @@ Setup (context => { IS_LOCAL_BUILD = string.IsNullOrWhiteSpace (EnvironmentVariable ("AGENT_ID")); Information ($"Is a local build? {IS_LOCAL_BUILD}"); - BACKSLASH = IS_LOCAL_BUILD ? @"\" : @"\"; + // Always use forward slashes for MSBuild targets on all platforms + BACKSLASH = "/"; }); Task("build") @@ -174,17 +175,18 @@ Task ("libs") .IsDependentOn("ci-setup") .Does(() => { - var msBuildSettings = new DotNetCoreMSBuildSettings (); var dotNetCoreBuildSettings = new DotNetCoreBuildSettings { Configuration = "Release", Verbosity = DotNetCoreVerbosity.Diagnostic, - MSBuildSettings = msBuildSettings + NoRestore = false }; - foreach (var target in SOURCES_TARGETS) - msBuildSettings.Targets.Add($@"source\{target}"); - - DotNetCoreBuild(SOLUTION_PATH, dotNetCoreBuildSettings); + // Build each artifact's csproj directly instead of using solution targets + foreach (var artifact in ARTIFACTS_TO_BUILD) { + var csprojPath = $"./source/{artifact.ComponentGroup}/{artifact.CsprojName}/{artifact.CsprojName}.csproj"; + Information ($"Building: {csprojPath}"); + DotNetCoreBuild(csprojPath, dotNetCoreBuildSettings); + } }); Task ("samples") @@ -199,13 +201,22 @@ Task ("samples") var dotNetCoreBuildSettings = new DotNetCoreBuildSettings { Configuration = "Release", Verbosity = DotNetCoreVerbosity.Diagnostic, + NoRestore = false, MSBuildSettings = msBuildSettings }; - foreach (var target in SAMPLES_TARGETS) - msBuildSettings.Targets.Add($@"samples-using-source\{target}"); - - DotNetCoreBuild(SOLUTION_PATH, dotNetCoreBuildSettings); + // Build each sample csproj directly + foreach (var artifact in ARTIFACTS_TO_BUILD) { + if (artifact.Samples == null) + continue; + foreach (var sample in artifact.Samples) { + var samplePath = $"./samples/{artifact.ComponentGroup}/{sample}/{sample}.csproj"; + if (FileExists(samplePath)) { + Information ($"Building sample: {samplePath}"); + DotNetCoreBuild(samplePath, dotNetCoreBuildSettings); + } + } + } }); Task ("nuget") @@ -222,8 +233,12 @@ Task ("nuget") Verbosity = DotNetCoreVerbosity.Diagnostic, }; - foreach (var target in SOURCES_TARGETS) - DotNetCorePack($"./source/{target}", dotNetCorePackSettings); + // Pack each artifact's csproj directly + foreach (var artifact in ARTIFACTS_TO_BUILD) { + var csprojPath = $"./source/{artifact.ComponentGroup}/{artifact.CsprojName}/{artifact.CsprojName}.csproj"; + Information ($"Packing: {csprojPath}"); + DotNetCorePack(csprojPath, dotNetCorePackSettings); + } }); Task ("clean") diff --git a/common.cake b/common.cake index ef9ca73ae..83b875afe 100644 --- a/common.cake +++ b/common.cake @@ -61,7 +61,9 @@ void UpdateVersionInCsproj (Artifact artifact) var componentGroup = artifact.ComponentGroup.ToString (); var csprojPath = $"./source/{componentGroup}/{artifact.CsprojName}/{artifact.CsprojName}.csproj"; XmlPoke(csprojPath, "/Project/PropertyGroup/FileVersion", artifact.NugetVersion); - XmlPoke(csprojPath, "/Project/PropertyGroup/PackageVersion", artifact.NugetVersion); + var currentPackageVersion = XmlPeek(csprojPath, "/Project/PropertyGroup/PackageVersion"); + if (!currentPackageVersion.Contains("-")) + XmlPoke(csprojPath, "/Project/PropertyGroup/PackageVersion", artifact.NugetVersion); } void CreateAndInstallPodfile (Artifact artifact) @@ -345,6 +347,14 @@ void BuildXcodeFatFramework (FilePath xcodeProject, PodSpec [] podSpecs, Platfor workingDirectory = workingDirectory ?? Directory("./externals/"); buildSettings = buildSettings ?? new Dictionary (); + if (!buildSettings.ContainsKey("CODE_SIGNING_ALLOWED")) + buildSettings["CODE_SIGNING_ALLOWED"] = "NO"; + if (!buildSettings.ContainsKey("CODE_SIGNING_REQUIRED")) + buildSettings["CODE_SIGNING_REQUIRED"] = "NO"; + if (!buildSettings.ContainsKey("CODE_SIGN_IDENTITY")) + buildSettings["CODE_SIGN_IDENTITY"] = ""; + if (!buildSettings.ContainsKey("EXPANDED_CODE_SIGN_IDENTITY")) + buildSettings["EXPANDED_CODE_SIGN_IDENTITY"] = ""; foreach (var podSpec in podSpecs) { var target = podSpec.TargetName; @@ -423,6 +433,14 @@ void BuildXcodeXcframework (FilePath xcodeProject, PodSpec [] podSpecs, Platform workingDirectory = workingDirectory ?? Directory ("./externals/"); buildSettings = buildSettings ?? new Dictionary (); + if (!buildSettings.ContainsKey("CODE_SIGNING_ALLOWED")) + buildSettings["CODE_SIGNING_ALLOWED"] = "NO"; + if (!buildSettings.ContainsKey("CODE_SIGNING_REQUIRED")) + buildSettings["CODE_SIGNING_REQUIRED"] = "NO"; + if (!buildSettings.ContainsKey("CODE_SIGN_IDENTITY")) + buildSettings["CODE_SIGN_IDENTITY"] = ""; + if (!buildSettings.ContainsKey("EXPANDED_CODE_SIGN_IDENTITY")) + buildSettings["EXPANDED_CODE_SIGN_IDENTITY"] = ""; foreach (var podSpec in podSpecs) { Information ($"Building the following framework: {podSpec.FrameworkName}..."); diff --git a/components.cake b/components.cake index 949666f2b..4282db6ff 100644 --- a/components.cake +++ b/components.cake @@ -14,7 +14,7 @@ Artifact FIREBASE_PERFORMANCE_MONITORING_ARTIFACT = new Artifact ("Firebase.Per Artifact FIREBASE_REMOTE_CONFIG_ARTIFACT = new Artifact ("Firebase.RemoteConfig", "12.5.0.4", "15.0", ComponentGroup.Firebase, csprojName: "RemoteConfig"); Artifact FIREBASE_STORAGE_ARTIFACT = new Artifact ("Firebase.Storage", "12.5.0.4", "15.0", ComponentGroup.Firebase, csprojName: "Storage"); //Artifact FIREBASE_APP_DISTRIBUTION_ARTIFACT = new Artifact ("Firebase.AppDistribution", "8.10.0.1", "15.0", ComponentGroup.Firebase, csprojName: "AppDistribution"); -//Artifact FIREBASE_APP_CHECK_ARTIFACT = new Artifact ("Firebase.AppCheck", "8.10.0.1", "15.0", ComponentGroup.Firebase, csprojName: "AppCheck"); +Artifact FIREBASE_APP_CHECK_ARTIFACT = new Artifact ("Firebase.AppCheck", "12.5.0.4", "15.0", ComponentGroup.Firebase, csprojName: "AppCheck"); // Google artifacts available to be built. These artifacts generate NuGets. Artifact GOOGLE_ANALYTICS_ARTIFACT = new Artifact ("Google.Analytics", "3.20.0.2", "15.0", ComponentGroup.Google, csprojName: "Analytics"); @@ -23,6 +23,7 @@ Artifact GOOGLE_MAPS_ARTIFACT = new Artifact ("Google.Maps" Artifact GOOGLE_MOBILE_ADS_ARTIFACT = new Artifact ("Google.MobileAds", "8.13.0.3", "15.0", ComponentGroup.Google, csprojName: "MobileAds"); Artifact GOOGLE_UMP_ARTIFACT = new Artifact ("Google.UserMessagingPlatform", "1.1.0.1", "15.0", ComponentGroup.Google, csprojName: "UserMessagingPlatform"); Artifact GOOGLE_PLACES_ARTIFACT = new Artifact ("Google.Places", "7.4.0.2", "15.0", ComponentGroup.Google, csprojName: "Places"); +Artifact GOOGLE_APP_CHECK_CORE_ARTIFACT = new Artifact ("Google.AppCheckCore", "11.2.0.0", "15.0", ComponentGroup.Google, csprojName: "AppCheckCore"); Artifact GOOGLE_SIGN_IN_ARTIFACT = new Artifact ("Google.SignIn", "9.0.0.0", "15.0", ComponentGroup.Google, csprojName: "SignIn"); Artifact GOOGLE_TAG_MANAGER_ARTIFACT = new Artifact ("Google.TagManager", "7.4.0.2", "15.0", ComponentGroup.Google, csprojName: "TagManager"); @@ -64,7 +65,7 @@ var ARTIFACTS = new Dictionary { { "Firebase.RemoteConfig", FIREBASE_REMOTE_CONFIG_ARTIFACT }, { "Firebase.Storage", FIREBASE_STORAGE_ARTIFACT }, // { "Firebase.AppDistribution", FIREBASE_APP_DISTRIBUTION_ARTIFACT }, - // { "Firebase.AppCheck", FIREBASE_APP_CHECK_ARTIFACT }, + { "Firebase.AppCheck", FIREBASE_APP_CHECK_ARTIFACT }, { "Google.GoogleAppMeasurement", GOOGLE_GOOGLE_APP_MEASUREMENT_ARTIFACT }, { "Google.Analytics", GOOGLE_ANALYTICS_ARTIFACT }, @@ -74,6 +75,7 @@ var ARTIFACTS = new Dictionary { { "Google.UserMessagingPlatform", GOOGLE_UMP_ARTIFACT }, { "Google.Places", GOOGLE_PLACES_ARTIFACT }, { "Google.SignIn", GOOGLE_SIGN_IN_ARTIFACT }, + { "Google.AppCheckCore", GOOGLE_APP_CHECK_CORE_ARTIFACT }, { "Google.TagManager", GOOGLE_TAG_MANAGER_ARTIFACT }, { "Google.GTMSessionFetcher", GOOGLE_GTM_SESSION_FETCHER_ARTIFACT }, { "Google.PromisesObjC", GOOGLE_PROMISES_OBJC_ARTIFACT }, @@ -113,7 +115,7 @@ void SetArtifactsDependencies () FIREBASE_REMOTE_CONFIG_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT, FIREBASE_INSTALLATIONS_ARTIFACT, FIREBASE_AB_TESTING_ARTIFACT }; FIREBASE_STORAGE_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT, FIREBASE_DATABASE_ARTIFACT, GOOGLE_GTM_SESSION_FETCHER_ARTIFACT /* Needed for sample FIREBASE_AUTH_ARTIFACT */ }; // FIREBASE_APP_DISTRIBUTION_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT, FIREBASE_INSTALLATIONS_ARTIFACT }; - // FIREBASE_APP_CHECK_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT }; + FIREBASE_APP_CHECK_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT, FIREBASE_INSTALLATIONS_ARTIFACT, GOOGLE_APP_CHECK_CORE_ARTIFACT }; GOOGLE_ANALYTICS_ARTIFACT.Dependencies = null; GOOGLE_CAST_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT }; @@ -121,8 +123,9 @@ void SetArtifactsDependencies () GOOGLE_MOBILE_ADS_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT }; GOOGLE_UMP_ARTIFACT.Dependencies = null; GOOGLE_PLACES_ARTIFACT.Dependencies = null; - GOOGLE_SIGN_IN_ARTIFACT.Dependencies = new [] { GOOGLE_GTM_SESSION_FETCHER_ARTIFACT, GOOGLE_PROMISES_OBJC_ARTIFACT, GOOGLE_GOOGLE_UTILITIES_ARTIFACT }; + GOOGLE_SIGN_IN_ARTIFACT.Dependencies = new [] { GOOGLE_GTM_SESSION_FETCHER_ARTIFACT, GOOGLE_PROMISES_OBJC_ARTIFACT, GOOGLE_GOOGLE_UTILITIES_ARTIFACT, GOOGLE_APP_CHECK_CORE_ARTIFACT }; GOOGLE_TAG_MANAGER_ARTIFACT.Dependencies = new [] { FIREBASE_CORE_ARTIFACT, FIREBASE_INSTALLATIONS_ARTIFACT, FIREBASE_ANALYTICS_ARTIFACT }; + GOOGLE_APP_CHECK_CORE_ARTIFACT.Dependencies = new [] { GOOGLE_PROMISES_OBJC_ARTIFACT, GOOGLE_GOOGLE_UTILITIES_ARTIFACT }; GOOGLE_PROMISES_OBJC_ARTIFACT.Dependencies = null; GOOGLE_GTM_SESSION_FETCHER_ARTIFACT.Dependencies = null; GOOGLE_NANOPB_ARTIFACT.Dependencies = null; @@ -209,9 +212,9 @@ void SetArtifactsPodSpecs () // FIREBASE_APP_DISTRIBUTION_ARTIFACT.PodSpecs = new [] { // PodSpec.Create ("Firebase", "8.10.0", frameworkSource: FrameworkSource.Pods, frameworkName: "FirebaseAppDistribution", targetName: "FirebaseAppDistribution", subSpecs: new [] { "AppDistribution" }) // }; - // FIREBASE_APP_CHECK_ARTIFACT.PodSpecs = new [] { - // PodSpec.Create ("Firebase", "8.10.0", frameworkSource: FrameworkSource.Pods, frameworkName: "FirebaseAppCheck", targetName: "FirebaseAppCheck", subSpecs: new [] { "AppCheck" }) - // }; + FIREBASE_APP_CHECK_ARTIFACT.PodSpecs = new [] { + PodSpec.Create ("FirebaseAppCheck", "12.5.0", frameworkSource: FrameworkSource.Pods) + }; // Google components GOOGLE_ANALYTICS_ARTIFACT.PodSpecs = new [] { @@ -235,9 +238,11 @@ void SetArtifactsPodSpecs () GOOGLE_SIGN_IN_ARTIFACT.PodSpecs = new [] { PodSpec.Create ("GoogleSignIn", "9.0.0", frameworkSource: FrameworkSource.Pods), PodSpec.Create ("AppAuth", "2.0.0", frameworkSource: FrameworkSource.Pods), - PodSpec.Create ("AppCheckCore", "11.2.0", frameworkSource: FrameworkSource.Pods), PodSpec.Create ("GTMAppAuth", "5.0.0", frameworkSource: FrameworkSource.Pods), }; + GOOGLE_APP_CHECK_CORE_ARTIFACT.PodSpecs = new [] { + PodSpec.Create ("AppCheckCore", "11.2.0", frameworkSource: FrameworkSource.Pods), + }; GOOGLE_TAG_MANAGER_ARTIFACT.PodSpecs = new [] { PodSpec.Create ("GoogleTagManager", "7.4.0") }; @@ -421,7 +426,7 @@ void SetArtifactsSamples () FIREBASE_REMOTE_CONFIG_ARTIFACT.Samples = new [] { "RemoteConfigSample" }; FIREBASE_STORAGE_ARTIFACT.Samples = new [] { "StorageSample" }; //FIREBASE_APP_DISTRIBUTION_ARTIFACT.Samples = new [] { "AppDistributionSample" }; - //FIREBASE_APP_CHECK_ARTIFACT.Samples = new [] { "AppCheckSample" }; + FIREBASE_APP_CHECK_ARTIFACT.Samples = new [] { "AppCheckSample" }; // Google components GOOGLE_ANALYTICS_ARTIFACT.Samples = new [] { "AnalyticsSample" }; diff --git a/docs/BUILDING.md b/docs/BUILDING.md index c8e2f187a..41632cfa6 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -14,6 +14,26 @@ Restore the local Cake tool (any version `< 1.0` should work): dotnet tool restore ``` +## Configure GitHub Packages feed (for fork contributors) + +If you are working on a fork of this repository and want to resolve NuGet packages published from your fork, run: + +```sh +# Using GitHub CLI (recommended) +./scripts/configure-github-feed.sh --gh + +# Or using a personal access token +export GITHUB_PACKAGES_PAT="your_github_pat_here" +./scripts/configure-github-feed.sh +``` + +This script: +- Auto-detects your fork owner from the git remote URL +- Configures a GitHub Packages feed (`github-`) +- Allows `dotnet restore` to resolve packages published from your fork + +**Note**: Your GitHub Personal Access Token must have the `read:packages` scope. See [GitHub docs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) for token creation. + ## Build + pack a component Build and produce `.nupkg` files into `./output`: @@ -41,3 +61,40 @@ To clean generated folders: ```sh dotnet tool run dotnet-cake -- --target=clean ``` + +## Troubleshooting + +### MSB4057: The target "source/..." does not exist + +This error can occur when using MSBuild solution-level targets with certain .NET SDK versions. The `build.cake` script avoids this by building each `.csproj` directly in dependency order, which is more explicit and reliable. + +### NU1101: Unable to find package AdamE.Google.iOS.AppCheckCore + +Some packages (like `SignIn`) depend on `AppCheckCore` which is built from this repo. The Cake script handles this automatically by building dependencies first. If you still encounter this: + +1. Ensure you're using the latest `build.cake` (it should iterate `ARTIFACTS_TO_BUILD`) +2. Run the full build: `dotnet tool run dotnet-cake -- --target=nuget --names=Google.SignIn` + +### Code signing errors during xcframework build + +The Cake scripts disable code signing by default (`CODE_SIGNING_ALLOWED=NO`) for CI compatibility. If you need signed frameworks, override the build settings in `common.cake`. + +### NuGet feed issues + +If you see errors like: +``` +NU1101: Unable to find package AdamE.Firebase.iOS.AppCheck [...] +``` + +This may occur if your GitHub Packages feed is not configured. See "[Configure GitHub Packages feed](#configure-github-packages-feed-for-fork-contributors)" above. + +Verify your feed configuration: +```sh +dotnet nuget list source +``` + +Clear the NuGet cache if needed: +```sh +dotnet nuget locals all --clear +dotnet restore +``` diff --git a/icons/firebaseiosappcheck_128x128.png b/icons/firebaseiosappcheck_128x128.png new file mode 100644 index 000000000..05c4f0fc8 Binary files /dev/null and b/icons/firebaseiosappcheck_128x128.png differ diff --git a/icons/firebaseiosappcheck_512x512.png b/icons/firebaseiosappcheck_512x512.png new file mode 100644 index 000000000..1391e934c Binary files /dev/null and b/icons/firebaseiosappcheck_512x512.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/AppCheckSample.NuGet.csproj b/samples/Firebase/AppCheck/AppCheckSample/AppCheckSample.NuGet.csproj new file mode 100644 index 000000000..7a20245ae --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/AppCheckSample.NuGet.csproj @@ -0,0 +1,75 @@ + + + net9.0-ios + iossimulator-x64;ios-arm64 + + iossimulator-x64 + Debug + iPhoneSimulator + Exe + enable + true + 15.0 + AppCheckSample + AppCheckSample + Resources + iPhoneSimulator;iPhone + Debug;Release + false + manual + $(DefaultItemExcludes);obj/AppCheckSample/**;bin/AppCheckSample/** + + + true + full + false + prompt + 4 + false + None + + + ios-arm64 + iPhone Developer + true + prompt + 4 + false + Entitlements.plist + SdkOnly + + + true + prompt + 4 + false + None + + + ios-arm64 + iPhone Developer + true + full + false + prompt + 4 + false + Entitlements.plist + SdkOnly + false + + + + + + + + + + + + + + + + diff --git a/samples/Firebase/AppCheck/AppCheckSample/AppCheckSample.csproj b/samples/Firebase/AppCheck/AppCheckSample/AppCheckSample.csproj new file mode 100644 index 000000000..7df9ed1d1 --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/AppCheckSample.csproj @@ -0,0 +1,79 @@ + + + net9.0-ios + iossimulator-x64;ios-arm64 + + iossimulator-x64 + Debug + iPhoneSimulator + Exe + enable + true + 15.0 + AppCheckSample + AppCheckSample + Resources + iPhoneSimulator;iPhone + Debug;Release + false + manual + $(DefaultItemExcludes);obj/AppCheckSample.NuGet/**;bin/AppCheckSample.NuGet/** + + + true + full + false + prompt + 4 + false + None + + + ios-arm64 + iPhone Developer + true + prompt + 4 + false + Entitlements.plist + SdkOnly + + + true + prompt + 4 + false + None + + + ios-arm64 + iPhone Developer + true + full + false + prompt + 4 + false + Entitlements.plist + SdkOnly + false + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Firebase/AppCheck/AppCheckSample/AppDelegate.cs b/samples/Firebase/AppCheck/AppCheckSample/AppDelegate.cs new file mode 100644 index 000000000..4d770bb4a --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/AppDelegate.cs @@ -0,0 +1,242 @@ +namespace AppCheckSample; + +using FirebaseAppCheck = Firebase.AppCheck.AppCheck; +using FirebaseCoreApp = Firebase.Core.App; +using Xamarin.iOS.Shared.Helpers; +using Xamarin.iOS.Shared.ViewControllers; + +[Register ("AppDelegate")] +public class AppDelegate : UIApplicationDelegate { + const string SelectedModeKey = "SelectedAppCheckMode"; + static bool IsFirebaseConfigured { get; set; } + + public override bool FinishedLaunching (UIApplication application, NSDictionary? launchOptions) + { + if (GoogleServiceInfoPlistHelper.FileExist () && !IsFirebaseConfigured) { + // Configure Firebase with the selected AppCheck provider + var selectedMode = NSUserDefaults.StandardUserDefaults.StringForKey (SelectedModeKey); + + if (!string.IsNullOrEmpty (selectedMode)) { + ConfigureFirebaseWithMode (selectedMode); + } + // If no mode selected yet, Firebase will be configured after user selection + } + + return true; + } + + static void ConfigureFirebaseWithMode (string mode) + { + switch (mode) { + case "Debug": + FirebaseAppCheck.SetAppCheckProviderFactory (new Firebase.AppCheck.AppCheckDebugProviderFactory ()); + break; + case "Device Check": + FirebaseAppCheck.SetAppCheckProviderFactory (new Firebase.AppCheck.DeviceCheckProviderFactory ()); + break; + case "App Attest": + // Note: AppAttestProviderFactory is not exposed in current bindings, using Device Check as fallback + FirebaseAppCheck.SetAppCheckProviderFactory (new Firebase.AppCheck.DeviceCheckProviderFactory ()); + break; + case "Disabled": + // Don't set any provider + break; + } + + FirebaseCoreApp.Configure (); + IsFirebaseConfigured = true; + } + + public override UISceneConfiguration GetConfiguration ( + UIApplication application, + UISceneSession connectingSceneSession, + UISceneConnectionOptions options) + { + return UISceneConfiguration.Create ("Default Configuration", connectingSceneSession.Role); + } + + internal static UIViewController CreateRootViewController () + { + if (!GoogleServiceInfoPlistHelper.FileExist ()) { + return new GoogleServiceInfoPlistNotFoundViewController (); + } + + var selectedMode = NSUserDefaults.StandardUserDefaults.StringForKey (SelectedModeKey); + + // If no mode selected yet, show selection screen + if (string.IsNullOrEmpty (selectedMode)) { + return new UINavigationController (CreateModeSelectionViewController ()); + } + + // Otherwise show the token test screen + return new UINavigationController (CreateTokenViewController (selectedMode)); + } + + static UIViewController CreateModeSelectionViewController () + { + var viewController = new UIViewController (); + viewController.View!.BackgroundColor = UIColor.White; + viewController.Title = "Select AppCheck Mode"; + + var headerLabel = new UILabel { + BackgroundColor = UIColor.White, + TextColor = UIColor.Black, + TextAlignment = UITextAlignment.Center, + Lines = 0, + Font = UIFont.BoldSystemFontOfSize (18), + TranslatesAutoresizingMaskIntoConstraints = false, + Text = "Choose an App Check mode to test.\n\nFirebase will be configured with your selection.\n\nTo test another mode, use the Reset button." + }; + + var stackView = new UIStackView { + Axis = UILayoutConstraintAxis.Vertical, + Spacing = 16, + TranslatesAutoresizingMaskIntoConstraints = false, + Alignment = UIStackViewAlignment.Fill, + Distribution = UIStackViewDistribution.FillEqually + }; + + var modes = new[] { "Disabled", "Debug", "Device Check", "App Attest" }; + foreach (var mode in modes) { + var button = UIButton.FromType (UIButtonType.System); + button.SetTitle (mode, UIControlState.Normal); + button.TitleLabel!.Font = UIFont.SystemFontOfSize (16); + button.TranslatesAutoresizingMaskIntoConstraints = false; + button.TouchUpInside += (_, _) => { + // Save selection + NSUserDefaults.StandardUserDefaults.SetString (mode, SelectedModeKey); + NSUserDefaults.StandardUserDefaults.Synchronize (); + + // Configure Firebase if not already done + if (!IsFirebaseConfigured) { + ConfigureFirebaseWithMode (mode); + } + + // Navigate to token test screen + var tokenVC = CreateTokenViewController (mode); + viewController.NavigationController?.PushViewController (tokenVC, true); + }; + stackView.AddArrangedSubview (button); + } + + viewController.View.AddSubview (headerLabel); + viewController.View.AddSubview (stackView); + + NSLayoutConstraint.ActivateConstraints (new [] { + headerLabel.LeadingAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.LeadingAnchor, 24), + headerLabel.TrailingAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.TrailingAnchor, -24), + headerLabel.TopAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.TopAnchor, 40), + + stackView.LeadingAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.LeadingAnchor, 40), + stackView.TrailingAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.TrailingAnchor, -40), + stackView.TopAnchor.ConstraintEqualTo (headerLabel.BottomAnchor, 40), + stackView.BottomAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.BottomAnchor, -120) + }); + + return viewController; + } + + static UIViewController CreateTokenViewController (string mode) + { + var viewController = new UIViewController (); + viewController.View!.BackgroundColor = UIColor.White; + viewController.Title = $"AppCheck: {mode}"; + + var statusLabel = new UILabel { + BackgroundColor = UIColor.White, + TextColor = UIColor.Black, + TextAlignment = UITextAlignment.Center, + Lines = 0, + TranslatesAutoresizingMaskIntoConstraints = false, + Text = mode == "App Attest" + ? $"Mode: {mode}\n(using Device Check as fallback)\n\nTap the button to fetch a token." + : mode == "Disabled" + ? "Mode: Disabled\n\nApp Check is not configured.\nNo provider factory was set." + : $"Mode: {mode}\n\nTap the button to fetch a token." + }; + + UIButton? tokenButton = null; + if (mode != "Disabled") { + tokenButton = UIButton.FromType (UIButtonType.System); + tokenButton.SetTitle ("Fetch App Check Token", UIControlState.Normal); + tokenButton.TranslatesAutoresizingMaskIntoConstraints = false; + tokenButton.TouchUpInside += (_, _) => { + statusLabel.Text = "Fetching App Check token..."; + FirebaseAppCheck.SharedInstance.TokenForcingRefresh (true, (token, error) => { + UIApplication.SharedApplication.BeginInvokeOnMainThread (() => { + if (error is not null) { + statusLabel.Text = $"Token request failed:\n{error.LocalizedDescription}"; + return; + } + + if (token is null || string.IsNullOrWhiteSpace (token.Token)) { + statusLabel.Text = "Token request returned an empty response."; + return; + } + + var rawToken = token.Token; + var preview = rawToken.Length > 20 ? $"{rawToken[..12]}...{rawToken[^8..]}" : rawToken; + statusLabel.Text = $"Token OK\n{preview}\nExpires: {token.ExpirationDate}"; + }); + }); + }; + } + + var resetButton = UIButton.FromType (UIButtonType.System); + resetButton.SetTitle ("Reset Mode (Restart Required)", UIControlState.Normal); + resetButton.SetTitleColor (UIColor.Red, UIControlState.Normal); + resetButton.TranslatesAutoresizingMaskIntoConstraints = false; + resetButton.TouchUpInside += (_, _) => { + var alert = UIAlertController.Create ( + "Reset App Check Mode", + "This will clear the selected mode. You must restart the app to select a new mode.\n\nNote: Firebase can only be configured once per app session.", + UIAlertControllerStyle.Alert + ); + alert.AddAction (UIAlertAction.Create ("Cancel", UIAlertActionStyle.Cancel, null)); + alert.AddAction (UIAlertAction.Create ("Reset & Exit", UIAlertActionStyle.Destructive, _ => { + NSUserDefaults.StandardUserDefaults.RemoveObject (SelectedModeKey); + NSUserDefaults.StandardUserDefaults.Synchronize (); + + // Exit the app (user must manually restart) + var exitAlert = UIAlertController.Create ( + "Mode Reset", + "Please restart the app to select a new mode.", + UIAlertControllerStyle.Alert + ); + exitAlert.AddAction (UIAlertAction.Create ("OK", UIAlertActionStyle.Default, _ => { + // iOS doesn't allow programmatic exit, but we can show this message + })); + viewController.PresentViewController (exitAlert, true, null); + })); + viewController.PresentViewController (alert, true, null); + }; + + viewController.View.AddSubview (statusLabel); + if (tokenButton is not null) { + viewController.View.AddSubview (tokenButton); + } + viewController.View.AddSubview (resetButton); + + var constraints = new List { + statusLabel.LeadingAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.LeadingAnchor, 24), + statusLabel.TrailingAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.TrailingAnchor, -24), + statusLabel.TopAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.TopAnchor, 80) + }; + + if (tokenButton is not null) { + constraints.AddRange (new [] { + tokenButton.TopAnchor.ConstraintEqualTo (statusLabel.BottomAnchor, 32), + tokenButton.CenterXAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.CenterXAnchor) + }); + constraints.Add (resetButton.TopAnchor.ConstraintEqualTo (tokenButton.BottomAnchor, 80)); + } else { + constraints.Add (resetButton.TopAnchor.ConstraintEqualTo (statusLabel.BottomAnchor, 80)); + } + + constraints.Add (resetButton.CenterXAnchor.ConstraintEqualTo (viewController.View.SafeAreaLayoutGuide.CenterXAnchor)); + + NSLayoutConstraint.ActivateConstraints (constraints.ToArray ()); + + return viewController; + } +} diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..fc9d33023 --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,117 @@ +{ + "images": [ + { + "scale": "2x", + "size": "20x20", + "idiom": "iphone", + "filename": "Icon40.png" + }, + { + "scale": "3x", + "size": "20x20", + "idiom": "iphone", + "filename": "Icon60.png" + }, + { + "scale": "2x", + "size": "29x29", + "idiom": "iphone", + "filename": "Icon58.png" + }, + { + "scale": "3x", + "size": "29x29", + "idiom": "iphone", + "filename": "Icon87.png" + }, + { + "scale": "2x", + "size": "40x40", + "idiom": "iphone", + "filename": "Icon80.png" + }, + { + "scale": "3x", + "size": "40x40", + "idiom": "iphone", + "filename": "Icon120.png" + }, + { + "scale": "2x", + "size": "60x60", + "idiom": "iphone", + "filename": "Icon120.png" + }, + { + "scale": "3x", + "size": "60x60", + "idiom": "iphone", + "filename": "Icon180.png" + }, + { + "scale": "1x", + "size": "20x20", + "idiom": "ipad", + "filename": "Icon20.png" + }, + { + "scale": "2x", + "size": "20x20", + "idiom": "ipad", + "filename": "Icon40.png" + }, + { + "scale": "1x", + "size": "29x29", + "idiom": "ipad", + "filename": "Icon29.png" + }, + { + "scale": "2x", + "size": "29x29", + "idiom": "ipad", + "filename": "Icon58.png" + }, + { + "scale": "1x", + "size": "40x40", + "idiom": "ipad", + "filename": "Icon40.png" + }, + { + "scale": "2x", + "size": "40x40", + "idiom": "ipad", + "filename": "Icon80.png" + }, + { + "scale": "1x", + "size": "76x76", + "idiom": "ipad", + "filename": "Icon76.png" + }, + { + "scale": "2x", + "size": "76x76", + "idiom": "ipad", + "filename": "Icon152.png" + }, + { + "scale": "2x", + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "Icon167.png" + }, + { + "scale": "1x", + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "Icon1024.png" + } + ], + "properties": {}, + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon1024.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon1024.png new file mode 100644 index 000000000..9174c989a Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon1024.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon120.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon120.png new file mode 100644 index 000000000..9c60a1761 Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon120.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon152.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon152.png new file mode 100644 index 000000000..448d6efb5 Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon152.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon167.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon167.png new file mode 100644 index 000000000..8524768f8 Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon167.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon180.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon180.png new file mode 100644 index 000000000..60a64703c Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon180.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon20.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon20.png new file mode 100644 index 000000000..45268a641 Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon20.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon29.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon29.png new file mode 100644 index 000000000..6a6c77a8b Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon29.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon40.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon40.png new file mode 100644 index 000000000..cc7edcf5c Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon40.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon58.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon58.png new file mode 100644 index 000000000..1ad04f004 Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon58.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon60.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon60.png new file mode 100644 index 000000000..2dd52620a Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon60.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon76.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon76.png new file mode 100644 index 000000000..b058cae2f Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon76.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon80.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon80.png new file mode 100644 index 000000000..02e47a261 Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon80.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon87.png b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon87.png new file mode 100644 index 000000000..4954a4bd3 Binary files /dev/null and b/samples/Firebase/AppCheck/AppCheckSample/Assets.xcassets/AppIcon.appiconset/Icon87.png differ diff --git a/samples/Firebase/AppCheck/AppCheckSample/Entitlements.plist b/samples/Firebase/AppCheck/AppCheckSample/Entitlements.plist new file mode 100644 index 000000000..36a870670 --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/Entitlements.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/Firebase/AppCheck/AppCheckSample/Info.plist b/samples/Firebase/AppCheck/AppCheckSample/Info.plist new file mode 100644 index 000000000..0c7e4fe7c --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleDisplayName + AppCheckSample + CFBundleIdentifier + com.xamarin.firebase.ios.appchecksample + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + + + + + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/AppIcon.appiconset + + diff --git a/samples/Firebase/AppCheck/AppCheckSample/LaunchScreen.storyboard b/samples/Firebase/AppCheck/AppCheckSample/LaunchScreen.storyboard new file mode 100644 index 000000000..780b7ed49 --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/LaunchScreen.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Firebase/AppCheck/AppCheckSample/Main.cs b/samples/Firebase/AppCheck/AppCheckSample/Main.cs new file mode 100644 index 000000000..5241af3eb --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/Main.cs @@ -0,0 +1,6 @@ +using AppCheckSample; + +// This is the main entry point of the application. +// If you want to use a different Application Delegate class from "AppDelegate" +// you can specify it here. +UIApplication.Main (args, null, typeof (AppDelegate)); diff --git a/samples/Firebase/AppCheck/AppCheckSample/README.md b/samples/Firebase/AppCheck/AppCheckSample/README.md new file mode 100644 index 000000000..0986e911b --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/README.md @@ -0,0 +1,75 @@ +# Firebase App Check — AppCheckSample (iOS) + +This sample validates the **Firebase App Check iOS** bindings in a real app. + +## Prerequisites + +- A Firebase project with an **iOS app** registered. +- A real device is strongly recommended. + - Simulator runs can fail with keychain/attestation related issues and may not be representative. +- Add `GoogleService-Info.plist` next to the sample project (do **not** commit secrets): + - `samples/Firebase/AppCheck/AppCheckSample/GoogleService-Info.plist` + +## App Check (Debug) — Two workflows + +Both workflows end up with the same outcome: +- Firebase Console knows your **debug token**. +- The app injects the same token at runtime. + +### Workflow A — Get token from app logs, then register it in Firebase Console + +1) Run the app once (see command below) and watch the console output. + +2) Copy the debug token printed by the Firebase SDK. + +3) Firebase Console: +- Go to **App Check** → select your iOS app → **Manage Debug tokens**. +- Paste the debug token value you copied. + +4) In the sample app, tap on the `Fetch App Check Token` button: the test should succeed. + +### Workflow B — Generate your own token in Firebase Console, then inject it at build/run time + +This is useful when you want a deterministic token without first scraping logs. + +1) Firebase Console: +- Go to **App Check** → select your iOS app → **Manage Debug tokens**. +- Generate a new debug token and copy its value. + +2) Inject that same value when launching the app using `FIRAAppCheckDebugToken` (see command below). + +3) In the sample app, tap on the `Fetch App Check Token` button: the test should succeed. + +## Run on a real device (recommended) + +Replace: +- `UDID` with your device UDID +- `YOUR_DEBUG_TOKEN` with your debug token (Workflow B only) + +```bash +dotnet build samples/Firebase/AppCheck/AppCheckSample/AppCheckSample.csproj \ + -c Debug -f net9.0-ios -t:Run \ + -p:Platform=iPhone -p:RuntimeIdentifier=ios-arm64 \ + -p:_DeviceName=UDID \ + -p:_BundlerDebug=true -v:n \ + -p:MlaunchAdditionalArgumentsProperty="--setenv=FIRAAppCheckDebugToken=YOUR_DEBUG_TOKEN" +``` + +Note: on some setups `mlaunch` expects `_DeviceName` to be the **raw UDID** (no `:v2:udid=` prefix). + +## Troubleshooting + +- **`403 PERMISSION_DENIED` / `App attestation failed`** + - The debug token is not registered for this Firebase app, or you’re using the wrong Firebase project / `GoogleService-Info.plist`. + +- **`Keychain access error` (often on simulator)** + - Prefer real device. + - If you must use simulator, try a fresh simulator device and reinstall. + +- **No Firebase configured UI / missing plist** + - Ensure `GoogleService-Info.plist` is present at: + - `samples/Firebase/AppCheck/AppCheckSample/GoogleService-Info.plist` + +## What success looks like + +The UI action to fetch an App Check token returns successfully. diff --git a/samples/Firebase/AppCheck/AppCheckSample/Resources/LaunchScreen.xib b/samples/Firebase/AppCheck/AppCheckSample/Resources/LaunchScreen.xib new file mode 100644 index 000000000..c13103dcf --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/Resources/LaunchScreen.xib @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Firebase/AppCheck/AppCheckSample/SceneDelegate.cs b/samples/Firebase/AppCheck/AppCheckSample/SceneDelegate.cs new file mode 100644 index 000000000..42a5e9535 --- /dev/null +++ b/samples/Firebase/AppCheck/AppCheckSample/SceneDelegate.cs @@ -0,0 +1,60 @@ +namespace AppCheckSample; + +[Register ("SceneDelegate")] +public class SceneDelegate : UIResponder, IUIWindowSceneDelegate { + + [Export ("window")] + public UIWindow? Window { get; set; } + + [Export ("scene:willConnectToSession:options:")] + public void WillConnect (UIScene scene, UISceneSession session, UISceneConnectionOptions connectionOptions) + { + if (scene is not UIWindowScene windowScene) { + return; + } + + Window = new UIWindow (windowScene) { + RootViewController = AppDelegate.CreateRootViewController () + }; + + Window.MakeKeyAndVisible (); + } + + [Export ("sceneDidDisconnect:")] + public void DidDisconnect (UIScene scene) + { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see UIApplicationDelegate `DidDiscardSceneSessions` instead). + } + + [Export ("sceneDidBecomeActive:")] + public void DidBecomeActive (UIScene scene) + { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + [Export ("sceneWillResignActive:")] + public void WillResignActive (UIScene scene) + { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + [Export ("sceneWillEnterForeground:")] + public void WillEnterForeground (UIScene scene) + { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + [Export ("sceneDidEnterBackground:")] + public void DidEnterBackground (UIScene scene) + { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/scripts/configure-github-feed.sh b/scripts/configure-github-feed.sh new file mode 100755 index 000000000..22d52b0e6 --- /dev/null +++ b/scripts/configure-github-feed.sh @@ -0,0 +1,139 @@ +#!/bin/bash +set -e + +# Script to configure GitHub Packages feed for private NuGet packages. +# +# This script auto-detects the fork owner from the git remote URL and adds +# the corresponding GitHub Packages feed, allowing contributors to resolve +# packages published from their fork. +# +# Usage: +# ./scripts/configure-github-feed.sh [--gh] +# +# Options: +# --gh Use GitHub CLI (gh) to retrieve the authentication token +# (requires: gh auth with read:packages scope) +# If not specified, uses GITHUB_PACKAGES_PAT environment variable +# +# Example: +# export GITHUB_PACKAGES_PAT="your_github_pat" +# ./scripts/configure-github-feed.sh +# +# Or: +# ./scripts/configure-github-feed.sh --gh + +use_gh_token=false +if [ "${1:-}" = "--gh" ]; then + use_gh_token=true +fi + +# Auto-detect fork owner from git remote URL +detect_fork_owner() { + local remote_url + remote_url="$(git config --get remote.origin.url)" + + if [ -z "$remote_url" ]; then + echo "ERROR: Unable to determine git remote URL (not a git repository?)" >&2 + exit 1 + fi + + # Extract owner from URL: github.com/Owner/repo.git or github.com:Owner/repo.git + local owner + owner="$(echo "$remote_url" | sed -E 's|.*[:/]([^/]+)/[^/]+\.git$|\1|')" + + if [ -z "$owner" ] || [ "$owner" = "$remote_url" ]; then + echo "ERROR: Unable to extract owner from git remote URL: $remote_url" >&2 + exit 1 + fi + + echo "$owner" +} + +# Verify GitHub token has required scope +assert_gh_packages_scope() { + local headers + headers="$(gh api -i /user 2>/dev/null || true)" + if [ -z "$headers" ]; then + return 0 + fi + + local scopes + scopes="$(echo "$headers" | tr -d '\r' | awk -F': ' 'tolower($1)=="x-oauth-scopes"{print $2}' | head -n 1)" + if [ -z "$scopes" ]; then + return 0 + fi + + case ",$scopes," in + *,read:packages,*|*,write:packages,*) + return 0 + ;; + esac + + echo "ERROR: GitHub token is missing required scope: read:packages" >&2 + echo "Fix:" >&2 + echo " gh auth refresh -h github.com -s read:packages" >&2 + echo "Then rerun:" >&2 + echo " ./scripts/configure-github-feed.sh --gh" >&2 + exit 1 +} + +# Main +FORK_OWNER="$(detect_fork_owner)" +echo "Detected fork owner: $FORK_OWNER" + +FEED_NAME="github-$(echo "$FORK_OWNER" | tr '[:upper:]' '[:lower:]')" # Convert to lowercase +FEED_URL="https://nuget.pkg.github.com/${FORK_OWNER}/index.json" + +if [ "$use_gh_token" = true ]; then + if ! command -v gh >/dev/null 2>&1; then + echo "ERROR: gh CLI is not installed." >&2 + echo "Install it from https://cli.github.com/ or use GITHUB_PACKAGES_PAT instead." >&2 + exit 1 + fi + PAT="$(gh auth token)" + if [ -z "$PAT" ]; then + echo "ERROR: gh CLI is not authenticated." >&2 + echo "Run: gh auth login" >&2 + exit 1 + fi + assert_gh_packages_scope + echo "Using GitHub token from gh CLI" +else + if [ -z "${GITHUB_PACKAGES_PAT:-}" ]; then + echo "ERROR: GITHUB_PACKAGES_PAT environment variable is not set." >&2 + echo "" >&2 + echo "Please set your GitHub Personal Access Token (needs read:packages):" >&2 + echo " export GITHUB_PACKAGES_PAT=\"your_github_pat_here\"" >&2 + echo "" >&2 + echo "Or use the gh CLI shortcut:" >&2 + echo " ./scripts/configure-github-feed.sh --gh" >&2 + exit 1 + fi + PAT="$GITHUB_PACKAGES_PAT" +fi + +# Add or update the feed +echo "Checking for existing feed '$FEED_NAME'..." +if dotnet nuget list source | grep -q "$FEED_NAME"; then + echo "Found existing feed, removing..." + dotnet nuget remove source "$FEED_NAME" >/dev/null 2>&1 || true +fi + +echo "Adding GitHub Packages feed..." +echo " Name: $FEED_NAME" +echo " URL: $FEED_URL" + +dotnet nuget add source "$FEED_URL" \ + --name "$FEED_NAME" \ + --username "$FORK_OWNER" \ + --password "$PAT" \ + --store-password-in-clear-text + +echo "" +echo "✓ GitHub Packages feed configured successfully!" +echo "" +echo "You can now restore packages from your fork:" +echo " dotnet restore" +echo "" +echo "Configured sources:" +dotnet nuget list source diff --git a/source/Firebase/AppCheck/ApiDefinition.cs b/source/Firebase/AppCheck/ApiDefinition.cs index c3a41b3c4..61371f46b 100644 --- a/source/Firebase/AppCheck/ApiDefinition.cs +++ b/source/Firebase/AppCheck/ApiDefinition.cs @@ -1,11 +1,14 @@ using System; using Firebase.Core; using Foundation; +using ObjCRuntime; namespace Firebase.AppCheck { // typedef void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error) delegate void TokenCompletionHandler (AppCheckToken token, NSError error); + interface IAppCheckProviderFactory { } + // @interface FIRAppCheck : NSObject [DisableDefaultCtor] [BaseType (typeof (NSObject), Name = "FIRAppCheck")] @@ -40,7 +43,7 @@ interface AppCheck { // +(void)setAppCheckProviderFactory:(id _Nullable)factory; [Static] [Export ("setAppCheckProviderFactory:")] - void SetAppCheckProviderFactory ([NullAllowed] AppCheckProviderFactory factory); + void SetAppCheckProviderFactory ([NullAllowed] IAppCheckProviderFactory factory); // @property (assign, nonatomic) BOOL isTokenAutoRefreshEnabled; [Export ("isTokenAutoRefreshEnabled")] @@ -48,8 +51,11 @@ interface AppCheck { } // @protocol FIRAppCheckProvider - [Protocol] - [BaseType (typeof (NSObject), Name = "FIRAppCheckProvider")] + interface IAppCheckProvider { } + + [Model] + [BaseType (typeof (NSObject))] + [Protocol (Name = "FIRAppCheckProvider")] interface AppCheckProvider { // @required -(void)getTokenWithCompletion:(void (^ _Nonnull)(FIRAppCheckToken * _Nullable, NSError * _Nullable))handler __attribute__((swift_name("getToken(completion:)"))); [Abstract] @@ -58,14 +64,15 @@ interface AppCheckProvider { } // @protocol FIRAppCheckProviderFactory - [Protocol] - [BaseType (typeof (NSObject), Name = "FIRAppCheckProviderFactory")] + [Model] + [BaseType (typeof (NSObject))] + [Protocol (Name = "FIRAppCheckProviderFactory")] interface AppCheckProviderFactory { // @required -(id _Nullable)createProviderWithApp:(FIRApp * _Nonnull)app; [Abstract] [Export ("createProviderWithApp:")] [return: NullAllowed] - AppCheckProvider CreateProviderWithApp (App app); + NSObject CreateProviderWithApp (App app); } // @interface FIRAppCheckToken : NSObject @@ -88,7 +95,7 @@ interface AppCheckToken { // @interface FIRAppCheckDebugProvider : NSObject [DisableDefaultCtor] [BaseType (typeof (NSObject), Name = "FIRAppCheckDebugProvider")] - interface AppCheckDebugProvider : AppCheckProvider { + interface AppCheckDebugProvider : IAppCheckProvider { // -(instancetype _Nullable)initWithApp:(FIRApp * _Nonnull)app; [Export ("initWithApp:")] NativeHandle Constructor (App app); diff --git a/source/Firebase/AppCheck/AppCheck.csproj b/source/Firebase/AppCheck/AppCheck.csproj index fd3bed340..ff0663d30 100644 --- a/source/Firebase/AppCheck/AppCheck.csproj +++ b/source/Firebase/AppCheck/AppCheck.csproj @@ -1,32 +1,32 @@ - + - xamarin.ios10;net6.0-ios - true + net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst enable true true true - 11.0 + 15.0 Firebase.AppCheck Firebase.AppCheck - 1.0.0.0 - 8.10.0 + 12.5.0.4 + 12.5.0.4 Resources true + true AdamE.Firebase.iOS.AppCheck Firebase APIs App Check iOS Library C# bindings for Firebase APIs App Check iOS Library C# bindings for Firebase APIs App Check iOS Library - Microsoft, Adam Essenmacher + Microsoft, Adam Essenmacher, Jean-Emmanuel Baillat Adam Essenmacher - © Microsoft Corporation. All rights reserved. © 2024, Adam Essenmacher. + © Microsoft Corporation. All rights reserved. © 2026, Adam Essenmacher. firebaseiosappcheck_128x128.png https://github.com/AdamEssenmacher/GoogleApisForiOSComponents License.md true - 8.10.0 + 12.5.0.4 @@ -35,6 +35,8 @@ + + @@ -46,16 +48,16 @@ -ObjC - - - + - - - - + + PackageVersion + + + PackageVersion + diff --git a/source/Firebase/AppCheck/AppCheck.targets b/source/Firebase/AppCheck/AppCheck.targets new file mode 100644 index 000000000..c7a149153 --- /dev/null +++ b/source/Firebase/AppCheck/AppCheck.targets @@ -0,0 +1,5 @@ + + + <_FirebaseAppCheckAssemblyName>Firebase.AppCheck, Version=12.5.0.4, Culture=neutral, PublicKeyToken=null + + diff --git a/source/Firebase/AppCheck/License.md b/source/Firebase/AppCheck/License.md index bbed91285..1dc43853b 100644 --- a/source/Firebase/AppCheck/License.md +++ b/source/Firebase/AppCheck/License.md @@ -1,8 +1,8 @@ -**Xamarin is not responsible for, nor does it grant any licenses to, third-party packages. Some packages may require or install dependencies which are governed by additional licenses.** +**This project is not responsible for, nor does it grant any licenses to, third-party packages. Some packages may require or install dependencies which are governed by additional licenses.** -Note: This component depends on [Firebase App Check for iOS](https://firebase.google.com/docs/analytics/ios/start), which is subject to the [Terms of Service for Firebase Services](https://firebase.google.com/terms/). +Note: This component depends on [Firebase App Check for iOS](https://firebase.google.com/docs/app-check/ios/overview), which is subject to the [Terms of Service for Firebase Services](https://firebase.google.com/terms/). -### Xamarin Component for Firebase App Distribution for iOS +### .NET iOS bindings for Firebase App Check **The MIT License (MIT)** @@ -13,5 +13,3 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -20160910 diff --git a/source/Google/AppCheckCore/ApiDefinition.cs b/source/Google/AppCheckCore/ApiDefinition.cs new file mode 100644 index 000000000..8696e80b7 --- /dev/null +++ b/source/Google/AppCheckCore/ApiDefinition.cs @@ -0,0 +1,4 @@ +using System; +namespace Google.AppCheckCore +{ +} diff --git a/source/Google/AppCheckCore/AppCheckCore.csproj b/source/Google/AppCheckCore/AppCheckCore.csproj new file mode 100644 index 000000000..446150db8 --- /dev/null +++ b/source/Google/AppCheckCore/AppCheckCore.csproj @@ -0,0 +1,53 @@ + + + net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst + enable + true + true + true + 15.0 + Google.AppCheckCore + Google.AppCheckCore + 11.2.0.0 + 11.2.0.0 + Resources + true + true + + + AdamE.Google.iOS.AppCheckCore + AppCheckCore + AppCheckCore packed for .NET for iOS + AppCheckCore packed for .NET for iOS + Microsoft, Adam Essenmacher, Jean-Emmanuel Baillat + Adam Essenmacher + © Microsoft Corporation. All rights reserved. © 2026, Adam Essenmacher. + https://github.com/AdamEssenmacher/GoogleApisForiOSComponents + License.md + true + 11.2.0.0 + + + + + + + + + + + + Framework + True + True + DeviceCheck + + + + + + + + + + diff --git a/source/Google/AppCheckCore/AppCheckCore.targets b/source/Google/AppCheckCore/AppCheckCore.targets new file mode 100644 index 000000000..6c569a53e --- /dev/null +++ b/source/Google/AppCheckCore/AppCheckCore.targets @@ -0,0 +1,5 @@ + + + <_GoogleAppCheckCoreAssemblyName>Google.AppCheckCore, Version=11.2.0.0, Culture=neutral, PublicKeyToken=null + + diff --git a/source/Google/AppCheckCore/License.md b/source/Google/AppCheckCore/License.md new file mode 100644 index 000000000..1dc43853b --- /dev/null +++ b/source/Google/AppCheckCore/License.md @@ -0,0 +1,15 @@ +**This project is not responsible for, nor does it grant any licenses to, third-party packages. Some packages may require or install dependencies which are governed by additional licenses.** + +Note: This component depends on [Firebase App Check for iOS](https://firebase.google.com/docs/app-check/ios/overview), which is subject to the [Terms of Service for Firebase Services](https://firebase.google.com/terms/). + +### .NET iOS bindings for Firebase App Check + +**The MIT License (MIT)** + +Copyright (c) .NET Foundation Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/source/Google/SignIn/SignIn.csproj b/source/Google/SignIn/SignIn.csproj index f1e4d0a8f..7a54d4ef2 100644 --- a/source/Google/SignIn/SignIn.csproj +++ b/source/Google/SignIn/SignIn.csproj @@ -50,13 +50,6 @@ SafariServices AuthenticationServices - - - Framework - True - True - DeviceCheck - Framework True @@ -83,4 +76,9 @@ + + + PackageVersion + +