diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1b02c20..1b45c52 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,6 +2,11 @@ name: publish on: workflow_dispatch: + inputs: + manual_version: + description: 'Force a specific version for NuGet.org publish (e.g., 1.0.0 or 1.0.0-rc.1). If empty, defaults to auto-generated preview for GitHub Packages.' + required: false + type: string push: tags: - "v*" @@ -13,6 +18,10 @@ permissions: jobs: pack_and_publish: runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write steps: - uses: actions/checkout@v4 with: @@ -22,18 +31,41 @@ jobs: id: version shell: bash run: | - if [[ "${{ github.ref }}" =~ ^refs/tags/v ]]; then - REF="${{ github.ref }}" + REF="${{ github.ref }}" + MANUAL_VERSION="${{ github.event.inputs.manual_version }}" + CSPROJ="src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.csproj" + BASE_VERSION=$(grep -oE '[^<]+' "$CSPROJ" | head -n 1 | sed 's///' || echo "0.1.0") + + if [[ -n "$MANUAL_VERSION" ]]; then + VERSION="$MANUAL_VERSION" + PUBLISH_TARGET="nuget" + echo "Manual version provided ($VERSION). Triggering NuGet.org publish." + elif [[ "$REF" =~ ^refs/tags/v ]]; then VERSION="${REF#refs/tags/v}" + PUBLISH_TARGET="nuget" else - CSPROJ="src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.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}" + VERSION="${BASE_VERSION}-preview.${RUN_NUMBER}.${COMMIT_SHORT}" + PUBLISH_TARGET="github" + fi + + if [[ "$PUBLISH_TARGET" == "nuget" ]]; then + 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 + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "publish_target=${PUBLISH_TARGET}" >> "$GITHUB_OUTPUT" echo "Package version: ${VERSION}" + echo "Publish target: ${PUBLISH_TARGET}" - uses: actions/setup-dotnet@v4 with: @@ -72,7 +104,23 @@ jobs: -o artifacts/nuget \ /p:PackageVersion="${{ steps.version.outputs.version }}" + - name: Push to NuGet.org + if: steps.version.outputs.publish_target == 'nuget' + uses: NuGet/login@v1 + id: nuget_login + with: + user: ${{ secrets.NUGET_USER }} + + - name: Push to NuGet.org + if: steps.version.outputs.publish_target == 'nuget' + run: | + dotnet nuget push artifacts/nuget/*.nupkg \ + --api-key "${{ steps.nuget_login.outputs.NUGET_API_KEY }}" \ + --source "https://api.nuget.org/v3/index.json" \ + --skip-duplicate + - name: Push to GitHub Packages + if: steps.version.outputs.publish_target == 'github' env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/AGENTS.md b/AGENTS.md index 9aaa645..73941c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # Kapusch.FacebookApisForAndroidComponents — AI Working Agreement ## Goals -- Produce a reproducible Android NuGet package for Facebook Login interop. +- Produce a reproducible Android NuGet package for selectable Facebook Login and Share interop. - Do not commit secrets. ## Packaging constraints @@ -20,3 +20,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 49ec310..e0d26c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,11 +32,14 @@ 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 both: - `src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/build.gradle.kts` - `DependencyLocks/Android/lockstate.txt` (URLs + SHA256) +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/DependencyLocks/Android/lockstate.txt b/DependencyLocks/Android/lockstate.txt index a9b1e48..8070eb7 100644 --- a/DependencyLocks/Android/lockstate.txt +++ b/DependencyLocks/Android/lockstate.txt @@ -5,3 +5,4 @@ https://repo1.maven.org/maven2/com/facebook/android/facebook-login/18.1.3/facebo https://repo1.maven.org/maven2/com/facebook/android/facebook-core/18.1.3/facebook-core-18.1.3.aar ef480916149e078f955195981130b05746ca33b41618911e7093aa32c03d601b facebook-core-18.1.3.aar https://repo1.maven.org/maven2/com/facebook/android/facebook-common/18.1.3/facebook-common-18.1.3.aar 62adfb1cd05184065d69033a9f917709b392004f87a8aa1cc1c4f0e9fb5ca781 facebook-common-18.1.3.aar https://repo1.maven.org/maven2/com/facebook/android/facebook-bolts/18.1.3/facebook-bolts-18.1.3.aar 05c6bae0130487e63add05854a3bbd21465c06a295a2104b3948d8759536b699 facebook-bolts-18.1.3.aar +https://repo1.maven.org/maven2/com/facebook/android/facebook-share/18.1.3/facebook-share-18.1.3.aar cc3bc76fdb987ac245ddc54b37096e2afcfb438872487034027639dd4564aef7 facebook-share-18.1.3.aar diff --git a/Docs/Integration.md b/Docs/Integration.md index 743fe34..ff3cf19 100644 --- a/Docs/Integration.md +++ b/Docs/Integration.md @@ -2,6 +2,8 @@ This package injects a small Android Activity (provided by an embedded AAR) that runs the Facebook Login flow and returns its result via `Intent` extras. +Set `KapuschFacebookFeatures` to `Login` (default), `Share`, or `Login;Share`. + ## Required AndroidManifest entries You must include the required Meta/Facebook Android SDK entries in your app manifest (example only; use resources, do not hardcode secrets): @@ -26,3 +28,11 @@ Then parse the `Intent` extras: ## Sign-out Call `AndroidFacebookInterop.SendSignOutBroadcast(context)`. + +## Photo sharing + +With the `Share` feature selected, start +`AndroidFacebookInterop.ShareActivityClassName` and pass a JPEG file path using +`AndroidFacebookInterop.ExtraShareImagePath`. Read `ExtraStatus` and +`ExtraErrorCode` from the activity result. The wrapper uses `SharePhotoContent` +and never injects a message. diff --git a/Docs/Release.md b/Docs/Release.md new file mode 100644 index 0000000..6d4bebb --- /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 native wrapper, restore the locked AARs, pack the NuGet and inspect its selected modules. +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 410bb98..b0783b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FacebookApisForAndroidComponents -Public OSS repository that packages **Facebook Login for Android** into a consumable .NET NuGet. +Public OSS repository that packages selected Facebook Android features into a consumable .NET NuGet. ## Package @@ -9,10 +9,13 @@ Public OSS repository that packages **Facebook Login for Android** into a consum ## What this repo ships A NuGet package that: -- provides a small managed API for starting a Facebook Login flow on Android, and +- provides managed entry points for Facebook Login and photo sharing, and - redistributes the required **Facebook Android SDK AARs** inside the `.nupkg` (classic/native packaging), - injects the AARs into consuming apps via `buildTransitive` `AndroidAarLibrary` items. +Select `KapuschFacebookFeatures=Login`, `Share`, or `Login;Share`. The default is +`Login`; only selected native AARs are injected into the consuming artifact. + ## Third-party licenses See `THIRD_PARTY_NOTICES.md`. @@ -21,6 +24,7 @@ See `THIRD_PARTY_NOTICES.md`. - Integration: `Docs/Integration.md` - Source mode: `Docs/SourceMode.md` +- Release workflow: `Docs/Release.md` - Samples: `samples/README.md` ## Build (local) @@ -41,10 +45,22 @@ Pack the NuGet: ## Consumption -- Install the package from GitHub Packages (pre-release). +- Install the package from NuGet.org for public release tags. +- Install the package from GitHub Packages for internal preview builds. - Follow `Docs/Integration.md` for required AndroidManifest entries / meta-data. ## CI - PR CI is build-only. -- Publishing is handled by a workflow that pushes a pre-release to GitHub Packages. +- 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 `master` -> NuGet.org (pre-release) + - non-tag runs (`workflow_dispatch`) -> GitHub Packages (`X.Y.Z-preview..`) + - `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`. + +See `Docs/Release.md` for the validated release sequence. Historical tags remain immutable even when obsolete branches are deleted. diff --git a/src/Kapusch.FacebookApisForAndroidComponents/Facebook/AndroidFacebookInterop.cs b/src/Kapusch.FacebookApisForAndroidComponents/Facebook/AndroidFacebookInterop.cs index 14cfe30..d188d2f 100644 --- a/src/Kapusch.FacebookApisForAndroidComponents/Facebook/AndroidFacebookInterop.cs +++ b/src/Kapusch.FacebookApisForAndroidComponents/Facebook/AndroidFacebookInterop.cs @@ -5,12 +5,14 @@ namespace Kapusch.Facebook.Android; public static class AndroidFacebookInterop { public static string LoginActivityClassName => "com.kapusch.facebook.androidinterop.FacebookLoginActivity"; + public static string ShareActivityClassName => "com.kapusch.facebook.androidinterop.FacebookShareActivity"; public static string ExtraStatus => "kfb_status"; public static string ExtraAccessToken => "kfb_access_token"; public static string ExtraUserId => "kfb_user_id"; public static string ExtraErrorCode => "kfb_error_code"; public static string ExtraErrorMessage => "kfb_error_message"; + public static string ExtraShareImagePath => "kfb_share_image_path"; public static string LogoutAction => "com.kapusch.facebook.androidinterop.LOGOUT"; diff --git a/src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.csproj b/src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.csproj index b7c10f3..d18f67f 100644 --- a/src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.csproj +++ b/src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.csproj @@ -15,7 +15,7 @@ $(NoWarn);NU5128 Kapusch.Facebook.Android Kapusch.Facebook.Android - 0.1.0 + 1.1.0 nuget-readme.md + diff --git a/src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/src/main/java/com/kapusch/facebook/androidinterop/FacebookInteropConstants.java b/src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/src/main/java/com/kapusch/facebook/androidinterop/FacebookInteropConstants.java index 332e6f6..19afdf0 100644 --- a/src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/src/main/java/com/kapusch/facebook/androidinterop/FacebookInteropConstants.java +++ b/src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/src/main/java/com/kapusch/facebook/androidinterop/FacebookInteropConstants.java @@ -6,6 +6,7 @@ public final class FacebookInteropConstants { public static final String EXTRA_USER_ID = "kfb_user_id"; public static final String EXTRA_ERROR_CODE = "kfb_error_code"; public static final String EXTRA_ERROR_MESSAGE = "kfb_error_message"; + public static final String EXTRA_SHARE_IMAGE_PATH = "kfb_share_image_path"; public static final String LOGOUT_ACTION = "com.kapusch.facebook.androidinterop.LOGOUT"; diff --git a/src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/src/main/java/com/kapusch/facebook/androidinterop/FacebookShareActivity.java b/src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/src/main/java/com/kapusch/facebook/androidinterop/FacebookShareActivity.java new file mode 100644 index 0000000..aefdc27 --- /dev/null +++ b/src/Kapusch.FacebookApisForAndroidComponents/Native/Android/facebookinterop/src/main/java/com/kapusch/facebook/androidinterop/FacebookShareActivity.java @@ -0,0 +1,95 @@ +package com.kapusch.facebook.androidinterop; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; + +import com.facebook.CallbackManager; +import com.facebook.FacebookCallback; +import com.facebook.FacebookException; +import com.facebook.share.Sharer; +import com.facebook.share.model.SharePhoto; +import com.facebook.share.model.SharePhotoContent; +import com.facebook.share.widget.ShareDialog; + +public final class FacebookShareActivity extends Activity { + private CallbackManager callbackManager; + private Bitmap bitmap; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String imagePath = getIntent().getStringExtra(FacebookInteropConstants.EXTRA_SHARE_IMAGE_PATH); + if (imagePath == null || imagePath.trim().isEmpty()) { + completeFailed("missing_image_path"); + return; + } + + bitmap = BitmapFactory.decodeFile(imagePath); + if (bitmap == null) { + completeFailed("image_decode_failed"); + return; + } + + callbackManager = CallbackManager.Factory.create(); + ShareDialog dialog = new ShareDialog(this); + dialog.registerCallback(callbackManager, new FacebookCallback() { + @Override + public void onSuccess(Sharer.Result result) { + complete("success", RESULT_OK, null); + } + + @Override + public void onCancel() { + complete("cancelled", RESULT_CANCELED, null); + } + + @Override + public void onError(FacebookException error) { + completeFailed(error.getClass().getSimpleName()); + } + }); + + SharePhoto photo = new SharePhoto.Builder().setBitmap(bitmap).build(); + SharePhotoContent content = new SharePhotoContent.Builder().addPhoto(photo).build(); + if (!dialog.canShow(content, ShareDialog.Mode.AUTOMATIC)) { + completeFailed("share_dialog_unavailable"); + return; + } + dialog.show(content, ShareDialog.Mode.AUTOMATIC); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (callbackManager != null) { + callbackManager.onActivityResult(requestCode, resultCode, data); + } + } + + private void completeFailed(String errorCode) { + complete("failed", RESULT_OK, errorCode); + } + + private void complete(String status, int resultCode, String errorCode) { + Intent data = new Intent(); + data.putExtra(FacebookInteropConstants.EXTRA_STATUS, status); + if (errorCode != null) { + data.putExtra(FacebookInteropConstants.EXTRA_ERROR_CODE, errorCode); + } + setResult(resultCode, data); + finish(); + } + + @Override + protected void onDestroy() { + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + bitmap = null; + super.onDestroy(); + } +} diff --git a/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.props b/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.props index 07fe0e9..efcab06 100644 --- a/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.props +++ b/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.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 false diff --git a/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.targets b/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.targets index a90f887..0ec3d5c 100644 --- a/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.targets +++ b/src/Kapusch.FacebookApisForAndroidComponents/buildTransitive/Kapusch.Facebook.Android.targets @@ -32,7 +32,8 @@ - + + @@ -52,7 +53,8 @@ - + + diff --git a/src/Kapusch.FacebookApisForAndroidComponents/nuget-readme.md b/src/Kapusch.FacebookApisForAndroidComponents/nuget-readme.md index 14fbf8d..d4f83c9 100644 --- a/src/Kapusch.FacebookApisForAndroidComponents/nuget-readme.md +++ b/src/Kapusch.FacebookApisForAndroidComponents/nuget-readme.md @@ -2,6 +2,8 @@ This package embeds a small Android interop Activity and the required Facebook Android SDK AARs, and injects them into consuming apps via `buildTransitive` MSBuild. +Set `KapuschFacebookFeatures` to `Login` (default), `Share`, or `Login;Share`. + ## Notes - No secrets are included. - You must configure your AndroidManifest / resources as required by the Facebook Android SDK.