fix(google): reconcile ambiguous purchase errors - #257
Conversation
Query current Play ownership when onPurchasesUpdated reports a transient, ambiguous failure. Only recover same-SKU purchases created after the billing flow starts so existing ownership cannot become a false success. Add regression coverage for NETWORK_ERROR subscription recovery, retriable response codes, stale ownership filtering, and callback races. Fixes #166
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughGoogle Play purchase-flow errors now reconcile recent owned purchases with bounded retries, timestamp filtering, and coordinated callback/listener delivery. Tests cover recovery races and retry outcomes, while release documentation lists updated package versions. ChangesPurchase recovery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant BillingClient
participant OpenIapModule
participant PurchaseListeners
participant PendingCallback
BillingClient->>OpenIapModule: onPurchasesUpdated(error)
OpenIapModule->>BillingClient: query owned purchases with cutoff and retries
BillingClient-->>OpenIapModule: recovered purchases or query failure
OpenIapModule->>PurchaseListeners: deliver recovered purchases
OpenIapModule->>PendingCallback: complete with recovered or empty results
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Retry transient ownership-query failures after an ambiguous purchase callback while preserving fatal errors, request matching, and duplicate-delivery guards.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt (1)
100-116: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueRetries fire immediately with no backoff, and the exception path ignores the retriable check.
Two small asymmetries worth considering:
SERVICE_DISCONNECTED/NETWORK_ERRORare re-queried synchronously in the same tick; without a short delay (or a reconnect forSERVICE_DISCONNECTED) the extra attempts will almost always reproduce the same failure and just burn the attempt budget.- The response-code path retries only when
isRetriablePurchaseQueryResponseis true, but thecatchblock retries on any exception (including a permanently not-ready client). Bounded bymaxAttempts, so no runaway, but the gating is inconsistent.Since this runs on an async callback path, a delayed retry would need a handler/coroutine; feel free to defer if the current best-effort behavior is intentional.
Also applies to: 132-140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt` around lines 100 - 116, The query retry flow in query should apply the same isRetriablePurchaseQueryResponse gating in its exception path, and schedule eligible retries with a short asynchronous delay rather than invoking query immediately. Preserve the maxAttempts bound and return empty results for non-retriable failures; use the existing callback-compatible Handler or coroutine mechanism.packages/google/openiap/src/testPlay/java/dev/hyo/openiap/OnPurchasesUpdatedRecoveryTest.kt (1)
152-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAttempt counts are hardcoded to the module's private max-attempts constant.
assertEquals(3, ...)silently encodesAMBIGUOUS_PURCHASE_QUERY_MAX_ATTEMPTS; changing that constant breaks these tests in a way that reads like a regression. Consider deriving the expectation (reflection on the constant, orpurchaseResponseCodes.size) or adding a comment tying the two together.Also applies to: 195-195
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/google/openiap/src/testPlay/java/dev/hyo/openiap/OnPurchasesUpdatedRecoveryTest.kt` at line 152, The assertions in OnPurchasesUpdatedRecoveryTest that expect 3 query calls hardcode the private retry limit. Derive the expected count from the existing purchaseResponseCodes size or the AMBIGUOUS_PURCHASE_QUERY_MAX_ATTEMPTS constant, and apply the same change to both affected assertions so they remain aligned when the retry limit changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt`:
- Around line 100-116: The query retry flow in query should apply the same
isRetriablePurchaseQueryResponse gating in its exception path, and schedule
eligible retries with a short asynchronous delay rather than invoking query
immediately. Preserve the maxAttempts bound and return empty results for
non-retriable failures; use the existing callback-compatible Handler or
coroutine mechanism.
In
`@packages/google/openiap/src/testPlay/java/dev/hyo/openiap/OnPurchasesUpdatedRecoveryTest.kt`:
- Line 152: The assertions in OnPurchasesUpdatedRecoveryTest that expect 3 query
calls hardcode the private retry limit. Derive the expected count from the
existing purchaseResponseCodes size or the AMBIGUOUS_PURCHASE_QUERY_MAX_ATTEMPTS
constant, and apply the same change to both affected assertions so they remain
aligned when the retry limit changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e5e5469f-4dad-4951-b06a-3d9fad085228
📒 Files selected for processing (3)
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.ktpackages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.ktpackages/google/openiap/src/testPlay/java/dev/hyo/openiap/OnPurchasesUpdatedRecoveryTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
Schedule transient ownership-query retries on the main looper, avoid retrying synchronous API exceptions, and keep retry tests aligned with their response sequences.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Verify ambiguous reconciliation remains pending until the main-looper retry delay elapses.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Final live IAPKit receipt E2EValidated revision
This completes the previously approval-gated live Google sandbox receipt vertical. |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/docs/src/pages/docs/updates/releases.tsx`:
- Around line 121-124: Remove or defer the future-dated release entry identified
by id google-play-ambiguous-purchase-recovery-2026-07-28 in the releases list;
do not publish it until July 28, 2026 or later. If retaining it, update the date
and accompanying copy to reflect an already-published release while preserving
the list’s chronological ordering.
🪄 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 Plus
Run ID: e8f361a9-55db-48ad-a1a4-8dc85bbd3bd4
📒 Files selected for processing (4)
packages/docs/src/pages/docs/updates/releases.tsxpackages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.ktpackages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.ktpackages/google/openiap/src/testPlay/java/dev/hyo/openiap/OnPurchasesUpdatedRecoveryTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/google/openiap/src/testPlay/java/dev/hyo/openiap/OnPurchasesUpdatedRecoveryTest.kt
- packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
Summary
queryPurchasesAsync()OKcallback wins the raceWhy
Google Play can complete an order but report
NETWORK_ERRORthroughonPurchasesUpdatedbefore the app receives the purchase. OpenIAP previously emitted only a purchase error and cleared the pending request, leaving a completed subscription unacknowledged.Fixes #166
Stable release plan
Release sequentially from
mainafter merge:The OpenIAP Spec and openiap-apple versions remain unchanged because this fix does not change the public contract.
Test plan
android.util.Log.w(unrelated to this Play recovery change)Preview
No visual preview is applicable. This changes native purchase reconciliation behavior and release history content; the native behavior is covered by deterministic regression tests and the physical-device receipt vertical.
Summary by CodeRabbit