Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: native

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: native-${{ github.ref }}
cancel-in-progress: true

jobs:
ios:
name: iOS bridge (SPM)
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Show Xcode + iOS SDK
# Float with whatever Xcode the macos-15 image ships (currently 16.x);
# SuperwallKit 4.15+ won't compile on Xcode 15.4.
run: xcodebuild -version && xcodebuild -showsdks | grep -i ios

- name: Cache SPM + DerivedData
uses: actions/cache@v4
with:
path: ci~/ios-harness/.build
key: xcb-ios-${{ runner.os }}-${{ hashFiles('ci~/ios-harness/Package.swift') }}
restore-keys: xcb-ios-${{ runner.os }}-

- name: xcodebuild (iOS SDK)
working-directory: ci~/ios-harness
# swift build would target the macOS SDK and fail on UIKit imports inside
# SuperwallKit. xcodebuild with an iOS destination picks the iOS SDK.
run: |
xcodebuild build \
-scheme SuperwallBridgeHarness \
-destination 'generic/platform=iOS' \
-derivedDataPath .build \
-skipPackagePluginValidation

android:
name: Android lib (Gradle)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- uses: android-actions/setup-android@v3
with:
packages: 'platforms;android-34 build-tools;34.0.0'

- uses: gradle/actions/setup-gradle@v3
with:
gradle-version: '8.7'

- name: assembleRelease
working-directory: ci~/android-harness
run: gradle :SuperwallSDK:assembleRelease --no-daemon --stacktrace
101 changes: 101 additions & 0 deletions .github/workflows/unity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Parked: full Unity build + test pipeline.
#
# This repo is the UPM package only — there is no Assets/ or ProjectSettings/
# here, so a Unity build needs a wrapping test project. Two ways to wire that:
#
# 1. Clone a separate Superwall-Unity-TestApp repo into ./_test-project, then
# replace its Packages/com.superwall.sdk/ with the current checkout (rsync
# or a symlink) before invoking game-ci.
# 2. Commit a minimal wrapping project to this repo under ./_test-project
# (only Assets/, ProjectSettings/, Packages/manifest.json) and reference
# com.superwall.sdk via a "file:.." path in manifest.json.
#
# Required secrets (https://game.ci/docs/github/activation):
# UNITY_LICENSE - contents of the .ulf produced by manual activation
# UNITY_EMAIL - Unity account email
# UNITY_PASSWORD - Unity account password
#
# To enable: implement the wrapping project step below, add the triggers you
# want (push / pull_request), and verify the unityVersion image exists on
# https://hub.docker.com/r/unityci/editor/tags for the editor pinned in the
# test project's ProjectSettings/ProjectVersion.txt.

name: unity

on:
workflow_dispatch:

concurrency:
group: unity-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: EditMode + PlayMode tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: true

# TODO: materialize the wrapping Unity project at ./_test-project here
# (clone or rsync), then point projectPath: _test-project on the steps below.

- uses: actions/cache@v4
with:
path: _test-project/Library
key: Library-test-${{ hashFiles('_test-project/Assets/**', '_test-project/Packages/**', '_test-project/ProjectSettings/**', '**/*.cs', 'Plugins/**') }}
restore-keys: Library-test-

- uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
projectPath: _test-project
unityVersion: auto
testMode: all
githubToken: ${{ secrets.GITHUB_TOKEN }}

build:
name: Build ${{ matrix.targetPlatform }}
runs-on: ${{ matrix.os }}
needs: test
strategy:
fail-fast: false
matrix:
include:
- targetPlatform: Android
os: ubuntu-latest
- targetPlatform: iOS
os: macos-14
steps:
- uses: actions/checkout@v4
with:
lfs: true

# TODO: same wrapping-project step as above.

- uses: actions/cache@v4
with:
path: _test-project/Library
key: Library-${{ matrix.targetPlatform }}-${{ hashFiles('_test-project/Assets/**', '_test-project/Packages/**', '_test-project/ProjectSettings/**', '**/*.cs', 'Plugins/**') }}
restore-keys: |
Library-${{ matrix.targetPlatform }}-
Library-

- uses: game-ci/unity-builder@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
projectPath: _test-project
unityVersion: auto
targetPlatform: ${{ matrix.targetPlatform }}

- uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.targetPlatform }}
path: _test-project/build/${{ matrix.targetPlatform }}
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
.DS_Store

