Skip to content

feat: support common Apple and Android store providers - #504

Open
hyochan wants to merge 94 commits into
mainfrom
feat/google-pluggable-store-providers
Open

hyochan wants to merge 94 commits into
mainfrom
feat/google-pluggable-store-providers

Conversation

@hyochan

@hyochan hyochan commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Apple and Android stores now share one provider contract in the native packages and the six SDKs. An app links a compatible provider, selects it and rebuilds. Purchase APIs stay the same, and Play, Horizon, Amazon, StoreKit and every legacy flag keep working.

Identity. Client Protocol 0.2.0 adds a required storeId to every output that carries IapStore. The enum is frozen: a new store reports unknown plus its own id, and an adapter for an existing store keeps the canonical id.

Android. openiap-core holds the contract and the Play, Horizon and Amazon artifacts depend on it. A provider is found through the dev.hyo.openiap.PROVIDER manifest key and selected with openiapStore and openiapProvider (Expo, Godot and MAUI have equivalents). A flavored app can keep a provider flavor beside its store flavors.

Apple. OpenIapModule keeps its API and selectors. StoreKit sits behind a provider chosen from Info.plist, and a bad selection is a developer error, never a silent fallback.

New. Kotlin (openiap-conformance) and Swift (OpenIapConformance) conformance suites, an optional store registry, a provider guide, and a community Amazon example. The example is educational; FireOS apps should use the official integration.

Breaking changes. The release card lists 14, each with the action to take, and the upgrade guide has the details. The main ones: storeId is required on hand-built purchases and IAPKit results, KMP's Store gains UNKNOWN, Kotlin purchase constructors and copy change, Apple needs tvOS 16, and error codes, Horizon restore events and iOS billing-grace checks change in some SDKs.

Release train (card included, written as published): Apple and Google 4.0.0, React Native 17.0.0, Expo 6.0.0, Flutter 11.0.0, Godot 4.0.0, KMP 4.0.0, MAUI 3.0.0, Client Protocol 0.2.0, CLI 0.2.0. Client Protocol 0.2.0 is already set in specs/client/package.json because the provider compatibility checks read it, so release it with version=current; minor would publish 0.3.0.

Verified. CI passed at c751be8b (76 checks). On real devices at c751be8b, the native example and all six SDKs completed a sandbox purchase, local IAPKit verification and finish on Google Play (Pixel) and Amazon (Fire tablet, App Tester). On Horizon (Quest 3) all seven connect, load products and read owned purchases; Flutter and Godot restore behave as documented. Horizon checkout costs real money, so no Horizon purchase was made, and Vega only builds and launches. On iPhone at c751be8b, swift test passes (180 tests), every example connects and loads products, and the Flutter plugin compiles against this PR's Apple package. New iOS sandbox purchases were last run at 63b54bc0 and are pending. The community example passes its 16 conformance behaviors and Fire-tablet App Tester flows at 412b86e3.

Also in this branch. Security work that is not provider work: braces stays on upstream with an expiring OSV exception (no upstream fix for GHSA-vfj7-8cjw-p6xm), the node-forge and image-size exceptions run to 2026-10-30, eight advisories published after the last green run are fixed in the locks (security notes), and the Flutter SwiftPM floor is openiap-apple 4.0.0. The Codecov step and coverage gate are rewritten. Root package.json and bun.lock changed, so merging deploys Kit to production.

Known limits. Native conformance does not certify server lifecycle behavior. KMP's neutral variant compiles against Play Billing only, which external-provider runtimes exclude. Invalid Godot settings fail dependency resolution because its export hook cannot cancel the export. A module without platform flavors still pins one store per build. The KMP example keeps one client per screen and crashes on Horizon when Available Purchases opens after the purchase flow screen; that code is unchanged by this PR.

Preview

common-provider-expo-public.mp4
provider-architecture-amazon-example.mp4

Earlier Fire label preview. Sampled Fire home and package usage guide.

Sampled browser previews: provider setup, nullable renewal, SDK configuration, verification errors, Apple lifecycle/authoring and the existing Expo app installation recipe.

provider-generic-vendors-docs-preview.mp4
provider-errors-and-setup.mp4
apple-provider-review.mp4
expo-community-guide-final.mp4

@hyochan hyochan added 🎯 feature New feature 💨 ci Cloud integration 📖 documentation Improvements or additions to documentation 📱 iOS Related to iOS 🤖 android Related to android cross-platform Cross-platform (both Android & iOS) ⬡ protocol ⚡️ breaking react-native-iap react-native-iap library expo-iap expo-iap library godot-iap godot-iap library kmp-iap kmp-iap library flutter-iap maui-iap .NET MAUI SDK labels Oct 1, 2026
@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.47%. Comparing base (64158e8) to head (c751be8).

Files with missing lines Patch % Lines
...ries/react-native-iap/src/utils/native-instance.ts 83.87% 5 Missing ⚠️
libraries/expo-iap/src/utils/restorePurchases.ts 75.00% 2 Missing ⚠️
libraries/react-native-iap/src/index.ts 98.41% 1 Missing ⚠️
.../react-native-iap/src/utils/available-purchases.ts 85.71% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #504      +/-   ##
==========================================
+ Coverage   77.35%   77.47%   +0.12%     
==========================================
  Files         160      162       +2     
  Lines       16836    16891      +55     
  Branches     4845     4860      +15     
