diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89d1156..67d00af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,10 +67,10 @@ jobs: required = [ "buildTransitive/Kapusch.Facebook.iOS.targets", "kfb.xcframework/Info.plist", + "kfbshare.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", ] @@ -87,6 +87,10 @@ jobs: print("ERROR: wrapper appears flattened (found 'Info.plist' at package root).") sys.exit(1) + if any(name.startswith("fb/FBSDKGamingServicesKit.xcframework/") for name in names): + print("ERROR: GamingServicesKit must not be packaged.") + sys.exit(1) + print("OK: nupkg layout looks correct.") PY diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d7a1e52..67c1c1b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,6 @@ jobs: shell: bash run: | REF="${{ github.ref }}" - EVENT="${{ github.event_name }}" MANUAL_VERSION="${{ github.event.inputs.manual_version }}" CSPROJ="src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj" BASE_VERSION=$(grep -oE '[^<]+' "$CSPROJ" | head -n 1 | sed 's///' || echo "0.1.0") @@ -52,27 +51,14 @@ jobs: fi if [[ "$PUBLISH_TARGET" == "nuget" ]]; then - git fetch origin +refs/heads/*:refs/remotes/origin/* - TAGGED_SHA="${{ github.sha }}" - CONTAINING_BRANCHES=$(git for-each-ref --format='%(refname:short)' refs/remotes/origin --contains "$TAGGED_SHA") - - ON_MASTER="false" - ON_RELEASE="false" - while IFS= read -r branch; do - [[ "$branch" == "origin/master" ]] && ON_MASTER="true" - [[ "$branch" == origin/release/* ]] && ON_RELEASE="true" - done <<< "$CONTAINING_BRANCHES" - - if [[ "$VERSION" == *-* ]]; then - if [[ "$ON_RELEASE" != "true" ]]; then - echo "ERROR: Pre-release version $VERSION must be on origin/release/*" - exit 1 - fi - else - if [[ "$ON_MASTER" != "true" ]]; then - echo "ERROR: Stable version $VERSION must be on origin/master" - exit 1 - fi + git fetch origin master + if ! git merge-base --is-ancestor "${{ github.sha }}" origin/master; then + echo "ERROR: NuGet.org releases must be reachable from origin/master." + exit 1 + fi + if [[ -n "$MANUAL_VERSION" && "$REF" != "refs/heads/master" ]]; then + echo "ERROR: Manual NuGet.org releases must be dispatched from master." + exit 1 fi fi @@ -136,10 +122,10 @@ jobs: required = [ "buildTransitive/Kapusch.Facebook.iOS.targets", "kfb.xcframework/Info.plist", + "kfbshare.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", ] @@ -155,6 +141,10 @@ jobs: print("ERROR: wrapper appears flattened (found 'Info.plist' at package root).") sys.exit(1) + if any(name.startswith("fb/FBSDKGamingServicesKit.xcframework/") for name in names): + print("ERROR: GamingServicesKit must not be packaged.") + sys.exit(1) + print("OK: nupkg layout looks correct.") PY diff --git a/AGENTS.md b/AGENTS.md index ceae60f..4dd0fdc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # Kapusch.FacebookApisForiOSComponents — AI Working Agreement ## Goals -- Produce a reproducible iOS NuGet package for Facebook Login interop. +- Produce a reproducible iOS NuGet package for selectable Facebook Login and Share interop. - Do not commit secrets. ## Packaging constraints @@ -19,3 +19,9 @@ ## Safety - Do not add new dependency ingestion paths without documenting them in `README.md`. - Do not commit real app ids/secrets. + +## Branches and releases +- `master` is the only long-lived branch and the source of every NuGet.org release. +- Implement changes on short-lived branches and target `master` through a PR. Never implement directly on `release/*`. +- Both stable (`vX.Y.Z`) and prerelease (`vX.Y.Z-rc.N`) tags must reference commits reachable from `origin/master`. +- Manual runs without a version publish previews to GitHub Packages. See `Docs/Release.md` for the complete contract. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 426b8d9..2eaddf3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,9 +32,12 @@ Pack the NuGet: ## Pull requests - Keep PRs focused and well-scoped. +- Branch from `master` and target `master`; this repository does not use long-lived `release/*` branches. - Do not commit secrets. - If you update the Facebook SDK version, update `Package.swift` and `Package.resolved` together. +See `Docs/Release.md` before changing versions, tags, or publishing workflows. + ## License By contributing, you agree that your contributions will be licensed under the repository license (MIT). diff --git a/Docs/Integration.md b/Docs/Integration.md index 1e0b4d8..b9fbe32 100644 --- a/Docs/Integration.md +++ b/Docs/Integration.md @@ -28,7 +28,22 @@ Call the interop hooks in your `AppDelegate`: - On launch: `NativeFacebookLogin.Initialize(app, options)` - On URL open: `NativeFacebookLogin.HandleOpenUrl(app, url, options)` -## 3) Limited Login +These hooks apply to `KapuschFacebookFeatures=Login`. For Share-only builds, +disable Meta automatic initialization and call +`NativeFacebookShare.ConfigureAndInitialize(app.Handle, trackingAllowed)` only +after the ATT decision, immediately before the first share. Then call +`NativeFacebookShare.SharePhotoAsync(...)`; no caption is accepted. + +## 3) Feature selection + +- `Login` (default): Login wrapper and LoginKit. +- `Share`: photo-share wrapper and ShareKit. +- `Login;Share`: both managed/native entry points. + +At SDK 18.0.2, ShareKit's CoreKit dependency requires FBAEMKit. Treat this as a +privacy-review blocker if the consumer's artifact policy excludes FBAEMKit. + +## 4) Limited Login For Limited Login, pass: - `FacebookTrackingMode.Limited` @@ -38,7 +53,7 @@ The result can contain: - `AuthenticationToken` - `Nonce` -## 4) Secrets policy +## 5) 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/Release.md b/Docs/Release.md new file mode 100644 index 0000000..665e3d4 --- /dev/null +++ b/Docs/Release.md @@ -0,0 +1,23 @@ +# Release workflow + +`master` is the only long-lived branch. Feature work uses short-lived branches and pull requests targeting `master`; this repository does not create `release/*` branches. + +## Channels + +- Manual workflow run without `manual_version`: build a preview package and publish it to GitHub Packages. +- `vX.Y.Z-rc.N` tag reachable from `origin/master`: publish that prerelease to NuGet.org. +- `vX.Y.Z` tag reachable from `origin/master`: publish that stable version to NuGet.org. +- Manual workflow run with `manual_version`: publish to NuGet.org only when the workflow runs from `master`. + +NuGet versions and Git tags are immutable. Query the official NuGet index before publishing and never move or recreate an existing tag. + +## Release sequence + +1. Fetch `origin`, start a short-lived branch from `origin/master`, and verify the worktree is clean. +2. Build the Swift wrappers, collect the locked Facebook xcframeworks, pack the NuGet and validate its module selection. +3. Open a PR to `master` and require CI to pass. +4. Merge and fetch the resulting `origin/master` commit. +5. Create the RC or stable tag on that exact commit and push only the new tag. +6. Verify the publish workflow and the resulting NuGet package before updating consumers. + +Do not delete historical tags. Delete a temporary branch only after every retained commit is reachable from `origin/master`. diff --git a/README.md b/README.md index e296c36..03436e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FacebookApisForiOSComponents -Public OSS repository that packages **Facebook Login for iOS** into a consumable .NET NuGet. +Public OSS repository that packages selected Facebook iOS features into a consumable .NET NuGet. ## Package @@ -9,10 +9,18 @@ Public OSS repository that packages **Facebook Login for iOS** into a consumable ## What this repo ships A NuGet package that: -- provides a small managed API for **Facebook Login (iOS)**, and +- provides managed APIs for **Facebook Login** and **Facebook photo sharing**, 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. +Select features with `KapuschFacebookFeatures=Login`, `Share`, or `Login;Share`. +The default remains `Login` for compatibility. + +Facebook iOS SDK 18.0.2 links `FBSDKCoreKit` to `FBAEMKit` non-weakly. Therefore +`Share` currently includes `ShareKit`, `CoreKit`, `CoreKit_Basics` and `FBAEMKit`. +A consumer that forbids `FBAEMKit` must block Store delivery; this repository +does not strip or conceal the dependency. + ## Third-party licenses See `THIRD_PARTY_NOTICES.md`. @@ -21,6 +29,7 @@ See `THIRD_PARTY_NOTICES.md`. - Formatting: `Docs/Formatting.md` - Source mode: `Docs/SourceMode.md` +- Release workflow: `Docs/Release.md` - Samples: `samples/README.md` ## Build (local) @@ -49,22 +58,13 @@ Pack the NuGet: - PR CI is build-only. - Publishing is handled by `.github/workflows/publish.yml` with channel routing: - tag `vX.Y.Z` on `master` -> NuGet.org (stable) - - tag `vX.Y.Z-rc.N` on `release/*` -> NuGet.org (pre-release) + - tag `vX.Y.Z-rc.N` on `master` -> NuGet.org (pre-release) - non-tag runs (`workflow_dispatch`) -> GitHub Packages (`X.Y.Z-preview..`) - - `workflow_dispatch` with `manual_version` -> NuGet.org (forced version) + - `workflow_dispatch` on `master` with `manual_version` -> NuGet.org (forced version) - NuGet.org publishing uses NuGet Trusted Publishing (OIDC via `NuGet/login@v1`), no long-lived NuGet API key. ### Required GitHub secret - `NUGET_USER`: your nuget.org profile username (not email), used by `NuGet/login@v1`. -## Release examples - -- Pre-release candidate from a release branch: - - `git checkout release/1.0.0` - - `git tag v1.0.0-rc.1` - - `git push origin v1.0.0-rc.1` -- Stable release from master: - - `git checkout master` - - `git tag v1.0.0` - - `git push origin v1.0.0` +See `Docs/Release.md` for the validated release sequence. Historical tags remain immutable even when obsolete branches are deleted. diff --git a/global.json b/global.json index 54b6d85..512142d 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "version": "10.0.100", - "rollForward": "patch" + "rollForward": "latestFeature" } } diff --git a/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs index 3488f50..c9d4dcc 100644 --- a/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs +++ b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookLogin.iOS.cs @@ -5,8 +5,6 @@ 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) @@ -14,7 +12,7 @@ public static void Initialize(IntPtr uiApplicationHandle, IntPtr launchOptionsHa try { - KfiFacebookInitialize( + ResolveInitialize()( uiApplicationHandle, launchOptionsHandle ); @@ -36,11 +34,11 @@ IntPtr optionsHandle try { - return KfiFacebookHandleOpenUrl( + return ResolveHandleOpenUrl()( uiApplicationHandle, nsUrlHandle, optionsHandle - ); + ) != 0; } catch { @@ -68,13 +66,29 @@ public static Task SignInAsync( var gch = GCHandle.Alloc(tcs); var context = GCHandle.ToIntPtr(gch); - KfiFacebookSignInStart( - presentingViewControllerHandle, - (int)trackingMode, - rawNonce, - &KfiFacebookCallback, - context - ); + var noncePointer = string.IsNullOrEmpty(rawNonce) + ? IntPtr.Zero + : Marshal.StringToCoTaskMemUTF8(rawNonce); + try + { + ResolveSignInStart()( + presentingViewControllerHandle, + (int)trackingMode, + noncePointer, + &KfiFacebookCallback, + context + ); + } + catch + { + gch.Free(); + throw; + } + finally + { + if (noncePointer != IntPtr.Zero) + Marshal.FreeCoTaskMem(noncePointer); + } _ = cancellationToken.Register(() => tcs.TrySetResult(new NativeFacebookSignInResult(NativeSignInStatus.Cancelled)) @@ -87,7 +101,7 @@ public static void SignOut() { try { - KfiFacebookSignOut(); + ResolveSignOut()(); } catch { @@ -130,35 +144,32 @@ IntPtr context } } - [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, + private static IntPtr Resolve(string symbol) => + NativeLibrary.GetExport(NativeLibrary.GetMainProgramHandle(), symbol); + + private static delegate* unmanaged[Cdecl] ResolveInitialize() => + (delegate* unmanaged[Cdecl])Resolve("kfb_facebook_initialize"); + + private static delegate* unmanaged[Cdecl] ResolveHandleOpenUrl() => + (delegate* unmanaged[Cdecl])Resolve( + "kfb_facebook_handle_open_url" + ); + + private static delegate* unmanaged[Cdecl]< + IntPtr, + int, + IntPtr, + delegate* unmanaged[Cdecl], + IntPtr, + void> ResolveSignInStart() => + (delegate* unmanaged[Cdecl]< IntPtr, + int, IntPtr, + delegate* unmanaged[Cdecl], IntPtr, - void> callback, - IntPtr context - ); + void>)Resolve("kfb_facebook_signin_start"); - [DllImport(LibraryName, EntryPoint = "kfb_facebook_signout")] - private static extern void KfiFacebookSignOut(); + private static delegate* unmanaged[Cdecl] ResolveSignOut() => + (delegate* unmanaged[Cdecl])Resolve("kfb_facebook_signout"); } diff --git a/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookShare.iOS.cs b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookShare.iOS.cs new file mode 100644 index 0000000..0774c69 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookShare.iOS.cs @@ -0,0 +1,98 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Kapusch.Facebook.iOS; + +public static unsafe class NativeFacebookShare +{ + public static void ConfigureAndInitialize( + IntPtr uiApplicationHandle, + bool trackingAllowed + ) + { + if (uiApplicationHandle == IntPtr.Zero) + throw new ArgumentException("UIApplication is required."); + + ResolveConfigureAndInitialize()(uiApplicationHandle, trackingAllowed ? (byte)1 : (byte)0); + } + + public static Task SharePhotoAsync( + IntPtr presentingViewControllerHandle, + string imagePath, + CancellationToken cancellationToken = default + ) + { + if (presentingViewControllerHandle == IntPtr.Zero) + throw new ArgumentException("Presenting view controller is required."); + if (string.IsNullOrWhiteSpace(imagePath)) + throw new ArgumentException("Image path is required.", nameof(imagePath)); + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + var completion = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously + ); + var handle = GCHandle.Alloc(completion); + var imagePathPointer = Marshal.StringToCoTaskMemUTF8(imagePath); + try + { + ResolveSharePhoto()( + presentingViewControllerHandle, + imagePathPointer, + &ShareCallback, + GCHandle.ToIntPtr(handle) + ); + } + catch + { + handle.Free(); + throw; + } + finally + { + Marshal.FreeCoTaskMem(imagePathPointer); + } + return completion.Task; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void ShareCallback(int status, IntPtr errorCode, IntPtr context) + { + var handle = GCHandle.FromIntPtr(context); + var completion = (TaskCompletionSource)handle.Target!; + try + { + completion.TrySetResult( + new NativeFacebookShareResult( + (NativeFacebookShareStatus)status, + Marshal.PtrToStringUTF8(errorCode) + ) + ); + } + finally + { + handle.Free(); + } + } + + private static IntPtr Resolve(string symbol) => + NativeLibrary.GetExport(NativeLibrary.GetMainProgramHandle(), symbol); + + private static delegate* unmanaged[Cdecl] ResolveConfigureAndInitialize() => + (delegate* unmanaged[Cdecl])Resolve( + "kfb_facebook_share_configure_and_initialize" + ); + + private static delegate* unmanaged[Cdecl]< + IntPtr, + IntPtr, + delegate* unmanaged[Cdecl], + IntPtr, + void> ResolveSharePhoto() => + (delegate* unmanaged[Cdecl]< + IntPtr, + IntPtr, + delegate* unmanaged[Cdecl], + IntPtr, + void>)Resolve("kfb_facebook_share_photo"); +} diff --git a/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookShareResult.cs b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookShareResult.cs new file mode 100644 index 0000000..fd8f242 --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Facebook/NativeFacebookShareResult.cs @@ -0,0 +1,13 @@ +namespace Kapusch.Facebook.iOS; + +public enum NativeFacebookShareStatus +{ + Success, + Cancelled, + Failed, +} + +public sealed record NativeFacebookShareResult( + NativeFacebookShareStatus Status, + string? ErrorCode = null +); diff --git a/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj b/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj index 2284075..e7a2f17 100644 --- a/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj +++ b/src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj @@ -16,12 +16,16 @@ $(NoWarn);NU5128 Kapusch.Facebook.iOS Kapusch.Facebook.iOS - 1.0.0 + 1.1.0 nuget-readme.md Native/iOS/build/kfb.xcframework + Native/iOS/build/kfbshare.xcframework Native/iOS/build/fb + + + @@ -58,6 +65,10 @@ Condition="'$(InteropIosWrapperDir)' == '' or !Exists('$(InteropIosWrapperDir)')" Text="Missing iOS native wrapper. Build it first via src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh or provide InteropIosWrapperDir." /> + ?, + UnsafeMutableRawPointer +) -> Void + +private enum ShareStatus: Int32 { + case success = 0 + case cancelled = 1 + case failed = 2 +} + +private func invokeShareCallback( + _ callback: KapuschFacebookShareCallback, + status: ShareStatus, + errorCode: String? = nil, + context: UnsafeMutableRawPointer +) { + guard let errorCode else { + callback(status.rawValue, nil, context) + return + } + errorCode.withCString { callback(status.rawValue, $0, context) } +} + +private final class ShareDelegate: NSObject, SharingDelegate { + let callback: KapuschFacebookShareCallback + let context: UnsafeMutableRawPointer + + init(callback: @escaping KapuschFacebookShareCallback, context: UnsafeMutableRawPointer) { + self.callback = callback + self.context = context + } + + func sharer(_ sharer: Sharing, didCompleteWithResults results: [String: Any]) { + invokeShareCallback(callback, status: .success, context: context) + ShareState.clear() + } + + func sharer(_ sharer: Sharing, didFailWithError error: Error) { + let nsError = error as NSError + invokeShareCallback( + callback, + status: .failed, + errorCode: "\(nsError.domain):\(nsError.code)", + context: context + ) + ShareState.clear() + } + + func sharerDidCancel(_ sharer: Sharing) { + invokeShareCallback(callback, status: .cancelled, context: context) + ShareState.clear() + } +} + +private final class ShareState { + nonisolated(unsafe) static var dialog: ShareDialog? + nonisolated(unsafe) static var delegate: ShareDelegate? + nonisolated(unsafe) static var isSdkInitialized = false + + static func clear() { + dialog = nil + delegate = nil + } +} + +@_cdecl("kfb_facebook_share_configure_and_initialize") +public func kfb_facebook_share_configure_and_initialize( + _ applicationPtr: UnsafeMutableRawPointer, + _ trackingAllowed: Bool +) { + Settings.shared.isAutoLogAppEventsEnabled = false + Settings.shared.isAdvertiserIDCollectionEnabled = trackingAllowed + Settings.shared.isAdvertiserTrackingEnabled = trackingAllowed + guard !ShareState.isSdkInitialized else { + return + } + + let application = Unmanaged + .fromOpaque(applicationPtr) + .takeUnretainedValue() + _ = ApplicationDelegate.shared.application( + application, + didFinishLaunchingWithOptions: nil + ) + ShareState.isSdkInitialized = true +} + +@_cdecl("kfb_facebook_share_photo") +public func kfb_facebook_share_photo( + _ presentingViewControllerPtr: UnsafeMutableRawPointer, + _ imagePathPtr: UnsafePointer, + _ callback: @escaping KapuschFacebookShareCallback, + _ context: UnsafeMutableRawPointer +) { + guard ShareState.dialog == nil else { + invokeShareCallback( + callback, + status: .failed, + errorCode: "already_in_progress", + context: context + ) + return + } + + let imagePath = String(cString: imagePathPtr) + guard let image = UIImage(contentsOfFile: imagePath) else { + invokeShareCallback( + callback, + status: .failed, + errorCode: "image_decode_failed", + context: context + ) + return + } + + let presenting = Unmanaged + .fromOpaque(presentingViewControllerPtr) + .takeUnretainedValue() + let photo = SharePhoto(image: image, isUserGenerated: false) + let content = SharePhotoContent() + content.photos = [photo] + let delegate = ShareDelegate(callback: callback, context: context) + let dialog = ShareDialog( + viewController: presenting, + content: content, + delegate: delegate + ) + ShareState.delegate = delegate + ShareState.dialog = dialog + if !dialog.show() { + ShareState.clear() + invokeShareCallback( + callback, + status: .failed, + errorCode: "share_dialog_unavailable", + context: context + ) + } +} diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/include/KapuschFacebookShareInterop.h b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/include/KapuschFacebookShareInterop.h new file mode 100644 index 0000000..dfd030c --- /dev/null +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/KapuschFacebookAuthInterop/include/KapuschFacebookShareInterop.h @@ -0,0 +1 @@ +// The public interop surface is exported through @_cdecl Swift symbols and consumed by .NET P/Invoke. diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh index 15816b9..7a5f018 100755 --- a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh @@ -6,6 +6,7 @@ PACKAGE_DIR="$ROOT_DIR/KapuschFacebookAuthInterop" BUILD_DIR="$ROOT_DIR/build" XCFRAMEWORK_OUT="$BUILD_DIR/kfb.xcframework" +SHARE_XCFRAMEWORK_OUT="$BUILD_DIR/kfbshare.xcframework" rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" @@ -44,6 +45,10 @@ SIM_ARM64_LIB="$(find "$SCRATCH_DIR/iphonesimulator-arm64" -maxdepth 4 -path "*/ 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" +SHARE_IOS_LIB="$(find "$SCRATCH_DIR/iphoneos" -maxdepth 4 -path "*/release/libKapuschFacebookShareInterop.a" | head -n 1)" +SHARE_SIM_ARM64_LIB="$(find "$SCRATCH_DIR/iphonesimulator-arm64" -maxdepth 4 -path "*/release/libKapuschFacebookShareInterop.a" | head -n 1)" +SHARE_SIM_X64_LIB="$(find "$SCRATCH_DIR/iphonesimulator-x86_64" -maxdepth 4 -path "*/release/libKapuschFacebookShareInterop.a" | head -n 1)" +SHARE_SIM_UNIVERSAL_LIB="$BUILD_DIR/libKapuschFacebookShareInterop_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 @@ -56,6 +61,7 @@ if [ ! -f "$SIM_X64_LIB" ]; then fi lipo -create "$SIM_ARM64_LIB" "$SIM_X64_LIB" -output "$SIM_UNIVERSAL_LIB" +lipo -create "$SHARE_SIM_ARM64_LIB" "$SHARE_SIM_X64_LIB" -output "$SHARE_SIM_UNIVERSAL_LIB" HEADERS_DIR="$PACKAGE_DIR/include" @@ -69,10 +75,29 @@ if [ ! -f "$SIM_UNIVERSAL_LIB" ]; then exit 1 fi +if [ -z "$SHARE_IOS_LIB" ] || [ ! -f "$SHARE_IOS_LIB" ]; then + echo "Expected share iOS static library not found: $SHARE_IOS_LIB" >&2 + exit 1 +fi + +if [ ! -f "$SHARE_SIM_UNIVERSAL_LIB" ]; then + echo "Expected share simulator static library not found: $SHARE_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 "[KapuschFacebookShareInterop] Creating xcframework..." +xcodebuild -create-xcframework \ + -library "$SHARE_IOS_LIB" -headers "$HEADERS_DIR" \ + -library "$SHARE_SIM_UNIVERSAL_LIB" -headers "$HEADERS_DIR" \ + -output "$SHARE_XCFRAMEWORK_OUT" + +bash "$ROOT_DIR/collect-facebook-xcframeworks.sh" + echo "[KapuschFacebookAuthInterop] Done: $XCFRAMEWORK_OUT" +echo "[KapuschFacebookShareInterop] Done: $SHARE_XCFRAMEWORK_OUT" diff --git a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh index c73a6cb..26050ef 100755 --- a/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh +++ b/src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh @@ -19,7 +19,6 @@ frameworks=( "FBAEMKit.xcframework" "FBSDKCoreKit.xcframework" "FBSDKCoreKit_Basics.xcframework" - "FBSDKGamingServicesKit.xcframework" "FBSDKLoginKit.xcframework" "FBSDKShareKit.xcframework" ) diff --git a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props index 04323da..2dd050c 100644 --- a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props +++ b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.props @@ -1,5 +1,9 @@ + Login + <_KapuschFacebookFeaturesDelimited>;$([System.String]::Copy('$(KapuschFacebookFeatures)').Replace(' ', '')); + <_KapuschFacebookLoginEnabled>$([System.String]::Copy('$(_KapuschFacebookFeaturesDelimited)').Contains(';Login;')) + <_KapuschFacebookShareEnabled>$([System.String]::Copy('$(_KapuschFacebookFeaturesDelimited)').Contains(';Share;')) false diff --git a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets index b58bd77..3b64a5e 100644 --- a/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets +++ b/src/Kapusch.FacebookApisForiOSComponents/buildTransitive/Kapusch.Facebook.iOS.targets @@ -12,21 +12,32 @@ > <_KapuschFacebookInteropIosWrapper>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropSourceRoot)', 'kfb.xcframework')) + <_KapuschFacebookShareInteropIosWrapper>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropSourceRoot)', 'kfbshare.xcframework')) <_KapuschFacebookInteropFbDir>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropSourceRoot)', 'fb')) + <_KapuschFacebookInteropSlice Condition="$([System.String]::Copy('$(RuntimeIdentifier)').StartsWith('iossimulator'))">ios-arm64_x86_64-simulator + <_KapuschFacebookInteropSlice Condition="'$(_KapuschFacebookInteropSlice)' == ''">ios-arm64 + - + Static true + + Static + true + Framework true @@ -39,18 +50,19 @@ Framework true - - Framework - true - - - Framework - true - - + + Framework + true + + Framework true + + + + + <_KapuschFacebookInteropIosWrapper>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropPackageRoot)', 'kfb.xcframework')) + <_KapuschFacebookShareInteropIosWrapper>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropPackageRoot)', 'kfbshare.xcframework')) <_KapuschFacebookInteropFbDir>$([System.IO.Path]::Combine('$(_KapuschFacebookInteropPackageRoot)', 'fb')) + <_KapuschFacebookInteropSlice Condition="$([System.String]::Copy('$(RuntimeIdentifier)').StartsWith('iossimulator'))">ios-arm64_x86_64-simulator + <_KapuschFacebookInteropSlice Condition="'$(_KapuschFacebookInteropSlice)' == ''">ios-arm64 - + Static true + + Static + true + Framework true @@ -79,18 +98,19 @@ 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 index 8787cb4..c6b26c3 100644 --- a/src/Kapusch.FacebookApisForiOSComponents/nuget-readme.md +++ b/src/Kapusch.FacebookApisForiOSComponents/nuget-readme.md @@ -1,7 +1,10 @@ # Kapusch.Facebook.iOS This package provides: -- a small managed API to trigger Facebook Login on iOS, and +- managed APIs for Facebook Login and photo sharing on iOS, and - the required Facebook iOS SDK `xcframework`s packaged for .NET iOS. +Set `KapuschFacebookFeatures` to `Login` (default), `Share`, or `Login;Share`. +SDK 18.0.2 requires FBAEMKit whenever ShareKit/CoreKit is selected. + See the repo `README.md` and `Docs/Integration.md` for integration steps.