From bff6bdb5a701b31cb5e9ebe4397a242207c4dc60 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Wed, 21 Apr 2021 21:51:57 +0300 Subject: [PATCH 01/12] APH-661 Refactoring of Purchase methods --- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 49 ++++- .../java/com/apphud/sdk/ApphudInternal.kt | 194 +++++++++++++++--- .../com/apphud/sdk/ApphudPurchaseResult.kt | 45 ++++ .../apphud/sdk/internal/AcknowledgeWrapper.kt | 9 +- .../com/apphud/sdk/internal/BillingWrapper.kt | 4 +- .../com/apphud/sdk/internal/ConsumeWrapper.kt | 10 +- .../apphud/sdk/internal/PurchasesUpdated.kt | 5 +- 7 files changed, 266 insertions(+), 50 deletions(-) create mode 100644 sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 2f0ea750..33a10042 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -158,25 +158,58 @@ object Apphud { } /** - * Purchases product and automatically submit + * Purchases product and automatically submit Play Market Receipt to Apphud + * @param activity: current Activity for use * @param productId: The identifier of the product you wish to purchase - * @param block: The closure that will be called when purchase completes. + * @param block: Optional. Returns `ApphudPurchaseResult` object. */ @kotlin.jvm.JvmStatic - fun purchase(activity: Activity, productId: String, block: (List) -> Unit) = - ApphudInternal.purchase(activity, productId, block) + fun purchase(activity: Activity, productId: String, block: ((ApphudPurchaseResult) -> Unit)?) = + ApphudInternal.purchase(activity, productId, true, block) /** - * Purchases product and automatically submit + * Purchases product and automatically submit Play Market Receipt to Apphud + * * @param activity current Activity for use - * @param details The skuDetails of the product you wish to purchase - * @param block The closure that will be called when purchase completes. + * @param details The SkuDetails of the product you wish to purchase + * @param block Optional. Returns `ApphudPurchaseResult` object. */ @kotlin.jvm.JvmStatic - fun purchase(activity: Activity, details: SkuDetails, block: (List) -> Unit) = + fun purchase(activity: Activity, details: SkuDetails, block: ((ApphudPurchaseResult) -> Unit)?) = ApphudInternal.purchase(activity, details, block) + /** + * Purchases product and automatically submit Play Market Receipt to Apphud + * + * This method doesn't wait until Apphud validates receipt from Play Market and immediately returns transaction object. + * This method may be useful if you don't care about receipt validation in callback. + * + * When using this method properties `subscription` and `nonRenewingPurchase` in `ApphudPurchaseResult` will always be `null` ! + * + * @param activity: current Activity for use + * @param productId: The identifier of the product you wish to purchase + * @param block: The closure that will be called when purchase completes. + */ + @kotlin.jvm.JvmStatic + fun purchaseWithoutValidation(activity: Activity, productId: String, block: ((ApphudPurchaseResult) -> Unit)?) = + ApphudInternal.purchase(activity, productId, false, block) + + /** + * Purchases product and automatically submit Play Market Receipt to Apphud + * + * This method doesn't wait until Apphud validates receipt from Play Market and immediately returns transaction object. + * This method may be useful if you don't care about receipt validation in callback. + * + * When using this method properties `subscription` and `nonRenewingPurchase` in `ApphudPurchaseResult` will always be `null` ! + * + * @param activity current Activity for use + * @param details The SkuDetails of the product you wish to purchase + * @param block The closure that will be called when purchase completes. + */ + @kotlin.jvm.JvmStatic + fun purchaseWithoutValidation(activity: Activity, details: SkuDetails, block: ((ApphudPurchaseResult) -> Unit)?) = + ApphudInternal.purchaseWithoutValidation(activity, details, block) /** * Set custom user property. diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 591c7c45..bff58931 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -177,25 +177,49 @@ internal object ApphudInternal { } internal fun purchase( - activity: Activity, - productId: String, - callback: (List) -> Unit + activity: Activity, + productId: String, + withValidation: Boolean = true, + callback: ((ApphudPurchaseResult) -> Unit)? ) { val sku = getSkuDetailsByProductId(productId) if (sku != null) { - purchase(activity, sku, callback) + when { + withValidation -> purchase(activity, sku, callback) + else -> purchaseWithoutValidation(activity, sku, callback) + } } else { ApphudLog.log("Could not find SkuDetails for product id: $productId in memory") ApphudLog.log("Now try fetch it from Google Billing") billing.details(BillingClient.SkuType.SUBS, listOf(productId)) { skuList -> ApphudLog.log("Google Billing (SUBS) return this info for product id = $productId :") skuList.forEach { ApphudLog.log("$it") } - skuList.takeIf { it.isNotEmpty() }?.let { skuDetails.addAll(it); purchase(activity, it.first() , callback) } + skuList.takeIf { it.isNotEmpty() }?.let { + skuDetails.addAll(it) + when { + withValidation -> purchase(activity, it.first(), callback) + else -> purchaseWithoutValidation(activity, it.first(), callback) + } + } ?: run { + val message = + "Unable to fetch product (SkuType.SUBS) with given product id: $productId" + callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + } } billing.details(BillingClient.SkuType.INAPP, listOf(productId)) { skuList -> ApphudLog.log("Google Billing (INAPP) return this info for product id = $productId :") skuList.forEach { ApphudLog.log("$it") } - skuList.takeIf { it.isNotEmpty() }?.let { skuDetails.addAll(it); purchase(activity, it.first() , callback) } + skuList.takeIf { it.isNotEmpty() }?.let { + skuDetails.addAll(it) + when { + withValidation -> purchase(activity, it.first(), callback) + else -> purchaseWithoutValidation(activity, it.first(), callback) + } + } ?: run { + val message = + "Unable to fetch product (SkuType.INAPP) with given product id: $productId" + callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + } } } } @@ -203,38 +227,142 @@ internal object ApphudInternal { internal fun purchase( activity: Activity, details: SkuDetails, - callback: (List) -> Unit + callback: ((ApphudPurchaseResult) -> Unit)? ) { - billing.acknowledgeCallback = { + billing.acknowledgeCallback = { _ -> ApphudLog.log("acknowledge success") } - billing.consumeCallback = { value -> + billing.consumeCallback = { value, _ -> ApphudLog.log("consume callback value: $value") } billing.purchasesCallback = { purchases -> - ApphudLog.log("purchases: $purchases") + if (purchases.isNotEmpty()) { + ApphudLog.log("purchases: $purchases") - client.purchased(mkPurchasesBody(purchases)) { customer -> - handler.post { - storage.customer = customer - callback.invoke(purchases.map { it.purchase }) - apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) - apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) + client.purchased(mkPurchasesBody(purchases)) { customer -> + handler.post { + ApphudLog.log("client.purchased: $customer") + + val newSubscriptions = storage.customer?.subscriptions?.let { + customer.subscriptions.minus(it) + } ?: customer.subscriptions + + val newPurchases = storage.customer?.purchases?.let { + customer.purchases.minus(it) + } ?: customer.purchases + + storage.customer = customer + apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) + apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) + + takeIf { newSubscriptions.isNotEmpty() }?.let { + callback?.invoke(ApphudPurchaseResult(newSubscriptions.first(), + null, + null, + null)) + } + takeIf { newPurchases.isNotEmpty() }?.let { + callback?.invoke(ApphudPurchaseResult(null, + newPurchases.first(), + null, + null)) + } + } } + + purchases.forEach { + when (it.purchase.purchaseState) { + Purchase.PurchaseState.PURCHASED -> + when (it.details?.type) { + BillingClient.SkuType.SUBS -> { + if (!it.purchase.isAcknowledged) { + billing.acknowledge(it.purchase) + } + } + BillingClient.SkuType.INAPP -> { + billing.consume(it.purchase) + } + else -> { + val message = "After purchase type is null" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, + null, + null, + Error(message))) + } + } + else -> { + val message = "After purchase state: ${it.purchase.purchaseState}" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + } + } + } + } else { + val message = "Unable to buy product with given product id: ${details.sku}" + callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) } + } + billing.purchase(activity, details) + } - purchases.forEach { + internal fun purchaseWithoutValidation( + activity: Activity, + details: SkuDetails, + callback: ((ApphudPurchaseResult) -> Unit)? + ) { + billing.acknowledgeCallback = { purchase -> + ApphudLog.log("acknowledge success") + callback?.invoke(ApphudPurchaseResult(null, null, purchase, null)) + } + billing.consumeCallback = { value, purchase -> + ApphudLog.log("consume callback value: $value") + callback?.invoke(ApphudPurchaseResult(null, null, purchase, null)) + } + billing.purchasesCallback = { purchases -> + if (purchases.isNotEmpty()) { + ApphudLog.log("purchases: $purchases") + + client.purchased(mkPurchasesBody(purchases)) { customer -> + handler.post { + ApphudLog.log("client.purchased: $customer") + storage.customer = customer + apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) + apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) + } + } - when (it.purchase.purchaseState) { - Purchase.PurchaseState.PURCHASED -> when (it.details?.type) { - BillingClient.SkuType.SUBS -> if (!it.purchase.isAcknowledged) { - billing.acknowledge(it.purchase.purchaseToken) + purchases.forEach { + when (it.purchase.purchaseState) { + Purchase.PurchaseState.PURCHASED -> + when (it.details?.type) { + BillingClient.SkuType.SUBS -> { + if (!it.purchase.isAcknowledged) { + billing.acknowledge(it.purchase) + } + } + BillingClient.SkuType.INAPP -> { + billing.consume(it.purchase) + } + else -> { + val message = "After purchase type is null" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, + null, + null, + Error(message))) + } + } + else -> { + val message = "After purchase state: ${it.purchase.purchaseState}" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) } - BillingClient.SkuType.INAPP -> billing.consume(it.purchase.purchaseToken) - else -> ApphudLog.log("After purchase type is null") } - else -> ApphudLog.log("After purchase state: ${it.purchase.purchaseState}") } + } else { + val message = "Unable to buy product with given product id: ${details.sku}" + callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) } } billing.purchase(activity, details) @@ -248,7 +376,7 @@ internal object ApphudInternal { when { prevPurchases.containsAll(records) -> ApphudLog.log("syncPurchases: Don't send equal purchases from prev state") - else -> client.purchased(mkPurchaseBody(records)) { customer -> + else -> client.purchased(mkPurchaseBody(records)) { customer -> handler.post { prevPurchases.addAll(records) storage.isNeedSync = false @@ -303,14 +431,14 @@ internal object ApphudInternal { if (provider == ApphudAttributionProvider.appsFlyer) { val temporary = storage.appsflyer when { - temporary == null -> Unit - temporary.id == body?.appsflyer_id -> return + temporary == null -> Unit + temporary.id == body?.appsflyer_id -> return temporary.data == body?.appsflyer_data -> return } } else if (provider == ApphudAttributionProvider.facebook) { val temporary = storage.facebook when { - temporary == null -> Unit + temporary == null -> Unit temporary.data == body?.facebook_data -> return } } @@ -323,11 +451,11 @@ internal object ApphudInternal { if (provider == ApphudAttributionProvider.appsFlyer) { val temporary = storage.appsflyer storage.appsflyer = when { - temporary == null -> AppsflyerInfo( + temporary == null -> AppsflyerInfo( id = body.appsflyer_id, data = body.appsflyer_data ) - temporary.id != body.appsflyer_id -> AppsflyerInfo( + temporary.id != body.appsflyer_id -> AppsflyerInfo( id = body.appsflyer_id, data = body.appsflyer_data ) @@ -335,14 +463,14 @@ internal object ApphudInternal { id = body.appsflyer_id, data = body.appsflyer_data ) - else -> temporary + else -> temporary } } else if (provider == ApphudAttributionProvider.facebook) { val temporary = storage.facebook storage.facebook = when { - temporary == null -> FacebookInfo(body.facebook_data) + temporary == null -> FacebookInfo(body.facebook_data) temporary.data != body.facebook_data -> FacebookInfo(body.facebook_data) - else -> temporary + else -> temporary } } } diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt b/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt new file mode 100644 index 00000000..fc28f308 --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt @@ -0,0 +1,45 @@ +package com.apphud.sdk + +import com.android.billingclient.api.Purchase +import com.apphud.sdk.domain.ApphudNonRenewingPurchase +import com.apphud.sdk.domain.ApphudSubscription + +class ApphudPurchaseResult ( + /** + * Autorenewable subscription object. May be nil if error occurred or if non renewing product purchased instead. + * + * Null if `purchaseWithoutValidation` method called. + */ + var subscription: ApphudSubscription? = null, + + /** + * Standard in-app purchase (non-consumable, consumable or non-renewing subscription) object. + * + * May be null if error occurred or if auto-renewable subscription purchased instead. + * + * Null if `purchaseWithoutValidation` method called. + */ + var nonRenewingPurchase: ApphudNonRenewingPurchase? = null, + + /** + * Purchase from Play Market. May be null, if no transaction made. + * + * For example, if couldn't sign promo offer or couldn't get Play Market receipt. + */ + var purchase: Purchase? = null, + + /** + * This error can be of three types. Check for error class. + * + * - `SKError` from StoreKit with `SKErrorDomain` codes. This is a system error when purchasing transaction. + * + * - `NSError` from HTTP Client with `NSURLErrorDomain` codes. This is a network/server issue when uploading receipt to Apphud. + * + * - Custom `ApphudError` without codes. For example, if couldn't sign promo offer or couldn't get App Store receipt. + */ + var error: Error? = null +) { + override fun toString(): String { + return "ApphudPurchaseResult(subscription=$subscription, nonRenewingPurchase=$nonRenewingPurchase, purchase=$purchase, error=$error)" + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt index 5205cbad..dd74a535 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt @@ -3,10 +3,11 @@ package com.apphud.sdk.internal import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.Purchase import com.apphud.sdk.response import java.io.Closeable -typealias AcknowledgeCallback = () -> Unit +typealias AcknowledgeCallback = (Purchase) -> Unit internal class AcknowledgeWrapper( private val billing: BillingClient @@ -18,7 +19,9 @@ internal class AcknowledgeWrapper( var onSuccess: AcknowledgeCallback? = null - fun purchase(token: String) { + fun purchase(purchase: Purchase) { + + val token = purchase.purchaseToken if (token.isEmpty() || token.isBlank()) { throw IllegalArgumentException("Token empty or blank") @@ -28,7 +31,7 @@ internal class AcknowledgeWrapper( .setPurchaseToken(token) .build() billing.acknowledgePurchase(params) { result: BillingResult -> - result.response(MESSAGE) { onSuccess?.invoke() } + result.response(MESSAGE) { onSuccess?.invoke(purchase) } } } diff --git a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt index 8fb4304e..2561e62a 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt @@ -102,9 +102,9 @@ internal class BillingWrapper(context: Context) : BillingClientStateListener, Cl flow.purchases(activity, details) } - fun acknowledge(token: String) = acknowledge.purchase(token) + fun acknowledge(purchase: Purchase) = acknowledge.purchase(purchase) - fun consume(token: String) = consume.purchase(token) + fun consume(purchase: Purchase) = consume.purchase(purchase) //BillingClientStateListener override fun onBillingServiceDisconnected() { diff --git a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt index 3268aa24..6646c191 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt @@ -2,10 +2,11 @@ package com.apphud.sdk.internal import com.android.billingclient.api.BillingClient import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.Purchase import com.apphud.sdk.response import java.io.Closeable -typealias ConsumeCallback = (String) -> Unit +typealias ConsumeCallback = (String, Purchase) -> Unit internal class ConsumeWrapper( private val billing: BillingClient @@ -13,13 +14,16 @@ internal class ConsumeWrapper( var callback: ConsumeCallback? = null - fun purchase(token: String) { + fun purchase(purchase: Purchase) { + + val token = purchase.purchaseToken + val params = ConsumeParams.newBuilder() .setPurchaseToken(token) .build() billing.consumeAsync(params) { result, value -> result.response("failed response with value: $value") { - callback?.invoke(value) + callback?.invoke(value, purchase) } } } diff --git a/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt b/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt index 8fb363bc..239e8d81 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt @@ -30,7 +30,10 @@ internal class PurchasesUpdated( } ?: emptyList() callback?.invoke(purchases) } - else -> result.logMessage("failed purchase") + else -> { + result.logMessage("failed purchase") + callback?.invoke(listOf()) + } } } } From b7d7108b8c10c678b47232de86d1d46f7055738a Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Mon, 26 Apr 2021 13:06:15 +0300 Subject: [PATCH 02/12] APH-661 Refactoring of Purchase methods --- .../java/com/apphud/sdk/ApphudInternal.kt | 142 ++++++++++++------ .../java/com/apphud/sdk/billing-result.kt | 5 + .../apphud/sdk/internal/AcknowledgeWrapper.kt | 18 ++- .../com/apphud/sdk/internal/BillingWrapper.kt | 9 +- .../com/apphud/sdk/internal/CallBackStatus.kt | 6 + .../com/apphud/sdk/internal/ConsumeWrapper.kt | 20 +-- 6 files changed, 131 insertions(+), 69 deletions(-) create mode 100644 sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index bff58931..e2a52811 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -1,5 +1,6 @@ package com.apphud.sdk +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.pm.ApplicationInfo @@ -14,6 +15,7 @@ import com.apphud.sdk.body.* import com.apphud.sdk.client.ApphudClient import com.apphud.sdk.domain.* import com.apphud.sdk.internal.BillingWrapper +import com.apphud.sdk.internal.CallBackStatus import com.apphud.sdk.parser.GsonParser import com.apphud.sdk.parser.Parser import com.apphud.sdk.storage.SharedPreferencesStorage @@ -21,6 +23,7 @@ import com.apphud.sdk.tasks.advertisingId import com.google.gson.GsonBuilder import java.util.* +@SuppressLint("StaticFieldLeak") internal object ApphudInternal { private val builder = GsonBuilder() @@ -229,72 +232,61 @@ internal object ApphudInternal { details: SkuDetails, callback: ((ApphudPurchaseResult) -> Unit)? ) { - billing.acknowledgeCallback = { _ -> - ApphudLog.log("acknowledge success") + billing.acknowledgeCallback = { status, purchase -> + when(status){ + is CallBackStatus.Error -> { + val message = "After purchase acknowledge is failed with code: ${status.error}" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) + } + is CallBackStatus.Success -> { + ApphudLog.log("acknowledge success") + ackPurchase(purchase, callback) + } + } } - billing.consumeCallback = { value, _ -> - ApphudLog.log("consume callback value: $value") + billing.consumeCallback = { status, purchase -> + when(status){ + is CallBackStatus.Error -> { + val message = "After purchase consume is failed with value: ${status.error}" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) + } + is CallBackStatus.Success -> { + ApphudLog.log("consume callback value: ${status.message}") + ackPurchase(purchase, callback) + } + } } billing.purchasesCallback = { purchases -> if (purchases.isNotEmpty()) { ApphudLog.log("purchases: $purchases") - client.purchased(mkPurchasesBody(purchases)) { customer -> - handler.post { - ApphudLog.log("client.purchased: $customer") - - val newSubscriptions = storage.customer?.subscriptions?.let { - customer.subscriptions.minus(it) - } ?: customer.subscriptions - - val newPurchases = storage.customer?.purchases?.let { - customer.purchases.minus(it) - } ?: customer.purchases - - storage.customer = customer - apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) - apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) - - takeIf { newSubscriptions.isNotEmpty() }?.let { - callback?.invoke(ApphudPurchaseResult(newSubscriptions.first(), - null, - null, - null)) - } - takeIf { newPurchases.isNotEmpty() }?.let { - callback?.invoke(ApphudPurchaseResult(null, - newPurchases.first(), - null, - null)) - } - } - } - purchases.forEach { when (it.purchase.purchaseState) { Purchase.PurchaseState.PURCHASED -> when (it.details?.type) { BillingClient.SkuType.SUBS -> { if (!it.purchase.isAcknowledged) { - billing.acknowledge(it.purchase) + billing.acknowledge(it) } } BillingClient.SkuType.INAPP -> { - billing.consume(it.purchase) + billing.consume(it) } else -> { val message = "After purchase type is null" ApphudLog.log(message) callback?.invoke(ApphudPurchaseResult(null, null, - null, + it.purchase, Error(message))) } } else -> { val message = "After purchase state: ${it.purchase.purchaseState}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, it.purchase, Error(message))) } } } @@ -306,18 +298,70 @@ internal object ApphudInternal { billing.purchase(activity, details) } + private fun ackPurchase(purchase: PurchaseDetails, callback: ((ApphudPurchaseResult) -> Unit)?){ + client.purchased(mkPurchasesBody(listOf(purchase))) { customer -> + handler.post { + ApphudLog.log("client.purchased: $customer") + + val newSubscriptions = storage.customer?.subscriptions?.let { + customer.subscriptions.minus(it) + } ?: customer.subscriptions + + val newPurchases = storage.customer?.purchases?.let { + customer.purchases.minus(it) + } ?: customer.purchases + + storage.customer = customer + + takeIf { newSubscriptions.isNotEmpty() }?.let { + apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) + callback?.invoke(ApphudPurchaseResult(newSubscriptions.first(), + null, + null, + null)) + } + takeIf { newPurchases.isNotEmpty() }?.let { + apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) + callback?.invoke(ApphudPurchaseResult(null, + newPurchases.first(), + null, + null)) + } + } + } + } + internal fun purchaseWithoutValidation( activity: Activity, details: SkuDetails, callback: ((ApphudPurchaseResult) -> Unit)? ) { - billing.acknowledgeCallback = { purchase -> - ApphudLog.log("acknowledge success") - callback?.invoke(ApphudPurchaseResult(null, null, purchase, null)) + billing.acknowledgeCallback = { status, purchase -> + when(status){ + is CallBackStatus.Error -> { + val message = "After purchase acknowledge is failed with code: ${status.error}" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) + } + is CallBackStatus.Success -> { + ApphudLog.log("acknowledge success") + callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, null)) + } + } } - billing.consumeCallback = { value, purchase -> - ApphudLog.log("consume callback value: $value") - callback?.invoke(ApphudPurchaseResult(null, null, purchase, null)) + billing.consumeCallback = { status, purchase -> + + when(status){ + is CallBackStatus.Error -> { + val message = "After purchase consume is failed with value: ${status.error}" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) + } + is CallBackStatus.Success -> { + ApphudLog.log("consume callback value: ${status.message}") + callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, null)) + } + } } billing.purchasesCallback = { purchases -> if (purchases.isNotEmpty()) { @@ -338,25 +382,25 @@ internal object ApphudInternal { when (it.details?.type) { BillingClient.SkuType.SUBS -> { if (!it.purchase.isAcknowledged) { - billing.acknowledge(it.purchase) + billing.acknowledge(it) } } BillingClient.SkuType.INAPP -> { - billing.consume(it.purchase) + billing.consume(it) } else -> { val message = "After purchase type is null" ApphudLog.log(message) callback?.invoke(ApphudPurchaseResult(null, null, - null, + it.purchase, Error(message))) } } else -> { val message = "After purchase state: ${it.purchase.purchaseState}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, it.purchase, Error(message))) } } } diff --git a/sdk/src/main/java/com/apphud/sdk/billing-result.kt b/sdk/src/main/java/com/apphud/sdk/billing-result.kt index aba3fa2a..5533d5d6 100644 --- a/sdk/src/main/java/com/apphud/sdk/billing-result.kt +++ b/sdk/src/main/java/com/apphud/sdk/billing-result.kt @@ -11,6 +11,11 @@ inline fun BillingResult.response(message: String, crossinline block: () -> Unit else -> logMessage(message) } +fun BillingResult.response(message: String, success: () -> Unit, error: () -> Unit) = when { + isSuccess() -> success.invoke() + else -> error.invoke().also{ logMessage(message) } +} + //TODO Логи будут постоянно идти, нужно делать on/off fun BillingResult.logMessage(template: String) = ApphudLog.logE("result failed with code: $responseCode message: $debugMessage template: $template") \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt index dd74a535..702d41a7 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt @@ -3,11 +3,11 @@ package com.apphud.sdk.internal import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingResult -import com.android.billingclient.api.Purchase +import com.apphud.sdk.domain.PurchaseDetails import com.apphud.sdk.response import java.io.Closeable -typealias AcknowledgeCallback = (Purchase) -> Unit +typealias AcknowledgeCallback = (CallBackStatus, PurchaseDetails) -> Unit internal class AcknowledgeWrapper( private val billing: BillingClient @@ -17,11 +17,11 @@ internal class AcknowledgeWrapper( private const val MESSAGE = "purchase acknowledge is failed" } - var onSuccess: AcknowledgeCallback? = null + var callBack: AcknowledgeCallback? = null - fun purchase(purchase: Purchase) { + fun purchase(purchase: PurchaseDetails) { - val token = purchase.purchaseToken + val token = purchase.purchase.purchaseToken if (token.isEmpty() || token.isBlank()) { throw IllegalArgumentException("Token empty or blank") @@ -31,12 +31,16 @@ internal class AcknowledgeWrapper( .setPurchaseToken(token) .build() billing.acknowledgePurchase(params) { result: BillingResult -> - result.response(MESSAGE) { onSuccess?.invoke(purchase) } + result.response( + MESSAGE, + { callBack?.invoke(CallBackStatus.Error(result.responseCode.toString()), purchase) }, + { callBack?.invoke(CallBackStatus.Success(), purchase) } + ) } } //Closeable override fun close() { - onSuccess = null + callBack = null } } \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt index 2561e62a..64a24c1a 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt @@ -6,6 +6,7 @@ import android.util.SparseArray import com.android.billingclient.api.* import com.apphud.sdk.ApphudLog import com.apphud.sdk.ProductId +import com.apphud.sdk.domain.PurchaseDetails import java.io.Closeable /** @@ -61,13 +62,13 @@ internal class BillingWrapper(context: Context) : BillingClientStateListener, Cl var acknowledgeCallback: AcknowledgeCallback? = null set(value) { field = value - acknowledge.onSuccess = value + acknowledge.callBack = value } var consumeCallback: ConsumeCallback? = null set(value) { field = value - consume.callback = value + consume.callBack = value } var historyCallback: PurchaseHistoryListener? = null @@ -102,9 +103,9 @@ internal class BillingWrapper(context: Context) : BillingClientStateListener, Cl flow.purchases(activity, details) } - fun acknowledge(purchase: Purchase) = acknowledge.purchase(purchase) + fun acknowledge(purchase: PurchaseDetails) = acknowledge.purchase(purchase) - fun consume(purchase: Purchase) = consume.purchase(purchase) + fun consume(purchase: PurchaseDetails) = consume.purchase(purchase) //BillingClientStateListener override fun onBillingServiceDisconnected() { diff --git a/sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt b/sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt new file mode 100644 index 00000000..ff16dda5 --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt @@ -0,0 +1,6 @@ +package com.apphud.sdk.internal + +sealed class CallBackStatus { + class Success(val message: String? = null) : CallBackStatus() + class Error(val error: String) : CallBackStatus() +} diff --git a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt index 6646c191..93dbe30d 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt @@ -2,34 +2,36 @@ package com.apphud.sdk.internal import com.android.billingclient.api.BillingClient import com.android.billingclient.api.ConsumeParams -import com.android.billingclient.api.Purchase +import com.apphud.sdk.domain.PurchaseDetails import com.apphud.sdk.response import java.io.Closeable -typealias ConsumeCallback = (String, Purchase) -> Unit +typealias ConsumeCallback = (CallBackStatus, PurchaseDetails) -> Unit internal class ConsumeWrapper( private val billing: BillingClient ) : Closeable { - var callback: ConsumeCallback? = null + var callBack: ConsumeCallback? = null - fun purchase(purchase: Purchase) { + fun purchase(purchase: PurchaseDetails) { - val token = purchase.purchaseToken + val token = purchase.purchase.purchaseToken val params = ConsumeParams.newBuilder() .setPurchaseToken(token) .build() billing.consumeAsync(params) { result, value -> - result.response("failed response with value: $value") { - callback?.invoke(value, purchase) - } + result.response( + "failed response with value: $value", + { callBack?.invoke(CallBackStatus.Error(value), purchase) }, + { callBack?.invoke(CallBackStatus.Success(value), purchase) } + ) } } //Closeable override fun close() { - callback = null + callBack = null } } \ No newline at end of file From 4b43307e69b45738e59351a496f9fcc9683d3521 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Mon, 26 Apr 2021 23:08:01 +0300 Subject: [PATCH 03/12] APH-661 Refactoring of Purchase methods --- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 4 +- .../java/com/apphud/sdk/ApphudInternal.kt | 155 +++++------------- .../java/com/apphud/sdk/billing-result.kt | 2 +- .../com/apphud/sdk/domain/PurchaseDetails.kt | 9 - .../apphud/sdk/internal/AcknowledgeWrapper.kt | 8 +- .../com/apphud/sdk/internal/BillingWrapper.kt | 6 +- .../com/apphud/sdk/internal/ConsumeWrapper.kt | 8 +- .../apphud/sdk/internal/PurchasesUpdated.kt | 21 +-- 8 files changed, 56 insertions(+), 157 deletions(-) delete mode 100644 sdk/src/main/java/com/apphud/sdk/domain/PurchaseDetails.kt diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 33a10042..7daa7f8c 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -177,7 +177,7 @@ object Apphud { */ @kotlin.jvm.JvmStatic fun purchase(activity: Activity, details: SkuDetails, block: ((ApphudPurchaseResult) -> Unit)?) = - ApphudInternal.purchase(activity, details, block) + ApphudInternal.purchase(activity, details, true, block) /** * Purchases product and automatically submit Play Market Receipt to Apphud @@ -209,7 +209,7 @@ object Apphud { */ @kotlin.jvm.JvmStatic fun purchaseWithoutValidation(activity: Activity, details: SkuDetails, block: ((ApphudPurchaseResult) -> Unit)?) = - ApphudInternal.purchaseWithoutValidation(activity, details, block) + ApphudInternal.purchase(activity, details, false, block) /** * Set custom user property. diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index e2a52811..407b4118 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -187,10 +187,7 @@ internal object ApphudInternal { ) { val sku = getSkuDetailsByProductId(productId) if (sku != null) { - when { - withValidation -> purchase(activity, sku, callback) - else -> purchaseWithoutValidation(activity, sku, callback) - } + purchase(activity, sku, withValidation, callback) } else { ApphudLog.log("Could not find SkuDetails for product id: $productId in memory") ApphudLog.log("Now try fetch it from Google Billing") @@ -199,10 +196,7 @@ internal object ApphudInternal { skuList.forEach { ApphudLog.log("$it") } skuList.takeIf { it.isNotEmpty() }?.let { skuDetails.addAll(it) - when { - withValidation -> purchase(activity, it.first(), callback) - else -> purchaseWithoutValidation(activity, it.first(), callback) - } + purchase(activity, it.first(), withValidation, callback) } ?: run { val message = "Unable to fetch product (SkuType.SUBS) with given product id: $productId" @@ -214,10 +208,7 @@ internal object ApphudInternal { skuList.forEach { ApphudLog.log("$it") } skuList.takeIf { it.isNotEmpty() }?.let { skuDetails.addAll(it) - when { - withValidation -> purchase(activity, it.first(), callback) - else -> purchaseWithoutValidation(activity, it.first(), callback) - } + purchase(activity, it.first(), withValidation, callback) } ?: run { val message = "Unable to fetch product (SkuType.INAPP) with given product id: $productId" @@ -230,6 +221,7 @@ internal object ApphudInternal { internal fun purchase( activity: Activity, details: SkuDetails, + withValidation: Boolean, callback: ((ApphudPurchaseResult) -> Unit)? ) { billing.acknowledgeCallback = { status, purchase -> @@ -237,11 +229,17 @@ internal object ApphudInternal { is CallBackStatus.Error -> { val message = "After purchase acknowledge is failed with code: ${status.error}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, purchase, Error(message))) } is CallBackStatus.Success -> { ApphudLog.log("acknowledge success") - ackPurchase(purchase, callback) + when { + withValidation -> ackPurchase(purchase, details, callback) + else -> { + callback?.invoke(ApphudPurchaseResult(null, null, purchase, null)) + ackPurchase(purchase, details, null) + } + } } } } @@ -250,11 +248,17 @@ internal object ApphudInternal { is CallBackStatus.Error -> { val message = "After purchase consume is failed with value: ${status.error}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, purchase, Error(message))) } is CallBackStatus.Success -> { ApphudLog.log("consume callback value: ${status.message}") - ackPurchase(purchase, callback) + when { + withValidation -> ackPurchase(purchase, details, callback) + else -> { + callback?.invoke(ApphudPurchaseResult(null, null, purchase, null)) + ackPurchase(purchase, details, null) + } + } } } } @@ -263,11 +267,11 @@ internal object ApphudInternal { ApphudLog.log("purchases: $purchases") purchases.forEach { - when (it.purchase.purchaseState) { + when (it.purchaseState) { Purchase.PurchaseState.PURCHASED -> - when (it.details?.type) { + when (details.type) { BillingClient.SkuType.SUBS -> { - if (!it.purchase.isAcknowledged) { + if (!it.isAcknowledged) { billing.acknowledge(it) } } @@ -279,14 +283,14 @@ internal object ApphudInternal { ApphudLog.log(message) callback?.invoke(ApphudPurchaseResult(null, null, - it.purchase, + it, Error(message))) } } else -> { - val message = "After purchase state: ${it.purchase.purchaseState}" + val message = "After purchase state: ${it.purchaseState}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, it.purchase, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, it, Error(message))) } } } @@ -298,8 +302,8 @@ internal object ApphudInternal { billing.purchase(activity, details) } - private fun ackPurchase(purchase: PurchaseDetails, callback: ((ApphudPurchaseResult) -> Unit)?){ - client.purchased(mkPurchasesBody(listOf(purchase))) { customer -> + private fun ackPurchase(purchase: Purchase, details: SkuDetails?, callback: ((ApphudPurchaseResult) -> Unit)?){ + client.purchased(mkPurchasesBody(purchase, details)) { customer -> handler.post { ApphudLog.log("client.purchased: $customer") @@ -317,101 +321,20 @@ internal object ApphudInternal { apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) callback?.invoke(ApphudPurchaseResult(newSubscriptions.first(), null, - null, + purchase, null)) } takeIf { newPurchases.isNotEmpty() }?.let { apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) callback?.invoke(ApphudPurchaseResult(null, newPurchases.first(), - null, + purchase, null)) } } } } - internal fun purchaseWithoutValidation( - activity: Activity, - details: SkuDetails, - callback: ((ApphudPurchaseResult) -> Unit)? - ) { - billing.acknowledgeCallback = { status, purchase -> - when(status){ - is CallBackStatus.Error -> { - val message = "After purchase acknowledge is failed with code: ${status.error}" - ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) - } - is CallBackStatus.Success -> { - ApphudLog.log("acknowledge success") - callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, null)) - } - } - } - billing.consumeCallback = { status, purchase -> - - when(status){ - is CallBackStatus.Error -> { - val message = "After purchase consume is failed with value: ${status.error}" - ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, Error(message))) - } - is CallBackStatus.Success -> { - ApphudLog.log("consume callback value: ${status.message}") - callback?.invoke(ApphudPurchaseResult(null, null, purchase.purchase, null)) - } - } - } - billing.purchasesCallback = { purchases -> - if (purchases.isNotEmpty()) { - ApphudLog.log("purchases: $purchases") - - client.purchased(mkPurchasesBody(purchases)) { customer -> - handler.post { - ApphudLog.log("client.purchased: $customer") - storage.customer = customer - apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) - apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) - } - } - - purchases.forEach { - when (it.purchase.purchaseState) { - Purchase.PurchaseState.PURCHASED -> - when (it.details?.type) { - BillingClient.SkuType.SUBS -> { - if (!it.purchase.isAcknowledged) { - billing.acknowledge(it) - } - } - BillingClient.SkuType.INAPP -> { - billing.consume(it) - } - else -> { - val message = "After purchase type is null" - ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, - null, - it.purchase, - Error(message))) - } - } - else -> { - val message = "After purchase state: ${it.purchase.purchaseState}" - ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, it.purchase, Error(message))) - } - } - } - } else { - val message = "Unable to buy product with given product id: ${details.sku}" - callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) - } - } - billing.purchase(activity, details) - } - internal fun syncPurchases() { storage.isNeedSync = true billing.restoreCallback = { records -> @@ -642,19 +565,19 @@ internal object ApphudInternal { return deviceId } - private fun mkPurchasesBody(purchases: List) = + private fun mkPurchasesBody(purchase: Purchase, details: SkuDetails?) = PurchaseBody( device_id = deviceId, - purchases = purchases.map { + purchases = listOf( PurchaseItemBody( - order_id = it.purchase.orderId, - product_id = it.purchase.sku, - purchase_token = it.purchase.purchaseToken, - price_currency_code = it.details?.priceCurrencyCode, - price_amount_micros = it.details?.priceAmountMicros, - subscription_period = it.details?.subscriptionPeriod + order_id = purchase.orderId, + product_id = purchase.sku, + purchase_token = purchase.purchaseToken, + price_currency_code = details?.priceCurrencyCode, + price_amount_micros = details?.priceAmountMicros, + subscription_period = details?.subscriptionPeriod ) - } + ) ) private fun mkPurchaseBody(purchases: List) = diff --git a/sdk/src/main/java/com/apphud/sdk/billing-result.kt b/sdk/src/main/java/com/apphud/sdk/billing-result.kt index 5533d5d6..00c8959a 100644 --- a/sdk/src/main/java/com/apphud/sdk/billing-result.kt +++ b/sdk/src/main/java/com/apphud/sdk/billing-result.kt @@ -11,7 +11,7 @@ inline fun BillingResult.response(message: String, crossinline block: () -> Unit else -> logMessage(message) } -fun BillingResult.response(message: String, success: () -> Unit, error: () -> Unit) = when { +fun BillingResult.response(message: String, error: () -> Unit, success: () -> Unit) = when { isSuccess() -> success.invoke() else -> error.invoke().also{ logMessage(message) } } diff --git a/sdk/src/main/java/com/apphud/sdk/domain/PurchaseDetails.kt b/sdk/src/main/java/com/apphud/sdk/domain/PurchaseDetails.kt deleted file mode 100644 index db0fc286..00000000 --- a/sdk/src/main/java/com/apphud/sdk/domain/PurchaseDetails.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.apphud.sdk.domain - -import com.android.billingclient.api.Purchase -import com.android.billingclient.api.SkuDetails - -data class PurchaseDetails( - val purchase: Purchase, - val details: SkuDetails? -) \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt index 702d41a7..f085b899 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt @@ -3,11 +3,11 @@ package com.apphud.sdk.internal import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingResult -import com.apphud.sdk.domain.PurchaseDetails +import com.android.billingclient.api.Purchase import com.apphud.sdk.response import java.io.Closeable -typealias AcknowledgeCallback = (CallBackStatus, PurchaseDetails) -> Unit +typealias AcknowledgeCallback = (CallBackStatus, Purchase) -> Unit internal class AcknowledgeWrapper( private val billing: BillingClient @@ -19,9 +19,9 @@ internal class AcknowledgeWrapper( var callBack: AcknowledgeCallback? = null - fun purchase(purchase: PurchaseDetails) { + fun purchase(purchase: Purchase) { - val token = purchase.purchase.purchaseToken + val token = purchase.purchaseToken if (token.isEmpty() || token.isBlank()) { throw IllegalArgumentException("Token empty or blank") diff --git a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt index 64a24c1a..dc12d10c 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/BillingWrapper.kt @@ -6,7 +6,6 @@ import android.util.SparseArray import com.android.billingclient.api.* import com.apphud.sdk.ApphudLog import com.apphud.sdk.ProductId -import com.apphud.sdk.domain.PurchaseDetails import java.io.Closeable /** @@ -99,13 +98,12 @@ internal class BillingWrapper(context: Context) : BillingClientStateListener, Cl sku.restoreAsync(type, products) fun purchase(activity: Activity, details: SkuDetails) { - purchases.startPurchase(details) flow.purchases(activity, details) } - fun acknowledge(purchase: PurchaseDetails) = acknowledge.purchase(purchase) + fun acknowledge(purchase: Purchase) = acknowledge.purchase(purchase) - fun consume(purchase: PurchaseDetails) = consume.purchase(purchase) + fun consume(purchase: Purchase) = consume.purchase(purchase) //BillingClientStateListener override fun onBillingServiceDisconnected() { diff --git a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt index 93dbe30d..93b64af4 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt @@ -2,11 +2,11 @@ package com.apphud.sdk.internal import com.android.billingclient.api.BillingClient import com.android.billingclient.api.ConsumeParams -import com.apphud.sdk.domain.PurchaseDetails +import com.android.billingclient.api.Purchase import com.apphud.sdk.response import java.io.Closeable -typealias ConsumeCallback = (CallBackStatus, PurchaseDetails) -> Unit +typealias ConsumeCallback = (CallBackStatus, Purchase) -> Unit internal class ConsumeWrapper( private val billing: BillingClient @@ -14,9 +14,9 @@ internal class ConsumeWrapper( var callBack: ConsumeCallback? = null - fun purchase(purchase: PurchaseDetails) { + fun purchase(purchase: Purchase) { - val token = purchase.purchase.purchaseToken + val token = purchase.purchaseToken val params = ConsumeParams.newBuilder() .setPurchaseToken(token) diff --git a/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt b/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt index 239e8d81..975a781b 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt @@ -1,14 +1,12 @@ package com.apphud.sdk.internal import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.SkuDetails -import com.apphud.sdk.ProductId -import com.apphud.sdk.domain.PurchaseDetails +import com.android.billingclient.api.Purchase import com.apphud.sdk.isSuccess import com.apphud.sdk.logMessage import java.io.Closeable -typealias PurchasesUpdatedCallback = (List) -> Unit +typealias PurchasesUpdatedCallback = (List) -> Unit internal class PurchasesUpdated( builder: BillingClient.Builder @@ -16,32 +14,21 @@ internal class PurchasesUpdated( var callback: PurchasesUpdatedCallback? = null - private val skuDetails = mutableMapOf() - init { builder.setListener { result, list -> when (result.isSuccess()) { true -> { - val purchases = list?.mapNotNull { purchase -> - PurchaseDetails( - purchase = purchase, - details = skuDetails.remove(purchase.sku) - ) - } ?: emptyList() + val purchases = list?.filterNotNull() ?: emptyList() callback?.invoke(purchases) } else -> { result.logMessage("failed purchase") - callback?.invoke(listOf()) + callback?.invoke(emptyList()) } } } } - fun startPurchase(details: SkuDetails) { - skuDetails[details.sku] = details - } - //Closeable override fun close() { callback = null From 27c748914f9ecd45a27acb39d2b19bec0010dda8 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Tue, 27 Apr 2021 10:46:13 +0300 Subject: [PATCH 04/12] APH-661 Refactoring of Purchase methods --- sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 407b4118..448e8e23 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -303,7 +303,7 @@ internal object ApphudInternal { } private fun ackPurchase(purchase: Purchase, details: SkuDetails?, callback: ((ApphudPurchaseResult) -> Unit)?){ - client.purchased(mkPurchasesBody(purchase, details)) { customer -> + client.purchased(makePurchaseBody(purchase, details)) { customer -> handler.post { ApphudLog.log("client.purchased: $customer") @@ -343,7 +343,7 @@ internal object ApphudInternal { when { prevPurchases.containsAll(records) -> ApphudLog.log("syncPurchases: Don't send equal purchases from prev state") - else -> client.purchased(mkPurchaseBody(records)) { customer -> + else -> client.purchased(makeRestorePurchasesBody(records)) { customer -> handler.post { prevPurchases.addAll(records) storage.isNeedSync = false @@ -565,7 +565,7 @@ internal object ApphudInternal { return deviceId } - private fun mkPurchasesBody(purchase: Purchase, details: SkuDetails?) = + private fun makePurchaseBody(purchase: Purchase, details: SkuDetails?) = PurchaseBody( device_id = deviceId, purchases = listOf( @@ -580,7 +580,7 @@ internal object ApphudInternal { ) ) - private fun mkPurchaseBody(purchases: List) = + private fun makeRestorePurchasesBody(purchases: List) = PurchaseBody( device_id = deviceId, purchases = purchases.map { purchase -> From 47325256fcf7861c0c11a7d57f05d1a87eda5664 Mon Sep 17 00:00:00 2001 From: ren6 Date: Tue, 27 Apr 2021 14:58:40 +0300 Subject: [PATCH 05/12] update documentation --- .../main/java/com/apphud/app/MainActivity.kt | 3 ++- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/apphud/app/MainActivity.kt b/app/src/main/java/com/apphud/app/MainActivity.kt index 6d50dbcb..56d9ce0c 100644 --- a/app/src/main/java/com/apphud/app/MainActivity.kt +++ b/app/src/main/java/com/apphud/app/MainActivity.kt @@ -70,8 +70,9 @@ class MainActivity : AppCompatActivity() { Log.e("Apphud", "onClick model: $model") when (model.details) { null -> Log.e("Apphud", "details is empty") - else -> Apphud.purchase(this, model.details) { _ -> + else -> Apphud.purchase(this, model.details) { result -> Log.d("apphud","PURCHASE RESULT: ${Apphud.subscriptions() }. Has active subscription: ${Apphud.hasActiveSubscription()}") + Log.d("apphud", "${result.error?.toString()}") } } } diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 7daa7f8c..86730982 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -158,7 +158,7 @@ object Apphud { } /** - * Purchases product and automatically submit Play Market Receipt to Apphud + * Purchase product by id and automatically submit Google Play purchase token to Apphud * @param activity: current Activity for use * @param productId: The identifier of the product you wish to purchase @@ -169,7 +169,7 @@ object Apphud { ApphudInternal.purchase(activity, productId, true, block) /** - * Purchases product and automatically submit Play Market Receipt to Apphud + * Purchase sku product and automatically submit Google Play purchase token to Apphud * * @param activity current Activity for use * @param details The SkuDetails of the product you wish to purchase @@ -180,12 +180,12 @@ object Apphud { ApphudInternal.purchase(activity, details, true, block) /** - * Purchases product and automatically submit Play Market Receipt to Apphud + * Purchase product by id and automatically submit Google Play purchase token to Apphud * - * This method doesn't wait until Apphud validates receipt from Play Market and immediately returns transaction object. - * This method may be useful if you don't care about receipt validation in callback. + * This method doesn't wait until Apphud validates purchase from Google Play and immediately returns result object. + * This method may be useful if you don't care about purchases validation in callback. * - * When using this method properties `subscription` and `nonRenewingPurchase` in `ApphudPurchaseResult` will always be `null` ! + * Note: When using this method properties `subscription` and `nonRenewingPurchase` in `ApphudPurchaseResult` will always be `null` ! * * @param activity: current Activity for use * @param productId: The identifier of the product you wish to purchase @@ -196,10 +196,10 @@ object Apphud { ApphudInternal.purchase(activity, productId, false, block) /** - * Purchases product and automatically submit Play Market Receipt to Apphud + * Purchase sku product and automatically submit Google Play purchase token to Apphud * - * This method doesn't wait until Apphud validates receipt from Play Market and immediately returns transaction object. - * This method may be useful if you don't care about receipt validation in callback. + * This method doesn't wait until Apphud validates purchase from Google Play and immediately returns result object. + * This method may be useful if you don't care about purchases validation in callback. * * When using this method properties `subscription` and `nonRenewingPurchase` in `ApphudPurchaseResult` will always be `null` ! * From a78dea9d255fdca3f2c51c0d3092f322cfe15dcc Mon Sep 17 00:00:00 2001 From: ren6 Date: Tue, 27 Apr 2021 15:07:49 +0300 Subject: [PATCH 06/12] update documentation --- .../java/com/apphud/sdk/ApphudPurchaseResult.kt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt b/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt index fc28f308..d6e77c48 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt @@ -6,7 +6,7 @@ import com.apphud.sdk.domain.ApphudSubscription class ApphudPurchaseResult ( /** - * Autorenewable subscription object. May be nil if error occurred or if non renewing product purchased instead. + * Apphud Subscription object. May be null if error occurred or if non renewing product purchased instead. * * Null if `purchaseWithoutValidation` method called. */ @@ -22,20 +22,14 @@ class ApphudPurchaseResult ( var nonRenewingPurchase: ApphudNonRenewingPurchase? = null, /** - * Purchase from Play Market. May be null, if no transaction made. + * Purchase from Play Market. May be null, if no was purchase made. * - * For example, if couldn't sign promo offer or couldn't get Play Market receipt. + * For example, if there was no internet connection. */ var purchase: Purchase? = null, /** - * This error can be of three types. Check for error class. - * - * - `SKError` from StoreKit with `SKErrorDomain` codes. This is a system error when purchasing transaction. - * - * - `NSError` from HTTP Client with `NSURLErrorDomain` codes. This is a network/server issue when uploading receipt to Apphud. - * - * - Custom `ApphudError` without codes. For example, if couldn't sign promo offer or couldn't get App Store receipt. + * Error during purchase, if any. */ var error: Error? = null ) { From e4f3a9e4aece7f29d8fd36846eb967e229e252b8 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Tue, 27 Apr 2021 21:58:36 +0300 Subject: [PATCH 07/12] APH-661 Added check for empty UserId and DeviceId --- .../java/com/apphud/sdk/ApphudInternal.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 448e8e23..e3ce8c1e 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -546,20 +546,26 @@ internal object ApphudInternal { } private fun updateUser(id: UserId?): UserId { - - val userId = when (id) { - null -> storage.userId ?: generatedUUID - else -> id + val userId = when { + id == null || id.isBlank() -> { + storage.userId ?: generatedUUID + } + else -> { + id + } } storage.userId = userId return userId } private fun updateDevice(id: DeviceId?): DeviceId { - - val deviceId = when (id) { - null -> storage.deviceId?.let { is_new = false; it } ?: generatedUUID - else -> id + val deviceId = when { + id == null || id.isBlank() -> { + storage.deviceId?.let { is_new = false; it } ?: generatedUUID + } + else -> { + id + } } storage.deviceId = deviceId return deviceId From 68ebcbb9086042a8dfe2ba6568008ccd5b820ad9 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Tue, 27 Apr 2021 22:56:51 +0300 Subject: [PATCH 08/12] APH-661 Refactoring of Purchase methods. Implementing PurchaseUpdatedCallbackStatus. CallBackStatus was renamed to PurchaseCallbackStatus --- .../java/com/apphud/sdk/ApphudInternal.kt | 74 ++++++++++--------- .../apphud/sdk/internal/AcknowledgeWrapper.kt | 6 +- .../com/apphud/sdk/internal/CallBackStatus.kt | 6 -- .../com/apphud/sdk/internal/ConsumeWrapper.kt | 6 +- .../sdk/internal/PurchaseCallbackStatus.kt | 6 ++ .../internal/PurchaseUpdatedCallbackStatus.kt | 9 +++ .../apphud/sdk/internal/PurchasesUpdated.kt | 10 +-- 7 files changed, 66 insertions(+), 51 deletions(-) delete mode 100644 sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt create mode 100644 sdk/src/main/java/com/apphud/sdk/internal/PurchaseCallbackStatus.kt create mode 100644 sdk/src/main/java/com/apphud/sdk/internal/PurchaseUpdatedCallbackStatus.kt diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index e3ce8c1e..8a691ba1 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -15,7 +15,8 @@ import com.apphud.sdk.body.* import com.apphud.sdk.client.ApphudClient import com.apphud.sdk.domain.* import com.apphud.sdk.internal.BillingWrapper -import com.apphud.sdk.internal.CallBackStatus +import com.apphud.sdk.internal.PurchaseCallbackStatus +import com.apphud.sdk.internal.PurchaseUpdatedCallbackStatus import com.apphud.sdk.parser.GsonParser import com.apphud.sdk.parser.Parser import com.apphud.sdk.storage.SharedPreferencesStorage @@ -226,12 +227,12 @@ internal object ApphudInternal { ) { billing.acknowledgeCallback = { status, purchase -> when(status){ - is CallBackStatus.Error -> { + is PurchaseCallbackStatus.Error -> { val message = "After purchase acknowledge is failed with code: ${status.error}" ApphudLog.log(message) callback?.invoke(ApphudPurchaseResult(null, null, purchase, Error(message))) } - is CallBackStatus.Success -> { + is PurchaseCallbackStatus.Success -> { ApphudLog.log("acknowledge success") when { withValidation -> ackPurchase(purchase, details, callback) @@ -245,12 +246,12 @@ internal object ApphudInternal { } billing.consumeCallback = { status, purchase -> when(status){ - is CallBackStatus.Error -> { + is PurchaseCallbackStatus.Error -> { val message = "After purchase consume is failed with value: ${status.error}" ApphudLog.log(message) callback?.invoke(ApphudPurchaseResult(null, null, purchase, Error(message))) } - is CallBackStatus.Success -> { + is PurchaseCallbackStatus.Success -> { ApphudLog.log("consume callback value: ${status.message}") when { withValidation -> ackPurchase(purchase, details, callback) @@ -262,41 +263,46 @@ internal object ApphudInternal { } } } - billing.purchasesCallback = { purchases -> - if (purchases.isNotEmpty()) { - ApphudLog.log("purchases: $purchases") - - purchases.forEach { - when (it.purchaseState) { - Purchase.PurchaseState.PURCHASED -> - when (details.type) { - BillingClient.SkuType.SUBS -> { - if (!it.isAcknowledged) { - billing.acknowledge(it) + billing.purchasesCallback = { purchasesResult -> + when(purchasesResult){ + is PurchaseUpdatedCallbackStatus.Error ->{ + val message = "Unable to buy product with given product id: ${details.sku} " + + "with error code: ${purchasesResult.result.responseCode} " + + "and debug message: ${purchasesResult.result.debugMessage}" + callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + } + is PurchaseUpdatedCallbackStatus.Success -> { + ApphudLog.log("purchases: $purchasesResult") + + purchasesResult.purchases.forEach { + when (it.purchaseState) { + Purchase.PurchaseState.PURCHASED -> + when (details.type) { + BillingClient.SkuType.SUBS -> { + if (!it.isAcknowledged) { + billing.acknowledge(it) + } + } + BillingClient.SkuType.INAPP -> { + billing.consume(it) + } + else -> { + val message = "After purchase type is null" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, + null, + it, + Error(message))) } } - BillingClient.SkuType.INAPP -> { - billing.consume(it) - } - else -> { - val message = "After purchase type is null" - ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, - null, - it, - Error(message))) - } + else -> { + val message = "After purchase state: ${it.purchaseState}" + ApphudLog.log(message) + callback?.invoke(ApphudPurchaseResult(null, null, it, Error(message))) } - else -> { - val message = "After purchase state: ${it.purchaseState}" - ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, it, Error(message))) } } } - } else { - val message = "Unable to buy product with given product id: ${details.sku}" - callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) } } billing.purchase(activity, details) diff --git a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt index f085b899..805b5cc1 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/AcknowledgeWrapper.kt @@ -7,7 +7,7 @@ import com.android.billingclient.api.Purchase import com.apphud.sdk.response import java.io.Closeable -typealias AcknowledgeCallback = (CallBackStatus, Purchase) -> Unit +typealias AcknowledgeCallback = (PurchaseCallbackStatus, Purchase) -> Unit internal class AcknowledgeWrapper( private val billing: BillingClient @@ -33,8 +33,8 @@ internal class AcknowledgeWrapper( billing.acknowledgePurchase(params) { result: BillingResult -> result.response( MESSAGE, - { callBack?.invoke(CallBackStatus.Error(result.responseCode.toString()), purchase) }, - { callBack?.invoke(CallBackStatus.Success(), purchase) } + { callBack?.invoke(PurchaseCallbackStatus.Error(result.responseCode.toString()), purchase) }, + { callBack?.invoke(PurchaseCallbackStatus.Success(), purchase) } ) } } diff --git a/sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt b/sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt deleted file mode 100644 index ff16dda5..00000000 --- a/sdk/src/main/java/com/apphud/sdk/internal/CallBackStatus.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.apphud.sdk.internal - -sealed class CallBackStatus { - class Success(val message: String? = null) : CallBackStatus() - class Error(val error: String) : CallBackStatus() -} diff --git a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt index 93b64af4..7c117649 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/ConsumeWrapper.kt @@ -6,7 +6,7 @@ import com.android.billingclient.api.Purchase import com.apphud.sdk.response import java.io.Closeable -typealias ConsumeCallback = (CallBackStatus, Purchase) -> Unit +typealias ConsumeCallback = (PurchaseCallbackStatus, Purchase) -> Unit internal class ConsumeWrapper( private val billing: BillingClient @@ -24,8 +24,8 @@ internal class ConsumeWrapper( billing.consumeAsync(params) { result, value -> result.response( "failed response with value: $value", - { callBack?.invoke(CallBackStatus.Error(value), purchase) }, - { callBack?.invoke(CallBackStatus.Success(value), purchase) } + { callBack?.invoke(PurchaseCallbackStatus.Error(value), purchase) }, + { callBack?.invoke(PurchaseCallbackStatus.Success(value), purchase) } ) } } diff --git a/sdk/src/main/java/com/apphud/sdk/internal/PurchaseCallbackStatus.kt b/sdk/src/main/java/com/apphud/sdk/internal/PurchaseCallbackStatus.kt new file mode 100644 index 00000000..c3f4fe2c --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/internal/PurchaseCallbackStatus.kt @@ -0,0 +1,6 @@ +package com.apphud.sdk.internal + +sealed class PurchaseCallbackStatus { + class Success(val message: String? = null) : PurchaseCallbackStatus() + class Error(val error: String) : PurchaseCallbackStatus() +} diff --git a/sdk/src/main/java/com/apphud/sdk/internal/PurchaseUpdatedCallbackStatus.kt b/sdk/src/main/java/com/apphud/sdk/internal/PurchaseUpdatedCallbackStatus.kt new file mode 100644 index 00000000..f6abcfed --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/internal/PurchaseUpdatedCallbackStatus.kt @@ -0,0 +1,9 @@ +package com.apphud.sdk.internal + +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.Purchase + +sealed class PurchaseUpdatedCallbackStatus { + class Success(val purchases: List) : PurchaseUpdatedCallbackStatus() + class Error(val result: BillingResult) : PurchaseUpdatedCallbackStatus() +} diff --git a/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt b/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt index 975a781b..6f55dbba 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/PurchasesUpdated.kt @@ -1,12 +1,12 @@ package com.apphud.sdk.internal import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.Purchase +import com.android.billingclient.api.BillingResult import com.apphud.sdk.isSuccess import com.apphud.sdk.logMessage import java.io.Closeable -typealias PurchasesUpdatedCallback = (List) -> Unit +typealias PurchasesUpdatedCallback = (PurchaseUpdatedCallbackStatus) -> Unit internal class PurchasesUpdated( builder: BillingClient.Builder @@ -15,15 +15,15 @@ internal class PurchasesUpdated( var callback: PurchasesUpdatedCallback? = null init { - builder.setListener { result, list -> + builder.setListener { result: BillingResult, list -> when (result.isSuccess()) { true -> { val purchases = list?.filterNotNull() ?: emptyList() - callback?.invoke(purchases) + callback?.invoke(PurchaseUpdatedCallbackStatus.Success(purchases)) } else -> { result.logMessage("failed purchase") - callback?.invoke(emptyList()) + callback?.invoke(PurchaseUpdatedCallbackStatus.Error(result)) } } } From fde43393a3c0f9f296190f565097b9b3de9b8f23 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Tue, 27 Apr 2021 23:57:18 +0300 Subject: [PATCH 09/12] APH-661 Refactoring of Purchase methods --- .../main/java/com/apphud/sdk/ApphudError.kt | 13 ++++++++++ .../java/com/apphud/sdk/ApphudInternal.kt | 26 +++++++++++-------- .../com/apphud/sdk/ApphudPurchaseResult.kt | 2 +- 3 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 sdk/src/main/java/com/apphud/sdk/ApphudError.kt diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudError.kt b/sdk/src/main/java/com/apphud/sdk/ApphudError.kt new file mode 100644 index 00000000..397822a2 --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/ApphudError.kt @@ -0,0 +1,13 @@ +package com.apphud.sdk + +data class ApphudError( + override val message: String, + /** + * Additional error message + * */ + val secondErrorMessage: String? = null, + /** + * Additional error code + * */ + val errorCode: Int? = null +) : Error(message) \ No newline at end of file diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 8a691ba1..3f2f83c0 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -201,7 +201,7 @@ internal object ApphudInternal { } ?: run { val message = "Unable to fetch product (SkuType.SUBS) with given product id: $productId" - callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, null, ApphudError(message))) } } billing.details(BillingClient.SkuType.INAPP, listOf(productId)) { skuList -> @@ -213,7 +213,7 @@ internal object ApphudInternal { } ?: run { val message = "Unable to fetch product (SkuType.INAPP) with given product id: $productId" - callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, null, ApphudError(message))) } } } @@ -230,7 +230,7 @@ internal object ApphudInternal { is PurchaseCallbackStatus.Error -> { val message = "After purchase acknowledge is failed with code: ${status.error}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, purchase, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, purchase, ApphudError(message))) } is PurchaseCallbackStatus.Success -> { ApphudLog.log("acknowledge success") @@ -249,7 +249,7 @@ internal object ApphudInternal { is PurchaseCallbackStatus.Error -> { val message = "After purchase consume is failed with value: ${status.error}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, purchase, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, null, purchase, ApphudError(message))) } is PurchaseCallbackStatus.Success -> { ApphudLog.log("consume callback value: ${status.message}") @@ -265,11 +265,12 @@ internal object ApphudInternal { } billing.purchasesCallback = { purchasesResult -> when(purchasesResult){ - is PurchaseUpdatedCallbackStatus.Error ->{ - val message = "Unable to buy product with given product id: ${details.sku} " + - "with error code: ${purchasesResult.result.responseCode} " + - "and debug message: ${purchasesResult.result.debugMessage}" - callback?.invoke(ApphudPurchaseResult(null, null, null, Error(message))) + is PurchaseUpdatedCallbackStatus.Error -> { + val error = ApphudError(message = "Unable to buy product with given product id: ${details.sku} ", + secondErrorMessage = purchasesResult.result.debugMessage, + errorCode = purchasesResult.result.responseCode + ) + callback?.invoke(ApphudPurchaseResult(null, null, null, error)) } is PurchaseUpdatedCallbackStatus.Success -> { ApphudLog.log("purchases: $purchasesResult") @@ -292,13 +293,16 @@ internal object ApphudInternal { callback?.invoke(ApphudPurchaseResult(null, null, it, - Error(message))) + ApphudError(message))) } } else -> { val message = "After purchase state: ${it.purchaseState}" ApphudLog.log(message) - callback?.invoke(ApphudPurchaseResult(null, null, it, Error(message))) + callback?.invoke(ApphudPurchaseResult(null, + null, + it, + ApphudError(message))) } } } diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt b/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt index d6e77c48..bbb44899 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudPurchaseResult.kt @@ -31,7 +31,7 @@ class ApphudPurchaseResult ( /** * Error during purchase, if any. */ - var error: Error? = null + var error: ApphudError? = null ) { override fun toString(): String { return "ApphudPurchaseResult(subscription=$subscription, nonRenewingPurchase=$nonRenewingPurchase, purchase=$purchase, error=$error)" From 9aa8e81528a8f0103b156929d3a5c16dd4579509 Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Thu, 29 Apr 2021 01:02:59 +0300 Subject: [PATCH 10/12] APH-698 New function "restorePurchases" was implemented --- .../main/java/com/apphud/app/MainActivity.kt | 6 +- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 15 +++- .../java/com/apphud/sdk/ApphudInternal.kt | 74 ++++++++++++++----- .../PurchaseRestoredCallbackStatus.kt | 9 +++ .../apphud/sdk/internal/SkuDetailsWrapper.kt | 17 ++++- 5 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 sdk/src/main/java/com/apphud/sdk/internal/PurchaseRestoredCallbackStatus.kt diff --git a/app/src/main/java/com/apphud/app/MainActivity.kt b/app/src/main/java/com/apphud/app/MainActivity.kt index 56d9ce0c..b043e42e 100644 --- a/app/src/main/java/com/apphud/app/MainActivity.kt +++ b/app/src/main/java/com/apphud/app/MainActivity.kt @@ -70,7 +70,7 @@ class MainActivity : AppCompatActivity() { Log.e("Apphud", "onClick model: $model") when (model.details) { null -> Log.e("Apphud", "details is empty") - else -> Apphud.purchase(this, model.details) { result -> + else -> Apphud.purchase(this, model.details.sku) { result -> Log.d("apphud","PURCHASE RESULT: ${Apphud.subscriptions() }. Has active subscription: ${Apphud.hasActiveSubscription()}") Log.d("apphud", "${result.error?.toString()}") } @@ -79,7 +79,9 @@ class MainActivity : AppCompatActivity() { val syncButton: Button = findViewById(R.id.syncButtonViewId) syncButton.setOnClickListener { - Apphud.syncPurchases() + Apphud.restorePurchases { subscriptions, purchases, error -> + Log.d("apphud", "restorePurchases: subscriptions=${subscriptions?.toString()} purchases=${purchases?.toString()} error=${error?.toString()} ") + } } val recyclerView = findViewById(R.id.recyclerViewId) recyclerView.adapter = adapter diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index 86730982..ced51117 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -2,7 +2,6 @@ package com.apphud.sdk import android.app.Activity import android.content.Context -import com.android.billingclient.api.Purchase import com.android.billingclient.api.SkuDetails import com.apphud.sdk.domain.ApphudNonRenewingPurchase import com.apphud.sdk.domain.ApphudSubscription @@ -126,6 +125,20 @@ object Apphud { @kotlin.jvm.JvmStatic fun syncPurchases() = ApphudInternal.syncPurchases() + /** + * Implements `Restore Purchases` mechanism. Basically it just sends current Play Market Receipt to Apphud and returns subscriptions info. + * + * Even if callback returns some subscription, it doesn't mean that subscription is active. You should check `subscription.isActive()` value. + * + * @param callback: Required. Returns array of subscription (or subscriptions in case you have more than one subscription group), array of standard in-app purchases and an error. All of three parameters are optional. + */ + @kotlin.jvm.JvmStatic + fun restorePurchases(callback: (subscriptions: List?, + purchases: List?, + error: ApphudError?) -> Unit) { + ApphudInternal.restorePurchases(callback) + } + /** * Returns an array of **SkuDetails** objects that you added in Apphud. * Note that this method will return **null** if products are not yet fetched. diff --git a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt index 3f2f83c0..84e06f0a 100644 --- a/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt +++ b/sdk/src/main/java/com/apphud/sdk/ApphudInternal.kt @@ -16,6 +16,7 @@ import com.apphud.sdk.client.ApphudClient import com.apphud.sdk.domain.* import com.apphud.sdk.internal.BillingWrapper import com.apphud.sdk.internal.PurchaseCallbackStatus +import com.apphud.sdk.internal.PurchaseRestoredCallbackStatus import com.apphud.sdk.internal.PurchaseUpdatedCallbackStatus import com.apphud.sdk.parser.GsonParser import com.apphud.sdk.parser.Parser @@ -345,31 +346,66 @@ internal object ApphudInternal { } } - internal fun syncPurchases() { - storage.isNeedSync = true - billing.restoreCallback = { records -> - - ApphudLog.log("restoreCallback: $records") + internal fun restorePurchases(callback: (subscriptions: List?, + purchases: List?, + error: ApphudError?) -> Unit) { + syncPurchases(allowsReceiptRefresh = true, callback = callback) + } - when { - prevPurchases.containsAll(records) -> ApphudLog.log("syncPurchases: Don't send equal purchases from prev state") - else -> client.purchased(makeRestorePurchasesBody(records)) { customer -> - handler.post { - prevPurchases.addAll(records) - storage.isNeedSync = false - storage.customer = customer - ApphudLog.log("syncPurchases: customer updated $customer") - apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) - apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) + internal fun syncPurchases( + allowsReceiptRefresh: Boolean = false, + callback: (( + subscriptions: List?, + purchases: List?, + error: ApphudError? + ) -> Unit)? = null + ) { + storage.isNeedSync = true + billing.restoreCallback = { restoreStatus -> + when(restoreStatus){ + is PurchaseRestoredCallbackStatus.Error -> { + when (restoreStatus.result == null) { + //Restore is success, but list of purchases is empty + true -> { + val error = ApphudError(message = restoreStatus.message ?: "") + callback?.invoke(null, null, error) + } + else -> { + val error = ApphudError(message = "Restore Purchases is failed for type: ${restoreStatus.message}", + secondErrorMessage = restoreStatus.result.debugMessage, + errorCode = restoreStatus.result.responseCode) + callback?.invoke(null, null, error) + } + } + } + is PurchaseRestoredCallbackStatus.Success -> { + ApphudLog.log("PurchaseRestoredCallback: ${restoreStatus.purchases}") + if (!allowsReceiptRefresh && prevPurchases.containsAll(restoreStatus.purchases)) { + ApphudLog.log("SyncPurchases: Don't send equal purchases from prev state") + } else { + client.purchased(makeRestorePurchasesBody(restoreStatus.purchases)) { customer -> + handler.post { + prevPurchases.addAll(restoreStatus.purchases) + storage.isNeedSync = false + storage.customer = customer + ApphudLog.log("SyncPurchases: customer updated $customer") + apphudListener?.apphudSubscriptionsUpdated(customer.subscriptions) + apphudListener?.apphudNonRenewingPurchasesUpdated(customer.purchases) + callback?.invoke(customer.subscriptions, customer.purchases, null) + } + ApphudLog.log("SyncPurchases: success send history purchases ${restoreStatus.purchases}") + } } - ApphudLog.log("syncPurchases: success send history purchases $records") } } + } billing.historyCallback = { purchases -> - ApphudLog.log("historyCallback: $purchases") - billing.restore(BillingClient.SkuType.SUBS, purchases) - billing.restore(BillingClient.SkuType.INAPP, purchases) + if(!purchases.isNullOrEmpty()){ + ApphudLog.log("historyCallback: $purchases") + billing.restore(BillingClient.SkuType.SUBS, purchases) + billing.restore(BillingClient.SkuType.INAPP, purchases) + } } billing.queryPurchaseHistory(BillingClient.SkuType.SUBS) billing.queryPurchaseHistory(BillingClient.SkuType.INAPP) diff --git a/sdk/src/main/java/com/apphud/sdk/internal/PurchaseRestoredCallbackStatus.kt b/sdk/src/main/java/com/apphud/sdk/internal/PurchaseRestoredCallbackStatus.kt new file mode 100644 index 00000000..fd01d856 --- /dev/null +++ b/sdk/src/main/java/com/apphud/sdk/internal/PurchaseRestoredCallbackStatus.kt @@ -0,0 +1,9 @@ +package com.apphud.sdk.internal + +import com.android.billingclient.api.BillingResult +import com.apphud.sdk.domain.PurchaseRecordDetails + +sealed class PurchaseRestoredCallbackStatus { + class Success(val purchases: List) : PurchaseRestoredCallbackStatus() + class Error(val result: BillingResult? = null, val message: String? = null) : PurchaseRestoredCallbackStatus() +} diff --git a/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt b/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt index ed32549b..30364a29 100644 --- a/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt +++ b/sdk/src/main/java/com/apphud/sdk/internal/SkuDetailsWrapper.kt @@ -13,7 +13,7 @@ import kotlin.concurrent.thread typealias SkuType = String typealias ApphudSkuDetailsCallback = (List) -> Unit -typealias ApphudSkuDetailsRestoreCallback = (List) -> Unit +typealias ApphudSkuDetailsRestoreCallback = (PurchaseRestoredCallbackStatus) -> Unit internal class SkuDetailsWrapper( private val billing: BillingClient @@ -46,11 +46,20 @@ internal class SkuDetailsWrapper( ) } when (purchases.isEmpty()) { - true -> ApphudLog.log("SkuDetails return empty list for $type and records: $records") - else -> restoreCallback?.invoke(purchases) + true -> { + val message = "SkuDetails return empty list for $type and records: $records" + ApphudLog.log(message) + restoreCallback?.invoke(PurchaseRestoredCallbackStatus.Error(result = null, message = message)) + } + else -> { + restoreCallback?.invoke(PurchaseRestoredCallbackStatus.Success(purchases)) + } } } - else -> result.logMessage("restoreAsync type: $type products: $products") + else -> { + result.logMessage("restoreAsync failed for type: $type products: $products") + restoreCallback?.invoke(PurchaseRestoredCallbackStatus.Error(result = result, message = type)) + } } } } From c7684c393a5709c3735dc449aa28ef0c580e06fb Mon Sep 17 00:00:00 2001 From: Aleksey Ivantsov Date: Thu, 29 Apr 2021 12:36:07 +0300 Subject: [PATCH 11/12] APH-661 Refactoring of Purchase methods --- sdk/src/main/java/com/apphud/sdk/Apphud.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/com/apphud/sdk/Apphud.kt b/sdk/src/main/java/com/apphud/sdk/Apphud.kt index ced51117..1f0d5620 100644 --- a/sdk/src/main/java/com/apphud/sdk/Apphud.kt +++ b/sdk/src/main/java/com/apphud/sdk/Apphud.kt @@ -126,7 +126,7 @@ object Apphud { fun syncPurchases() = ApphudInternal.syncPurchases() /** - * Implements `Restore Purchases` mechanism. Basically it just sends current Play Market Receipt to Apphud and returns subscriptions info. + * Implements `Restore Purchases` mechanism. Basically it just sends current Play Market Purchase Tokens to Apphud and returns subscriptions info. * * Even if callback returns some subscription, it doesn't mean that subscription is active. You should check `subscription.isActive()` value. * From 82c5ea9e738fa7c34b48dad404dd577c9d8c5bd0 Mon Sep 17 00:00:00 2001 From: ren6 Date: Thu, 29 Apr 2021 15:29:55 +0300 Subject: [PATCH 12/12] update example app --- app/src/main/java/com/apphud/app/Constants.kt | 2 +- app/src/main/java/com/apphud/app/MainActivity.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/apphud/app/Constants.kt b/app/src/main/java/com/apphud/app/Constants.kt index 4f2d5b65..b536fd97 100644 --- a/app/src/main/java/com/apphud/app/Constants.kt +++ b/app/src/main/java/com/apphud/app/Constants.kt @@ -1,5 +1,5 @@ package com.apphud.app object Constants { - const val API_KEY = "app_DZzKAkuBWhe9nz3qeofJMU9PJezkxn" + const val API_KEY = "app_4sY9cLggXpMDDQMmvc5wXUPGReMp8G" } \ No newline at end of file diff --git a/app/src/main/java/com/apphud/app/MainActivity.kt b/app/src/main/java/com/apphud/app/MainActivity.kt index b043e42e..209c8c07 100644 --- a/app/src/main/java/com/apphud/app/MainActivity.kt +++ b/app/src/main/java/com/apphud/app/MainActivity.kt @@ -36,7 +36,10 @@ class MainActivity : AppCompatActivity() { null -> mapper.map(subscription) else -> mapper.map(product, subscription) } - products[model.productId] = model + when (val existingSubscription = products[model.productId]?.subscription) { + null -> products[model.productId] = model + else -> Log.d("apphud","already has subscription, will not update") + } } adapter.products = products.values.toList()