Skip to content

Commit

Permalink
Adds product_ids to post receipt (#335)
Browse files Browse the repository at this point in the history
  • Loading branch information
vegaro authored Jul 9, 2021
1 parent 8e6c6db commit afa4870
Show file tree
Hide file tree
Showing 22 changed files with 205 additions and 134 deletions.
2 changes: 1 addition & 1 deletion .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class Backend(

val body = mapOf(
"fetch_token" to purchaseToken,
"product_id" to receiptInfo.productID,
"product_ids" to receiptInfo.productIDs,
"app_user_id" to appUserID,
"is_restore" to isRestore,
"presented_offering_identifier" to receiptInfo.offeringIdentifier,
Expand Down Expand Up @@ -194,9 +194,11 @@ class Backend(
if (result.isSuccessful()) {
onSuccess(result.body.buildPurchaserInfo(), result.body)
} else {
val purchasesError = result.toPurchasesError().also { errorLog(it) }
onError(
result.toPurchasesError().also { errorLog(it) },
result.responseCode < RCHTTPStatusCodes.ERROR,
purchasesError,
result.responseCode < RCHTTPStatusCodes.ERROR &&
purchasesError.code != PurchasesErrorCode.UnsupportedError,
result.body
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.revenuecat.purchases.common
import com.revenuecat.purchases.models.ProductDetails

class ReceiptInfo(
val productID: String,
val productIDs: List<String>,
val offeringIdentifier: String? = null,
val productDetails: ProductDetails? = null
) {
Expand All @@ -20,7 +20,7 @@ class ReceiptInfo(

other as ReceiptInfo

if (productID != other.productID) return false
if (productIDs != other.productIDs) return false
if (offeringIdentifier != other.offeringIdentifier) return false
if (price != other.price) return false
if (currency != other.currency) return false
Expand All @@ -32,15 +32,15 @@ class ReceiptInfo(
}

override fun hashCode(): Int {
var result = productID.hashCode()
var result = productIDs.hashCode()
result = 31 * result + (offeringIdentifier?.hashCode() ?: 0)
result = 31 * result + (productDetails?.hashCode() ?: 0)
return result
}

override fun toString(): String {
return "ReceiptInfo(" +
"productID='$productID', " +
"productIDs='${productIDs.joinToString()}', " +
"offeringIdentifier=$offeringIdentifier, " +
"price=$price, " +
"currency=$currency, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ enum class BackendErrorCode(val value: Int) {
BackendUserIneligibleForPromoOffer(7232),
BackendInvalidAppleSubscriptionKey(7234),
BackendInvalidSubscriberAttributes(7263),
BackendInvalidSubscriberAttributesBody(7264);
BackendInvalidSubscriberAttributesBody(7264),
BackendProductIDsMalformed(7662);

companion object {
fun valueOf(backendErrorCode: Int): BackendErrorCode? {
Expand Down Expand Up @@ -85,6 +86,7 @@ fun BackendErrorCode.toPurchasesErrorCode(): PurchasesErrorCode {
BackendErrorCode.BackendInvalidAppleSubscriptionKey,
BackendErrorCode.BackendBadRequest,
BackendErrorCode.BackendInternalServerError -> PurchasesErrorCode.UnexpectedBackendResponseError
BackendErrorCode.BackendProductIDsMalformed -> PurchasesErrorCode.UnsupportedError
}
}

Expand Down
73 changes: 48 additions & 25 deletions common/src/test/java/com/revenuecat/purchases/common/BackendTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class BackendTest {
private var receivedCreated: Boolean? = null
private var receivedOfferingsJSON: JSONObject? = null
private var receivedError: PurchasesError? = null
private var receivedShouldConsumePurchase: Boolean? = null

private val onReceivePurchaserInfoSuccessHandler: (PurchaserInfo) -> Unit = { info ->
this@BackendTest.receivedPurchaserInfo = info
Expand All @@ -83,8 +84,9 @@ class BackendTest {
}

private val postReceiptErrorCallback: (PurchasesError, Boolean, JSONObject?) -> Unit =
{ error, _, _ ->
{ error, shouldConsumePurchase, _ ->
this@BackendTest.receivedError = error
this@BackendTest.receivedShouldConsumePurchase = shouldConsumePurchase
}

private val onReceivePurchaserInfoErrorHandler: (PurchasesError) -> Unit = {
Expand Down Expand Up @@ -188,7 +190,7 @@ class BackendTest {
return info
}

private val productID = "product_id"
private val productIDs = listOf("product_id_0", "product_id_1")

private fun mockPostReceiptResponse(
isRestore: Boolean,
Expand All @@ -204,7 +206,7 @@ class BackendTest {
val body = mapOf(
"fetch_token" to fetchToken,
"app_user_id" to appUserID,
"product_id" to receiptInfo.productID,
"product_ids" to receiptInfo.productIDs,
"is_restore" to isRestore,
"presented_offering_identifier" to receiptInfo.offeringIdentifier,
"observer_mode" to observerMode,
Expand Down Expand Up @@ -298,7 +300,7 @@ class BackendTest {
clientException = null,
resultBody = null,
observerMode = false,
receiptInfo = ReceiptInfo(productID),
receiptInfo = ReceiptInfo(productIDs),
storeAppUserID = null
)

Expand All @@ -314,7 +316,7 @@ class BackendTest {
clientException = null,
resultBody = null,
observerMode = false,
receiptInfo = ReceiptInfo(productID),
receiptInfo = ReceiptInfo(productIDs),
storeAppUserID = null
)

Expand Down Expand Up @@ -444,7 +446,7 @@ class BackendTest {
@Test
fun `given multiple post calls for same subscriber, only one is triggered`() {
val receiptInfo = ReceiptInfo(
productID = productID,
productIDs,
offeringIdentifier = "offering_a"
)
val (fetchToken, _) = mockPostReceiptResponse(
Expand All @@ -455,7 +457,7 @@ class BackendTest {
delayed = true,
observerMode = false,
receiptInfo = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_a"
),
storeAppUserID = null
Expand All @@ -476,7 +478,7 @@ class BackendTest {
onError = postReceiptErrorCallback
)
val productInfo1 = ReceiptInfo(
productID = productID,
productIDs,
offeringIdentifier = "offering_a"
)
asyncBackend.postReceiptData(
Expand Down Expand Up @@ -576,14 +578,14 @@ class BackendTest {
delayed = true,
observerMode = false,
receiptInfo = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_a"
),
storeAppUserID = null
)
val lock = CountDownLatch(2)
val receiptInfo = ReceiptInfo(
productID = productID,
productIDs,
offeringIdentifier = "offering_a"
)
asyncBackend.postReceiptData(
Expand All @@ -607,13 +609,13 @@ class BackendTest {
delayed = true,
observerMode = false,
receiptInfo = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_b"
),
storeAppUserID = null
)
val productInfo1 = ReceiptInfo(
productID = productID,
productIDs,
offeringIdentifier = "offering_b"
)
asyncBackend.postReceiptData(
Expand Down Expand Up @@ -648,7 +650,7 @@ class BackendTest {
clientException = null,
resultBody = null,
observerMode = true,
receiptInfo = ReceiptInfo(productID),
receiptInfo = ReceiptInfo(productIDs),
storeAppUserID = null
)

Expand All @@ -667,7 +669,7 @@ class BackendTest {
resultBody = null,
observerMode = true,
receiptInfo = ReceiptInfo(
productID,
productIDs,
productDetails = productDetails
),
storeAppUserID = null
Expand All @@ -687,7 +689,7 @@ class BackendTest {
delayed = true,
observerMode = false,
receiptInfo = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_a"
),
storeAppUserID = null
Expand All @@ -696,12 +698,12 @@ class BackendTest {
val productDetails = mockProductDetails()
val productDetails1 = mockProductDetails(price = 350000)
val receiptInfo = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_a",
productDetails = productDetails
)
val productInfo1 = ReceiptInfo(
productID = productID,
productIDs,
offeringIdentifier = "offering_a",
productDetails = productDetails1
)
Expand Down Expand Up @@ -773,7 +775,7 @@ class BackendTest {
delayed = true,
observerMode = false,
receiptInfo = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_a"
),
storeAppUserID = null
Expand All @@ -783,12 +785,12 @@ class BackendTest {
val productDetails1 = mockProductDetails(duration = "P2M")

val receiptInfo = ReceiptInfo(
productID = productID,
productIDs,
offeringIdentifier = "offering_a",
productDetails = productDetails
)
val productInfo1 = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_a",
productDetails = productDetails1
)
Expand Down Expand Up @@ -855,7 +857,7 @@ class BackendTest {
val productDetails = mockProductDetails()

val receiptInfo = ReceiptInfo(
productID,
productIDs,
offeringIdentifier = "offering_a",
productDetails = productDetails
)
Expand Down Expand Up @@ -923,7 +925,7 @@ class BackendTest {
resultBody = null,
observerMode = true,
receiptInfo = ReceiptInfo(
productID = productID,
productIDs,
productDetails = productDetails
),
storeAppUserID = null,
Expand Down Expand Up @@ -1110,9 +1112,7 @@ class BackendTest {
fun `given multiple post calls for same subscriber different store user ID, both are triggered`() {

val lock = CountDownLatch(2)
val receiptInfo = ReceiptInfo(
productID = productID
)
val receiptInfo = ReceiptInfo(productIDs)
val (fetchToken, _) = mockPostReceiptResponse(
isRestore = false,
responseCode = 200,
Expand Down Expand Up @@ -1179,6 +1179,29 @@ class BackendTest {
}
}

@Test
fun `postReceipt calls fail for multiple product ids errors`() {
postReceipt(
responseCode = 400,
isRestore = false,
clientException = null,
resultBody = """
{"code":7662,
"message":"The product IDs list provided is not an array or does not contain only a single element."
}""".trimIndent(),
observerMode = false,
receiptInfo = ReceiptInfo(productIDs),
storeAppUserID = null
)

assertThat(receivedPurchaserInfo).`as`("Received info is null").isNull()
assertThat(receivedError).`as`("Received error is not null").isNotNull
assertThat(receivedError!!.code)
.`as`("Received error code is the right one")
.isEqualTo(PurchasesErrorCode.UnsupportedError)
assertThat(receivedShouldConsumePurchase).`as`("Purchase shouldn't be consumed").isFalse()
}

private fun mockProductDetails(
price: Long = 25000000,
duration: String = "P1M",
Expand Down
3 changes: 2 additions & 1 deletion config/detekt/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<ID>LongMethod:Purchases.kt$Purchases$ fun restorePurchases( listener: ReceivePurchaserInfoListener )</ID>
<ID>LongParameterList:EntitlementInfo.kt$EntitlementInfo$( val identifier: String, val isActive: Boolean, val willRenew: Boolean, val periodType: PeriodType, val latestPurchaseDate: Date, val originalPurchaseDate: Date, val expirationDate: Date?, val store: Store, val productIdentifier: String, val isSandbox: Boolean, val unsubscribeDetectedAt: Date?, val billingIssueDetectedAt: Date? )</ID>
<ID>LongParameterList:ProductDetails.kt$ProductDetails$( /** * The product ID. */ val sku: String, /** * Type of product. One of [ProductType]. */ val type: ProductType, /** * Formatted price of the item, including its currency sign. For example $3.00. */ val price: String, /** * Price in micro-units, where 1,000,000 micro-units equal one unit of the currency. * * For example, if price is "€7.99", price_amount_micros is 7,990,000. This value represents * the localized, rounded price for a particular currency. */ val priceAmountMicros: Long, /** * Returns ISO 4217 currency code for price and original price. * * For example, if price is specified in British pounds sterling, price_currency_code is "GBP". */ val priceCurrencyCode: String, /** * Formatted original price of the item, including its currency sign. * * Note: returned only for Google products. */ val originalPrice: String?, /** * Returns the original price in micro-units, where 1,000,000 micro-units equal one unit * of the currency. * * Note: returned only for Google products. */ val originalPriceAmountMicros: Long, /** * Title of the product. */ val title: String, /** * The description of the product. */ val description: String, /** * Subscription period, specified in ISO 8601 format. For example, P1W equates to one week, * P1M equates to one month, P3M equates to three months, P6M equates to six months, * and P1Y equates to one year. * * Note: Returned only for subscriptions. */ val subscriptionPeriod: String?, /** * Subscription period, specified in ISO 8601 format. For example, P1W equates to one week, * P1M equates to one month, P3M equates to three months, P6M equates to six months, * and P1Y equates to one year. * * Note: null for non subscriptions. */ val freeTrialPeriod: String?, /** * The billing period of the introductory price, specified in ISO 8601 format. * * Note: Returned only for Google subscriptions which have an introductory period configured. */ val introductoryPrice: String?, /** * Introductory price in micro-units. The currency is the same as price_currency_code. * * Note: Returns 0 if the product is not Google a subscription or doesn't * have an introductory period. */ val introductoryPriceAmountMicros: Long, /** * The billing period of the introductory price, specified in ISO 8601 format. * * Note: Returned only for Google subscriptions which have an introductory period configured. */ val introductoryPricePeriod: String?, /** * The number of subscription billing periods for which the user will be given the * introductory price, such as 3. * * Note: Returns 0 if the SKU is not a Google subscription or doesn't * have an introductory period. */ val introductoryPriceCycles: Int, /** * The icon of the product if present. */ val iconUrl: String, /** * JSONObject representing the original product class from Google. * * Note: there's a convenience extension property that can be used to get the original * SkuDetails class: `ProductDetails.skuDetails`. * Alternatively, the original SkuDetails can be built doing the following: * `SkuDetails(this.originalJson.toString())` */ val originalJson: JSONObject )</ID>
<ID>LongParameterList:StubGooglePurchase.kt$( productId: String = "com.revenuecat.lifetime", purchaseTime: Long = System.currentTimeMillis(), purchaseToken: String = "abcdefghijipehfnbbnldmai.AO-J1OxqriTepvB7suzlIhxqPIveA0IHtX9amMedK0KK9CsO0S3Zk5H6gdwvV" + "7HzZIJeTzqkY4okyVk8XKTmK1WZKAKSNTKop4dgwSmFnLWsCxYbahUmADg", signature: String = "signature${System.currentTimeMillis()}", purchaseState: Int = Purchase.PurchaseState.PURCHASED, acknowledged: Boolean = true, orderId: String = "GPA.3372-4150-8203-17209" )</ID>
<ID>LongParameterList:StubGooglePurchase.kt$( productIds: List&lt;String&gt; = listOf("com.revenuecat.lifetime"), purchaseTime: Long = System.currentTimeMillis(), purchaseToken: String = "abcdefghijipehfnbbnldmai.AO-J1OxqriTepvB7suzlIhxqPIveA0IHtX9amMedK0KK9CsO0S3Zk5H6gdwvV" + "7HzZIJeTzqkY4okyVk8XKTmK1WZKAKSNTKop4dgwSmFnLWsCxYbahUmADg", signature: String = "signature${System.currentTimeMillis()}", purchaseState: Int = Purchase.PurchaseState.PURCHASED, acknowledged: Boolean = true, orderId: String = "GPA.3372-4150-8203-17209" )</ID>
<ID>MagicNumber:SampleWeatherData.kt$SampleWeatherData.Companion$120</ID>
<ID>MagicNumber:SampleWeatherData.kt$SampleWeatherData.Companion$20</ID>
<ID>MagicNumber:SampleWeatherData.kt$SampleWeatherData.Companion$32</ID>
Expand All @@ -40,6 +40,7 @@
<ID>MagicNumber:errors.kt$BackendErrorCode.BackendPlayStoreGenericError$7231</ID>
<ID>MagicNumber:errors.kt$BackendErrorCode.BackendPlayStoreInvalidPackageName$7230</ID>
<ID>MagicNumber:errors.kt$BackendErrorCode.BackendPlayStoreQuotaExceeded$7229</ID>
<ID>MagicNumber:errors.kt$BackendErrorCode.BackendProductIDsMalformed$7662</ID>
<ID>MagicNumber:errors.kt$BackendErrorCode.BackendProductIdForGoogleReceiptNotProvided$7106</ID>
<ID>MagicNumber:errors.kt$BackendErrorCode.BackendStoreProblem$7101</ID>
<ID>MagicNumber:errors.kt$BackendErrorCode.BackendUserIneligibleForPromoOffer$7232</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ internal class AmazonBilling constructor(
queryAllPurchases(
appUserID,
onReceivePurchaseHistory = {
val record: PurchaseDetails? = it.firstOrNull { record -> sku == record.sku }
// We get skus[0] because the list is guaranteed to have just one item in Amazon's case.
val record: PurchaseDetails? = it.firstOrNull { record -> sku == record.skus[0] }
if (record != null) {
onCompletion(record)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fun Receipt.toRevenueCatPurchaseDetails(
val type = this.productType.toRevenueCatProductType()
return PurchaseDetails(
orderId = null,
sku = sku,
skus = listOf(sku),
type = type,
purchaseTime = this.purchaseDate.time,
purchaseToken = this.receiptId,
Expand Down
Loading

0 comments on commit afa4870

Please sign in to comment.