fix(google,flutter): surface BillingResult debugMessage to callers#98
Conversation
OpenIapError.DeveloperError and PurchaseFailed were `object` singletons with hardcoded messages, so Google Play's raw `BillingResult.debugMessage` returned from launchBillingFlow / onPurchasesUpdated had nowhere to live — fromBillingResponseCode already took the string as a parameter but dropped it on the floor. That left callers (e.g. Flutter investigating a DEFERRED subscription replacement DEVELOPER_ERROR) with only the generic "Invalid arguments provided to the API" text and no way to see which argument Play actually rejected without adb. Changes: - Make DeveloperError and PurchaseFailed data classes that carry an optional `debugMessage: String?`. OpenIapError.toJSON() now emits a `debugMessage` field so downstream framework libraries receive it. - fromBillingResponseCode (both play and horizon flavors) constructs `DeveloperError(debugMessage)` so Play's raw message reaches the JSON payload. - The play and horizon launchBillingFlow sync-failure paths now map DEVELOPER_ERROR to `DeveloperError(result.debugMessage)` (matching the onPurchasesUpdated async path) and other non-OK codes to `PurchaseFailed(result.debugMessage)`. - Flutter's convertToPurchaseError forwards `result.responseCode` and `result.debugMessage` through to `PurchaseError`; previously only `code` and `message` were copied, so even though PurchaseError has these fields they were always null. A helpers_unit_test locks this wiring in. Call-site updates: OpenIapError.DeveloperError / .PurchaseFailed are no longer singleton values, so references like `throw OpenIapError.DeveloperError` become `throw OpenIapError.DeveloperError()`. Tests updated accordingly; companion `.CODE` / `.MESSAGE` accesses and `is OpenIapError.DeveloperError` type checks keep working as before. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
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 35 minutes and 2 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughForwarded billing-layer debug messages and response codes into PurchaseError/OpenIapError across platforms; added Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request enhances error reporting by surfacing debugMessage and responseCode from the native billing layer through to the Flutter implementation. Key changes include refactoring PurchaseFailed and DeveloperError from objects to classes to carry diagnostic data, updating the Android method channel logic, and adding comprehensive unit tests and release documentation. Feedback was provided to implement these new error types as data classes to provide standard utility methods and adhere to the repository's coding style.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt (1)
25-30: Cover the mapping path too, not just the constructors.These assertions prove the new classes serialize
debugMessage, but the production async flow goes throughfromBillingResponseCode(...). A focused test likefromBillingResponseCode(DEVELOPER_ERROR, "x")would catch regressions in the actual forwarding path.Also applies to: 196-200
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt` around lines 25 - 30, The test currently asserts that the PurchaseFailed constructor preserves debugMessage but misses exercising the real production mapping; update the test to call OpenIapError.fromBillingResponseCode(DEVELOPER_ERROR, "x") (using the same DEVELOPER_ERROR value used in production) and assert that the returned error has debugMessage == "x" and that error.toJSON()["debugMessage"] == "x"; do the same for the other affected test block (lines ~196-200) to cover mapping paths rather than only constructors.
🤖 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/flutter_inapp_purchase/lib/helpers.dart`:
- Around line 408-413: The returned iap_err.PurchaseError is missing the
resolved platform, so forward the local platform value into the constructor
(e.g., add platform: platform) when creating the PurchaseError in this helper;
ensure the local variable named platform (resolved earlier in the function) is
used and that the PurchaseError constructor signature accepts a platform
parameter so error.platform/getPlatformCode() will work for
purchaseErrorListener consumers.
In `@packages/docs/src/pages/docs/updates/releases.tsx`:
- Around line 26-195: The new release entry (id
"subscription-replacement-and-debug-message-2026-04-15") is not
Prettier-formatted and is failing CI; re-run Prettier on
packages/docs/src/pages/docs/updates/releases.tsx (or run your repo's format
script) and apply the formatting fixes for the JSX block containing the <div
key="subscription-replacement-and-debug-message-2026-04-15"
style={noteCardStyle}>, the AnchorLink element, and all inline style objects and
nested JSX so the file passes `prettier --check` (fix spacing, wrapping and
trailing commas as Prettier requires) before pushing.
In `@packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt`:
- Around line 36-43: onPurchasesUpdated now forwards result.debugMessage into
fromBillingResponseCode(...), but most returned OpenIapError singletons (e.g.,
the branches that return ERROR, SERVICE_UNAVAILABLE, ITEM_UNAVAILABLE, etc.) do
not preserve that diagnostic text because only PurchaseFailed and DeveloperError
have debugMessage fields; update fromBillingResponseCode(onPurchasesUpdated) to
accept and forward a debugMessage param and change the async-mapped variants to
construct per-call instances that set debugMessage (or add a debug-capable
wrapper) instead of returning singletons—modify the factory used in
fromBillingResponseCode, plus the classes referenced (PurchaseFailed,
DeveloperError and the other OpenIapError branches around the same area and the
async mappings at lines ~246-253) so every returned OpenIapError can carry the
provided debugMessage.
In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt`:
- Around line 1135-1136: In finishTransaction where you check "if
(result.responseCode != BillingClient.BillingResponseCode.OK)" include Play's
diagnostic text by passing the BillingResult.debugMessage into the thrown error
instead of throwing a bare OpenIapError.PurchaseFailed(); update the throw to
construct or wrap OpenIapError.PurchaseFailed with result.debugMessage (or
create an overload/variant that accepts a message) so consume/acknowledge
failure paths preserve the BillingResult.debugMessage for callers.
- Around line 558-566: The code resumes the coroutine with a bare
OpenIapError.PurchaseFailed() in multiple places (inside the catch block and the
else branch after the createBillingProgramReportingDetails() failure) losing the
underlying debug message; update the resumeWithException calls to pass a
PurchaseFailed that includes the original error reason (e.g.,
result?.debugMessage or the caught exception's message) so callers of
createBillingProgramReportingDetails() receive the failure payload; specifically
modify the continuation.resumeWithException invocations in the catch(Exception)
block and the else branch (and the similar locations around lines 600-603) to
construct OpenIapError.PurchaseFailed(detailMessage) using the available
result?.debugMessage or e.message as appropriate.
---
Nitpick comments:
In `@packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt`:
- Around line 25-30: The test currently asserts that the PurchaseFailed
constructor preserves debugMessage but misses exercising the real production
mapping; update the test to call
OpenIapError.fromBillingResponseCode(DEVELOPER_ERROR, "x") (using the same
DEVELOPER_ERROR value used in production) and assert that the returned error has
debugMessage == "x" and that error.toJSON()["debugMessage"] == "x"; do the same
for the other affected test block (lines ~196-200) to cover mapping paths rather
than only constructors.
🪄 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: 80753d98-c9f3-45e2-b3ed-79ab8fc093e8
📒 Files selected for processing (9)
libraries/flutter_inapp_purchase/lib/helpers.dartlibraries/flutter_inapp_purchase/test/helpers_unit_test.dartpackages/docs/src/pages/docs/updates/releases.tsxpackages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.ktpackages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.ktpackages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.ktpackages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.ktpackages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.ktpackages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt
Addresses CodeRabbit/Gemini review on #98 plus cross-platform alignment so debugMessage isn't Android-only. Kotlin (openiap-google): - Add `data` keyword to PurchaseFailed / DeveloperError (style-guide). - Expand debug-capable error types: UserCancelled, ServiceUnavailable, BillingUnavailable, ItemUnavailable, BillingError, ItemAlreadyOwned, ItemNotOwned, ServiceDisconnected, FeatureNotSupported, ServiceTimeout, UnknownError all become data classes that accept `debugMessage: String? = null`. `fromBillingResponseCode` now forwards BillingResult.debugMessage into every returned error, not just DEVELOPER_ERROR. Call sites in both play and horizon flavors updated to `()` instantiation; tests assert the debugMessage flows through fromBillingResponseCode for all response codes. - reporting-details failure path (`createBillingProgramReportingDetails`) and `finishTransaction` now populate PurchaseFailed(debugMessage) instead of a bare instance so the underlying reason reaches callers. GQL schema + iOS parity: - Add optional `debugMessage` field to `type PurchaseError` in `packages/gql/src/error.graphql`. Regenerate Swift/Kotlin/Dart/GDScript/ TypeScript types (Apple and Google packages + all framework libraries). - Swift codegen plugin: emit `= nil` defaults for nullable struct properties so the synthesized memberwise initializer stays source-compatible when new optional fields are added (needed to keep existing `PurchaseError(code:, message:, productId:)` call sites compiling on the iOS side). - iOS `makePurchaseError` accepts an optional `debugMessage: String?`; populate it from `error.localizedDescription` at the two StoreKit catch sites (queryProduct and promoted-product retrieval) plus the enhanced promotional-offer failure path. - New Swift test `testPurchaseErrorCarriesDebugMessage` round-trips a PurchaseError through JSONEncoder/Decoder to lock the wire format. KMP: - Re-sync Types.kt from gql with the KMP-specific package declaration (`io.github.hyochan.kmpiap.openiap`). Apply the same enum-needs-semicolon regex the Google post-process uses. - Add `ErrorCode.DuplicatePurchase` branch to `ErrorMapping.kt` so the newly-synced enum compiles (the monorepo gql schema was ahead of what KMP had shipped). Flutter: - `convertToPurchaseError` now forwards `result.debugMessage`, `result.responseCode`, and the resolved `platform` onto PurchaseError; previously `PurchaseError.platform` was always null even though the helper already knew it. New helpers_unit_test case locks the full set of forwarded fields in. Docs: - Re-run Prettier on the 2026-04-15 release notes entry (CI had been red on `prettier --check`). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Surfaces platform billing-layer diagnostics (notably Google Play BillingResult.debugMessage) through PurchaseError.debugMessage across OpenIAP implementations and generated client types, and updates docs/tests accordingly.
Changes:
- Add
debugMessagetoPurchaseErroracross GraphQL schema + generated types (TS/Dart/Kotlin/Swift/GDScript) and downstream library type definitions. - Update Google (Play + Horizon)
OpenIapErrorto carry/emitdebugMessage, and forward billingdebugMessagethroughfromBillingResponseCodeand key failure paths. - Update Flutter helper mapping to forward
responseCode+debugMessageinto DartPurchaseError, with a new unit test.
Reviewed changes
Copilot reviewed 21 out of 27 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/gql/src/error.graphql | Extends schema PurchaseError with nullable debugMessage. |
| packages/gql/src/generated/types.ts | Adds debugMessage to generated TS PurchaseError. |
| packages/gql/src/generated/types.dart | Adds debugMessage to generated Dart PurchaseError. |
| packages/gql/src/generated/Types.kt | Adds debugMessage to generated Kotlin PurchaseError. |
| packages/gql/src/generated/Types.swift | Adds debugMessage to generated Swift PurchaseError and updates optional defaults. |
| packages/gql/src/generated/types.gd | Adds debugMessage to generated GDScript PurchaseError serialization. |
| packages/gql/codegen/plugins/swift.ts | Updates Swift codegen to default nullable stored properties to nil. |
| packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.kt | Adds debugMessage to OpenIapError JSON and updates several error types to carry it. |
| packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt | Forwards billing debugMessage into mapped OpenIapError instances (Play). |
| packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt | Forwards billing debugMessage into mapped OpenIapError instances (Horizon). |
| packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt | Improves error propagation to include debugMessage for key Play failure paths. |
| packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt | Improves error propagation to include debugMessage for key Horizon failure paths. |
| packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt | Updates thrown singleton usage for updated error types. |
| packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt | Adds debugMessage to Kotlin PurchaseError serialization. |
| packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt | Adds/updates tests to assert debug message forwarding and new constructors. |
| libraries/flutter_inapp_purchase/lib/helpers.dart | Forwards responseCode + debugMessage into Dart PurchaseError. |
| libraries/flutter_inapp_purchase/test/helpers_unit_test.dart | Adds a unit test ensuring forwarding of debugMessage + responseCode. |
| libraries/flutter_inapp_purchase/lib/types.dart | Adds debugMessage to Dart PurchaseError model (plus formatting changes). |
| packages/apple/Sources/OpenIapModule.swift | Threads debugMessage into constructed PurchaseErrors (iOS). |
| packages/apple/Sources/Models/Types.swift | Updates Swift models for nullable defaults and PurchaseError.debugMessage. |
| packages/apple/Tests/OpenIapTests.swift | Adds tests verifying PurchaseError.debugMessage roundtrips via Codable. |
| libraries/react-native-iap/src/types.ts | Adds debugMessage to PurchaseError interface. |
| libraries/expo-iap/src/types.ts | Adds debugMessage to PurchaseError interface. |
| libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt | Adds debugMessage to KMP PurchaseError model (and extends ErrorCode). |
| libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt | Adds message mapping for newly added ErrorCode. |
| libraries/godot-iap/addons/godot-iap/types.gd | Adds debugMessage to Godot PurchaseError serialization. |
| packages/docs/src/pages/docs/updates/releases.tsx | Adds consolidated release note entry documenting the diagnostics change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a debugMessage field to PurchaseError across all platforms to provide better diagnostics from underlying billing layers. It also updates the GraphQL Swift code generation to default nullable properties to nil and converts several Android error objects into data classes. Review feedback highlights that the DuplicatePurchase error code was manually added to auto-generated Kotlin files, violating the repository's style guide which requires changes to be made in the GraphQL schema. Additionally, the ServiceTimeout error should be moved into the ErrorCode enum to replace its current hardcoded string implementation for better consistency.
- Schema: add `ErrorCode.ServiceTimeout` to `packages/gql/src/error.graphql` so the code is part of the shared enum instead of a hand-typed `"service-timeout"` string literal. `OpenIapError.ServiceTimeout` now sources its CODE from `ErrorCode.ServiceTimeout.rawValue` like every other entry; KMP and Apple error-message switches gain a matching `.serviceTimeout` / `ErrorCode.ServiceTimeout` branch so the `when`/`switch` stays exhaustive. - GDScript plugin: nullable scalar fields no longer round-trip a sentinel default (e.g. `""`, `0`) through `to_dict()`. Nullable properties keep their non-null declaration-time default so the type-checker stays happy, but `to_dict()` now emits the key only when the value has been explicitly set (not equal to the default). This preserves the schema's `null`/absent signal for `PurchaseError .debugMessage` and every other nullable scalar — Dart/Kotlin/Swift consumers no longer receive `""` where they should see null. - KMP sync automation: `packages/gql/scripts/sync-to-platforms.mjs` now also copies the generated types into `libraries/*/` every time `bun run generate` runs. Flutter, Godot, React Native, and Expo each get the generated file verbatim; KMP additionally gets its package declaration (`io.github.hyochan.kmpiap.openiap`) injected and the same enum-companion semicolon regex the Google post-process uses. This removes the "manual edit to an auto-generated file" smell that the review flagged on `libraries/kmp-iap/.../Types.kt`; the file is now reproducible from `error.graphql` + the codegen + this sync script. - KMP ErrorMapping: add the `ServiceTimeout` branch (and keep the `DuplicatePurchase` branch from the previous round) so the exhaustive `when` stays green against the newly-synced ErrorCode enum. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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)
packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt (1)
511-519:⚠️ Potential issue | 🟠 MajorPreserve response-code parity for synchronous
launchBillingFlowfailures.This branch still collapses most non-
OKresponses intoPurchaseFailed, whileonPurchasesUpdatedalready usesOpenIapError.fromBillingResponseCode(...). Immediate failures likeITEM_ALREADY_OWNED,FEATURE_NOT_SUPPORTED,SERVICE_UNAVAILABLE, and evenUSER_CANCELEDtherefore lose their specific error type on the sync path, which breaks the parity this PR is aiming for.Suggested fix
if (result.responseCode != BillingClient.BillingResponseCode.OK) { - val err = when (result.responseCode) { - BillingClient.BillingResponseCode.DEVELOPER_ERROR -> { - OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) - OpenIapError.DeveloperError(result.debugMessage) - } - BillingClient.BillingResponseCode.USER_CANCELED -> OpenIapError.UserCancelled() - else -> OpenIapError.PurchaseFailed(result.debugMessage) - } + if (result.responseCode == BillingClient.BillingResponseCode.DEVELOPER_ERROR) { + OpenIapLog.w("DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group.", TAG) + } + val err = OpenIapError.fromBillingResponseCode( + result.responseCode, + result.debugMessage, + ) purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } } consumePurchaseCallback(Result.success(emptyList())) } else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt` around lines 511 - 519, The sync launchBillingFlow failure branch currently maps most non-OK response codes to OpenIapError.PurchaseFailed and only special-cases DEVELOPER_ERROR/USER_CANCELED, breaking parity with onPurchasesUpdated; change the mapping in the launchBillingFlow failure path (the code that inspects result.responseCode) to delegate to OpenIapError.fromBillingResponseCode(result.responseCode, result.debugMessage) (or the existing overload used by onPurchasesUpdated) instead of collapsing to PurchaseFailed so codes like ITEM_ALREADY_OWNED, FEATURE_NOT_SUPPORTED, SERVICE_UNAVAILABLE and USER_CANCELED preserve their specific OpenIapError types. Ensure DEVELOPER_ERROR logging remains if desired but return the error from fromBillingResponseCode for consistency.packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt (1)
774-842:⚠️ Potential issue | 🟠 MajorALTERNATIVE_ONLY flow currently misclassifies multiple failure modes.
Line 776 treats any dialog failure as
UserCancelled, butshowAlternativeBillingInformationDialog()returnsfalsefor both cancel and non-cancel errors. Line 840 then maps all caught exceptions toFeatureNotSupported, and Line 834 emitsPurchaseFailed()with no reason. This drops diagnostics and returns misleading error codes to callers.Suggested fix
- if (!dialogSuccess) { - val err = OpenIapError.UserCancelled() + if (!dialogSuccess) { + val err = OpenIapError.PurchaseFailed( + "Alternative billing information dialog was cancelled or failed" + ) for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() } @@ - } else { - val err = OpenIapError.PurchaseFailed() + } else { + val err = OpenIapError.PurchaseFailed( + "Failed to create alternative billing reporting token" + ) for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() } - } catch (e: Exception) { + } catch (e: NoSuchMethodException) { + OpenIapLog.e("Alternative billing API not available: ${e.message}", e, TAG) + val err = OpenIapError.FeatureNotSupported() + for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } + return@withContext emptyList() + } catch (e: Exception) { OpenIapLog.e("Alternative billing only flow failed: ${e.message}", e, TAG) - val err = OpenIapError.FeatureNotSupported() + val err = OpenIapError.PurchaseFailed( + e.message ?: "Alternative billing only flow failed" + ) for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } } return@withContext emptyList() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt` around lines 774 - 842, The ALTERNATIVE_ONLY flow currently conflates dialog false with user cancellation and collapses all exceptions into FeatureNotSupported; change showAlternativeBillingInformationDialog(activity) to return a richer result (e.g., enum or sealed class like DialogResult { Cancelled, Failure(error) , Confirmed }) or throw a specific exception so this routine can distinguish cancel vs error, then: when dialog result == Cancelled emit OpenIapError.UserCancelled to purchaseErrorListeners; when dialog result == Failure(error) emit OpenIapError.PurchaseFailed(with original error/message) and log it; in the catch block only emit OpenIapError.FeatureNotSupported for explicit capability exceptions and otherwise emit OpenIapError.PurchaseFailed including e.message and the exception as context; update logging to include the original exception details. Reference: showAlternativeBillingInformationDialog, createAlternativeBillingReportingToken, purchaseErrorListeners, OpenIapError.UserCancelled, OpenIapError.PurchaseFailed, OpenIapError.FeatureNotSupported.
🧹 Nitpick comments (2)
packages/gql/scripts/sync-to-platforms.mjs (1)
86-111: Add explicit “source missing” warnings for new sync targets.Dart/GDScript/TypeScript/KMP blocks silently skip when sources are absent, unlike Kotlin/Swift. A warning keeps stale-generated-file drift visible in CI logs.
Also applies to: 117-143
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/gql/scripts/sync-to-platforms.mjs` around lines 86 - 111, When a source file is missing the script should log a visible warning instead of silently skipping; for each block that checks existsSync(dartSource), existsSync(gdSource), and existsSync(tsSource) add an else branch that calls console.warn with a clear message including the missing source variable and the intended target(s) (use dartTarget, gdTarget, and rnTsTarget/expoTsTarget for TypeScript), and do the same pattern for the KMP/other sync blocks later (the blocks around symbols like kmpSource/kmpTarget or any Kotlin/Swift checks at 117-143); keep existing mkdirSync/copyFileSync behavior in the exists branch and only warn in the else branch so CI logs show stale-generated-file drift.packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt (1)
1210-1212: Add explicit diagnostics for provider validation failures.Line 1210 and Line 1212 throw generic errors without context. Including the provider/missing-field reason would make these failures actionable for callers.
Suggested fix
- if (props.provider != PurchaseVerificationProvider.Iapkit) { - throw OpenIapError.FeatureNotSupported() - } - val options = props.iapkit ?: throw OpenIapError.DeveloperError() + if (props.provider != PurchaseVerificationProvider.Iapkit) { + throw OpenIapError.FeatureNotSupported( + "Unsupported provider: ${props.provider}" + ) + } + val options = props.iapkit ?: throw OpenIapError.DeveloperError( + "Missing iapkit options for provider=${props.provider}" + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt` around lines 1210 - 1212, The code throws generic OpenIapError.FeatureNotSupported() and OpenIapError.DeveloperError() at the provider validation points; update the throws in the OpenIapModule.kt validation logic to include explicit diagnostic text indicating which provider or field failed (e.g., include props.provider or a missing-field name from props.iapkit) so callers receive actionable errors; modify the places that currently call OpenIapError.FeatureNotSupported() and OpenIapError.DeveloperError() to construct the error with a descriptive message (or a typed payload) that mentions the provider identifier and the missing/invalid field encountered.
🤖 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/godot-iap/addons/godot-iap/types.gd`:
- Around line 2042-2055: The PurchaseAndroid.to_dict() implementation is
dropping real boolean fields when they are false; update the method
(PurchaseAndroid.to_dict) to always include the keys "autoRenewingAndroid",
"isAcknowledgedAndroid", and "isSuspendedAndroid" in the returned dict
(assigning their current boolean values) instead of only setting them when !=
false; if this file is generated, change the generator/template so boolean
fields are emitted unconditionally rather than omitted when false.
In
`@libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt`:
- Around line 209-217: The ErrorCode enum now includes ServiceTimeout
("service-timeout") and DuplicatePurchase ("duplicate-purchase") but
ErrorMapping normalization still lacks mappings from legacy Android constants
E_SERVICE_TIMEOUT and E_DUPLICATE_PURCHASE; update the mapping logic in
ErrorMapping.kt (the function that normalizes incoming error strings) to
translate "E_SERVICE_TIMEOUT" -> "service-timeout" and "E_DUPLICATE_PURCHASE" ->
"duplicate-purchase" (and their lowercase/underscore variants if present) so
ErrorCode.valueOf/lookup resolves to ServiceTimeout and DuplicatePurchase
instead of the generic fallback; ensure the same additions are applied to the
other mapping block referenced (lines ~311-337) to cover all code paths.
In
`@libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt`:
- Around line 156-157: Add Android string-code aliases for the new ErrorCode
entries so legacyCodeMap resolves them instead of falling back to Unknown:
update ErrorMapping (ErrorMapping.kt) to map the Android platform codes
"SERVICE_TIMEOUT" and "DUPLICATE_PURCHASE" to ErrorCode.ServiceTimeout and
ErrorCode.DuplicatePurchase respectively (the same place where other Android
aliases are defined and used by fromPlatformCode(..., IapPlatform.Android) and
legacyCodeMap); ensure the mapping keys match existing Android string code
naming conventions and are added alongside the other Android/legacy aliases to
prevent Unknown fallback.
In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt`:
- Around line 564-566: The current call that constructs
OpenIapError.PurchaseFailed uses e.message which may be null; update both places
where continuation.resumeWithException(OpenIapError.PurchaseFailed(e.message))
is used to pass a non-null debug message by using a fallback like (e.message ?:
e.javaClass.name) so the debugMessage is always populated with either the
exception message or the exception class name; apply this change for the
OpenIapError.PurchaseFailed invocation(s) in OpenIapModule (both occurrences).
In `@packages/gql/codegen/plugins/gdscript.ts`:
- Around line 423-435: The current logic in gdscript.ts (inside the block
handling type.nullable) treats sentinel defaults from getDefaultValue(type) as
"unset" by comparing fieldName to the sentinel and omitting the key, which loses
legitimate values like 0/false/"". Change this to always emit
dict["{graphqlName}"] = {fieldName} for nullable scalars (remove the sentinel
equality branch) so from_dict() → to_dict() round-trips are preserved; if
omission semantics are required later, implement explicit presence tracking
(e.g., a separate has_<field> flag set by from_dict()/constructor) rather than
comparing by value. Ensure references to getDefaultValue, fieldName,
graphqlName, and the emit calls are updated accordingly.
In `@packages/gql/scripts/sync-to-platforms.mjs`:
- Around line 123-127: The current insertion uses
/(`@file`:[^\n]+\n)(?!\s*package\b)/ which only captures the first `@file`:
annotation and can wrongly place the package between multiple `@file`: lines;
update the regex used in the replacement (the expression applied to variable
text around the string "package io.github.hyochan.kmpiap.openiap") to match all
leading `@file`: lines (e.g. a grouped repeat like
/((?:`@file`:[^\n]+\n)+)(?!\s*package\b)/) and use that capture in the replacement
so the package line is inserted after all `@file`: annotations.
---
Outside diff comments:
In `@packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt`:
- Around line 511-519: The sync launchBillingFlow failure branch currently maps
most non-OK response codes to OpenIapError.PurchaseFailed and only special-cases
DEVELOPER_ERROR/USER_CANCELED, breaking parity with onPurchasesUpdated; change
the mapping in the launchBillingFlow failure path (the code that inspects
result.responseCode) to delegate to
OpenIapError.fromBillingResponseCode(result.responseCode, result.debugMessage)
(or the existing overload used by onPurchasesUpdated) instead of collapsing to
PurchaseFailed so codes like ITEM_ALREADY_OWNED, FEATURE_NOT_SUPPORTED,
SERVICE_UNAVAILABLE and USER_CANCELED preserve their specific OpenIapError
types. Ensure DEVELOPER_ERROR logging remains if desired but return the error
from fromBillingResponseCode for consistency.
In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt`:
- Around line 774-842: The ALTERNATIVE_ONLY flow currently conflates dialog
false with user cancellation and collapses all exceptions into
FeatureNotSupported; change showAlternativeBillingInformationDialog(activity) to
return a richer result (e.g., enum or sealed class like DialogResult {
Cancelled, Failure(error) , Confirmed }) or throw a specific exception so this
routine can distinguish cancel vs error, then: when dialog result == Cancelled
emit OpenIapError.UserCancelled to purchaseErrorListeners; when dialog result ==
Failure(error) emit OpenIapError.PurchaseFailed(with original error/message) and
log it; in the catch block only emit OpenIapError.FeatureNotSupported for
explicit capability exceptions and otherwise emit OpenIapError.PurchaseFailed
including e.message and the exception as context; update logging to include the
original exception details. Reference: showAlternativeBillingInformationDialog,
createAlternativeBillingReportingToken, purchaseErrorListeners,
OpenIapError.UserCancelled, OpenIapError.PurchaseFailed,
OpenIapError.FeatureNotSupported.
---
Nitpick comments:
In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt`:
- Around line 1210-1212: The code throws generic
OpenIapError.FeatureNotSupported() and OpenIapError.DeveloperError() at the
provider validation points; update the throws in the OpenIapModule.kt validation
logic to include explicit diagnostic text indicating which provider or field
failed (e.g., include props.provider or a missing-field name from props.iapkit)
so callers receive actionable errors; modify the places that currently call
OpenIapError.FeatureNotSupported() and OpenIapError.DeveloperError() to
construct the error with a descriptive message (or a typed payload) that
mentions the provider identifier and the missing/invalid field encountered.
In `@packages/gql/scripts/sync-to-platforms.mjs`:
- Around line 86-111: When a source file is missing the script should log a
visible warning instead of silently skipping; for each block that checks
existsSync(dartSource), existsSync(gdSource), and existsSync(tsSource) add an
else branch that calls console.warn with a clear message including the missing
source variable and the intended target(s) (use dartTarget, gdTarget, and
rnTsTarget/expoTsTarget for TypeScript), and do the same pattern for the
KMP/other sync blocks later (the blocks around symbols like kmpSource/kmpTarget
or any Kotlin/Swift checks at 117-143); keep existing mkdirSync/copyFileSync
behavior in the exists branch and only warn in the else branch so CI logs show
stale-generated-file drift.
🪄 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: 55bf9d09-aee4-4bb2-8c4e-7df4174b843c
⛔ Files ignored due to path filters (5)
packages/gql/src/generated/Types.ktis excluded by!**/generated/**packages/gql/src/generated/Types.swiftis excluded by!**/generated/**packages/gql/src/generated/types.dartis excluded by!**/generated/**packages/gql/src/generated/types.gdis excluded by!**/generated/**packages/gql/src/generated/types.tsis excluded by!**/generated/**
📒 Files selected for processing (25)
libraries/expo-iap/src/types.tslibraries/flutter_inapp_purchase/lib/helpers.dartlibraries/flutter_inapp_purchase/lib/types.dartlibraries/flutter_inapp_purchase/test/helpers_unit_test.dartlibraries/godot-iap/addons/godot-iap/types.gdlibraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.ktlibraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.ktlibraries/react-native-iap/src/types.tspackages/apple/Sources/Models/OpenIapError.swiftpackages/apple/Sources/Models/Types.swiftpackages/apple/Sources/OpenIapModule.swiftpackages/apple/Tests/OpenIapTests.swiftpackages/docs/src/pages/docs/updates/releases.tsxpackages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.ktpackages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.ktpackages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapError.ktpackages/google/openiap/src/main/java/dev/hyo/openiap/Types.ktpackages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.ktpackages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.ktpackages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.ktpackages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.ktpackages/gql/codegen/plugins/gdscript.tspackages/gql/codegen/plugins/swift.tspackages/gql/scripts/sync-to-platforms.mjspackages/gql/src/error.graphql
✅ Files skipped from review due to trivial changes (2)
- libraries/flutter_inapp_purchase/test/helpers_unit_test.dart
- libraries/expo-iap/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapErrorExtensions.kt
- packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapErrorExtensions.kt
- packages/google/openiap/src/test/java/dev/hyo/openiap/OpenIapErrorTest.kt
- packages/docs/src/pages/docs/updates/releases.tsx
The 2nd-round review added `ServiceTimeout` to the shared ErrorCode enum but missed the two framework-side `Record<ErrorCode, string>` maps that enforce an exhaustive set at compile time, breaking `libraries/react-native-iap` and `libraries/expo-iap` CI on `tsc` (TS2741). Add the missing entry so both maps stay exhaustive. Also revert the incidental ktlint reformatting that `bun install` ran against `libraries/expo-iap/android/**` / example / plugin — those diffs were picked up by `expo-module prepare`'s prepare script and are unrelated to this PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GDScript codegen: nullable scalar fields now declare as \`var x: Variant = null\` instead of typed-with-sentinel-default. The previous "omit when equal to default" approach from the last round was lossy for booleans/ints — \`false\` is a real value, not "unset". Using Variant lets the field actually hold null so from_dict/to_dict round-trips preserve the null-vs-default distinction without dropping legitimate zero/false/empty values. - KMP ErrorMapping: add \`E_SERVICE_TIMEOUT\` / \`SERVICE_TIMEOUT\` and \`E_DUPLICATE_PURCHASE\` / \`DUPLICATE_PURCHASE\` legacy aliases to \`legacyCodeMap\` so the Android path doesn't collapse those codes to \`ErrorCode.Unknown\`. - OpenIapModule (play): the reporting-details catch block and the outer \`createBillingProgramReportingDetails\` exception handler now fall back to \`e.javaClass.simpleName\` when \`e.message\` is null, so \`PurchaseFailed.debugMessage\` always carries something diagnostic instead of just null. - sync-to-platforms.mjs: the KMP package-insertion now walks the file line-by-line and splices the package declaration immediately after the LAST \`@file:\` annotation. The previous single-line regex misplaced the package on files with leading comments (it fell through to prepend-mode, putting the package before the file annotations, which Kotlin rejects). The new approach also handles future additions of multiple \`@file:\` lines without breakage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a debugMessage field to the PurchaseError type across all supported platforms (Android, iOS, Flutter, React Native, and Godot) to improve error diagnostics by surfacing raw messages from the underlying billing layers. It also adds a ServiceTimeout error code and updates the necessary mappings and codegen plugins to support these changes. Regarding the review feedback, I have noted the concern about breaking API changes caused by converting error objects to data classes. I will ensure that these changes are communicated appropriately or, if necessary, adjusted to maintain backward compatibility while still achieving the goal of improved error reporting.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 32 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Bump the planned release version for openiap-google and openiap-google-horizon from 1.3.32 to 2.0.0. The error-type refactor (`OpenIapError.DeveloperError`/`PurchaseFailed`/UserCancelled/… singletons → data classes) is a source-breaking change for direct Kotlin consumers, so SemVer requires a major bump. Updated the 2026-04-15 release note entry and the PR description to reflect the bump and include the migration notes (the 11 affected names, the `()` migration pattern, companion access staying unchanged). - GDScript codegen: `generateToDictField` now omits nullable scalar keys from `to_dict()` when the value is `null`, instead of emitting an explicit `null`. Legitimate `0` / `false` / `""` / any explicitly-set value still serialize; only the "unset" (`null`) state is dropped. That restores the absent-vs-null distinction without reintroducing the sentinel-based-omission bug that drops legitimate `false`/`0`/`""`. Regenerated `types.gd`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the "Test GQL Types" job only checked that the generated files *exist* after `bun run generate`, not that they *match* what's committed. That allowed plugin changes (or missed regenerations) to ship with stale copies in the framework libraries. - Add a `git diff --exit-code` step after `bun run generate` so CI fails with a clear message if the codegen output diverges from the committed files. - Revert the in-flight attempt to run `dart format` inside sync-to-platforms.mjs. The flutter_inapp_purchase CLAUDE.md explicitly excludes `lib/types.dart` from the Dart format check, and running the formatter inside the sync would have required a Dart SDK on every CI runner that hits this job (plus hidden reformat churn every time the formatter version shifts). Instead the sync now copies the raw generator output verbatim, matching every other library. - Re-commit `libraries/flutter_inapp_purchase/lib/types.dart` in the raw generator-output shape so `bun run generate` is idempotent. An earlier local `dart format` pass had introduced blank lines between enum entries that the codegen doesn't emit; the checked-in file now matches `packages/gql/src/generated/types.dart` byte-for-byte. Verified locally: `bun run generate` run twice in succession produces no working-tree diff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a ServiceTimeout error code across multiple platforms (Android, iOS, Flutter, React Native, Expo, and Godot) and updates the PurchaseError model to include an optional debugMessage field. The changes also include updates to the GDScript and Swift codegen plugins to better handle nullable scalars using Variant and nil defaults, respectively. I have reviewed the comments and found that index 0 is a valid improvement opportunity regarding the PurchaseError model, while indices 1 and 2 are general observations that do not require code changes.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 33 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Broaden the drift check to also fail on untracked files that appear after \`bun run generate\`. The previous \`git diff --exit-code\` only flagged modified *tracked* files, so a plugin that starts emitting a new output would slip through silently. Combine \`git status --porcelain\` with the existing diff check and print the offending paths so the failure mode is easy to diagnose. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
packages/gql/scripts/sync-to-platforms.mjs (1)
132-147: This block should normalize any existingpackagedeclaration to match the target, consistent with the Google post-processor.The code correctly checks for the target package before inserting, and currently no duplicates occur because
packages/gql/src/generated/Types.ktemits no package declaration. However, the regex check is specific to the target package name only—if the generated source ever starts emitting a different package declaration, the code would insert the target package alongside it, resulting in a compile error. Matching the Google normalization approach (which handles rewriting/replacing any existing package) would prevent this scenario and keep both Kotlin targets in lockstep.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/gql/scripts/sync-to-platforms.mjs` around lines 132 - 147, Current logic only checks for the target package and inserts it, which will duplicate if the generated source contains a different package; change the behavior to detect and normalize any existing package declaration. In the block manipulating text/lines (variables: text, pkg, lines, lastFileAnnotation), first search for a package declaration with a general regex like /^\s*package\s+[\w.]+/m; if found, replace that package line with the target pkg; otherwise preserve the existing insertion logic around `@file`: annotations (splice after lastFileAnnotation or unshift when none). Ensure you update the text variable from the modified lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt`:
- Around line 834-835: OpenIapError.PurchaseFailed() is being constructed with
no debugMessage, which discards actionable context; modify the failure branch
that currently creates val err = OpenIapError.PurchaseFailed() to include a
meaningful debug message (for example the underlying exception message, billing
response code, or any local error string available in that scope) and pass that
enriched error into each purchaseErrorListeners invocation (listeners via
listener.onPurchaseError(err)); ensure you extract the relevant diagnostic value
from the surrounding scope and use it when constructing
OpenIapError.PurchaseFailed so logs/clients receive concrete context.
- Around line 873-875: The code rejects a concurrent purchase by logging
"requestPurchase rejected: another purchase is already in progress" but resumes
the coroutine with OpenIapError.DeveloperError() without any diagnostic text;
update the resumeWithException call to construct DeveloperError with a
debugMessage (e.g., "another purchase is already in progress") so callers can
distinguish this case—modify the call at the
continuation.resumeWithException(OpenIapError.DeveloperError()) site (near
OpenIapLog.w) to pass the descriptive debugMessage.
- Around line 1055-1056: The USER_CANCELED branches should pass the diagnostic
message into the UserCancelled error: update both occurrences that currently
construct OpenIapError.UserCancelled() to instead call
OpenIapError.UserCancelled(result.debugMessage) (and the other to
OpenIapError.UserCancelled(billingResult.debugMessage)), mirroring how
OpenIapError.PurchaseFailed and OpenIapError.DeveloperError forward
debugMessage; locate the branches that pattern-match
BillingClient.BillingResponseCode.USER_CANCELED and replace the no-arg
UserCancelled() with the corresponding debugMessage argument so the optional
parameter is consistently populated.
---
Nitpick comments:
In `@packages/gql/scripts/sync-to-platforms.mjs`:
- Around line 132-147: Current logic only checks for the target package and
inserts it, which will duplicate if the generated source contains a different
package; change the behavior to detect and normalize any existing package
declaration. In the block manipulating text/lines (variables: text, pkg, lines,
lastFileAnnotation), first search for a package declaration with a general regex
like /^\s*package\s+[\w.]+/m; if found, replace that package line with the
target pkg; otherwise preserve the existing insertion logic around `@file`:
annotations (splice after lastFileAnnotation or unshift when none). Ensure you
update the text variable from the modified lines.
🪄 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: 823efbda-6965-4218-bbd9-f8226b640831
⛔ Files ignored due to path filters (1)
packages/gql/src/generated/types.gdis excluded by!**/generated/**
📒 Files selected for processing (10)
.github/workflows/ci.ymllibraries/expo-iap/src/utils/errorMapping.tslibraries/flutter_inapp_purchase/lib/types.dartlibraries/godot-iap/addons/godot-iap/types.gdlibraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.ktlibraries/react-native-iap/src/utils/errorMapping.tspackages/docs/src/pages/docs/updates/releases.tsxpackages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.ktpackages/gql/codegen/plugins/gdscript.tspackages/gql/scripts/sync-to-platforms.mjs
🚧 Files skipped from review as they are similar to previous changes (3)
- libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/utils/ErrorMapping.kt
- packages/gql/codegen/plugins/gdscript.ts
- packages/docs/src/pages/docs/updates/releases.tsx
Addresses the remaining CodeRabbit flags from the 6th review round
(3086661442 / 3086661449 / 3086661461): every \`OpenIapError.X()\`
throw/emit where a precise local cause is known now forwards that
context through \`debugMessage\`.
- \`requestAlternativeBillingAndroid\`: \`PurchaseFailed()\` for the
null-token branch becomes
\`PurchaseFailed("Alternative billing token creation returned null")\`,
and the alternative-billing dialog-dismissed path upgrades from
\`UserCancelled()\` to include the concrete cause.
- Concurrent purchase rejection (compareAndSet fails): throws
\`DeveloperError("Another purchase is already in progress")\`.
- \`launchBillingFlow\` result and \`onPurchasesUpdated\` USER_CANCELED
branches forward \`result.debugMessage\` / \`billingResult.debugMessage\`
to \`UserCancelled\`, matching the pattern the other response codes
already use.
- \`verifyPurchaseWithProvider\` and Horizon's \`verifyPurchase\` /
\`verifyPurchaseWithProvider\` null-guards now throw
\`DeveloperError\` with the concrete missing-parameter text
("Missing IAPKit verification parameters" / "Horizon verifyPurchase
requires appId to be set during initConnection") instead of a bare
\`DeveloperError()\`.
All changes mirrored across the play and horizon flavors where the
same code exists.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The two flutter_inapp_purchase entries shipped from the same investigation thread (#96 → #97 channel fix → #98 debugMessage pipe) so grouping them under a single header keeps the 2026-04-15 cut readable instead of splitting the narrative across two cards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nges Add a "Cross-Library Verification for Shared-Package Changes" section to knowledge/internal/04-platform-packages.md. Lessons learned across PR #98: changes to packages/google or packages/apple public APIs silently compile against the source package but break every downstream framework library that imports the artifact. CI catches these too late — the new guide mandates a concrete downstream compile matrix (flutter, react-native-iap, expo-iap, kmp-iap) plus a grep guard for the specific OpenIapError singleton-vs-data-class trap, and a cross-library SemVer table so the release order and version bumps stay consistent when shared-package APIs evolve. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
OpenIapError.DeveloperError,PurchaseFailed, and every other error type returned byfromBillingResponseCode(UserCancelled, ServiceUnavailable, BillingUnavailable, ItemUnavailable, BillingError, ItemAlreadyOwned, ItemNotOwned, ServiceDisconnected, FeatureNotSupported, ServiceTimeout, UnknownError) stop beingobjectsingletons and become data classes that accept an optionaldebugMessage: String?. This is source-breaking for direct Kotlin consumers (throw OpenIapError.DeveloperError→throw OpenIapError.DeveloperError()), hence the major version bump per SemVer. Companion.CODE/.MESSAGEaccesses andis OpenIapError.Xtype checks keep working.OpenIapError.toJSON()emitsdebugMessage, andfromBillingResponseCodeforwards Google Play's rawBillingResult.debugMessageinto the error instance for every response code instead of silently dropping it. ThelaunchBillingFlowsync-failure path now also producesDeveloperError(result.debugMessage)(matching theonPurchasesUpdatedasync path) instead of the genericPurchaseFailed. Tests assert the message flows through every response code, including theelse → UnknownErrorbranch.debugMessage: String(optional) totype PurchaseErrorandServiceTimeoutto theErrorCodeenum inpackages/gql/src/error.graphql. Regenerated Swift/Kotlin/Dart/GDScript/TypeScript types; the Swift codegen plugin now emits= nildefaults on nullable struct properties so existing memberwise-init call sites stay source-compatible when new optional fields land. The GDScript plugin declares nullable scalars asVariant = null(sofalse/0/""round-trip as themselves) and omits the key fromto_dict()only when the value is actuallynull— preserving the absent-vs-null distinction without losing legitimate zero/false/empty values.makePurchaseErrorgains an optionaldebugMessage: String?; populated at the StoreKit catch sites (queryProduct, promoted-product retrieval, promotional-offer failure) witherror.localizedDescription. NewtestPurchaseErrorCarriesDebugMessageround-trips a PurchaseError throughJSONEncoder/Decoderto lock the wire format. This side is purely additive (nullable optional field with defaultnil), so the Apple package is NOT a breaking change.convertToPurchaseErrornow forwardsresponseCode,debugMessage, and the resolvedplatformfrom the native payload ontoPurchaseError. Those fields already existed onPurchaseErrorbut the helper only copiedcode/message, soPurchaseError.debugMessage/.responseCode/.platformwere always null even when the information was available. Locked in with a newhelpers_unit_testcase. Dart-facing API is unchanged — flutter_inapp_purchase stays on the 9.x line (9.0.4 picks up openiap-google 2.0.0).ErrorMapping.legacyCodeMapgainsE_SERVICE_TIMEOUT/SERVICE_TIMEOUTandE_DUPLICATE_PURCHASE/DUPLICATE_PURCHASEAndroid-alias entries, and the exhaustivewhenongetErrorMessagegets branches for both new ErrorCode values so they no longer fall back toUnknown.packages/gql/scripts/sync-to-platforms.mjsnow also copies the generated types into every framework library (flutter_inapp_purchase,godot-iap,react-native-iap,expo-iap,kmp-iap) as part ofbun run generate, with KMP receiving the required package declaration (inserted after every leading@file:annotation, multi-annotation safe) and enum-companion semicolon post-process. Removes the "manual edit to an auto-generated file" drift flagged by review.Addresses the diagnosis thread on #96 — callers investigating
DEVELOPER_ERRORon subscription replacement flows (e.g. DEFERRED downgrades) now see Play's exact rejection reason viaPurchaseError.debugMessagewithout needing adb.Impact / migration notes
Kotlin (openiap-google consumers): the following
OpenIapErrormembers are no longerobjectsingletons, so value references need():DeveloperError,PurchaseFailed,UserCancelled,ServiceUnavailable,BillingUnavailable,ItemUnavailable,BillingError,ItemAlreadyOwned,ItemNotOwned,ServiceDisconnected,FeatureNotSupported,ServiceTimeout,UnknownError.Migration:
throw OpenIapError.DeveloperError→throw OpenIapError.DeveloperError()continuation.resumeWithException(OpenIapError.PurchaseFailed)→continuation.resumeWithException(OpenIapError.PurchaseFailed())Companion
.CODE/.MESSAGEaccesses andis OpenIapError.DeveloperErrortype checks keep working unchanged. All internal call sites inpackages/googleandlibraries/flutter_inapp_purchase/androidupdated; tests updated accordingly.Dart / Swift / TypeScript / GDScript consumers: no source-breaking changes.
PurchaseError.debugMessageis an additive optional field; existing constructor calls keep compiling.Versions
openiap-google/openiap-google-horizon— 2.0.0 (major bump for the error data-class refactor)flutter_inapp_purchase9.0.4 (picks up openiap-google 2.0.0; Dart API unchanged)Release notes entry consolidated into a single 2026-04-15 item in
packages/docs/src/pages/docs/updates/releases.tsx.Test plan
cd packages/google && ./gradlew :openiap:compilePlayDebugKotlin :openiap:compileHorizonDebugKotlin— both flavors compilecd packages/google && ./gradlew :openiap:testPlayDebugUnitTest— allOpenIapErrorTestassertions pass, including newfromBillingResponseCode forwards debugMessage for every response code/DeveloperError carries debug message when provided/PurchaseFailed carries debug message when providedcasescd packages/apple && swift build && swift test --filter OpenIapTests— 87 tests pass, including newtestPurchaseErrorCarriesDebugMessagecd libraries/flutter_inapp_purchase && flutter analyze && flutter test— 244 tests pass (newconvertToPurchaseError forwards debugMessage and responseCode from PurchaseResult)cd libraries/kmp-iap && ./gradlew :library:build -x test— builds against the auto-synced Types.ktcd libraries/expo-iap && bun run test && bun run lint:tsc— 46 tests pass, tsc cleancd libraries/react-native-iap && yarn typecheck:lib && yarn test:library— 269 tests passcd packages/docs && bunx prettier --check . && bun run typecheck— clean🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
ServiceTimeouterror code for billing service timeout scenarios.Bug Fixes
Documentation