Skip to content

Add QDeferredPurchasesListener with QDeferredTransaction model#785

Open
NickSxti wants to merge 5 commits intodevelopfrom
feature/deferred-purchases-listener
Open

Add QDeferredPurchasesListener with QDeferredTransaction model#785
NickSxti wants to merge 5 commits intodevelopfrom
feature/deferred-purchases-listener

Conversation

@NickSxti
Copy link
Copy Markdown

Summary

  • New QDeferredPurchasesListener interface with deferredPurchaseCompleted(transaction) callback
  • New QDeferredTransaction data class with productId, transactionId, originalTransactionId, type, value, currency
  • Deprecated QEntitlementsUpdateListener interface, config builder method, and Qonversion setter
  • Both listeners fire in deferred purchase paths (API success + local fallback)
  • Public API on Qonversion interface + QonversionConfig.Builder

Architecture

Deferred purchase completes (pending -> purchased)
  -> API validation success: entitlementsUpdateListener fires (backward compat) + deferredPurchasesListener fires
  -> API validation failure (fallback): same dual notification

Files changed

  • QDeferredPurchasesListener.kt - new interface
  • QDeferredTransaction.kt - new data class
  • QEntitlementsUpdateListener.kt - deprecated
  • Qonversion.kt - new setter + deprecated old
  • QonversionConfig.kt - new builder method + deprecated old
  • InternalConfig.kt - new property
  • QProductCenterManager.kt - new setter + callback invocation at both deferred paths
  • QonversionInternal.kt - delegates to product center manager
  • DeferredPurchasesListenerTest.kt - 6 unit tests

Test plan

  • 6 unit tests written (setter, null, coexistence, isolation, data class validation)
  • Tests could not run locally (Java 8 only, needs 11+) - CI should validate

Note

value and currency are 0.0/null for Android because Google Play Purchase object doesn't expose price. These fields will be populated at the Sandwich/cross-platform layer using product catalog data.

DEV-643

Generated with Claude Code

- New QDeferredPurchasesListener interface with deferredPurchaseCompleted callback
- New QDeferredTransaction data class with productId, transactionId,
  originalTransactionId, type, value, currency
- Deprecated QEntitlementsUpdateListener (interface + config builder + setter)
- Both listeners fire in deferred purchase paths (API success + fallback)
- Public API on Qonversion + QonversionConfig.Builder
- 6 unit tests

DEV-643

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@NickSxti NickSxti requested a review from SpertsyanKM March 17, 2026 16:22
@NickSxti
Copy link
Copy Markdown
Author

LEVER Self-Review

Logic

  • Callback invocation at two deferred purchase paths:
    1. API success (line ~1067): when purchaseCallback == null (deferred), notifies both listeners
    2. Local fallback (line ~572): when callback is null after failed API + local permission grant, notifies both
  • QDeferredTransaction constructed from Google Play Purchase object: productId from purchase, purchaseToken as transactionId, orderId as originalTransactionId
  • value/currency are 0.0/null because Google Play Purchase doesn't expose price data. These will be enriched at the Sandwich/cross-platform layer using product catalog.

Edge Cases

  • If deferredPurchasesListener is null, the call is silently skipped (Kotlin null-safe ?.)
  • Both listeners are independent - setting one doesn't affect the other
  • The deprecated @Suppress("DEPRECATION") annotation prevents build warnings when we call the old listener internally
  • QDeferredTransaction is a data class - provides equals/hashCode/copy for free

Verify

  • New interface follows the pattern of QEntitlementsUpdateListener
  • Config builder follows existing setEntitlementsUpdateListener pattern
  • QonversionInternal delegates to QProductCenterManager
  • Deprecation annotations on interface, builder, and Qonversion setter
  • 6 unit tests written

Risks

  • Tests could not run locally (only Java 8 available, needs 11+). CI should validate.
  • Purchase.productId returns the first product ID for multi-product purchases (edge case for bundles)

Linear: DEV-665

…back)

- Create EntitlementsUpdateListenerAdapter that wraps QEntitlementsUpdateListener
  as QDeferredPurchasesListener, extracting entitlements from purchaseResult
- Change deferredPurchaseCompleted signature to include QPurchaseResult
  (a deferred purchase is a purchase, so the callback provides both
  transaction details and the purchase result with entitlements)
- Remove entitlementsUpdateListener from InternalConfig - only
  deferredPurchasesListener remains
- Delete unused EntitlementsUpdateListenerProvider interface
- Deprecated setters now wrap legacy listener via adapter
- Both deferred call sites in QProductCenterManager use single
  deferredPurchasesListener invocation (no more dual listener logic)
- Update tests for adapter behavior and removed properties

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
callback.onSuccess(user)
}

// Review feedback (Task 6): deprecated setter wraps legacy listener in adapter.
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.

No need to mention review feedback in comments

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed. Also cleaned up the other review-feedback comments throughout the PR.

* including product ID, transaction ID, type, value, and currency.
* @param purchaseResult the purchase result containing entitlements granted by this purchase.
*/
fun deferredPurchaseCompleted(transaction: QDeferredTransaction, purchaseResult: QPurchaseResult)
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.

What is the necessity of the DeferredTransaction here, if there is the PurchaseResult? I think all the information about the purchase can be taken from there

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. Removed QDeferredTransaction entirely. The listener now takes just QPurchaseResult:

interface QDeferredPurchasesListener {
    fun deferredPurchaseCompleted(purchaseResult: QPurchaseResult)
}

QPurchaseResult.purchase already has productId, purchaseToken, orderId, and the result itself has entitlements, status, source. No need for a separate model.

Also updated the Sandwich SDK Android mapper to use purchaseResult.toMap() directly.

internal val primaryConfig: PrimaryConfig,
internal val cacheConfig: CacheConfig,
internal val entitlementsUpdateListener: QEntitlementsUpdateListener?
// Review feedback (Task 6): removed entitlementsUpdateListener from config.
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.

here either

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed.

NickSxti and others added 2 commits March 24, 2026 18:37
Purchase.productId returns String? (via products.firstOrNull()) but
QDeferredTransaction.productId expects non-nullable String. Use the
same `?: ""` convention as PurchaseRecord.kt.

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

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

@SpertsyanKM Привет!
Внес правки, можешь повторно посмотреть, пожалуйста?

@NickSxti NickSxti force-pushed the feature/deferred-purchases-listener branch from d9da704 to 3320845 Compare March 25, 2026 14:44
deferredPurchaseCompleted is only ever called with successful
QPurchaseResult (via success() or successFromFallback()). All call
sites guard with null-callback checks. Document this explicitly so
the adapter contract is clear.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants