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
44 changes: 26 additions & 18 deletions .github/workflows/ci-maui-iap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ env:

jobs:
compile-check:
name: Compile Check (net9.0 shared)
name: Compile Check (net9.0 / net10.0 shared)
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
Expand All @@ -52,28 +52,30 @@ jobs:
with:
fetch-depth: 1

- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v5
- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2
with:
dotnet-version: '9.0.x'
dotnet-version: "10.0.x"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# The shared `net9.0` build is the fast lane — no AAR / xcframework, no
# The shared `net9.0` / `net10.0` builds are the fast lane — no AAR / xcframework, no
# MAUI workload, no native binding csprojs in the project graph. Two
# things make this work:
# 1. `<UseMaui>` is conditional on having a platform identifier in
# OpenIap.Maui.csproj, so the net9.0 TFM doesn't activate MAUI.
# OpenIap.Maui.csproj, so the shared TFMs don't activate MAUI.
# 2. `-p:TargetFrameworks=net9.0` (PLURAL — overrides the project's
# `<TargetFrameworks>` list to a single TFM). This must be plural,
# not `-p:TargetFramework=net9.0` (singular) or `-f net9.0`: those
# filter the inner BUILD but leave the implicit RESTORE walking
# all 4 TFMs in the multi-target list, which loads the conditional
# all platform TFMs in the multi-target list, which loads the conditional
# ProjectReferences to Bindings.Android / Bindings.iOS for
# restore-time evaluation and triggers NETSDK1147 / NETSDK1178.
- name: Build library (net9.0)
run: dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0 --nologo
- name: Build library (shared)
run: |
dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0 --nologo
dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net10.0 --nologo

android-binding:
name: Android binding (net9.0-android)
name: Android binding (net9.0-android + net10.0-android)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand All @@ -87,10 +89,10 @@ jobs:
distribution: 'temurin'
java-version: '17'

- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v5
- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2
with:
dotnet-version: '9.0.x'
dotnet-version: "10.0.x"

- name: Install MAUI workload
run: dotnet workload install maui-android --skip-sign-check
Expand All @@ -115,11 +117,13 @@ jobs:
- name: Build Android binding + library
working-directory: libraries/maui-iap
run: |
dotnet build src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj --nologo
dotnet build src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj -p:TargetFrameworks=net9.0-android --nologo
dotnet build src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj -p:TargetFrameworks=net10.0-android --nologo
dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0-android --nologo
dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net10.0-android --nologo

ios-binding:
name: iOS binding (net9.0-ios + maccatalyst)
name: iOS binding (net9.0/net10.0 ios + maccatalyst)
runs-on: macos-15
timeout-minutes: 45
steps:
Expand All @@ -132,10 +136,10 @@ jobs:
with:
xcode-version: ${{ env.XCODE_VERSION }}

- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v5
- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2
with:
dotnet-version: '9.0.x'
dotnet-version: "10.0.x"

- name: Install MAUI workload
run: dotnet workload install maui --skip-sign-check
Expand All @@ -155,10 +159,14 @@ jobs:
working-directory: libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS
run: |
dotnet build -p:TargetFrameworks=net9.0-ios --nologo
dotnet build -p:TargetFrameworks=net10.0-ios --nologo
dotnet build -p:TargetFrameworks=net9.0-maccatalyst --nologo
dotnet build -p:TargetFrameworks=net10.0-maccatalyst --nologo

- name: Build library (ios + maccatalyst)
working-directory: libraries/maui-iap/src/OpenIap.Maui
run: |
dotnet build -p:TargetFrameworks=net9.0-ios --nologo
dotnet build -p:TargetFrameworks=net10.0-ios --nologo
dotnet build -p:TargetFrameworks=net9.0-maccatalyst --nologo
dotnet build -p:TargetFrameworks=net10.0-maccatalyst --nologo
32 changes: 18 additions & 14 deletions .github/workflows/release-maui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,30 @@ env:

