Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.chargebee.android.models.PurchaseTransaction
import com.chargebee.android.exceptions.CBException
import com.chargebee.android.exceptions.ChargebeeResult
import com.chargebee.android.models.CBProduct
import com.chargebee.android.models.StoreStatus
import com.chargebee.android.network.CBReceiptResponse
import com.chargebee.android.restore.CBRestorePurchaseManager
import kotlin.collections.ArrayList
Expand Down Expand Up @@ -309,10 +310,7 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener {
queryPurchaseHistory { purchaseHistoryList ->
val storeTransactions = arrayListOf<PurchaseTransaction>()
storeTransactions.addAll(purchaseHistoryList)
CBRestorePurchaseManager.fetchStoreSubscriptionStatus(
storeTransactions,
restorePurchaseCallBack
)
fetchStoreSubscriptionStatus(storeTransactions)
}
} else {
restorePurchaseCallBack.onError(
Expand All @@ -321,6 +319,63 @@ class BillingClientManager(context: Context) : PurchasesUpdatedListener {
}
}

internal fun fetchStoreSubscriptionStatus(storeTransactions: ArrayList<PurchaseTransaction>){
CBRestorePurchaseManager.fetchStoreSubscriptionStatus(storeTransactions, result = { restorePurchases ->
val activePurchases = restorePurchases.filter { subscription ->
subscription.storeStatus == StoreStatus.Active.value
}
val allPurchases = restorePurchases.filter { subscription ->
subscription.storeStatus == StoreStatus.Active.value || subscription.storeStatus == StoreStatus.InTrial.value
|| subscription.storeStatus == StoreStatus.Cancelled.value || subscription.storeStatus == StoreStatus.Paused.value
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need this? Should restorePurchases by definition be same as allPurchases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

if (CBPurchase.includeInActivePurchases) {
restorePurchaseCallBack.onSuccess(allPurchases)
syncPurchaseWithChargebee(CBRestorePurchaseManager.allTransactions)
} else {
restorePurchaseCallBack.onSuccess(activePurchases)
syncPurchaseWithChargebee(CBRestorePurchaseManager.activeTransactions)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: alignment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

}
}, error = {
restorePurchaseCallBack.onError(error = it)
})
}

internal fun syncPurchaseWithChargebee(storeTransactions: ArrayList<PurchaseTransaction>) {
storeTransactions.forEach { productIdList ->
retrieveProducts(
CBPurchase.ProductType.SUBS.value,
ArrayList(productIdList.productId),
object : CBCallback.ListProductsCallback<ArrayList<CBProduct>> {
override fun onSuccess(productIDs: ArrayList<CBProduct>) {
if (productIDs.size == 0) {
Log.i(javaClass.simpleName, "Product not available")
return
}
CBPurchase.validateReceipt(
productIdList.purchaseToken,
productIDs.first()
) {
when (it) {
is ChargebeeResult.Success -> {
Log.i(javaClass.simpleName, "result : ${it.data}")
}
is ChargebeeResult.Error -> {
Log.e(
javaClass.simpleName,
"Exception from Server - validateReceipt() : ${it.exp.message}"
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How could this be conveyed to the app?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We are just doing sync purchase. this not required to be notified to user.

}
}
}

override fun onError(error: CBException) {
Log.e(javaClass.simpleName, "Error: ${error.message}")
}
})
}
}

private fun queryPurchaseHistory(
storeTransactions: (List<PurchaseTransaction>) -> Unit
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ object CBPurchase {
completion: (ChargebeeResult<Any>) -> Unit
) {
try {
validateReceipt(purchaseToken, product.productId, completion)
val offerDetail = getOfferDetail(product)
validateReceipt(purchaseToken, product.productId, offerDetail, completion)
} catch (exp: Exception) {
Log.e(javaClass.simpleName, "Exception in validateReceipt() :" + exp.message)
ChargebeeResult.Error(
Expand All @@ -184,14 +185,16 @@ object CBPurchase {
internal fun validateReceipt(
purchaseToken: String,
productId: String,
offerDetail: OfferDetail,
completion: (ChargebeeResult<Any>) -> Unit
) {
val logger = CBLogger(name = "buy", action = "process_purchase_command")
val params = Params(
purchaseToken,
productId,
customer,
Chargebee.channel
Chargebee.channel,
offerDetail
)
ResultHandler.safeExecuter(
{ ReceiptResource().validateReceipt(params) },
Expand Down Expand Up @@ -301,4 +304,39 @@ object CBPurchase {
}
return billingClientManager as BillingClientManager
}

private fun convertIntroductoryPriceAmountInMicros(product: CBProduct): Long {
return product.skuDetails.introductoryPriceAmountMicros / 1_000_0
}

private fun getOfferDetail(product: CBProduct): OfferDetail {
var offerDetail = OfferDetail( introductoryPrice = "",
introductoryPriceAmountMicros = 0,
introductoryPricePeriod = 0,
introductoryOfferType = "")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to return this offerdetail even for cases where there are no offers? Can we return and optional empty instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, made changes accordingly.

val introductoryOfferType: String
val numberOfUnits: Int
if (product.skuDetails.introductoryPrice.isNotEmpty()) {
val subscriptionPeriod = product.skuDetails.introductoryPricePeriod
if (product.skuDetails.introductoryPriceCycles == 1) {
introductoryOfferType = "pay_up_front"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we treat this as an enum?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

numberOfUnits =
subscriptionPeriod.substring(1, subscriptionPeriod.length - 1).toInt()
} else {
introductoryOfferType = "pay_as_you_go"
numberOfUnits = product.skuDetails.introductoryPriceCycles
}
val introductoryPriceAmountMicros = convertIntroductoryPriceAmountInMicros(product)
offerDetail = OfferDetail(
introductoryPrice = product.skuDetails.introductoryPrice,
introductoryPriceAmountMicros = introductoryPriceAmountMicros,
introductoryPricePeriod = numberOfUnits,
introductoryOfferType = introductoryOfferType
)
return offerDetail
}
return offerDetail
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chargebee.android.models

data class OfferDetail(
val introductoryPrice: String?,
val introductoryPriceAmountMicros: Long?,
val introductoryPricePeriod: Int?,
val introductoryOfferType: String?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we treat this as an enum?
And are all of these values need to be optional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

)
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.chargebee.android.network

import com.chargebee.android.models.OfferDetail

internal class CBReceiptRequestBody( val receipt: String,
val productId: String,
val customer: CBCustomer?,
val channel: String) {
val channel: String,
val offerDetail: OfferDetail) {
companion object {
fun fromCBReceiptReqBody(params: Params): CBReceiptRequestBody {
return CBReceiptRequestBody(
params.receipt,
params.productId,
params.customer,
params.channel
params.channel,
params.offerDetail
)
}
}
Expand All @@ -24,6 +28,7 @@ internal class CBReceiptRequestBody( val receipt: String,
)
}


fun toCBReceiptReqCustomerBody(): Map<String, String?> {
return mapOf(
"receipt" to this.receipt,
Expand All @@ -32,14 +37,20 @@ internal class CBReceiptRequestBody( val receipt: String,
"customer[first_name]" to this.customer?.firstName,
"customer[last_name]" to this.customer?.lastName,
"customer[email]" to this.customer?.email,
"channel" to this.channel
"channel" to this.channel,
"introductory_offer[price]" to this.offerDetail.introductoryPriceAmountMicros.toString(),
"introductory_offer[type]" to this.offerDetail.introductoryOfferType,
"introductory_offer[period]" to this.offerDetail.introductoryPricePeriod.toString()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are these values mandatory for the API?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not mandatory for this API. if no offers associated for the plans yes we can ignore it. thanks @cb-haripriyan

)
}
fun toMap(): Map<String, String> {
fun toMap(): Map<String, String?> {
return mapOf(
"receipt" to this.receipt,
"product[id]" to this.productId,
"channel" to this.channel
"channel" to this.channel,
"introductory_offer[price]" to this.offerDetail.introductoryPriceAmountMicros.toString(),
"introductory_offer[type]" to this.offerDetail.introductoryOfferType,
"introductory_offer[period]" to this.offerDetail.introductoryPricePeriod.toString()
)
}
}
Expand All @@ -48,7 +59,8 @@ data class Params(
val receipt: String,
val productId: String,
val customer: CBCustomer?,
val channel: String
val channel: String,
val offerDetail: OfferDetail
)
data class CBCustomer(
val id: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import com.chargebee.android.responseFromServer
internal class ReceiptResource : BaseResource(baseUrl = Chargebee.baseUrl){

internal suspend fun validateReceipt(params: Params): ChargebeeResult<Any> {
var dataMap = mapOf<String, String?>()
val paramDetail = CBReceiptRequestBody.fromCBReceiptReqBody(params)
dataMap = if (params.customer != null && !(TextUtils.isEmpty(params.customer.id))) {
val dataMap = if (params.customer != null && !(TextUtils.isEmpty(params.customer.id))) {
paramDetail.toCBReceiptReqCustomerBody()
} else{
paramDetail.toMap()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.chargebee.android.restore

import android.content.Context
import android.util.Log
import com.chargebee.android.ErrorDetail
import com.chargebee.android.billingservice.CBCallback
Expand All @@ -15,9 +16,9 @@ import com.chargebee.android.resources.RestorePurchaseResource
class CBRestorePurchaseManager {

companion object {
private var allTransactions = ArrayList<PurchaseTransaction>()
internal var allTransactions = ArrayList<PurchaseTransaction>()
private var restorePurchases = ArrayList<CBRestoreSubscription>()
private var activeTransactions = ArrayList<PurchaseTransaction>()
internal var activeTransactions = ArrayList<PurchaseTransaction>()
private lateinit var completionCallback: CBCallback.RestorePurchaseCallback

private fun retrieveStoreSubscription(
Expand Down Expand Up @@ -55,9 +56,9 @@ class CBRestorePurchaseManager {

internal fun fetchStoreSubscriptionStatus(
storeTransactions: ArrayList<PurchaseTransaction>,
completionCallback: CBCallback.RestorePurchaseCallback
result: (List<CBRestoreSubscription>) -> Unit,
error: (CBException) -> Unit
) {
this.completionCallback = completionCallback
if (storeTransactions.isNotEmpty()) {
val storeTransaction =
storeTransactions.firstOrNull()?.also { storeTransactions.remove(it) }
Expand All @@ -68,20 +69,24 @@ class CBRestorePurchaseManager {
StoreStatus.Active.value -> activeTransactions.add(storeTransaction)
else -> allTransactions.add(storeTransaction)
}
getRestorePurchases(storeTransactions)
getRestorePurchases(storeTransactions, result, error)
}, { _ ->
getRestorePurchases(storeTransactions)
getRestorePurchases(storeTransactions, result, error)
})
}
} else {
completionCallback.onSuccess(emptyList())
result(emptyList())
}
}

internal fun getRestorePurchases(storeTransactions: ArrayList<PurchaseTransaction>) {
internal fun getRestorePurchases(
storeTransactions: ArrayList<PurchaseTransaction>,
result: (List<CBRestoreSubscription>) -> Unit,
error: (CBException) -> Unit
) {
if (storeTransactions.isEmpty()) {
if (restorePurchases.isEmpty()) {
completionCallback.onError(
error(
CBException(
ErrorDetail(
message = GPErrorCode.InvalidPurchaseToken.errorMsg,
Expand All @@ -90,46 +95,13 @@ class CBRestorePurchaseManager {
)
)
} else {
val activePurchases = restorePurchases.filter { subscription ->
subscription.storeStatus == StoreStatus.Active.value
}
val allPurchases = restorePurchases.filter { subscription ->
subscription.storeStatus == StoreStatus.Active.value || subscription.storeStatus == StoreStatus.InTrial.value
|| subscription.storeStatus == StoreStatus.Cancelled.value || subscription.storeStatus == StoreStatus.Paused.value
}
if (CBPurchase.includeInActivePurchases) {
completionCallback.onSuccess(allPurchases)
syncPurchaseWithChargebee(allTransactions)
} else {
completionCallback.onSuccess(activePurchases)
syncPurchaseWithChargebee(activeTransactions)
}
result(restorePurchases)
}
restorePurchases.clear()
allTransactions.clear()
activeTransactions.clear()
} else {
fetchStoreSubscriptionStatus(storeTransactions, completionCallback)
}
}

internal fun syncPurchaseWithChargebee(storeTransactions: ArrayList<PurchaseTransaction>) {
storeTransactions.forEach { productIdList ->
validateReceipt(productIdList.purchaseToken, productIdList.productId.first())
}
}

internal fun validateReceipt(purchaseToken: String, productId: String) {
CBPurchase.validateReceipt(purchaseToken, productId) {
when (it) {
is ChargebeeResult.Success -> {
Log.i(javaClass.simpleName, "result : ${it.data}")
}
is ChargebeeResult.Error -> {
Log.e(
javaClass.simpleName,
"Exception from Server - validateReceipt() : ${it.exp.message}"
)
}
}
fetchStoreSubscriptionStatus(storeTransactions, result, error)
}
}
}
Expand Down
Loading