From 925cfefd8d0aad5c2f9ef4f958b472edb63492d9 Mon Sep 17 00:00:00 2001 From: hyochan Date: Tue, 16 Jun 2026 00:03:14 +0900 Subject: [PATCH 1/5] fix(maui): add net10 targets and trim android aars Add net10 MAUI target frameworks, move Android Google dependencies to NuGet PackageReferences, and introduce OpenIapClient as the preferred facade while keeping Iap as a legacy shim. Closes #178 Closes #179 Closes #180 --- .github/workflows/ci-maui-iap.yml | 38 ++++--- .github/workflows/release-maui.yml | 26 +++-- knowledge/_claude-context/context.md | 52 +++++++-- knowledge/internal/04-platform-packages.md | 2 +- libraries/maui-iap/CLAUDE.md | 33 ++++-- libraries/maui-iap/CONVENTION.md | 10 +- libraries/maui-iap/README.md | 27 +++-- .../Pages/AllProductsPage.xaml.cs | 2 +- .../Pages/AlternativeBillingPage.xaml.cs | 14 +-- .../Pages/AvailablePurchasesPage.xaml.cs | 6 +- .../Pages/HomePage.xaml.cs | 2 +- .../Pages/OfferCodePage.xaml.cs | 2 +- .../Pages/PurchaseFlowPage.xaml.cs | 18 +-- .../Pages/SubscriptionFlowPage.xaml.cs | 16 +-- .../Pages/WebhookStreamPage.xaml.cs | 2 +- .../Utils/IapLifecycle.cs | 4 +- libraries/maui-iap/src/Directory.Build.props | 17 ++- .../OpenIap.Maui.Bindings.Android.csproj | 33 ++---- .../OpenIap.Maui.Bindings.iOS.csproj | 2 +- .../src/OpenIap.Maui/OpenIap.Maui.csproj | 36 +++--- .../maui-iap/src/OpenIap.Maui/OpenIap.cs | 47 ++++++-- llms-full.txt | 35 +++--- llms.txt | 24 ++-- packages/docs/public/llms-full.txt | 35 +++--- packages/docs/public/llms.txt | 24 ++-- packages/docs/scripts/replace-csharp-tabs.mjs | 4 +- .../android/acknowledge-purchase-android.tsx | 2 +- ...ternative-billing-availability-android.tsx | 2 +- .../apis/android/consume-purchase-android.tsx | 2 +- ...eate-alternative-billing-token-android.tsx | 2 +- ...ling-program-reporting-details-android.tsx | 2 +- .../enable-billing-program-android.tsx | 2 +- .../is-billing-program-available-android.tsx | 2 +- .../android/launch-external-link-android.tsx | 2 +- ...how-alternative-billing-dialog-android.tsx | 2 +- .../docs/apis/deep-link-to-subscriptions.tsx | 2 +- .../src/pages/docs/apis/end-connection.tsx | 2 +- .../src/pages/docs/apis/fetch-products.tsx | 2 +- .../pages/docs/apis/finish-transaction.tsx | 2 +- .../docs/apis/get-active-subscriptions.tsx | 2 +- .../docs/apis/get-available-purchases.tsx | 2 +- .../src/pages/docs/apis/get-storefront.tsx | 2 +- .../docs/apis/has-active-subscriptions.tsx | 2 +- .../src/pages/docs/apis/init-connection.tsx | 4 +- .../apis/ios/begin-refund-request-ios.tsx | 2 +- ...n-present-external-purchase-notice-ios.tsx | 2 +- .../docs/apis/ios/clear-transaction-ios.tsx | 2 +- .../docs/apis/ios/current-entitlement-ios.tsx | 2 +- .../apis/ios/get-all-transactions-ios.tsx | 2 +- .../docs/apis/ios/get-app-transaction-ios.tsx | 2 +- ...xternal-purchase-custom-link-token-ios.tsx | 2 +- .../apis/ios/get-pending-transactions-ios.tsx | 2 +- .../apis/ios/get-promoted-product-ios.tsx | 2 +- .../docs/apis/ios/get-receipt-data-ios.tsx | 2 +- .../docs/apis/ios/get-storefront-ios.tsx | 4 +- .../docs/apis/ios/get-transaction-jws-ios.tsx | 2 +- ...-for-external-purchase-custom-link-ios.tsx | 2 +- .../ios/is-eligible-for-intro-offer-ios.tsx | 2 +- .../apis/ios/is-transaction-verified-ios.tsx | 2 +- .../docs/apis/ios/latest-transaction-ios.tsx | 2 +- .../ios/present-code-redemption-sheet-ios.tsx | 2 +- .../present-external-purchase-link-ios.tsx | 2 +- ...ent-external-purchase-notice-sheet-ios.tsx | 2 +- ...quest-purchase-on-promoted-product-ios.tsx | 2 +- ...ternal-purchase-custom-link-notice-ios.tsx | 2 +- .../ios/show-manage-subscriptions-ios.tsx | 2 +- .../docs/apis/ios/subscription-status-ios.tsx | 2 +- .../docs/src/pages/docs/apis/ios/sync-ios.tsx | 2 +- .../docs/apis/ios/validate-receipt-ios.tsx | 2 +- .../src/pages/docs/apis/request-purchase.tsx | 10 +- .../src/pages/docs/apis/restore-purchases.tsx | 2 +- ...oper-provided-billing-listener-android.tsx | 4 +- .../user-choice-billing-listener-android.tsx | 2 +- .../ios/promoted-product-listener-ios.tsx | 2 +- .../docs/events/purchase-error-listener.tsx | 6 +- .../docs/events/purchase-updated-listener.tsx | 8 +- .../subscription-billing-issue-listener.tsx | 4 +- .../docs/src/pages/docs/features/discount.tsx | 6 +- .../docs/src/pages/docs/features/refund.tsx | 2 +- .../features/subscription-billing-issue.tsx | 2 +- .../docs/features/subscription/index.tsx | 28 ++--- .../subscription/upgrade-downgrade.tsx | 26 ++--- .../src/pages/docs/features/validation.tsx | 2 +- .../docs/src/pages/docs/getting-started.tsx | 6 +- packages/docs/src/pages/docs/kit-backend.tsx | 2 +- packages/docs/src/pages/docs/setup/maui.tsx | 33 ++++-- .../docs/types/alternative-billing-types.tsx | 26 ++--- .../src/pages/docs/types/billing-programs.tsx | 8 +- .../docs/types/external-purchase-link.tsx | 6 +- .../docs/types/ios/app-transaction-ios.tsx | 2 +- .../src/pages/docs/types/product-request.tsx | 6 +- .../purchase-updated-listener-options.tsx | 2 +- .../docs/types/request-purchase-props.tsx | 4 +- .../verify-purchase-with-provider-result.tsx | 2 +- packages/docs/src/pages/introduction.tsx | 2 +- scripts/agent/compile-context.ts | 31 +++--- scripts/audit-non-godot-parity.mjs | 104 +++++++++++++++--- scripts/sync-versions.sh | 26 ++--- 98 files changed, 575 insertions(+), 390 deletions(-) diff --git a/.github/workflows/ci-maui-iap.yml b/.github/workflows/ci-maui-iap.yml index a48ce58a..f40006fb 100644 --- a/.github/workflows/ci-maui-iap.yml +++ b/.github/workflows/ci-maui-iap.yml @@ -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: @@ -52,28 +52,30 @@ jobs: with: fetch-depth: 1 - - name: Setup .NET 9 SDK + - name: Setup .NET 10 SDK uses: actions/setup-dotnet@v5 with: - dotnet-version: '9.0.x' + dotnet-version: "10.0.x" - # 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. `` 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 # `` 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: @@ -87,10 +89,10 @@ jobs: distribution: 'temurin' java-version: '17' - - name: Setup .NET 9 SDK + - name: Setup .NET 10 SDK uses: actions/setup-dotnet@v5 with: - dotnet-version: '9.0.x' + dotnet-version: "10.0.x" - name: Install MAUI workload run: dotnet workload install maui-android --skip-sign-check @@ -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: @@ -132,10 +136,10 @@ jobs: with: xcode-version: ${{ env.XCODE_VERSION }} - - name: Setup .NET 9 SDK + - name: Setup .NET 10 SDK uses: actions/setup-dotnet@v5 with: - dotnet-version: '9.0.x' + dotnet-version: "10.0.x" - name: Install MAUI workload run: dotnet workload install maui --skip-sign-check @@ -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 diff --git a/.github/workflows/release-maui.yml b/.github/workflows/release-maui.yml index acb53943..42388ab0 100644 --- a/.github/workflows/release-maui.yml +++ b/.github/workflows/release-maui.yml @@ -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 + - name: Setup .NET 10 SDK uses: actions/setup-dotnet@v5 with: - dotnet-version: "9.0.x" + dotnet-version: "10.0.x" - # 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: @@ -72,10 +74,10 @@ jobs: distribution: "temurin" java-version: "17" - - name: Setup .NET 9 SDK + - name: Setup .NET 10 SDK uses: actions/setup-dotnet@v5 with: - dotnet-version: "9.0.x" + dotnet-version: "10.0.x" - name: Install MAUI workload run: dotnet workload install maui --skip-sign-check @@ -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 @@ -118,10 +122,10 @@ jobs: distribution: "temurin" java-version: "17" - - name: Setup .NET 9 SDK + - name: Setup .NET 10 SDK uses: actions/setup-dotnet@v5 with: - dotnet-version: "9.0.x" + dotnet-version: "10.0.x" - name: Install MAUI workload run: dotnet workload install maui --skip-sign-check diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 9b62d064..6a85902b 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -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` @@ -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: @@ -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 @@ -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 "":` 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.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) @@ -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 `?? ` expression and JSDoc / KDoc / etc. @@ -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`. @@ -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 @@ -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`) @@ -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: @@ -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: + - `` targets - `fieldName` mentions inside Returns / Parameters tables - String-literal enum values in `'…'` blocks diff --git a/knowledge/internal/04-platform-packages.md b/knowledge/internal/04-platform-packages.md index d814869e..afc3d660 100644 --- a/knowledge/internal/04-platform-packages.md +++ b/knowledge/internal/04-platform-packages.md @@ -174,7 +174,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 "":` 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.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) diff --git a/libraries/maui-iap/CLAUDE.md b/libraries/maui-iap/CLAUDE.md index 95f8d90c..ff40250d 100644 --- a/libraries/maui-iap/CLAUDE.md +++ b/libraries/maui-iap/CLAUDE.md @@ -7,8 +7,11 @@ imports the generated [`Types.cs`](src/OpenIap.Maui/Types.cs) from (`IOpenIap`), and delegates the actual purchase work to the OpenIAP native packages — `packages/apple` on iOS / macCatalyst, `packages/google` on Android. It also exposes the same IAPKit HTTP/webhook helper surface as the -TypeScript SDKs through `Iap.KitApi(...)`, `Iap.ConnectWebhookStream(...)`, and -`Iap.ParseWebhookEventData(...)`. +TypeScript SDKs through `OpenIapClient.KitApi(...)`, +`OpenIapClient.ConnectWebhookStream(...)`, and +`OpenIapClient.ParseWebhookEventData(...)`. The legacy `Iap` facade remains as +a compatibility shim, but new code should use `OpenIapClient` to avoid +namespace/type collisions with app namespaces such as `OpenIap.Maui.Iap`. ## Required pre-work @@ -21,8 +24,9 @@ Before editing anything in this library: [`04-platform-packages.md`](../../knowledge/internal/04-platform-packages.md#sdk-parity-checklist-critical--prevents-declared-but-not-implemented). 2. Read [`CONVENTION.md`](./CONVENTION.md) for C# / MAUI-specific naming and style rules. -3. Run `dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -f net9.0` (the - shared TFM compiles without the MAUI workload) before pushing. +3. Run `dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0` + and `dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net10.0` + (the shared TFMs compile without the MAUI workload) before pushing. ## Project layout @@ -34,8 +38,8 @@ libraries/maui-iap/ ├── openiap-versions.json — symlink for native spec/apple/google versions └── src/ └── OpenIap.Maui/ - ├── OpenIap.Maui.csproj — multi-target (net9.0 + ios/android/maccatalyst) - ├── OpenIap.cs — IOpenIap contract + static facade + ├── OpenIap.Maui.csproj — multi-target (net9.0/net10.0 + ios/android/maccatalyst) + ├── OpenIap.cs — IOpenIap contract + static facades ├── UnsupportedOpenIap.cs — fallback for non-platform builds ├── Types.cs — AUTO-GENERATED, do not edit └── Platforms/ @@ -138,9 +142,13 @@ so the main package flattens their outputs instead of declaring unpublished The package includes: - binding DLLs in `lib//` -- Android AARs in `lib/net9.0-android35.0/`, including the binding support AAR - with Maven-resolved jars, the MAUI-owned module AAR, and the unbound - `openiap-play-release.aar` runtime dependency +- Android AARs in `lib/net9.0-android35.0/` and + `lib/net10.0-android36.0/`, limited to OpenIAP-owned artifacts: the + MAUI-owned module AAR and the unbound `openiap-play-release.aar` runtime + dependency +- Android Google Billing, Play Services, Gson, AndroidX, and Kotlin runtime + libraries as normal NuGet `PackageReference` dependencies, not embedded AAR + copies - iOS / macCatalyst `OpenIap.Maui.Bindings.iOS.resources.zip` sidecars next to the iOS binding DLLs @@ -180,7 +188,8 @@ For maui-iap specifically: ```bash # Cross-platform shared compile (no MAUI workload required) -dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -f net9.0 +dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0 +dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net10.0 # Full multi-target build (requires MAUI workload) dotnet workload install maui @@ -192,7 +201,9 @@ dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj 1. Regenerate types if `packages/gql/src/*.graphql` changed: `cd packages/gql && bun run generate` 2. Run `bash scripts/sync-versions.sh` from repo root. -3. Run `dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -f net9.0`. +3. Run the shared compile checks: + `dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net9.0` + and `dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net10.0`. 4. Verify `Types.cs` matches `packages/gql/src/generated/Types.cs` byte-for-byte (the sync should keep them in lockstep). diff --git a/libraries/maui-iap/CONVENTION.md b/libraries/maui-iap/CONVENTION.md index d1a576c5..542ab1c0 100644 --- a/libraries/maui-iap/CONVENTION.md +++ b/libraries/maui-iap/CONVENTION.md @@ -8,17 +8,17 @@ C# / .NET MAUI specifics on top of the monorepo-wide rules in - **C# 12** with `enable` and `true` (configured in the .csproj). -- **.NET 8** target with platform-specific TFMs: - `net9.0;net9.0-android;net9.0-ios;net9.0-maccatalyst`. -- The shared `net9.0` TFM compiles without the MAUI workload — keep it - green for fast PR-time validation. +- **.NET 9 / .NET 10** targets with platform-specific TFMs: + `net9.0;net10.0;net9.0-android;net10.0-android;net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst`. +- The shared `net9.0` and `net10.0` TFMs compile without the MAUI workload — + keep both green for fast PR-time validation. ## Namespaces | Namespace | Owns | | ------------------------------------ | ---------------------------------------------- | | `OpenIap` | Generated types, enums, resolver interfaces. | -| `OpenIap.Maui` | `IOpenIap` contract, static `Iap` facade. | +| `OpenIap.Maui` | `IOpenIap` contract, static `OpenIapClient` facade, legacy `Iap` shim. | | `OpenIap.Maui.Platforms.Android` | Android bridge implementation. | | `OpenIap.Maui.Platforms.iOS` | iOS bridge implementation. | | `OpenIap.Maui.Platforms.MacCatalyst` | macCatalyst bridge implementation. | diff --git a/libraries/maui-iap/README.md b/libraries/maui-iap/README.md index 8864377e..9e3b1e80 100644 --- a/libraries/maui-iap/README.md +++ b/libraries/maui-iap/README.md @@ -8,14 +8,14 @@ macCatalyst from a single C# API. | Layer | iOS | Android | macCatalyst | | ------------------------------------------ | :-------------------: | :-------------------: | :-------------------: | | Generated types (`Types.cs`) | yes | yes | yes | -| `Iap.Instance` facade and listener streams | yes | yes | yes | +| `OpenIapClient.Instance` facade and listener streams | yes | yes | yes | | StoreKit 2 / Play Billing native bindings | yes | yes | yes | | Example MAUI app | yes | yes | yes | | NuGet package shape | single public package | single public package | single public package | ## Install -Requires .NET 9 SDK and the MAUI workload: +Requires the .NET 9 or .NET 10 SDK and the MAUI workload: ```bash dotnet workload install maui @@ -25,9 +25,11 @@ dotnet add package OpenIap.Maui For manual `.csproj` edits, copy the current PackageReference from the [OpenIap.Maui NuGet package page](https://www.nuget.org/packages/OpenIap.Maui). -`OpenIap.Maui` is the only NuGet package apps reference. The Android binding, -iOS binding, Google Play Billing AARs, and StoreKit xcframework resources are -flattened into the main NuGet package. +`OpenIap.Maui` is the only NuGet package apps reference. The Android and iOS +binding outputs are flattened into the main NuGet package, while Google +Billing, Play Services, Gson, AndroidX, and Kotlin Android libraries remain +normal NuGet dependencies so apps can deduplicate them with their own package +graph. ## Usage @@ -35,7 +37,7 @@ flattened into the main NuGet package. using OpenIap; using OpenIap.Maui; -var iap = Iap.Instance; +var iap = OpenIapClient.Instance; var query = (QueryResolver)iap; var mutate = (MutationResolver)iap; @@ -77,6 +79,10 @@ Always validate purchases on your server before granting entitlement, then call `FinishTransactionAsync`. On Android, unfinished purchases are refunded automatically after 3 days. +`Iap` remains available as a backward-compatible facade, but new code should use +`OpenIapClient` so app namespaces such as `OpenIap.Maui.Iap` do not collide +with the facade type name. + ## IAPKit API and webhooks MAUI exposes the same kit helper surface as `expo-iap` and @@ -86,7 +92,7 @@ MAUI exposes the same kit helper surface as `expo-iap` and using OpenIap; using OpenIap.Maui; -var kit = Iap.KitApi(new KitApiOptions +var kit = OpenIapClient.KitApi(new KitApiOptions { ApiKey = "iapkit_...", BaseUrl = "https://kit.openiap.dev", @@ -96,7 +102,7 @@ var status = await kit.StatusAsync("user-123"); var entitlements = await kit.EntitlementsAsync("user-123"); await kit.BindUserAsync(purchaseToken: "token", userId: "user-123"); -using var listener = Iap.ConnectWebhookStream(new WebhookListenerOptions +using var listener = OpenIapClient.ConnectWebhookStream(new WebhookListenerOptions { ApiKey = "iapkit_...", OnEvent = webhookEvent => @@ -109,7 +115,7 @@ using var listener = Iap.ConnectWebhookStream(new WebhookListenerOptions }, }); -ParsedWebhookEventResult parsed = Iap.ParseWebhookEventData(rawSseData); +ParsedWebhookEventResult parsed = OpenIapClient.ParseWebhookEventData(rawSseData); ``` ## Example app @@ -129,6 +135,9 @@ dotnet build -t:Run -f net9.0-ios dotnet build -t:Run -f net9.0-maccatalyst ``` +For .NET 10 apps, use the matching `net10.0-android`, `net10.0-ios`, and +`net10.0-maccatalyst` target frameworks. + VS Code launch configurations are in `libraries/maui-iap/.vscode/launch.json`. The Android launcher builds both AARs before compiling the example app. diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AllProductsPage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AllProductsPage.xaml.cs index 5146251d..6c3b70c4 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AllProductsPage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AllProductsPage.xaml.cs @@ -39,7 +39,7 @@ private async Task ConnectAndFetchAsync() { try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; await IapLifecycle.InitConnectionAsync(); ConnectionLabel.Text = "✅ Connected"; diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AlternativeBillingPage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AlternativeBillingPage.xaml.cs index da2567f6..da8db5da 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AlternativeBillingPage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AlternativeBillingPage.xaml.cs @@ -30,8 +30,8 @@ public AlternativeBillingPage() protected override async void OnAppearing() { base.OnAppearing(); - _purchaseSub ??= Iap.Instance.PurchaseUpdated.Subscribe(p => MainThread.BeginInvokeOnMainThread(() => OnPurchase(p))); - _errorSub ??= Iap.Instance.PurchaseError.Subscribe(err => MainThread.BeginInvokeOnMainThread(() => OnPurchaseError(err))); + _purchaseSub ??= OpenIapClient.Instance.PurchaseUpdated.Subscribe(p => MainThread.BeginInvokeOnMainThread(() => OnPurchase(p))); + _errorSub ??= OpenIapClient.Instance.PurchaseError.Subscribe(err => MainThread.BeginInvokeOnMainThread(() => OnPurchaseError(err))); await ConnectAndFetchAsync(); } @@ -75,7 +75,7 @@ private async Task LoadProductsAsync() { try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var result = await query.FetchProductsAsync(new ProductRequest { Skus = Constants.ConsumableProductIds, @@ -307,7 +307,7 @@ private async Task HandleIOSAlternativeBillingPurchaseAsync(Product product) ShowResult("🌐 Opening external purchase link..."); try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; var result = await mutate.PresentExternalPurchaseLinkIOSAsync(externalUrl); if (!string.IsNullOrEmpty(result.Error)) { @@ -339,7 +339,7 @@ private async Task HandleAndroidBillingProgramsAsync(Product product) try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; var availability = await mutate.IsBillingProgramAvailableAndroidAsync(_billingProgram); if (!availability.IsAvailable) { @@ -384,7 +384,7 @@ private async Task HandleAndroidUserChoiceBillingAsync(Product product) try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; await mutate.RequestPurchaseAsync(new RequestPurchaseProps { RequestPurchase = new RequestPurchasePropsByPlatforms @@ -416,7 +416,7 @@ private async void OnPurchase(Purchase purchase) try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; await mutate.FinishTransactionAsync( purchase: new PurchaseInput(purchase), isConsumable: Constants.ConsumableProductIdSet.Contains(common.ProductId)); diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AvailablePurchasesPage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AvailablePurchasesPage.xaml.cs index 2e394d8c..fac8fb88 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AvailablePurchasesPage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/AvailablePurchasesPage.xaml.cs @@ -60,7 +60,7 @@ private async Task RefreshAsync() try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; await query.FetchProductsAsync(new ProductRequest { Skus = Constants.SubscriptionProductIds, @@ -170,7 +170,7 @@ private async void OnDeepLinkClicked(object sender, EventArgs e) { try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; var sku = _active.FirstOrDefault()?.ProductId ?? Constants.DefaultSubscriptionProductId; await mutate.DeepLinkToSubscriptionsAsync(new DeepLinkOptions { @@ -194,7 +194,7 @@ private async Task RefreshStorefrontAsync(bool showAlert) { try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var storefront = await query.GetStorefrontAsync(); StorefrontLabel.Text = $"Storefront: {storefront ?? string.Empty}"; StorefrontLabel.IsVisible = !string.IsNullOrEmpty(storefront); diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/HomePage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/HomePage.xaml.cs index 2e6c2b18..c0af6a0a 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/HomePage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/HomePage.xaml.cs @@ -18,7 +18,7 @@ protected override async void OnAppearing() try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var storefront = await query.GetStorefrontAsync(); StorefrontLabel.Text = string.IsNullOrEmpty(storefront) ? "Best Practice Implementations" diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs index d2d5aac5..26579744 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs @@ -58,7 +58,7 @@ private async void OnPresentClicked(object sender, EventArgs e) #if IOS || MACCATALYST try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; var presented = await mutate.PresentCodeRedemptionSheetIOSAsync(); ResultPanel.IsVisible = true; ResultLabel.Text = presented diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/PurchaseFlowPage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/PurchaseFlowPage.xaml.cs index 07035288..af7d5a93 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/PurchaseFlowPage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/PurchaseFlowPage.xaml.cs @@ -36,8 +36,8 @@ public PurchaseFlowPage() protected override async void OnAppearing() { base.OnAppearing(); - _purchaseSub ??= Iap.Instance.PurchaseUpdated.Subscribe(p => MainThread.BeginInvokeOnMainThread(() => OnPurchase(p))); - _errorSub ??= Iap.Instance.PurchaseError.Subscribe(err => MainThread.BeginInvokeOnMainThread(() => OnPurchaseError(err))); + _purchaseSub ??= OpenIapClient.Instance.PurchaseUpdated.Subscribe(p => MainThread.BeginInvokeOnMainThread(() => OnPurchase(p))); + _errorSub ??= OpenIapClient.Instance.PurchaseError.Subscribe(err => MainThread.BeginInvokeOnMainThread(() => OnPurchaseError(err))); await ConnectAndFetchAsync(); } @@ -81,7 +81,7 @@ private async Task LoadProductsAsync() { try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var result = await query.FetchProductsAsync(new ProductRequest { Skus = Constants.ProductIds, @@ -109,7 +109,7 @@ private async Task RefreshStorefrontAsync(bool showAlert = true) StorefrontRefreshButton.Text = "Refreshing storefront..."; try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var storefront = await query.GetStorefrontAsync().WaitAsync(TimeSpan.FromSeconds(10)); StorefrontValueLabel.Text = string.IsNullOrEmpty(storefront) ? "Not available" : storefront; StorefrontErrorLabel.IsVisible = false; @@ -140,7 +140,7 @@ private async Task RefreshAvailablePurchasesAsync(bool showAlert = true) RefreshPurchasesButton.Text = "Refreshing purchases..."; try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var purchases = await query.GetAvailablePurchasesAsync(new PurchaseOptions { OnlyIncludeActiveItemsIOS = true, @@ -318,7 +318,7 @@ private async Task HandlePurchaseAsync(string sku) RenderProducts(); try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; var requestTask = mutate.RequestPurchaseAsync(new RequestPurchaseProps { RequestPurchase = new RequestPurchasePropsByPlatforms @@ -391,7 +391,7 @@ private async void OnPurchase(Purchase purchase) { try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; if (_verification == VerificationMethod.Local) { var result = await mutate.VerifyPurchaseAsync(new VerifyPurchaseProps @@ -453,7 +453,7 @@ private static async Task FinishPurchaseTransactionAsync(Purchase purchase, bool { try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; await mutate.FinishTransactionAsync( purchase: new PurchaseInput(purchase), isConsumable: isConsumable).WaitAsync(TimeSpan.FromSeconds(10)); @@ -525,7 +525,7 @@ private async void OnCheckAppTransactionClicked(object sender, EventArgs e) #if IOS || MACCATALYST try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var t = await query.GetAppTransactionIOSAsync(); if (t is null) { diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/SubscriptionFlowPage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/SubscriptionFlowPage.xaml.cs index 292372f3..3cc3965b 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/SubscriptionFlowPage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/SubscriptionFlowPage.xaml.cs @@ -48,8 +48,8 @@ public SubscriptionFlowPage() protected override async void OnAppearing() { base.OnAppearing(); - _purchaseSub ??= Iap.Instance.PurchaseUpdated.Subscribe(p => MainThread.BeginInvokeOnMainThread(() => OnPurchase(p))); - _errorSub ??= Iap.Instance.PurchaseError.Subscribe(err => MainThread.BeginInvokeOnMainThread(() => OnPurchaseError(err))); + _purchaseSub ??= OpenIapClient.Instance.PurchaseUpdated.Subscribe(p => MainThread.BeginInvokeOnMainThread(() => OnPurchase(p))); + _errorSub ??= OpenIapClient.Instance.PurchaseError.Subscribe(err => MainThread.BeginInvokeOnMainThread(() => OnPurchaseError(err))); await ConnectAndFetchAsync(); } @@ -95,7 +95,7 @@ private async Task FetchSubscriptionsAsync() { try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var result = await query.FetchProductsAsync(new ProductRequest { Skus = Constants.SubscriptionProductIds, @@ -125,7 +125,7 @@ private async Task RefreshActiveAsync(bool showAlert = true, bool renderSubscrip RefreshActiveButton.Text = "Refreshing status..."; try { - var query = (QueryResolver)Iap.Instance; + var query = (QueryResolver)OpenIapClient.Instance; var active = await query.GetActiveSubscriptionsAsync(Constants.SubscriptionProductIds) .WaitAsync(TimeSpan.FromSeconds(20)); _active.Clear(); @@ -508,7 +508,7 @@ private async Task SubmitSubscriptionRequestAsync(ProductSubscription sub, Cance try { SetActionStatus($"Dispatching StoreKit request: {common.Id}"); - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; var requestTask = mutate.RequestPurchaseAsync(new RequestPurchaseProps { RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -669,7 +669,7 @@ private static async Task FinishSubscriptionTransactionAsync(Purchase purchase) { try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; await mutate.FinishTransactionAsync( purchase: new PurchaseInput(purchase), isConsumable: false).WaitAsync(TimeSpan.FromSeconds(10)); @@ -693,7 +693,7 @@ private async Task VerifySubscriptionIfNeededAsync(Purchase purchase) try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; if (_verification == VerificationMethod.Local) { var result = await mutate.VerifyPurchaseAsync(new VerifyPurchaseProps @@ -780,7 +780,7 @@ private async void OnManageClicked(object sender, EventArgs e) { try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; if (IsApplePlatform) { try diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/WebhookStreamPage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/WebhookStreamPage.xaml.cs index c4ddae9b..bbdb5cd5 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/WebhookStreamPage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/WebhookStreamPage.xaml.cs @@ -46,7 +46,7 @@ private void OnConnectClicked(object sender, EventArgs e) ResetEmptyLog(); Append($"→ Connecting {url}"); - _listener = Iap.ConnectWebhookStream(new WebhookListenerOptions + _listener = OpenIapClient.ConnectWebhookStream(new WebhookListenerOptions { ApiKey = apiKey, BaseUrl = BaseUrlEntry.Text, diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Utils/IapLifecycle.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Utils/IapLifecycle.cs index cfa2a6e4..475505fc 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Utils/IapLifecycle.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Utils/IapLifecycle.cs @@ -12,7 +12,7 @@ public static async Task InitConnectionAsync(InitConnectionConfig? config await Gate.WaitAsync(); try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; return await mutate.InitConnectionAsync(config).WaitAsync(TimeSpan.FromSeconds(15)); } finally @@ -26,7 +26,7 @@ public static async Task EndConnectionQuietlyAsync(string owner) await Gate.WaitAsync(); try { - var mutate = (MutationResolver)Iap.Instance; + var mutate = (MutationResolver)OpenIapClient.Instance; await mutate.EndConnectionAsync().WaitAsync(TimeSpan.FromSeconds(5)); } catch (Exception ex) diff --git a/libraries/maui-iap/src/Directory.Build.props b/libraries/maui-iap/src/Directory.Build.props index 7698bdb8..86435cbb 100644 --- a/libraries/maui-iap/src/Directory.Build.props +++ b/libraries/maui-iap/src/Directory.Build.props @@ -3,14 +3,13 @@ 8.3.0 2.10.1 - 2.2.10 - 1.9.0.3 - 3.0.0 - 3.1.8 - 3.1.8 - 18.5.0 - 18.9.0 - 19.0.0 - 18.2.0 + 8.3.0.2 + 2.14.0 + 1.12.4.1 + 1.8.9.2 + 2.10.0.2 + 1.4.0.2 + 2.3.10.1 + 1.10.2.3 diff --git a/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj b/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj index 2cb2faab..9ca24264 100644 --- a/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj +++ b/libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj @@ -1,7 +1,7 @@ - net9.0-android + net9.0-android;net10.0-android 24.0 OpenIap.Maui.Bindings.Android OpenIap.Maui.Bindings.Android @@ -17,7 +17,7 @@ deliberate to surface each subtype's own field set. BG86xx-BG88xx: known Java-binding noise about renamed/wrapped events. --> - @@ -39,30 +39,13 @@ - + - - - - - - - - - - - - + + diff --git a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/OpenIap.Maui.Bindings.iOS.csproj b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/OpenIap.Maui.Bindings.iOS.csproj index d2388a2d..33ee884e 100644 --- a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/OpenIap.Maui.Bindings.iOS.csproj +++ b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/OpenIap.Maui.Bindings.iOS.csproj @@ -1,7 +1,7 @@ - net9.0-ios;net9.0-maccatalyst + net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst 15.0 15.0 OpenIap.Maui.Bindings.iOS diff --git a/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj b/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj index 92b6fc3f..c3ffaaff 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj +++ b/libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj @@ -1,9 +1,9 @@ - net9.0;net9.0-android;net9.0-ios;net9.0-maccatalyst + net9.0;net10.0;net9.0-android;net10.0-android;net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst - - - - + + + + + + + + + + + + + + + @@ -80,11 +91,10 @@ - + $(TargetsForTfmSpecificBuildOutput);IncludeBindingsInNuPkg $(AllowedOutputExtensionsInPackageBuildOutputFolder);.aar;.zip @@ -93,8 +103,8 @@ - - + + diff --git a/libraries/maui-iap/src/OpenIap.Maui/OpenIap.cs b/libraries/maui-iap/src/OpenIap.Maui/OpenIap.cs index b2cbde0b..6c5da9db 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/OpenIap.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/OpenIap.cs @@ -2,11 +2,11 @@ // OpenIAP — public API surface for .NET MAUI // ============================================================================ // -// The static `Iap` class is the recommended entry point. It delegates to a -// platform implementation that is selected at compile time (see the -// Platforms/ folder). The class is named `Iap` (not `OpenIap`) to avoid -// shadowing the `OpenIap` namespace when consumers `using` both -// `OpenIap` and `OpenIap.Maui`. Mirrors the API surface of: +// The static `OpenIapClient` class is the recommended entry point. It +// delegates to a platform implementation that is selected at compile time (see +// the Platforms/ folder). The older `Iap` facade remains as a compatibility +// shim, but the longer name avoids collisions with app namespaces such as +// `OpenIap.Maui.Iap`. Mirrors the API surface of: // - react-native-iap / expo-iap (TypeScript) // - flutter_inapp_purchase (Dart) // - kmp-iap (Kotlin) @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading.Tasks; using OpenIap; @@ -90,10 +91,10 @@ public interface IOpenIap /// /// Static convenience facade. Resolves the platform implementation lazily so -/// host apps can write await Iap.Instance.FetchProductsAsync(...) +/// host apps can write await OpenIapClient.Instance.FetchProductsAsync(...) /// once the platform impl also implements QueryResolver. /// -public static class Iap +public static class OpenIapClient { private static IOpenIap? _instance; @@ -153,6 +154,38 @@ public static ParsedWebhookEventResult ParseWebhookEventData(string raw) => WebhookClient.ParseWebhookEventData(raw); } +/// +/// Backward-compatible alias for . New code should +/// use to avoid namespace/type name collisions in +/// projects whose namespaces start with OpenIap.Maui.Iap. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class Iap +{ + /// + public static IOpenIap Instance => OpenIapClient.Instance; + + /// + public static void OverrideInstance(IOpenIap instance) + => OpenIapClient.OverrideInstance(instance); + + /// + public static KitApiClient KitApi(KitApiOptions options) + => OpenIapClient.KitApi(options); + + /// + public static WebhookListener ConnectWebhookStream(WebhookListenerOptions options) + => OpenIapClient.ConnectWebhookStream(options); + + /// + public static IReadOnlyList WebhookEventTypes + => OpenIapClient.WebhookEventTypes; + + /// + public static ParsedWebhookEventResult ParseWebhookEventData(string raw) + => OpenIapClient.ParseWebhookEventData(raw); +} + /// /// Platform factory. The actual implementation is provided by the /// per-platform OpenIapPlatform.<platform>.cs file. The diff --git a/llms-full.txt b/llms-full.txt index 561c961e..86d8fb5c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-06-15T14:57:16.350Z ## Table of Contents 1. Installation @@ -30,19 +30,19 @@ cd ios && pod install ### Swift (iOS/macOS) ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") // CocoaPods -pod 'openiap', '~> 2.1.9' +pod 'openiap', '~> 2.2.1' ``` ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.1") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.1") ``` ### Flutter @@ -51,13 +51,13 @@ flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap-2.2.10.zip` from GitHub Releases, extract it to +Download `godot-iap-2.3.1.zip` from GitHub Releases, extract it to `addons/godot-iap/`, then enable the plugin in Project Settings. ### Kotlin Multiplatform ```kotlin dependencies { - implementation("io.github.hyochan:kmp-iap:2.2.8") + implementation("io.github.hyochan:kmp-iap:2.3.1") } ``` @@ -69,9 +69,9 @@ https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.1 -Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. +Requires .NET 9 or .NET 10, the MAUI workload, iOS 15.0+, and Android API 24+. --- @@ -117,20 +117,23 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. are private implementation details and are flattened into `OpenIap.Maui` instead of being published as separate package dependencies. - Implementation: .NET MAUI projection with generated `Types.cs`, a static - `Iap.Instance` facade, `IOpenIap` observables, and per-platform resolvers. + `OpenIapClient.Instance` facade, legacy `Iap` shim, `IOpenIap` + observables, and per-platform resolvers. - iOS/macCatalyst bridge: .NET-for-iOS binding over `OpenIAP.xcframework` and `OpenIapModule+ObjC.swift`; NuGet consumers get the official `OpenIap.Maui.Bindings.iOS.resources.zip` sidecar so no app-level `NativeReference` is required. - Android bridge: Xamarin.Android binding over the MAUI-owned `openiap-release.aar`, which wraps the unbound - `openiap-play-release.aar` runtime dependency; resolved BillingClient / - Play Services AARs are included in the main nupkg for app packaging. + `openiap-play-release.aar` runtime dependency. Google Billing, Play + Services, Gson, AndroidX, and Kotlin Android libraries stay as NuGet + `PackageReference` dependencies so consuming apps can deduplicate them. - Public surface: `QueryResolver`, `MutationResolver`, and `IOpenIap` implemented by `OpenIapIOS`, `OpenIapAndroid`, and `OpenIapMacCatalyst`; - IAPKit helpers mirror the TypeScript SDKs via `Iap.KitApi(...)`, - `Iap.ConnectWebhookStream(...)`, `Iap.ParseWebhookEventData(...)`, and - `Iap.WebhookEventTypes`. + IAPKit helpers mirror the TypeScript SDKs via + `OpenIapClient.KitApi(...)`, `OpenIapClient.ConnectWebhookStream(...)`, + `OpenIapClient.ParseWebhookEventData(...)`, and + `OpenIapClient.WebhookEventTypes`. - Example app: `libraries/maui-iap/example/OpenIap.Maui.Example`, mirroring the `expo-iap` example flows. @@ -191,7 +194,7 @@ iap.purchaseUpdatedListener.collect { purchase -> using OpenIap; using OpenIap.Maui; -var iap = Iap.Instance; +var iap = OpenIapClient.Instance; await ((MutationResolver)iap).InitConnectionAsync(); await ((QueryResolver)iap).FetchProductsAsync(new ProductRequest diff --git a/llms.txt b/llms.txt index 45cfb2e2..7194caf9 100644 --- a/llms.txt +++ b/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-06-15T14:57:16.350Z ## Installation @@ -19,12 +19,12 @@ npm install react-native-iap ### Native ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") ``` ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.1") ``` ```bash @@ -34,12 +34,12 @@ flutter pub add flutter_inapp_purchase ```gdscript # Godot -# Install godot-iap 2.2.10 to addons/godot-iap and enable the plugin +# Install godot-iap 2.3.1 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.2.8") +implementation("io.github.hyochan:kmp-iap:2.3.1") ``` ```bash @@ -47,7 +47,7 @@ implementation("io.github.hyochan:kmp-iap:2.2.8") dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.1 ## Framework Libraries @@ -56,11 +56,13 @@ Current NuGet package version: 1.0.4 - `flutter_inapp_purchase`: Dart API with generated OpenIAP types and streams. - `godot-iap`: Godot 4.x plugin with GDScript functions and signals. - `kmp-iap`: Kotlin Multiplatform API with Flow-based purchase events. -- `maui-iap`: `OpenIap.Maui` package with `Iap.Instance`, - generated `Types.cs`, IAPKit helpers (`Iap.KitApi`, - `Iap.ConnectWebhookStream`, `Iap.ParseWebhookEventData`), flattened iOS - xcframework / Android AAR bindings in one NuGet package, and MAUI example - flows matching `expo-iap`. +- `maui-iap`: `OpenIap.Maui` package with `OpenIapClient.Instance`, + generated `Types.cs`, IAPKit helpers (`OpenIapClient.KitApi`, + `OpenIapClient.ConnectWebhookStream`, + `OpenIapClient.ParseWebhookEventData`), flattened OpenIAP-owned iOS + xcframework / Android AAR bindings, Google and AndroidX Android + dependencies as NuGet package references, and MAUI example flows matching + `expo-iap`. ## Core APIs diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index 561c961e..86d8fb5c 100644 --- a/packages/docs/public/llms-full.txt +++ b/packages/docs/public/llms-full.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Quick Reference: https://openiap.dev/llms.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-06-15T14:57:16.350Z ## Table of Contents 1. Installation @@ -30,19 +30,19 @@ cd ios && pod install ### Swift (iOS/macOS) ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") // CocoaPods -pod 'openiap', '~> 2.1.9' +pod 'openiap', '~> 2.2.1' ``` ### Kotlin (Android) ```kotlin // Gradle (build.gradle.kts) -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.1") // For Meta Horizon OS -implementation("io.github.hyochan.openiap:openiap-google-horizon:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google-horizon:2.2.1") ``` ### Flutter @@ -51,13 +51,13 @@ flutter pub add flutter_inapp_purchase ``` ### Godot -Download `godot-iap-2.2.10.zip` from GitHub Releases, extract it to +Download `godot-iap-2.3.1.zip` from GitHub Releases, extract it to `addons/godot-iap/`, then enable the plugin in Project Settings. ### Kotlin Multiplatform ```kotlin dependencies { - implementation("io.github.hyochan:kmp-iap:2.2.8") + implementation("io.github.hyochan:kmp-iap:2.3.1") } ``` @@ -69,9 +69,9 @@ https://central.sonatype.com/artifact/io.github.hyochan/kmp-iap dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.1 -Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. +Requires .NET 9 or .NET 10, the MAUI workload, iOS 15.0+, and Android API 24+. --- @@ -117,20 +117,23 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. are private implementation details and are flattened into `OpenIap.Maui` instead of being published as separate package dependencies. - Implementation: .NET MAUI projection with generated `Types.cs`, a static - `Iap.Instance` facade, `IOpenIap` observables, and per-platform resolvers. + `OpenIapClient.Instance` facade, legacy `Iap` shim, `IOpenIap` + observables, and per-platform resolvers. - iOS/macCatalyst bridge: .NET-for-iOS binding over `OpenIAP.xcframework` and `OpenIapModule+ObjC.swift`; NuGet consumers get the official `OpenIap.Maui.Bindings.iOS.resources.zip` sidecar so no app-level `NativeReference` is required. - Android bridge: Xamarin.Android binding over the MAUI-owned `openiap-release.aar`, which wraps the unbound - `openiap-play-release.aar` runtime dependency; resolved BillingClient / - Play Services AARs are included in the main nupkg for app packaging. + `openiap-play-release.aar` runtime dependency. Google Billing, Play + Services, Gson, AndroidX, and Kotlin Android libraries stay as NuGet + `PackageReference` dependencies so consuming apps can deduplicate them. - Public surface: `QueryResolver`, `MutationResolver`, and `IOpenIap` implemented by `OpenIapIOS`, `OpenIapAndroid`, and `OpenIapMacCatalyst`; - IAPKit helpers mirror the TypeScript SDKs via `Iap.KitApi(...)`, - `Iap.ConnectWebhookStream(...)`, `Iap.ParseWebhookEventData(...)`, and - `Iap.WebhookEventTypes`. + IAPKit helpers mirror the TypeScript SDKs via + `OpenIapClient.KitApi(...)`, `OpenIapClient.ConnectWebhookStream(...)`, + `OpenIapClient.ParseWebhookEventData(...)`, and + `OpenIapClient.WebhookEventTypes`. - Example app: `libraries/maui-iap/example/OpenIap.Maui.Example`, mirroring the `expo-iap` example flows. @@ -191,7 +194,7 @@ iap.purchaseUpdatedListener.collect { purchase -> using OpenIap; using OpenIap.Maui; -var iap = Iap.Instance; +var iap = OpenIapClient.Instance; await ((MutationResolver)iap).InitConnectionAsync(); await ((QueryResolver)iap).FetchProductsAsync(new ProductRequest diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index 45cfb2e2..7194caf9 100644 --- a/packages/docs/public/llms.txt +++ b/packages/docs/public/llms.txt @@ -3,7 +3,7 @@ > OpenIAP: Unified in-app purchase specification for iOS & Android > Documentation: https://openiap.dev > Full Reference: https://openiap.dev/llms-full.txt -> Generated: 2026-05-16T12:59:43.331Z +> Generated: 2026-06-15T14:57:16.350Z ## Installation @@ -19,12 +19,12 @@ npm install react-native-iap ### Native ```swift // Swift Package Manager -.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.1.9") +.package(url: "https://github.com/hyodotdev/openiap.git", from: "2.2.1") ``` ```kotlin // Gradle -implementation("io.github.hyochan.openiap:openiap-google:2.1.5") +implementation("io.github.hyochan.openiap:openiap-google:2.2.1") ``` ```bash @@ -34,12 +34,12 @@ flutter pub add flutter_inapp_purchase ```gdscript # Godot -# Install godot-iap 2.2.10 to addons/godot-iap and enable the plugin +# Install godot-iap 2.3.1 to addons/godot-iap and enable the plugin ``` ```kotlin // Kotlin Multiplatform -implementation("io.github.hyochan:kmp-iap:2.2.8") +implementation("io.github.hyochan:kmp-iap:2.3.1") ``` ```bash @@ -47,7 +47,7 @@ implementation("io.github.hyochan:kmp-iap:2.2.8") dotnet add package OpenIap.Maui ``` -Current NuGet package version: 1.0.4 +Current NuGet package version: 1.1.1 ## Framework Libraries @@ -56,11 +56,13 @@ Current NuGet package version: 1.0.4 - `flutter_inapp_purchase`: Dart API with generated OpenIAP types and streams. - `godot-iap`: Godot 4.x plugin with GDScript functions and signals. - `kmp-iap`: Kotlin Multiplatform API with Flow-based purchase events. -- `maui-iap`: `OpenIap.Maui` package with `Iap.Instance`, - generated `Types.cs`, IAPKit helpers (`Iap.KitApi`, - `Iap.ConnectWebhookStream`, `Iap.ParseWebhookEventData`), flattened iOS - xcframework / Android AAR bindings in one NuGet package, and MAUI example - flows matching `expo-iap`. +- `maui-iap`: `OpenIap.Maui` package with `OpenIapClient.Instance`, + generated `Types.cs`, IAPKit helpers (`OpenIapClient.KitApi`, + `OpenIapClient.ConnectWebhookStream`, + `OpenIapClient.ParseWebhookEventData`), flattened OpenIAP-owned iOS + xcframework / Android AAR bindings, Google and AndroidX Android + dependencies as NuGet package references, and MAUI example flows matching + `expo-iap`. ## Core APIs diff --git a/packages/docs/scripts/replace-csharp-tabs.mjs b/packages/docs/scripts/replace-csharp-tabs.mjs index d0ac213e..1f7fc4bc 100644 --- a/packages/docs/scripts/replace-csharp-tabs.mjs +++ b/packages/docs/scripts/replace-csharp-tabs.mjs @@ -38,13 +38,13 @@ function kotlinToCSharp(kotlin) { ); // Call sites: `openIapStore.x(`, `kmpIAP.x(`, `kmpIapInstance.x(`, `iap.x(` → - // `await ((QueryResolver)Iap.Instance).XAsync(` (best-effort; reader can + // `await ((QueryResolver)OpenIapClient.Instance).XAsync(` (best-effort; reader can // swap to MutationResolver where appropriate). s = s.replace( /\b(openIapStore|kmpIAP|kmpIapInstance|iap)\.([a-z][A-Za-z0-9]*)\(/g, (_m, _recv, method) => { const csName = method[0].toUpperCase() + method.slice(1) + 'Async'; - return `await ((QueryResolver)Iap.Instance).${csName}(`; + return `await ((QueryResolver)OpenIapClient.Instance).${csName}(`; } ); diff --git a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx index c2b47a15..496acf79 100644 --- a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx @@ -128,7 +128,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).AcknowledgePurchaseAndroidAsync(purchase.PurchaseToken);`} +await ((MutationResolver)OpenIapClient.Instance).AcknowledgePurchaseAndroidAsync(purchase.PurchaseToken);`} ), gdscript: ( {`if iap.get_platform() == "Android": diff --git a/packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx b/packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx index 37116294..3e16ed4b 100644 --- a/packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx @@ -102,7 +102,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -var ok = await ((MutationResolver)Iap.Instance).CheckAlternativeBillingAvailabilityAndroidAsync();`} +var ok = await ((MutationResolver)OpenIapClient.Instance).CheckAlternativeBillingAvailabilityAndroidAsync();`} ), gdscript: ( {`if iap.get_platform() == "Android": diff --git a/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx index 6c95fe55..99823d03 100644 --- a/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx @@ -125,7 +125,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).ConsumePurchaseAndroidAsync(purchase.PurchaseToken);`} +await ((MutationResolver)OpenIapClient.Instance).ConsumePurchaseAndroidAsync(purchase.PurchaseToken);`} ), gdscript: ( {`if iap.get_platform() == "Android": diff --git a/packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx b/packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx index 32fce7bc..0483006c 100644 --- a/packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx @@ -105,7 +105,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -var token = await ((MutationResolver)Iap.Instance).CreateAlternativeBillingTokenAndroidAsync();`} +var token = await ((MutationResolver)OpenIapClient.Instance).CreateAlternativeBillingTokenAndroidAsync();`} ), gdscript: ( {`if iap.get_platform() == "Android": diff --git a/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx b/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx index 3efc477f..b7e76800 100644 --- a/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx @@ -168,7 +168,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -var details = await ((MutationResolver)Iap.Instance).CreateBillingProgramReportingDetailsAndroidAsync( +var details = await ((MutationResolver)OpenIapClient.Instance).CreateBillingProgramReportingDetailsAndroidAsync( BillingProgramAndroid.ExternalOffer );`} ), diff --git a/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx b/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx index 0e5d71ee..5a6085af 100644 --- a/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx @@ -171,7 +171,7 @@ function App() { {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).InitConnectionAsync( +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync( new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer, diff --git a/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx b/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx index aced401c..c272a218 100644 --- a/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx @@ -157,7 +157,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -var result = await ((MutationResolver)Iap.Instance).IsBillingProgramAvailableAndroidAsync( +var result = await ((MutationResolver)OpenIapClient.Instance).IsBillingProgramAvailableAndroidAsync( BillingProgramAndroid.ExternalOffer );`} ), diff --git a/packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx b/packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx index 8344dbc7..4d4f7d13 100644 --- a/packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx @@ -201,7 +201,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).LaunchExternalLinkAndroidAsync( +await ((MutationResolver)OpenIapClient.Instance).LaunchExternalLinkAndroidAsync( new LaunchExternalLinkParamsAndroid { BillingProgram = BillingProgramAndroid.ExternalOffer, diff --git a/packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx b/packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx index 22596421..093a6400 100644 --- a/packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx @@ -106,7 +106,7 @@ if (Platform.OS === 'android') { {`using OpenIap; using OpenIap.Maui; -var accepted = await ((MutationResolver)Iap.Instance).ShowAlternativeBillingDialogAndroidAsync();`} +var accepted = await ((MutationResolver)OpenIapClient.Instance).ShowAlternativeBillingDialogAndroidAsync();`} ), gdscript: ( {`if iap.get_platform() == "Android": diff --git a/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx b/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx index a8d47164..951d35ee 100644 --- a/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx +++ b/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx @@ -195,7 +195,7 @@ function ManageSubscriptionsButton() { {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).DeepLinkToSubscriptionsAsync( +await ((MutationResolver)OpenIapClient.Instance).DeepLinkToSubscriptionsAsync( new DeepLinkOptions { SkuAndroid = "com.app.premium", diff --git a/packages/docs/src/pages/docs/apis/end-connection.tsx b/packages/docs/src/pages/docs/apis/end-connection.tsx index 6065d894..83ca32f3 100644 --- a/packages/docs/src/pages/docs/apis/end-connection.tsx +++ b/packages/docs/src/pages/docs/apis/end-connection.tsx @@ -125,7 +125,7 @@ function PurchaseScreen() { {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).EndConnectionAsync();`} +await ((MutationResolver)OpenIapClient.Instance).EndConnectionAsync();`} ), gdscript: ( {`# In _exit_tree or cleanup diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index 479634ba..47a6d864 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -304,7 +304,7 @@ var products = await iap.fetch_products(request)`} {`using OpenIap; using OpenIap.Maui; -var iap = (QueryResolver)Iap.Instance; +var iap = (QueryResolver)OpenIapClient.Instance; var result = await iap.FetchProductsAsync(new ProductRequest { Skus = new[] { "com.app.coins_100", "com.app.premium" }, diff --git a/packages/docs/src/pages/docs/apis/finish-transaction.tsx b/packages/docs/src/pages/docs/apis/finish-transaction.tsx index e48c3d60..85ff419a 100644 --- a/packages/docs/src/pages/docs/apis/finish-transaction.tsx +++ b/packages/docs/src/pages/docs/apis/finish-transaction.tsx @@ -193,7 +193,7 @@ kmpIAP.finishTransaction( {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).FinishTransactionAsync( +await ((MutationResolver)OpenIapClient.Instance).FinishTransactionAsync( purchase: new PurchaseInput(purchase), isConsumable: true);`} ), diff --git a/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx index 30da4b6e..1cc5a1bf 100644 --- a/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx +++ b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx @@ -207,7 +207,7 @@ function SubscriptionStatus() { {`using OpenIap; using OpenIap.Maui; -var subscriptions = await ((QueryResolver)Iap.Instance).GetActiveSubscriptionsAsync()`} +var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync()`} ), gdscript: ( {`var subscriptions = await iap.get_active_subscriptions()`} diff --git a/packages/docs/src/pages/docs/apis/get-available-purchases.tsx b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx index 6db96c73..415cf0f8 100644 --- a/packages/docs/src/pages/docs/apis/get-available-purchases.tsx +++ b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx @@ -192,7 +192,7 @@ function PendingPurchases() { {`using OpenIap; using OpenIap.Maui; -var purchases = await ((QueryResolver)Iap.Instance).GetAvailablePurchasesAsync()`} +var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync()`} ), gdscript: ( {`var purchases = await iap.get_available_purchases()`} diff --git a/packages/docs/src/pages/docs/apis/get-storefront.tsx b/packages/docs/src/pages/docs/apis/get-storefront.tsx index 52c9640b..25b24ccc 100644 --- a/packages/docs/src/pages/docs/apis/get-storefront.tsx +++ b/packages/docs/src/pages/docs/apis/get-storefront.tsx @@ -124,7 +124,7 @@ function StorefrontBadge() { {`using OpenIap; using OpenIap.Maui; -var countryCode = await ((QueryResolver)Iap.Instance).GetStorefrontAsync()`} +var countryCode = await ((QueryResolver)OpenIapClient.Instance).GetStorefrontAsync()`} ), gdscript: ( {`var country_code = await iap.get_storefront()`} diff --git a/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx b/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx index 9e228f9d..5020205a 100644 --- a/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx +++ b/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx @@ -137,7 +137,7 @@ function PremiumGate({ children }: { children: React.ReactNode }) { {`using OpenIap; using OpenIap.Maui; -var isPremium = await ((QueryResolver)Iap.Instance).HasActiveSubscriptionsAsync()`} +var isPremium = await ((QueryResolver)OpenIapClient.Instance).HasActiveSubscriptionsAsync()`} ), gdscript: ( {`var is_premium = await iap.has_active_subscriptions()`} diff --git a/packages/docs/src/pages/docs/apis/init-connection.tsx b/packages/docs/src/pages/docs/apis/init-connection.tsx index b724f3db..10659b6a 100644 --- a/packages/docs/src/pages/docs/apis/init-connection.tsx +++ b/packages/docs/src/pages/docs/apis/init-connection.tsx @@ -196,10 +196,10 @@ kmpIAP.initConnection( using OpenIap.Maui; // Standard connection -await ((MutationResolver)Iap.Instance).InitConnectionAsync(); +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(); // With alternative billing -await ((MutationResolver)Iap.Instance).InitConnectionAsync( +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync( new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling, diff --git a/packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx b/packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx index 34678e7e..503dad89 100644 --- a/packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx @@ -112,7 +112,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // iOS targets only — no-op on Android. -var status = await ((MutationResolver)Iap.Instance) +var status = await ((MutationResolver)OpenIapClient.Instance) .BeginRefundRequestIOSAsync(sku: "com.app.premium");`} ), gdscript: ( diff --git a/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx b/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx index 1267b915..c366ac54 100644 --- a/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx @@ -96,7 +96,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var can = await ((QueryResolver)Iap.Instance).CanPresentExternalPurchaseNoticeIOSAsync()`} +var can = await ((QueryResolver)OpenIapClient.Instance).CanPresentExternalPurchaseNoticeIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx index f507900c..3985b3ff 100644 --- a/packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx @@ -94,7 +94,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -await ((MutationResolver)Iap.Instance).ClearTransactionIOSAsync();`} +await ((MutationResolver)OpenIapClient.Instance).ClearTransactionIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx b/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx index b9b3f757..2b4f7ace 100644 --- a/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx @@ -111,7 +111,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var entitlement = await ((QueryResolver)Iap.Instance).CurrentEntitlementIOSAsync(sku: "com.app.premium")`} +var entitlement = await ((QueryResolver)OpenIapClient.Instance).CurrentEntitlementIOSAsync(sku: "com.app.premium")`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx index ec829883..c70da567 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx @@ -105,7 +105,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var txs = await ((QueryResolver)Iap.Instance).GetAllTransactionsIOSAsync()`} +var txs = await ((QueryResolver)OpenIapClient.Instance).GetAllTransactionsIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx index b8be793c..7b6b5963 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx @@ -97,7 +97,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var appTx = await ((QueryResolver)Iap.Instance).GetAppTransactionIOSAsync()`} +var appTx = await ((QueryResolver)OpenIapClient.Instance).GetAppTransactionIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx index 7ecdfb51..7c5b0d79 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx @@ -140,7 +140,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var token = await ((QueryResolver)Iap.Instance).GetExternalPurchaseCustomLinkTokenIOSAsync( +var token = await ((QueryResolver)OpenIapClient.Instance).GetExternalPurchaseCustomLinkTokenIOSAsync( tokenType = ExternalPurchaseCustomLinkTokenTypeIOS.ACQUISITION )`} ), diff --git a/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx index c6eab6b6..16b9e85f 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx @@ -100,7 +100,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var txs = await ((QueryResolver)Iap.Instance).GetPendingTransactionsIOSAsync()`} +var txs = await ((QueryResolver)OpenIapClient.Instance).GetPendingTransactionsIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx index 432f54b2..c4b3260a 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx @@ -98,7 +98,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var product = await ((QueryResolver)Iap.Instance).GetPromotedProductIOSAsync()`} +var product = await ((QueryResolver)OpenIapClient.Instance).GetPromotedProductIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx index 86cb9919..c0704820 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx @@ -95,7 +95,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var data = await ((QueryResolver)Iap.Instance).GetReceiptDataIOSAsync()`} +var data = await ((QueryResolver)OpenIapClient.Instance).GetReceiptDataIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx index a9d1f909..2936e88b 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx @@ -110,8 +110,8 @@ if (Platform.isIOS) { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -// Deprecated — prefer await ((QueryResolver)Iap.Instance).GetStorefrontAsync() -var code = await ((QueryResolver)Iap.Instance).GetStorefrontIOSAsync()`} +// Deprecated — prefer await ((QueryResolver)OpenIapClient.Instance).GetStorefrontAsync() +var code = await ((QueryResolver)OpenIapClient.Instance).GetStorefrontIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx index 2c75cc7f..860306e3 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx @@ -108,7 +108,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var jws = await ((QueryResolver)Iap.Instance).GetTransactionJwsIOSAsync(sku: "com.app.premium")`} +var jws = await ((QueryResolver)OpenIapClient.Instance).GetTransactionJwsIOSAsync(sku: "com.app.premium")`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx index 0e770ea5..bf40f967 100644 --- a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx @@ -106,7 +106,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var ok = await ((QueryResolver)Iap.Instance).IsEligibleForExternalPurchaseCustomLinkIOSAsync()`} +var ok = await ((QueryResolver)OpenIapClient.Instance).IsEligibleForExternalPurchaseCustomLinkIOSAsync()`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx index ec265a01..6dab35ac 100644 --- a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx @@ -113,7 +113,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var ok = await ((QueryResolver)Iap.Instance).IsEligibleForIntroOfferIOSAsync(groupId: "com.app.subgroup")`} +var ok = await ((QueryResolver)OpenIapClient.Instance).IsEligibleForIntroOfferIOSAsync(groupId: "com.app.subgroup")`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx index d0269ced..41030961 100644 --- a/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx @@ -109,7 +109,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var ok = await ((QueryResolver)Iap.Instance).IsTransactionVerifiedIOSAsync(sku: "com.app.premium")`} +var ok = await ((QueryResolver)OpenIapClient.Instance).IsTransactionVerifiedIOSAsync(sku: "com.app.premium")`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx index 6602103b..845ab07a 100644 --- a/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx @@ -111,7 +111,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var tx = await ((QueryResolver)Iap.Instance).LatestTransactionIOSAsync(sku: "com.app.premium")`} +var tx = await ((QueryResolver)OpenIapClient.Instance).LatestTransactionIOSAsync(sku: "com.app.premium")`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx b/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx index 31ca5652..ba9f9eac 100644 --- a/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx @@ -96,7 +96,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -await ((MutationResolver)Iap.Instance).PresentCodeRedemptionSheetIOSAsync();`} +await ((MutationResolver)OpenIapClient.Instance).PresentCodeRedemptionSheetIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx index c5cc9542..c492a531 100644 --- a/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx @@ -120,7 +120,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // iOS targets only — no-op on Android -var result = await ((MutationResolver)Iap.Instance).PresentExternalPurchaseLinkIOSAsync( +var result = await ((MutationResolver)OpenIapClient.Instance).PresentExternalPurchaseLinkIOSAsync( "https://yourstore.com/checkout");`} ), gdscript: ( diff --git a/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx index 4eddff12..30a606be 100644 --- a/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx @@ -129,7 +129,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var result = await ((MutationResolver)Iap.Instance).PresentExternalPurchaseNoticeSheetIOSAsync();`} +var result = await ((MutationResolver)OpenIapClient.Instance).PresentExternalPurchaseNoticeSheetIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx b/packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx index b85d6f43..d5f90c4f 100644 --- a/packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx @@ -115,7 +115,7 @@ using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) // Deprecated — prefer promotedProductListenerIOS + requestPurchase. -await ((MutationResolver)Iap.Instance).RequestPurchaseOnPromotedProductIOSAsync();`} +await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseOnPromotedProductIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/show-external-purchase-custom-link-notice-ios.tsx b/packages/docs/src/pages/docs/apis/ios/show-external-purchase-custom-link-notice-ios.tsx index fe6c466c..42a9787b 100644 --- a/packages/docs/src/pages/docs/apis/ios/show-external-purchase-custom-link-notice-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/show-external-purchase-custom-link-notice-ios.tsx @@ -153,7 +153,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var result = await ((MutationResolver)Iap.Instance).ShowExternalPurchaseCustomLinkNoticeIOSAsync( +var result = await ((MutationResolver)OpenIapClient.Instance).ShowExternalPurchaseCustomLinkNoticeIOSAsync( ExternalPurchaseCustomLinkNoticeTypeIOS.Browser);`} ), gdscript: ( diff --git a/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx index ef5fdfb3..44b43ef1 100644 --- a/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx @@ -107,7 +107,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // OpenIap.Maui (iOS targets only — no-op on Android) -var changed = await ((MutationResolver)Iap.Instance).ShowManageSubscriptionsIOSAsync();`} +var changed = await ((MutationResolver)OpenIapClient.Instance).ShowManageSubscriptionsIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx b/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx index 5217f6fe..763458df 100644 --- a/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx @@ -136,7 +136,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var status = await ((QueryResolver)Iap.Instance).SubscriptionStatusIOSAsync(sku: "com.app.monthly")`} +var status = await ((QueryResolver)OpenIapClient.Instance).SubscriptionStatusIOSAsync(sku: "com.app.monthly")`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/sync-ios.tsx b/packages/docs/src/pages/docs/apis/ios/sync-ios.tsx index c0b446fd..e59238e1 100644 --- a/packages/docs/src/pages/docs/apis/ios/sync-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/sync-ios.tsx @@ -93,7 +93,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -await ((MutationResolver)Iap.Instance).SyncIOSAsync();`} +await ((MutationResolver)OpenIapClient.Instance).SyncIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx index db5b11e4..2ae2162c 100644 --- a/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx @@ -133,7 +133,7 @@ using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) // Deprecated — prefer verifyPurchase(). -await ((MutationResolver)Iap.Instance).ValidateReceiptIOSAsync( +await ((MutationResolver)OpenIapClient.Instance).ValidateReceiptIOSAsync( new VerifyPurchaseProps { Apple = new VerifyPurchaseAppleOptions diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index e610a4da..13140249 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -112,7 +112,7 @@ type RequestPurchaseProps = csharp: ( {`Task RequestPurchaseAsync(RequestPurchaseProps props); -// Result is event-based — listen via Iap.Instance.PurchaseUpdated / +// Result is event-based — listen via OpenIapClient.Instance.PurchaseUpdated / // PurchaseError. The returned RequestPurchaseResult is for legacy consumers.`} ), }} @@ -374,20 +374,20 @@ await iap.request_purchase(props)`} using OpenIap.Maui; // Subscribe to results FIRST — requestPurchase is event-based. -Iap.Instance.PurchaseUpdated.Subscribe(async purchase => { +OpenIapClient.Instance.PurchaseUpdated.Subscribe(async purchase => { // 1. Validate on your server, 2. Grant entitlement, // 3. Finish transaction (Android auto-refunds after 3 days otherwise!) - await ((MutationResolver)Iap.Instance).FinishTransactionAsync( + await ((MutationResolver)OpenIapClient.Instance).FinishTransactionAsync( purchase: new PurchaseInput(purchase), isConsumable: true); }); -Iap.Instance.PurchaseError.Subscribe(error => { +OpenIapClient.Instance.PurchaseError.Subscribe(error => { Console.WriteLine($"{error.Code}: {error.Message}"); }); // Then request the purchase -await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps { +await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { RequestPurchase = new RequestPurchasePropsByPlatforms { Apple = new RequestPurchaseIosProps { Sku = "com.app.premium" }, Google = new RequestPurchaseAndroidProps { Skus = new[] { "com.app.premium" } }, diff --git a/packages/docs/src/pages/docs/apis/restore-purchases.tsx b/packages/docs/src/pages/docs/apis/restore-purchases.tsx index 935c1009..e7a8f07e 100644 --- a/packages/docs/src/pages/docs/apis/restore-purchases.tsx +++ b/packages/docs/src/pages/docs/apis/restore-purchases.tsx @@ -177,7 +177,7 @@ function RestoreButton() { {`using OpenIap; using OpenIap.Maui; -await ((MutationResolver)Iap.Instance).RestorePurchasesAsync();`} +await ((MutationResolver)OpenIapClient.Instance).RestorePurchasesAsync();`} ), gdscript: ( {`await iap.restore_purchases()`} diff --git a/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx b/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx index 8d788193..01b94694 100644 --- a/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx +++ b/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx @@ -60,7 +60,7 @@ fun addDeveloperProvidedBillingListener( using OpenIap.Maui; // Observable callback approach. -IDisposable subscription = Iap.Instance.DeveloperProvidedBillingAndroid.Subscribe(details => +IDisposable subscription = OpenIapClient.Instance.DeveloperProvidedBillingAndroid.Subscribe(details => { Console.WriteLine(details.ExternalTransactionToken); });`} @@ -172,7 +172,7 @@ subscription.cancel();`} {`using OpenIap; using OpenIap.Maui; -var subscription = Iap.Instance.DeveloperProvidedBillingAndroid.Subscribe(async details => +var subscription = OpenIapClient.Instance.DeveloperProvidedBillingAndroid.Subscribe(async details => { Console.WriteLine("User selected developer billing"); Console.WriteLine($"Token: {details.ExternalTransactionToken}"); diff --git a/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx index f4483832..97e028cb 100644 --- a/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx +++ b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx @@ -174,7 +174,7 @@ subscription.cancel();`} using OpenIap.Maui; using System; -using var subscription = Iap.Instance.UserChoiceBillingAndroid.Subscribe(details => +using var subscription = OpenIapClient.Instance.UserChoiceBillingAndroid.Subscribe(details => { Console.WriteLine("User chose alternative billing"); Console.WriteLine($"Products: {string.Join(", ", details.Products)}"); diff --git a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx index a986ed34..52ec7a23 100644 --- a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx +++ b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx @@ -163,7 +163,7 @@ final subscription = FlutterInappPurchase.promotedProductIOS.listen((productId) subscription.cancel();`} ), csharp: ( - {`// .NET MAUI — see OpenIap.Maui.Iap.Instance. + {`// .NET MAUI — see OpenIap.Maui.OpenIapClient.Instance. // The full operation surface lives on OpenIap.QueryResolver / // MutationResolver / SubscriptionResolver (auto-generated from the schema).`} ), diff --git a/packages/docs/src/pages/docs/events/purchase-error-listener.tsx b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx index bef43149..ab36d25a 100644 --- a/packages/docs/src/pages/docs/events/purchase-error-listener.tsx +++ b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx @@ -49,7 +49,7 @@ val purchaseErrors: Flow`} using OpenIap.Maui; using System; -IObservable purchaseErrors = Iap.Instance.PurchaseError;`} +IObservable purchaseErrors = OpenIapClient.Instance.PurchaseError;`} ), }} @@ -211,7 +211,7 @@ subscription.cancel();`} {`using OpenIap; using OpenIap.Maui; -var subscription = Iap.Instance.PurchaseError.Subscribe(async error => +var subscription = OpenIapClient.Instance.PurchaseError.Subscribe(async error => { Console.WriteLine($"Purchase error: {error.Code} - {error.Message}"); @@ -222,7 +222,7 @@ var subscription = Iap.Instance.PurchaseError.Subscribe(async error => break; case ErrorCode.AlreadyOwned: // Restore purchases instead. - await ((MutationResolver)Iap.Instance).RestorePurchasesAsync(); + await ((MutationResolver)OpenIapClient.Instance).RestorePurchasesAsync(); break; case ErrorCode.NetworkError: ShowRetryDialog(); diff --git a/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx b/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx index e5649f19..f3c73a6d 100644 --- a/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx +++ b/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx @@ -70,7 +70,7 @@ val purchaseUpdates: Flow = kmpIAP.purchaseUpdatedListener`} purchaseUpdates = Iap.Instance.PurchaseUpdated;`} +IObservable purchaseUpdates = OpenIapClient.Instance.PurchaseUpdated;`} ), gdscript: ( {`signal purchase_updated(purchase: Purchase)`} @@ -116,7 +116,7 @@ IObservable purchaseUpdates = Iap.Instance.PurchaseUpdated;`} ), csharp: ( - {`var updates = Iap.Instance.PurchaseUpdatedWithOptions( + {`var updates = OpenIapClient.Instance.PurchaseUpdatedWithOptions( new PurchaseUpdatedListenerOptions { DedupeTransactionIOS = false, @@ -247,7 +247,7 @@ subscription.cancel();`} {`using OpenIap; using OpenIap.Maui; -var subscription = Iap.Instance.PurchaseUpdated.Subscribe(async purchase => +var subscription = OpenIapClient.Instance.PurchaseUpdated.Subscribe(async purchase => { if (purchase is PurchaseCommon purchaseInfo) { @@ -262,7 +262,7 @@ var subscription = Iap.Instance.PurchaseUpdated.Subscribe(async purchase => await DeliverProductAsync(validPurchase.ProductId); } - await ((MutationResolver)Iap.Instance).FinishTransactionAsync( + await ((MutationResolver)OpenIapClient.Instance).FinishTransactionAsync( new PurchaseInput(purchase), isConsumable: false); } diff --git a/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx index 6e78d392..7380fc86 100644 --- a/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx +++ b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx @@ -61,7 +61,7 @@ val subscriptionBillingIssueListener: Flow`} using OpenIap.Maui; // Observable callback approach (iOS 18+ / Play Billing 8.1+). -IDisposable subscription = Iap.Instance.SubscriptionBillingIssue.Subscribe(purchase => +IDisposable subscription = OpenIapClient.Instance.SubscriptionBillingIssue.Subscribe(purchase => { Console.WriteLine("Subscription billing issue received"); });`} @@ -172,7 +172,7 @@ subscription.cancel();`} using OpenIap.Maui; // iOS 18+ / Play Billing Library 8.1+ -var subscription = Iap.Instance.SubscriptionBillingIssue.Subscribe(purchase => +var subscription = OpenIapClient.Instance.SubscriptionBillingIssue.Subscribe(purchase => { if (purchase is PurchaseCommon purchaseInfo) { diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx index af3e010f..eec51289 100644 --- a/packages/docs/src/pages/docs/features/discount.tsx +++ b/packages/docs/src/pages/docs/features/discount.tsx @@ -443,7 +443,7 @@ using System; using OpenIap.Maui; using System.Linq; -var result = await ((QueryResolver)Iap.Instance).FetchProductsAsync(new ProductRequest +var result = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync(new ProductRequest { Skus = new[] { "premium_feature", "coins_100" }, Type = ProductQueryType.InApp, @@ -853,7 +853,7 @@ ProductCardViewModel BuildProductCard(ProductAndroid product) async Task PurchaseAsync(ProductAndroid product) { - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { RequestPurchase = new RequestPurchasePropsByPlatforms { @@ -1371,7 +1371,7 @@ async Task PurchaseWithOfferAsync(ProductAndroid product, int offerIndex = 0) var selectedOffer = offers.ElementAtOrDefault(offerIndex) ?? throw new ArgumentOutOfRangeException(nameof(offerIndex)); - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.InApp, RequestPurchase = new RequestPurchasePropsByPlatforms diff --git a/packages/docs/src/pages/docs/features/refund.tsx b/packages/docs/src/pages/docs/features/refund.tsx index 6b92a8a3..89c42284 100644 --- a/packages/docs/src/pages/docs/features/refund.tsx +++ b/packages/docs/src/pages/docs/features/refund.tsx @@ -205,7 +205,7 @@ switch (status) { {`using OpenIap; using OpenIap.Maui; -var status = await ((MutationResolver)Iap.Instance) +var status = await ((MutationResolver)OpenIapClient.Instance) .BeginRefundRequestIOSAsync(purchase.ProductId); switch (status) diff --git a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx b/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx index e576c4f2..cbb1d898 100644 --- a/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx +++ b/packages/docs/src/pages/docs/features/subscription-billing-issue.tsx @@ -183,7 +183,7 @@ final sub = iap.subscriptionBillingIssueListener.listen((purchase) { await sub.cancel();`} ), csharp: ( - {`// .NET MAUI — see OpenIap.Maui.Iap.Instance. + {`// .NET MAUI — see OpenIap.Maui.OpenIapClient.Instance. // The full operation surface lives on OpenIap.QueryResolver / // MutationResolver / SubscriptionResolver (auto-generated from the schema).`} ), diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx index 2cc14b08..d09d1dc7 100644 --- a/packages/docs/src/pages/docs/features/subscription/index.tsx +++ b/packages/docs/src/pages/docs/features/subscription/index.tsx @@ -256,7 +256,7 @@ using System; using System.Linq; // Fetch subscription products -var result = await ((QueryResolver)Iap.Instance).FetchProductsAsync(new ProductRequest +var result = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync(new ProductRequest { Skus = new[] { "premium_monthly" }, Type = ProductQueryType.Subs, @@ -454,7 +454,7 @@ string? DisplayIntroOffer(ProductSubscriptionIOS subscription) } // Check eligibility -var isEligible = await ((QueryResolver)Iap.Instance) +var isEligible = await ((QueryResolver)OpenIapClient.Instance) .IsEligibleForIntroOfferIOSAsync("premium_monthly"); if (isEligible && subscription is not null) @@ -695,7 +695,7 @@ async Task PurchaseWithPromoOfferAsync(string subscriptionId, string offerId) timestamp: timestamp); // 2. Purchase with the promotional offer - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -837,7 +837,7 @@ using OpenIap.Maui; async Task PurchaseSubscriptionAsync(string subscriptionId) { // Intro offer is applied automatically when eligible. - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -1163,7 +1163,7 @@ using System; using System.Linq; // Fetch subscription products -var result = await ((QueryResolver)Iap.Instance).FetchProductsAsync(new ProductRequest +var result = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync(new ProductRequest { Skus = new[] { "premium_monthly" }, Type = ProductQueryType.Subs, @@ -1404,7 +1404,7 @@ async Task PurchaseSubscriptionAsync(string subscriptionId, ProductSubscriptionA return; } - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -1755,7 +1755,7 @@ async Task HandlePurchaseAsync( // Store it before purchase. purchasedBasePlanId = basePlanId; - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -2202,7 +2202,7 @@ async Task PurchaseWithOfferAsync( return; } - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -2750,7 +2750,7 @@ using OpenIap.Maui; using System; // Check if user has any active subscription -var hasActive = await ((QueryResolver)Iap.Instance).HasActiveSubscriptionsAsync(); +var hasActive = await ((QueryResolver)OpenIapClient.Instance).HasActiveSubscriptionsAsync(); if (hasActive) { Console.WriteLine("User has premium access"); @@ -2758,7 +2758,7 @@ if (hasActive) // Get all active subscriptions var activeSubscriptions = - await ((QueryResolver)Iap.Instance).GetActiveSubscriptionsAsync(); + await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync(); foreach (var subscription in activeSubscriptions) { @@ -3193,7 +3193,7 @@ using System; // Note: subscriptionStatusIOS is iOS-only. async Task<(bool IsActive, string Status)> CheckSubscriptionStatusAsync(string sku) { - var statuses = await ((QueryResolver)Iap.Instance).SubscriptionStatusIOSAsync(sku); + var statuses = await ((QueryResolver)OpenIapClient.Instance).SubscriptionStatusIOSAsync(sku); foreach (var status in statuses) { @@ -3223,7 +3223,7 @@ async Task<(bool IsActive, string Status)> CheckSubscriptionStatusAsync(string s // Using ActiveSubscription for quick checks. async Task CheckFromActiveSubscriptionsAsync() { - var subscriptions = await ((QueryResolver)Iap.Instance).GetActiveSubscriptionsAsync(); + var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync(); var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); foreach (var subscription in subscriptions) @@ -3560,7 +3560,7 @@ public sealed record SubscriptionAccessResult( // Client-side: Can only check if purchase exists. async Task CheckAndroidSubscriptionAsync(string subscriptionId) { - var purchases = await ((QueryResolver)Iap.Instance).GetAvailablePurchasesAsync(); + var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync(); var purchase = purchases .OfType() .FirstOrDefault(purchase => purchase.ProductId == subscriptionId); @@ -3822,7 +3822,7 @@ Future manageSubscriptions() async { using OpenIap.Maui; // Open subscription management page. -await ((MutationResolver)Iap.Instance).DeepLinkToSubscriptionsAsync();`} +await ((MutationResolver)OpenIapClient.Instance).DeepLinkToSubscriptionsAsync();`} ), gdscript: ( {`# Open subscription management page diff --git a/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx b/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx index ffa91395..bc893e70 100644 --- a/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx +++ b/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx @@ -359,7 +359,7 @@ for (final sub in subscriptions) { using OpenIap.Maui; // Detecting upgrades with pendingUpgradeProductId. -var subscriptions = await ((QueryResolver)Iap.Instance).GetActiveSubscriptionsAsync(); +var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync(); foreach (var subscription in subscriptions) { @@ -532,7 +532,7 @@ for (final sub in subscriptions) { using OpenIap.Maui; // Detecting downgrades. -var subscriptions = await ((QueryResolver)Iap.Instance).GetActiveSubscriptionsAsync(); +var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync(); foreach (var subscription in subscriptions) { @@ -694,7 +694,7 @@ for (final sub in subscriptions) { {`using OpenIap; using OpenIap.Maui; -var subscriptions = await ((QueryResolver)Iap.Instance).GetActiveSubscriptionsAsync(); +var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync(); foreach (var subscription in subscriptions) { @@ -1119,7 +1119,7 @@ using OpenIap.Maui; // Complete example: build a subscription status message. async Task GetSubscriptionStatusMessageAsync() { - var subscriptions = await ((QueryResolver)Iap.Instance).GetActiveSubscriptionsAsync(); + var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync(); var subscription = subscriptions.FirstOrDefault(); if (subscription is null) @@ -1660,7 +1660,7 @@ using System.Linq; // Android upgrade with proration // Get current subscription -var purchases = await ((QueryResolver)Iap.Instance).GetAvailablePurchasesAsync(); +var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync(); var currentSub = purchases .OfType() .FirstOrDefault(purchase => purchase.ProductId == "basic_monthly"); @@ -1668,7 +1668,7 @@ var currentSub = purchases if (currentSub is not null) { // Upgrade to premium with time proration - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -1887,7 +1887,7 @@ using System.Linq; // Android downgrade with deferred replacement // Get current subscription -var purchases = await ((QueryResolver)Iap.Instance).GetAvailablePurchasesAsync(); +var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync(); var premiumPurchase = purchases .OfType() .FirstOrDefault(purchase => purchase.ProductId == "premium_monthly"); @@ -1895,7 +1895,7 @@ var premiumPurchase = purchases if (premiumPurchase is not null) { // Downgrade - takes effect at next billing cycle - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -2082,7 +2082,7 @@ using System.Linq; // Android subscription replacement with 8.1.0+ API // Get current subscription -var purchases = await ((QueryResolver)Iap.Instance).GetAvailablePurchasesAsync(); +var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync(); var currentSub = purchases .OfType() .FirstOrDefault(purchase => purchase.ProductId == "premium_monthly"); @@ -2090,7 +2090,7 @@ var currentSub = purchases if (currentSub is not null) { // Upgrade using the new per-product replacement params - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -2225,7 +2225,7 @@ using System.Linq; // Android - check if subscription will change -var purchases = await ((QueryResolver)Iap.Instance).GetAvailablePurchasesAsync(); +var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync(); foreach (var purchase in purchases.OfType()) { @@ -2539,7 +2539,7 @@ using System.Linq; async Task ChangeSubscriptionAsync(string newSku, bool isUpgrade) { // Get current subscription - var purchases = await ((QueryResolver)Iap.Instance).GetAvailablePurchasesAsync(); + var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync(); var currentSub = purchases .OfType() .FirstOrDefault(purchase => @@ -2558,7 +2558,7 @@ async Task ChangeSubscriptionAsync(string newSku, bool isUpgrade) try { - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms diff --git a/packages/docs/src/pages/docs/features/validation.tsx b/packages/docs/src/pages/docs/features/validation.tsx index c07a999c..b394e7b0 100644 --- a/packages/docs/src/pages/docs/features/validation.tsx +++ b/packages/docs/src/pages/docs/features/validation.tsx @@ -133,7 +133,7 @@ if (result.isValid) { {`using OpenIap; using OpenIap.Maui; -var result = await ((MutationResolver)Iap.Instance).VerifyPurchaseAsync( +var result = await ((MutationResolver)OpenIapClient.Instance).VerifyPurchaseAsync( new VerifyPurchaseProps { Google = new VerifyPurchaseGoogleOptions diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx index 60ddbc1a..1e15802a 100644 --- a/packages/docs/src/pages/docs/getting-started.tsx +++ b/packages/docs/src/pages/docs/getting-started.tsx @@ -307,8 +307,8 @@ await iap.requestPurchaseWithBuilder( {`using OpenIap; using OpenIap.Maui; -var query = (QueryResolver)Iap.Instance; -var mutation = (MutationResolver)Iap.Instance; +var query = (QueryResolver)OpenIapClient.Instance; +var mutation = (MutationResolver)OpenIapClient.Instance; // 1. Open the store connection on app start. await mutation.InitConnectionAsync(); @@ -321,7 +321,7 @@ var products = await query.FetchProductsAsync(new ProductRequest }); // 3. Listen for purchase results. -var subscription = Iap.Instance.PurchaseUpdated.Subscribe(async purchase => +var subscription = OpenIapClient.Instance.PurchaseUpdated.Subscribe(async purchase => { // Verify on your backend, grant entitlement, then finish. await mutation.FinishTransactionAsync( diff --git a/packages/docs/src/pages/docs/kit-backend.tsx b/packages/docs/src/pages/docs/kit-backend.tsx index 615e8f37..88c40c4c 100644 --- a/packages/docs/src/pages/docs/kit-backend.tsx +++ b/packages/docs/src/pages/docs/kit-backend.tsx @@ -181,7 +181,7 @@ if (active) { using OpenIap.Maui; var openiapProjectKey = ""; -var api = Iap.KitApi(new KitApiOptions { ApiKey = openiapProjectKey }); +var api = OpenIapClient.KitApi(new KitApiOptions { ApiKey = openiapProjectKey }); var status = await api.StatusAsync("user-1"); if (status.Active) { diff --git a/packages/docs/src/pages/docs/setup/maui.tsx b/packages/docs/src/pages/docs/setup/maui.tsx index 14e99987..6f200cd2 100644 --- a/packages/docs/src/pages/docs/setup/maui.tsx +++ b/packages/docs/src/pages/docs/setup/maui.tsx @@ -22,10 +22,11 @@ function MauiSetup() {