==========================================
+ Hits        13023    13086      +63     
+ Misses       3813     3805       -8     
Flag Coverage Δ
expo-iap 91.11% <93.10%> (-0.07%) ⬇️
flutter-inapp-purchase 90.89% <100.00%> (+0.39%) ⬆️
iapkit 69.12% <ø> (ø)
react-native-iap 93.49% <94.21%> (+0.23%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
React Native IAP 93.49% <94.21%> (+0.23%) ⬆️
Expo IAP 91.11% <93.10%> (-0.07%) ⬇️
flutter_inapp_purchase 90.89% <100.00%> (+0.39%) ⬆️
IAPKit Server 92.13% <ø> (ø)
IAPKit Convex 63.59% <ø> (ø)
Files with missing lines Coverage Δ
libraries/expo-iap/src/index.ts 92.44% <100.00%> (ø)
libraries/expo-iap/src/modules/ios.ts 100.00% <100.00%> (ø)
libraries/expo-iap/src/utils/availablePurchases.ts 88.63% <100.00%> (+1.13%) ⬆️
libraries/expo-iap/src/vega-adapter.ts 87.74% <ø> (ø)
...ter_inapp_purchase/lib/flutter_inapp_purchase.dart 90.20% <100.00%> (+0.60%) ⬆️
libraries/flutter_inapp_purchase/lib/helpers.dart 91.60% <100.00%> (+0.08%) ⬆️
libraries/react-native-iap/src/hooks/useIAP.ts 96.01% <100.00%> (+2.15%) ⬆️
...es/react-native-iap/src/utils/restore-purchases.ts 100.00% <100.00%> (ø)
...ibraries/react-native-iap/src/utils/type-bridge.ts 98.33% <100.00%> (+0.01%) ⬆️
libraries/react-native-iap/src/vega-adapter.ts 87.22% <100.00%> (+0.01%) ⬆️
... and 4 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

hyochan and others added 28 commits October 6, 2026 01:35
…dge cases

Only JSX text decodes entities, so literals are scanned as written. The clean
string-literal fixture never compared a pair, so it is replaced, and fixtures
now pin the nearest-text rule inside elements and the skip over comments and
empty literals.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The listener teardown test reads ExpoIapModule.kt for the cleanup call, and
the endConnection fix changed that line to guard on the stored handles.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The JS source-text check can no longer tell endConnection from OnDestroy,
and the no-provider test runs without handles, so nothing covered the
listener removal in endConnection. The new Robolectric test runs it with a
provider and stored handles and checks each listener is removed once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixtures pin that a literal inside an element keeps its entity as written,
that the first text is found through nested elements, and that the scan
recurses in the right direction.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…hase once

A purchase built in code writes a blank storeId, so finishing it failed with
"Invalid store identity" and the purchase was never acknowledged. The native
finish paths now stamp the connected provider's identity when the input has
none, and keep an explicit identity untouched.

On Horizon the provider's restore already notifies the purchase listeners, and
Godot's own loop signalled every owned purchase again. Listener emissions are
now dropped while the provider restore runs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lists

React Native iOS sent the purchase JSON for every finish, so a purchase that
carries only an id no longer finished by lookup, and a malformed transaction
id read as success. It now sends the JSON only when the purchase carries a
store identity and rejects a malformed id again. An init failure that is a
canonical error keeps its code instead of becoming init-connection.

KMP and MAUI on iOS now treat an empty subscription id list as no filter,
as they did before the native calls took the list.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…flavors

With a provider platform flavor and openiapStore/openiapProvider set,
assemblePlayDebug failed with a conflict against the play flavor. The
properties now select the provider flavor only, through the same provider
validation as the plain path. The version-less error also names the artifact
the build really links.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…se MAUI ids

The Expo plugin gave one message for reserved ids, malformed ids and missing
coordinates, and the fix it named could not work for the first two. Each case
now has its own message. MAUI lowercases a community id before matching, as
Gradle, Expo, Godot and the doctor do.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ol in version errors

A cancelled requestPurchase (a SwiftUI task ending) was wrapped and delivered
as a purchase-error event, and CancellationError never reached the caller. It
is now rethrown as is, as are the other operations with the same catch shape.

A Client Protocol mismatch reused the core-version messages. Kotlin and Swift
now say "Client Protocol" with both versions and the fix. The unused public
ProviderBehaviors.officialCapabilities is no longer generated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The release gates did not list openiap-core, the Kotlin conformance artifact
or the Swift OpenIapConformance sources, so a change to only those needed no
release card. The parity audit still looked for a removed conformance
directory and passed with conformance on the published classpath. Both now
check the current layout.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The React Native, Expo and Apple conformance tests checked only that the store
is not unknown. They now check storeId, including a community provider. New
registry tests fail when the pass rule, the scope check, the 90-day window or
the tier rule is weakened.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…cription changes

React Native, Expo and Flutter now run the provider's Android restore, which
on Horizon delivers each owned purchase to the listeners. Expo and Flutter pass
the provider's error code through. KMP's Store enum gains UNKNOWN, and Flutter,
KMP and MAUI on iOS check StoreKit's active flag for id-filtered calls. The AI
reference no longer says the Expo flags are removed in 6.0.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Follows the architecture note that says which suites assert the provider
profile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nce README version

The card now covers React Native in the error-code and iOS subscription notes,
names each SDK's own restore surface, and discloses that a cancelled Apple
task throws CancellationError without an error event. The restore page names
Amazon's PurchaseUpdates and Godot's per-store signal, and the isActive note
matches the code. The conformance README coordinate is pinned to the suite
version, and the registry notes say to keep the previous suite major's profile
through the maintenance window.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The restore page said every SDK runs the provider's restore first, which is
not true for KMP. It now names React Native, Expo, Flutter and MAUI. The card
uses the real listener names, mentions Godot's restore, and limits the
cancellation note to requestPurchase and the operations that rethrow it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On main the GDScript restore never reached the plugin's native restore on
Android, so restoring signalled nothing. Wiring it to the provider's restore
also reached a loop that signalled every owned purchase on every store. The
native restore now runs the provider's restore with listener emissions
dropped, queries the owned purchases, and emits no signal. AARs rebuilt and
digests updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Godot's restore signals nothing and its Android failures carry the provider's
code. The restore page names KMP's provider builds, the card splits its long
bullets and calls the iOS flag OpenIAP's own, and the isActive intro matches
the code. The registry note says a major's profile stays while any entry cites
it, and the Expo restore comment describes the provider restore.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…limit

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he result API

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sions only on Horizon

Restoring before connecting reported service-error where main reported
not-prepared. The restore gate closed on every store, but only the Horizon
restore notifies purchase listeners, so on Play and Amazon it could drop a
genuine purchase update that arrived during the restore. It now closes only
for Horizon. AARs rebuilt and digests updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
braces has no patched release (GHSA-vfj7-8cjw-p6xm). The six dependency
graphs return to upstream braces 3.0.3 instead of a single-maintainer fork,
and each lock gets a scoped, justified exception that expires on 2026-10-30.
The node-forge exceptions that would have expired on 2026-10-09 are renewed
to the same date.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Eight advisories (compression, joi, source-map-js, @fastify/busboy,
proxy-addr, smol-toml, graphql-tools executor-legacy-ws and utils 12) are
fixed by in-place lock edits. sprintf-js has no fix and the graphql-codegen
chain still pins utils 11, so those two get expiring exceptions. The
image-size reasons now name the real blocker (Metro pins ^1.0.2), and every
temporary exception is rechecked on 2026-10-30. The expiry tests pin the
earliest date, and the README no longer copies the exception list.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The plugin calls OpenIapModule.storeId and StoreIds, which first ship in
openiap-apple 4.0.0. A floor of 3.6.1 would resolve a 3.x release that does
not compile. The parity audit pins the new floor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The release card gets a Breaking changes list that names the affected
packages and the action to take, and the migration guide gets a Provider
contract upgrade section with the details. The API, error and KMP pages
agree with them. The release-note audit pins each item, and the process
docs now say that a feature PR may set the Client Protocol version, run
scripts/sync-release-generated.sh, and release it with version=current.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The React Native lock reaches image-size only through Metro 0.84.4, and Metro
0.84.5 drops it, so a Metro bump clears both advisories. The Expo example
reaches it only through a nested Metro 0.76.9 under stale peer resolutions.
The policy wording now covers an advisory the lock does not clear yet. Two
audit tests no longer crash without the root exception file or match "11
dependency audit findings" for "1".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The cancellation, Godot error-code and Android error-code notes now name the
exact operations and packages that changed. Flutter's subscription-check
message change is documented. A feature PR that sets the Client Protocol
version also refreshes bun.lock before running the sync script. The release
note audit masks version numbers, so a post-release version correction does
not fail CI.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The React Native lock reaches image-size through Metro 0.84.4, pinned by
@expo/metro 56.0.0 and resolved for the community CLI plugin. Re-resolving
Metro inside the locked range clears both advisories. The policy now says an
exception may also cover a clearing bump that has not been tested yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Godot Apple error-code note now names what changed (get_storefront,
products_fetched failure payloads and the iOS-only calls) and what they
carried before. Flutter's hasActiveSubscriptions no longer checks init in
Dart, which the guide now says. AGENTS.md and the sync-script comment match
the lockfile refresh step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread scripts/audit-release-notes.test.mjs Fixed
CodeQL flagged the single-pass tag regex as incomplete multi-character
sanitization. A depth counter drops nested or partial tags instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 android Related to android ⚡️ breaking 💨 ci Cloud integration cross-platform Cross-platform (both Android & iOS) 📖 documentation Improvements or additions to documentation expo-iap expo-iap library 🎯 feature New feature flutter-iap godot-iap godot-iap library 📱 iOS Related to iOS kmp-iap kmp-iap library maui-iap .NET MAUI SDK ⬡ protocol react-native-iap react-native-iap library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants