Skip to content

feat: add cross-platform subscriptionBillingIssue event#99

Merged
hyochan merged 21 commits into
mainfrom
feat/gql-apple-google-subscription-billing-issue
Apr 16, 2026
Merged

feat: add cross-platform subscriptionBillingIssue event#99
hyochan merged 21 commits into
mainfrom
feat/gql-apple-google-subscription-billing-issue

Conversation

@hyochan
Copy link
Copy Markdown
Member

@hyochan hyochan commented Apr 15, 2026

Summary

Unifies StoreKit 2's Message.Reason.billingIssue (iOS 18+ / Mac Catalyst 18+) and Google Play Billing's Purchase.isSuspended (Play Billing 8.1+) into a single cross-platform event — subscriptionBillingIssue. Apps now get one listener that fires on either platform when a user's active subscription needs attention due to a payment problem.

Every downstream library, both example apps, the react-native-iap useIAP hook, a feature docs page, and a Horizon no-op unit test land in this PR.

What's in this PR

Schema (packages/gql)

  • New IapEvent.SubscriptionBillingIssue enum value in type.graphql.
  • New subscriptionBillingIssue: Purchase! field on Subscription in event.graphql.
  • All generated types regenerated: Types.swift, Types.kt, types.ts, types.dart, types.gd, and all 5 downstream library type mirrors.

iOS (packages/apple)

  • subscriptionBillingIssueListener added to OpenIapModuleProtocol, OpenIapModule, OpenIapModule+ObjC, and IapState.
  • startMessageListener() spins up a StoreKit.Message.messages loop on init. Gated to iOS 18+ / Mac Catalyst 18+ (where Message.Reason.billingIssue exists). macOS / tvOS / watchOS / visionOS are silent no-ops (Message API not available on those platforms).
  • On .billingIssue, batches a single StoreKit.Product.products(for:) call for all current auto-renewable entitlements, then iterates each subscription's full status array, emitting one event per subscription whose renewal state is .inBillingRetryPeriod or .inGracePeriod.

Android Play flavor (packages/google/.../play)

  • addSubscriptionBillingIssueListener / removeSubscriptionBillingIssueListener implemented with thread-safe CopyOnWriteArraySet + ConcurrentHashMap.newKeySet() so add/remove on the main thread is safe against iteration from Dispatchers.IO.
  • Emission hook runs both in getAvailablePurchases (always queries includeSuspended=true so suspended purchases reach the notifier even when the caller opted not to include them in the returned list) and in onPurchasesUpdated (push path, parity with iOS).
  • Deduplicated per session by purchaseToken.

Android Horizon flavor (packages/google/.../horizon) — explicit no-op + automated guarantee

  • Both add/remove are implemented as no-ops with Log.w warnings. Horizon Billing Compatibility SDK targets Play Billing 7.0 which does not expose isSuspended. The listener interface exists for API parity but never fires on Horizon.
  • SubscriptionBillingIssueHorizonNoOpTest (Robolectric) asserts the callback is never invoked, guarding against accidental Play-flavor logic leaking into Horizon. Runs on CI via :openiap:testHorizonDebugUnitTest.

Downstream library bridges

Library What landed
react-native-iap Nitro spec (add/removeSubscriptionBillingIssueListener), regenerated nitrogen bridge, Swift + Kotlin implementations, public subscriptionBillingIssueListener() JS API, useIAP({ onSubscriptionBillingIssue }) hook callback
expo-iap OpenIapEvent.SubscriptionBillingIssue enum, EventPayloads mapping, subscriptionBillingIssueListener() JS API, Kotlin event emission via ExpoIapHelper.setupListeners, Swift event emission via ExpoIapHelper.setupListeners
flutter_inapp_purchase Dart subscriptionBillingIssueListener Stream<Purchase> on FlutterInappPurchase, Android method-channel forwarding, iOS method-channel forwarding
godot-iap GDScript subscription_billing_issue(purchase) signal, Android AAR SignalInfo + listener registration, iOS GDExtension @Signal + listener registration
kmp-iap subscriptionBillingIssueListener: Flow<Purchase> in commonMain, Android backing MutableSharedFlow + emission inside getAvailablePurchasesHandler, iOS backing flow + cinterop subscription via openIapModule.addSubscriptionBillingIssueListener

Example apps (manual-test ready)

  • packages/apple/ExampleSubscriptionFlowScreen renders an orange "Subscription needs attention" banner with a Fix payment method button that calls OpenIapModule.shared.deepLinkToSubscriptions(nil). Listener registered in setupIapProvider, torn down on screen dispose.
  • packages/google/Example — Compose LazyColumn gains an analogous banner registered via DisposableEffect(iapStore), dispatching iapStore.deepLinkToSubscriptions with the failing purchase's SKU + package name. OpenIapStore now exposes add/removeSubscriptionBillingIssueListener pass-through so Compose callers don't have to reach the underlying OpenIapProtocol.

Documentation

  • Feature page: packages/docs/src/pages/docs/features/subscription-billing-issue.tsx — new /docs/features/subscription-billing-issue route + sidebar entry. Platform-behavior table with min versions, recommended UX, per-language usage snippets (RN/expo, Flutter, Godot, KMP), deduping semantics.
  • Release notes: packages/docs/src/pages/docs/updates/releases.tsx entry for 2026-04-16.
  • Sandbox E2E guide: knowledge/internal/sandbox-subscription-billing-issue.md — step-by-step for iOS 18 sandbox (billing issue toggle / remove payment method) and Play 8.1+ sandbox (remove payment method / Play Console test suspensions). Includes a per-library smoke matrix using libraries-versions.jsonc "local" mode.
  • External API docs drift fixes: knowledge/external/storekit2-api.md (Message API, eligibleWinBackOfferIDs, Transaction iOS 18.4 fields, iOS 17.4 correction), google-billing-api.md (External Payments 8.3+), horizon-api.md (accessToken field + hyphenation).
  • llms: knowledge/_claude-context/context.md + packages/docs/public/llms{,-full}.txt recompiled.

Verification (every target compiled locally before push)

Target Command Result
apple package swift build + swift test (87 tests) OK
google Play gradlew :openiap:compilePlayDebugKotlin OK
google Horizon gradlew :openiap:compileHorizonDebugKotlin OK
google Horizon test gradlew :openiap:testHorizonDebugUnitTest OK (Robolectric no-op assertion passes)
google Example gradlew :example:compilePlayDebugKotlin OK
react-native-iap yarn specs + yarn tsc --noEmit -p tsconfig.build.json OK
expo-iap bun run tsc --noEmit OK
flutter_inapp_purchase flutter analyze OK
kmp-iap gradlew :library:compileCommonMainKotlinMetadata compileDebugKotlinAndroid OK
docs tsc --noEmit + prettier --check OK

CI is green across all workflows.

Release plan

All five downstream libraries pick this up as a minor version bump (new feature, additive, non-breaking). Version bumps + releases happen per-library after merge through their usual release workflows.

Test plan

  • Regenerate types in all 5 downstream libraries and confirm compile still green (rn-iap + expo-iap typecheck, flutter analyze, kmp-iap compile, apple swift test — all green locally and on CI).
  • Horizon flavor exposes the listener as an explicit no-op: automated by SubscriptionBillingIssueHorizonNoOpTest (Robolectric). CI runs :openiap:testHorizonDebugUnitTest.
  • Horizon + Play flavors both compile with the new public API.
  • deepLinkToSubscriptions flow resolves on both platforms (existing API, unchanged; referenced from the new feature page and wired in both example app banners).
  • Live iOS 18 sandbox billing-issue message — manual run by release QA. Procedure documented in knowledge/internal/sandbox-subscription-billing-issue.md.
  • Live Play 8.1+ sandbox suspended subscription — manual run by release QA. Same document, Android section.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Cross-platform "subscription billing issue" event: notifies when active subscriptions need user attention (iOS 18+, Play Billing 8.1+). Exposed across Expo, React Native, Flutter, Godot, KMP and examples include a user-facing "Subscription needs attention" banner and listener APIs.
  • Documentation

    • New feature guide, API listener usage, recommended UX (deep-linking to subscription management), sandbox QA workflow, and release-note entry.

Unify StoreKit 2 Message.billingIssue (iOS 18+) and Play Billing isSuspended
(8.1+) into a single SubscriptionBillingIssue event + Purchase payload.

- packages/gql: new IapEvent.SubscriptionBillingIssue + subscriptionBillingIssue
  Subscription field; types regenerated for swift/kotlin/ts/dart/gdscript.
- packages/apple: Message.messages listener (iOS 18+, iOS-only, silent no-op
  elsewhere); resolves affected subscriptions via currentEntitlements +
  RenewalState.inBillingRetryPeriod / inGracePeriod.
- packages/google (Play): isSuspended detection in getAvailablePurchases,
  deduped by purchaseToken per session.
- packages/google (Horizon): explicit no-op with warning log; Horizon Billing
  Compatibility SDK targets Play Billing 7.0 which lacks isSuspended.
- knowledge/external: storekit2-api.md Message / eligibleWinBackOfferIDs /
  Transaction iOS 18.4 / consumable history sections added; iOS 17.4 / 18.2
  corrections. google-billing-api.md External Payments (8.3+) section.
  horizon-api.md accessToken field correction.
- packages/docs: release notes entry.

Downstream library native bridges (react-native-iap, expo-iap,
flutter_inapp_purchase, godot-iap, kmp-iap) receive synced types; bridge
wiring to expose the new listener will land per-library.

Verified: swift build, gradlew compile{Play,Horizon}DebugKotlin,
tsc rn-iap, tsc expo-iap, flutter analyze, gradlew kmp-iap common compile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

Warning

Rate limit exceeded

@hyochan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 25 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1c3cc03-fb60-4423-a653-00fe2cb99fa5

📥 Commits

Reviewing files that changed from the base of the PR and between 5e5bfdf and e0d6335.

📒 Files selected for processing (3)
  • knowledge/internal/sandbox-subscription-billing-issue.md
  • packages/apple/Sources/OpenIapModule.swift
  • packages/docs/src/pages/docs/features/subscription-billing-issue.tsx
📝 Walkthrough

Walkthrough

Adds a cross-platform "subscription billing issue" event and listener surface across OpenIAP, wiring StoreKit 2 message-loop (iOS) and Play Billing suspended-subscription detection (Android), Horizon no-op behavior, GraphQL/type updates, SDK bindings for multiple frameworks, docs, examples, and tests.

Changes