# CI harness build outputs — see ci~/ios-harness and ci~/android-harness.
# (The ci~ folder is tilde-suffixed so Unity ignores it during package import.)
ci~/ios-harness/.build/
ci~/ios-harness/.swiftpm/
ci~/ios-harness/Package.resolved
ci~/android-harness/.gradle/
ci~/android-harness/build/
ci~/android-harness/**/build/
ci~/android-harness/local.properties

# Parked Unity workflow materializes a wrapping project here at CI time.
_test-project/
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
# Changelog
All notable changes to this package will be documented in this file.

## [0.2.4]

### Fixes
* Fix configure result emitting an unknown error on success
* iOS `_SuperwallBridge_ShowAlert` ABI mismatch (3 pointers vs C# `(string)`) — now takes a single `alertJson` string matching the C# extern. iOS additionally fires the `onCloseCallbackId` immediately so the pending C# callback is cleaned up.
* Android `showAlert` was reading `actionCallbackId` / `closeCallbackId`, but C# emits `onActionCallbackId` / `onCloseCallbackId` — alert callbacks never fired. Fixed.
* iOS bridge: removed access to internal `Superwall.shared.options` — `SetLocalResources` now stashes the resource map and applies it via `SuperwallOptions.localResources` during `Configure` (must be called before `Configure` on iOS; logs a warning otherwise).
* Post-build `pod install` now locates `pod` across common install paths (`/usr/local/bin`, `/opt/homebrew/bin`, rbenv/asdf shims, `which pod` under a login shell) and runs under a login shell so user PATH from `~/.zshrc`/`~/.bash_profile` is honored. Clear manual-install instructions on failure.
* Post-build `pod install` now sets `LANG=en_US.UTF-8` and `LC_ALL=en_US.UTF-8` in the subprocess so CocoaPods no longer crashes with `Encoding::CompatibilityError` when Unity is launched from Finder (inherits launchd's empty `LANG`).

### Tests
* Added `Tests/Runtime/BridgeContractTests.cs` covering the async-response contract, Configure success/failure paths, and the delegate/handler payload shapes for the regressions fixed in this release.

### CI
* Added `.github/workflows/native.yml` — builds the iOS Swift bridge via SPM on macOS and the Android `.androidlib` via Gradle on Ubuntu, on every push and PR. No Unity license required.
* Added parked `.github/workflows/unity.yml` for full game-ci Unity build + test runs, documented with the licensing secrets and wrapping-project steps needed to enable it.
* CI harness scaffolding lives under `ci~/` (tilde-suffixed so Unity ignores it during package import — never copied into player builds).

### Compatibility
* Minimum Unity version lowered from `6000.4` to `6000.3`.

## [0.2.3]

## Enhancements
Expand Down
94 changes: 84 additions & 10 deletions Editor/SuperwallPostBuildProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,100 @@ private static void PostProcessIOS(string buildPath)

project.WriteToFile(projPath);

// Auto-run pod install
RunPodInstall(buildPath);
}

private static void RunPodInstall(string buildPath)
{
string podPath = LocatePodExecutable();
if (podPath == null)
{
Debug.LogWarning(
"[Superwall] Could not locate the CocoaPods 'pod' executable. " +
$"Run manually:\n\n cd \"{buildPath}\" && pod install\n\n" +
"If you don't have CocoaPods installed: 'sudo gem install cocoapods' " +
"or 'brew install cocoapods'.");
return;
}

var process = new System.Diagnostics.Process();
process.StartInfo.FileName = "/bin/bash";
process.StartInfo.Arguments = $"-c \"cd '{buildPath}' && pod install\"";
// Login shell so user PATH from ~/.zshrc / ~/.bash_profile is loaded (rbenv/asdf shims, Homebrew).
process.StartInfo.Arguments = $"-l -c \"cd '{buildPath}' && '{podPath}' install\"";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
// Some Ruby installs (rbenv/asdf) misbehave when these env vars are inherited from Unity.
process.StartInfo.EnvironmentVariables.Remove("MONO_PATH");
process.StartInfo.EnvironmentVariables.Remove("MONO_CFG_DIR");
process.StartInfo.EnvironmentVariables.Remove("DYLD_FALLBACK_LIBRARY_PATH");
process.StartInfo.EnvironmentVariables.Remove("DYLD_LIBRARY_PATH");
// CocoaPods crashes with Encoding::CompatibilityError under non-UTF-8 locales —
// Unity launched from Finder inherits launchd's empty LANG.
process.StartInfo.EnvironmentVariables["LANG"] = "en_US.UTF-8";
process.StartInfo.EnvironmentVariables["LC_ALL"] = "en_US.UTF-8";

if (process.ExitCode == 0)
try
{
Debug.Log($"[Superwall] pod install completed successfully.\n{output}");
process.Start();
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();

if (process.ExitCode == 0)
{
Debug.Log($"[Superwall] pod install completed successfully.\n{output}");
}
else
{
Debug.LogWarning(
$"[Superwall] pod install failed (exit code {process.ExitCode}). " +
$"Run manually:\n\n cd \"{buildPath}\" && pod install\n\n{error}");
}
}
else
catch (System.Exception e)
{
Debug.LogWarning($"[Superwall] pod install failed (exit code {process.ExitCode}). Run manually in: {buildPath}\n{error}");
Debug.LogWarning(
$"[Superwall] Could not run pod install automatically ({e.Message}). " +
$"Run manually:\n\n cd \"{buildPath}\" && pod install");
}
}

private static string LocatePodExecutable()
{
string home = System.Environment.GetEnvironmentVariable("HOME") ?? "";
string[] candidates =
{
"/usr/local/bin/pod",
"/opt/homebrew/bin/pod",
Path.Combine(home, ".rbenv/shims/pod"),
Path.Combine(home, ".asdf/shims/pod"),
"/usr/bin/pod",
};
foreach (var path in candidates)
{
if (!string.IsNullOrEmpty(path) && File.Exists(path)) return path;
}

// Fall back to `which pod` under a login shell.
try
{
var which = new System.Diagnostics.Process();
which.StartInfo.FileName = "/bin/bash";
which.StartInfo.Arguments = "-l -c \"command -v pod\"";
which.StartInfo.UseShellExecute = false;
which.StartInfo.RedirectStandardOutput = true;
which.StartInfo.RedirectStandardError = true;
which.Start();
string result = which.StandardOutput.ReadToEnd().Trim();
which.WaitForExit();
if (which.ExitCode == 0 && !string.IsNullOrEmpty(result) && File.Exists(result))
{
return result;
}
}
catch { }
return null;
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,28 @@ class SuperwallUnityBridge {

fun setSubscriptionStatus(statusJson: String) {
try {
val type = JSONObject(statusJson).optString("type", "unknown")
val json = JSONObject(statusJson)
val type = json.optString("type", "unknown")
val status: SubscriptionStatus = when (type.lowercase()) {
"active" -> SubscriptionStatus.Active(emptySet())
"active" -> SubscriptionStatus.Active(parseEntitlementsFromJson(json.optJSONArray("entitlements")))
"inactive" -> SubscriptionStatus.Inactive
else -> SubscriptionStatus.Unknown
}
Superwall.instance.setSubscriptionStatus(status)
} catch (_: JSONException) {}
}

private fun parseEntitlementsFromJson(arr: JSONArray?): Set<Entitlement> {
if (arr == null) return emptySet()
val out = mutableSetOf<Entitlement>()
for (i in 0 until arr.length()) {
val o = arr.optJSONObject(i) ?: continue
val id = o.optString("id", null) ?: o.optString("identifier", null) ?: continue
out.add(Entitlement(id = id))
}
return out
}

fun getConfigurationStatus(): String = when (Superwall.instance.configurationState) {
is com.superwall.sdk.config.models.ConfigurationStatus.Configured -> "configured"
is com.superwall.sdk.config.models.ConfigurationStatus.Failed -> "failed"
Expand Down Expand Up @@ -519,8 +531,8 @@ class SuperwallUnityBridge {
val message = json.optString("message", null)
val actionTitle = json.optString("actionTitle", null)
val closeActionTitle = json.optString("closeActionTitle", "Done")
val actionCallbackId = json.optString("actionCallbackId", null)
val closeCallbackId = json.optString("closeCallbackId", null)
val actionCallbackId = json.optString("onActionCallbackId", null)
val closeCallbackId = json.optString("onCloseCallbackId", null)

val action: (() -> Unit)? = actionCallbackId?.let {
{ sendAsyncResponse(it, JSONObject().put("action", "performed")) }
Expand Down
2 changes: 1 addition & 1 deletion Plugins/iOS/SuperwallUnityBridge-Bridging-Header.h.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading