Skip to content

Commit

Permalink
Support non consumables by listening to backend response
Browse files Browse the repository at this point in the history
  • Loading branch information
tonidero committed May 8, 2024
1 parent 61dcfdd commit 9d9c9bb
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.revenuecat.purchases.common.PostReceiptDataErrorCallback
import com.revenuecat.purchases.common.PostReceiptErrorHandlingBehavior
import com.revenuecat.purchases.common.ReceiptInfo
import com.revenuecat.purchases.common.caching.DeviceCache
import com.revenuecat.purchases.common.networking.PostReceiptResponse
import com.revenuecat.purchases.common.offlineentitlements.OfflineEntitlementsManager
import com.revenuecat.purchases.models.StoreProduct
import com.revenuecat.purchases.models.StoreTransaction
Expand Down Expand Up @@ -52,9 +53,9 @@ internal class PostReceiptHelper(
storeUserID,
marketplace,
initiationSource,
onSuccess = {
onSuccess = { postReceiptResponse ->
deviceCache.addSuccessfullyPostedToken(purchaseToken)
onSuccess(it)
onSuccess(postReceiptResponse.customerInfo)
},
onError = { backendError, errorHandlingBehavior, _ ->
if (errorHandlingBehavior == PostReceiptErrorHandlingBehavior.SHOULD_BE_CONSUMED) {
Expand Down Expand Up @@ -102,13 +103,23 @@ internal class PostReceiptHelper(
storeUserID = purchase.storeUserID,
marketplace = purchase.marketplace,
initiationSource = initiationSource,
onSuccess = { info ->
billing.consumeAndSave(finishTransactions, purchase, initiationSource)
onSuccess?.let { it(purchase, info) }
onSuccess = { postReceiptResponse ->
// Currently we only support a single token per postReceipt call but multiple product Ids.
// The backend would fail if given more than one product id (multiline purchases which are
// not supported) so it's safe to pickup the first one.
// We would need to refactor this if/when we support multiple tokens per call.
val shouldConsume = postReceiptResponse.productInfoByProductId
?.filterKeys { it in purchase.productIds }
?.values
?.firstOrNull()
?.shouldConsume
?: true
billing.consumeAndSave(finishTransactions, purchase, shouldConsume, initiationSource)
onSuccess?.let { it(purchase, postReceiptResponse.customerInfo) }
},
onError = { backendError, errorHandlingBehavior, _ ->
if (errorHandlingBehavior == PostReceiptErrorHandlingBehavior.SHOULD_BE_CONSUMED) {
billing.consumeAndSave(finishTransactions, purchase, initiationSource)
billing.consumeAndSave(finishTransactions, purchase, shouldConsume = true, initiationSource)
}
useOfflineEntitlementsCustomerInfoIfNeeded(
errorHandlingBehavior,
Expand All @@ -133,7 +144,7 @@ internal class PostReceiptHelper(
storeUserID: String?,
marketplace: String?,
initiationSource: PostReceiptInitiationSource,
onSuccess: (CustomerInfo) -> Unit,
onSuccess: (PostReceiptResponse) -> Unit,
onError: PostReceiptDataErrorCallback,
) {
val presentedPaywall = paywallPresentedCache.getAndRemovePresentedEvent()
Expand All @@ -149,15 +160,15 @@ internal class PostReceiptHelper(
marketplace = marketplace,
initiationSource = initiationSource,
paywallPostReceiptData = presentedPaywall?.toPaywallPostReceiptData(),
onSuccess = { customerInfo, responseBody ->
onSuccess = { postReceiptResponse ->
offlineEntitlementsManager.resetOfflineCustomerInfoCache()
subscriberAttributesManager.markAsSynced(
appUserID,
unsyncedSubscriberAttributesByKey,
responseBody.getAttributeErrors(),
postReceiptResponse.body.getAttributeErrors(),
)
customerInfoUpdateHandler.cacheAndNotifyListeners(customerInfo)
onSuccess(customerInfo)
customerInfoUpdateHandler.cacheAndNotifyListeners(postReceiptResponse.customerInfo)
onSuccess(postReceiptResponse)
},
onError = { error, errorHandlingBehavior, responseBody ->
presentedPaywall?.let { paywallPresentedCache.cachePresentedPaywall(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,17 @@ internal class AmazonBilling(
// endregion

override fun consumeAndSave(
shouldTryToConsume: Boolean,
finishTransactions: Boolean,
purchase: StoreTransaction,
shouldConsume: Boolean,
initiationSource: PostReceiptInitiationSource,
) {
if (checkObserverMode() || purchase.type == RevenueCatProductType.UNKNOWN) return

// PENDING purchases should not be fulfilled
if (purchase.purchaseState == PurchaseState.PENDING) return

if (shouldTryToConsume) {
if (finishTransactions) {
executeRequestOnUIThread { connectionError ->
if (connectionError == null) {
purchasingServiceProvider.notifyFulfillment(purchase.purchaseToken, FulfillmentResult.FULFILLED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.common.networking.Endpoint
import com.revenuecat.purchases.common.networking.HTTPResult
import com.revenuecat.purchases.common.networking.PostReceiptResponse
import com.revenuecat.purchases.common.networking.RCHTTPStatusCodes
import com.revenuecat.purchases.common.networking.buildPostReceiptResponse
import com.revenuecat.purchases.common.offlineentitlements.ProductEntitlementMapping
import com.revenuecat.purchases.common.verification.SignatureVerificationMode
import com.revenuecat.purchases.models.GoogleReplacementMode
Expand Down Expand Up @@ -44,7 +46,7 @@ internal typealias CallbackCacheKey = List<String>
internal typealias OfferingsCallback = Pair<(JSONObject) -> Unit, (PurchasesError, isServerError: Boolean) -> Unit>

/** @suppress */
internal typealias PostReceiptDataSuccessCallback = (CustomerInfo, body: JSONObject) -> Unit
internal typealias PostReceiptDataSuccessCallback = (PostReceiptResponse) -> Unit

/** @suppress */
internal typealias PostReceiptDataErrorCallback = (
Expand Down Expand Up @@ -256,7 +258,7 @@ internal class Backend(
}?.forEach { (onSuccess, onError) ->
try {
if (result.isSuccessful()) {
onSuccess(CustomerInfoFactory.buildCustomerInfo(result), result.body)
onSuccess(buildPostReceiptResponse(result))
} else {
val purchasesError = result.toPurchasesError().also { errorLog(it) }
val errorHandlingBehavior = determinePostReceiptErrorHandlingBehavior(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ internal abstract class BillingAbstract(
)

abstract fun consumeAndSave(
shouldTryToConsume: Boolean,
finishTransactions: Boolean,
purchase: StoreTransaction,
shouldConsume: Boolean,
initiationSource: PostReceiptInitiationSource,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ internal class HTTPClient(
HTTPRequest.POST_PARAMS_HASH to postFieldsToSignHeader,
"X-Custom-Entitlements-Computation" to if (appConfig.customEntitlementComputation) "true" else null,
"X-Storefront" to storefrontProvider.getStorefront(),
"X-RC-Canary" to "iapconsum",
)
.plus(authenticationHeaders)
.plus(eTagManager.getETagHeaders(urlPath, shouldSignResponse, refreshETag))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.revenuecat.purchases.common.networking

import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.common.CustomerInfoFactory
import com.revenuecat.purchases.utils.toMap
import org.json.JSONObject

internal fun buildPostReceiptResponse(result: HTTPResult) = PostReceiptResponse(
customerInfo = CustomerInfoFactory.buildCustomerInfo(result),
productInfoByProductId = result.body.optJSONObject("purchased_products")?.let {
it.toMap<JSONObject>().mapValues { (_, value) ->
PostReceiptProductInfo(
shouldConsume = value.optBoolean("should_consume"),
)
}
},
body = result.body,
)

internal data class PostReceiptProductInfo(
val shouldConsume: Boolean,
)

internal data class PostReceiptResponse(
val customerInfo: CustomerInfo,
val productInfoByProductId: Map<String, PostReceiptProductInfo>?,
val body: JSONObject,
)
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,9 @@ internal class BillingWrapper(
}

override fun consumeAndSave(
shouldTryToConsume: Boolean,
finishTransactions: Boolean,
purchase: StoreTransaction,
shouldConsume: Boolean,
initiationSource: PostReceiptInitiationSource,
) {
if (purchase.type == ProductType.UNKNOWN) {
Expand All @@ -364,13 +365,13 @@ internal class BillingWrapper(

val originalGooglePurchase = purchase.originalGooglePurchase
val alreadyAcknowledged = originalGooglePurchase?.isAcknowledged ?: false
if (shouldTryToConsume && purchase.type == ProductType.INAPP) {
if (finishTransactions && shouldConsume && purchase.type == ProductType.INAPP) {
consumePurchase(
purchase.purchaseToken,
initiationSource,
onConsumed = deviceCache::addSuccessfullyPostedToken,
)
} else if (shouldTryToConsume && !alreadyAcknowledged) {
} else if (finishTransactions && !alreadyAcknowledged) {
acknowledge(
purchase.purchaseToken,
initiationSource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ internal open class BasePurchasesTest {
protected var capturedPurchasesUpdatedListener = slot<BillingAbstract.PurchasesUpdatedListener>()
protected var capturedBillingWrapperStateListener = slot<BillingAbstract.StateListener>()
private val capturedConsumePurchaseWrapper = slot<StoreTransaction>()
private val capturedShouldTryToConsume = slot<Boolean>()
private val capturedFinishedTransactions = slot<Boolean>()
private val capturedShouldConsume = slot<Boolean>()

protected lateinit var paywallPresentedCache: PaywallPresentedCache

Expand Down Expand Up @@ -147,8 +148,9 @@ internal open class BasePurchasesTest {
} just Runs
every {
consumeAndSave(
capture(capturedShouldTryToConsume),
capture(capturedFinishedTransactions),
capture(capturedConsumePurchaseWrapper),
capture(capturedShouldConsume),
any(),
)
} just Runs
Expand Down
Loading

0 comments on commit 9d9c9bb

Please sign in to comment.