Skip to content

fix(google): recover already-owned purchases#189

Draft
hyochan wants to merge 7 commits into
mainfrom
codex/fix/google-already-owned-recovery
Draft

fix(google): recover already-owned purchases#189
hyochan wants to merge 7 commits into
mainfrom
codex/fix/google-already-owned-recovery

Conversation

@hyochan

@hyochan hyochan commented Jun 21, 2026

Copy link
Copy Markdown
Member

Summary

  • Recover Android Play Billing ITEM_ALREADY_OWNED results returned immediately from launchBillingFlow by querying Play Billing owned purchases for the requested SKU/type.
  • Re-publish matching recovered purchases to purchase update listeners so React Native onPurchaseSuccess / purchaseUpdatedListener can receive the purchase instead of only seeing already-owned.
  • Avoid a local purchase cache; the recovery path asks Play Billing for current ownership and keeps duplicate callback handling guarded by focused Play flavor tests.

Closes #166

Test plan

  • ./gradlew :openiap:testPlayDebugUnitTest --tests dev.hyo.openiap.QueryPurchasesRaceTest
  • ./gradlew :openiap:compilePlayDebugKotlin :openiap:compileHorizonDebugKotlin

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved handling of already-owned purchases by recovering and returning existing purchase data (including subscription base plans) instead of failing the request.
    • Strengthened concurrency safety for purchase and billing listener notifications during simultaneous operations.
  • Tests

    • Added Play test coverage to ensure already-owned recovery is resilient to duplicate concurrent callbacks, filters to requested SKUs, preserves subscription base-plan IDs, and completes safely on query failures.
  • Chores

    • Updated release/CI workflows to sync and commit generated version metadata consistently across release paths.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2451b85-b588-4d63-9261-28ec25991c92

📥 Commits

Reviewing files that changed from the base of the PR and between f1e7587 and ea62c92.

📒 Files selected for processing (3)
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
  • packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
  • packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt

📝 Walkthrough

Walkthrough

Adds a new queryAlreadyOwnedPurchases helper in Helpers.kt that queries Play Billing for already-owned items filtered by SKU. In OpenIapModule, the ITEM_ALREADY_OWNED billing response now triggers this helper to recover and forward purchases instead of simply erroring. Listener sets are switched to CopyOnWriteArraySet for thread safety, and new tests validate race and filter behavior. All release workflows are updated to stage and commit the generated docs version-metadata file.

Changes

ITEM_ALREADY_OWNED Recovery Flow

Layer / File(s) Summary
queryAlreadyOwnedPurchases helper
packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
Adds AtomicBoolean import and queryAlreadyOwnedPurchases function that validates inputs, guards against duplicate async callbacks, filters Play Billing results to the requested SKUs, and maps results through toPurchase with base-plan IDs.
ITEM_ALREADY_OWNED recovery wiring and thread-safe listeners
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
Imports queryAlreadyOwnedPurchases; changes purchaseUpdateListeners, purchaseErrorListeners, userChoiceBillingListeners, and developerProvidedBillingListeners from mutableSetOf to CopyOnWriteArraySet; moves desiredType computation earlier in requestPurchase; replaces the generic ITEM_ALREADY_OWNED error path with recovery logic that calls queryAlreadyOwnedPurchases, notifies suspended-subscription billing issues and purchaseUpdateListeners on success, or notifies purchaseErrorListeners and resolves with an empty list when no purchases are recovered.
Race condition and SKU filter tests
packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt
Adds billingPurchase helper to construct Purchase objects; extends DuplicateBillingClient to accept optional injected purchases list and optional throw-on-query flag; updates queryPurchasesAsync to return injected purchases when provided; adds four tests verifying duplicate-callback tolerance, SKU-only filtering, subscription base-plan preservation, and completion when the billing client throws.

Release Workflow Version Metadata Integration

