Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds product_ids to post receipt #335

Merged
merged 4 commits into from
Jul 9, 2021
Merged
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
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>,
Copy link
Contributor

Choose a reason for hiding this comment

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

i might just not be remembering correctly, but how can we have multiple product IDs but only one product details object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main reason why we are adding this new parameter to the call is so we can know when this goes live. There are still some questions regarding how this is going to work on the Google side, some of them change our data model in the backend. For now we just want to know if there ever is a transaction with more than one product ID to know when this is live.

There is going to be more future work around this, like adding the price, currency or durations of all products.

Copy link
Contributor

Choose a reason for hiding this comment

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

got it. perhaps we should outline that in the release notes, so people don't expect full functionality right away

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] }
Copy link
Contributor

Choose a reason for hiding this comment

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

would we ever get an empty list? if so, we should use skus.getOrNull(0)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The list will never be empty, it always has one product ID. It will be at least one product ID when multi line subscriptions are available.

Copy link
Member

Choose a reason for hiding this comment

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

perhaps we can add a comment so future devs know about this particular detail.
I mean, hopefully we'll have real multi-line subscription support soon, but in the meantime, folks reading through the code might be left wondering

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