Cohort / File(s) Summary
GraphQL & types
packages/gql/src/type.graphql, packages/gql/src/event.graphql
Added IapEvent.SubscriptionBillingIssue enum and subscriptionBillingIssue: Purchase! subscription field.
Apple core & ObjC bridge
packages/apple/Sources/Models/Types.swift, packages/apple/Sources/Helpers/IapState.swift, packages/apple/Sources/OpenIapProtocol.swift, packages/apple/Sources/OpenIapModule.swift, packages/apple/Sources/OpenIapModule+ObjC.swift
Added event case, listener typealias/API, actor listener storage/snapshot, ObjC bridge, StoreKit Message listener/dispatcher, emitter and registration API.
Google/OpenIap core & listener types
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt, .../OpenIapProtocol.kt, .../listener/OpenIapListener.kt, .../store/OpenIapStore.kt, .../helpers/CommonHelpers.kt
Extended IapEvent and SubscriptionResolver contracts, added handler typealias, listener interface, store passthroughs, and a suspend helper to await billing-issue callbacks.
Android Play implementation
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt, libraries/kmp-iap/.../InAppPurchaseAndroid.kt
Detects suspended subscriptions, per-token dedupe, notifies listeners on restore/purchase updates, exposes add/remove listener APIs, clears dedupe on endConnection, and updates getAvailablePurchases behavior to optionally include suspended purchases.
Android Horizon flavor
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt, knowledge/external/horizon-api.md
Implemented explicit no-op listener overrides that log warnings; docs updated to use single accessToken format.
SDK bindings — React Native / Expo / Nitro
libraries/react-native-iap/src/..., libraries/expo-iap/*, libraries/react-native-iap/src/specs/RnIap.nitro.ts
Added event enum, exported JS listener helpers, native attach/reset logic, hook support, Nitro interface methods, and example wiring; includes tests and example changes.
SDK bindings — Flutter / KMP / Godot
libraries/flutter_inapp_purchase/*, libraries/kmp-iap/*, libraries/godot-iap/*
Added enum/case, stream/flow listeners, subscription resolver APIs, native listener wiring and signal emission across platforms; updated public handlers and tests.
Framework plumbing
libraries/expo-iap/android/.../ExpoIapHelper.kt, ExpoIapModule.kt, libraries/expo-iap/ios/*, libraries/flutter_inapp_purchase/..., libraries/godot-iap/...
Wired native listeners to emit subscription-billing-issue events into Expo, React Native, Flutter, Godot runtimes.
Docs, release notes & knowledge
packages/docs/src/pages/docs/features/subscription-billing-issue.tsx, packages/docs/src/pages/docs/updates/releases.tsx, knowledge/*, packages/docs/public/llms-full.txt
Added feature doc, release note entry, sandbox QA procedure, expanded StoreKit/Play/Horizon docs, monorepo/gql generation docs, and updated generated docs/timestamps.
Examples & tests
packages/apple/Example/.../SubscriptionFlowScreen.swift, packages/google/Example/.../SubscriptionFlowScreen.kt, libraries/.../test/*, packages/*/test/*, libraries/react-native-iap/example/ios/...
UI banners/examples, numerous unit/integration tests verifying wiring, dedupe reset, Horizon no-op behavior, and added example test target and XCTest cases.
Build / tooling
packages/google/openiap/build.gradle.kts, libraries/react-native-iap/example/ios/Podfile, add_test_target.rb
Gradle test source/test options for Horizon, Podfile test target & fmt patch, and Xcode project test-target automation script.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,230,255,0.5)
    participant App as Client App
    end
    rect rgba(200,255,200,0.5)
    participant SDK as Framework SDK (RN/Expo/Flutter/Godot/KMP)
    end
    rect rgba(255,230,200,0.5)
    participant OpenIap as OpenIapModule
    end
    rect rgba(255,200,200,0.5)
    participant Store as Platform Store (StoreKit / Play Billing)
    end

    App->>SDK: register subscriptionBillingIssue listener
    SDK->>OpenIap: addSubscriptionBillingIssueListener(...)
    Note right of OpenIap: iOS: start StoreKit Message loop (iOS18+)\nAndroid Play: inspect restored purchases for isSuspended
    Store-->>OpenIap: billing-issue message / suspended purchase
    OpenIap->>SDK: emit subscriptionBillingIssue payload (deduped)
    SDK->>App: deliver Purchase payload to registered callbacks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🤖 android, 📱 iOS, ❄️ types, flutter_inapp_purchase

Poem

🐰 I heard a tiny chime and leapt,

A subscription wobbled, nearly wept.
I hopped through StoreKit, Play, and code,
Wired listeners, lights aglowed.
Now users glide back on their way—hip hop!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.01% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a cross-platform subscriptionBillingIssue event. It is concise, specific, and clearly conveys the primary feature being introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gql-apple-google-subscription-billing-issue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hyochan hyochan added 🎯 feature New feature 📖 documentation Improvements or additions to documentation labels Apr 15, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a cross-platform subscriptionBillingIssue event, unifying StoreKit 2's billing issue messages and Google Play's suspension signals across the monorepo. The implementation includes updates to the GraphQL schema, native modules, framework libraries, and documentation. Feedback highlights the need for thread-safe collections in the Android module, broader platform availability checks for the StoreKit Message API, performance optimizations for product fetching on iOS, and improved event parity on Android by checking suspension states during purchase updates.

Comment thread packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt Outdated
Comment thread packages/apple/Sources/OpenIapModule.swift Outdated
Comment thread packages/apple/Sources/OpenIapModule.swift
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libraries/flutter_inapp_purchase/lib/types.dart (1)

5110-5128: ⚠️ Potential issue | 🔴 Critical

Add runtime wiring for the new subscriptionBillingIssue handler.

The types.dart file declares the subscriptionBillingIssue field and handler typedef, but the runtime dispatch in flutter_inapp_purchase.dart is incomplete. The handler is not being instantiated in the SubscriptionHandlers constructor (line 2272). To match the pattern used for other event handlers (userChoiceBillingAndroid, developerProvidedBillingAndroid), you need to:

  1. Add a StreamController<Purchase> _subscriptionBillingIssueListener field to the class
  2. Expose it as a getter property
  3. Pass subscriptionBillingIssue: () async => await _subscriptionBillingIssueListener.stream.first, in the SubscriptionHandlers constructor instantiation
  4. Wire the listener to receive and dispatch events from the platform channel when the subscription-billing-issue event is received
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/flutter_inapp_purchase/lib/types.dart` around lines 5110 - 5128,
The runtime dispatch is missing wiring for the new subscriptionBillingIssue
handler declared in types.dart: add a StreamController<Purchase> field (e.g.
_subscriptionBillingIssueListener) and a public getter, instantiate
SubscriptionHandlers with subscriptionBillingIssue: () async => await
_subscriptionBillingIssueListener.stream.first (matching the pattern used for
userChoiceBillingAndroid/developerProvidedBillingAndroid), and wire the platform
event "subscription-billing-issue" to add incoming Purchase objects into
_subscriptionBillingIssueListener so the handler receives events; update
flutter_inapp_purchase.dart to create, expose, and dispose the controller
alongside the other event controllers.
packages/apple/Sources/OpenIapModule.swift (1)

18-34: ⚠️ Potential issue | 🟡 Minor

Cancel the StoreKit message task during normal connection teardown.

endConnection() goes through cleanupExistingState(), but that path still only stops updateListenerTask. Since this module is a singleton, relying on deinit means messageListenerTask can stay parked on StoreKit.Message.messages until the next init or process exit.

♻️ Proposed fix
 private func cleanupExistingState() async {
     updateListenerTask?.cancel()
     updateListenerTask = nil
+    messageListenerTask?.cancel()
+    messageListenerTask = nil
     await state.reset()

Also applies to: 69-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/apple/Sources/OpenIapModule.swift` around lines 18 - 34,
cleanupExistingState()/endConnection() currently only cancels
updateListenerTask, leaving messageListenerTask (Task listening on
StoreKit.Message.messages) running; update cleanupExistingState() (the
endConnection teardown path) to also cancel and nil out messageListenerTask (and
mirror what deinit does) so the StoreKit message listener is stopped during
normal connection teardown — reference messageListenerTask,
cleanupExistingState(), endConnection(), and updateListenerTask to locate the
change.
🧹 Nitpick comments (3)
packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt (1)

57-58: Tighten the wording of the event semantics comment.

The phrase “previously-healthy subscription” is stronger than the current implementation behavior (first seen suspended token in session). Consider rewording to avoid implying health-transition tracking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt`
around lines 57 - 58, The KDoc for the event in OpenIapListener.kt currently
says "previously-healthy subscription" which overstates behavior; update the
comment on the event (near class/function OpenIapListener) to state that the
event fires once per session when a subscription token is first observed in a
suspended state (e.g., payment failed, card expired) rather than implying the
system tracks a prior healthy state or health transition.
knowledge/external/google-billing-api.md (1)

378-429: Add direct reference links for the new 8.3 API names.

This new section introduces several new symbols (enableDeveloperBillingOption, DeveloperBillingOptionParams, DeveloperProvidedBillingListener, BillingOption.EXTERNAL_PAYMENTS); adding official links (as done in earlier sections) would reduce drift risk over time.

Based on learnings: "Update documentation for API changes and reference OpenIAP specification for new features."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@knowledge/external/google-billing-api.md` around lines 378 - 429, Add
official reference links for the new Billing Library 8.3 APIs introduced in this
section: link enableDeveloperBillingOption (BillingClient.Builder),
DeveloperBillingOptionParams, DeveloperProvidedBillingListener,
DeveloperProvidedBillingDetails, and BillingOption.EXTERNAL_PAYMENTS to their
respective Android developer docs (or OpenIAP equivalents used elsewhere in the
doc). Update the examples' inline type mentions to be linked the same way as
earlier sections (consistent anchor style), and add a short parenthetical
OpenIAP mapping for the AlternativeBilling* surface to match the other
API-reference entries.
knowledge/_claude-context/context.md (1)

1008-1131: LGTM! Clear IR-based code generation documentation.

The architecture description accurately documents the parser → IR → plugin flow and provides helpful reference tables for IR types and plugin features. The schema marker documentation (# => Union, # Future) is particularly useful.

Optional: Fix markdown formatting

Static analysis flagged missing blank lines around the code fence at line 1124 and missing language specification at line 1235. While acceptable for internal docs, adding these would improve markdown compliance:

 bun run generate
+
 # Generate specific platform
 bun run generate:swift
-```
+```text
 GraphQL Schema (src/*.graphql)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@knowledge/_claude-context/context.md` around lines 1008 - 1131, Add proper
blank lines around fenced code blocks and include language identifiers on fences
lacking them: locate the code fence that contains "GraphQL Schema
(src/*.graphql)" and ensure there is an empty line before and after the ```text
fence, and add a language spec (e.g., ```text or ```bash) to other fences
missing a language label elsewhere in context.md; update the surrounding
markdown so each triple-backtick block has a blank line separation and an
explicit language tag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libraries/react-native-iap/src/types.ts`:
- Around line 1563-1576: Add full listener plumbing for the new
subscriptionBillingIssue event: declare a subscriptionBillingIssueJsListeners
state array, implement a subscriptionBillingIssueListener registration function
that pushes/removes callbacks mirroring the pattern in
userChoiceBillingListenerAndroid and developerProvidedBillingListenerAndroid,
wire a native-to-JS handler that fans out incoming native messages to all
functions in subscriptionBillingIssueJsListeners (ensure event shape matches
SubscriptionArgsMap.subscriptionBillingIssue / Purchase), attach that native
handler where other Android/iOS native listeners are attached in index.ts, and
add cleanup/reset logic in resetListenerState() to clear
subscriptionBillingIssueJsListeners; follow the exact naming and behavior used
by the referenced listeners for consistency.

In `@packages/apple/Sources/OpenIapModule.swift`:
- Around line 1557-1571: The code only checks statusArray.first for billing
issues which can miss other statuses; iterate the full statusArray from
subscription.status and treat the subscription as having a billing issue if any
status in the array has state .inBillingRetryPeriod or .inGracePeriod, i.e.
replace the guard-let latest = statusArray.first / switch latest.state logic
with a loop over statusArray checking each element's state and when a matching
state is found, build the purchase via StoreKitTypesBridge.purchase(from:
transaction, jwsRepresentation: verification.jwsRepresentation), call
emitSubscriptionBillingIssue(purchase), and set emitted = true; keep references
to transaction, verification.jwsRepresentation, emitSubscriptionBillingIssue,
and emitted to ensure behavior remains the same.

In `@packages/docs/public/llms-full.txt`:
- Line 912: Change the wording for the compound modifier in the documentation
entry for enableAutoServiceReconnection() so "Auto reconnect feature (8.0+)" is
hyphenated as "Auto-reconnect feature (8.0+)" to fix the grammar; locate the
string that documents enableAutoServiceReconnection() in
packages/docs/public/llms-full.txt and update the text accordingly.
- Line 883: Change the phrase "Google Play Billing Library compatible wrapper"
to use a hyphenated compound modifier: replace it with "Google Play Billing
Library-compatible wrapper" wherever that exact phrase appears (e.g., the line
containing "2. **Billing Compatibility SDK** - Google Play Billing Library
compatible wrapper").

In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt`:
- Around line 245-247: The notifier is being called with a filtered purchase
list so suspended subscriptions get dropped; update restorePurchases flow so
notifySuspendedSubscriptions runs on the unfiltered Play result (or feed it the
raw purchases) before restorePurchasesHelper/filtering happens. Concretely, call
notifySuspendedSubscriptions(purchasesRaw) prior to any
includeSuspendedAndroid-based filtering in restorePurchases or change
restorePurchasesHelper to return both rawPlayPurchases and filteredPurchases and
pass the rawPlayPurchases into notifySuspendedSubscriptions so
subscriptionBillingIssue events are emitted for all suspended subscriptions.
- Around line 113-115: Replace the non-thread-safe mutableSetOf usages with
concurrent collections and use their atomic operations: change
subscriptionBillingIssueListeners to a thread-safe listener set (e.g.,
CopyOnWriteArraySet or ConcurrentHashMap.newKeySet()) so
addSubscriptionBillingIssueListener/removeSubscriptionBillingIssueListener are
safe from any thread, and change emittedBillingIssueTokens to a concurrent Set
(e.g., ConcurrentHashMap.newKeySet()) and use its add() return value inside
notifySuspendedSubscriptions to atomically test-and-register tokens to preserve
the "once per session" guarantee; update any code that iterates those sets to
rely on the concurrent set semantics and remove manual synchronization.

---

Outside diff comments:
In `@libraries/flutter_inapp_purchase/lib/types.dart`:
- Around line 5110-5128: The runtime dispatch is missing wiring for the new
subscriptionBillingIssue handler declared in types.dart: add a
StreamController<Purchase> field (e.g. _subscriptionBillingIssueListener) and a
public getter, instantiate SubscriptionHandlers with subscriptionBillingIssue:
() async => await _subscriptionBillingIssueListener.stream.first (matching the
pattern used for userChoiceBillingAndroid/developerProvidedBillingAndroid), and
wire the platform event "subscription-billing-issue" to add incoming Purchase
objects into _subscriptionBillingIssueListener so the handler receives events;
update flutter_inapp_purchase.dart to create, expose, and dispose the controller
alongside the other event controllers.

In `@packages/apple/Sources/OpenIapModule.swift`:
- Around line 18-34: cleanupExistingState()/endConnection() currently only
cancels updateListenerTask, leaving messageListenerTask (Task listening on
StoreKit.Message.messages) running; update cleanupExistingState() (the
endConnection teardown path) to also cancel and nil out messageListenerTask (and
mirror what deinit does) so the StoreKit message listener is stopped during
normal connection teardown — reference messageListenerTask,
cleanupExistingState(), endConnection(), and updateListenerTask to locate the
change.

---

Nitpick comments:
In `@knowledge/_claude-context/context.md`:
- Around line 1008-1131: Add proper blank lines around fenced code blocks and
include language identifiers on fences lacking them: locate the code fence that
contains "GraphQL Schema (src/*.graphql)" and ensure there is an empty line
before and after the ```text fence, and add a language spec (e.g., ```text or
```bash) to other fences missing a language label elsewhere in context.md;
update the surrounding markdown so each triple-backtick block has a blank line
separation and an explicit language tag.

In `@knowledge/external/google-billing-api.md`:
- Around line 378-429: Add official reference links for the new Billing Library
8.3 APIs introduced in this section: link enableDeveloperBillingOption
(BillingClient.Builder), DeveloperBillingOptionParams,
DeveloperProvidedBillingListener, DeveloperProvidedBillingDetails, and
BillingOption.EXTERNAL_PAYMENTS to their respective Android developer docs (or
OpenIAP equivalents used elsewhere in the doc). Update the examples' inline type
mentions to be linked the same way as earlier sections (consistent anchor
style), and add a short parenthetical OpenIAP mapping for the
AlternativeBilling* surface to match the other API-reference entries.

In
`@packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt`:
- Around line 57-58: The KDoc for the event in OpenIapListener.kt currently says
"previously-healthy subscription" which overstates behavior; update the comment
on the event (near class/function OpenIapListener) to state that the event fires
once per session when a subscription token is first observed in a suspended
state (e.g., payment failed, card expired) rather than implying the system
tracks a prior healthy state or health transition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59ba6b72-f72c-48e8-9085-ba7eb3347200

📥 Commits

Reviewing files that changed from the base of the PR and between 0cffffd and 2b9b54c.

⛔ Files ignored due to path filters (5)
  • packages/gql/src/generated/Types.kt is excluded by !**/generated/**
  • packages/gql/src/generated/Types.swift is excluded by !**/generated/**
  • packages/gql/src/generated/types.dart is excluded by !**/generated/**
  • packages/gql/src/generated/types.gd is excluded by !**/generated/**
  • packages/gql/src/generated/types.ts is excluded by !**/generated/**
📒 Files selected for processing (24)
  • knowledge/_claude-context/context.md
  • knowledge/external/google-billing-api.md
  • knowledge/external/horizon-api.md
  • knowledge/external/storekit2-api.md
  • libraries/expo-iap/src/types.ts
  • libraries/flutter_inapp_purchase/lib/types.dart
  • libraries/godot-iap/addons/godot-iap/types.gd
  • libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt
  • libraries/react-native-iap/src/types.ts
  • packages/apple/Sources/Helpers/IapState.swift
  • packages/apple/Sources/Models/Types.swift
  • packages/apple/Sources/OpenIapModule+ObjC.swift
  • packages/apple/Sources/OpenIapModule.swift
  • packages/apple/Sources/OpenIapProtocol.swift
  • packages/docs/public/llms-full.txt
  • packages/docs/public/llms.txt
  • packages/docs/src/pages/docs/updates/releases.tsx
  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
  • packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt
  • packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
  • packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
  • packages/gql/src/event.graphql
  • packages/gql/src/type.graphql

Comment thread libraries/react-native-iap/src/types.ts
Comment thread packages/apple/Sources/OpenIapModule.swift Outdated
Comment thread packages/docs/public/llms-full.txt Outdated
Comment thread packages/docs/public/llms-full.txt Outdated
Comment thread packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt Outdated
Comment thread packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt Outdated
hyochan and others added 8 commits April 16, 2026 01:08
…listener Flow

CI failure root cause: the regenerated SubscriptionHandlers interface adds
`suspend fun subscriptionBillingIssue(): Purchase` and the concrete
InAppPurchaseAndroid/InAppPurchaseIOS classes were not implementing it,
so the Android compile failed with "Class is not abstract and does not
implement abstract member".

- common KmpInAppPurchase: new `subscriptionBillingIssueListener: Flow<Purchase>`
  alongside `purchaseUpdatedListener` and `promotedProductListener`.
- android InAppPurchaseAndroid: backing MutableSharedFlow + emission hook in
  the getAvailablePurchasesHandler (dedup by purchase token per session,
  only fires for PurchaseAndroid with isSuspendedAndroid == true).
- android subscriptionHandlers: wires the new handler.
- ios InAppPurchaseIOS: placeholder Flow + throwing override (uses
  listener-based collection; cinterop bridge to Swift's Message listener
  lands in a follow-up release).

Verified: ./gradlew :library:compileDebugKotlinAndroid -> BUILD SUCCESSFUL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ios: swift tests fakes now implement subscriptionBillingIssueListener
  (unblocks CI Test iOS)
- ios: message listener gate widened from iOS-only to iOS + Mac Catalyst
  (StoreKit.Message ships on both; macOS/tvOS/watchOS/visionOS still out
  because Apple doesn't ship Message there — gemini was partly right)
- ios: dispatchBillingIssueMessage batches a single Product.products(for:)
  call for all currentEntitlements instead of one call per loop iteration
- ios: iterate the full subscription.status array rather than only .first —
  the array is unordered across group members per Apple docs
- google(play): switch subscriptionBillingIssueListeners to
  CopyOnWriteArraySet and emittedBillingIssueTokens to
  ConcurrentHashMap.newKeySet() so add/remove on the main thread is safe
  against iteration from Dispatchers.IO
- google(play): notify suspended subscriptions from onPurchasesUpdated too,
  not only from getAvailablePurchases, for parity with iOS push delivery
- google(play): always query with includeSuspended=true so the notifier
  sees suspended purchases even when the caller opted not to include them
  in the returned list, then filter for the caller
- docs(horizon-api.md): hyphenate "Google Play Billing Library-compatible"
  and "Auto-reconnect feature" compound modifiers

Verified locally:
- swift build + swift test (87 tests pass)
- gradlew :openiap:compilePlayDebugKotlin / compileHorizonDebugKotlin
- gradlew :library:compileDebugKotlinAndroid (kmp-iap)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…S + Android)

- specs/RnIap.nitro.ts: add/remove SubscriptionBillingIssueListener Nitro methods
- src/index.ts: subscriptionBillingIssueListener() public API with the same
  JS-side pattern as userChoiceBillingListenerAndroid / developerProvidedBillingListenerAndroid
- ios/HybridRnIap.swift: bridges OpenIapModule.subscriptionBillingIssueListener
  through to Nitro callbacks
- android/.../HybridRnIap.kt: bridges openIap.addSubscriptionBillingIssueListener
  (Play flavor) through convertToNitroPurchase to Nitro callbacks. Horizon
  flavor's OpenIapProtocol implementation is a no-op, so the listener is inert
  on Horizon builds — consistent with the rest of the stack.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- src/index.ts: add OpenIapEvent.SubscriptionBillingIssue + EventPayloads
  entry + subscriptionBillingIssueListener() public API
- android ExpoIapModule.kt: register EVENT_SUBSCRIPTION_BILLING_ISSUE
- android ExpoIapHelper.kt: subscribe to
  openIap.addSubscriptionBillingIssueListener and relay to the expo emitter
- ios ExpoIapModule.swift: declare OpenIapEvent.subscriptionBillingIssue
  in Events(...)
- ios ExpoIapHelper.swift: subscribe to
  OpenIapModule.shared.subscriptionBillingIssueListener and forward the
  serialized Purchase payload through sendEvent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…roid)

- lib/flutter_inapp_purchase.dart: add subscriptionBillingIssueListener
  Stream<Purchase> getter + 'subscription-billing-issue' method-channel
  case that converts the payload to a Purchase via convertToPurchase
- android AndroidInappPurchasePlugin.kt: subscribe to
  openIap.addSubscriptionBillingIssueListener and forward over the
  method channel
- ios FlutterInappPurchasePlugin.swift: subscribe to
  OpenIapModule.shared.subscriptionBillingIssueListener, serialize via
  OpenIapSerialization.purchase, and invokeMethod
  "subscription-billing-issue"

Verified: flutter analyze (no issues).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ndroid + iOS)

- addons/godot-iap/godot_iap.gd: new subscription_billing_issue signal +
  signal connection for both iOS native (Dictionary arg) and Android
  (JSON string arg parsed into Dictionary)
- android GodotIap.kt: register SUBSCRIPTION_BILLING_ISSUE SignalInfo,
  subscribe OpenIapSubscriptionBillingIssueListener and forward the
  serialized purchase JSON
- ios-gdextension GodotIap.swift: declare @signal
  subscription_billing_issue, subscribe to
  openIap.subscriptionBillingIssueListener and emit a VariantDictionary
  with id, productId, transactionId, transactionDate, store +
  purchaseJson blob for consumers that need the full shape

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
iosMain InAppPurchaseIOS.kt now subscribes to
openIapModule.addSubscriptionBillingIssueListener during setupListeners(),
converts the incoming NSDictionary payload via convertAnyToPurchase, and
emits onto _subscriptionBillingIssueFlow. endConnection removes the
subscription alongside the other listener tokens. The previous throwing
stub for the suspend override is replaced with
subscriptionBillingIssueListener.first() so either collection path works.

commonMain + Android already land the Flow in the earlier commit. Android
Flow is driven by openIap.addSubscriptionBillingIssueListener +
getAvailablePurchases isSuspended detection inside openiap-google.

Verified locally:
- ./gradlew :library:compileCommonMainKotlinMetadata -> BUILD SUCCESSFUL
- ./gradlew :library:compileDebugKotlinAndroid       -> BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New /docs/features/subscription-billing-issue route + sidebar entry
covering the cross-platform event introduced in this PR.

Sections:
- Platform behavior table (iOS / iPadOS / Mac Catalyst / Android Play /
  Android Horizon / non-iOS Apple) with min versions and delivery model
- Recommended UX (route users to deepLinkToSubscriptions, do not re-grant
  entitlements on the assumption the subscription is still active)
- Per-language usage snippets: react-native-iap/expo-iap, Flutter, Godot,
  kmp-iap
- Deduping behavior on both platforms

References Apple (StoreKit.Message, Reason.billingIssue) and Google
(Suspended subscriptions) official docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new cross-platform subscriptionBillingIssue subscription event to OpenIAP, unifying StoreKit 2’s billing-issue messages (iOS 18+/Mac Catalyst 18+) with Google Play Billing’s suspended subscription signal (Billing 8.1+, Play flavor only), and propagates the schema/types + bridge wiring across downstream libraries and docs.

Changes:

  • Extend GraphQL schema + regenerate all platform/type mirrors to include IapEvent.SubscriptionBillingIssue and Subscription.subscriptionBillingIssue: Purchase!.
  • Implement native emission/listeners on iOS (StoreKit Message.messages loop) and Android Play flavor (Purchase.isSuspended), with Horizon flavor as explicit no-op.
  • Add downstream library bridges (RN/Expo/Flutter/Godot/KMP) and documentation/release notes updates.

Reviewed changes

Copilot reviewed 46 out of 51 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/gql/src/type.graphql Adds IapEvent.SubscriptionBillingIssue to schema.
packages/gql/src/event.graphql Adds subscriptionBillingIssue: Purchase! subscription field.
packages/gql/src/generated/types.ts Regenerated TS types to include new event + subscription field.
packages/gql/src/generated/types.gd Regenerated GDScript types to include new event mapping.
packages/gql/src/generated/types.dart Regenerated Dart types to include new event + resolver handler.
packages/gql/src/generated/Types.swift Regenerated Swift types to include new event + resolver handler.
packages/gql/src/generated/Types.kt Regenerated Kotlin types to include new event + resolver handler.
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt Implements Play flavor listener registration + suspended-subscription emission/dedupe.
packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt Adds OpenIapSubscriptionBillingIssueListener interface.
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt Updates Android main Types mirror with new enum + subscription handler.
packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt Extends protocol with add/remove subscription billing issue listeners.
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt Adds Horizon explicit no-op add/remove with warning logs.
packages/docs/src/pages/docs/updates/releases.tsx Adds release note entry for the new event.
packages/docs/src/pages/docs/index.tsx Registers new docs route + sidebar entry.
packages/docs/src/pages/docs/features/subscription-billing-issue.tsx New feature doc page describing behavior/UX and library snippets.
packages/docs/public/llms.txt Regenerated LLM quick reference timestamp.
packages/docs/public/llms-full.txt Regenerated full LLM reference content.
packages/apple/Tests/OpenIapTests/VerifyPurchaseWithProviderTests.swift Updates fake module to satisfy new protocol listener method.
packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift Updates fake module to satisfy new protocol listener method.
packages/apple/Sources/OpenIapProtocol.swift Adds SubscriptionBillingIssueListener typealias + protocol method.
packages/apple/Sources/OpenIapModule.swift Adds StoreKit Message listener task + emission plumbing for billing-issue.
packages/apple/Sources/OpenIapModule+ObjC.swift Exposes ObjC bridge method for subscription billing issue listener.
packages/apple/Sources/Models/Types.swift Updates Apple Types mirror with new enum + subscription handler.
packages/apple/Sources/Helpers/IapState.swift Stores/snapshots new billing-issue listener list in actor state.
libraries/react-native-iap/src/types.ts Updates RN type mirror with new event + subscription field.
libraries/react-native-iap/src/specs/RnIap.nitro.ts Adds Nitro spec add/remove methods for billing-issue listener.
libraries/react-native-iap/src/index.ts Adds JS API subscriptionBillingIssueListener() and listener state wiring.
libraries/react-native-iap/ios/HybridRnIap.swift Adds iOS Nitro bridge wiring to OpenIapModule subscriptionBillingIssueListener.
libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt Adds Android Nitro bridge wiring via openiap-google listener.
libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt Adds iOS Flow bridge for subscription billing issue + resolver method.
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt Updates KMP type mirror with new enum + subscription handler.
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt Adds subscriptionBillingIssueListener: Flow<Purchase> to KMP interface.
libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt Adds Android Flow emission + per-process dedupe in getAvailablePurchases.
libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift Adds iOS GDExtension signal + listener wiring for billing-issue event.
libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt Adds Android signal + listener registration for billing-issue event.
libraries/godot-iap/addons/godot-iap/types.gd Updates Godot addon types mirror with new event mapping.
libraries/godot-iap/addons/godot-iap/godot_iap.gd Adds subscription_billing_issue signal and platform hookups.
libraries/flutter_inapp_purchase/lib/types.dart Updates Flutter Dart types mirror with new enum + resolver handler.
libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart Adds Dart stream + method-channel dispatch for new event.
libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift Adds iOS channel emission for subscription billing issue.
libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt Adds Android channel emission for subscription billing issue.
libraries/expo-iap/src/types.ts Updates Expo type mirror with new event + subscription field.
libraries/expo-iap/src/index.ts Adds JS API + enum/payload typing for subscription billing issue.
libraries/expo-iap/ios/ExpoIapModule.swift Registers new event in Expo module event list.
libraries/expo-iap/ios/ExpoIapHelper.swift Wires iOS OpenIapModule listener to Expo event emission.
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt Registers new event constant + module Events list.
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt Wires Android OpenIapModule listener to Expo safeEmitEvent.
knowledge/external/storekit2-api.md Updates StoreKit 2 external reference (Message API, version fixes, new fields).
knowledge/external/horizon-api.md Fixes Horizon reference (accessToken shape, hyphenation).
knowledge/external/google-billing-api.md Adds External Payments Program (8.3+) reference section.
knowledge/_claude-context/context.md Regenerates project context and adds more repo conventions/details.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread knowledge/external/storekit2-api.md
Comment thread libraries/react-native-iap/ios/HybridRnIap.swift
hyochan and others added 2 commits April 16, 2026 06:46
- rn-iap useIAP: add onSubscriptionBillingIssue callback option with
  matching listener lifecycle (attach during setup, cleanup with others)
- OpenIapStore (Android): expose add/removeSubscriptionBillingIssueListener
  pass-through so Compose callers don't have to reach the underlying
  OpenIapProtocol directly
- apple Example SubscriptionFlowScreen: banner card + "Fix payment method"
  button wired to OpenIapModule.shared.deepLinkToSubscriptions, listener
  registered in setupIapProvider and torn down on screen dispose
- google Example SubscriptionFlowScreen: analogous banner on the Compose
  LazyColumn, registered via DisposableEffect on iapStore, dispatches
  deepLinkToSubscriptions with sku + package name from BuildConfig

Verified locally:
- rn-iap: yarn tsc --noEmit -p tsconfig.build.json -> clean
- apple:  swift build -> clean
- google: ./gradlew :example:compilePlayDebugKotlin -> BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Horizon no-op test (packages/google):
- Add Robolectric 4.13 + androidx.test:core 1.5.0 to testImplementation
- Enable unitTests.isIncludeAndroidResources for manifest-free test
- Add src/testHorizon/java sourceSet
- SubscriptionBillingIssueHorizonNoOpTest asserts that
  add/removeSubscriptionBillingIssueListener on the Horizon OpenIapModule
  never invokes the callback, guarding against accidental Play-flavor
  emission logic leaking into Horizon

Sandbox E2E guide (knowledge/internal):
- Concrete step-by-step for iOS 18 sandbox (billing issue toggle or
  remove payment method) and Play 8.1+ sandbox (remove payment method /
  Play Console test suspensions)
- Per-library smoke matrix using libraries-versions.jsonc "local" mode
- Automated coverage matrix separating what CI enforces from what
  release QA must run manually against live stores

Verified: gradlew :openiap:testHorizonDebugUnitTest -> BUILD SUCCESSFUL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new cross-platform subscriptionBillingIssue event across the OpenIAP schema and all platform/framework implementations, unifying StoreKit 2’s billing-issue messaging (iOS 18+) with Google Play Billing’s suspended-subscription signal (Billing 8.1+), plus documentation, examples, and a Horizon no-op test.

Changes:

  • Extends the GraphQL schema + regenerates types to include IapEvent.SubscriptionBillingIssue and subscriptionBillingIssue: Purchase!.
  • Implements native event emission/listeners on iOS and Android (Play), with explicit Horizon no-op behavior + a Robolectric guard test.
  • Wires the event through downstream libraries (RN, Expo, Flutter, Godot, KMP) and adds docs + example-app UI banners.

Reviewed changes

Copilot reviewed 53 out of 58 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/gql/src/type.graphql Adds IapEvent.SubscriptionBillingIssue enum value.
packages/gql/src/event.graphql Adds subscriptionBillingIssue: Purchase! subscription field.
packages/gql/src/generated/types.ts Regenerated TS types with new event + subscription field.
packages/gql/src/generated/types.gd Regenerated GDScript types/mappings with new event.
packages/gql/src/generated/types.dart Regenerated Dart types with new event + resolver hooks.
packages/gql/src/generated/Types.swift Regenerated Swift types with new event + resolver hooks.
packages/gql/src/generated/Types.kt Regenerated Kotlin types with new event + resolver hooks.
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt Play-flavor listener registration + isSuspended detection + dedupe.
packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt Exposes add/remove listener passthrough on OpenIapStore.
packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt Adds OpenIapSubscriptionBillingIssueListener interface.
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt Mirrors generated type updates in Android main Types.
packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt Adds add/remove listener API to protocol.
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt Horizon explicit no-op listener methods with warnings.
packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionBillingIssueHorizonNoOpTest.kt Robolectric test guaranteeing Horizon remains a no-op.
packages/google/openiap/build.gradle.kts Adds testHorizon sources + Robolectric deps/config.
packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt Example UI banner + listener wiring + deep-link action.
packages/apple/Sources/OpenIapProtocol.swift Adds SubscriptionBillingIssueListener typealias + protocol method.
packages/apple/Sources/OpenIapModule.swift Starts StoreKit Message.messages loop and emits event based on subscription status.
packages/apple/Sources/OpenIapModule+ObjC.swift ObjC bridge: addSubscriptionBillingIssueListener.
packages/apple/Sources/Models/Types.swift Mirrors generated types update in Apple Models.
packages/apple/Sources/Helpers/IapState.swift Adds state storage/snapshot for billing-issue listeners.
packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift Updates protocol fakes to implement new listener method.
packages/apple/Tests/OpenIapTests/VerifyPurchaseWithProviderTests.swift Updates protocol fakes to implement new listener method.
packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift Example UI banner + listener token lifecycle management.
libraries/react-native-iap/src/types.ts Updates RN type mirror for new event/subscription field.
libraries/react-native-iap/src/specs/RnIap.nitro.ts Adds Nitro spec methods for add/remove listener.
libraries/react-native-iap/src/index.ts Adds JS subscriptionBillingIssueListener implementation.
libraries/react-native-iap/src/hooks/useIAP.ts Adds onSubscriptionBillingIssue hook callback support.
libraries/react-native-iap/ios/HybridRnIap.swift iOS Nitro bridge forwards event from OpenIapModule.
libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt Android Nitro bridge forwards event from openiap-google.
libraries/expo-iap/src/types.ts Updates Expo type mirror for new event/subscription field.
libraries/expo-iap/src/index.ts Adds OpenIapEvent.SubscriptionBillingIssue + JS listener helper.
libraries/expo-iap/ios/ExpoIapModule.swift Registers new event name for Expo module events.
libraries/expo-iap/ios/ExpoIapHelper.swift Emits subscription-billing-issue via Expo event emitter.
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt Registers new event constant in Expo Android module.
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt Attaches native listener and emits event payload to JS.
libraries/flutter_inapp_purchase/lib/types.dart Updates Flutter type mirror with new event/subscription field.
libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart Adds stream + method-channel dispatch for event.
libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift iOS plugin bridges billing-issue listener to Flutter.
libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt Android plugin bridges billing-issue listener to Flutter.
libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift Adds Godot iOS signal + OpenIAP listener wiring.
libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt Adds Godot Android signal + listener registration.
libraries/godot-iap/addons/godot-iap/types.gd Updates Godot type mirror with new event/mappings.
libraries/godot-iap/addons/godot-iap/godot_iap.gd Adds cross-platform signal hookup + handlers.
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt Updates KMP type mirror with new event/subscription field.
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt Adds subscriptionBillingIssueListener: Flow<Purchase> to public API.
libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt Emits event in Android KMP implementation + dedupe.
libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt Bridges ObjC listener to Flow on iOS.
packages/docs/src/pages/docs/index.tsx Adds docs route + sidebar entry for feature page.
packages/docs/src/pages/docs/features/subscription-billing-issue.tsx New feature page documenting semantics + usage snippets.
packages/docs/src/pages/docs/updates/releases.tsx Adds release-note entry for the feature.
packages/docs/public/llms.txt Recompiled LLM quick reference output timestamp.
packages/docs/public/llms-full.txt Recompiled LLM full reference content.
knowledge/internal/sandbox-subscription-billing-issue.md Adds internal manual sandbox verification guide.
knowledge/external/storekit2-api.md Updates StoreKit 2 external reference (Message API etc.).
knowledge/external/google-billing-api.md Updates Google Billing external reference (External Payments section).
knowledge/external/horizon-api.md Updates Horizon external reference (wording + accessToken docs).
knowledge/_claude-context/context.md Recompiled project context for tooling/LLM ingestion.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/docs/src/pages/docs/updates/releases.tsx
Comment thread packages/apple/Sources/OpenIapModule.swift Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libraries/react-native-iap/ios/HybridRnIap.swift (1)

1136-1174: ⚠️ Potential issue | 🟠 Major

Cleanup misses the new subscription-billing-issue listener state.

cleanupExistingState() does not remove subscriptionBillingIssueSub and does not clear subscriptionBillingIssueListeners. This can leave stale callbacks/subscriptions alive after endConnection().

🧹 Suggested fix
     private func cleanupExistingState() {
         // Cancel transaction listener if any
         updateListenerTask?.cancel()
         updateListenerTask = nil
         isInitialized = false
         isInitializing = false

         // Remove OpenIAP listeners & end connection
         if let sub = purchaseUpdatedSub {
             RnIapLog.payload("removeListener", "purchaseUpdated")
             OpenIapModule.shared.removeListener(sub)
         }
         if let sub = purchaseErrorSub {
             RnIapLog.payload("removeListener", "purchaseError")
             OpenIapModule.shared.removeListener(sub)
         }
         if let sub = promotedProductSub {
             RnIapLog.payload("removeListener", "promotedProduct")
             OpenIapModule.shared.removeListener(sub)
         }
+        if let sub = subscriptionBillingIssueSub {
+            RnIapLog.payload("removeListener", "subscriptionBillingIssue")
+            OpenIapModule.shared.removeListener(sub)
+        }
         purchaseUpdatedSub = nil
         purchaseErrorSub = nil
         promotedProductSub = nil
+        subscriptionBillingIssueSub = nil
         Task {
             RnIapLog.payload("endConnection", nil)
             let result = try? await OpenIapModule.shared.endConnection()
             RnIapLog.result("endConnection", result as Any)
         }

         // Clear event listeners, error dedup state, and delivery state (thread-safe)
         listenerLock.withLock {
             purchaseUpdatedListeners.removeAll()
             purchaseErrorListeners.removeAll()
             promotedProductListeners.removeAll()
+            subscriptionBillingIssueListeners.removeAll()
             lastPurchaseErrorKey = nil
             lastPurchaseErrorTimestamp = 0
             deliveredPurchaseEventKeys.removeAll()
             deliveredPurchaseEventOrder.removeAll()
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/react-native-iap/ios/HybridRnIap.swift` around lines 1136 - 1174,
The cleanupExistingState() function currently misses removing the subscription
billing issue listener and clearing its listener state; update
cleanupExistingState() to cancel/remove subscriptionBillingIssueSub (call
OpenIapModule.shared.removeListener and RnIapLog.payload("removeListener",
"subscriptionBillingIssue") if sub exists), set subscriptionBillingIssueSub =
nil, and inside listenerLock.withLock also clear
subscriptionBillingIssueListeners (and any related state like delivered keys if
applicable) so no stale callbacks remain after endConnection(); reference the
subscriptionBillingIssueSub and subscriptionBillingIssueListeners symbols when
making these changes.
🧹 Nitpick comments (1)
libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (1)

1689-1723: Move this block into the event-listener section.

This new listener API sits below private/helper sections, which breaks the native bridge ordering convention used for react-native-iap.

As per coding guidelines, "libraries/react-native-iap/**/*.{swift,kt}: Organize native implementation classes (Swift/Kotlin) in strict order: Properties and Initialization, Public Cross-platform Methods, Platform-specific Public Methods, Event Listener Methods, Private Helper Methods".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt`
around lines 1689 - 1723, The subscription billing-issue listener block (vars
and methods subscriptionBillingIssueAttached,
addSubscriptionBillingIssueListener, removeSubscriptionBillingIssueListener, and
attachSubscriptionBillingIssueIfNeeded which references openIap and
convertToNitroPurchase) is placed below private/helper sections; move this
entire block into the Event Listener Methods section of the class so it follows
the project's ordering convention (Properties/Init, Public Cross-platform
Methods, Platform-specific Public Methods, Event Listener Methods, then Private
Helpers). Ensure you relocate the declaration of
subscriptionBillingIssueAttached and the three methods together, keeping their
synchronized blocks and the openIap.addSubscriptionBillingIssueListener usage
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@knowledge/_claude-context/context.md`:
- Around line 1235-1245: The fenced code block in
knowledge/_claude-context/context.md triggers MD040 because it lacks a language;
fix it by adding a language identifier (e.g., change the opening fence from ```
to ```text) for the code block that lists the src/pages/docs/features tree so
the markdown linter passes (addressing MD040 for that fenced code block).
- Line 1233: The markdown contains three identical headings "Directory
Structure" (one shown in the diff) causing MD024 duplicate-heading violations;
rename each duplicate heading to a unique title (e.g., append a qualifier like
"Directory Structure — CLI", "Directory Structure — API", or include a
numeric/section qualifier) and update any internal links/anchors that reference
those headings so anchors remain correct; locate the headings by their exact
text "Directory Structure" in the file and change each to a distinct variant.
- Around line 1123-1130: The fenced GraphQL example starting with the code block
containing "type RequestPurchaseResult" violates MD031; fix it by adding a blank
line immediately before the opening ```graphql fence and a blank line
immediately after the closing ``` fence so there is an empty line both above and
below the fenced block (ensure the block around the "type RequestPurchaseResult"
example is separated by those blank lines).

In `@libraries/expo-iap/src/index.ts`:
- Around line 320-323: The subscriptionBillingIssueListener currently forwards
raw Purchase objects from
emitter.addListener(OpenIapEvent.SubscriptionBillingIssue, listener); update it
to normalize the purchase payload (reuse the same normalizePurchase /
normalizePurchasePayload helper used by purchaseUpdatedListener) before invoking
the consumer callback: register a wrapper listener with emitter.addListener that
receives the raw purchase, runs normalizePurchase(raw) and then calls the
provided listener(normalizedPurchase) so consumers always receive a consistent,
normalized Purchase shape.

In `@libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt`:
- Line 86: The endConnection() method is not removing the subscription
billing-issue listener registered in initConnection():
subscriptionBillingIssueListener is added via
openIap.addSubscriptionBillingIssueListener(subscriptionBillingIssueListener)
but never removed, causing duplicate listeners on reconnect; update
endConnection() to call
openIap.removeSubscriptionBillingIssueListener(subscriptionBillingIssueListener)
alongside the existing removal of purchase update/error listeners so the
subscriptionBillingIssueListener is properly detached before disconnecting.

In
`@libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt`:
- Around line 706-718: The mutableSet emittedBillingIssueTokens used in
notifySuspendedSubscriptions is not thread-safe and can race when
getAvailablePurchasesHandler runs on Dispatchers.IO concurrently; replace
emittedBillingIssueTokens with a concurrent set (e.g.,
ConcurrentHashMap.newKeySet() or Collections.newSetFromMap(ConcurrentHashMap()))
to mirror the cachedProductDetails concurrency pattern, and keep the same
add-and-check logic (emittedBillingIssueTokens.add(token)) in
notifySuspendedSubscriptions so duplicate tokens are still suppressed safely.

In
`@libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt`:
- Around line 107-116: initConnection() does not re-register listeners after a
reconnect, so listeners like subscriptionBillingIssueSubscription (registered
via openIapModule.addSubscriptionBillingIssueListener in setupListeners()) stop
emitting after endConnection()/init cycle; fix by invoking setupListeners() (or
extracting listener registration into a reusable method and calling it) after
the connection succeeds inside initConnection(), ensuring
subscriptionBillingIssueSubscription and the other listener subscriptions are
re-created and the flows (e.g., _subscriptionBillingIssueFlow) will emit again.

In
`@libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt`:
- Line 87: endConnection() currently clears other listener collections but
leaves subscriptionBillingIssueListeners and the flag
subscriptionBillingIssueAttached intact, which allows stale JS callbacks after
reconnect; update endConnection() to clear the subscriptionBillingIssueListeners
list and reset subscriptionBillingIssueAttached to false, and ensure any code
path that removes all listeners (e.g., the teardown/cleanup logic similar to
other listener collections) performs the same reset so that
addSubscriptionBillingIssueListener() will reattach the native OpenIAP callback
on next connect.

In `@libraries/react-native-iap/src/hooks/useIAP.ts`:
- Around line 479-489: The listener for subscription billing issues is only
created when optionsRef.current?.onSubscriptionBillingIssue exists at init, so
it never registers if the callback is added later; change the condition to only
check that subscriptionsRef.current.subscriptionBillingIssue is not already set,
then always call subscriptionBillingIssueListener to assign
subscriptionsRef.current.subscriptionBillingIssue, and inside the listener guard
the call with optionsRef.current?.onSubscriptionBillingIssue before invoking it
(use the existing purchase parameter). This ensures the listener is attached
independently of initial callback presence while still checking for the callback
at invocation time.

In `@libraries/react-native-iap/src/index.ts`:
- Around line 598-613: subscriptionBillingIssueNativeHandler currently calls
convertNitroPurchaseToPurchase without validating the native payload, so a
malformed nitroPurchase can throw and prevent notifications; update
subscriptionBillingIssueNativeHandler to call
validateNitroPurchase(nitroPurchase) first (like purchaseUpdateNativeHandler
does), and if validation fails log the error via RnIapConsole.error and return
early, otherwise proceed to convertNitroPurchaseToPurchase and notify
subscriptionBillingIssueJsListeners; keep the same try/catch around each JS
listener callback.

In `@libraries/react-native-iap/src/specs/RnIap.nitro.ts`:
- Around line 1098-1119: The iOS implementation of
removeSubscriptionBillingIssueListener currently clears all listeners; change
HybridRnIap.removeSubscriptionBillingIssueListener to remove only the provided
listener by using subscriptionBillingIssueListeners.removeAll { $0 === listener
} (inside the existing listenerLock.withLock block) instead of
subscriptionBillingIssueListeners.removeAll(), ensuring behavior matches
Android's removeSubscriptionBillingIssueListener and the NitroPurchase listener
contract.

In `@packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift`:
- Around line 241-245: Ensure you don't register the same listener twice by
checking or tearing down any existing token before assigning a new one: before
calling OpenIapModule.shared.subscriptionBillingIssueListener, either guard that
billingIssueListenerToken is nil (so you only register once) or explicitly
remove/invalidate the previous token (e.g., if let old =
billingIssueListenerToken { old.invalidate() /* or call the module's
remove/unsubscribe API with old */; billingIssueListenerToken = nil }) and then
assign the new token to billingIssueListenerToken; refer to
billingIssueListenerToken and
OpenIapModule.shared.subscriptionBillingIssueListener to locate the code.

In `@packages/apple/Sources/OpenIapModule.swift`:
- Line 18: cleanupExistingState() currently only cancels updateListenerTask and
forgets messageListenerTask, so the StoreKit message loop (messageListenerTask)
keeps draining Message.messages after endConnection(); modify
cleanupExistingState() (and ensure endConnection() calls it) to cancel and nil
out messageListenerTask (call messageListenerTask?.cancel() and set
messageListenerTask = nil) and likewise ensure any similar teardown paths clear
that Task so it stops when state.isInitialized is false.

In `@packages/docs/public/llms-full.txt`:
- Around line 343-374: Add documentation for the new event listener by inserting
a new section alongside purchaseUpdatedListener and purchaseErrorListener that
shows how to import and use subscriptionBillingIssueListener; reference the
symbol subscriptionBillingIssueListener in the example, show a minimal callback
(e.g., logging purchase.productId and prompting the user to update payment info
or deep-link to subscriptions), and show cleanup via subscription.remove();
ensure wording and placement match the existing Event Listeners section and
follow the style used for purchaseUpdatedListener and purchaseErrorListener.

In `@packages/docs/src/pages/docs/features/subscription-billing-issue.tsx`:
- Around line 163-172: The GDScript example uses a raw Dictionary for
deep_link_to_subscriptions which will fail because deep_link_to_subscriptions
expects a Types.DeepLinkOptions instance (or null) and calls .to_dict() on it;
modify the example around the subscription_billing_issue handler to construct
and pass a Types.DeepLinkOptions object (e.g. instantiate Types.DeepLinkOptions,
set skuAndroid and packageNameAndroid) or pass null, and then call
godot_iap.deep_link_to_subscriptions(...) with that object so the method's
.to_dict() call succeeds.

---

Outside diff comments:
In `@libraries/react-native-iap/ios/HybridRnIap.swift`:
- Around line 1136-1174: The cleanupExistingState() function currently misses
removing the subscription billing issue listener and clearing its listener
state; update cleanupExistingState() to cancel/remove
subscriptionBillingIssueSub (call OpenIapModule.shared.removeListener and
RnIapLog.payload("removeListener", "subscriptionBillingIssue") if sub exists),
set subscriptionBillingIssueSub = nil, and inside listenerLock.withLock also
clear subscriptionBillingIssueListeners (and any related state like delivered
keys if applicable) so no stale callbacks remain after endConnection();
reference the subscriptionBillingIssueSub and subscriptionBillingIssueListeners
symbols when making these changes.

---

Nitpick comments:
In
`@libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt`:
- Around line 1689-1723: The subscription billing-issue listener block (vars and
methods subscriptionBillingIssueAttached, addSubscriptionBillingIssueListener,
removeSubscriptionBillingIssueListener, and
attachSubscriptionBillingIssueIfNeeded which references openIap and
convertToNitroPurchase) is placed below private/helper sections; move this
entire block into the Event Listener Methods section of the class so it follows
the project's ordering convention (Properties/Init, Public Cross-platform
Methods, Platform-specific Public Methods, Event Listener Methods, then Private
Helpers). Ensure you relocate the declaration of
subscriptionBillingIssueAttached and the three methods together, keeping their
synchronized blocks and the openIap.addSubscriptionBillingIssueListener usage
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 114536cb-4e28-468d-9aea-f135e6d942f7

📥 Commits

Reviewing files that changed from the base of the PR and between 2b9b54c and 79c0a15.

📒 Files selected for processing (32)
  • knowledge/_claude-context/context.md
  • knowledge/external/horizon-api.md
  • libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt
  • libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt
  • libraries/expo-iap/ios/ExpoIapHelper.swift
  • libraries/expo-iap/ios/ExpoIapModule.swift
  • libraries/expo-iap/src/index.ts
  • libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt
  • libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift
  • libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart
  • libraries/godot-iap/addons/godot-iap/godot_iap.gd
  • libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt
  • libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift
  • libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt
  • libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt
  • libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
  • libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
  • libraries/react-native-iap/ios/HybridRnIap.swift
  • libraries/react-native-iap/src/hooks/useIAP.ts
  • libraries/react-native-iap/src/index.ts
  • libraries/react-native-iap/src/specs/RnIap.nitro.ts
  • packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift
  • packages/apple/Sources/OpenIapModule.swift
  • packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift
  • packages/apple/Tests/OpenIapTests/VerifyPurchaseWithProviderTests.swift
  • packages/docs/public/llms-full.txt
  • packages/docs/public/llms.txt
  • packages/docs/src/pages/docs/features/subscription-billing-issue.tsx
  • packages/docs/src/pages/docs/index.tsx
  • packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt
  • packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
✅ Files skipped from review due to trivial changes (1)
  • packages/docs/public/llms.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • knowledge/external/horizon-api.md

Comment thread knowledge/_claude-context/context.md
Comment thread knowledge/_claude-context/context.md
Comment thread knowledge/_claude-context/context.md
Comment thread libraries/expo-iap/src/index.ts
Comment thread libraries/react-native-iap/src/specs/RnIap.nitro.ts
Comment thread packages/apple/Sources/OpenIapModule.swift
Comment thread packages/docs/public/llms-full.txt
hyochan and others added 4 commits April 16, 2026 07:08
… listener

The listener callback receives `Purchase` (the sealed union), not
`PurchaseIOS`. Cast via `.asIOS()` before assigning to the
`OpenIapPurchase` (aka PurchaseIOS) state. Matches the pattern already
used for `onPurchaseSuccess` in this file.

Xcode build failure reported: "Cannot assign value of type 'Purchase' to
type 'OpenIapPurchase' (aka 'PurchaseIOS')" at
SubscriptionFlowScreen.swift:243.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Apple (packages/apple):
- OpenIapModule.startMessageListener docstring now matches Apple's actual
  API surface (18.0+ for .billingIssue; not 16+ — that's a different
  availability)
- cleanupExistingState cancels messageListenerTask alongside
  updateListenerTask so endConnection() stops draining Message.messages
- apple Example SubscriptionFlowScreen guards duplicate listener
  registration if setup runs again before teardown

Play flavor (packages/google):
- endConnection now clears emittedBillingIssueTokens and
  subscriptionBillingIssueListeners; a fresh initConnection() can
  re-emit for previously-seen tokens and late-attached listeners

react-native-iap:
- src/index.ts: subscriptionBillingIssueListener() retries native
  attachment every call (previously a listener registered before Nitro
  came up stayed inert forever); validates payload via
  validateNitroPurchase before converting
- ios HybridRnIap: removeSubscriptionBillingIssueListener pops the last
  entry instead of removing all — matches Android semantics
- android HybridRnIap: endConnection resets
  subscriptionBillingIssueListeners + subscriptionBillingIssueAttached
  so stale JS callbacks don't survive reconnect
- useIAP hook: listener attached unconditionally; late-added
  onSubscriptionBillingIssue callback now fires

expo-iap: subscriptionBillingIssueListener wraps the emitter callback
through normalizePurchasePlatform, matching purchaseUpdatedListener

kmp-iap:
- Android emittedBillingIssueTokens switched to
  ConcurrentHashMap.newKeySet() for safe add() under Dispatchers.IO
- iOS: setupListeners is now idempotent + called from initConnection()
  after a successful reconnect; endConnection nulls the subscription
  tokens so flows resume emitting post reconnect
- iOS: stale "follow-up release" comment removed

godot-iap Android: endConnection now calls
removeSubscriptionBillingIssueListener, matching the other listener
teardowns so reconnects don't double-register

Docs:
- feature page SEO "Android 8.1+" → "Play Billing Library 8.1+"
- knowledge/external/storekit2-api.md section header "External Purchase
  Support (iOS 18.2+)" → "(iOS 17.4+)" to match the corrected version
  table; follow-on custom-link APIs noted as 18.1+
- packages/docs releases.tsx note now says downstream wiring ships with
  this PR (not "next per-library release")
- llms.txt / llms-full.txt / context.md recompiled

Verified locally:
- swift build -> clean
- gradlew :openiap:{compilePlayDebugKotlin,compileHorizonDebugKotlin,
  testHorizonDebugUnitTest,assembleHorizonDebug},
  :example:compilePlayDebugKotlin -> all green
- rn-iap yarn tsc --noEmit -p tsconfig.build.json -> clean
- expo-iap bun run tsc --noEmit -> clean
- flutter analyze -> no issues
- kmp-iap gradlew :library:compileDebugKotlinAndroid -> clean
- docs tsc --noEmit -> clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The useIAP hook now attaches the listener unconditionally (fix for
review comment #3089615611). Four existing test files mock IAP.instance
but did not include the new Nitro method, causing:

  TypeError: IAP.instance.addSubscriptionBillingIssueListener is not a
  function

in useIAP.test.ts / useIAP.android.test.ts during registerListeners.

Added add/removeSubscriptionBillingIssueListener stubs in:
- src/__tests__/hooks/useIAP.test.ts
- src/__tests__/hooks/useIAP.android.test.ts
- src/__tests__/index.test.ts
- src/__tests__/platform-detection.test.ts

Verified: yarn test -> 269/269 tests pass, 12/12 suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- [P1] rn-iap iOS: clear subscriptionBillingIssueSub and listeners in
  cleanupExistingState() so reconnect re-registers the OpenIAP listener
- [P2] openiap-google Play: wire subscriptionBillingIssue into
  SubscriptionHandlers via new onSubscriptionBillingIssue helper
- [P2] openiap-google Horizon: wire handler that throws FeatureNotSupported
  (Horizon Billing 7.0 lacks isSuspended signal)
- [P2] flutter: add subscriptionBillingIssue to subscriptionHandlers getter
- [P2] kmp-iap Android: clear emittedBillingIssueTokens on endConnection

Tests:
- Play/Horizon handler bundle exposure (Robolectric)
- Flutter subscriptionHandlers non-null assertion
- kmp-iap dedupe set reset via reflection
- rn-iap iOS XCTest target + reconnect regression test (Xcode 26.4
  env issue blocks local run; CI with Xcode 16.x should pass)

Docs:
- events.tsx: subscription-billing-issue-event section
- home.tsx: expand APIs (6), Events (6), Types (6) cards

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new cross-platform subscriptionBillingIssue event that unifies StoreKit 2 billing-issue messages (iOS 18+ / Mac Catalyst 18+) and Google Play Billing suspended subscriptions (Purchase.isSuspended, Billing 8.1+) into a single listener surface, and wires it through the schema, native SDKs, downstream bridges, examples, docs, and tests.

Changes:

  • GraphQL schema + generated types updated with IapEvent.SubscriptionBillingIssue and subscriptionBillingIssue: Purchase!.
  • iOS adds a StoreKit Message.messages loop and emits events for subscriptions in billing retry/grace.
  • Android Play flavor detects suspended purchases (deduped by purchaseToken), Horizon flavor is explicit no-op/unsupported; downstream libraries + docs updated accordingly.

Reviewed changes

Copilot reviewed 70 out of 75 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/gql/src/type.graphql Add SubscriptionBillingIssue enum
packages/gql/src/event.graphql Add subscriptionBillingIssue subscription field
packages/gql/src/generated/types.ts Regenerate TS types for event/field
packages/gql/src/generated/types.gd Regenerate GDScript types for event
packages/gql/src/generated/types.dart Regenerate Dart types for event
packages/gql/src/generated/Types.swift Regenerate Swift types for event
packages/gql/src/generated/Types.kt Regenerate Kotlin types for event
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt Emit event from Play suspended purchases
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt Horizon handler throws + listener no-op
packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt Add listener methods to protocol
packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt Add listener interface type
packages/google/openiap/src/main/java/dev/hyo/openiap/helpers/CommonHelpers.kt Add suspend helper for event
packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt Expose add/remove passthroughs
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt Add event enum + handler signature
packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueTest.kt Play handler presence test
packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueHorizonTest.kt Horizon handler throws test
packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionBillingIssueHorizonNoOpTest.kt Horizon no-op listener test
packages/google/openiap/build.gradle.kts Add testHorizon src + Robolectric deps
packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt Example billing-issue banner + deep link
packages/apple/Sources/OpenIapProtocol.swift Add Swift listener type + protocol method
packages/apple/Sources/OpenIapModule.swift Start Message listener + emit event
packages/apple/Sources/OpenIapModule+ObjC.swift ObjC bridge for new listener
packages/apple/Sources/Helpers/IapState.swift Store/snapshot new listeners
packages/apple/Sources/Models/Types.swift Mirror generated Types additions
packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift Fake module implements new API
packages/apple/Tests/OpenIapTests/VerifyPurchaseWithProviderTests.swift Fake module implements new API
packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift Example banner + listener wiring
libraries/react-native-iap/src/specs/RnIap.nitro.ts Nitro spec add/remove methods
libraries/react-native-iap/src/index.ts JS API subscriptionBillingIssueListener
libraries/react-native-iap/src/hooks/useIAP.ts Hook callback onSubscriptionBillingIssue
libraries/react-native-iap/src/types.ts Update generated TS types mirror
libraries/react-native-iap/src/tests/platform-detection.test.ts Mock new native methods
libraries/react-native-iap/src/tests/index.test.ts Mock new native methods
libraries/react-native-iap/src/tests/hooks/useIAP.test.ts Mock new native methods
libraries/react-native-iap/src/tests/hooks/useIAP.android.test.ts Mock new native methods
libraries/react-native-iap/ios/HybridRnIap.swift iOS bridge wiring + reconnect cleanup
libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt Android bridge wiring + listener list
libraries/react-native-iap/example/ios/exampleTests/SubscriptionBillingIssueReconnectTests.swift iOS reconnect regression test
libraries/react-native-iap/example/ios/exampleTests/Info.plist Add test bundle Info.plist
libraries/react-native-iap/example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme Scheme target id update
libraries/react-native-iap/example/ios/example.xcodeproj/project.pbxproj Add exampleTests target/project wiring
libraries/react-native-iap/example/ios/add_test_target.rb Helper script to add test target
libraries/react-native-iap/example/ios/Podfile Add test target + fmt workaround
libraries/expo-iap/src/types.ts Update generated TS types mirror
libraries/expo-iap/src/index.ts Add event enum + listener helper
libraries/expo-iap/ios/ExpoIapModule.swift Register new emitted event
libraries/expo-iap/ios/ExpoIapHelper.swift Wire OpenIap listener to Expo events
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt Register new emitted event
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt Wire OpenIap listener to Expo events
libraries/flutter_inapp_purchase/lib/types.dart Update generated Dart types mirror
libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart Add stream + method channel routing
libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift iOS listener to method channel
libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt Android listener to method channel
libraries/flutter_inapp_purchase/test/subscription_handlers_test.dart Flutter handler presence test
libraries/godot-iap/addons/godot-iap/types.gd Update generated GDScript types mirror
libraries/godot-iap/addons/godot-iap/godot_iap.gd Add new signal + handlers
libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt Emit new signal on Android
libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift Emit new signal on iOS
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt Add Flow to public KMP API
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt Update generated Kotlin types mirror
libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt Emit Flow + token dedupe
libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/SubscriptionBillingIssueDedupResetTest.kt Dedup reset regression test
libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt iOS flow + reconnect listener fix
packages/docs/src/pages/home.tsx Add quick links for APIs/events/types
packages/docs/src/pages/docs/index.tsx Add route + sidebar entry
packages/docs/src/pages/docs/features/subscription-billing-issue.tsx New feature guide page
packages/docs/src/pages/docs/events.tsx Document new event
packages/docs/src/pages/docs/updates/releases.tsx Add release note entry
packages/docs/public/llms.txt Refresh generated docs index stamp
knowledge/internal/sandbox-subscription-billing-issue.md Add sandbox QA procedure
knowledge/external/storekit2-api.md Update StoreKit2 external reference
knowledge/external/google-billing-api.md Add external payments program docs
knowledge/external/horizon-api.md Update Horizon API notes
knowledge/_claude-context/context.md Refresh generated repo context

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt (1)

666-703: ⚠️ Potential issue | 🟠 Major

Billing-issue emission is accidentally gated behind includeSuspendedAndroid.

notifySuspendedSubscriptions(all) only inspects whatever queryPurchasesAsync returned. Since setIncludeSuspended(true) is only called when the caller opts in via options?.includeSuspendedAndroid == true, the default getAvailablePurchases() path never retrieves suspended subscriptions. Consequently, subscriptionBillingIssueListener / subscriptionBillingIssue() can wait indefinitely unless the app explicitly passes an Android-only flag.

🩹 Always inspect for billing issues, but filter from returned list
             val all = mutableListOf<Purchase>()
             all += query(BillingClient.ProductType.INAPP, includeSuspendedSubs = false)
-            all += query(BillingClient.ProductType.SUBS, includeSuspendedSubs = includeSuspended)
-            notifySuspendedSubscriptions(all)
+
+            val subs = query(
+                BillingClient.ProductType.SUBS,
+                includeSuspendedSubs = includeSuspended
+            )
+            all += subs
+
+            val subsForBillingIssue = if (includeSuspended) {
+                subs
+            } else {
+                query(
+                    BillingClient.ProductType.SUBS,
+                    includeSuspendedSubs = true
+                )
+            }
+            notifySuspendedSubscriptions(subsForBillingIssue)
             all
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt`
around lines 666 - 703, The code currently only calls setIncludeSuspended when
options?.includeSuspendedAndroid is true, so notifySuspendedSubscriptions inside
getAvailablePurchasesHandler can never detect suspended subscriptions unless the
caller opted in; change logic to always request suspended subscriptions from the
BillingClient when querying SUBS (invoke the reflected setIncludeSuspended(true)
inside the query call for BillingClient.ProductType.SUBS regardless of options),
then, if options?.includeSuspendedAndroid is not true, filter out purchases with
isSuspendedAndroid==true from the returned list before adding to all; keep
notifySuspendedSubscriptions(all) working on the full list that includes
suspended entries so the subscriptionBillingIssueListener is always triggered.
🧹 Nitpick comments (3)
libraries/flutter_inapp_purchase/test/subscription_handlers_test.dart (1)