Layer / File(s) Summary
Version metadata staging across release workflows
.github/workflows/ci.yml, release.yml, release-apple.yml, release-google.yml, release-maui.yml
All five release workflows and ci.yml are updated to stage, commit, and conflict-resolve packages/docs/src/generated/version-metadata.json alongside other version-related files. The audit-parity and release-maui workflows add explicit sync-versions.sh execution steps to regenerate metadata before committing.
Audit and deploy script version metadata expectations
scripts/audit-non-godot-parity.mjs, scripts/deploy.sh
Audit script now expects packages/docs/src/generated/version-metadata.json in release workflow file-sync steps. Deploy script detects and stages the same file alongside other version files during version-bump commits.

Sequence Diagram

sequenceDiagram
    participant User
    participant requestPurchase
    participant BillingClient
    participant queryAlreadyOwnedPurchases
    participant purchaseUpdateListeners
    participant purchaseErrorListeners

    User->>requestPurchase: Request purchase (SKU already owned)
    requestPurchase->>BillingClient: launchBillingFlow
    BillingClient-->>requestPurchase: ITEM_ALREADY_OWNED response
    requestPurchase->>queryAlreadyOwnedPurchases: Recover owned purchases<br/>(with base-plan mapping)
    queryAlreadyOwnedPurchases-->>requestPurchase: List of recovered purchases
    alt Purchases found
        requestPurchase->>purchaseUpdateListeners: Notify suspended-subscription issue
        requestPurchase->>purchaseUpdateListeners: Dispatch recovered purchases
        requestPurchase-->>User: Resolve with purchases
    else No purchases found
        requestPurchase->>purchaseErrorListeners: Notify error
        requestPurchase-->>User: Resolve with empty list
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • hyodotdev/openiap#39: Main PR's queryAlreadyOwnedPurchases recovery derives subscription base-plan IDs and maps recovered BillingPurchase via toPurchase(..., basePlanId), building on the base-plan ID introduction in that PR.
  • hyodotdev/openiap#159: Both PRs modify Play OpenIapModule.requestPurchase and async callback handling to safely guard against double-resume and duplicate callback invocations in purchase queries.

Suggested labels

🛠 bugfix, 🧪 test

🐇 When the shop says "already owned,"
don't give up—query what's owed!
Recover each purchase with care,
thread-safe listeners everywhere.
Workflows now sync metadata clean,
the finest release flow ever seen! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Changes to workflow files and script infrastructure appear necessary to support version metadata management, but are tangential to the core purchase recovery functionality; however, these changes lack clear justification in the PR objectives. Clarify whether the workflow and script changes are necessary dependencies for this fix or should be addressed in a separate PR focused on version metadata management.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(google): recover already-owned purchases' is concise and specific, directly describing the main change where the code now recovers and republishes already-owned purchases from Play Billing.
Linked Issues check ✅ Passed The PR implements the core requirement from #166: ensuring purchases are properly reported through listeners when ITEM_ALREADY_OWNED is returned, directly addressing the issue where purchase data was not reaching success handlers.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix/google-already-owned-recovery

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 react-native-iap react-native-iap library 🐛 bug Something isn't working 🤖 android Related to android labels Jun 21, 2026

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

Copy link
Copy Markdown
Contributor

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 introduces recovery logic for ITEM_ALREADY_OWNED billing responses in the Google Play Billing implementation by tracking purchase request contexts and querying already owned purchases. The review feedback highlights a critical thread-safety issue where purchaseUpdateListeners and purchaseErrorListeners should use CopyOnWriteArraySet to prevent concurrent modification exceptions. Additionally, it recommends handling the fallback case for ITEM_ALREADY_OWNED recovery failures to avoid mapping to a generic error, and suggests using purchaseList.orEmpty() to safely handle potential null platform types from the billing library.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

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/helpers/Helpers.kt Outdated
@hyochan hyochan force-pushed the codex/fix/google-already-owned-recovery branch 2 times, most recently from 4554753 to ef32272 Compare June 21, 2026 15:44
When Play Billing reports ITEM_ALREADY_OWNED during a purchase flow, query the current owned purchases for the requested SKU and publish matching purchases to update listeners instead of only surfacing an already-owned error.

