Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ internal class BillingWrapper(
subscriptionOptionId,
replaceProductInfo?.replacementMode.toStoreReplacementModeOrNull(),
subscriptionOptionIdForProductIDs,
initiationDate = dateProvider.now,
)
}
executeRequestOnUIThread {
Expand Down Expand Up @@ -531,7 +532,7 @@ internal class BillingWrapper(
diagnosticsTrackerIfEnabled,
appInBackground,
),
onSuccess,
{ transactions -> onSuccess(attachCachedPurchaseContext(transactions)) },
onError,
::withConnectedClient,
::executeRequestOnUIThread,
Expand Down Expand Up @@ -872,6 +873,29 @@ internal class BillingWrapper(
return stringWriter.toString()
}

private fun attachCachedPurchaseContext(
transactions: Map<String, StoreTransaction>,
): Map<String, StoreTransaction> {
return transactions.mapValues { (_, transaction) ->
if (transaction.presentedOfferingContext != null) {
transaction
} else {
val productId = transaction.productIds.firstOrNull()
val context = synchronized(this@BillingWrapper) {
productId?.let { purchaseContext[it] }
}
if (context != null && transaction.purchaseTime >= context.initiationDate.time) {
log(LogIntent.DEBUG) {
BillingStrings.BILLING_WRAPPER_ATTACHING_CACHED_PURCHASE_CONTEXT.format(productId)
}
transaction.originalGooglePurchase?.toStoreTransaction(context) ?: transaction
} else {
transaction
}
}
}
}

private fun getStoreTransaction(
purchase: Purchase,
completion: (storeTxn: StoreTransaction) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package com.revenuecat.purchases.google
import com.revenuecat.purchases.PresentedOfferingContext
import com.revenuecat.purchases.ProductType
import com.revenuecat.purchases.models.StoreReplacementMode
import java.util.Date

internal class PurchaseContext(
val productType: ProductType,
val presentedOfferingContext: PresentedOfferingContext?,
val selectedSubscriptionOptionId: String?,
val replacementMode: StoreReplacementMode?,
val subscriptionOptionIdForProductIDs: Map<String, String>?,
val initiationDate: Date,
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal object BillingStrings {
"Original error message: %s"
const val BILLING_WRAPPER_PURCHASES_ERROR = "BillingWrapper purchases failed to update: %s"
const val BILLING_WRAPPER_PURCHASES_UPDATED = "BillingWrapper purchases updated: %s"
const val BILLING_WRAPPER_ATTACHING_CACHED_PURCHASE_CONTEXT =
"Attaching cached purchase context for product %s"
const val BILLING_PURCHASE_HISTORY_RECORD_MORE_THAN_ONE_SKU = "There's more than one sku in the " +
"PurchaseHistoryRecord, but only one will be used."
const val CANNOT_CALL_CAN_MAKE_PAYMENTS = "canMakePayments requires the Google Play Store. Skipping " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,104 @@ class BillingWrapperTest {

// endregion diagnostics tracking

// region queryPurchases purchase context attachment

@Test
fun `queryPurchases attaches cached presented offering context when purchase time is at or after initiation date`() {
val productId = "com.revenuecat.lifetime"
val presentedOfferingContext = PresentedOfferingContext("offering_a")
val initiationDate = Date(timestamp0)
val purchaseTime = timestamp123

wrapper.purchaseContext[productId] = PurchaseContext(
ProductType.SUBS,
presentedOfferingContext,
null,
null,
null,
initiationDate,
)

val purchase = stubGooglePurchase(productIds = listOf(productId), purchaseTime = purchaseTime)
mockClient.mockQueryPurchasesAsync(
billingClientOKResult,
billingClientOKResult,
listOf(purchase),
emptyList(),
)

var purchasesByHashedToken: Map<String, StoreTransaction>? = null
wrapper.queryPurchases(
appUserID = appUserId,
onSuccess = { purchasesByHashedToken = it },
onError = { fail("should be a success") },
)

assertThat(purchasesByHashedToken).isNotNull
val transaction = purchasesByHashedToken!!.values.first()
assertThat(transaction.presentedOfferingContext).isEqualTo(presentedOfferingContext)
}

@Test
fun `queryPurchases does not attach cached presented offering context when purchase time is before initiation date`() {

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.

Should we also have a test for when there is a cached context for a different productId? (Shouldn't attach I think)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ohh true. Good call!

val productId = "com.revenuecat.lifetime"
val initiationDate = Date(timestamp123)
val purchaseTime = timestamp0

wrapper.purchaseContext[productId] = PurchaseContext(
ProductType.SUBS,
PresentedOfferingContext("offering_a"),
null,
null,
null,
initiationDate,
)

val purchase = stubGooglePurchase(productIds = listOf(productId), purchaseTime = purchaseTime)
mockClient.mockQueryPurchasesAsync(
billingClientOKResult,
billingClientOKResult,
listOf(purchase),
emptyList(),
)

var purchasesByHashedToken: Map<String, StoreTransaction>? = null
wrapper.queryPurchases(
appUserID = appUserId,
onSuccess = { purchasesByHashedToken = it },
onError = { fail("should be a success") },
)

assertThat(purchasesByHashedToken).isNotNull
val transaction = purchasesByHashedToken!!.values.first()
assertThat(transaction.presentedOfferingContext).isNull()
}

@Test
fun `queryPurchases returns transaction unchanged when no cached purchase context exists`() {
val productId = "com.revenuecat.lifetime"
val purchase = stubGooglePurchase(productIds = listOf(productId), purchaseTime = timestamp123)
mockClient.mockQueryPurchasesAsync(
billingClientOKResult,
billingClientOKResult,
listOf(purchase),
emptyList(),
)

var purchasesByHashedToken: Map<String, StoreTransaction>? = null
wrapper.queryPurchases(
appUserID = appUserId,
onSuccess = { purchasesByHashedToken = it },
onError = { fail("should be a success") },
)

assertThat(purchasesByHashedToken).isNotNull
val transaction = purchasesByHashedToken!!.values.first()
assertThat(transaction.presentedOfferingContext).isNull()
}

// endregion queryPurchases purchase context attachment

// region inapp messages

@Test
Expand Down