Skip to content

Commit

Permalink
Use correct product context with DEFERRED purchases (#1766)
Browse files Browse the repository at this point in the history
### Description
Another followup to #1751 and #1764 

When making `DEFERRED` purchases, Google returns the result using the
old product id. We need to cache all the context information using the
old product id so we can attribute it correctly.
  • Loading branch information
tonidero authored Jun 28, 2024
1 parent a36b1cc commit b5bc297
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,8 @@ internal class PurchasesOrchestrator constructor(
}

if (!state.purchaseCallbacksByProductId.containsKey(purchasingData.productId)) {
// When using DEFERRED proration mode, callback needs to be associated with the *old* product we are
// switching from, because the transaction we receive on successful purchase is for the old product.
val productId =
if (googleReplacementMode == GoogleReplacementMode.DEFERRED) {
oldProductId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ internal class BillingWrapper(
@Volatile
var billingClient: BillingClient? = null

private val purchaseContext = mutableMapOf<String, PurchaseContext>()
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val purchaseContext = mutableMapOf<String, PurchaseContext>()

private val serviceRequests =
ConcurrentLinkedQueue<Pair<(connectionError: PurchasesError?) -> Unit, Long?>>()
Expand Down Expand Up @@ -266,7 +267,12 @@ internal class BillingWrapper(
}

synchronized(this@BillingWrapper) {
val productId = googlePurchasingData.productId
// When using DEFERRED proration mode, callback needs to be associated with the *old* product we are
// switching from, because the transaction we receive on successful purchase is for the old product.
val productId =
if (replaceProductInfo?.replacementMode == GoogleReplacementMode.DEFERRED) {
replaceProductInfo.oldPurchase.productIds.first()
} else googlePurchasingData.productId
purchaseContext[productId] = PurchaseContext(
googlePurchasingData.productType,
presentedOfferingContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,13 @@ class BillingWrapperTest {
} returns billingClientOKResult

val storeProduct = createStoreProductWithoutOffers()
val purchasingData = storeProduct.subscriptionOptions!!.first().purchasingData

billingClientStateListener!!.onBillingSetupFinished(billingClientOKResult)
wrapper.makePurchaseAsync(
mockActivity,
appUserId,
storeProduct.subscriptionOptions!!.first().purchasingData,
purchasingData,
mockReplaceSkuInfo(),
PresentedOfferingContext("offering_a"),
)
Expand All @@ -319,6 +320,50 @@ class BillingWrapperTest {
any()
)
}

assertThat(wrapper.purchaseContext.size).isEqualTo(1)
val purchaseContext = wrapper.purchaseContext[purchasingData.productId]
assertThat(purchaseContext).isNotNull
assertThat(purchaseContext?.productType).isEqualTo(ProductType.SUBS)
assertThat(purchaseContext?.presentedOfferingContext).isEqualTo(PresentedOfferingContext("offering_a"))
assertThat(purchaseContext?.selectedSubscriptionOptionId).isEqualTo(storeProduct.subscriptionOptions!!.first().id)
assertThat(purchaseContext?.replacementMode).isEqualTo(GoogleReplacementMode.CHARGE_FULL_PRICE)
}

@Test
fun `making a deferred purchase uses previous product id cached context`() {
every {
mockClient.launchBillingFlow(any(), any())
} returns billingClientOKResult

val storeProduct = createStoreProductWithoutOffers()
val purchasingData = storeProduct.subscriptionOptions!!.first().purchasingData
val oldPurchase = mockPurchaseHistoryRecordWrapper()
val replaceInfo = ReplaceProductInfo(oldPurchase, GoogleReplacementMode.DEFERRED)

billingClientStateListener!!.onBillingSetupFinished(billingClientOKResult)
wrapper.makePurchaseAsync(
mockActivity,
appUserId,
purchasingData,
replaceInfo,
PresentedOfferingContext("offering_a"),
)

verify {
mockClient.launchBillingFlow(
eq(mockActivity),
any()
)
}

assertThat(wrapper.purchaseContext.size).isEqualTo(1)
val purchaseContext = wrapper.purchaseContext[oldPurchase.productIds.first()]
assertThat(purchaseContext).isNotNull
assertThat(purchaseContext?.productType).isEqualTo(ProductType.SUBS)
assertThat(purchaseContext?.presentedOfferingContext).isEqualTo(PresentedOfferingContext("offering_a"))
assertThat(purchaseContext?.selectedSubscriptionOptionId).isEqualTo(storeProduct.subscriptionOptions!!.first().id)
assertThat(purchaseContext?.replacementMode).isEqualTo(GoogleReplacementMode.DEFERRED)
}

@Test
Expand Down

0 comments on commit b5bc297

Please sign in to comment.