diff --git a/app/src/main/java/com/qonversion/android/app/HomeFragment.kt b/app/src/main/java/com/qonversion/android/app/HomeFragment.kt index 373e45a4..8f71fa40 100644 --- a/app/src/main/java/com/qonversion/android/app/HomeFragment.kt +++ b/app/src/main/java/com/qonversion/android/app/HomeFragment.kt @@ -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 @@ -189,6 +190,7 @@ class HomeFragment : Fragment() { showError(requireContext(), error, TAG) } }) + } private fun showLoading(isLoading: Boolean) { diff --git a/build.gradle b/build.gradle index 8168fcee..ebc71c84 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask buildscript { ext { release = [ - versionName: "8.0.2", + versionName: "8.1.0", versionCode: 1 ] } diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 53d16f42..4c50fef9 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -30,11 +30,10 @@ FinalNewline:com.qonversion.android.sdk.internal.storage.util.kt:1 FinalNewline:com.qonversion.android.sdk.utils.kt:1 LargeClass:QProductCenterManager.kt$QProductCenterManager : PurchasesListenerUserStateProvider - LongParameterList:IBillingClientWrapper.kt$IBillingClientWrapper$( activity: Activity, product: QProduct, offerId: String?, applyOffer: Boolean, updatePurchaseInfo: UpdatePurchaseInfo?, onFailed: (error: BillingError) -> Unit ) + LongParameterList:IBillingClientWrapper.kt$IBillingClientWrapper$( activity: Activity, product: QProduct, offerId: String?, applyOffer: Boolean? = true, updatePurchaseInfo: UpdatePurchaseInfo?, onFailed: (error: BillingError) -> Unit ) LongParameterList:ManagersModule.kt$ManagersModule$( appContext: Application, repository: QRepository, propertiesStorage: UserPropertiesStorage, incrementalDelayCalculator: IncrementalDelayCalculator, appStateProvider: AppStateProvider, logger: Logger ) LongParameterList:ManagersModule.kt$ManagersModule$( repository: QRepository, preferences: SharedPreferences, eventMapper: AutomationsEventMapper, appContext: Application, activityProvider: ActivityProvider, appStateProvider: AppStateProvider ) - LongParameterList:PurchaseModelInternalEnriched.kt$PurchaseModelInternalEnriched$( productId: String, val product: QProduct, offerId: String?, applyOffer: Boolean, oldProductId: String?, val oldProduct: QProduct?, updatePolicy: QPurchaseUpdatePolicy?, ) - LongParameterList:QonversionBillingService.kt$QonversionBillingService$( activity: Activity, product: QProduct, offerId: String?, applyOffer: Boolean, oldProduct: QProduct, updatePolicy: QPurchaseUpdatePolicy? ) + LongParameterList:QonversionBillingService.kt$QonversionBillingService$( activity: Activity, product: QProduct, offerId: String?, applyOffer: Boolean?, oldProduct: QProduct, updatePolicy: QPurchaseUpdatePolicy? ) LongParameterList:QonversionBillingService.kt$QonversionBillingService$( private val mainHandler: Handler, private val purchasesListener: PurchasesListener, private val logger: Logger, private val isAnalyticsMode: Boolean, private val billingClientHolder: BillingClientHolder, private val billingClientWrapper: BillingClientWrapper, private val legacyBillingClientWrapper: LegacyBillingClientWrapper ) LongParameterList:RepositoryModule.kt$RepositoryModule$( retrofit: Retrofit, environmentProvider: EnvironmentProvider, config: InternalConfig, logger: Logger, apiErrorMapper: ApiErrorMapper, sharedPreferences: SharedPreferences, delayCalculator: IncrementalDelayCalculator ) LongParameterList:RepositoryModule.kt$RepositoryModule$( retrofit: Retrofit, environmentProvider: EnvironmentProvider, config: InternalConfig, logger: Logger, apiErrorMapper: ApiErrorMapper, sharedPreferences: SharedPreferences, delayCalculator: IncrementalDelayCalculator, rateLimiter: RateLimiter ) @@ -90,6 +89,8 @@ MaxLineLength:AutomationsEventMapperTest.kt$AutomationsEventMapperTest.GetEventFromRemoteMessage$"{\"name\": \"subscription_started\", \"happened\": $timeInSec}" to AutomationsEventType.SubscriptionStarted MaxLineLength:AutomationsEventMapperTest.kt$AutomationsEventMapperTest.GetEventFromRemoteMessage$"{\"name\": \"subscription_upgraded\", \"happened\": $timeInSec}" to AutomationsEventType.SubscriptionUpgraded MaxLineLength:AutomationsEventMapperTest.kt$AutomationsEventMapperTest.GetEventFromRemoteMessage$"{\"name\": \"trial_billing_retry_entered\", \"happened\": $timeInSec}" to AutomationsEventType.TrialBillingRetry + MaxLineLength:GooglePurchaseConverter.kt$GooglePurchaseConverter$override + MaxLineLength:LaunchResultCacheWrapperTest.kt$LaunchResultCacheWrapperTest$cacheWrapper = LaunchResultCacheWrapper(mockMoshi, mockPrefsCache, mockCacheConfigProvider, mockQFallbacksService) MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest$"lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest$purchaseToken = "lgeigljfpmeoddkcebkcepjc.AO-J1Oy305qZj99jXTPEVBN8UZGoYAtjDLj4uTjRQvUFaG0vie-nr6VBlN0qnNDMU8eJR-sI7o3CwQyMOEHKl8eJsoQ86KSFzxKBR07PSpHLI_o7agXhNKY" MaxLineLength:OutagerIntegrationTest.kt$OutagerIntegrationTest$val token = "dt70kovLQdKymNnhIY6I94:APA91bGfg6m108VFio2ZdgLR6U0B2PtqAn0hIPVU7M4jKklkMxqDUrjoThpX_K60M7CfH8IVZqtku31ei2hmjdJZDfm-bdAl7uxLDWFU8yVcA6-3wBMn3nsYmUrhYWom-qgGC7yIUYzR" @@ -101,12 +102,18 @@ MaxLineLength:QAutomationsManager.kt$QAutomationsManager$"To override default animation, please, provide an activity context to AutomationsDelegate.contextForScreenIntent" MaxLineLength:QAutomationsManager.kt$QAutomationsManager$getScreenTransactionAnimations(screenPresentationConfig.presentationStyle) MaxLineLength:QEntitlementsUpdateListener.kt$QEntitlementsUpdateListener$* + MaxLineLength:QProduct.kt$QProduct$@Deprecated("Use new QPurchaseOptions object instead", replaceWith = ReplaceWith("QPurchaseOptions.Builder().setOffer(offer).build()")) + MaxLineLength:QProduct.kt$QProduct$@Deprecated("Use new QPurchaseOptions object instead", replaceWith = ReplaceWith("QPurchaseOptions.Builder().setOfferId(offerId).build()")) + MaxLineLength:QProduct.kt$QProduct$@Deprecated("Use new QPurchaseOptions object instead", replaceWith = ReplaceWith("QPurchaseOptions.Builder().setOldProduct(TODO(\"pass old product here\")).build()")) + MaxLineLength:QProductCenterManager.kt$QProductCenterManager$val oldProduct: QProduct? = purchaseModel.options?.oldProduct ?: getProductForPurchase(purchaseModel.oldProductId, products) MaxLineLength:QProductCenterManagerTest.kt$QProductCenterManagerTest${ Assert.assertEquals("Wrong installDate value", installDate.milliSecondsToSeconds(), installDateSlot.captured) } MaxLineLength:QProductCenterManagerTest.kt$QProductCenterManagerTest${ Assert.assertEquals("Wrong purchaseToken value", purchaseToken, entityPurchaseSlot.captured.purchaseToken) } MaxLineLength:QProductStoreDetails.kt$QProductStoreDetails$basePlanSubscriptionOfferDetails?.basePlan?.recurrenceMode == QProductPricingPhase.RecurrenceMode.NonRecurring MaxLineLength:QRemoteConfigManager.kt$QRemoteConfigManager.<no name provided>$val remoteConfigs = baseRemoteConfigList.remoteConfigs.filter { contextKeys.contains(it.source.contextKey) }.toMutableList() MaxLineLength:QUserPropertiesManagerTest.kt$QUserPropertiesManagerTest$fun MaxLineLength:Qonversion.kt$Qonversion$* + MaxLineLength:Qonversion.kt$Qonversion$@Deprecated("Use the new purchase() method", replaceWith = ReplaceWith("purchase(context, TODO(\"pass product here\"), callback)")) + MaxLineLength:Qonversion.kt$Qonversion$@Deprecated("Use the new updatePurchase() method", replaceWith = ReplaceWith("updatePurchase(context, TODO(\"pass product here\"), TODO(\"pass purchase options here\"), callback)")) MaxLineLength:QonversionBillingService.kt$QonversionBillingService$"updatePurchase() -> Purchase was found successfully for store product: ${purchaseHistoryRecord.productId}" MaxLineLength:QonversionBillingService.kt$QonversionBillingService$logger.debug("queryPurchaseHistoryAsync() -> purchase history for $productType is retrieved ${record.getDescription()}") MaxLineLength:QonversionConfig.kt$QonversionConfig.Builder$* @@ -126,6 +133,8 @@ MaxLineLength:util.kt$Util.Companion$"\"offerings\":[{\"id\":\"main\",\"tag\":1,\"products\":[{\"id\":\"in_app\",\"store_id\":\"qonversion_inapp_consumable\",\"type\":2},{\"id\":\"main\",\"store_id\":\"qonversion_subs_weekly\",\"type\":0,\"duration\":0}]" MaxLineLength:util.kt$Util.Companion$"\"permissions\":[{\"id\":\"standart\",\"associated_product\":\"in_app\",\"renew_state\":-1,\"started_timestamp\":1612880300,\"source\":\"playstore\",\"active\":1},{\"id\":\"Test Permission\",\"associated_product\":\"in_app\",\"renew_state\":-1,\"started_timestamp\":1612880300,\"source\":\"appstore\",\"active\":1}],\"user_products\":[{\"id\":\"in_app\",\"store_id\":\"qonversion_inapp_consumable\",\"type\":2}]," MaxLineLength:utils.kt$"ProductId: ${this.productId}; PurchaseTime: ${this.purchaseTime.convertLongToTime()}; PurchaseToken: ${this.purchaseToken}" + MaximumLineLength:com.qonversion.android.sdk.Qonversion.kt:131 + MaximumLineLength:com.qonversion.android.sdk.Qonversion.kt:146 MaximumLineLength:com.qonversion.android.sdk.automations.internal.AutomationsEventMapperTest.kt:105 MaximumLineLength:com.qonversion.android.sdk.automations.internal.AutomationsEventMapperTest.kt:106 MaximumLineLength:com.qonversion.android.sdk.automations.internal.AutomationsEventMapperTest.kt:107 @@ -140,13 +149,17 @@ MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:142 MaximumLineLength:com.qonversion.android.sdk.automations.mvp.ScreenPresenterTest.kt:159 MaximumLineLength:com.qonversion.android.sdk.dto.QonversionError.kt:46 + MaximumLineLength:com.qonversion.android.sdk.dto.products.QProduct.kt:109 + MaximumLineLength:com.qonversion.android.sdk.dto.products.QProduct.kt:120 + MaximumLineLength:com.qonversion.android.sdk.dto.products.QProduct.kt:139 MaximumLineLength:com.qonversion.android.sdk.dto.products.QProductStoreDetails.kt:130 MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:213 MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:370 MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:429 MaximumLineLength:com.qonversion.android.sdk.internal.OutagerIntegrationTest.kt:90 - MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:147 - MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:148 + MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManager.kt:332 + MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:152 + MaximumLineLength:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:153 MaximumLineLength:com.qonversion.android.sdk.internal.QRemoteConfigManager.kt:225 MaximumLineLength:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:175 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:286 @@ -159,11 +172,13 @@ MaximumLineLength:com.qonversion.android.sdk.internal.billing.QonversionBillingService.kt:253 MaximumLineLength:com.qonversion.android.sdk.internal.billing.QonversionBillingService.kt:371 MaximumLineLength:com.qonversion.android.sdk.internal.billing.utils.kt:22 + MaximumLineLength:com.qonversion.android.sdk.internal.converter.GooglePurchaseConverter.kt:17 MaximumLineLength:com.qonversion.android.sdk.internal.errors.kt:33 - MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:166 - MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:188 - MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:21 - MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:72 + MaximumLineLength:com.qonversion.android.sdk.internal.storage.LaunchResultCacheWrapperTest.kt:29 + MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:158 + MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:167 + MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:20 + MaximumLineLength:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:67 MaximumLineLength:com.qonversion.android.sdk.internal.storage.SharedPreferencesCacheTest.kt:219 MaximumLineLength:com.qonversion.android.sdk.internal.storage.SharedPreferencesCacheTest.kt:220 MaximumLineLength:com.qonversion.android.sdk.internal.storage.SharedPreferencesCacheTest.kt:221 @@ -193,6 +208,7 @@ NoConsecutiveBlankLines:com.qonversion.android.sdk.automations.internal.QAutomationsManagerTest.kt:396 NoConsecutiveBlankLines:com.qonversion.android.sdk.internal.QAttributionManagerTest.kt:141 NoConsecutiveBlankLines:com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt:18 + NoUnusedImports:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:4 NoUnusedImports:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:5 NoWildcardImports:com.qonversion.android.sdk.automations.internal.AutomationsEventMapperTest.kt:12 NoWildcardImports:com.qonversion.android.sdk.automations.internal.AutomationsEventMapperTest.kt:6 @@ -209,8 +225,8 @@ NoWildcardImports:com.qonversion.android.sdk.internal.QHandledPurchasesCacheTest.kt:4 NoWildcardImports:com.qonversion.android.sdk.internal.QIdentityManagerTest.kt:7 NoWildcardImports:com.qonversion.android.sdk.internal.QIdentityManagerTest.kt:9 - NoWildcardImports:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:21 - NoWildcardImports:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:29 + NoWildcardImports:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:23 + NoWildcardImports:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:31 NoWildcardImports:com.qonversion.android.sdk.internal.QProductCenterManagerTest.kt:9 NoWildcardImports:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:18 NoWildcardImports:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:19 @@ -218,13 +234,13 @@ NoWildcardImports:com.qonversion.android.sdk.internal.billing.QonversionBillingService.kt:5 NoWildcardImports:com.qonversion.android.sdk.internal.services.QUserInfoServiceTest.kt:11 NoWildcardImports:com.qonversion.android.sdk.internal.services.QUserInfoServiceTest.kt:5 - NoWildcardImports:com.qonversion.android.sdk.internal.storage.LaunchResultCacheWrapperTest.kt:7 + NoWildcardImports:com.qonversion.android.sdk.internal.storage.LaunchResultCacheWrapperTest.kt:8 NoWildcardImports:com.qonversion.android.sdk.internal.storage.PurchasesCacheTest.kt:8 NoWildcardImports:com.qonversion.android.sdk.internal.storage.SharedPreferencesCacheTest.kt:5 NoWildcardImports:com.qonversion.android.sdk.internal.storage.util.kt:24 NoWildcardImports:com.qonversion.android.sdk.utils.kt:4 ReturnCount:AutomationsEventMapper.kt$AutomationsEventMapper$fun getEventFromRemoteMessage(messageData: Map<String, String>): AutomationsEvent? - ReturnCount:BillingClientWrapper.kt$BillingClientWrapper$override fun makePurchase( activity: Activity, product: QProduct, offerId: String?, applyOffer: Boolean, updatePurchaseInfo: UpdatePurchaseInfo?, onFailed: (error: BillingError) -> Unit ) + ReturnCount:BillingClientWrapper.kt$BillingClientWrapper$override fun makePurchase( activity: Activity, product: QProduct, offerId: String?, applyOffer: Boolean?, updatePurchaseInfo: UpdatePurchaseInfo?, onFailed: (error: BillingError) -> Unit ) ReturnCount:ExceptionHandler.kt$ExceptionHandler$private fun isQonversionException(exception: Throwable): Boolean ReturnCount:QExceptionManager.kt$QExceptionManager$private fun getContentOfCrashReport(filename: String): CrashRequest.ExceptionInfo? ReturnCount:QProductCenterManager.kt$QProductCenterManager$@Synchronized private fun executeProductsBlocks(loadStoreProductsError: QonversionError? = null) @@ -269,7 +285,6 @@ TooManyFunctions:Cache.kt$Cache TooManyFunctions:DefaultRepository.kt$DefaultRepository : QRepository TooManyFunctions:LaunchResultCacheWrapper.kt$LaunchResultCacheWrapper - TooManyFunctions:LegacyBillingClientWrapper.kt$LegacyBillingClientWrapper : BillingClientWrapperBaseIBillingClientWrapper TooManyFunctions:QAutomationsManager.kt$QAutomationsManager TooManyFunctions:QProductCenterManager.kt$QProductCenterManager : PurchasesListenerUserStateProvider TooManyFunctions:QRemoteConfigManager.kt$QRemoteConfigManager diff --git a/fastlane/report.xml b/fastlane/report.xml index 2d8c36f5..1814b4fe 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,7 +5,7 @@ - + diff --git a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt index 1d7ca44b..7389a7dd 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt @@ -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 @@ -75,6 +77,50 @@ 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 @@ -82,7 +128,12 @@ interface Qonversion { * @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 @@ -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, diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseOptions.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseOptions.kt new file mode 100644 index 00000000..efcfed96 --- /dev/null +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseOptions.kt @@ -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 ( + internal val contextKeys: List? = 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? = 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): 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) + } + } +} diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProduct.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProduct.kt index 6efa3e79..2f332e85 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProduct.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProduct.kt @@ -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) @@ -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. @@ -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, diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt index 4bce9ad3..30b3f08f 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt @@ -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 @@ -94,6 +95,10 @@ internal class QProductCenterManager internal constructor( private var converter: PurchaseConverter = GooglePurchaseConverter() + private val processingPurchaseOptions: MutableMap by lazy { + purchasesCache.loadProcessingPurchasesOptions().toMutableMap() + } + @Volatile lateinit var billingService: BillingService @Synchronized set @@ -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) } @@ -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?) { + 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 @@ -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) @@ -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) + } outerCallback?.onSuccess(launchResult) } @@ -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, @@ -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 @@ -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, diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt index 15be6732..f0b6b97d 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionInternal.kt @@ -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 @@ -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, diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapper.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapper.kt index 14b58231..396e7888 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapper.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapper.kt @@ -54,7 +54,7 @@ internal class BillingClientWrapper( activity: Activity, product: QProduct, offerId: String?, - applyOffer: Boolean, + applyOffer: Boolean?, updatePurchaseInfo: UpdatePurchaseInfo?, onFailed: (error: BillingError) -> Unit ) { @@ -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 diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/IBillingClientWrapper.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/IBillingClientWrapper.kt index 4bae185f..e6be0ba6 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/IBillingClientWrapper.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/IBillingClientWrapper.kt @@ -21,7 +21,7 @@ internal interface IBillingClientWrapper { activity: Activity, product: QProduct, offerId: String?, - applyOffer: Boolean, + applyOffer: Boolean? = true, updatePurchaseInfo: UpdatePurchaseInfo?, onFailed: (error: BillingError) -> Unit ) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/LegacyBillingClientWrapper.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/LegacyBillingClientWrapper.kt index a999ce57..8b061dc8 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/LegacyBillingClientWrapper.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/LegacyBillingClientWrapper.kt @@ -46,7 +46,7 @@ internal class LegacyBillingClientWrapper( activity: Activity, product: QProduct, offerId: String?, // ignored - applyOffer: Boolean, // ignored + applyOffer: Boolean?, // ignored updatePurchaseInfo: UpdatePurchaseInfo?, onFailed: (error: BillingError) -> Unit ) { diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/QonversionBillingService.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/QonversionBillingService.kt index 5d7c5a61..7a9a5655 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/QonversionBillingService.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/QonversionBillingService.kt @@ -103,16 +103,16 @@ internal class QonversionBillingService internal constructor( updatePurchase( activity, purchaseModel.product, - purchaseModel.offerId, - purchaseModel.applyOffer, + purchaseModel.options?.offerId, + purchaseModel.options?.applyOffer, purchaseModel.oldProduct, purchaseModel.updatePolicy) } else { makePurchase( activity, purchaseModel.product, - purchaseModel.offerId, - purchaseModel.applyOffer + purchaseModel.options?.offerId, + purchaseModel.options?.applyOffer ) } } @@ -232,7 +232,7 @@ internal class QonversionBillingService internal constructor( activity: Activity, product: QProduct, offerId: String?, - applyOffer: Boolean, + applyOffer: Boolean?, oldProduct: QProduct, updatePolicy: QPurchaseUpdatePolicy? ) { @@ -274,7 +274,7 @@ internal class QonversionBillingService internal constructor( activity: Activity, product: QProduct, offerId: String?, - applyOffer: Boolean, + applyOffer: Boolean?, updatePurchaseInfo: UpdatePurchaseInfo? = null ) { executeOnMainThread { billingSetupError -> diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/GooglePurchaseConverter.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/GooglePurchaseConverter.kt index fb48c63a..00454798 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/GooglePurchaseConverter.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/GooglePurchaseConverter.kt @@ -1,5 +1,6 @@ package com.qonversion.android.sdk.internal.converter +import com.qonversion.android.sdk.dto.QPurchaseOptions import com.qonversion.android.sdk.internal.billing.productId import com.qonversion.android.sdk.internal.milliSecondsToSeconds import com.qonversion.android.sdk.internal.purchase.Purchase @@ -7,18 +8,20 @@ import com.qonversion.android.sdk.internal.purchase.Purchase internal class GooglePurchaseConverter : PurchaseConverter { override fun convertPurchases( - purchases: List + purchases: List, + options: Map? ): List { - return purchases.map { convertPurchase(it) } + return purchases.map { convertPurchase(it, options?.get(it.productId)) } } - override fun convertPurchase(purchase: com.android.billingclient.api.Purchase): Purchase { + override fun convertPurchase(purchase: com.android.billingclient.api.Purchase, options: QPurchaseOptions?): Purchase { return Purchase( storeProductId = purchase.productId, orderId = purchase.orderId ?: "", originalOrderId = formatOriginalTransactionId(purchase.orderId ?: ""), purchaseTime = purchase.purchaseTime.milliSecondsToSeconds(), purchaseToken = purchase.purchaseToken, + contextKeys = options?.contextKeys ) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/PurchaseConverter.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/PurchaseConverter.kt index edf997a7..1dbf0154 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/PurchaseConverter.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/converter/PurchaseConverter.kt @@ -1,9 +1,13 @@ package com.qonversion.android.sdk.internal.converter +import com.qonversion.android.sdk.dto.QPurchaseOptions import com.qonversion.android.sdk.internal.purchase.Purchase internal interface PurchaseConverter { - fun convertPurchase(purchase: com.android.billingclient.api.Purchase): Purchase + fun convertPurchase(purchase: com.android.billingclient.api.Purchase, options: QPurchaseOptions?): Purchase - fun convertPurchases(purchases: List): List + fun convertPurchases( + purchases: List, + options: Map? + ): List } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/AppModule.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/AppModule.kt index 8a6c8b15..e9b0ddd6 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/AppModule.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/AppModule.kt @@ -62,7 +62,7 @@ internal class AppModule( @ApplicationScope @Provides - fun providePurchasesCache(sharedPreferences: SharedPreferences): PurchasesCache { + fun providePurchasesCache(sharedPreferences: SharedPreferencesCache): PurchasesCache { return PurchasesCache(sharedPreferences) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/Inapp.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/Inapp.kt index c483fa1b..66dc7aab 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/Inapp.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/Inapp.kt @@ -15,5 +15,6 @@ internal data class PurchaseDetails( @Json(name = "transaction_id") val transactionId: String, @Json(name = "original_transaction_id") val originalTransactionId: String, @Json(name = "product") val storeProductId: String, - @Json(name = "product_id") val qProductId: String + @Json(name = "product_id") val qProductId: String, + @Json(name = "context_keys") val contextKeys: List?, ) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternal.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternal.kt index f098aa85..d4fa4c02 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternal.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternal.kt @@ -1,34 +1,39 @@ package com.qonversion.android.sdk.internal.dto.purchase 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.QPurchaseUpdatePolicy import com.qonversion.android.sdk.dto.products.QProduct internal open class PurchaseModelInternal( val productId: String, - val offerId: String?, - val applyOffer: Boolean, val oldProductId: String?, val updatePolicy: QPurchaseUpdatePolicy?, + val options: QPurchaseOptions? ) { constructor(purchaseModel: QPurchaseModel) : this( purchaseModel.productId, - purchaseModel.offerId, - purchaseModel.applyOffer, null, null, + QPurchaseOptions(offerId = purchaseModel.offerId, applyOffer = purchaseModel.applyOffer) + ) + + constructor(product: QProduct, options: QPurchaseOptions? = null) : this( + product.qonversionID, + options?.oldProduct?.qonversionID, + options?.updatePolicy, + options ) constructor(purchaseModel: QPurchaseUpdateModel) : this( purchaseModel.productId, - purchaseModel.offerId, - purchaseModel.applyOffer, purchaseModel.oldProductId, purchaseModel.updatePolicy, + QPurchaseOptions(offerId = purchaseModel.offerId, applyOffer = purchaseModel.applyOffer) ) fun enrich(product: QProduct, oldProduct: QProduct?) = PurchaseModelInternalEnriched( - productId, product, offerId, applyOffer, oldProductId, oldProduct, updatePolicy + productId, product, oldProduct, updatePolicy, options ) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternalEnriched.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternalEnriched.kt index 6218e874..f0ab2a67 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternalEnriched.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/purchase/PurchaseModelInternalEnriched.kt @@ -1,29 +1,25 @@ package com.qonversion.android.sdk.internal.dto.purchase +import com.qonversion.android.sdk.dto.QPurchaseOptions import com.qonversion.android.sdk.dto.QPurchaseUpdatePolicy import com.qonversion.android.sdk.dto.products.QProduct internal class PurchaseModelInternalEnriched( productId: String, val product: QProduct, - offerId: String?, - applyOffer: Boolean, - oldProductId: String?, val oldProduct: QProduct?, updatePolicy: QPurchaseUpdatePolicy?, -) : PurchaseModelInternal(productId, offerId, applyOffer, oldProductId, updatePolicy) { + options: QPurchaseOptions? +) : PurchaseModelInternal(productId, oldProduct?.qonversionID, updatePolicy, options) { constructor( purchaseModel: PurchaseModelInternal, - product: QProduct, - oldProduct: QProduct? + product: QProduct ) : this( purchaseModel.productId, product, - purchaseModel.offerId, - purchaseModel.applyOffer, - purchaseModel.oldProductId, - oldProduct, - purchaseModel.updatePolicy + purchaseModel.options?.oldProduct, + purchaseModel.updatePolicy, + purchaseModel.options ) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/purchase/Purchase.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/purchase/Purchase.kt index d5a7c468..46c252c4 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/purchase/Purchase.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/purchase/Purchase.kt @@ -9,4 +9,5 @@ internal data class Purchase( val originalOrderId: String, val purchaseTime: Long, val purchaseToken: String, + val contextKeys: List?, ) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt index 56ec9295..eda39d74 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/DefaultRepository.kt @@ -598,7 +598,8 @@ internal class DefaultRepository internal constructor( purchase.orderId, purchase.originalOrderId, purchase.storeProductId ?: "", - qProductId ?: "" + qProductId ?: "", + purchase.contextKeys ) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/PurchasesCache.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/PurchasesCache.kt index e28342b2..450eb7d7 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/PurchasesCache.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/PurchasesCache.kt @@ -1,6 +1,6 @@ package com.qonversion.android.sdk.internal.storage -import android.content.SharedPreferences +import com.qonversion.android.sdk.dto.QPurchaseOptions import com.qonversion.android.sdk.internal.purchase.Purchase import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -9,15 +9,22 @@ import java.io.IOException import java.lang.reflect.Type internal class PurchasesCache( - private val preferences: SharedPreferences + private val preferences: SharedPreferencesCache ) { private val moshi = Moshi.Builder().build() private val collectionPurchaseType: Type = Types.newParameterizedType( Set::class.java, Purchase::class.java ) - private val jsonAdapter: JsonAdapter> = + private val collectionPurchaseOptionsType: Type = Types.newParameterizedType( + Map::class.java, + String::class.java, + QPurchaseOptions::class.java + ) + private val purchasesJsonAdapter: JsonAdapter> = moshi.adapter(collectionPurchaseType) + private val purchasesOptionsJsonAdapter: JsonAdapter> = + moshi.adapter(collectionPurchaseOptionsType) fun savePurchase(purchase: Purchase) { val purchases = loadPurchases().toMutableSet() @@ -37,7 +44,7 @@ internal class PurchasesCache( return setOf() } return try { - val purchases: Set? = jsonAdapter.fromJson(json) + val purchases: Set? = purchasesJsonAdapter.fromJson(json) purchases ?: setOf() } catch (e: IOException) { setOf() @@ -51,12 +58,23 @@ internal class PurchasesCache( savePurchasesAsJson(purchases) } + fun saveProcessingPurchasesOptions(options: Map) { + preferences.putObject(PURCHASE_OPTIONS_KEY, options, purchasesOptionsJsonAdapter) + } + + fun loadProcessingPurchasesOptions(): Map { + val purchaseOptions = preferences.getObject(PURCHASE_OPTIONS_KEY, purchasesOptionsJsonAdapter) ?: emptyMap() + + return purchaseOptions + } + private fun savePurchasesAsJson(purchases: MutableSet) { - val jsonStr: String = jsonAdapter.toJson(purchases) - preferences.edit().putString(PURCHASE_KEY, jsonStr).apply() + val jsonStr: String = purchasesJsonAdapter.toJson(purchases) + preferences.putString(PURCHASE_KEY, jsonStr) } companion object { + private const val PURCHASE_OPTIONS_KEY = "purchase_options" private const val PURCHASE_KEY = "purchase" private const val MAX_PURCHASES_NUMBER = 5 private const val MAX_OLD_PURCHASES_NUMBER = 1 diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt index 07d1d0b0..619ba9fc 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/QProductCenterManagerTest.kt @@ -7,6 +7,7 @@ import android.os.Build import com.android.billingclient.api.BillingClient import com.android.billingclient.api.Purchase import com.android.billingclient.api.* +import com.qonversion.android.sdk.dto.QPurchaseOptions import com.qonversion.android.sdk.listeners.QonversionLaunchCallback import com.qonversion.android.sdk.internal.billing.BillingError import com.qonversion.android.sdk.internal.billing.QonversionBillingService @@ -18,6 +19,7 @@ import com.qonversion.android.sdk.internal.repository.QRepository import com.qonversion.android.sdk.internal.services.QUserInfoService import com.qonversion.android.sdk.internal.storage.LaunchResultCacheWrapper import com.qonversion.android.sdk.internal.storage.PurchasesCache +import com.qonversion.android.sdk.mockPrivateField import io.mockk.* import org.junit.Assert import org.junit.Before @@ -111,6 +113,9 @@ internal class QProductCenterManagerTest { @Test fun `handle pending purchases when launching is finished and query purchases completed`() { + val spykProductCenterManager = spyk(productCenterManager, recordPrivateCalls = true) + spykProductCenterManager.mockPrivateField("processingPurchaseOptions", emptyMap()) + val purchase = mockPurchase(Purchase.PurchaseState.PURCHASED, false) val purchases = listOf(purchase) every { @@ -135,7 +140,7 @@ internal class QProductCenterManagerTest { every { mockBillingService.consumePurchases(any()) } just Runs - productCenterManager.onAppForeground() + spykProductCenterManager.onAppForeground() verify(exactly = 1) { mockBillingService.queryPurchases(any(), any()) diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/PurchasesCacheTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/PurchasesCacheTest.kt index ef1502d0..3642527c 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/PurchasesCacheTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/PurchasesCacheTest.kt @@ -11,8 +11,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test internal class PurchasesCacheTest { - private val mockPrefs: SharedPreferences = mockk(relaxed = true) - private val mockEditor: SharedPreferences.Editor = mockk(relaxed = true) + private val mockPrefs: SharedPreferencesCache = mockk(relaxed = true) private lateinit var purchasesCache: PurchasesCache @@ -24,8 +23,6 @@ internal class PurchasesCacheTest { fun setUp() { clearAllMocks() - mockSharedPreferences() - purchasesCache = PurchasesCache(mockPrefs) } @@ -38,8 +35,7 @@ internal class PurchasesCacheTest { purchasesCache.savePurchase(purchase) verifyOrder { - mockEditor.putString(purchaseKey, onePurchaseStr) - mockEditor.apply() + mockPrefs.putString(purchaseKey, any()) } } @@ -53,8 +49,7 @@ internal class PurchasesCacheTest { purchasesCache.savePurchase(purchase) verifyOrder { - mockEditor.putString(purchaseKey, onePurchaseStr) - mockEditor.apply() + mockPrefs.putString(purchaseKey, onePurchaseStr) } val purchases = purchasesCache.loadPurchases() assertThat(purchases.size).isEqualTo(1) @@ -72,8 +67,7 @@ internal class PurchasesCacheTest { "[${generatePurchaseJson("2")},${generatePurchaseJson("3")},${generatePurchaseJson("4")},${generatePurchaseJson("5")}]" verifyOrder { - mockEditor.putString(purchaseKey, fourNewestPurchasesStr) - mockEditor.apply() + mockPrefs.putString(purchaseKey, fourNewestPurchasesStr) } } } @@ -138,8 +132,7 @@ internal class PurchasesCacheTest { purchasesCache.clearPurchase(purchase) verifyOrder { - mockEditor.putString(purchaseKey, emptyList) - mockEditor.apply() + mockPrefs.putString(purchaseKey, emptyList) } } @@ -151,8 +144,7 @@ internal class PurchasesCacheTest { purchasesCache.clearPurchase(purchase) verifyOrder { - mockEditor.putString(purchaseKey, emptyList) - mockEditor.apply() + mockPrefs.putString(purchaseKey, emptyList) } } } @@ -164,23 +156,10 @@ internal class PurchasesCacheTest { originalOrderId = "GPA.3375-4436-3573-53474$originalOrderId", purchaseTime = 1611323804, purchaseToken = "gfegjilekkmecbonpfjiaakm.AO-J1OxQCaAn0NPlHTh5CoOiXK0p19X7qEymW9SHtssrggp7S9YafjA1oPBPlWO4Ur3W5rtyNJBzIrVoLOb5In0Jxofv4xV_7t1HaUYYd_f8xOBk7nRIY7g", + contextKeys = listOf("test_1", "test_2") ) } - private fun mockSharedPreferences() { - every { - mockEditor.putString(purchaseKey, any()) - } returns mockEditor - - every { - mockPrefs.edit() - } returns mockEditor - - every { - mockEditor.apply() - } just runs - } - private fun generatePurchaseJson(originalOrderId: String = ""): String { return "{\"orderId\":\"GPA.3375-4436-3573-53474\"," + "\"originalOrderId\":\"GPA.3375-4436-3573-53474$originalOrderId\"," +