-
-
Notifications
You must be signed in to change notification settings - Fork 8
feat: add cross-platform subscriptionBillingIssue event #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
hyochan
merged 21 commits into
main
from
feat/gql-apple-google-subscription-billing-issue
Apr 16, 2026
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
2b9b54c
feat: add cross-platform subscriptionBillingIssue event
hyochan 787d04a
fix(kmp-iap): override subscriptionBillingIssue + add cross-platform …
hyochan e2b60cf
fix(review): address PR 99 review comments
hyochan f4d360c
feat(rn-iap): wire subscriptionBillingIssue listener (Nitro + JS + iO…
hyochan 1dae662
feat(expo-iap): wire subscriptionBillingIssue event (JS + iOS + Android)
hyochan 2f4fb1c
feat(flutter): wire subscriptionBillingIssue stream (Dart + iOS + And…
hyochan 2adeabc
feat(godot-iap): wire subscription_billing_issue signal (GDScript + A…
hyochan 0133b8a
feat(kmp-iap): wire iOS subscriptionBillingIssue Flow via cinterop
hyochan f98c479
docs(docs-site): add Subscription Billing Issue feature page
hyochan 79c0a15
feat(examples,hook): onSubscriptionBillingIssue hook + example banners
hyochan 8633c3b
test,docs: Horizon no-op Robolectric test + sandbox E2E guide
hyochan ca20da6
fix(apple-example): unwrap iOS variant from Purchase in billing-issue…
hyochan 592308e
fix(review): address 24 PR 99 review threads (Copilot + CodeRabbit)
hyochan 94c77a8
test(rn-iap): add addSubscriptionBillingIssueListener to jest mocks
hyochan 7548934
fix: address 4 codex review findings + tests + docs home expansion
hyochan 1d6109d
fix(apple): add debug logging to Message.messages listener for sandbo…
hyochan 488c3e7
fix(review): address 6 unresolved PR 99 review threads
hyochan 24b4fa9
docs: update sandbox billing-issue guide with Apple's official method
hyochan 5e5bfdf
fix(review): address 6 new PR 99 review threads
hyochan 04fe84b
fix(review): address 3 PR 99 review threads
hyochan e0d6335
style(docs): prettier format subscription-billing-issue.tsx
hyochan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
knowledge/internal/sandbox-subscription-billing-issue.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| --- | ||
| title: Sandbox E2E — subscriptionBillingIssue | ||
| audience: contributors, release QA | ||
| --- | ||
|
|
||
| # Sandbox E2E: `subscriptionBillingIssue` | ||
|
|
||
| The `subscriptionBillingIssue` event requires live store signals that cannot be produced from a unit-test JVM. This document captures the exact sandbox procedure for both platforms so any reviewer can reproduce. | ||
|
|
||
| All code paths verified by local compile + Horizon Robolectric unit test: | ||
|
|
||
| ```bash | ||
| cd packages/google | ||
| ./gradlew :openiap:compilePlayDebugKotlin | ||
| ./gradlew :openiap:compileHorizonDebugKotlin | ||
| ./gradlew :openiap:testHorizonDebugUnitTest # Robolectric no-op assertion | ||
|
|
||
| cd ../apple | ||
| swift build && swift test # 87 tests | ||
|
|
||
| cd ../../libraries/kmp-iap | ||
| ./gradlew :library:compileDebugKotlinAndroid | ||
|
|
||
| cd ../react-native-iap && yarn typecheck | ||
| cd ../expo-iap && bun run tsc --noEmit | ||
| cd ../flutter_inapp_purchase && flutter analyze | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## iOS (StoreKit 2 sandbox) | ||
|
|
||
| **Prereqs** | ||
|
|
||
| - Physical iOS device running **iOS 18.0 or later** (the `Message.Reason.billingIssue` API is iOS 18+ / Mac Catalyst 18+; the iOS Simulator does not deliver StoreKit Messages). | ||
| - A sandbox Apple ID enrolled in App Store Connect → Users and Access → Sandbox Testers. | ||
| - An auto-renewable subscription product configured on App Store Connect, and the Example project's `subscriptionIds` list pointing at it (`dev.hyo.martie.premium` by default). | ||
|
|
||
| **Step-by-step** | ||
|
|
||
| 1. Sign the device out of its production Apple ID. Sign the sandbox tester into **Settings → App Store → Sandbox Account**. | ||
| 2. Open the Example app: | ||
| - `packages/apple/Example/OpenIapExample.xcodeproj` — run the `OpenIapExample` scheme. | ||
| 3. In-app: navigate to the **Subscription Flow** screen and subscribe to `dev.hyo.martie.premium`. | ||
| 4. Force a billing issue on the **device** (requires iOS 16+ / iPadOS 16+): | ||
| - Go to **Settings → Developer → Sandbox Account → Manage → Account Settings**. | ||
| - Disable the **Allow Purchases & Renewals** setting. | ||
| - This causes all in-app purchases to fail and auto-renewable subscriptions to stop renewing. | ||
| - The setting applies to all devices the sandbox account signs in to. | ||
| - Reference: <https://developer.apple.com/documentation/storekit/testing-failing-subscription-renewals-and-in-app-purchases#Configure-the-sandbox-environment-to-simulate-billing-issues>. | ||
| 5. Wait for the next renewal cycle (Renewal Rate = 5 minutes → wait ~5 min). The renewal fails, and StoreKit delivers `Message.Reason.billingIssue` when the app is in the foreground. | ||
| 6. To simulate the user fixing the issue, re-enable **Allow Purchases & Renewals**. | ||
| 7. Expected UI: the orange "Subscription needs attention" banner appears at the top of the Subscription Flow screen. Tapping **Fix payment method** opens `SKPaymentQueue` / `showManageSubscriptions`. | ||
|
|
||
| **What success looks like** | ||
|
|
||
| - Console logs: | ||
|
|
||
| ```text | ||
| 🔔 [MessageListener] billingIssue received | ||
| Emitting subscriptionBillingIssue: dev.hyo.martie.premium | ||
| ``` | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| - Banner visible on `SubscriptionFlowScreen`. | ||
| - `Transaction.currentEntitlements` shows the affected subscription in `.inBillingRetryPeriod` or `.inGracePeriod`. | ||
|
|
||
| **If nothing fires** | ||
|
|
||
| - iOS < 18 — silent no-op by design (confirm with `#available` trace in logs). | ||
| - tvOS / watchOS / native macOS (non-Catalyst) / visionOS build — silent no-op by design (StoreKit.Message API is iOS / Mac Catalyst only). | ||
| - App not foregrounded when the message is posted — StoreKit delivers on next `Message.messages` await; bring the app to foreground. | ||
|
|
||
| --- | ||
|
|
||
| ## Android (Play Billing 8.1+ sandbox) | ||
|
|
||
| **Prereqs** | ||
|
|
||
| - Physical Android device (or emulator with Play Store) running the Play flavor of the Example app: | ||
| `packages/google/Example` → run with product flavor **play**. | ||
| - A Play Console sandbox tester account on the device. | ||
| - A subscription product configured in the Play Console, matching `subscriptionSkus` in `SubscriptionFlowScreen.kt`. | ||
|
|
||
| **Step-by-step** | ||
|
|
||
| 1. Install the Example APK (`./gradlew :Example:installPlayDebug`). | ||
| 2. Sign in with the sandbox tester account in the Play Store app. | ||
| 3. Subscribe to a test subscription in the Example app. | ||
| 4. Force a suspension: | ||
| - In the **Google Play Store → Payment methods**, remove all payment methods for the sandbox account, OR | ||
| - Use Play Console → **Subscriptions → Test suspensions** (requires appropriate Play Console role). Reference: <https://developer.android.com/google/play/billing/subscriptions#suspended>. | ||
| 5. Wait for Play's renewal cycle. When Play suspends the subscription, the next `getAvailablePurchases` or `onPurchasesUpdated` will include the purchase with `isSuspended == true`. | ||
| 6. Return to the Example app. The banner fires once per session per affected purchase (deduped by `purchaseToken`). | ||
|
|
||
| **What success looks like** | ||
|
|
||
| - `logcat` shows: | ||
|
|
||
| ```text | ||
| D OpenIapModule: onPurchasesUpdated isSuspended=true ... | ||
| D Example: subscriptionBillingIssue fired for sku=... | ||
| ``` | ||
|
|
||
| - Banner visible on `SubscriptionFlowScreen`. | ||
| - Tapping **Fix payment method** launches `deepLinkToSubscriptions` which routes to Play's subscription center. | ||
|
|
||
| **Horizon flavor (do NOT attempt)** | ||
|
|
||
| - The Horizon flavor's `addSubscriptionBillingIssueListener` is a documented no-op. Verified by | ||
| `SubscriptionBillingIssueHorizonNoOpTest` (Robolectric, runs on CI). There is no sandbox path on Horizon because the Billing Compatibility SDK 1.1.1 targets Play Billing 7.0 which does not expose `Purchase.isSuspended`. | ||
|
|
||
| --- | ||
|
|
||
| ## Cross-library smoke (optional) | ||
|
|
||
| Use `libraries-versions.jsonc` to point example apps at the local monorepo sources (already `"local"` by default), then verify each downstream library surfaces the event: | ||
|
|
||
| | Library | Check | | ||
| |---------|-------| | ||
| | react-native-iap | `useIAP({ onSubscriptionBillingIssue: p => console.log(p) })` fires the callback. `subscriptionBillingIssueListener()` also fires independently. | | ||
| | expo-iap | `subscriptionBillingIssueListener((p) => console.log(p))` fires via expo event emitter. | | ||
| | flutter_inapp_purchase | `iap.subscriptionBillingIssueListener.listen(...)` emits the Purchase. | | ||
| | godot-iap | `godot_iap.subscription_billing_issue.connect(...)` emits the Dictionary payload. | | ||
| | kmp-iap | `kmpIapInstance.subscriptionBillingIssueListener.collect {...}` emits in the Flow. | | ||
|
|
||
| --- | ||
|
|
||
| ## Automated coverage matrix | ||
|
|
||
| | Layer | Mechanism | Status | | ||
| |-------|-----------|--------| | ||
| | Horizon no-op guarantee | Robolectric unit test (`SubscriptionBillingIssueHorizonNoOpTest`) | Runs on CI | | ||
| | Play-flavor compile of listener surface | `compilePlayDebugKotlin` | Runs on CI | | ||
| | Apple Swift test fakes implement protocol | `swift test` | Runs on CI | | ||
| | Downstream types synced | Gen check by each library's typecheck task | Runs on CI | | ||
| | Live sandbox behavior (iOS 18 message + Play suspended) | Manual, this document | Release QA | | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.