Closes #166
@hyochan hyochan force-pushed the codex/fix/google-already-owned-recovery branch from ef32272 to a083093 Compare June 21, 2026 15:52

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

Copy link
Copy Markdown
Contributor

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 introduces a recovery mechanism for ITEM_ALREADY_OWNED responses during billing flows by querying and filtering already owned purchases. It also updates listener sets to thread-safe CopyOnWriteArraySet and adds corresponding unit tests. The review feedback suggests updating the mock Purchase JSON in tests to include the productIds array for compatibility with Google Play Billing Library v5.0+.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

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

Copy link
Copy Markdown
Contributor

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 adds support for handling the ITEM_ALREADY_OWNED billing response code by querying already owned purchases and notifying listeners. It also enhances thread safety by converting purchaseUpdateListeners and purchaseErrorListeners to CopyOnWriteArraySet, and adds corresponding unit tests. The reviewer recommended extending this thread-safety improvement to userChoiceBillingListeners and developerProvidedBillingListeners to prevent potential ConcurrentModificationExceptions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt (1)

111-114: 💤 Low value

Thread-safe listener sets look good; consider applying to remaining sets for consistency.

The switch to CopyOnWriteArraySet for purchaseUpdateListeners and purchaseErrorListeners correctly addresses concurrent iteration from callback threads. Note that userChoiceBillingListeners and developerProvidedBillingListeners at lines 113-114 remain mutableSetOf and are also iterated in BillingClient proxy callbacks—if those could race with add/remove operations, consider making them thread-safe in a follow-up.

🤖 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/OpenIapModule.kt`
around lines 111 - 114, Replace the non-thread-safe mutableSetOf implementations
for userChoiceBillingListeners and developerProvidedBillingListeners with
java.util.concurrent.CopyOnWriteArraySet, matching the pattern already used for
purchaseUpdateListeners and purchaseErrorListeners. This ensures thread-safe
iteration when these sets are accessed from BillingClient proxy callbacks that
may race with add/remove operations.
🤖 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/OpenIapModule.kt`:
- Around line 111-114: Replace the non-thread-safe mutableSetOf implementations
for userChoiceBillingListeners and developerProvidedBillingListeners with
java.util.concurrent.CopyOnWriteArraySet, matching the pattern already used for
purchaseUpdateListeners and purchaseErrorListeners. This ensures thread-safe
iteration when these sets are accessed from BillingClient proxy callbacks that
may race with add/remove operations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a87dfdb-fa23-4d0a-b3cc-7208bfa5672a

📥 Commits

Reviewing files that changed from the base of the PR and between 1259ac2 and 3b28c81.

📒 Files selected for processing (3)
  • packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
  • packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
  • packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt

@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jun 21, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jun 21, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jun 21, 2026

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

Copy link
Copy Markdown
Contributor

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 introduces handling for the ITEM_ALREADY_OWNED billing response code by querying already owned purchases and recovering them. It updates listener sets to thread-safe CopyOnWriteArraySet collections, adds the queryAlreadyOwnedPurchases helper function, and includes corresponding unit tests. Feedback suggests resolving the correct basePlanId by matching the requested offer token instead of using firstOrNull(), and wrapping the asynchronous query in a try-catch block to prevent coroutines from hanging in case of unexpected IPC failures.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt Outdated

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

Copy link
Copy Markdown
Contributor

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 introduces handling for the ITEM_ALREADY_OWNED billing response code in the Android OpenIapModule. When this response is received, the module now queries already owned purchases to recover and notify listeners of matching active purchases. To support this, thread-safe CopyOnWriteArraySet listeners and new helper functions (queryAlreadyOwnedPurchases, resolveBasePlanIdForOfferToken) were introduced, along with corresponding unit tests. Additionally, the release and deployment scripts were updated to track packages/docs/src/generated/version-metadata.json. I have no feedback to provide as there are no review comments to address.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jun 22, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 android Related to android 🐛 bug Something isn't working react-native-iap react-native-iap library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android purchases not being reported by success handler or listeners

1 participant