Package shape: apps reference only{' '} - OpenIap.Maui. The Android binding, iOS binding, Google - Play Billing AARs, and StoreKit xcframework resources are flattened - into that package, so NuGet consumers do not add separate binding - packages or NativeReference entries. + OpenIap.Maui. OpenIAP-owned Android and iOS binding + outputs are flattened into that package. Google Billing, Play + Services, Gson, AndroidX, and Kotlin Android libraries stay as normal + NuGet dependencies so your app can deduplicate them with its own + package graph.

@@ -61,7 +62,7 @@ function MauiSetup() { .NET - .NET 9 SDK and the MAUI workload:{' '} + .NET 9 or .NET 10 SDK and the MAUI workload:{' '} dotnet workload install maui @@ -150,7 +151,11 @@ function MauiSetup() { TargetFrameworks:

- {`net9.0-ios;net9.0-android;net9.0-maccatalyst + {` +net9.0-ios;net9.0-android;net9.0-maccatalyst + + +net10.0-ios;net10.0-android;net10.0-maccatalyst 15.0 15.0 @@ -220,14 +225,14 @@ function MauiSetup() {

- Use Iap.Instance as the entry point. Cast it to the - generated resolver interfaces when calling query or mutation APIs. + Use OpenIapClient.Instance as the entry point. Cast it to + the generated resolver interfaces when calling query or mutation APIs.

{`using OpenIap; using OpenIap.Maui; -var iap = Iap.Instance; +var iap = OpenIapClient.Instance; var query = (QueryResolver)iap; var mutate = (MutationResolver)iap; @@ -316,7 +321,7 @@ await mutate.RequestPurchaseAsync(new RequestPurchaseProps {`using OpenIap; using OpenIap.Maui; -var kit = Iap.KitApi(new KitApiOptions +var kit = OpenIapClient.KitApi(new KitApiOptions { ApiKey = "openiap-kit_", BaseUrl = "https://kit.openiap.dev", @@ -326,14 +331,14 @@ StatusResponse status = await kit.StatusAsync("user_123"); EntitlementsResponse entitlements = await kit.EntitlementsAsync("user_123"); BindUserResponse bind = await kit.BindUserAsync(purchase.PurchaseToken!, "user_123"); -using WebhookListener listener = Iap.ConnectWebhookStream(new WebhookListenerOptions +using WebhookListener listener = OpenIapClient.ConnectWebhookStream(new WebhookListenerOptions { ApiKey = "openiap-kit_", OnEvent = webhookEvent => Console.WriteLine(webhookEvent.Type), OnError = error => Console.WriteLine($"{error.Code}: {error.Message}"), }); -ParsedWebhookEventResult parsed = Iap.ParseWebhookEventData(rawSseData);`} +ParsedWebhookEventResult parsed = OpenIapClient.ParseWebhookEventData(rawSseData);`}

@@ -385,6 +390,10 @@ dotnet build -t:Run -f net9.0-ios # macCatalyst dotnet build -t:Run -f net9.0-maccatalyst`} +

+ Replace net9.0-* with net10.0-* when your + app targets .NET 10. +

VS Code launch configurations are available in{' '} libraries/maui-iap/.vscode/launch.json. The iOS device diff --git a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx index b6e80656..80d60069 100644 --- a/packages/docs/src/pages/docs/types/alternative-billing-types.tsx +++ b/packages/docs/src/pages/docs/types/alternative-billing-types.tsx @@ -261,28 +261,28 @@ await FlutterInappPurchase.instance.initConnection();`} using OpenIap.Maui; // Initialize with user choice billing (7.0+) -await ((MutationResolver)Iap.Instance).InitConnectionAsync( +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync( new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling, }); // Initialize with external offer (alternative only) -await ((MutationResolver)Iap.Instance).InitConnectionAsync( +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync( new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer, }); // Initialize with external payments (Japan only, 8.3.0+) -await ((MutationResolver)Iap.Instance).InitConnectionAsync( +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync( new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments, }); // Standard billing (default) -await ((MutationResolver)Iap.Instance).InitConnectionAsync();`} +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync();`} ), gdscript: ( {`# Initialize with user choice billing (7.0+) @@ -470,7 +470,7 @@ userChoiceSubscription.cancel();`} using OpenIap.Maui; // Step 1: Set up listener for when user selects alternative billing. -var userChoiceSubscription = Iap.Instance.UserChoiceBillingAndroid.Subscribe(async details => +var userChoiceSubscription = OpenIapClient.Instance.UserChoiceBillingAndroid.Subscribe(async details => { Console.WriteLine("User chose alternative billing"); Console.WriteLine($"Products: {string.Join(", ", details.Products)}"); @@ -487,20 +487,20 @@ var userChoiceSubscription = Iap.Instance.UserChoiceBillingAndroid.Subscribe(asy }); // Step 2: Initialize with user choice billing (recommended). -await ((MutationResolver)Iap.Instance).InitConnectionAsync(new InitConnectionConfig +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling, }); // Step 3: Fetch products and purchase as normal. -await ((QueryResolver)Iap.Instance).FetchProductsAsync(new ProductRequest +await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync(new ProductRequest { Skus = new[] { "premium_subscription" }, Type = ProductQueryType.Subs, }); // Step 4: Request purchase - dialog will show both options. -await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(new RequestPurchaseProps +await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps { Type = ProductQueryType.Subs, RequestSubscription = new RequestSubscriptionPropsByPlatforms @@ -728,14 +728,14 @@ using OpenIap.Maui; using System.Linq; // Step 1: Initialize with external offer (recommended). -await ((MutationResolver)Iap.Instance).InitConnectionAsync(new InitConnectionConfig +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer, }); // Step 2: Check if alternative billing is available. var isAvailable = - await ((MutationResolver)Iap.Instance).CheckAlternativeBillingAvailabilityAndroidAsync(); + await ((MutationResolver)OpenIapClient.Instance).CheckAlternativeBillingAvailabilityAndroidAsync(); if (!isAvailable) { Console.WriteLine("Alternative billing not available in this region"); @@ -743,7 +743,7 @@ if (!isAvailable) } // Step 3: Fetch products (still needed to show prices). -var result = await ((QueryResolver)Iap.Instance).FetchProductsAsync(new ProductRequest +var result = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync(new ProductRequest { Skus = new[] { "premium_subscription" }, Type = ProductQueryType.Subs, @@ -754,7 +754,7 @@ var product = result is FetchProductsResultSubscriptions subscriptions // Step 4: Show required Google Play disclosure dialog. var accepted = - await ((MutationResolver)Iap.Instance).ShowAlternativeBillingDialogAndroidAsync(); + await ((MutationResolver)OpenIapClient.Instance).ShowAlternativeBillingDialogAndroidAsync(); if (!accepted || product is null) { Console.WriteLine("User did not accept alternative billing"); @@ -763,7 +763,7 @@ if (!accepted || product is null) // Step 5: Create token for this transaction. var token = - await ((MutationResolver)Iap.Instance).CreateAlternativeBillingTokenAndroidAsync(); + await ((MutationResolver)OpenIapClient.Instance).CreateAlternativeBillingTokenAndroidAsync(); // Step 6: Process purchase with your backend. var paymentResult = await ProcessAlternativePurchaseAsync( diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index 17e3e387..6b957e64 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -582,20 +582,20 @@ using OpenIap.Maui; using System; // Enable External Payments via InitConnectionConfig. -await ((MutationResolver)Iap.Instance).InitConnectionAsync(new InitConnectionConfig +await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(new InitConnectionConfig { EnableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments, }); // Listen for developer billing selection. -using var subscription = Iap.Instance.DeveloperProvidedBillingAndroid.Subscribe(details => +using var subscription = OpenIapClient.Instance.DeveloperProvidedBillingAndroid.Subscribe(details => { Console.WriteLine($"Token: {details.ExternalTransactionToken}"); // Report token to Google via your backend within 24 hours. }); // Check availability (Japan only). -var result = await ((MutationResolver)Iap.Instance) +var result = await ((MutationResolver)OpenIapClient.Instance) .IsBillingProgramAvailableAndroidAsync(BillingProgramAndroid.ExternalPayments); if (result.IsAvailable) { @@ -617,7 +617,7 @@ if (result.IsAvailable) }, Type = ProductQueryType.InApp, }; - await ((MutationResolver)Iap.Instance).RequestPurchaseAsync(props); + await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(props); }`} ), gdscript: ( diff --git a/packages/docs/src/pages/docs/types/external-purchase-link.tsx b/packages/docs/src/pages/docs/types/external-purchase-link.tsx index 56ec335d..a97fb7fa 100644 --- a/packages/docs/src/pages/docs/types/external-purchase-link.tsx +++ b/packages/docs/src/pages/docs/types/external-purchase-link.tsx @@ -581,7 +581,7 @@ async Task HandleExternalPurchaseAsync(string externalUrl) { // Step 1: Check if external purchase is available. var canPresent = - await ((QueryResolver)Iap.Instance).CanPresentExternalPurchaseNoticeIOSAsync(); + await ((QueryResolver)OpenIapClient.Instance).CanPresentExternalPurchaseNoticeIOSAsync(); if (!canPresent) { Console.WriteLine("External purchase not available on this device"); @@ -590,7 +590,7 @@ async Task HandleExternalPurchaseAsync(string externalUrl) // Step 2: Present Apple's compliance notice sheet. var noticeResult = - await ((MutationResolver)Iap.Instance).PresentExternalPurchaseNoticeSheetIOSAsync(); + await ((MutationResolver)OpenIapClient.Instance).PresentExternalPurchaseNoticeSheetIOSAsync(); if (noticeResult.Result == ExternalPurchaseNoticeAction.Dismissed) { Console.WriteLine("User dismissed the notice sheet"); @@ -599,7 +599,7 @@ async Task HandleExternalPurchaseAsync(string externalUrl) // Step 3: Open external purchase link. var linkResult = - await ((MutationResolver)Iap.Instance).PresentExternalPurchaseLinkIOSAsync(externalUrl); + await ((MutationResolver)OpenIapClient.Instance).PresentExternalPurchaseLinkIOSAsync(externalUrl); if (linkResult.Success) { Console.WriteLine("User redirected to external payment"); diff --git a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx index f5cb5ea7..b34871ff 100644 --- a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx @@ -359,7 +359,7 @@ if (appTransaction != null) { using OpenIap.Maui; var appTransaction = - await ((QueryResolver)Iap.Instance).GetAppTransactionIOSAsync(); + await ((QueryResolver)OpenIapClient.Instance).GetAppTransactionIOSAsync(); if (appTransaction is not null) { diff --git a/packages/docs/src/pages/docs/types/product-request.tsx b/packages/docs/src/pages/docs/types/product-request.tsx index d9d0479b..96051063 100644 --- a/packages/docs/src/pages/docs/types/product-request.tsx +++ b/packages/docs/src/pages/docs/types/product-request.tsx @@ -174,11 +174,11 @@ final allProducts = await FlutterInappPurchase.instance.fetchProducts( using OpenIap.Maui; // Fetch in-app purchases (default) -var inappProducts = await ((QueryResolver)Iap.Instance).FetchProductsAsync( +var inappProducts = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync( new ProductRequest { Skus = new[] { "product1", "product2" } }); // Fetch only subscriptions -var subscriptions = await ((QueryResolver)Iap.Instance).FetchProductsAsync( +var subscriptions = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync( new ProductRequest { Skus = new[] { "sub1", "sub2" }, @@ -186,7 +186,7 @@ var subscriptions = await ((QueryResolver)Iap.Instance).FetchProductsAsync( }); // Fetch all products (both in-app and subscriptions) -var allProducts = await ((QueryResolver)Iap.Instance).FetchProductsAsync( +var allProducts = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync( new ProductRequest { Skus = new[] { "product1", "sub1" }, diff --git a/packages/docs/src/pages/docs/types/purchase-updated-listener-options.tsx b/packages/docs/src/pages/docs/types/purchase-updated-listener-options.tsx index f2f6662f..20b5ab16 100644 --- a/packages/docs/src/pages/docs/types/purchase-updated-listener-options.tsx +++ b/packages/docs/src/pages/docs/types/purchase-updated-listener-options.tsx @@ -109,7 +109,7 @@ function PurchaseUpdatedListenerOptions() { );`} ), csharp: ( - {`Iap.Instance.PurchaseUpdatedWithOptions( + {`OpenIapClient.Instance.PurchaseUpdatedWithOptions( new PurchaseUpdatedListenerOptions { DedupeTransactionIOS = false, diff --git a/packages/docs/src/pages/docs/types/request-purchase-props.tsx b/packages/docs/src/pages/docs/types/request-purchase-props.tsx index 2b6a37af..3bd3b219 100644 --- a/packages/docs/src/pages/docs/types/request-purchase-props.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -227,7 +227,7 @@ await FlutterInappPurchase.instance.requestPurchase( using OpenIap.Maui; // Standard in-app purchase -await ((MutationResolver)Iap.Instance).RequestPurchaseAsync( +await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync( new RequestPurchaseProps { RequestPurchase = new RequestPurchasePropsByPlatforms @@ -241,7 +241,7 @@ await ((MutationResolver)Iap.Instance).RequestPurchaseAsync( }); // Subscription purchase -await ((MutationResolver)Iap.Instance).RequestPurchaseAsync( +await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync( new RequestPurchaseProps { RequestPurchase = new RequestPurchasePropsByPlatforms diff --git a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx index 36e3bd82..4af6a36e 100644 --- a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx +++ b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx @@ -422,7 +422,7 @@ var props = new VerifyPurchaseWithProviderProps }; // Verify purchase -var result = await ((MutationResolver)Iap.Instance).VerifyPurchaseWithProviderAsync(props); +var result = await ((MutationResolver)OpenIapClient.Instance).VerifyPurchaseWithProviderAsync(props); // Check result var iapkit = result.Iapkit; diff --git a/packages/docs/src/pages/introduction.tsx b/packages/docs/src/pages/introduction.tsx index 8d1fc4f5..4bc57950 100644 --- a/packages/docs/src/pages/introduction.tsx +++ b/packages/docs/src/pages/introduction.tsx @@ -511,7 +511,7 @@ await iap.endConnection();`} {`using OpenIap; using OpenIap.Maui; -var iap = Iap.Instance; +var iap = OpenIapClient.Instance; var query = (QueryResolver)iap; var mutation = (MutationResolver)iap; diff --git a/scripts/agent/compile-context.ts b/scripts/agent/compile-context.ts index 52666295..7e8d37b3 100644 --- a/scripts/agent/compile-context.ts +++ b/scripts/agent/compile-context.ts @@ -177,7 +177,7 @@ dotnet add package ${versions.mauiPackageId} Current NuGet package version: ${versions.maui} -Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. +Requires .NET 9 or .NET 10, the MAUI workload, iOS 15.0+, and Android API 24+. --- @@ -223,20 +223,23 @@ Requires .NET 9+, the MAUI workload, iOS 15.0+, and Android API 24+. are private implementation details and are flattened into \`OpenIap.Maui\` instead of being published as separate package dependencies. - Implementation: .NET MAUI projection with generated \`Types.cs\`, a static - \`Iap.Instance\` facade, \`IOpenIap\` observables, and per-platform resolvers. + \`OpenIapClient.Instance\` facade, legacy \`Iap\` shim, \`IOpenIap\` + observables, and per-platform resolvers. - iOS/macCatalyst bridge: .NET-for-iOS binding over \`OpenIAP.xcframework\` and \`OpenIapModule+ObjC.swift\`; NuGet consumers get the official \`OpenIap.Maui.Bindings.iOS.resources.zip\` sidecar so no app-level \`NativeReference\` is required. - Android bridge: Xamarin.Android binding over the MAUI-owned \`openiap-release.aar\`, which wraps the unbound - \`openiap-play-release.aar\` runtime dependency; resolved BillingClient / - Play Services AARs are included in the main nupkg for app packaging. + \`openiap-play-release.aar\` runtime dependency. Google Billing, Play + Services, Gson, AndroidX, and Kotlin Android libraries stay as NuGet + \`PackageReference\` dependencies so consuming apps can deduplicate them. - Public surface: \`QueryResolver\`, \`MutationResolver\`, and \`IOpenIap\` implemented by \`OpenIapIOS\`, \`OpenIapAndroid\`, and \`OpenIapMacCatalyst\`; - IAPKit helpers mirror the TypeScript SDKs via \`Iap.KitApi(...)\`, - \`Iap.ConnectWebhookStream(...)\`, \`Iap.ParseWebhookEventData(...)\`, and - \`Iap.WebhookEventTypes\`. + IAPKit helpers mirror the TypeScript SDKs via + \`OpenIapClient.KitApi(...)\`, \`OpenIapClient.ConnectWebhookStream(...)\`, + \`OpenIapClient.ParseWebhookEventData(...)\`, and + \`OpenIapClient.WebhookEventTypes\`. - Example app: \`libraries/maui-iap/example/OpenIap.Maui.Example\`, mirroring the \`expo-iap\` example flows. @@ -297,7 +300,7 @@ iap.purchaseUpdatedListener.collect { purchase -> using OpenIap; using OpenIap.Maui; -var iap = Iap.Instance; +var iap = OpenIapClient.Instance; await ((MutationResolver)iap).InitConnectionAsync(); await ((QueryResolver)iap).FetchProductsAsync(new ProductRequest @@ -405,11 +408,13 @@ Current NuGet package version: ${versions.maui} - \`flutter_inapp_purchase\`: Dart API with generated OpenIAP types and streams. - \`godot-iap\`: Godot 4.x plugin with GDScript functions and signals. - \`kmp-iap\`: Kotlin Multiplatform API with Flow-based purchase events. -- \`maui-iap\`: \`OpenIap.Maui\` package with \`Iap.Instance\`, - generated \`Types.cs\`, IAPKit helpers (\`Iap.KitApi\`, - \`Iap.ConnectWebhookStream\`, \`Iap.ParseWebhookEventData\`), flattened iOS - xcframework / Android AAR bindings in one NuGet package, and MAUI example - flows matching \`expo-iap\`. +- \`maui-iap\`: \`OpenIap.Maui\` package with \`OpenIapClient.Instance\`, + generated \`Types.cs\`, IAPKit helpers (\`OpenIapClient.KitApi\`, + \`OpenIapClient.ConnectWebhookStream\`, + \`OpenIapClient.ParseWebhookEventData\`), flattened OpenIAP-owned iOS + xcframework / Android AAR bindings, Google and AndroidX Android + dependencies as NuGet package references, and MAUI example flows matching + \`expo-iap\`. ## Core APIs diff --git a/scripts/audit-non-godot-parity.mjs b/scripts/audit-non-godot-parity.mjs index e266c45d..51275b19 100644 --- a/scripts/audit-non-godot-parity.mjs +++ b/scripts/audit-non-godot-parity.mjs @@ -2744,25 +2744,32 @@ function checkFrameworkDependencyHygiene() { const mauiProps = read('libraries/maui-iap/src/Directory.Build.props'); const mauiBillingVersion = mauiProps.match(/([^<]+)<\/MauiPlayBillingVersion>/)?.[1]; const mauiGsonVersion = mauiProps.match(/([^<]+)<\/MauiGsonVersion>/)?.[1]; + const mauiBillingClientNuGetVersion = mauiProps.match(/([^<]+)<\/MauiBillingClientNuGetVersion>/)?.[1]; + const mauiGoogleGsonNuGetVersion = mauiProps.match(/([^<]+)<\/MauiGoogleGsonNuGetVersion>/)?.[1]; + const mauiAndroidXActivityVersion = mauiProps.match(/([^<]+)<\/MauiAndroidXActivityVersion>/)?.[1]; + const mauiAndroidXFragmentVersion = mauiProps.match(/([^<]+)<\/MauiAndroidXFragmentVersion>/)?.[1]; + const mauiAndroidXLifecycleVersion = mauiProps.match(/([^<]+)<\/MauiAndroidXLifecycleVersion>/)?.[1]; + const mauiAndroidXSavedStateVersion = mauiProps.match(/([^<]+)<\/MauiAndroidXSavedStateVersion>/)?.[1]; expectIncludes('scripts/sync-versions.sh', [ 'sync_maui_android_versions', 'packages/google/openiap/build.gradle.kts', 'MauiPlayBillingVersion', - 'MauiGoogleTransportApiVersion', - 'MauiGooglePlayServicesTasksVersion', + 'MauiBillingClientNuGetVersion', + 'MauiGoogleGsonNuGetVersion', + 'MauiAndroidXActivityVersion', + 'MauiAndroidXLifecycleVersion', 'mauiGsonVersion', ], 'root version sync must update MAUI Android dependency versions from packages/google'); expectIncludes('libraries/maui-iap/src/Directory.Build.props', [ 'Generated by scripts/sync-versions.sh', 'MauiPlayBillingVersion', 'MauiGsonVersion', - 'MauiGoogleTransportApiVersion', - 'MauiGoogleTransportBackendCctVersion', - 'MauiGoogleTransportRuntimeVersion', - 'MauiGooglePlayServicesBaseVersion', - 'MauiGooglePlayServicesBasementVersion', - 'MauiGooglePlayServicesLocationVersion', - 'MauiGooglePlayServicesTasksVersion', + 'MauiBillingClientNuGetVersion', + 'MauiGoogleGsonNuGetVersion', + 'MauiAndroidXActivityVersion', + 'MauiAndroidXFragmentVersion', + 'MauiAndroidXLifecycleVersion', + 'MauiAndroidXSavedStateVersion', ], 'MAUI Directory.Build.props must be generated by version sync'); if (!mauiBillingVersion) { fail('MAUI Directory.Build.props must define MauiPlayBillingVersion'); @@ -2770,12 +2777,38 @@ function checkFrameworkDependencyHygiene() { if (!mauiGsonVersion) { fail('MAUI Directory.Build.props must define MauiGsonVersion'); } + if (!mauiBillingClientNuGetVersion) { + fail('MAUI Directory.Build.props must define MauiBillingClientNuGetVersion'); + } + if (!mauiGoogleGsonNuGetVersion) { + fail('MAUI Directory.Build.props must define MauiGoogleGsonNuGetVersion'); + } + if (!mauiAndroidXActivityVersion) { + fail('MAUI Directory.Build.props must define MauiAndroidXActivityVersion'); + } + if (!mauiAndroidXFragmentVersion) { + fail('MAUI Directory.Build.props must define MauiAndroidXFragmentVersion'); + } + if (!mauiAndroidXLifecycleVersion) { + fail('MAUI Directory.Build.props must define MauiAndroidXLifecycleVersion'); + } + if (!mauiAndroidXSavedStateVersion) { + fail('MAUI Directory.Build.props must define MauiAndroidXSavedStateVersion'); + } if (googleBillingVersions.length === 1) { if (mauiBillingVersion !== googleBillingVersions[0]) { fail( `MAUI Android Billing Maven version ${mauiBillingVersion} must match packages/google ${googleBillingVersions[0]}`, ); } + if ( + mauiBillingClientNuGetVersion !== googleBillingVersions[0] && + !mauiBillingClientNuGetVersion.startsWith(`${googleBillingVersions[0]}.`) + ) { + fail( + `MAUI Android BillingClient NuGet version ${mauiBillingClientNuGetVersion} must track packages/google ${googleBillingVersions[0]}`, + ); + } } if (googleGsonVersions.length === 1) { if (mauiGsonVersion !== googleGsonVersions[0]) { @@ -2834,19 +2867,15 @@ function checkFrameworkDependencyHygiene() { ], `${mauiGradlePluginFile} must not hardcode Google Gradle plugin versions`); } expectIncludes('libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj', [ - 'Version="$(MauiPlayBillingVersion)"', - 'Version="$(MauiGsonVersion)"', + 'Xamarin.Android.Google.BillingClient', + 'Version="$(MauiBillingClientNuGetVersion)"', + 'GoogleGson', + 'Version="$(MauiGoogleGsonNuGetVersion)"', 'Version="$(MauiKotlinStdLibVersion)"', 'Version="$(MauiKotlinCoroutinesVersion)"', - 'Version="$(MauiGoogleTransportApiVersion)"', - 'Version="$(MauiGoogleTransportBackendCctVersion)"', - 'Version="$(MauiGoogleTransportRuntimeVersion)"', - 'Version="$(MauiGooglePlayServicesBaseVersion)"', - 'Version="$(MauiGooglePlayServicesBasementVersion)"', - 'Version="$(MauiGooglePlayServicesLocationVersion)"', - 'Version="$(MauiGooglePlayServicesTasksVersion)"', ], 'MAUI Android binding dependency versions'); expectNotIncludes('libraries/maui-iap/src/OpenIap.Maui.Bindings.Android/OpenIap.Maui.Bindings.Android.csproj', [ + 'net9.0-* with net10.0-*', + ], 'MAUI setup docs must describe net10 and NuGet Google dependency shape'); expectIncludes('libraries/flutter_inapp_purchase/android/settings.gradle', [ "new File(settingsDir, '../../../packages/google/openiap')", ], 'Flutter Android local OpenIAP module hint'); diff --git a/scripts/sync-versions.sh b/scripts/sync-versions.sh index 7083e1d7..9b6d0d4b 100755 --- a/scripts/sync-versions.sh +++ b/scripts/sync-versions.sh @@ -236,15 +236,14 @@ import sys path, play_billing, gson = sys.argv[1:] text = open(path, encoding="utf-8").read() preserved_property_names = [ + "MauiBillingClientNuGetVersion", + "MauiGoogleGsonNuGetVersion", + "MauiAndroidXActivityVersion", + "MauiAndroidXFragmentVersion", + "MauiAndroidXLifecycleVersion", + "MauiAndroidXSavedStateVersion", "MauiKotlinStdLibVersion", "MauiKotlinCoroutinesVersion", - "MauiGoogleTransportApiVersion", - "MauiGoogleTransportBackendCctVersion", - "MauiGoogleTransportRuntimeVersion", - "MauiGooglePlayServicesBaseVersion", - "MauiGooglePlayServicesBasementVersion", - "MauiGooglePlayServicesLocationVersion", - "MauiGooglePlayServicesTasksVersion", ] preserved = {} for name in preserved_property_names: @@ -258,15 +257,14 @@ content = f""" {play_billing} {gson} + {preserved["MauiBillingClientNuGetVersion"]} + {preserved["MauiGoogleGsonNuGetVersion"]} + {preserved["MauiAndroidXActivityVersion"]} + {preserved["MauiAndroidXFragmentVersion"]} + {preserved["MauiAndroidXLifecycleVersion"]} + {preserved["MauiAndroidXSavedStateVersion"]} {preserved["MauiKotlinStdLibVersion"]} {preserved["MauiKotlinCoroutinesVersion"]} - {preserved["MauiGoogleTransportApiVersion"]} - {preserved["MauiGoogleTransportBackendCctVersion"]} - {preserved["MauiGoogleTransportRuntimeVersion"]} - {preserved["MauiGooglePlayServicesBaseVersion"]} - {preserved["MauiGooglePlayServicesBasementVersion"]} - {preserved["MauiGooglePlayServicesLocationVersion"]} - {preserved["MauiGooglePlayServicesTasksVersion"]} """ From 9fd446d290cb2ffcb739b18ab786e92e85ceb550 Mon Sep 17 00:00:00 2001 From: hyochan Date: Tue, 16 Jun 2026 00:36:11 +0900 Subject: [PATCH 2/5] fix(maui): address review feedback Fix MAUI C# documentation snippets, avoid logging external transaction tokens in examples, and guard the parity audit against missing BillingClient NuGet metadata. --- .../apis/ios/can-present-external-purchase-notice-ios.tsx | 2 +- .../docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx | 2 +- .../docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx | 4 ++-- .../docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx | 2 +- .../apis/ios/get-external-purchase-custom-link-token-ios.tsx | 4 ++-- .../src/pages/docs/apis/ios/get-pending-transactions-ios.tsx | 4 ++-- .../docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx | 2 +- .../docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx | 2 +- .../docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx | 2 +- .../android/developer-provided-billing-listener-android.tsx | 4 ++-- .../events/android/user-choice-billing-listener-android.tsx | 2 +- scripts/audit-non-godot-parity.mjs | 1 + 12 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx b/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx index c366ac54..8491e8ac 100644 --- a/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx @@ -96,7 +96,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var can = await ((QueryResolver)OpenIapClient.Instance).CanPresentExternalPurchaseNoticeIOSAsync()`} +var can = await ((QueryResolver)OpenIapClient.Instance).CanPresentExternalPurchaseNoticeIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx b/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx index 2b4f7ace..39038768 100644 --- a/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx @@ -111,7 +111,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var entitlement = await ((QueryResolver)OpenIapClient.Instance).CurrentEntitlementIOSAsync(sku: "com.app.premium")`} +var entitlement = await ((QueryResolver)OpenIapClient.Instance).CurrentEntitlementIOSAsync(sku: "com.app.premium");`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx index c70da567..f665623f 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx @@ -55,7 +55,7 @@ function GetAllTransactionsIOS() { {`Future> getAllTransactionsIOS();`} ), csharp: ( - {`Task> GetAllTransactionsIOSAsync()`} + {`Task> GetAllTransactionsIOSAsync()`} ), gdscript: ( {`func get_all_transactions_ios() -> Variant`} @@ -105,7 +105,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var txs = await ((QueryResolver)OpenIapClient.Instance).GetAllTransactionsIOSAsync()`} +var txs = await ((QueryResolver)OpenIapClient.Instance).GetAllTransactionsIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx index 7b6b5963..3f07f131 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx @@ -97,7 +97,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var appTx = await ((QueryResolver)OpenIapClient.Instance).GetAppTransactionIOSAsync()`} +var appTx = await ((QueryResolver)OpenIapClient.Instance).GetAppTransactionIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx index 7c5b0d79..a2873d0a 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx @@ -141,8 +141,8 @@ using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) var token = await ((QueryResolver)OpenIapClient.Instance).GetExternalPurchaseCustomLinkTokenIOSAsync( - tokenType = ExternalPurchaseCustomLinkTokenTypeIOS.ACQUISITION -)`} + tokenType: ExternalPurchaseCustomLinkTokenTypeIOS.ACQUISITION +);`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx index 16b9e85f..38829cd1 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx @@ -50,7 +50,7 @@ function GetPendingTransactionsIOS() { {`Future> getPendingTransactionsIOS();`} ), csharp: ( - {`Task> GetPendingTransactionsIOSAsync()`} + {`Task> GetPendingTransactionsIOSAsync()`} ), gdscript: ( {`func get_pending_transactions_ios() -> Variant`} @@ -100,7 +100,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var txs = await ((QueryResolver)OpenIapClient.Instance).GetPendingTransactionsIOSAsync()`} +var txs = await ((QueryResolver)OpenIapClient.Instance).GetPendingTransactionsIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx index c4b3260a..0d151d98 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx @@ -98,7 +98,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var product = await ((QueryResolver)OpenIapClient.Instance).GetPromotedProductIOSAsync()`} +var product = await ((QueryResolver)OpenIapClient.Instance).GetPromotedProductIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx index c0704820..520a7b33 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx @@ -95,7 +95,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var data = await ((QueryResolver)OpenIapClient.Instance).GetReceiptDataIOSAsync()`} +var data = await ((QueryResolver)OpenIapClient.Instance).GetReceiptDataIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx index 2ae2162c..a8f52d92 100644 --- a/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx @@ -133,7 +133,7 @@ using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) // Deprecated — prefer verifyPurchase(). -await ((MutationResolver)OpenIapClient.Instance).ValidateReceiptIOSAsync( +await ((QueryResolver)OpenIapClient.Instance).ValidateReceiptIOSAsync( new VerifyPurchaseProps { Apple = new VerifyPurchaseAppleOptions diff --git a/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx b/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx index 01b94694..18697d4b 100644 --- a/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx +++ b/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx @@ -62,7 +62,7 @@ using OpenIap.Maui; // Observable callback approach. IDisposable subscription = OpenIapClient.Instance.DeveloperProvidedBillingAndroid.Subscribe(details => { - Console.WriteLine(details.ExternalTransactionToken); + Console.WriteLine("External transaction token received; send it to your backend without logging it."); });`} ), }} @@ -175,7 +175,7 @@ using OpenIap.Maui; var subscription = OpenIapClient.Instance.DeveloperProvidedBillingAndroid.Subscribe(async details => { Console.WriteLine("User selected developer billing"); - Console.WriteLine($"Token: {details.ExternalTransactionToken}"); + Console.WriteLine("External transaction token received; send it to your backend without logging it."); var paymentResult = await ProcessPaymentWithYourGatewayAsync( details.ExternalTransactionToken); diff --git a/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx index 97e028cb..a0e03d43 100644 --- a/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx +++ b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx @@ -178,7 +178,7 @@ using var subscription = OpenIapClient.Instance.UserChoiceBillingAndroid.Subscri { Console.WriteLine("User chose alternative billing"); Console.WriteLine($"Products: {string.Join(", ", details.Products)}"); - Console.WriteLine($"Token: {details.ExternalTransactionToken}"); + Console.WriteLine("External transaction token received; send it to your backend without logging it."); // Process payment with your backend. _ = ProcessUserChoiceBillingAsync(details); diff --git a/scripts/audit-non-godot-parity.mjs b/scripts/audit-non-godot-parity.mjs index 51275b19..1b7cf430 100644 --- a/scripts/audit-non-godot-parity.mjs +++ b/scripts/audit-non-godot-parity.mjs @@ -2802,6 +2802,7 @@ function checkFrameworkDependencyHygiene() { ); } if ( + mauiBillingClientNuGetVersion && mauiBillingClientNuGetVersion !== googleBillingVersions[0] && !mauiBillingClientNuGetVersion.startsWith(`${googleBillingVersions[0]}.`) ) { From ebd901aa316f9efcc51a0142abc8609e238b9068 Mon Sep 17 00:00:00 2001 From: hyochan Date: Tue, 16 Jun 2026 00:39:49 +0900 Subject: [PATCH 3/5] ci(maui): pin setup-dotnet action Pin actions/setup-dotnet to the current v5 commit SHA in MAUI CI and release workflows. --- .github/workflows/ci-maui-iap.yml | 6 +++--- .github/workflows/release-maui.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-maui-iap.yml b/.github/workflows/ci-maui-iap.yml index f40006fb..3d5a2c11 100644 --- a/.github/workflows/ci-maui-iap.yml +++ b/.github/workflows/ci-maui-iap.yml @@ -53,7 +53,7 @@ jobs: fetch-depth: 1 - name: Setup .NET 10 SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 with: dotnet-version: "10.0.x" @@ -90,7 +90,7 @@ jobs: java-version: '17' - name: Setup .NET 10 SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 with: dotnet-version: "10.0.x" @@ -137,7 +137,7 @@ jobs: xcode-version: ${{ env.XCODE_VERSION }} - name: Setup .NET 10 SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 with: dotnet-version: "10.0.x" diff --git a/.github/workflows/release-maui.yml b/.github/workflows/release-maui.yml index 42388ab0..e01dbf93 100644 --- a/.github/workflows/release-maui.yml +++ b/.github/workflows/release-maui.yml @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@v6 - name: Setup .NET 10 SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 with: dotnet-version: "10.0.x" @@ -75,7 +75,7 @@ jobs: java-version: "17" - name: Setup .NET 10 SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 with: dotnet-version: "10.0.x" @@ -123,7 +123,7 @@ jobs: java-version: "17" - name: Setup .NET 10 SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 with: dotnet-version: "10.0.x" From 3a79c75126ba81a21f1765abc1bfcf138426cdc4 Mon Sep 17 00:00:00 2001 From: Hyo Date: Tue, 16 Jun 2026 01:12:13 +0900 Subject: [PATCH 4/5] fix(maui): add net10 example coverage Extend the MAUI sample app to build net10 Android, iOS, and Mac Catalyst targets alongside net9. Add a parity audit guard for the sample target matrix and clean up generated docs snippets flagged during review. --- .../OpenIap.Maui.Example/OpenIap.Maui.Example.csproj | 9 ++++++++- .../src/pages/docs/apis/get-active-subscriptions.tsx | 2 +- .../docs/src/pages/docs/apis/get-available-purchases.tsx | 2 +- packages/docs/src/pages/docs/apis/get-storefront.tsx | 2 +- .../src/pages/docs/apis/has-active-subscriptions.tsx | 2 +- .../docs/src/pages/docs/apis/ios/get-storefront-ios.tsx | 4 ++-- .../src/pages/docs/apis/ios/get-transaction-jws-ios.tsx | 2 +- ...is-eligible-for-external-purchase-custom-link-ios.tsx | 2 +- .../docs/apis/ios/is-eligible-for-intro-offer-ios.tsx | 2 +- .../pages/docs/apis/ios/is-transaction-verified-ios.tsx | 2 +- .../src/pages/docs/apis/ios/latest-transaction-ios.tsx | 2 +- .../src/pages/docs/apis/ios/subscription-status-ios.tsx | 2 +- scripts/audit-non-godot-parity.mjs | 6 ++++++ 13 files changed, 26 insertions(+), 13 deletions(-) diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/OpenIap.Maui.Example.csproj b/libraries/maui-iap/example/OpenIap.Maui.Example/OpenIap.Maui.Example.csproj index e26e73a5..e2c8b2c3 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/OpenIap.Maui.Example.csproj +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/OpenIap.Maui.Example.csproj @@ -1,7 +1,7 @@ - net9.0-android;net9.0-ios;net9.0-maccatalyst + net9.0-android;net10.0-android;net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst Exe OpenIap.Maui.Example true @@ -28,11 +28,18 @@ + + + + + + + diff --git a/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx index 1cc5a1bf..fdbb1ead 100644 --- a/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx +++ b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx @@ -207,7 +207,7 @@ function SubscriptionStatus() { {`using OpenIap; using OpenIap.Maui; -var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync()`} +var subscriptions = await ((QueryResolver)OpenIapClient.Instance).GetActiveSubscriptionsAsync();`} ), gdscript: ( {`var subscriptions = await iap.get_active_subscriptions()`} diff --git a/packages/docs/src/pages/docs/apis/get-available-purchases.tsx b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx index 415cf0f8..fd52fd7e 100644 --- a/packages/docs/src/pages/docs/apis/get-available-purchases.tsx +++ b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx @@ -192,7 +192,7 @@ function PendingPurchases() { {`using OpenIap; using OpenIap.Maui; -var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync()`} +var purchases = await ((QueryResolver)OpenIapClient.Instance).GetAvailablePurchasesAsync();`} ), gdscript: ( {`var purchases = await iap.get_available_purchases()`} diff --git a/packages/docs/src/pages/docs/apis/get-storefront.tsx b/packages/docs/src/pages/docs/apis/get-storefront.tsx index 25b24ccc..1c03c71f 100644 --- a/packages/docs/src/pages/docs/apis/get-storefront.tsx +++ b/packages/docs/src/pages/docs/apis/get-storefront.tsx @@ -124,7 +124,7 @@ function StorefrontBadge() { {`using OpenIap; using OpenIap.Maui; -var countryCode = await ((QueryResolver)OpenIapClient.Instance).GetStorefrontAsync()`} +var countryCode = await ((QueryResolver)OpenIapClient.Instance).GetStorefrontAsync();`} ), gdscript: ( {`var country_code = await iap.get_storefront()`} diff --git a/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx b/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx index 5020205a..cf786dc9 100644 --- a/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx +++ b/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx @@ -137,7 +137,7 @@ function PremiumGate({ children }: { children: React.ReactNode }) { {`using OpenIap; using OpenIap.Maui; -var isPremium = await ((QueryResolver)OpenIapClient.Instance).HasActiveSubscriptionsAsync()`} +var isPremium = await ((QueryResolver)OpenIapClient.Instance).HasActiveSubscriptionsAsync();`} ), gdscript: ( {`var is_premium = await iap.has_active_subscriptions()`} diff --git a/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx index 2936e88b..c92d40a8 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx @@ -110,8 +110,8 @@ if (Platform.isIOS) { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -// Deprecated — prefer await ((QueryResolver)OpenIapClient.Instance).GetStorefrontAsync() -var code = await ((QueryResolver)OpenIapClient.Instance).GetStorefrontIOSAsync()`} +// Deprecated — prefer await ((QueryResolver)OpenIapClient.Instance).GetStorefrontAsync(); +var code = await ((QueryResolver)OpenIapClient.Instance).GetStorefrontIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx index 860306e3..d5546104 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx @@ -108,7 +108,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var jws = await ((QueryResolver)OpenIapClient.Instance).GetTransactionJwsIOSAsync(sku: "com.app.premium")`} +var jws = await ((QueryResolver)OpenIapClient.Instance).GetTransactionJwsIOSAsync(sku: "com.app.premium");`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx index bf40f967..4b625007 100644 --- a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx @@ -106,7 +106,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var ok = await ((QueryResolver)OpenIapClient.Instance).IsEligibleForExternalPurchaseCustomLinkIOSAsync()`} +var ok = await ((QueryResolver)OpenIapClient.Instance).IsEligibleForExternalPurchaseCustomLinkIOSAsync();`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx index 6dab35ac..ceadb13a 100644 --- a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx @@ -113,7 +113,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var ok = await ((QueryResolver)OpenIapClient.Instance).IsEligibleForIntroOfferIOSAsync(groupId: "com.app.subgroup")`} +var ok = await ((QueryResolver)OpenIapClient.Instance).IsEligibleForIntroOfferIOSAsync(groupId: "com.app.subgroup");`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx index 41030961..66c91404 100644 --- a/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx @@ -109,7 +109,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var ok = await ((QueryResolver)OpenIapClient.Instance).IsTransactionVerifiedIOSAsync(sku: "com.app.premium")`} +var ok = await ((QueryResolver)OpenIapClient.Instance).IsTransactionVerifiedIOSAsync(sku: "com.app.premium");`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx index 845ab07a..87269a14 100644 --- a/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx @@ -111,7 +111,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var tx = await ((QueryResolver)OpenIapClient.Instance).LatestTransactionIOSAsync(sku: "com.app.premium")`} +var tx = await ((QueryResolver)OpenIapClient.Instance).LatestTransactionIOSAsync(sku: "com.app.premium");`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx b/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx index 763458df..2a16c4b5 100644 --- a/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx @@ -136,7 +136,7 @@ if (Platform.OS === 'ios') { using OpenIap.Maui; // kmp-iap (iOS targets only — no-op on Android) -var status = await ((QueryResolver)OpenIapClient.Instance).SubscriptionStatusIOSAsync(sku: "com.app.monthly")`} +var status = await ((QueryResolver)OpenIapClient.Instance).SubscriptionStatusIOSAsync(sku: "com.app.monthly");`} ), gdscript: ( {`if iap.get_platform() == "iOS": diff --git a/scripts/audit-non-godot-parity.mjs b/scripts/audit-non-godot-parity.mjs index 1b7cf430..9e4fe148 100644 --- a/scripts/audit-non-godot-parity.mjs +++ b/scripts/audit-non-godot-parity.mjs @@ -2909,6 +2909,12 @@ function checkFrameworkDependencyHygiene() { expectNotIncludes('libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj', [ ' Date: Tue, 16 Jun 2026 01:14:01 +0900 Subject: [PATCH 5/5] docs: avoid logging billing tokens Update the MAUI billing-programs C# example to avoid printing ExternalTransactionToken values in logs. --- packages/docs/src/pages/docs/types/billing-programs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx index 6b957e64..297c46ec 100644 --- a/packages/docs/src/pages/docs/types/billing-programs.tsx +++ b/packages/docs/src/pages/docs/types/billing-programs.tsx @@ -590,7 +590,7 @@ await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(new InitCon // Listen for developer billing selection. using var subscription = OpenIapClient.Instance.DeveloperProvidedBillingAndroid.Subscribe(details => { - Console.WriteLine($"Token: {details.ExternalTransactionToken}"); + Console.WriteLine("External transaction token received; send it to your backend without logging it."); // Report token to Google via your backend within 24 hours. });