jobs:
validate:
name: Validate (net9.0 shared)
name: Validate (net9.0 / net10.0 shared)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6

- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v5
- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2
with:
dotnet-version: "9.0.x"
dotnet-version: "10.0.x"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# The shared `net9.0` build catches Types.cs / public-API regressions
# The shared `net9.0` / `net10.0` builds catch Types.cs / public-API regressions
# without needing the MAUI / android / ios workloads. Two things make
# this work — see ci-maui-iap.yml for the same pattern with details.
# Note: `-p:TargetFrameworks=...` is PLURAL (overrides the project's
# multi-target list); the singular `-p:TargetFramework=...` and `-f`
# leave the implicit restore walking all TFMs.
- name: Build library (net9.0)
run: dotnet build libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0
- name: Build library (shared)
run: |
dotnet build libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0
dotnet build libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net10.0

validate-multitarget:
name: Validate (net9.0-android / net9.0-ios / net9.0-maccatalyst)
name: Validate (net9.0/net10.0 platform TFMs)
runs-on: macos-15
timeout-minutes: 60
steps:
Expand All @@ -72,10 +74,10 @@ jobs:
distribution: "temurin"
java-version: "17"

- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v5
- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2
with:
dotnet-version: "9.0.x"
dotnet-version: "10.0.x"

- name: Install MAUI workload
run: dotnet workload install maui --skip-sign-check
Expand All @@ -95,6 +97,8 @@ jobs:
working-directory: libraries/maui-iap/android
run: ../../../packages/google/gradlew :openiap:assembleRelease

# OpenIap.Maui.csproj includes net9.0-android, net10.0-android,
# net9.0-ios, net10.0-ios, net9.0-maccatalyst, and net10.0-maccatalyst.
- name: Build all target frameworks
run: dotnet build libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj

Expand All @@ -118,10 +122,10 @@ jobs:
distribution: "temurin"
java-version: "17"

- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v5
- name: Setup .NET 10 SDK
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2
with:
dotnet-version: "9.0.x"
dotnet-version: "10.0.x"

- name: Install MAUI workload
run: dotnet workload install maui --skip-sign-check
Expand Down
52 changes: 45 additions & 7 deletions knowledge/_claude-context/context.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# OpenIAP Project Context

> **Auto-generated for Claude Code**
> Last updated: 2026-05-16T12:59:43.317Z
> Last updated: 2026-06-15T14:57:16.333Z
>
> Usage: `claude --context knowledge/_claude-context/context.md`