13-24: Test validates wiring; consider adding error-path coverage.

The smoke test guards against regression in the handler bundle wiring, which is its stated purpose. For comprehensive coverage per coding guidelines, consider adding tests for:

  • The error catch block in the 'subscription-billing-issue' handler (verifying debugPrint is called and stream doesn't break on malformed JSON)
  • Actual event emission through subscriptionBillingIssueListener

This can be addressed in a follow-up if the native bridge work is deferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/flutter_inapp_purchase/test/subscription_handlers_test.dart` around
lines 13 - 24, Add tests covering the error path and actual emission for the
subscription handlers: write a test that invokes the
'subscription-billing-issue' handler with malformed JSON and asserts that
debugPrint is called and the subscriptionBillingIssue stream does not close or
throw (reference the subscription-billing-issue handler and debugPrint), and add
a test that emits a well-formed event through the platform handler and verifies
subscriptionBillingIssueListener (or subscriptionBillingIssue stream from
FlutterInappPurchase.instance.subscriptionHandlers) receives the expected parsed
payload; locate logic around subscriptionHandlers, subscriptionBillingIssue, and
subscriptionBillingIssueListener to hook the platform handler and simulate both
malformed and valid event payloads.
libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt (1)

1698-1726: Move these methods into the Event Listener Methods section.

addSubscriptionBillingIssueListener / removeSubscriptionBillingIssueListener are event listener methods but are currently placed in the billing-program area. Please align placement with the class ordering rule.

As per coding guidelines libraries/react-native-iap/**/*.{swift,kt}: "Organize native implementation classes (Swift/Kotlin) in strict order: Properties and Initialization, Public Cross-platform Methods, Platform-specific Public Methods, Event Listener Methods, Private Helper Methods".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt`
around lines 1698 - 1726, The addSubscriptionBillingIssueListener and
removeSubscriptionBillingIssueListener methods are in the wrong section—move
both methods (and their helper attachSubscriptionBillingIssueIfNeeded and
related fields subscriptionBillingIssueAttached and the
openIap.addSubscriptionBillingIssueListener block) into the "Event Listener
Methods" section of HybridRnIap.kt so the class follows the prescribed ordering
(Properties/Init, Public Cross-platform, Platform-specific Public, Event
Listener Methods, Private Helper Methods); ensure you keep the synchronized
blocks and the attachSubscriptionBillingIssueIfNeeded implementation intact when
relocating.
libraries/react-native-iap/example/ios/Podfile (1)

90-99: Add a warning when the fmt header patch doesn't match the expected text.

Line 97 depends on the exact token '# define FMT_USE_CONSTEVAL 1' existing in the installed header. If fmt reflows that macro or ships an update with different formatting, pod install silently does nothing and the Xcode 26 build fails later without clear context. Add a check that warns if the header exists but the expected token is missing.

🧩 One way to make drift obvious
     if File.exist?(fmt_base)
       File.chmod(0644, fmt_base) rescue nil  # pod checkout is read-only
       src = File.read(fmt_base)
-      patched = src.gsub('#  define FMT_USE_CONSTEVAL 1', '#  define FMT_USE_CONSTEVAL 0  // Xcode 26 workaround')
+      expected = '#  define FMT_USE_CONSTEVAL 1'
+      unless src.include?(expected)
+        warn '[Podfile] fmt workaround did not match the installed header; revisit the Xcode 26 patch.'
+      end
+      patched = src.gsub(expected, '#  define FMT_USE_CONSTEVAL 0  // Xcode 26 workaround')
       File.write(fmt_base, patched) if patched != src
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libraries/react-native-iap/example/ios/Podfile` around lines 90 - 99, The
patch silently no-ops if the expected token '#  define FMT_USE_CONSTEVAL 1' is
absent; update the block around fmt_base/File.read that computes patched (and
uses gsub) to detect when File.exist?(fmt_base) is true but the source does not
contain the exact token, and emit a clear warning (e.g., via UI.warn or puts)
indicating the header was found but the expected macro line wasn't present so
the Xcode 26 workaround could not be applied; keep the existing File.chmod, gsub
and File.write behavior but add this additional conditional warning when patched
== src and src.scan for the token returns none to make drift obvious.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@knowledge/internal/sandbox-subscription-billing-issue.md`:
- Around line 53-57: The markdown blocks with nested code fences are missing
blank lines around the fenced code (triggering MD031); update the sections that
show "Console logs:" and the "logcat" block (the console text blocks showing "🔔
[MessageListener] billingIssue received..." and "D OpenIapModule:
onPurchasesUpdated...") to insert a blank line both immediately before the
opening ```text fence and immediately after the closing ``` fence (also apply
the same fix to the second occurrence around the 91-95 block) so each fenced
code block is separated by an empty line from surrounding paragraph text.

In
`@libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt`:
- Around line 1711-1725: The attachSubscriptionBillingIssueIfNeeded function can
race: check and set of subscriptionBillingIssueAttached happen unsynchronized
allowing double-registration; fix by performing the test-and-set and the call to
openIap.addSubscriptionBillingIssueListener inside a single synchronized block
(use the existing subscriptionBillingIssueListeners as the monitor or introduce
a dedicated lock object) so that subscriptionBillingIssueAttached is read, set
to true, and the listener is registered atomically; keep the existing listener
body (convertToNitroPurchase, snapshot copy, forEach) outside or inside the same
block as appropriate to avoid holding the lock during callbacks.

In `@libraries/react-native-iap/example/ios/example.xcodeproj/project.pbxproj`:
- Around line 42-43: The Foundation.framework PBXFileReference is hard-pinned to
iPhoneOS18.0.sdk; update the PBXFileReference entry for the identifier/name
"7CB695629D08A69F09E048C9" / "Foundation.framework" so it uses an
SDKROOT-relative reference like the JavaScriptCore entry does: set the path to
the SDK-relative System/Library/Frameworks/Foundation.framework and change
sourceTree to SDKROOT (instead of embedding the specific iPhoneOS18.0.sdk in the
path) so the project works across Xcode versions.

In `@libraries/react-native-iap/src/__tests__/index.test.ts`:
- Around line 32-33: Add unit/integration tests in
libraries/react-native-iap/src/__tests__/index.test.ts that exercise the new
subscriptionBillingIssueListener lifecycle: verify that
addSubscriptionBillingIssueListener attaches the native listener (spy on
addSubscriptionBillingIssueListener mock and simulate native emit to ensure the
JS handler is called), verify removeSubscriptionBillingIssueListener removes the
handler so subsequent native emits do not call JS, test that malformed payloads
emitted from native are dropped (no handler invocation and optionally log/error
path), and test reconnect/reset behavior by simulating a native reconnect/reset
sequence and ensuring the listener is re-attached or cleared appropriately;
target the functions addSubscriptionBillingIssueListener,
removeSubscriptionBillingIssueListener and the public
subscriptionBillingIssueListener event path and assert correct
attach/remove/invocation behavior and no-calls on malformed payloads.

In `@libraries/react-native-iap/src/index.ts`:
- Around line 640-646: The JS listener is added to
subscriptionBillingIssueJsListeners before attempting native registration, so if
addSubscriptionBillingIssueListener (called inside
tryAttachSubscriptionBillingIssueNative) throws a non-"Nitro not ready" error
the listener remains in the Set; update subscriptionBillingIssueListener to
remove the listener from subscriptionBillingIssueJsListeners when native
registration fails (catch the thrown error from
tryAttachSubscriptionBillingIssueNative/addSubscriptionBillingIssueListener,
rethrow the error after removing the listener), and preserve the
EventSubscription remove semantics so callers that never received a handle
cannot leak callbacks.

---

Outside diff comments:
In
`@libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt`:
- Around line 666-703: The code currently only calls setIncludeSuspended when
options?.includeSuspendedAndroid is true, so notifySuspendedSubscriptions inside
getAvailablePurchasesHandler can never detect suspended subscriptions unless the
caller opted in; change logic to always request suspended subscriptions from the
BillingClient when querying SUBS (invoke the reflected setIncludeSuspended(true)
inside the query call for BillingClient.ProductType.SUBS regardless of options),
then, if options?.includeSuspendedAndroid is not true, filter out purchases with
isSuspendedAndroid==true from the returned list before adding to all; keep
notifySuspendedSubscriptions(all) working on the full list that includes
suspended entries so the subscriptionBillingIssueListener is always triggered.

---

Nitpick comments:
In `@libraries/flutter_inapp_purchase/test/subscription_handlers_test.dart`:
- Around line 13-24: Add tests covering the error path and actual emission for
the subscription handlers: write a test that invokes the
'subscription-billing-issue' handler with malformed JSON and asserts that
debugPrint is called and the subscriptionBillingIssue stream does not close or
throw (reference the subscription-billing-issue handler and debugPrint), and add
a test that emits a well-formed event through the platform handler and verifies
subscriptionBillingIssueListener (or subscriptionBillingIssue stream from
FlutterInappPurchase.instance.subscriptionHandlers) receives the expected parsed
payload; locate logic around subscriptionHandlers, subscriptionBillingIssue, and
subscriptionBillingIssueListener to hook the platform handler and simulate both
malformed and valid event payloads.

In
`@libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt`:
- Around line 1698-1726: The addSubscriptionBillingIssueListener and
removeSubscriptionBillingIssueListener methods are in the wrong section—move
both methods (and their helper attachSubscriptionBillingIssueIfNeeded and
related fields subscriptionBillingIssueAttached and the
openIap.addSubscriptionBillingIssueListener block) into the "Event Listener
Methods" section of HybridRnIap.kt so the class follows the prescribed ordering
(Properties/Init, Public Cross-platform, Platform-specific Public, Event
Listener Methods, Private Helper Methods); ensure you keep the synchronized
blocks and the attachSubscriptionBillingIssueIfNeeded implementation intact when
relocating.

In `@libraries/react-native-iap/example/ios/Podfile`:
- Around line 90-99: The patch silently no-ops if the expected token '#  define
FMT_USE_CONSTEVAL 1' is absent; update the block around fmt_base/File.read that
computes patched (and uses gsub) to detect when File.exist?(fmt_base) is true
but the source does not contain the exact token, and emit a clear warning (e.g.,
via UI.warn or puts) indicating the header was found but the expected macro line
wasn't present so the Xcode 26 workaround could not be applied; keep the
existing File.chmod, gsub and File.write behavior but add this additional
conditional warning when patched == src and src.scan for the token returns none
to make drift obvious.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f87ca44-2bb0-41f8-b4ef-12131d369a9d

📥 Commits

Reviewing files that changed from the base of the PR and between 79c0a15 and 7548934.

📒 Files selected for processing (39)
  • knowledge/_claude-context/context.md
  • knowledge/external/storekit2-api.md
  • knowledge/internal/sandbox-subscription-billing-issue.md
  • libraries/expo-iap/src/index.ts
  • libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart
  • libraries/flutter_inapp_purchase/test/subscription_handlers_test.dart
  • libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt
  • libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt
  • libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/SubscriptionBillingIssueDedupResetTest.kt
  • libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
  • libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
  • libraries/react-native-iap/example/ios/Podfile
  • libraries/react-native-iap/example/ios/add_test_target.rb
  • libraries/react-native-iap/example/ios/example.xcodeproj/project.pbxproj
  • libraries/react-native-iap/example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme
  • libraries/react-native-iap/example/ios/exampleTests/Info.plist
  • libraries/react-native-iap/example/ios/exampleTests/SubscriptionBillingIssueReconnectTests.swift
  • libraries/react-native-iap/ios/HybridRnIap.swift
  • libraries/react-native-iap/src/__tests__/hooks/useIAP.android.test.ts
  • libraries/react-native-iap/src/__tests__/hooks/useIAP.test.ts
  • libraries/react-native-iap/src/__tests__/index.test.ts
  • libraries/react-native-iap/src/__tests__/platform-detection.test.ts
  • libraries/react-native-iap/src/hooks/useIAP.ts
  • libraries/react-native-iap/src/index.ts
  • packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift
  • packages/apple/Sources/OpenIapModule.swift
  • packages/docs/public/llms-full.txt
  • packages/docs/public/llms.txt
  • packages/docs/src/pages/docs/events.tsx
  • packages/docs/src/pages/docs/features/subscription-billing-issue.tsx
  • packages/docs/src/pages/docs/updates/releases.tsx
  • packages/docs/src/pages/home.tsx
  • packages/google/openiap/build.gradle.kts
  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
  • packages/google/openiap/src/main/java/dev/hyo/openiap/helpers/CommonHelpers.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
  • packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionBillingIssueHorizonNoOpTest.kt
  • packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueHorizonTest.kt
  • packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueTest.kt
✅ Files skipped from review due to trivial changes (12)
  • libraries/react-native-iap/example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme
  • libraries/react-native-iap/src/tests/hooks/useIAP.test.ts
  • packages/docs/public/llms.txt
  • libraries/react-native-iap/src/tests/hooks/useIAP.android.test.ts
  • libraries/react-native-iap/src/tests/platform-detection.test.ts
  • libraries/react-native-iap/example/ios/exampleTests/Info.plist
  • packages/docs/src/pages/home.tsx
  • packages/docs/src/pages/docs/features/subscription-billing-issue.tsx
  • packages/docs/src/pages/docs/events.tsx
  • libraries/expo-iap/src/index.ts
  • packages/docs/src/pages/docs/updates/releases.tsx
  • packages/docs/public/llms-full.txt
🚧 Files skipped from review as they are similar to previous changes (6)
  • libraries/react-native-iap/src/hooks/useIAP.ts
  • packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift
  • libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
  • libraries/react-native-iap/ios/HybridRnIap.swift
  • knowledge/external/storekit2-api.md
  • packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt

Comment thread knowledge/internal/sandbox-subscription-billing-issue.md
Comment thread libraries/react-native-iap/example/ios/example.xcodeproj/project.pbxproj Outdated
Comment thread libraries/react-native-iap/src/__tests__/index.test.ts
Comment thread libraries/react-native-iap/src/index.ts Outdated
hyochan and others added 3 commits April 16, 2026 10:52
…x testing

Log all incoming StoreKit messages (not just billingIssue) and listener
lifecycle events so sandbox testers can verify whether the listener is
active and what messages arrive during renewal cycles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- index.ts: clean up JS listener from Set when native attach throws
- HybridRnIap.kt: synchronize attach-guard with lock to prevent
  double-registration of native billing-issue listener
- Play OpenIapModule.kt: stop clearing subscriptionBillingIssueListeners
  on endConnection — only clear dedupe tokens for consistency with
  purchaseUpdate/purchaseError listener lifecycle
- project.pbxproj: use SDKROOT instead of SDK-versioned path for
  Foundation.framework
- sandbox-subscription-billing-issue.md: MD031 blank lines around fences
- index.test.ts: behavioral tests for subscriptionBillingIssueListener
  (attach, remove, cleanup on native failure)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the outdated App Store Connect "Billing Issue toggle" approach
with the device-side method: Settings → Developer → Sandbox Account →
Manage → disable "Allow Purchases & Renewals" (iOS 16+).

Reference: https://developer.apple.com/documentation/storekit/testing-failing-subscription-renewals-and-in-app-purchases

Also fix MD031 blank-line warnings and ordered list numbering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new cross-platform subscriptionBillingIssue event that normalizes StoreKit 2 billing-issue messages (iOS 18+/Mac Catalyst 18+) and Google Play suspended subscriptions (Billing 8.1+) into a single listener surface across the core SDK, schema/types, downstream libraries, examples, and docs.

Changes:

  • Extend the GraphQL schema + regenerate all platform type outputs to include IapEvent.SubscriptionBillingIssue and Subscription.subscriptionBillingIssue.
  • Implement native emit/listener plumbing on iOS (StoreKit Message.messages) and Android Play flavor (Purchase.isSuspended + session dedupe), with Horizon parity APIs as no-op/unsupported.
  • Wire the event through RN / Expo / Flutter / Godot / KMP bridges + examples + docs and add regression tests.

Reviewed changes

Copilot reviewed 70 out of 75 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/gql/src/type.graphql Adds new IapEvent.SubscriptionBillingIssue enum value and schema docstring.
packages/gql/src/event.graphql Adds subscriptionBillingIssue: Purchase! subscription field.
packages/gql/src/generated/types.ts Regenerated TS types including new event + subscription field.
packages/gql/src/generated/types.gd Regenerated GDScript types/mappings for new event.
packages/gql/src/generated/types.dart Regenerated Dart types including new event + handler bundle wiring.
packages/gql/src/generated/Types.swift Regenerated Swift types including new event + handler bundle wiring.
packages/gql/src/generated/Types.kt Regenerated Kotlin types including new event + handler bundle wiring.
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt Play implementation: listener registration, suspended detection, session dedupe, emit hook.
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt Horizon implementation: handler throws unsupported; add/remove listener are explicit no-ops with warnings.
packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt Adds new listener methods to the public protocol.
packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt Introduces OpenIapSubscriptionBillingIssueListener interface + docs.
packages/google/openiap/src/main/java/dev/hyo/openiap/helpers/CommonHelpers.kt Adds onSubscriptionBillingIssue() suspend helper.
packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt Exposes store-level add/remove passthrough for the new listener.
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt Regenerated Kotlin types mirror for main package.
packages/google/openiap/src/testPlay/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueTest.kt Play test: asserts handler bundle wiring exists.
packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueHorizonTest.kt Horizon test: asserts handler exists and throws FeatureNotSupported.
packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionBillingIssueHorizonNoOpTest.kt Horizon test: listener add/remove are no-ops and never invoke callback.
packages/google/openiap/build.gradle.kts Adds Robolectric + test source set wiring for Horizon JVM tests.
packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt Example UI: banner + listener registration + deep-link action.
packages/apple/Sources/OpenIapProtocol.swift Adds subscriptionBillingIssueListener to the protocol.
packages/apple/Sources/OpenIapModule.swift iOS implementation: StoreKit Message.messages loop + entitlement scan + emit.
packages/apple/Sources/OpenIapModule+ObjC.swift ObjC bridge for addSubscriptionBillingIssueListener.
packages/apple/Sources/Helpers/IapState.swift Tracks subscription billing-issue listeners in actor state.
packages/apple/Sources/Models/Types.swift Regenerated local Swift model types mirror for new event + handler.
packages/apple/Tests/OpenIapTests/VerifyPurchaseTests.swift Updates test fake to implement new protocol method.
packages/apple/Tests/OpenIapTests/VerifyPurchaseWithProviderTests.swift Updates test fake to implement new protocol method.
packages/apple/Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift Example UI: banner + listener lifecycle management.
libraries/react-native-iap/src/specs/RnIap.nitro.ts Adds Nitro spec methods for add/remove subscription billing issue listener.
libraries/react-native-iap/src/index.ts Adds JS API subscriptionBillingIssueListener() and internal listener management.
libraries/react-native-iap/src/hooks/useIAP.ts Adds onSubscriptionBillingIssue hook option + listener wiring/cleanup.
libraries/react-native-iap/src/types.ts Regenerated TS types mirror with new event + subscription field.
libraries/react-native-iap/src/tests/platform-detection.test.ts Updates mocks to include new native methods.
libraries/react-native-iap/src/tests/index.test.ts Adds tests for subscriptionBillingIssueListener.
libraries/react-native-iap/src/tests/hooks/useIAP.test.ts Updates hook mocks to include new native methods.
libraries/react-native-iap/src/tests/hooks/useIAP.android.test.ts Updates Android hook mocks to include new native methods.
libraries/react-native-iap/ios/HybridRnIap.swift iOS Nitro bridge: attach/detach + listener list management.
libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt Android Nitro bridge: listener list + native attach path for billing-issue events.
libraries/react-native-iap/example/ios/exampleTests/SubscriptionBillingIssueReconnectTests.swift Adds iOS regression test for reconnect behavior using reflection.
libraries/react-native-iap/example/ios/exampleTests/Info.plist Adds Info.plist for new XCTest bundle target.
libraries/react-native-iap/example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme Scheme tweak to include the new test target.
libraries/react-native-iap/example/ios/example.xcodeproj/project.pbxproj Adds exampleTests target + sources/framework wiring.
libraries/react-native-iap/example/ios/add_test_target.rb One-shot script to create the test target (project maintenance).
libraries/react-native-iap/example/ios/Podfile Adds CocoaPods target for exampleTests.
libraries/expo-iap/src/index.ts Adds OpenIapEvent.SubscriptionBillingIssue + JS listener helper.
libraries/expo-iap/src/types.ts Regenerated TS types mirror with new event + subscription field.
libraries/expo-iap/ios/ExpoIapModule.swift Registers the new event name for Expo module events.
libraries/expo-iap/ios/ExpoIapHelper.swift Wires OpenIAP listener to Expo event emitter.
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt Registers new Android event name and buffering paths.
libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt Adds native subscription billing-issue listener emission to JS.
libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart Adds Dart stream + method-channel handling + handler bundle wiring.
libraries/flutter_inapp_purchase/lib/types.dart Regenerated Dart types mirror with new event + subscription field.
libraries/flutter_inapp_purchase/test/subscription_handlers_test.dart Test ensuring handler bundle exposes subscriptionBillingIssue.
libraries/flutter_inapp_purchase/ios/Classes/FlutterInappPurchasePlugin.swift iOS listener wiring + method-channel emission.
libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt Android listener wiring + method-channel emission.
libraries/godot-iap/addons/godot-iap/types.gd Regenerated GDScript types mirror with new event + mapping.
libraries/godot-iap/addons/godot-iap/godot_iap.gd Adds new signal + platform hookups to propagate event to GDScript.
libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt Android plugin: add/remove listener and emit signal payload.
libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift iOS extension: add signal + listener wiring + payload conversion.
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/KmpIap.kt Adds subscriptionBillingIssueListener: Flow<Purchase> to KMP API.
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt Regenerated common types mirror including new event + handler.
libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt iOS wiring: OpenIAP listener -> shared flow + reconnect handling.
libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt Android wiring: suspended detection + dedupe + flow emission.
libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/SubscriptionBillingIssueDedupResetTest.kt Android unit test guarding dedupe reset on endConnection.
packages/docs/src/pages/docs/features/subscription-billing-issue.tsx Adds a dedicated feature guide page (behavior table, usage snippets, deduping).
packages/docs/src/pages/docs/events.tsx Documents the new event in the events reference page.
packages/docs/src/pages/docs/index.tsx Adds route + sidebar entry for the new feature page.
packages/docs/src/pages/docs/updates/releases.tsx Adds release note entry for the new event.
packages/docs/src/pages/home.tsx Adds home page quick links related to APIs/events/types.
packages/docs/public/llms.txt Regenerated llms.txt timestamp/content header.
knowledge/internal/sandbox-subscription-billing-issue.md Adds internal manual sandbox E2E verification guide.
knowledge/external/storekit2-api.md Updates external StoreKit2 reference (Message API and other drift fixes).
knowledge/external/google-billing-api.md Updates external Google Billing reference (External Payments 8.3+).
knowledge/external/horizon-api.md Updates Horizon reference (hyphenation + accessToken docs).
knowledge/_claude-context/context.md Regenerated repo context snapshot.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/docs/src/pages/docs/features/subscription-billing-issue.tsx Outdated
Comment thread libraries/react-native-iap/ios/HybridRnIap.swift Outdated
- HybridRnIap.kt: track native listener instance and remove on
  endConnection to prevent duplicate registration on reconnect
- Horizon test: wrap handler invocation in withTimeout(5s) to prevent
  CI hang if regression suspends forever
- subscription-billing-issue.tsx: separate react-native-iap and expo-iap
  import examples for copy-paste correctness
- OpenIapListener.kt: update KDoc to match actual semantics — fires once
  per session per purchaseToken, including already-suspended subscriptions
- kmp-iap: always query subs with suspended=true for billing-issue
  notifier, then filter returned list based on caller preference
- HybridRnIap.swift: align removeSubscriptionBillingIssueListener with
  other remove*Listener methods (removeAll pattern)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@knowledge/internal/sandbox-subscription-billing-issue.md`:
- Around line 69-70: The macOS bullet currently groups all macOS builds into a
silent no-op which conflicts with the earlier note that "Mac Catalyst 18+ is
supported"; update the macOS bullet in the list to exclude Catalyst by renaming
it to "native macOS (non-Catalyst)" or explicitly add a separate exception line
for "Mac Catalyst (Catalyst 18+) — supported", so the bullets read e.g. "native
macOS (non-Catalyst) build — silent no-op by design (StoreKit.Message API is
iOS-only)" and/or add "Mac Catalyst 18+ — supported" to avoid contradiction with
the existing Mac Catalyst statement.

In `@packages/apple/Sources/OpenIapModule.swift`:
- Around line 67-69: Currently initConnection() calls startMessageListener()
unconditionally which begins iterating StoreKit.Message.messages and can
suppress Apple's native billing sheet; change this so the StoreKit message loop
is opt-in: remove the unconditional call from initConnection() and instead start
startMessageListener() only when the app registers a
subscriptionBillingIssueListener (i.e., on the first
subscriptionBillingIssueListener registration callback/registration method), or
implement a fallback inside startMessageListener() that, when no custom handler
is installed, calls message.display(in:) (or DisplayMessageAction) to show the
native sheet; ensure the logic references startMessageListener(),
initConnection(), and the subscriptionBillingIssueListener registration path and
guards on the iOS 18+/Mac Catalyst 18+ behavior.

In `@packages/docs/src/pages/docs/features/subscription-billing-issue.tsx`:
- Around line 206-210: The doc paragraph incorrectly claims a purchase will
"re-emit" after recovery in the same session; clarify that Android's native
layer deduplicates by purchaseToken and only clears that set on
disconnect/reconnect, so update the wording in the paragraph referencing
getAvailablePurchases and purchaseToken to say the event will re-emit only after
a reconnect or app restart (or when the native session resets), not simply after
recovery in the same session.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e742fa27-ce08-40eb-86fa-ce046683b958

📥 Commits

Reviewing files that changed from the base of the PR and between 7548934 and 5e5bfdf.

📒 Files selected for processing (12)
  • knowledge/internal/sandbox-subscription-billing-issue.md
  • libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt
  • libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt
  • libraries/react-native-iap/example/ios/example.xcodeproj/project.pbxproj
  • libraries/react-native-iap/ios/HybridRnIap.swift
  • libraries/react-native-iap/src/__tests__/index.test.ts
  • libraries/react-native-iap/src/index.ts
  • packages/apple/Sources/OpenIapModule.swift
  • packages/docs/src/pages/docs/features/subscription-billing-issue.tsx
  • packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
  • packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueHorizonTest.kt
✅ Files skipped from review due to trivial changes (2)
  • libraries/react-native-iap/src/tests/index.test.ts
  • libraries/react-native-iap/example/ios/example.xcodeproj/project.pbxproj
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/google/openiap/src/main/java/dev/hyo/openiap/listener/OpenIapListener.kt
  • libraries/react-native-iap/ios/HybridRnIap.swift
  • packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/SubscriptionHandlersBillingIssueHorizonTest.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt

Comment thread knowledge/internal/sandbox-subscription-billing-issue.md Outdated
Comment thread packages/apple/Sources/OpenIapModule.swift Outdated
Comment thread packages/docs/src/pages/docs/features/subscription-billing-issue.tsx Outdated
hyochan and others added 2 commits April 16, 2026 12:15
- apple: make Message.messages listener opt-in — only start when
  subscriptionBillingIssueListener is registered, not on initConnection.
  Prevents suppression of Apple's native billing-issue sheet for apps
  that don't use this event.
- sandbox doc: exclude Mac Catalyst from silent no-op list (it's
  supported via iOS 18+ / Mac Catalyst 18+)
- feature docs: clarify Android dedupe re-emission only happens after
  endConnection/app restart, not within the same session

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hyochan hyochan merged commit f2caa3b into main Apr 16, 2026
14 checks passed
@hyochan hyochan deleted the feat/gql-apple-google-subscription-billing-issue branch April 16, 2026 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📖 documentation Improvements or additions to documentation 🎯 feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants