Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added context keys to purchase #625

Merged
merged 11 commits into from
Aug 19, 2024
2 changes: 2 additions & 0 deletions app/src/main/java/com/qonversion/android/app/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.qonversion.android.sdk.automations.dto.QActionResultType
import com.qonversion.android.sdk.automations.dto.QScreenPresentationConfig
import com.qonversion.android.sdk.automations.dto.QScreenPresentationStyle
import com.qonversion.android.sdk.dto.QPurchaseModel
import com.qonversion.android.sdk.dto.QPurchaseOptions
import com.qonversion.android.sdk.dto.entitlements.QEntitlement
import com.qonversion.android.sdk.dto.QonversionError
import com.qonversion.android.sdk.dto.products.QProduct
Expand Down Expand Up @@ -189,6 +190,7 @@ class HomeFragment : Fragment() {
showError(requireContext(), error, TAG)
}
})

}

private fun showLoading(isLoading: Boolean) {
Expand Down
43 changes: 29 additions & 14 deletions config/detekt/baseline.xml

Large diffs are not rendered by default.

54 changes: 53 additions & 1 deletion sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import android.app.Activity
import android.util.Log
import com.qonversion.android.sdk.dto.QAttributionProvider
import com.qonversion.android.sdk.dto.QPurchaseModel
import com.qonversion.android.sdk.dto.QPurchaseOptions
import com.qonversion.android.sdk.dto.QPurchaseUpdateModel
import com.qonversion.android.sdk.dto.products.QProduct
import com.qonversion.android.sdk.dto.properties.QUserPropertyKey
import com.qonversion.android.sdk.internal.InternalConfig
import com.qonversion.android.sdk.internal.QonversionInternal
Expand Down Expand Up @@ -75,14 +77,63 @@ interface Qonversion {
*/
fun syncHistoricalData()

/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend
* @param context current activity context
* @param product product for purchase
* @param options necessary information for purchase
* @param callback - callback that will be called when response is received
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
*/
fun purchase(
context: Activity,
product: QProduct,
options: QPurchaseOptions,
callback: QonversionEntitlementsCallback
)

/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend
* @param context current activity context
* @param product product for purchase
* @param callback - callback that will be called when response is received
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
*/
fun purchase(
context: Activity,
product: QProduct,
callback: QonversionEntitlementsCallback
)

/**
* Update (upgrade/downgrade) subscription and validate it through server-to-server using Qonversion's Backend
* @param context current activity context
* @param product product for purchase
* @param options necessary information for purchase
* @param callback - callback that will be called when response is received
* @see [Update policy](https://developer.android.com/google/play/billing/subscriptions#replacement-modes)
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
*/
fun updatePurchase(
context: Activity,
product: QProduct,
options: QPurchaseOptions,
callback: QonversionEntitlementsCallback
)

/**
* Make a purchase and validate it through server-to-server using Qonversion's Backend
* @param context current activity context
* @param purchaseModel necessary information for purchase
* @param callback - callback that will be called when response is received
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
*/
fun purchase(context: Activity, purchaseModel: QPurchaseModel, callback: QonversionEntitlementsCallback)
@Deprecated("Use the new purchase() method", replaceWith = ReplaceWith("purchase(context, TODO(\"pass product here\"), callback)"))
fun purchase(
context: Activity,
purchaseModel: QPurchaseModel,
callback: QonversionEntitlementsCallback
)

/**
* Update (upgrade/downgrade) subscription and validate it through server-to-server using Qonversion's Backend
Expand All @@ -92,6 +143,7 @@ interface Qonversion {
* @see [Update policy](https://developer.android.com/google/play/billing/subscriptions#replacement-modes)
* @see [Making Purchases](https://documentation.qonversion.io/docs/making-purchases)
*/
@Deprecated("Use the new updatePurchase() method", replaceWith = ReplaceWith("updatePurchase(context, TODO(\"pass product here\"), TODO(\"pass purchase options here\"), callback)"))
fun updatePurchase(
context: Activity,
purchaseUpdateModel: QPurchaseUpdateModel,
Expand Down
102 changes: 102 additions & 0 deletions sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.qonversion.android.sdk.dto

import com.qonversion.android.sdk.QonversionConfig.Builder
import com.qonversion.android.sdk.dto.products.QProduct
import com.qonversion.android.sdk.dto.products.QProductOfferDetails
import com.qonversion.android.sdk.dto.products.QProductStoreDetails
import com.squareup.moshi.JsonClass

/**
* Purchase options that may be used to modify purchase process.
* To create an instance, use the nested [Builder] class.
*/
@JsonClass(generateAdapter = true)
class QPurchaseOptions internal constructor (
SpertsyanKM marked this conversation as resolved.
Show resolved Hide resolved
internal val contextKeys: List<String>? = null,
internal val offerId: String? = null,
internal val applyOffer: Boolean = true,
internal val oldProduct: QProduct? = null,
internal val updatePolicy: QPurchaseUpdatePolicy? = null
) {
/**
* The builder of QPurchaseOptions instance.
*
* This class contains a variety of methods to customize the purchase behavior.
* You can call them sequentially and call [build] finally to get the [QPurchaseOptions] instance.
*/
class Builder {
private var contextKeys: List<String>? = null
private var offerId: String? = null
private var applyOffer: Boolean = true
private var oldProduct: QProduct? = null
private var updatePolicy: QPurchaseUpdatePolicy? = null

/**
* Set the context keys associated with a purchase.
*
* @param contextKeys context keys for the purchase.
* @return builder instance for chain calls.
*/
fun setContextKeys(contextKeys: List<String>): QPurchaseOptions.Builder = apply {
this.contextKeys = contextKeys
}

/**
* Set context keys associated with a purchase.
*
* @param oldProduct Qonversion product from which the upgrade/downgrade
* will be initialized.
* @return builder instance for chain calls.
*/
fun setOldProduct(oldProduct: QProduct): QPurchaseOptions.Builder = apply {
this.oldProduct = oldProduct
}

/**
* Set the update policy for the purchase.
* If the [updatePolicy] is not provided, then default one
* will be selected - [QPurchaseUpdatePolicy.WithTimeProration].
* @param updatePolicy update policy for the purchase.
* @return builder instance for chain calls.
*/
fun setUpdatePolicy(updatePolicy: QPurchaseUpdatePolicy): QPurchaseOptions.Builder = apply {
this.updatePolicy = updatePolicy
}

/**
* Set offer for the purchase.
* @param offer concrete offer which you'd like to purchase.
* @return builder instance for chain calls.
*/
fun setOffer(offer: QProductOfferDetails): QPurchaseOptions.Builder = apply {
this.offerId = offer.offerId
}

/**
* Set the offer Id to the purchase.
* If [offerId] is not specified, then the default offer will be applied. To know how we choose
* the default offer, see [QProductStoreDetails.defaultSubscriptionOfferDetails].
* @param offerId concrete offer Id which you'd like to purchase.
* @return builder instance for chain calls.
*/
fun setOfferId(offerId: String): QPurchaseOptions.Builder = apply {
this.offerId = offerId
}

/**
* Call this function to remove any intro/trial offer from the purchase (use only a bare base plan).
* @return builder instance for chain calls.
*/
fun removeOffer(): QPurchaseOptions.Builder = apply {
this.applyOffer = false
}

/**
* Generate [QPurchaseOptions] instance with all the provided options.
* @return the complete [QPurchaseOptions] instance.
*/
fun build(): QPurchaseOptions {
return QPurchaseOptions(contextKeys, offerId, applyOffer, oldProduct, updatePolicy)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ data class QProduct(
* To know how we choose the default offer, see [QProductStoreDetails.defaultSubscriptionOfferDetails].
* @return purchase model to pass to the purchase method.
*/
@Deprecated("Use new QPurchaseOptions object instead", replaceWith = ReplaceWith("QPurchaseOptions.Builder().setOfferId(offerId).build()"))
@JvmOverloads
fun toPurchaseModel(offerId: String? = null): QPurchaseModel {
return QPurchaseModel(qonversionID, offerId)
Expand All @@ -116,6 +117,7 @@ data class QProduct(
* @param offer concrete offer which you'd like to purchase.
* @return purchase model to pass to the purchase method.
*/
@Deprecated("Use new QPurchaseOptions object instead", replaceWith = ReplaceWith("QPurchaseOptions.Builder().setOffer(offer).build()"))
fun toPurchaseModel(offer: QProductOfferDetails?): QPurchaseModel {
val model = toPurchaseModel(offer?.offerId)
// Remove offer for the case when provided offer details are for bare base plan.
Expand All @@ -134,6 +136,7 @@ data class QProduct(
* @param updatePolicy purchase update policy.
* @return purchase model to pass to the update purchase method.
*/
@Deprecated("Use new QPurchaseOptions object instead", replaceWith = ReplaceWith("QPurchaseOptions.Builder().setOldProduct(TODO(\"pass old product here\")).build()"))
@JvmOverloads
fun toPurchaseUpdateModel(
oldProductId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.Application
import android.content.pm.PackageManager
import android.os.Build
import com.android.billingclient.api.Purchase
import com.qonversion.android.sdk.dto.QPurchaseOptions
import com.qonversion.android.sdk.dto.entitlements.QEntitlement
import com.qonversion.android.sdk.listeners.QonversionEligibilityCallback
import com.qonversion.android.sdk.dto.QonversionError
Expand Down Expand Up @@ -94,6 +95,10 @@ internal class QProductCenterManager internal constructor(

private var converter: PurchaseConverter = GooglePurchaseConverter()

private val processingPurchaseOptions: MutableMap<String, QPurchaseOptions> by lazy {
purchasesCache.loadProcessingPurchasesOptions().toMutableMap()
}

@Volatile
lateinit var billingService: BillingService
@Synchronized set
Expand Down Expand Up @@ -324,7 +329,7 @@ internal class QProductCenterManager internal constructor(
callback.onError(QonversionError(QonversionErrorCode.ProductNotFound))
return
}
val oldProduct: QProduct? = getProductForPurchase(purchaseModel.oldProductId, products)
val oldProduct: QProduct? = purchaseModel.options?.oldProduct ?: getProductForPurchase(purchaseModel.oldProductId, products)
val purchaseModelEnriched = purchaseModel.enrich(product, oldProduct)
processPurchase(context, purchaseModelEnriched, callback)
}
Expand All @@ -349,9 +354,28 @@ internal class QProductCenterManager internal constructor(
}

purchasingCallbacks[purchaseModel.product.storeID] = callback

updatePurchaseOptions(purchaseModel.options, purchaseModel.product.storeID)

billingService.purchase(context, purchaseModel)
}

private fun updatePurchaseOptions(options: QPurchaseOptions?, storeProductId: String?) {
SpertsyanKM marked this conversation as resolved.
Show resolved Hide resolved
storeProductId?.let { productId ->
options?.let {
processingPurchaseOptions[productId] = it
} ?: run {
processingPurchaseOptions.remove(productId)
}

purchasesCache.saveProcessingPurchasesOptions(processingPurchaseOptions)
}
}

private fun removePurchaseOptions(productId: String?) {
updatePurchaseOptions(null, productId)
}

private fun getProductForPurchase(
productId: String?,
products: Map<String, QProduct>
Expand Down Expand Up @@ -651,7 +675,7 @@ internal class QProductCenterManager internal constructor(

processingPurchases = completedPurchases

val purchasesInfo = converter.convertPurchases(completedPurchases)
val purchasesInfo = converter.convertPurchases(completedPurchases, processingPurchaseOptions)

val handledPurchasesCallback =
getWrappedPurchasesCallback(completedPurchases, callback)
Expand All @@ -673,6 +697,9 @@ internal class QProductCenterManager internal constructor(
return object : QonversionLaunchCallback {
override fun onSuccess(launchResult: QLaunchResult) {
handledPurchasesCache.saveHandledPurchases(trackingPurchases)
trackingPurchases.forEach {
removePurchaseOptions(it.productId)
}
SpertsyanKM marked this conversation as resolved.
Show resolved Hide resolved
outerCallback?.onSuccess(launchResult)
}

Expand Down Expand Up @@ -959,7 +986,8 @@ internal class QProductCenterManager internal constructor(
val product: QProduct? = launchResultCache.getActualProducts()?.values?.find {
it.storeID == purchase.productId
}
val purchaseInfo = converter.convertPurchase(purchase)
val currentPurchaseOptions = processingPurchaseOptions[purchase.productId]
val purchaseInfo = converter.convertPurchase(purchase, currentPurchaseOptions)
repository.purchase(
installDate,
purchaseInfo,
Expand All @@ -970,6 +998,7 @@ internal class QProductCenterManager internal constructor(

val entitlements = launchResult.permissions.toEntitlementsMap()

removePurchaseOptions(product?.storeID)
purchaseCallback?.onSuccess(entitlements) ?: run {
internalConfig.entitlementsUpdateListener?.onEntitlementsUpdated(
entitlements
Expand All @@ -981,6 +1010,8 @@ internal class QProductCenterManager internal constructor(
override fun onError(error: QonversionError) {
storeFailedPurchaseIfNecessary(purchase, purchaseInfo, product)

removePurchaseOptions(product?.storeID)

if (shouldCalculatePermissionsLocally(error)) {
calculatePurchasePermissionsLocally(
purchase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.qonversion.android.sdk.Qonversion
import com.qonversion.android.sdk.automations.internal.QAutomationsManager
import com.qonversion.android.sdk.dto.QAttributionProvider
import com.qonversion.android.sdk.dto.QPurchaseModel
import com.qonversion.android.sdk.dto.QPurchaseOptions
import com.qonversion.android.sdk.dto.QPurchaseUpdateModel
import com.qonversion.android.sdk.dto.entitlements.QEntitlement
import com.qonversion.android.sdk.dto.QRemoteConfig
Expand Down Expand Up @@ -170,6 +171,44 @@ internal class QonversionInternal(
)
}

override fun purchase(
context: Activity,
product: QProduct,
options: QPurchaseOptions,
callback: QonversionEntitlementsCallback
) {
productCenterManager.purchaseProduct(
context,
PurchaseModelInternal(product, options),
mainEntitlementsCallback(callback)
)
}

override fun purchase(
context: Activity,
product: QProduct,
callback: QonversionEntitlementsCallback
) {
productCenterManager.purchaseProduct(
context,
PurchaseModelInternal(product),
mainEntitlementsCallback(callback)
)
}

override fun updatePurchase(
context: Activity,
product: QProduct,
options: QPurchaseOptions,
callback: QonversionEntitlementsCallback
) {
productCenterManager.purchaseProduct(
context,
PurchaseModelInternal(product, options),
mainEntitlementsCallback(callback)
)
}

override fun updatePurchase(
context: Activity,
purchaseUpdateModel: QPurchaseUpdateModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal class BillingClientWrapper(
activity: Activity,
product: QProduct,
offerId: String?,
applyOffer: Boolean,
applyOffer: Boolean?,
updatePurchaseInfo: UpdatePurchaseInfo?,
onFailed: (error: BillingError) -> Unit
) {
Expand All @@ -76,7 +76,7 @@ internal class BillingClientWrapper(

val offerDetails: QProductOfferDetails? = when {
storeDetails.isInApp -> null
!applyOffer -> {
applyOffer == false -> {
storeDetails.basePlanSubscriptionOfferDetails ?: run {
fireError("Failed to find base plan offer for Qonversion product ${product.qonversionID}")
return
Expand Down
Loading
Loading