Expand Down Expand Up @@ -861,6 +861,12 @@ The mechanical guardrail for this checklist is:
bun run audit:parity
```

This mirrors CI's **Audit SDK Parity** job and is intentionally run by the
pre-commit hook on every commit. Do not bypass it for docs/version-only changes:
the audit also checks generated docs version metadata and the Godot Android
GDAP dependency pin against `openiap-versions.json`, so release-version drift can
break CI even when no SDK source code changed.

This audit treats `libraries/expo-iap/example` as the non-Godot example SSOT
and fails when:

Expand All @@ -871,11 +877,17 @@ and fails when:
- a GraphQL Query/Mutation/Subscription operation is added or removed without
updating the operation parity registry
- generated types or shared TS runtime helpers drift from `packages/gql`

Run it after type generation and before opening a PR for SDK/API/example
changes. If it fails for a newly introduced operation or feature, update the
missing SDK bridge/example/test coverage first, then update the parity registry
in [`scripts/audit-non-godot-parity.mjs`](../../scripts/audit-non-godot-parity.mjs).
- framework/package version metadata or Godot Android GDAP dependencies drift
from the package/version SSOTs

Run it after type generation, after version syncs, and before opening a PR for
SDK/API/example/docs-version changes. If it fails for a newly introduced
operation or feature, update the missing SDK bridge/example/test coverage first,
then update the parity registry in
[`scripts/audit-non-godot-parity.mjs`](../../scripts/audit-non-godot-parity.mjs).
If it fails for Godot GDAP dependency drift, run
`./libraries/godot-iap/scripts/write-gdap.sh` and commit the regenerated
`libraries/godot-iap/addons/godot-iap/android/GodotIap.gdap`.

### The bug pattern

Expand All @@ -900,7 +912,7 @@ For every new/changed handler in the generated types, verify **all five** of the
| **flutter_inapp_purchase** | `lib/types.dart` (generated) | getter on `FlutterInappPurchase` in `lib/flutter_inapp_purchase.dart` | `case "<name>":` in `ios/Classes/FlutterInappPurchasePlugin.swift`, Android plugin `onMethodCall` | `queryHandlers` / `mutationHandlers` / `subscriptionHandlers` bundles near the bottom of `flutter_inapp_purchase.dart` | Mock + test in `test/ios_methods_test.dart` (and the `errors_unit_test.dart` error-mapping test) |
| **kmp-iap** | `library/src/commonMain/.../openiap/Types.kt` (generated interface) | exposed via `KmpInAppPurchase` / `kmpIapInstance` | `library/src/iosMain/.../InAppPurchaseIOS.kt` — must call `openIapModule.<name>WithCompletion { ... }`, **never** `throw UnsupportedOperationException` | Not required (interface dispatch) | `library/src/commonTest/` if testable cross-platform |
| **godot-iap** | `addons/godot-iap/types.gd` (generated) | public `snake_case` function in `addons/godot-iap/godot_iap.gd` | `ios-gdextension/Sources/GodotIap/GodotIap.swift` (iOS), `android/src/main/java/.../GodotIap.java` (Android) | Not required | Manual testing — no automated test suite yet |
| **maui-iap** | `src/OpenIap.Maui/Types.cs` (generated) | `OpenIap.QueryResolver` / `MutationResolver` interfaces in `Types.cs`; `IOpenIap` adds the listener-stream contract; static facade is `OpenIap.Maui.Iap`; IAPKit helpers mirror TypeScript via `Iap.KitApi(...)`, `Iap.ConnectWebhookStream(...)`, `Iap.ParseWebhookEventData(...)`, and `Iap.WebhookEventTypes` | Android: `OpenIapMauiModule.kt` in `libraries/maui-iap/android/openiap/` (JSON-shaped Java facade over `packages/google`), bound by `OpenIap.Maui.Bindings.Android.csproj`, consumed by `Platforms/Android/OpenIapAndroid.cs`. iOS / macCatalyst: existing `OpenIapModule+ObjC.swift` bridge in `packages/apple`, bound by hand-written `OpenIap.Maui.Bindings.iOS/ApiDefinition.cs`, consumed by `Platforms/iOS/OpenIapIOS.cs` (+ subclass `OpenIapMacCatalyst`). | Not required (interface dispatch) | Example app `libraries/maui-iap/example/OpenIap.Maui.Example` builds for net9.0-android / net9.0-ios / net9.0-maccatalyst (manual device testing for purchase flow); no xUnit tests yet |
| **maui-iap** | `src/OpenIap.Maui/Types.cs` (generated) | `OpenIap.QueryResolver` / `MutationResolver` interfaces in `Types.cs`; `IOpenIap` adds the listener-stream contract; static facade is `OpenIap.Maui.OpenIapClient` (`OpenIap.Maui.Iap` remains as a legacy shim); IAPKit helpers mirror TypeScript via `OpenIapClient.KitApi(...)`, `OpenIapClient.ConnectWebhookStream(...)`, `OpenIapClient.ParseWebhookEventData(...)`, and `OpenIapClient.WebhookEventTypes` | Android: `OpenIapMauiModule.kt` in `libraries/maui-iap/android/openiap/` (JSON-shaped Java facade over `packages/google`), bound by `OpenIap.Maui.Bindings.Android.csproj`, consumed by `Platforms/Android/OpenIapAndroid.cs`. Google Billing / Play Services / Gson / AndroidX / Kotlin dependencies must stay NuGet `PackageReference`s, not fat-bundled AARs. iOS / macCatalyst: existing `OpenIapModule+ObjC.swift` bridge in `packages/apple`, bound by hand-written `OpenIap.Maui.Bindings.iOS/ApiDefinition.cs`, consumed by `Platforms/iOS/OpenIapIOS.cs` (+ subclass `OpenIapMacCatalyst`). | Not required (interface dispatch) | Example app `libraries/maui-iap/example/OpenIap.Maui.Example` builds for net9.0-android / net9.0-ios / net9.0-maccatalyst; package CI builds net9/net10 shared, Android, iOS, and macCatalyst TFMs (manual device testing for purchase flow); no xUnit tests yet |

### Platform suffix rule (who needs what)

Expand Down Expand Up @@ -1864,6 +1876,7 @@ react-native-iap / godot-iap, then the Apple wrapper must also default to
description is the canonical statement.

When changing a default, update:

1. The GraphQL schema description.
2. Re-run `bun run generate`.
3. Every wrapper SDK's `?? <default>` expression and JSDoc / KDoc / etc.
Expand All @@ -1878,6 +1891,7 @@ The audit script greps for fields that don't appear in the type definition
and flags them.

Example failure modes already encountered:

- `BillingProgramAvailabilityResultAndroid` doc listed
`responseCode` + `debugMessage` — neither field exists; the type has
`billingProgram` + `isAvailable`.
Expand All @@ -1903,6 +1917,7 @@ the union is `'browser'` only, but the doc claimed

Anchor links should point to existing pages and section anchors. Common
recent failures:

- "Use verifyPurchase" link pointed to `/docs/apis/get-active-subscriptions`
(totally unrelated).
- `getExternalPurchaseCustomLinkTokenIOS` Returns linked to the
Expand Down Expand Up @@ -1930,6 +1945,7 @@ exactly as Google / Apple states it.
Code examples in doc pages should at minimum parse / type-check against
the wrapper they target. The audit script does NOT yet run a full
TypeScript / Kotlin / Dart parser, but it does:

- Verify imports (`import {…} from 'expo-iap'`) reference symbols that
expo-iap actually exports.
- Verify field accesses on shown objects (e.g. `purchase.purchaseToken`)
Expand Down Expand Up @@ -1961,6 +1977,27 @@ the GitHub Release does not exist yet.
`bun run audit:docs` fails bare package/version entries under published
`Package Releases` blocks so link regressions are caught before publishing.

### R10 — Docs version metadata stays synced with package metadata

`packages/docs/src/lib/versioning.ts` must not import package metadata from
outside `packages/docs`. Vercel uploads the docs package root, so imports such
as `../../../../libraries/expo-iap/package.json?raw` pass locally but fail in
Vercel builds.

Framework package versions and Android SDK constants used by docs must flow
through `packages/docs/src/generated/version-metadata.json`, which is generated
by `scripts/sync-versions.sh` from the real SSOT files:

- Expo / React Native: each library `package.json`
- Flutter: `libraries/flutter_inapp_purchase/pubspec.yaml`
- Godot: `libraries/godot-iap/addons/godot-iap/plugin.cfg`
- KMP: `libraries/kmp-iap/gradle.properties` and `gradle/libs.versions.toml`
- MAUI: `libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj`
- Google Android SDK / Play Billing: `packages/google/openiap/build.gradle.kts`

`bun run audit:docs` fails if this generated metadata drifts from the SSOT
files or if `versioning.ts` reintroduces raw imports outside `packages/docs`.

## Pre-commit checklist

Run before every `git push` on docs / SDK changes:
Expand Down Expand Up @@ -1990,6 +2027,7 @@ positives in CI.

`scripts/audit-docs.ts` is the executable companion to this guide. It
parses every `/docs/apis/*.tsx` and `/docs/types/*.tsx` page, extracts:

- `<Link to="/docs/...">` targets
- `<code>fieldName</code>` mentions inside Returns / Parameters tables
- String-literal enum values in `<code>'…'</code>` blocks
Expand Down
Loading
Loading