From 16b88e00f17a03a751bbabd437b40e915f8ea008 Mon Sep 17 00:00:00 2001 From: kapusch Date: Fri, 23 Jan 2026 20:44:17 +0100 Subject: [PATCH 1/5] Add CI and publish workflows, project structure, and initial implementation for Facebook Login interop - Created CI workflow for building and packaging the iOS NuGet. - Added publish workflow for releasing the package to GitHub Packages. - Introduced .gitignore to exclude build artifacts and sensitive files. - Documented goals, packaging constraints, and repo layout in AGENTS.md. - Added contributing guidelines in CONTRIBUTING.md. - Created integration documentation for iOS in Docs/Integration.md. - Updated README.md with package details and usage instructions. - Established security policy in SECURITY.md for reporting vulnerabilities. - Included third-party notices for the Facebook iOS SDK in THIRD_PARTY_NOTICES.md. - Defined .NET SDK version in global.json. - Added sample app README for usage examples. - Implemented Facebook tracking mode enum and native login functionality. - Created NativeFacebookLogin class for handling Facebook login operations. - Defined NativeFacebookSignInResult for encapsulating sign-in results. - Configured project file for the Kapusch.FacebookApisForiOSComponents library. - Resolved package dependencies for Facebook SDK in Package.resolved. - Created Swift package manifest for KapuschFacebookAuthInterop. - Implemented interop functions in Swift for Facebook login. - Added build scripts for creating xcframeworks and collecting Facebook SDK frameworks. - Defined NativeSignInStatus enum for representing sign-in outcomes. - Configured build transitive properties and targets for native references. - Added NuGet readme for package description and integration guidance. --- .editorconfig | 15 ++ .github/pull_request_template.md | 11 + .github/workflows/ci.yml | 54 ++++ .github/workflows/publish.yml | 81 ++++++ .gitignore | 31 +++ AGENTS.md | 21 ++ CODE_OF_CONDUCT.md | 27 ++ CONTRIBUTING.md | 40 +++ Docs/Formatting.md | 32 +++ Docs/Integration.md | 44 ++++ Docs/SourceMode.md | 34 +++ README.md | 48 +++- SECURITY.md | 10 + THIRD_PARTY_NOTICES.md | 13 + global.json | 6 + samples/README.md | 7 + .../Facebook/FacebookTrackingMode.cs | 7 + .../Facebook/NativeFacebookLogin.iOS.cs | 164 ++++++++++++ .../Facebook/NativeFacebookSignInResult.cs | 11 + ...apusch.FacebookApisForiOSComponents.csproj | 65 +++++ .../Package.resolved | 15 ++ .../KapuschFacebookAuthInterop/Package.swift | 29 ++ .../KapuschFacebookAuthInterop/Interop.swift | 248 ++++++++++++++++++ .../include/KapuschFacebookAuthInterop.h | 5 + .../Native/iOS/build.sh | 78 ++++++ .../iOS/collect-facebook-xcframeworks.sh | 48 ++++ .../NativeSignInStatus.cs | 8 + .../Kapusch.Facebook.iOS.props | 11 + .../Kapusch.Facebook.iOS.targets | 50 ++++ .../nuget-readme.md | 7 + 30 files changed, 1219 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Docs/Formatting.md create mode 100644 Docs/Integration.md create mode 100644 Docs/SourceMode.md create mode 100644 SECURITY.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 global.json create mode 100644 samples/README.md create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Facebook/FacebookTrackingMode.cs create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookSignInResult.cs create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.resolved create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.swift create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Sources/KapuschFacebookAuthInterop/Interop.swift create mode 100644 src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/include/KapuschFacebookAuthInterop.h create mode 100755 src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh create mode 100755 src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh create mode 100644 src/Kapusch.FacebookApisForiOSComponents/NativeSignInStatus.cs create mode 100644 src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props create mode 100644 src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets create mode 100644 src/Kapusch.FacebookApisForiOSComponents/nuget-readme.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7639b87 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.cs] +indent_style = tab +indent_size = 4 + +[*.{yml,yaml,json,md,sh}] +indent_style = space +indent_size = 2 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..f7685de --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Summary + +- What does this change do? + +## Checklist + +- [ ] No secrets committed +- [ ] Built wrapper: `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh` +- [ ] Collected SDK xcframeworks: `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh` +- [ ] Packed NuGet: `dotnet pack src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj -c Release -o artifacts/nuget` +- [ ] Updated docs if behavior/integration changed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5ef19dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: ci + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build_pack_ios: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Install iOS workload + run: | + dotnet workload install ios + + - name: Cache NuGet + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', '**/*.csproj') }} + + - name: Cache SwiftPM scratch + uses: actions/cache@v4 + with: + path: | + src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build/spm + key: ${{ runner.os }}-swiftpm-${{ hashFiles('src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.resolved') }} + + - name: Build iOS wrapper + run: | + bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh + + - name: Collect Facebook xcframeworks + run: | + bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh + + - name: Pack + run: | + dotnet pack src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj \ + -c Release \ + -o artifacts/nuget + + - uses: actions/upload-artifact@v4 + with: + name: nuget + path: artifacts/nuget/*.nupkg diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7459433 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,81 @@ +name: publish + +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: read + packages: write + +jobs: + pack_and_publish: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine package version + id: version + shell: bash + run: | + if [[ "${{ github.ref }}" =~ ^refs/tags/v ]]; then + REF="${{ github.ref }}" + VERSION="${REF#refs/tags/v}" + else + CSPROJ="src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj" + BASE_VERSION=$(grep -oE '[^<]+' "$CSPROJ" | head -n 1 | sed 's///' || echo "0.1.0") + COMMIT_SHORT=$(git rev-parse --short HEAD) + RUN_NUMBER="${{ github.run_number }}" + VERSION="${BASE_VERSION}-prerelease.${RUN_NUMBER}.${COMMIT_SHORT}" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Package version: ${VERSION}" + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Install iOS workload + run: | + dotnet workload install ios + + - name: Cache NuGet + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('global.json', '**/*.csproj') }} + + - name: Cache SwiftPM scratch + uses: actions/cache@v4 + with: + path: | + src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build/spm + key: ${{ runner.os }}-swiftpm-${{ hashFiles('src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.resolved') }} + + - name: Build iOS wrapper + run: | + bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh + + - name: Collect Facebook xcframeworks + run: | + bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh + + - name: Pack + run: | + dotnet pack src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj \ + -c Release \ + -o artifacts/nuget \ + /p:PackageVersion="${{ steps.version.outputs.version }}" + + - name: Push to GitHub Packages + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + dotnet nuget push artifacts/nuget/*.nupkg \ + --api-key "$NUGET_AUTH_TOKEN" \ + --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \ + --skip-duplicate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..63b54b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# .NET +bin/ +obj/ +*.user +*.suo +*.userosscache +*.sln.docstates + +# NuGet +*.nupkg +*.snupkg +artifacts/ + +# macOS +.DS_Store + +# SwiftPM / Xcode +.build/ +*.xcworkspace/ +DerivedData/ + +# Local caches used by buildTransitive restore +.kapusch/ + +# Native build output (repo-only) +src/**/Native/iOS/build/ + +# Sample local config +samples/**/Info.plist +samples/**/GoogleService-Info.plist +samples/**/secrets.* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ceae60f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,21 @@ +# Kapusch.FacebookApisForiOSComponents — AI Working Agreement + +## Goals +- Produce a reproducible iOS NuGet package for Facebook Login interop. +- Do not commit secrets. + +## Packaging constraints +- Public OSS repo: keep docs/sample generic and not app-specific. +- The NuGet ships the required `xcframework`s and references them via `NativeReference`. +- Consuming apps must not download native deps at build time. +- The repo may use SwiftPM during CI/build to fetch the upstream Facebook iOS SDK, but consuming apps must not download native deps at build time. + +## Repo layout +- `src/Kapusch.FacebookApisForiOSComponents/` — NuGet project (managed API + buildTransitive MSBuild) +- `src/Kapusch.FacebookApisForiOSComponents/Native/iOS/` — Swift wrapper source + scripts (repo-only) +- `Docs/` — integration docs +- `samples/` — optional sample template (no secrets committed) + +## Safety +- Do not add new dependency ingestion paths without documenting them in `README.md`. +- Do not commit real app ids/secrets. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..462c8cd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,27 @@ +# Code of Conduct + +## Our Pledge + +We pledge to make participation in this project and our community a harassment-free experience for everyone. + +## Our Standards + +Examples of behavior that contributes to a positive environment include: +- Being respectful and considerate in language and actions +- Giving and gracefully accepting constructive feedback +- Focusing on what is best for the community + +Examples of unacceptable behavior include: +- Harassment, discrimination, or hateful conduct +- Trolling, insulting or derogatory comments, and personal or political attacks +- Publishing others’ private information without explicit permission + +## Enforcement + +Project maintainers are responsible for clarifying and enforcing standards. + +To report a Code of Conduct issue, contact the project maintainers. + +## Attribution + +This document is based on the Contributor Covenant. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..426b8d9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing + +Thanks for contributing! + +## Prerequisites + +- macOS with Xcode installed (required for iOS SDK tooling) +- .NET SDK 10 (this repo pins `10.0.100` via `global.json`) + +## Local build + +If you are working without the NuGet (ProjectReference), see `Docs/SourceMode.md`. + + +Build the Swift wrapper: + +- `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh` + +Collect the Facebook SDK xcframeworks (from the SwiftPM scratch produced by the build): + +- `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh` + +Pack the NuGet: + +- `dotnet pack src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj -c Release -o artifacts/nuget` + +## Formatting + +- C#: follow `.editorconfig` (tabs, LF). +- Swift: keep changes minimal and consistent with existing style. + +## Pull requests + +- Keep PRs focused and well-scoped. +- Do not commit secrets. +- If you update the Facebook SDK version, update `Package.swift` and `Package.resolved` together. + +## License + +By contributing, you agree that your contributions will be licensed under the repository license (MIT). diff --git a/Docs/Formatting.md b/Docs/Formatting.md new file mode 100644 index 0000000..c62cf16 --- /dev/null +++ b/Docs/Formatting.md @@ -0,0 +1,32 @@ +# Formatting + +This repository uses `.editorconfig` as the single source of truth for formatting. + +## What `.editorconfig` enforces here + +- UTF-8 +- LF line endings +- Final newline +- No trailing whitespace +- C#: tabs (indent size 4) + +## How to apply it (practically) + +- Make sure your editor reads `.editorconfig`. +- Reformat only files you touched; avoid repo-wide reformat PRs. + +## VS Code + +- EditorConfig is supported natively. +- Recommended: enable **Format on Save** for C#. +- Avoid overriding indentation in per-language settings (let `.editorconfig` drive it). + +## JetBrains Rider + +- Enable EditorConfig support: Settings → Editor → Code Style → Enable EditorConfig support. +- You can run Code Cleanup on touched files before committing. + +## CI expectations + +PRs should not introduce purely formatting-only changes. +If you need to reformat, keep it scoped and mention it in the PR description. diff --git a/Docs/Integration.md b/Docs/Integration.md new file mode 100644 index 0000000..1e0b4d8 --- /dev/null +++ b/Docs/Integration.md @@ -0,0 +1,44 @@ +# Integration (iOS) + +This document applies whether you consume the package via NuGet or via ProjectReference (see `Docs/SourceMode.md`). + +## 1) Info.plist requirements + +Your iOS app must include: + +- `FacebookDisplayName` +- `FacebookAppID` +- `FacebookClientToken` + +And a `CFBundleURLTypes` entry with the URL scheme: + +- `fb` + +Additionally, for app-to-app flows (Facebook app), include these query schemes: + +- `fbapi` +- `fb` +- `fb-messenger-share-api` +- `fb-app-share` + +## 2) AppDelegate hooks + +Call the interop hooks in your `AppDelegate`: + +- On launch: `NativeFacebookLogin.Initialize(app, options)` +- On URL open: `NativeFacebookLogin.HandleOpenUrl(app, url, options)` + +## 3) Limited Login + +For Limited Login, pass: +- `FacebookTrackingMode.Limited` +- a non-empty `nonce` (raw nonce string) + +The result can contain: +- `AuthenticationToken` +- `Nonce` + +## 4) Secrets policy + +Do not commit real values in this repo. +Use templates and `.gitignore`d local files for any sample app configuration. diff --git a/Docs/SourceMode.md b/Docs/SourceMode.md new file mode 100644 index 0000000..395c733 --- /dev/null +++ b/Docs/SourceMode.md @@ -0,0 +1,34 @@ +# Source mode (ProjectReference) + +This repo is primarily consumed as a NuGet package (`Kapusch.Facebook.iOS`). + +Sometimes you may want to debug/iterate using a project reference instead. + +## Option A (recommended): use NuGet + +Use GitHub Packages pre-release and consume the package. + +## Option B: ProjectReference (source mode) + +1) Clone this repo (or add it as a git submodule) next to your app. + +2) Build native assets (required for iOS runtime behavior): + +- `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh` +- `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh` + +This produces: +- `src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build/kfb.xcframework` +- `src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build/fb/*.xcframework` + +3) Add a project reference from your app to: + +- `src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj` + +4) If you build the repo in a different layout, you can set MSBuild properties on the consuming project: + +- `InteropIosWrapperDir` (path to `kfb.xcframework`) +- `FacebookIosFrameworksDir` (path to the folder containing `fb/*.xcframework`) + +Notes: +- Source mode is intended for local iteration; publishing should use the NuGet workflows. diff --git a/README.md b/README.md index 56d4ad7..7c2dbaf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ # FacebookApisForiOSComponents -Facebook iOS native bindings packaging, integration docs and sample + +Public OSS repository that packages **Facebook Login for iOS** into a consumable .NET NuGet. + +## Package + +- NuGet ID: `Kapusch.Facebook.iOS` + +## What this repo ships + +A NuGet package that: +- provides a small managed API for **Facebook Login (iOS)**, and +- redistributes the required **Facebook iOS SDK xcframeworks** inside the `.nupkg` (classic/native binding packaging), +- injects the xcframeworks into consuming apps via `buildTransitive` `NativeReference` items. + +## Third-party licenses + +See `THIRD_PARTY_NOTICES.md`. + +## Developer docs + +- Formatting: `Docs/Formatting.md` +- Source mode: `Docs/SourceMode.md` + +## Build (local) + +Prereqs: +- Xcode installed (for `xcrun`, iOS SDKs) +- .NET SDK 10 (`global.json` pins 10.0.100) + +Build the native wrapper: +- `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh` + +Collect Facebook xcframeworks for packing: +- `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh` + +Pack the NuGet: +- `dotnet pack src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj -c Release -o artifacts/nuget` + +## Consumption + +- Install the package from GitHub Packages (pre-release). +- Follow `Docs/Integration.md` for Info.plist keys and AppDelegate hooks. + +## CI + +- PR CI is build-only. +- Publishing is handled by a workflow that pushes a pre-release to GitHub Packages. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..07af9a0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Security Policy + +## Reporting a Vulnerability + +Please do not open public issues for security reports. + +Instead, use GitHub Security Advisories for this repository: +- Go to the repository page → **Security** → **Advisories** → **Report a vulnerability**. + +We will acknowledge receipt and work on a fix. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..4c85a68 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,13 @@ +# Third-Party Notices + +This repository and its NuGet package (`Kapusch.Facebook.iOS`) redistribute third-party binaries. + +## Facebook iOS SDK + +- Upstream: facebook-ios-sdk +- Source: https://github.com/facebook/facebook-ios-sdk + +The NuGet package includes Facebook SDK `xcframework`s under `fb/`. +Each included `xcframework` contains an upstream `LICENSE` file. + +If you are redistributing this package further, ensure you comply with the upstream license terms. diff --git a/global.json b/global.json new file mode 100644 index 0000000..54b6d85 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "patch" + } +} diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..2a298f0 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,7 @@ +# Samples + +This repo can be validated either by consuming the NuGet from your app, or via the small local iOS sample. + +Principles: +- No secrets committed. +- Keep `Info.plist` values in local-only files (ignored by `.gitignore`). diff --git a/src/Kapusch.FacebookApisForiOSComponents/Facebook/FacebookTrackingMode.cs b/src/Kapusch.FacebookApisForiOSComponents/Facebook/FacebookTrackingMode.cs new file mode 100644 index 0000000..344218b --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Facebook/FacebookTrackingMode.cs @@ -0,0 +1,7 @@ +namespace Kapusch.Facebook.iOS; + +public enum FacebookTrackingMode +{ + Enabled = 0, + Limited = 1, +} diff --git a/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs new file mode 100644 index 0000000..fa286e1 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs @@ -0,0 +1,164 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Kapusch.Facebook.iOS; + +public static unsafe class NativeFacebookLogin +{ + private const string LibraryName = "__Internal"; + + public static void Initialize(IntPtr uiApplicationHandle, IntPtr launchOptionsHandle) + { + if (uiApplicationHandle == IntPtr.Zero) + return; + + try + { + KfiFacebookInitialize( + uiApplicationHandle, + launchOptionsHandle == IntPtr.Zero ? null : launchOptionsHandle + ); + } + catch + { + // Best-effort only. + } + } + + public static bool HandleOpenUrl( + IntPtr uiApplicationHandle, + IntPtr nsUrlHandle, + IntPtr optionsHandle + ) + { + if (uiApplicationHandle == IntPtr.Zero || nsUrlHandle == IntPtr.Zero) + return false; + + try + { + return KfiFacebookHandleOpenUrl( + uiApplicationHandle, + nsUrlHandle, + optionsHandle == IntPtr.Zero ? null : optionsHandle + ); + } + catch + { + return false; + } + } + + public static Task SignInAsync( + IntPtr presentingViewControllerHandle, + FacebookTrackingMode trackingMode, + string? rawNonce, + CancellationToken cancellationToken = default + ) + { + if (presentingViewControllerHandle == IntPtr.Zero) + throw new ArgumentException("Presenting view controller is required."); + + if (cancellationToken.IsCancellationRequested) + return Task.FromResult(new NativeFacebookSignInResult(NativeSignInStatus.Cancelled)); + + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously + ); + + var gch = GCHandle.Alloc(tcs); + var context = GCHandle.ToIntPtr(gch); + + KfiFacebookSignInStart( + presentingViewControllerHandle, + (int)trackingMode, + rawNonce, + &KfiFacebookCallback, + context + ); + + _ = cancellationToken.Register(() => + tcs.TrySetResult(new NativeFacebookSignInResult(NativeSignInStatus.Cancelled)) + ); + + return tcs.Task; + } + + public static void SignOut() + { + try + { + KfiFacebookSignOut(); + } + catch + { + // Best-effort only. + } + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void KfiFacebookCallback( + int status, + IntPtr accessToken, + IntPtr authenticationToken, + IntPtr userId, + IntPtr nonce, + IntPtr errorCode, + IntPtr errorMessage, + IntPtr context + ) + { + var gch = GCHandle.FromIntPtr(context); + var tcs = (TaskCompletionSource)gch.Target!; + + try + { + var result = new NativeFacebookSignInResult( + Status: (NativeSignInStatus)status, + AccessToken: Marshal.PtrToStringUTF8(accessToken), + AuthenticationToken: Marshal.PtrToStringUTF8(authenticationToken), + UserId: Marshal.PtrToStringUTF8(userId), + Nonce: Marshal.PtrToStringUTF8(nonce), + ErrorCode: Marshal.PtrToStringUTF8(errorCode), + ErrorMessage: Marshal.PtrToStringUTF8(errorMessage) + ); + + _ = tcs.TrySetResult(result); + } + finally + { + gch.Free(); + } + } + + [DllImport(LibraryName, EntryPoint = "kfb_facebook_initialize")] + private static extern void KfiFacebookInitialize(IntPtr uiApplication, IntPtr? launchOptions); + + [DllImport(LibraryName, EntryPoint = "kfb_facebook_handle_open_url")] + [return: MarshalAs(UnmanagedType.I1)] + private static extern bool KfiFacebookHandleOpenUrl( + IntPtr uiApplication, + IntPtr nsUrl, + IntPtr? options + ); + + [DllImport(LibraryName, EntryPoint = "kfb_facebook_signin_start")] + private static extern void KfiFacebookSignInStart( + IntPtr presentingViewController, + int trackingMode, + [MarshalAs(UnmanagedType.LPUTF8Str)] string? rawNonce, + delegate* unmanaged[Cdecl]< + int, + IntPtr, + IntPtr, + IntPtr, + IntPtr, + IntPtr, + IntPtr, + IntPtr, + void> callback, + IntPtr context + ); + + [DllImport(LibraryName, EntryPoint = "kfb_facebook_signout")] + private static extern void KfiFacebookSignOut(); +} diff --git a/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookSignInResult.cs b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookSignInResult.cs new file mode 100644 index 0000000..b940aa7 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookSignInResult.cs @@ -0,0 +1,11 @@ +namespace Kapusch.Facebook.iOS; + +public sealed record NativeFacebookSignInResult( + NativeSignInStatus Status, + string? AccessToken = null, + string? AuthenticationToken = null, + string? UserId = null, + string? Nonce = null, + string? ErrorCode = null, + string? ErrorMessage = null +); diff --git a/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj b/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj new file mode 100644 index 0000000..0e21afd --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj @@ -0,0 +1,65 @@ + + + net10.0-ios + enable + enable + true + $(DefaultItemExcludes);Native/** + Kapusch.Facebook.iOS + Kapusch + Facebook iOS auth interop wrapper + classic xcframework packaging. + MIT + https://github.com/Kapusch/FacebookApisForiOSComponents + true + true + false + $(NoWarn);NU5128 + Kapusch.Facebook.iOS + Kapusch.Facebook.iOS + 0.1.0 + nuget-readme.md + Native/iOS/build/kfb.xcframework + Native/iOS/build/fb + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.resolved b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.resolved new file mode 100644 index 0000000..cbd500e --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "4a1b378c0ad0f27417c6595a92de2e449324cddc294f9537a804632e6039b503", + "pins" : [ + { + "identity" : "facebook-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/facebook/facebook-ios-sdk", + "state" : { + "revision" : "32da5bdef917ccd845fcf319c5fb67c654459d27", + "version" : "18.0.2" + } + } + ], + "version" : 3 +} diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.swift b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.swift new file mode 100644 index 0000000..720cf3c --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "KapuschFacebookAuthInterop", + platforms: [ + .iOS(.v15), + ], + products: [ + .library( + name: "KapuschFacebookAuthInterop", + type: .static, + targets: ["KapuschFacebookAuthInterop"] + ), + ], + dependencies: [ + .package(url: "https://github.com/facebook/facebook-ios-sdk", from: "18.0.1"), + ], + targets: [ + .target( + name: "KapuschFacebookAuthInterop", + dependencies: [ + .product(name: "FacebookLogin", package: "facebook-ios-sdk"), + .product(name: "FacebookCore", package: "facebook-ios-sdk"), + ], + path: "Sources/KapuschFacebookAuthInterop" + ), + ] +) diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Sources/KapuschFacebookAuthInterop/Interop.swift b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Sources/KapuschFacebookAuthInterop/Interop.swift new file mode 100644 index 0000000..9d684fa --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/Sources/KapuschFacebookAuthInterop/Interop.swift @@ -0,0 +1,248 @@ +import Foundation +import UIKit + +import FacebookCore +import FacebookLogin + +public typealias KapuschFacebookSignInCallback = @convention(c) ( + Int32, + UnsafePointer?, + UnsafePointer?, + UnsafePointer?, + UnsafePointer?, + UnsafePointer?, + UnsafePointer?, + UnsafeMutableRawPointer +) -> Void + +private enum ProviderStatus: Int32 { + case success = 0 + case cancelled = 1 + case failed = 2 +} + +private func withCString(_ value: String?, _ body: (UnsafePointer?) -> Void) { + guard let value else { + body(nil) + return + } + + value.withCString { cstr in + body(cstr) + } +} + +private func callFacebookCallback( + _ callback: KapuschFacebookSignInCallback, + status: ProviderStatus, + accessToken: String? = nil, + authenticationToken: String? = nil, + userId: String? = nil, + nonce: String? = nil, + errorCode: String? = nil, + errorMessage: String? = nil, + context: UnsafeMutableRawPointer +) { + withCString(accessToken) { accessTokenC in + withCString(authenticationToken) { authTokenC in + withCString(userId) { userIdC in + withCString(nonce) { nonceC in + withCString(errorCode) { errorCodeC in + withCString(errorMessage) { errorMessageC in + callback( + status.rawValue, + accessTokenC, + authTokenC, + userIdC, + nonceC, + errorCodeC, + errorMessageC, + context + ) + } + } + } + } + } + } +} + +private final class FacebookState { + nonisolated(unsafe) static var inProgress = false + nonisolated(unsafe) static var callback: KapuschFacebookSignInCallback? + nonisolated(unsafe) static var context: UnsafeMutableRawPointer? +} + +private enum FacebookTrackingMode: Int32 { + case enabled = 0 + case limited = 1 +} + +@_cdecl("kfb_facebook_initialize") +public func kfb_facebook_initialize( + _ applicationPtr: UnsafeMutableRawPointer, + _ launchOptionsPtr: UnsafeMutableRawPointer? +) { + let application = Unmanaged + .fromOpaque(applicationPtr) + .takeUnretainedValue() + + let options: NSDictionary? = { + guard let launchOptionsPtr else { return nil } + return Unmanaged.fromOpaque(launchOptionsPtr).takeUnretainedValue() + }() + + let launchOptions = options as? [UIApplication.LaunchOptionsKey: Any] + _ = ApplicationDelegate.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) +} + +@_cdecl("kfb_facebook_handle_open_url") +public func kfb_facebook_handle_open_url( + _ applicationPtr: UnsafeMutableRawPointer, + _ urlPtr: UnsafeMutableRawPointer, + _ optionsPtr: UnsafeMutableRawPointer? +) -> Bool { + let application = Unmanaged + .fromOpaque(applicationPtr) + .takeUnretainedValue() + + let url = Unmanaged.fromOpaque(urlPtr).takeUnretainedValue() as URL + + let options: NSDictionary? = { + guard let optionsPtr else { return nil } + return Unmanaged.fromOpaque(optionsPtr).takeUnretainedValue() + }() + + let openUrlOptions = options as? [UIApplication.OpenURLOptionsKey: Any] ?? [:] + + return ApplicationDelegate.shared.application( + application, + open: url, + options: openUrlOptions + ) +} + +@_cdecl("kfb_facebook_signin_start") +public func kfb_facebook_signin_start( + _ presentingViewControllerPtr: UnsafeMutableRawPointer, + _ trackingMode: Int32, + _ noncePtr: UnsafePointer?, + _ callback: @escaping KapuschFacebookSignInCallback, + _ context: UnsafeMutableRawPointer +) { + if FacebookState.inProgress { + callFacebookCallback( + callback, + status: .failed, + errorCode: "already_in_progress", + errorMessage: "Facebook sign-in is already in progress.", + context: context + ) + return + } + + FacebookState.inProgress = true + FacebookState.callback = callback + FacebookState.context = context + + let presenting = Unmanaged + .fromOpaque(presentingViewControllerPtr) + .takeUnretainedValue() + + let tracking = trackingMode == FacebookTrackingMode.enabled.rawValue + ? LoginTracking.enabled + : LoginTracking.limited + let nonce = noncePtr.flatMap { String(cString: $0) } + if tracking == .limited && nonce == nil { + callFacebookCallback( + callback, + status: .failed, + errorCode: "missing_nonce", + errorMessage: "Facebook Limited Login requires a nonce.", + context: context + ) + FacebookState.inProgress = false + FacebookState.callback = nil + FacebookState.context = nil + return + } + + let loginNonce = nonce ?? "" + + let loginConfig = LoginConfiguration( + permissions: ["public_profile", "email"], + tracking: tracking, + nonce: loginNonce + ) + + LoginManager().logIn( + viewController: presenting, + configuration: loginConfig, + completion: { result in + guard let callback = FacebookState.callback, + let context = FacebookState.context + else { + FacebookState.inProgress = false + return + } + + defer { + FacebookState.inProgress = false + FacebookState.callback = nil + FacebookState.context = nil + } + + switch result { + case .cancelled: + callFacebookCallback(callback, status: .cancelled, context: context) + return + case .failed(let error): + let nsError = error as NSError + callFacebookCallback( + callback, + status: .failed, + errorCode: "\(nsError.domain):\(nsError.code)", + errorMessage: nsError.localizedDescription, + context: context + ) + return + case .success(_, _, let token): + let accessToken = token?.tokenString + let authenticationToken = AuthenticationToken.current?.tokenString + let resolvedNonce = AuthenticationToken.current?.nonce ?? nonce + let userId = token?.userID + + if accessToken?.isEmpty == true + && authenticationToken?.isEmpty == true + { + callFacebookCallback( + callback, + status: .failed, + errorCode: "missing_token", + errorMessage: "Facebook sign-in returned no token.", + context: context + ) + return + } + + callFacebookCallback( + callback, + status: .success, + accessToken: accessToken, + authenticationToken: authenticationToken, + userId: userId, + nonce: resolvedNonce, + context: context + ) + } + } + ) +} + +@_cdecl("kfb_facebook_signout") +public func kfb_facebook_signout() { + LoginManager().logOut() +} diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/include/KapuschFacebookAuthInterop.h b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/include/KapuschFacebookAuthInterop.h new file mode 100644 index 0000000..cb0c9fa --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/include/KapuschFacebookAuthInterop.h @@ -0,0 +1,5 @@ +// Intentionally minimal. +// +// This header exists only to satisfy `xcodebuild -create-xcframework -headers ...` when packaging the +// static library product. The interop surface is exposed via `@_cdecl` symbols in Swift and invoked +// from .NET via P/Invoke. diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh new file mode 100755 index 0000000..15816b9 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$ROOT_DIR/KapuschFacebookAuthInterop" +BUILD_DIR="$ROOT_DIR/build" + +XCFRAMEWORK_OUT="$BUILD_DIR/kfb.xcframework" + +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +SDK_IPHONEOS_PATH="$(xcrun --sdk iphoneos --show-sdk-path)" +SDK_SIMULATOR_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)" + +SCRATCH_DIR="$BUILD_DIR/spm" + +echo "[KapuschFacebookAuthInterop] Building (iOS device arm64)..." +swift build \ + --package-path "$PACKAGE_DIR" \ + --scratch-path "$SCRATCH_DIR/iphoneos" \ + -c release \ + --sdk "$SDK_IPHONEOS_PATH" \ + --triple "arm64-apple-ios15.0" + +echo "[KapuschFacebookAuthInterop] Building (iOS simulator arm64)..." +swift build \ + --package-path "$PACKAGE_DIR" \ + --scratch-path "$SCRATCH_DIR/iphonesimulator-arm64" \ + -c release \ + --sdk "$SDK_SIMULATOR_PATH" \ + --triple "arm64-apple-ios15.0-simulator" + +echo "[KapuschFacebookAuthInterop] Building (iOS simulator x86_64)..." +swift build \ + --package-path "$PACKAGE_DIR" \ + --scratch-path "$SCRATCH_DIR/iphonesimulator-x86_64" \ + -c release \ + --sdk "$SDK_SIMULATOR_PATH" \ + --triple "x86_64-apple-ios15.0-simulator" + +IOS_LIB="$(find "$SCRATCH_DIR/iphoneos" -maxdepth 4 -path "*/release/libKapuschFacebookAuthInterop.a" | head -n 1)" +SIM_ARM64_LIB="$(find "$SCRATCH_DIR/iphonesimulator-arm64" -maxdepth 4 -path "*/release/libKapuschFacebookAuthInterop.a" | head -n 1)" +SIM_X64_LIB="$(find "$SCRATCH_DIR/iphonesimulator-x86_64" -maxdepth 4 -path "*/release/libKapuschFacebookAuthInterop.a" | head -n 1)" + +SIM_UNIVERSAL_LIB="$BUILD_DIR/libKapuschFacebookAuthInterop_simulator_universal.a" +echo "[KapuschFacebookAuthInterop] Creating universal simulator static library..." +if [ ! -f "$SIM_ARM64_LIB" ]; then + echo "Expected simulator (arm64) static library not found: $SIM_ARM64_LIB" >&2 + exit 1 +fi + +if [ ! -f "$SIM_X64_LIB" ]; then + echo "Expected simulator (x86_64) static library not found: $SIM_X64_LIB" >&2 + exit 1 +fi + +lipo -create "$SIM_ARM64_LIB" "$SIM_X64_LIB" -output "$SIM_UNIVERSAL_LIB" + +HEADERS_DIR="$PACKAGE_DIR/include" + +if [ -z "$IOS_LIB" ] || [ ! -f "$IOS_LIB" ]; then + echo "Expected iOS static library not found: $IOS_LIB" >&2 + exit 1 +fi + +if [ ! -f "$SIM_UNIVERSAL_LIB" ]; then + echo "Expected simulator static library not found: $SIM_UNIVERSAL_LIB" >&2 + exit 1 +fi + +echo "[KapuschFacebookAuthInterop] Creating xcframework..." +xcodebuild -create-xcframework \ + -library "$IOS_LIB" -headers "$HEADERS_DIR" \ + -library "$SIM_UNIVERSAL_LIB" -headers "$HEADERS_DIR" \ + -output "$XCFRAMEWORK_OUT" + +echo "[KapuschFacebookAuthInterop] Done: $XCFRAMEWORK_OUT" diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh new file mode 100755 index 0000000..c73a6cb --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="$ROOT_DIR/build" +SCRATCH_DIR="$BUILD_DIR/spm" +OUT_DIR="$BUILD_DIR/fb" + +if [ ! -d "$SCRATCH_DIR" ]; then + echo "SwiftPM scratch dir not found: $SCRATCH_DIR" >&2 + echo "Run build.sh first." >&2 + exit 1 +fi + +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" + +frameworks=( + "FBAEMKit.xcframework" + "FBSDKCoreKit.xcframework" + "FBSDKCoreKit_Basics.xcframework" + "FBSDKGamingServicesKit.xcframework" + "FBSDKLoginKit.xcframework" + "FBSDKShareKit.xcframework" +) + +for name in "${frameworks[@]}"; do + src="$(find "$SCRATCH_DIR" -maxdepth 8 -type d -name "$name" | head -n 1 || true)" + if [ -z "$src" ] || [ ! -d "$src" ]; then + echo "Missing xcframework in scratch: $name" >&2 + exit 1 + fi + + dst="$OUT_DIR/$name" + cp -R "$src" "$dst" + + # Keep iOS + iOS simulator only. + rm -rf "$dst/ios-arm64_x86_64-maccatalyst" 2>/dev/null || true + + # Strip debug symbols, signatures and Swift compile-time module artifacts. + find "$dst" -type d -name "dSYMs" -prune -exec rm -rf {} + + find "$dst" -type d -name "_CodeSignature" -prune -exec rm -rf {} + + find "$dst" -type d -name "*.swiftmodule" -prune -exec rm -rf {} + + find "$dst" -type f -name "*.bcsymbolmap" -delete || true + +done + +echo "Collected Facebook xcframeworks to: $OUT_DIR" diff --git a/src/Kapusch.FacebookApisForiOSComponents/NativeSignInStatus.cs b/src/Kapusch.FacebookApisForiOSComponents/NativeSignInStatus.cs new file mode 100644 index 0000000..feaa603 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/NativeSignInStatus.cs @@ -0,0 +1,8 @@ +namespace Kapusch.Facebook.iOS; + +public enum NativeSignInStatus +{ + Success = 0, + Cancelled = 1, + Failed = 2, +} diff --git a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props new file mode 100644 index 0000000..04323da --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props @@ -0,0 +1,11 @@ + + + false + false + + diff --git a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets new file mode 100644 index 0000000..ffa3218 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets @@ -0,0 +1,50 @@ + + + <_KapuschFacebookInteropPackageRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..')) + + + + + + + <_KapuschFacebookInteropIosWrapper>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropPackageRoot)', 'kfb.xcframework')) + <_KapuschFacebookInteropFbDir>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropPackageRoot)', 'fb')) + + + + Static + true + + + Framework + true + + + Framework + true + + + Framework + true + + + Framework + true + + + Framework + true + + + Framework + true + + + + diff --git a/src/Kapusch.FacebookApisForiOSComponents/nuget-readme.md b/src/Kapusch.FacebookApisForiOSComponents/nuget-readme.md new file mode 100644 index 0000000..8787cb4 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/nuget-readme.md @@ -0,0 +1,7 @@ +# Kapusch.Facebook.iOS + +This package provides: +- a small managed API to trigger Facebook Login on iOS, and +- the required Facebook iOS SDK `xcframework`s packaged for .NET iOS. + +See the repo `README.md` and `Docs/Integration.md` for integration steps. From 42fde4cb86d4eacf083532cf4128d7664276e8e1 Mon Sep 17 00:00:00 2001 From: kapusch Date: Fri, 23 Jan 2026 23:33:26 +0100 Subject: [PATCH 2/5] Enhance iOS build configuration with native references and error handling for Facebook SDK integration --- .../Kapusch.Facebook.iOS.targets | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets index ffa3218..b58bd77 100644 --- a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets +++ b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets @@ -2,11 +2,57 @@ <_KapuschFacebookInteropPackageRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..')) - + <_KapuschFacebookInteropSourceRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../Native/iOS/build')) + + - - + + <_KapuschFacebookInteropIosWrapper>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropSourceRoot)', 'kfb.xcframework')) + <_KapuschFacebookInteropFbDir>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropSourceRoot)', 'fb')) + + + + + + Static + true + + + Framework + true + + + Framework + true + + + Framework + true + + + Framework + true + + + Framework + true + + + Framework + true + + + Date: Sat, 24 Jan 2026 00:25:43 +0100 Subject: [PATCH 3/5] Update packaging path for InteropIosWrapperDir to maintain directory structure for kfb.xcframework --- .../Kapusch.FacebookApisForiOSComponents.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj b/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj index 0e21afd..49bb01b 100644 --- a/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj +++ b/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj @@ -43,7 +43,8 @@ - + + From 137dfb786e17de6388545b27d530e427d7f937a6 Mon Sep 17 00:00:00 2001 From: kapusch Date: Sat, 24 Jan 2026 08:38:06 +0100 Subject: [PATCH 4/5] Add iOS sample project with build configuration and documentation - Implement AppDelegate for sample app lifecycle management - Create Info.template.plist for app configuration - Add project file for sample app with necessary properties - Include README.md for sample project instructions - Update main README.md to reference the new iOS sample - Enhance CI workflow to build the iOS sample --- .github/workflows/ci.yml | 7 +++ README.md | 1 + .../AppDelegate.cs | 63 +++++++++++++++++++ .../Info.template.plist | 28 +++++++++ .../Kapusch.Facebook.iOS.Sample.csproj | 32 ++++++++++ samples/Kapusch.Facebook.iOS.Sample/Main.cs | 4 ++ samples/Kapusch.Facebook.iOS.Sample/README.md | 32 ++++++++++ samples/README.md | 4 ++ 8 files changed, 171 insertions(+) create mode 100644 samples/Kapusch.Facebook.iOS.Sample/AppDelegate.cs create mode 100644 samples/Kapusch.Facebook.iOS.Sample/Info.template.plist create mode 100644 samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj create mode 100644 samples/Kapusch.Facebook.iOS.Sample/Main.cs create mode 100644 samples/Kapusch.Facebook.iOS.Sample/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ef19dd..228caa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,13 @@ jobs: -c Release \ -o artifacts/nuget + - name: Build iOS sample + run: | + dotnet build samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj \ + -c Debug \ + -p:RuntimeIdentifier=iossimulator-arm64 \ + -p:EnableCodeSigning=false + - uses: actions/upload-artifact@v4 with: name: nuget diff --git a/README.md b/README.md index 7c2dbaf..6255b47 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ See `THIRD_PARTY_NOTICES.md`. - Formatting: `Docs/Formatting.md` - Source mode: `Docs/SourceMode.md` +- Samples: `samples/README.md` ## Build (local) diff --git a/samples/Kapusch.Facebook.iOS.Sample/AppDelegate.cs b/samples/Kapusch.Facebook.iOS.Sample/AppDelegate.cs new file mode 100644 index 0000000..43f5c6c --- /dev/null +++ b/samples/Kapusch.Facebook.iOS.Sample/AppDelegate.cs @@ -0,0 +1,63 @@ +using Foundation; +using Kapusch.Facebook.iOS; +using UIKit; + +namespace Kapusch.Facebook.iOS.Sample; + +[Register("AppDelegate")] +public sealed class AppDelegate : UIApplicationDelegate +{ + public override UIWindow? Window { get; set; } + + public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions) + { + NativeFacebookLogin.Initialize(application.Handle, launchOptions?.Handle ?? IntPtr.Zero); + + Window = new UIWindow(UIScreen.MainScreen.Bounds); + + var viewController = new UIViewController(); + viewController.View!.BackgroundColor = UIColor.SystemBackground; + + var signInButton = new UIButton(UIButtonType.System); + signInButton.SetTitle("Sign In (Limited)", UIControlState.Normal); + signInButton.Frame = new CoreGraphics.CGRect(40, 120, 280, 44); + signInButton.TouchUpInside += async (_, _) => + { + var presenter = Window?.RootViewController; + if (presenter is null) + return; + + try + { + var result = await NativeFacebookLogin.SignInAsync( + presenter.Handle, + FacebookTrackingMode.Limited, + rawNonce: null, + CancellationToken.None + ); + + Console.WriteLine( + $"Facebook sign-in: Status={result.Status} UserId={result.UserId ?? "(null)"} Error={result.ErrorCode ?? "(null)"} {result.ErrorMessage ?? ""}" + ); + } + catch (Exception ex) + { + Console.WriteLine($"Facebook sign-in exception: {ex}"); + } + }; + + viewController.View.AddSubview(signInButton); + + Window.RootViewController = viewController; + Window.MakeKeyAndVisible(); + + return true; + } + + public override bool OpenUrl(UIApplication application, NSUrl url, NSDictionary options) => + NativeFacebookLogin.HandleOpenUrl( + application.Handle, + url.Handle, + options?.Handle ?? IntPtr.Zero + ); +} diff --git a/samples/Kapusch.Facebook.iOS.Sample/Info.template.plist b/samples/Kapusch.Facebook.iOS.Sample/Info.template.plist new file mode 100644 index 0000000..d7b93e7 --- /dev/null +++ b/samples/Kapusch.Facebook.iOS.Sample/Info.template.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDisplayName + Kapusch Facebook iOS Sample + CFBundleIdentifier + com.kapusch.facebookios.sample + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + KapuschFacebookiOSSample + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj b/samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj new file mode 100644 index 0000000..6e5445d --- /dev/null +++ b/samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj @@ -0,0 +1,32 @@ + + + net10.0-ios + Exe + enable + enable + 15.0 + com.kapusch.facebookios.sample + Kapusch Facebook iOS Sample + true + $(NoWarn);CA1422 + + + <_SampleInfoPlist>$(MSBuildThisFileDirectory)Info.plist + <_SampleInfoPlistTemplate>$(MSBuildThisFileDirectory)Info.template.plist + $(_SampleInfoPlist) + + + + + + + + + diff --git a/samples/Kapusch.Facebook.iOS.Sample/Main.cs b/samples/Kapusch.Facebook.iOS.Sample/Main.cs new file mode 100644 index 0000000..e9701f4 --- /dev/null +++ b/samples/Kapusch.Facebook.iOS.Sample/Main.cs @@ -0,0 +1,4 @@ +using Kapusch.Facebook.iOS.Sample; +using UIKit; + +UIApplication.Main(args, null, typeof(AppDelegate)); diff --git a/samples/Kapusch.Facebook.iOS.Sample/README.md b/samples/Kapusch.Facebook.iOS.Sample/README.md new file mode 100644 index 0000000..7e12e9d --- /dev/null +++ b/samples/Kapusch.Facebook.iOS.Sample/README.md @@ -0,0 +1,32 @@ +# Kapusch.Facebook.iOS.Sample + +Small, buildable iOS sample project for validating: +- Managed API compilation +- Native asset injection via `NativeReference` (wrapper + Facebook xcframeworks) + +No secrets are committed. + +## Build (local) + +Prereqs: +- macOS + Xcode +- .NET SDK 10 (`global.json` pins 10.0.100) + +Build native assets (repo-only): +```bash +bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh +bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh +``` + +Build the sample (simulator, no signing): +```bash +dotnet build samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj \ + -c Debug \ + -p:RuntimeIdentifier=iossimulator-arm64 \ + -p:EnableCodeSigning=false +``` + +## Runtime notes + +This sample calls into the interop API but does not include a configured Facebook App ID / URL schemes. +To make the login flow work end-to-end, update the generated `Info.plist` (gitignored) with the required Facebook keys and URL schemes. diff --git a/samples/README.md b/samples/README.md index 2a298f0..3f645c8 100644 --- a/samples/README.md +++ b/samples/README.md @@ -5,3 +5,7 @@ This repo can be validated either by consuming the NuGet from your app, or via t Principles: - No secrets committed. - Keep `Info.plist` values in local-only files (ignored by `.gitignore`). + +## iOS + +- `samples/Kapusch.Facebook.iOS.Sample/` — minimal UIKit app that links against the wrapper + Facebook xcframeworks. From d09c9611b5ab52342a990831d748b826038d1b54 Mon Sep 17 00:00:00 2001 From: kapusch Date: Sat, 24 Jan 2026 09:49:56 +0100 Subject: [PATCH 5/5] Add validation for nupkg layout in CI workflows and document sample iOS workflow --- .github/workflows/ci.yml | 45 ++++++++++++++++--- .github/workflows/publish.yml | 41 ++++++++++++++++++ .github/workflows/sample-ios.yml | 74 ++++++++++++++++++++++++++++++++ samples/README.md | 3 +- 4 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/sample-ios.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 228caa1..89d1156 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,12 +48,47 @@ jobs: -c Release \ -o artifacts/nuget - - name: Build iOS sample + - name: Validate nupkg layout run: | - dotnet build samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj \ - -c Debug \ - -p:RuntimeIdentifier=iossimulator-arm64 \ - -p:EnableCodeSigning=false + python3 - <<'PY' + import glob, sys, zipfile + + nupkgs = glob.glob("artifacts/nuget/*.nupkg") + if not nupkgs: + print("ERROR: no .nupkg found under artifacts/nuget/") + sys.exit(1) + + nupkg = sorted(nupkgs)[-1] + print(f"Validating {nupkg}") + + z = zipfile.ZipFile(nupkg) + names = set(z.namelist()) + + required = [ + "buildTransitive/Kapusch.Facebook.iOS.targets", + "kfb.xcframework/Info.plist", + "fb/FBAEMKit.xcframework/Info.plist", + "fb/FBSDKCoreKit.xcframework/Info.plist", + "fb/FBSDKCoreKit_Basics.xcframework/Info.plist", + "fb/FBSDKGamingServicesKit.xcframework/Info.plist", + "fb/FBSDKLoginKit.xcframework/Info.plist", + "fb/FBSDKShareKit.xcframework/Info.plist", + ] + + missing = [p for p in required if p not in names] + if missing: + print("ERROR: missing required paths in nupkg:") + for p in missing: + print(f" - {p}") + sys.exit(1) + + # Guardrail: ensure the wrapper is not flattened at the package root. + if "Info.plist" in names: + print("ERROR: wrapper appears flattened (found 'Info.plist' at package root).") + sys.exit(1) + + print("OK: nupkg layout looks correct.") + PY - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7459433..deeaa6a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -71,6 +71,47 @@ jobs: -o artifacts/nuget \ /p:PackageVersion="${{ steps.version.outputs.version }}" + - name: Validate nupkg layout + run: | + python3 - <<'PY' + import glob, sys, zipfile + + nupkgs = glob.glob("artifacts/nuget/*.nupkg") + if not nupkgs: + print("ERROR: no .nupkg found under artifacts/nuget/") + sys.exit(1) + + nupkg = sorted(nupkgs)[-1] + print(f"Validating {nupkg}") + + z = zipfile.ZipFile(nupkg) + names = set(z.namelist()) + + required = [ + "buildTransitive/Kapusch.Facebook.iOS.targets", + "kfb.xcframework/Info.plist", + "fb/FBAEMKit.xcframework/Info.plist", + "fb/FBSDKCoreKit.xcframework/Info.plist", + "fb/FBSDKCoreKit_Basics.xcframework/Info.plist", + "fb/FBSDKGamingServicesKit.xcframework/Info.plist", + "fb/FBSDKLoginKit.xcframework/Info.plist", + "fb/FBSDKShareKit.xcframework/Info.plist", + ] + + missing = [p for p in required if p not in names] + if missing: + print("ERROR: missing required paths in nupkg:") + for p in missing: + print(f" - {p}") + sys.exit(1) + + if "Info.plist" in names: + print("ERROR: wrapper appears flattened (found 'Info.plist' at package root).") + sys.exit(1) + + print("OK: nupkg layout looks correct.") + PY + - name: Push to GitHub Packages env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sample-ios.yml b/.github/workflows/sample-ios.yml new file mode 100644 index 0000000..5800a95 --- /dev/null +++ b/.github/workflows/sample-ios.yml @@ -0,0 +1,74 @@ +name: sample-ios (manual) + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build_sample_ios: + runs-on: macos-15 + env: + DOTNET_MULTILEVEL_LOOKUP: 0 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: true + DOTNET_CLI_WORKLOAD_UPDATE_NOTIFICATION_LEVEL: Disable + + steps: + - uses: actions/checkout@v4 + + - name: Setup Xcode 26.0 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.0" + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Pin workload update mode (manifests) + shell: bash + run: | + dotnet workload config --update-mode manifests + dotnet workload config --update-mode + + - name: Purge incompatible iOS packs (cache defense) + shell: bash + run: | + set -euo pipefail + PACKS_DIR="$HOME/.dotnet/packs" + for v in 26.2; do + if [ -d "$PACKS_DIR" ] && compgen -G "$PACKS_DIR/Microsoft.iOS.*_${v}*" > /dev/null; then + echo "Removing cached iOS ${v} packs from $PACKS_DIR" + rm -rf "$PACKS_DIR"/Microsoft.iOS.*_${v}* + fi + done + + - name: Install iOS workload + shell: bash + run: | + dotnet workload install ios --skip-manifest-update + + - name: Diagnostics (.NET + workloads) + shell: bash + run: | + dotnet --info + dotnet workload list + + - name: Build iOS wrapper + run: | + bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh + + - name: Collect Facebook xcframeworks + run: | + bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh + + - name: Build iOS sample (simulator, no signing) + run: | + dotnet build samples/Kapusch.Facebook.iOS.Sample/Kapusch.Facebook.iOS.Sample.csproj \ + -c Debug \ + -p:RuntimeIdentifier=iossimulator-arm64 \ + -p:EnableCodeSigning=false + diff --git a/samples/README.md b/samples/README.md index 3f645c8..be21653 100644 --- a/samples/README.md +++ b/samples/README.md @@ -8,4 +8,5 @@ Principles: ## iOS -- `samples/Kapusch.Facebook.iOS.Sample/` — minimal UIKit app that links against the wrapper + Facebook xcframeworks. +- `samples/Kapusch.Facebook.iOS.Sample/` — minimal UIKit app that links against the wrapper + Facebook xcframeworks (local validation). +- Manual CI: `.github/workflows/sample-ios.yml` (run via `workflow_dispatch`).