diff --git a/app/build.gradle b/app/build.gradle index df37db992..e9350c51d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,11 +15,11 @@ buildscript { } android { - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.qonversion.sample" - minSdkVersion 19 - targetSdkVersion 33 + minSdkVersion 21 + targetSdkVersion 34 versionCode 1 versionName "1.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/assets/qonversion_android_fallbacks.json b/app/src/main/assets/qonversion_android_fallbacks.json new file mode 100644 index 000000000..40f80af82 --- /dev/null +++ b/app/src/main/assets/qonversion_android_fallbacks.json @@ -0,0 +1,230 @@ +{ + "products": [ + { + "id": "android_installment", + "duration": 1, + "store_id": "gb7_test_subscription", + "type": 1, + "base_plan_id": "monthly-installment" + }, + { + "id": "android_prepaid_2", + "duration": null, + "store_id": "daniel_prepaid", + "type": 1, + "base_plan_id": "monthly" + }, + { + "id": "dan_test", + "duration": null, + "store_id": "dan_test_annual", + "type": null + }, + { + "id": "gb6_annual", + "duration": 4, + "store_id": "gb6_test", + "type": 0, + "base_plan_id": "annual" + }, + { + "id": "gb6_monthly", + "duration": 1, + "store_id": "gb6_test", + "type": 0, + "base_plan_id": "monthly" + }, + { + "id": "gb6_weekly", + "duration": 0, + "store_id": "gb6_test", + "type": 1, + "base_plan_id": "weekly" + }, + { + "id": "android_prepaid", + "duration": 2, + "store_id": "gp5_test_subscription_4", + "type": 1, + "base_plan_id": "prepaid-3m" + }, + { + "id": "weekly", + "duration": 0, + "store_id": "gp5_test_subscription_4", + "type": 1, + "base_plan_id": "monthly-2" + }, + { + "id": "consumable", + "duration": null, + "store_id": "qonversion_inapp_sample", + "type": 2 + }, + { + "id": "subs_plus_trial", + "duration": 1, + "store_id": "gp5_test_subscription_4", + "type": 0 + }, + { + "id": "annual", + "duration": 4, + "store_id": "article_test_trial", + "type": 0 + }, + { + "id": "in_app", + "duration": null, + "store_id": "qonversion_sample_purchase", + "type": 2 + } + ], + "offerings": [ + { + "id": "main", + "tag": 1, + "products": [ + { + "id": "weekly", + "duration": 0, + "store_id": "gp5_test_subscription_4", + "type": 1, + "base_plan_id": "monthly-2" + }, + { + "id": "annual", + "duration": 4, + "store_id": "article_test_trial", + "type": 0 + }, + { + "id": "consumable", + "duration": null, + "store_id": "qonversion_inapp_sample", + "type": 2 + } + ] + }, + { + "id": "discounted_offer", + "tag": 0, + "products": [] + } + ], + "products_permissions": { + "android_installment": [ + "standart" + ], + "android_prepaid_2": [ + "premium" + ], + "dan_test": [ + "test_permission" + ], + "gb6_annual": [ + "premium" + ], + "gb6_monthly": [ + "plus" + ], + "gb6_weekly": [ + "standart" + ], + "android_prepaid": [ + "premium" + ], + "weekly": [ + "plus" + ], + "consumable": [], + "subs_plus_trial": [ + "standart" + ], + "annual": [ + "standart", + "sample" + ], + "in_app": [ + "Premium Movies" + ] + }, + "remote_config_list": [ + { + "experiment": null, + "payload": { + "CTA": "Start Trial", + "CTA_color": "#307BF6", + "main_image": "[IMAGE_URL]", + "product_id": "prod_7d_trial_5.99", + "show_close_button": true + }, + "source": { + "assignment_type": "auto", + "context_key": "main_paywall", + "name": "default paywall", + "type": "remote_configuration", + "uid": "0dcb1bd9-9bc3-4668-84aa-4540d1042c5d" + } + }, + { + "experiment": null, + "payload": { + "CTA": "Start you trial", + "CTA_color": "red", + "main_image": "111", + "product_id": "123123123123123", + "show_close_button": true + }, + "source": { + "assignment_type": "auto", + "context_key": "trulala", + "name": "Default settings", + "type": "remote_configuration", + "uid": "12feb1dd-8096-47bc-a5a1-443fd2828ecc" + } + }, + { + "experiment": null, + "payload": { + "test_key": "test_value" + }, + "source": { + "assignment_type": "auto", + "context_key": "test_context_key", + "name": "Test with context key1", + "type": "remote_configuration", + "uid": "c5077ec4-acf4-41ea-8b43-05114be5d7ce" + } + }, + { + "experiment": null, + "payload": { + "test_key_2": "test_value_2" + }, + "source": { + "assignment_type": "auto", + "context_key": "test_context_key_2", + "name": "Test with context key2 - copy", + "type": "remote_configuration", + "uid": "1c000f2a-2f4b-4736-b5dd-75b13bf73deb" + } + }, + { + "experiment": null, + "payload": { + "bool": true, + "json": { + "key": "value" + } + }, + "source": { + "assignment_type": "auto", + "context_key": "swift_key ", + "name": "Swift", + "type": "remote_configuration", + "uid": "9f85d738-56d8-4f6c-b54a-c08658be2cb4" + } + } + ] +} \ No newline at end of file diff --git a/app/src/main/java/com/qonversion/android/app/ManualTrackingActivity.java b/app/src/main/java/com/qonversion/android/app/ManualTrackingActivity.java index 0b93113ec..48762cf98 100644 --- a/app/src/main/java/com/qonversion/android/app/ManualTrackingActivity.java +++ b/app/src/main/java/com/qonversion/android/app/ManualTrackingActivity.java @@ -11,6 +11,7 @@ import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.PendingPurchasesParams; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.QueryProductDetailsParams; import com.qonversion.android.sdk.Qonversion; @@ -34,7 +35,12 @@ public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableB client = BillingClient .newBuilder(this) - .enablePendingPurchases() + .enablePendingPurchases( + PendingPurchasesParams.newBuilder() + .enableOneTimeProducts() + .enablePrepaidPlans() + .build() + ) .setListener((billingResult, purchases) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { if (purchases != null && !purchases.isEmpty()) { diff --git a/app/src/main/java/com/qonversion/android/app/ManualTrackingActivityKt.kt b/app/src/main/java/com/qonversion/android/app/ManualTrackingActivityKt.kt index 00361b399..ee71cbbce 100644 --- a/app/src/main/java/com/qonversion/android/app/ManualTrackingActivityKt.kt +++ b/app/src/main/java/com/qonversion/android/app/ManualTrackingActivityKt.kt @@ -7,6 +7,7 @@ import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.QueryProductDetailsParams import com.qonversion.android.sdk.Qonversion @@ -20,7 +21,12 @@ class ManualTrackingActivityKt : AppCompatActivity() { super.onCreate(savedInstanceState, persistentState) client = BillingClient .newBuilder(this) - .enablePendingPurchases() + .enablePendingPurchases( + PendingPurchasesParams.newBuilder() + .enableOneTimeProducts() + .enablePrepaidPlans() + .build() + ) .setListener { billingResult, purchases -> if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { if (!purchases.isNullOrEmpty()) { diff --git a/app/src/main/res/raw/qonversion_android_fallbacks.json b/app/src/main/res/raw/qonversion_android_fallbacks.json new file mode 100644 index 000000000..26f88eabb --- /dev/null +++ b/app/src/main/res/raw/qonversion_android_fallbacks.json @@ -0,0 +1,118 @@ +{ + "products": [ + { + "duration": 4, + "id": "annual", + "store_id": "article_test_trial", + "type": 0 + }, + { + "duration": 1, + "id": "subs_plus_trial", + "store_id": "gp5_test_subscription_4", + "type": 0 + }, + { + "duration": 0, + "id": "weekly", + "store_id": "gp5_test_subscription_4", + "type": 1 + } + ], + "offerings": [ + { + "id": "main", + "products": [ + { + "duration": 0, + "id": "weekly", + "store_id": "gp5_test_subscription_4", + "type": 1 + }, + { + "duration": 4, + "id": "annual", + "store_id": "io.qonversion.subs.annual", + "type": 0 + }, + { + "duration": null, + "id": "consumable", + "store_id": "io.qonversion.consumable", + "type": 2 + } + ], + "tag": 1 + }, + { + "id": "discounted_offer", + "products": [], + "tag": 0 + } + ], + "products_permissions": { + "annual": [ + "standart", + "sample" + ], + "subs_plus_trial": [ + "standart" + ], + "weekly": [ + "plus" + ] + }, + "remote_config_list": [ + { + "experiment": null, + "payload": { + "CTA": "Start you trial", + "CTA_color": "red", + "main_image": "111", + "product_id": "123123123123123", + "show_close_button": 1 + }, + "source": { + "assignment_type": "auto", + "context_key": "", + "name": "Default settings", + "type": "remote_configuration", + "uid": "12feb1dd-8096-47bc-a5a1-443fd2828ecc" + } + }, + { + "experiment": null, + "payload": { + "CTA": "New test", + "CTA_color": "blue", + "main_image": "1111", + "product_id": "2332", + "show_close_button": 1 + }, + "source": { + "assignment_type": "auto", + "context_key": "new_test", + "name": "Default settings", + "type": "remote_configuration", + "uid": "12feb1dd-8096-47bc-a5a1-443fd2828ecc" + } + }, + { + "experiment": null, + "payload": { + "CTA": "One more test", + "CTA_color": "yellow", + "main_image": "1111", + "product_id": "2332", + "show_close_button": 1 + }, + "source": { + "assignment_type": "auto", + "context_key": "one_more_test", + "name": "Default settings", + "type": "remote_configuration", + "uid": "12feb1dd-8096-47bc-a5a1-443fd2828ecc" + } + } + ] +} diff --git a/build.gradle b/build.gradle index 18d5d4714..d4b6e5f68 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask buildscript { ext { release = [ - versionName: "7.5.0", + versionName: "8.0.0", versionCode: 1 ] } diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 65ec9b6a4..53d16f421 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -104,6 +104,7 @@ 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:QonversionBillingService.kt$QonversionBillingService$"updatePurchase() -> Purchase was found successfully for store product: ${purchaseHistoryRecord.productId}" @@ -138,7 +139,7 @@ MaximumLineLength:com.qonversion.android.sdk.automations.internal.QAutomationsManager.kt:135 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:45 + MaximumLineLength:com.qonversion.android.sdk.dto.QonversionError.kt:46 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 @@ -146,14 +147,15 @@ 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.QRemoteConfigManager.kt:225 MaximumLineLength:com.qonversion.android.sdk.internal.QUserPropertiesManagerTest.kt:175 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:286 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:355 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:684 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:89 MaximumLineLength:com.qonversion.android.sdk.internal.QonversionRepositoryIntegrationTest.kt:905 - MaximumLineLength:com.qonversion.android.sdk.internal.api.ApiErrorMapper.kt:117 MaximumLineLength:com.qonversion.android.sdk.internal.api.ApiErrorMapper.kt:118 + MaximumLineLength:com.qonversion.android.sdk.internal.api.ApiErrorMapper.kt:119 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 @@ -227,6 +229,7 @@ ReturnCount:QExceptionManager.kt$QExceptionManager$private fun getContentOfCrashReport(filename: String): CrashRequest.ExceptionInfo? ReturnCount:QProductCenterManager.kt$QProductCenterManager$@Synchronized private fun executeProductsBlocks(loadStoreProductsError: QonversionError? = null) ReturnCount:QProductCenterManager.kt$QProductCenterManager$fun identify(identityId: String, callback: QonversionUserCallback? = null) + ReturnCount:QProductCenterManager.kt$QProductCenterManager$private fun calculatePurchasePermissionsLocally( purchase: Purchase, purchaseCallback: QonversionEntitlementsCallback?, purchaseError: QonversionError ) ReturnCount:ScreenPresenter.kt$ScreenPresenter$override fun shouldOverrideUrlLoading(url: String?): Boolean SpacingAroundColon:com.qonversion.android.sdk.internal.requests.ProviderDataRequestTest.kt:45 SpacingAroundCurly:com.qonversion.android.sdk.automations.internal.QAutomationsManagerTest.kt:263 @@ -258,6 +261,7 @@ TooGenericExceptionCaught:FacebookAttribution.kt$FacebookAttribution$e: Exception TooGenericExceptionCaught:QAutomationsManager.kt$QAutomationsManager$e: Exception TooGenericExceptionCaught:QExceptionManager.kt$QExceptionManager$cause: Exception + TooGenericExceptionCaught:QFallbacksService.kt$QFallbacksService$e: Exception TooGenericExceptionCaught:ScreenFragment.kt$ScreenFragment$e: Exception TooManyFunctions:Api.kt$Api TooManyFunctions:AppComponent.kt$AppComponent @@ -292,6 +296,7 @@ UnusedPrivateMember:QonversionMappingAdapters.kt$QPermissionsAdapter$@ToJson private fun toJson(permissions: Map<String, QPermission>): List<QPermission> UnusedPrivateMember:QonversionMappingAdapters.kt$QProductRenewStateAdapter$@ToJson private fun toJson(enum: QProductRenewState): Int UnusedPrivateMember:QonversionMappingAdapters.kt$QProductsAdapter$@ToJson private fun toJson(products: Map<String, QProduct>): List<QProduct> + UnusedPrivateMember:QonversionMappingAdapters.kt$QRemoteConfigListAdapter$@ToJson private fun toJson(remoteConfigList: QRemoteConfigList?): List<QRemoteConfig> UnusedPrivateMember:QonversionMappingAdapters.kt$QRemoteConfigurationSourceAssignmentTypeAdapter$@ToJson private fun toJson(enum: QRemoteConfigurationAssignmentType): String UnusedPrivateMember:QonversionMappingAdapters.kt$QRemoteConfigurationSourceTypeAdapter$@ToJson private fun toJson(enum: QRemoteConfigurationSourceType): String UnusedPrivateMember:QonversionMappingAdapters.kt$QTransactionEnvironmentAdapter$@ToJson private fun toJson(enum: QTransactionEnvironment): String diff --git a/fastlane/report.xml b/fastlane/report.xml index da5ada953..c49e9ea1b 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,7 +5,7 @@ - + diff --git a/sdk/build.gradle b/sdk/build.gradle index 8b92e9f93..4dfeb02b6 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdk 33 + compileSdk 34 defaultConfig { - minSdkVersion 19 - targetSdkVersion 33 + minSdkVersion 21 + targetSdkVersion 34 consumerProguardFiles 'consumer-rules.pro' group = 'io.qonversion.android.sdk' @@ -68,7 +68,7 @@ ext { moshiVersion = '1.14.0' retrofit_version = '2.9.0' okhttp_version = '3.12.13' - billing = '6.1.0' + billing = '7.0.0' lifecycle_version = '2.1.0' assertj_version = '3.16.1' junit_version = '5.6.2' diff --git a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt index 65a4d7992..b1e152041 100644 --- a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt +++ b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/OutagerIntegrationTest.kt @@ -120,7 +120,7 @@ internal class OutagerIntegrationTest { signal.countDown() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fail("Shouldn't fail") } } @@ -178,7 +178,7 @@ internal class OutagerIntegrationTest { signal.countDown() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fail("Shouldn't fail") } } @@ -255,7 +255,7 @@ internal class OutagerIntegrationTest { signal.countDown() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fail("Shouldn't fail") } } @@ -549,7 +549,7 @@ internal class OutagerIntegrationTest { onComplete(null) } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { onComplete(error) } } diff --git a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt index cb3b4fb9d..cae0e34e1 100644 --- a/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt +++ b/sdk/src/androidTest/java/com/qonversion/android/sdk/internal/QonversionRepositoryIntegrationTest.kt @@ -115,7 +115,7 @@ internal class QonversionRepositoryIntegrationTest { signal.countDown() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fail("Shouldn't fail") } } @@ -144,7 +144,7 @@ internal class QonversionRepositoryIntegrationTest { fail("Shouldn't succeed") } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { // then assertIncorrectProjectKeyError(error) signal.countDown() @@ -182,7 +182,7 @@ internal class QonversionRepositoryIntegrationTest { signal.countDown() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fail("Shouldn't fail") } } @@ -232,7 +232,7 @@ internal class QonversionRepositoryIntegrationTest { signal.countDown() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fail("Shouldn't fail") } } @@ -256,7 +256,7 @@ internal class QonversionRepositoryIntegrationTest { fail("Shouldn't succeed") } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { assertIncorrectProjectKeyError(error) signal.countDown() } @@ -325,7 +325,7 @@ internal class QonversionRepositoryIntegrationTest { signal.countDown() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fail("Shouldn't fail") } } @@ -363,7 +363,7 @@ internal class QonversionRepositoryIntegrationTest { fail("Shouldn't succeed") } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { assertIncorrectProjectKeyError(error) signal.countDown() } @@ -555,9 +555,9 @@ internal class QonversionRepositoryIntegrationTest { val signal = CountDownLatch(1) val productIds = listOf(monthlyProduct.qonversionID, annualProduct.qonversionID) val expectedResult = mapOf( - monthlyProduct.qonversionID to QEligibility(QIntroEligibilityStatus.NonIntroProduct), + monthlyProduct.qonversionID to QEligibility(QIntroEligibilityStatus.NonIntroOrTrialProduct), annualProduct.qonversionID to QEligibility(QIntroEligibilityStatus.Unknown), - inappProduct.qonversionID to QEligibility(QIntroEligibilityStatus.NonIntroProduct) + inappProduct.qonversionID to QEligibility(QIntroEligibilityStatus.NonIntroOrTrialProduct) ) val callback = object : QonversionEligibilityCallback { @@ -874,7 +874,7 @@ internal class QonversionRepositoryIntegrationTest { onComplete(null) } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { onComplete(error) } } 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 e4f993b78..1d7ca44b2 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/Qonversion.kt @@ -277,6 +277,12 @@ interface Qonversion { */ fun userProperties(callback: QonversionUserPropertiesCallback) + /** + * Call this function to check if the fallback file is accessible. + * @return flag that indicates whether Qonversion was able to read data from the fallback file or not. + */ + fun isFallbackFileAccessible(): Boolean + /** * Provide a listener to be notified about asynchronous user entitlements updates. * diff --git a/sdk/src/main/java/com/qonversion/android/sdk/QonversionConfig.kt b/sdk/src/main/java/com/qonversion/android/sdk/QonversionConfig.kt index b4e7018a4..46b4d7b81 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/QonversionConfig.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/QonversionConfig.kt @@ -5,6 +5,7 @@ import android.util.Log import com.qonversion.android.sdk.dto.QEnvironment import com.qonversion.android.sdk.dto.QLaunchMode import android.content.Context +import androidx.annotation.RawRes import com.qonversion.android.sdk.dto.entitlements.QEntitlementsCacheLifetime import com.qonversion.android.sdk.internal.dto.config.CacheConfig import com.qonversion.android.sdk.internal.dto.config.PrimaryConfig @@ -49,6 +50,8 @@ class QonversionConfig internal constructor( internal var entitlementsUpdateListener: QEntitlementsUpdateListener? = null internal var proxyUrl: String? = null internal var isKidsMode: Boolean = false + @RawRes + internal var fallbackFileIdentifier: Int? = null /** * Set current application [QEnvironment]. Used to distinguish sandbox and production users. @@ -72,6 +75,23 @@ class QonversionConfig internal constructor( this.entitlementsCacheLifetime = lifetime } + /** + * Sets fallback file identifier. + * Fallback file will be used in rare cases of network connection or Qonversion API issues for new users without a cache available. + * This allows purchases and entitlements to be processed for new users even if the Qonversion API faces issues. + * This also makes it possible to receive remote configs for cases when the network connection is unavailable. + * There is no need to use this function if you put qonversion_fallbacks.json into the `assets` folder. + * Use this function only if you put qonversion_fallbacks.json into the `res/raw` folder. + * In that case, `id` should look like `R.raw.qonversion_fallbacks`. + * + * @param id the identifier for the fallback file. + * + * @see [The documentation](https://documentation.qonversion.io/docs/system-reliability#fallback-files) + */ + fun setFallbackFileIdentifier(@RawRes id: Int): Builder = apply { + this.fallbackFileIdentifier = id + } + /** * Provide a listener to be notified about asynchronous user entitlements updates. * @@ -131,7 +151,7 @@ class QonversionConfig internal constructor( } val primaryConfig = PrimaryConfig(projectKey, launchMode, environment, proxyUrl, isKidsMode) - val cacheConfig = CacheConfig(entitlementsCacheLifetime) + val cacheConfig = CacheConfig(entitlementsCacheLifetime, fallbackFileIdentifier) return QonversionConfig( context.application, diff --git a/sdk/src/main/java/com/qonversion/android/sdk/automations/internal/QAutomationsManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/automations/internal/QAutomationsManager.kt index c89d528fb..58fcd31af 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/automations/internal/QAutomationsManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/automations/internal/QAutomationsManager.kt @@ -149,7 +149,7 @@ internal class QAutomationsManager @Inject constructor( logger.error("loadScreen() -> $errorMessage") callback?.onError( QonversionError( - QonversionErrorCode.UnknownError, + QonversionErrorCode.Unknown, errorMessage ) ) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/automations/mvp/ScreenFragment.kt b/sdk/src/main/java/com/qonversion/android/sdk/automations/mvp/ScreenFragment.kt index b1ed8cdf1..816a0c959 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/automations/mvp/ScreenFragment.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/automations/mvp/ScreenFragment.kt @@ -216,7 +216,7 @@ class ScreenFragment : Fragment(), ScreenContract.View { }) } ?: run { logger.error("loadWebView() -> Failed to fetch html page for the app screen") - onError(QonversionError(QonversionErrorCode.UnknownError), true) + onError(QonversionError(QonversionErrorCode.Unknown), true) } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QFallbackObject.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QFallbackObject.kt new file mode 100644 index 000000000..b417c8d89 --- /dev/null +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QFallbackObject.kt @@ -0,0 +1,14 @@ +package com.qonversion.android.sdk.dto + +import com.qonversion.android.sdk.dto.offerings.QOfferings +import com.qonversion.android.sdk.dto.products.QProduct +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class QFallbackObject( + @Json(name = "products") val products: Map = mapOf(), + @Json(name = "offerings") val offerings: QOfferings?, + @Json(name = "products_permissions") val productPermissions: Map>?, + @Json(name = "remote_config_list") val remoteConfigList: QRemoteConfigList?, +) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseUpdatePolicy.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseUpdatePolicy.kt index c4560356d..9c7a10007 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseUpdatePolicy.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QPurchaseUpdatePolicy.kt @@ -1,6 +1,5 @@ package com.qonversion.android.sdk.dto -import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode /** @@ -55,33 +54,4 @@ enum class QPurchaseUpdatePolicy { else -> ReplacementMode.UNKNOWN_REPLACEMENT_MODE } } - - @Suppress("DEPRECATION") - internal fun toProrationMode(): Int { - return when (this) { - ChargeFullPrice -> BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE - ChargeProratedPrice -> BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE - WithTimeProration -> BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION - Deferred -> BillingFlowParams.ProrationMode.DEFERRED - WithoutProration -> BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION - else -> BillingFlowParams.ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY - } - } - - companion object { - - @Suppress("DEPRECATION") - fun fromProrationMode( - @BillingFlowParams.ProrationMode prorationMode: Int? - ): QPurchaseUpdatePolicy { - return when (prorationMode) { - BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE -> ChargeFullPrice - BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE -> ChargeProratedPrice - BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION -> WithTimeProration - BillingFlowParams.ProrationMode.DEFERRED -> Deferred - BillingFlowParams.ProrationMode.IMMEDIATE_WITHOUT_PRORATION -> WithoutProration - else -> Unknown - } - } - } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigList.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigList.kt index 408dd1673..cde8d85eb 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigList.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QRemoteConfigList.kt @@ -1,5 +1,7 @@ package com.qonversion.android.sdk.dto +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class QRemoteConfigList internal constructor( val remoteConfigs: List ) { diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt index 0a41eca1f..3f0a236d2 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/QonversionError.kt @@ -2,7 +2,8 @@ package com.qonversion.android.sdk.dto data class QonversionError( val code: QonversionErrorCode, - val additionalMessage: String = "" + val additionalMessage: String = "", + internal val httpCode: Int? = null ) { val description: String = code.specification @@ -18,20 +19,20 @@ data class QonversionError( * To get rid of billing errors make sure you follow the [Google Play's billing system integration](https://documentation.qonversion.io/docs/google-plays-billing-integration) */ enum class QonversionErrorCode(val specification: String) { - UnknownError("Unknown error"), + Unknown("Unknown error"), PlayStoreError("There was an issue with the Play Store service"), BillingUnavailable("The Billing service is unavailable on the device"), PurchasePending("Purchase is pending"), PurchaseUnspecified("Unspecified state of the purchase"), PurchaseInvalid("Failure of purchase"), - CanceledPurchase("User pressed back or canceled a dialog for purchase"), + PurchaseCanceled("User pressed back or canceled a dialog for purchase"), ProductNotOwned("Failed to consume purchase since item is not owned"), ProductAlreadyOwned("Failed to purchase since item is already owned"), FeatureNotSupported("The requested feature is not supported"), - ProductUnavailable("Requested product is not available for purchase or its product id was not found"), + StoreProductNotAvailable("Requested product is not available for purchase or its product id was not found"), NetworkConnectionFailed("There was a network issue. " + "Please make sure that the Internet connection is available on the device"), - ParseResponseFailed("A problem occurred while serializing or deserializing data"), + ResponseParsingFailed("A problem occurred while serializing or deserializing data"), BackendError("There was a backend error"), ProductNotFound("Failed to purchase since the Qonversion product was not found"), OfferingsNotFound("No offerings found"), @@ -43,5 +44,5 @@ enum class QonversionErrorCode(val specification: String) { ProjectConfigError("The project is not configured or configured incorrectly in the Qonversion Dashboard"), InvalidStoreCredentials("This account does not have access to the requested application"), RemoteConfigurationNotAvailable("Remote configuration is not available for the current user or for the provided context key"), - ApiRateLimitExceeded("API requests rate limit exceeded"), + ApiRateLimitExceeded("API requests rate limit exceeded") } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/eligibility/QIntroEligibilityStatus.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/eligibility/QIntroEligibilityStatus.kt index 24c73d7a4..1c07ad441 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/eligibility/QIntroEligibilityStatus.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/eligibility/QIntroEligibilityStatus.kt @@ -3,7 +3,7 @@ package com.qonversion.android.sdk.dto.eligibility import com.qonversion.android.sdk.dto.products.QProductType enum class QIntroEligibilityStatus(val type: String) { - NonIntroProduct("non_intro_or_trial_product"), + NonIntroOrTrialProduct("non_intro_or_trial_product"), Eligible("intro_or_trial_eligible"), Ineligible("intro_or_trial_ineligible"), Unknown("unknown"); @@ -11,7 +11,7 @@ enum class QIntroEligibilityStatus(val type: String) { companion object { fun fromType(type: String): QIntroEligibilityStatus { return when (type) { - "non_intro_or_trial_product" -> NonIntroProduct + "non_intro_or_trial_product" -> NonIntroOrTrialProduct "intro_or_trial_eligible" -> Eligible "intro_or_trial_ineligible" -> Ineligible else -> Unknown @@ -22,7 +22,7 @@ enum class QIntroEligibilityStatus(val type: String) { return when (productType) { QProductType.Intro, QProductType.Trial -> Eligible QProductType.Subscription -> Ineligible - QProductType.InApp -> NonIntroProduct + QProductType.InApp -> NonIntroOrTrialProduct QProductType.Unknown -> Unknown } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductInstallmentPlanDetails.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductInstallmentPlanDetails.kt new file mode 100644 index 000000000..10eda3194 --- /dev/null +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductInstallmentPlanDetails.kt @@ -0,0 +1,29 @@ +package com.qonversion.android.sdk.dto.products + +import com.android.billingclient.api.ProductDetails.InstallmentPlanDetails + +/** + * This class represents the details about the installment plan for a subscription product. + */ +data class QProductInstallmentPlanDetails( + /** + * Original [InstallmentPlanDetails] received from Google Play Billing Library + */ + val originalInstallmentPlanDetails: InstallmentPlanDetails +) { + /** + * Committed payments count after a user signs up for this subscription plan. + */ + val commitmentPaymentsCount: Int = + originalInstallmentPlanDetails.installmentPlanCommitmentPaymentsCount + + /** + * Subsequent committed payments count after this subscription plan renews. + * + * Returns 0 if the installment plan doesn't have any subsequent commitment, + * which means this subscription plan will fall back to a normal + * non-installment monthly plan when the plan renews. + */ + val subsequentCommitmentPaymentsCount: Int = + originalInstallmentPlanDetails.subsequentInstallmentPlanCommitmentPaymentsCount +} diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductOfferDetails.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductOfferDetails.kt index 533478820..a6265a0a9 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductOfferDetails.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductOfferDetails.kt @@ -44,6 +44,13 @@ data class QProductOfferDetails( */ val basePlan: QProductPricingPhase? = pricingPhases.find { it.isBasePlan } + /** + * Additional details of an installment plan, if exists. + */ + val installmentPlanDetails: QProductInstallmentPlanDetails? = originalOfferDetails.installmentPlanDetails?.let { + QProductInstallmentPlanDetails(it) + } + /** * A trial phase details, if exists. */ diff --git a/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductStoreDetails.kt b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductStoreDetails.kt index 173ca52f5..e1541bebe 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductStoreDetails.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/dto/products/QProductStoreDetails.kt @@ -129,6 +129,13 @@ data class QProductStoreDetails( val isPrepaid: Boolean = isSubscription && basePlanSubscriptionOfferDetails?.basePlan?.recurrenceMode == QProductPricingPhase.RecurrenceMode.NonRecurring + /** + * True, if the subscription product is installment, which means that users commit + * to pay for a specified amount of periods every month. + */ + val isInstallment: Boolean = isSubscription && + basePlanSubscriptionOfferDetails?.installmentPlanDetails != null + /** * Find an offer with the specified id. * @param offerId identifier of the searching offer. 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 ff6b01f31..6e8855f75 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 @@ -190,7 +190,7 @@ internal class QProductCenterManager internal constructor( processIdentity(identityId) } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { processingPartnersIdentityId = null remoteConfigManager.userChangingRequestFailedWithError(error) @@ -225,7 +225,7 @@ internal class QProductCenterManager internal constructor( fireIdentitySuccess(identityId) } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { fireIdentityError(identityId, error) } }) @@ -253,7 +253,7 @@ internal class QProductCenterManager internal constructor( val product = it.value if (product.storeDetails?.isPrepaid == true) { - QEligibility(QIntroEligibilityStatus.NonIntroProduct) + QEligibility(QIntroEligibilityStatus.NonIntroOrTrialProduct) } else { QEligibility(QIntroEligibilityStatus.fromProductType(product.type)) } @@ -281,7 +281,7 @@ internal class QProductCenterManager internal constructor( } private fun getOfferings(): QOfferings? { - return launchResultCache.getLaunchResult()?.offerings + return launchResultCache.getActualOfferings() } fun purchaseProduct( @@ -315,16 +315,16 @@ internal class QProductCenterManager internal constructor( purchaseModel: PurchaseModelInternal, callback: QonversionEntitlementsCallback ) { - val launchResult = launchResultCache.getLaunchResult() ?: run { + val products = launchResultCache.getActualProducts() ?: run { callback.onError(launchError ?: QonversionError(QonversionErrorCode.LaunchError)) return } - val product: QProduct = getProductForPurchase(purchaseModel.productId, launchResult) ?: run { + val product: QProduct = getProductForPurchase(purchaseModel.productId, products) ?: run { callback.onError(QonversionError(QonversionErrorCode.ProductNotFound)) return } - val oldProduct: QProduct? = getProductForPurchase(purchaseModel.oldProductId, launchResult) + val oldProduct: QProduct? = getProductForPurchase(purchaseModel.oldProductId, products) val purchaseModelEnriched = purchaseModel.enrich(product, oldProduct) processPurchase(context, purchaseModelEnriched, callback) } @@ -354,13 +354,13 @@ internal class QProductCenterManager internal constructor( private fun getProductForPurchase( productId: String?, - launchResult: QLaunchResult + products: Map ): QProduct? { if (productId == null) { return null } - return launchResult.products[productId] + return products[productId] } fun checkEntitlements(callback: QonversionEntitlementsCallback) { @@ -390,8 +390,8 @@ internal class QProductCenterManager internal constructor( executeRestoreBlocksOnSuccess(launchResult.permissions.toEntitlementsMap()) } - override fun onError(error: QonversionError, httpCode: Int?) { - if (shouldCalculatePermissionsLocally(error, httpCode)) { + override fun onError(error: QonversionError) { + if (shouldCalculatePermissionsLocally(error)) { calculateRestorePermissionsLocally(historyRecords, error) } else { executeRestoreBlocksOnError(error) @@ -430,22 +430,25 @@ internal class QProductCenterManager internal constructor( purchaseHistoryRecords: List, restoreError: QonversionError ) { - val launchResult = launchResultCache.getLaunchResult() ?: run { + val products = launchResultCache.getActualProducts() ?: run { failLocallyGrantingRestorePermissionsWithError( launchError ?: QonversionError(QonversionErrorCode.LaunchError) ) return } - launchResultCache.productPermissions?.let { - val permissions = grantPermissionsAfterFailedRestore( - purchaseHistoryRecords, - launchResult.products.values, - it - ) + val productPermissions = launchResultCache.getProductPermissions() ?: run { + failLocallyGrantingRestorePermissionsWithError(restoreError) + return + } + + val permissions = grantPermissionsAfterFailedRestore( + purchaseHistoryRecords, + products.values, + productPermissions + ) - executeRestoreBlocksOnSuccess(permissions.toEntitlementsMap()) - } ?: failLocallyGrantingRestorePermissionsWithError(restoreError) + executeRestoreBlocksOnSuccess(permissions.toEntitlementsMap()) } private fun calculatePurchasePermissionsLocally( @@ -453,7 +456,7 @@ internal class QProductCenterManager internal constructor( purchaseCallback: QonversionEntitlementsCallback?, purchaseError: QonversionError ) { - val launchResult = launchResultCache.getLaunchResult() ?: run { + val products = launchResultCache.getActualProducts() ?: run { failLocallyGrantingPurchasePermissionsWithError( purchaseCallback, launchError ?: QonversionError(QonversionErrorCode.LaunchError) @@ -461,21 +464,24 @@ internal class QProductCenterManager internal constructor( return } - launchResultCache.productPermissions?.let { - val purchasedProduct = launchResult.products.values.find { product -> - product.storeID == purchase.productId - } ?: run { - failLocallyGrantingPurchasePermissionsWithError(purchaseCallback, purchaseError) - return - } + val productPermissions = launchResultCache.getProductPermissions() ?: run { + failLocallyGrantingPurchasePermissionsWithError(purchaseCallback, purchaseError) + return + } - val permissions = grantPermissionsAfterFailedPurchaseTracking( - purchase, - purchasedProduct, - it - ) - purchaseCallback?.onSuccess(permissions.toEntitlementsMap()) - } ?: failLocallyGrantingPurchasePermissionsWithError(purchaseCallback, purchaseError) + val purchasedProduct = products.values.find { product -> + product.storeID == purchase.productId + } ?: run { + failLocallyGrantingPurchasePermissionsWithError(purchaseCallback, purchaseError) + return + } + + val permissions = grantPermissionsAfterFailedPurchaseTracking( + purchase, + purchasedProduct, + productPermissions + ) + purchaseCallback?.onSuccess(permissions.toEntitlementsMap()) } private fun failLocallyGrantingPurchasePermissionsWithError( @@ -670,8 +676,8 @@ internal class QProductCenterManager internal constructor( outerCallback?.onSuccess(launchResult) } - override fun onError(error: QonversionError, httpCode: Int?) { - outerCallback?.onError(error, httpCode) + override fun onError(error: QonversionError) { + outerCallback?.onError(error) } } } @@ -699,14 +705,14 @@ internal class QProductCenterManager internal constructor( callback?.onSuccess(launchResult) } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { launchError = error handlePendingRequests(error) loadStoreProductsIfPossible() - callback?.onError(error, httpCode) + callback?.onError(error) } } } @@ -745,14 +751,14 @@ internal class QProductCenterManager internal constructor( } private fun loadStoreProductsIfPossible() { - val launchResult = launchResultCache.getLaunchResult() ?: run { + val products = launchResultCache.getActualProducts() ?: run { val error = launchError ?: QonversionError(QonversionErrorCode.LaunchError) executeProductsBlocks(error) return } billingService.enrichStoreDataAsync( - launchResult.products.values.toList(), + products.values.toList(), { error -> executeProductsBlocks(error.toQonversionError()) } ) { executeProductsBlocks() @@ -789,7 +795,7 @@ internal class QProductCenterManager internal constructor( purchasesCache.clearPurchase(purchase) } - override fun onError(error: QonversionError, httpCode: Int?) {} + override fun onError(error: QonversionError) {} }) } } @@ -808,16 +814,16 @@ internal class QProductCenterManager internal constructor( return } - val launchResult = launchResultCache.getLaunchResult() ?: run { + val products = launchResultCache.getActualProducts() ?: run { val error = launchError ?: QonversionError(QonversionErrorCode.LaunchError) fireProductsFailure(callbacks, error) return } - val products = launchResult.products.values.toList() - billingService.enrichStoreData(products) + val productsList = products.values.toList() + billingService.enrichStoreData(productsList) callbacks.forEach { callback -> - callback.onSuccess(products.associateBy { it.qonversionID }) + callback.onSuccess(productsList.associateBy { it.qonversionID }) } } @@ -875,7 +881,7 @@ internal class QProductCenterManager internal constructor( ) { launch(object : QonversionLaunchCallback { override fun onSuccess(launchResult: QLaunchResult) = onSuccess(launchResult) - override fun onError(error: QonversionError, httpCode: Int?) = onError(error) + override fun onError(error: QonversionError) = onError(error) }) } @@ -955,7 +961,7 @@ internal class QProductCenterManager internal constructor( if (!handledPurchasesCache.shouldHandlePurchase(purchase)) return@forEach - val product: QProduct? = launchResultCache.getLaunchResult()?.products?.values?.find { + val product: QProduct? = launchResultCache.getActualProducts()?.values?.find { it.storeID == purchase.productId } val purchaseInfo = converter.convertPurchase(purchase) @@ -977,10 +983,10 @@ internal class QProductCenterManager internal constructor( handledPurchasesCache.saveHandledPurchase(purchase) } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { storeFailedPurchaseIfNecessary(purchase, purchaseInfo, product) - if (shouldCalculatePermissionsLocally(error, httpCode)) { + if (shouldCalculatePermissionsLocally(error)) { calculatePurchasePermissionsLocally( purchase, purchaseCallback, @@ -1017,10 +1023,10 @@ internal class QProductCenterManager internal constructor( } ?: storePurchase() } - private fun shouldCalculatePermissionsLocally(error: QonversionError, httpCode: Int?): Boolean { + private fun shouldCalculatePermissionsLocally(error: QonversionError): Boolean { return !internalConfig.isAnalyticsMode && ( error.code == QonversionErrorCode.NetworkConnectionFailed || - httpCode?.isInternalServerError() == true + error.httpCode?.isInternalServerError() == true ) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt index df7705153..345a45608 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QRemoteConfigManager.kt @@ -1,9 +1,11 @@ package com.qonversion.android.sdk.internal +import com.qonversion.android.sdk.dto.QFallbackObject import com.qonversion.android.sdk.dto.QRemoteConfig import com.qonversion.android.sdk.dto.QRemoteConfigList import com.qonversion.android.sdk.dto.QonversionError import com.qonversion.android.sdk.internal.provider.UserStateProvider +import com.qonversion.android.sdk.internal.services.QFallbacksService import com.qonversion.android.sdk.internal.services.QRemoteConfigService import com.qonversion.android.sdk.listeners.QonversionEmptyCallback import com.qonversion.android.sdk.listeners.QonversionExperimentAttachCallback @@ -16,7 +18,12 @@ private val EmptyContextKey: String? = null internal class QRemoteConfigManager @Inject constructor( private val remoteConfigService: QRemoteConfigService, + private val fallbacksService: QFallbacksService ) { + private val fallbackData: QFallbackObject? by lazy { + fallbacksService.obtainFallbackData() + } + internal class LoadingState( var loadedConfig: QRemoteConfig? = null, val callbacks: MutableList = mutableListOf(), @@ -89,7 +96,25 @@ internal class QRemoteConfigManager @Inject constructor( } override fun onError(error: QonversionError) { - fireToCallbacks(contextKey) { onError(error) } + if (!error.shouldFireFallback) { + fireToCallbacks(contextKey) { onError(error) } + return + } + + val baseRemoteConfigList = fallbackData?.remoteConfigList ?: run { + fireToCallbacks(contextKey) { onError(error) } + return@onError + } + + val remoteConfig = if (contextKey == null) { + baseRemoteConfigList.remoteConfigForEmptyContextKey + } else { + baseRemoteConfigList.remoteConfigForContextKey(contextKey) + } + + remoteConfig?.let { + onSuccess(it) + } ?: fireToCallbacks(contextKey) { onError(error) } } }) } @@ -118,7 +143,7 @@ internal class QRemoteConfigManager @Inject constructor( remoteConfigService.loadRemoteConfigs( contextKeys, includeEmptyContextKey, - getRemoteConfigListCallbackWrapper(callback), + getRemoteConfigListCallbackWrapper(contextKeys, includeEmptyContextKey, callback), ) } }) @@ -132,7 +157,7 @@ internal class QRemoteConfigManager @Inject constructor( userPropertiesManager.forceSendProperties(object : QonversionEmptyCallback { override fun onComplete() { - remoteConfigService.loadRemoteConfigs(getRemoteConfigListCallbackWrapper(callback)) + remoteConfigService.loadRemoteConfigs(getRemoteConfigListCallbackWrapper(null, true, callback)) } }) } @@ -164,6 +189,8 @@ internal class QRemoteConfigManager @Inject constructor( } private fun getRemoteConfigListCallbackWrapper( + contextKeys: List?, + includeEmptyContextKey: Boolean, callback: QonversionRemoteConfigListCallback ): QonversionRemoteConfigListCallback { // Remembering loading states for the case of user change - @@ -182,7 +209,29 @@ internal class QRemoteConfigManager @Inject constructor( } override fun onError(error: QonversionError) { - callback.onError(error) + if (!error.shouldFireFallback) { + callback.onError(error) + return + } + + val baseRemoteConfigList = fallbackData?.remoteConfigList ?: run { + callback.onError(error) + return@onError + } + + val remoteConfigList = if (contextKeys == null) { + baseRemoteConfigList.copy() + } else { + val remoteConfigs = baseRemoteConfigList.remoteConfigs.filter { contextKeys.contains(it.source.contextKey) }.toMutableList() + if (includeEmptyContextKey) { + baseRemoteConfigList.remoteConfigs.find { it.source.contextKey?.isEmpty() == true }?.let { + remoteConfigs.add(it) + } + } + QRemoteConfigList(remoteConfigs.toList()) + } + + onSuccess(remoteConfigList) } } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QUserPropertiesManager.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QUserPropertiesManager.kt index e78e98ddd..5dbd19809 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QUserPropertiesManager.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QUserPropertiesManager.kt @@ -111,7 +111,7 @@ internal class QUserPropertiesManager @Inject internal constructor( retryPropertiesRequest() } - override fun onError(error: QonversionError, httpCode: Int?) { + override fun onError(error: QonversionError) { retryPropertiesRequest() } }) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionFactory.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionFactory.kt index 5cd7383a3..c108b16c3 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionFactory.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/QonversionFactory.kt @@ -4,6 +4,7 @@ import android.app.Application import android.os.Handler import androidx.annotation.UiThread import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.PurchasesUpdatedListener import com.qonversion.android.sdk.internal.billing.BillingClientWrapper import com.qonversion.android.sdk.internal.billing.BillingClientHolder @@ -95,7 +96,12 @@ internal class QonversionFactory( @UiThread private fun createBillingClient(listener: PurchasesUpdatedListener): BillingClient { val builder = BillingClient.newBuilder(context) - builder.enablePendingPurchases() + builder.enablePendingPurchases( + PendingPurchasesParams.newBuilder() + .enableOneTimeProducts() + .enablePrepaidPlans() + .build() + ) builder.setListener(listener) return builder.build() } 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 dd74687ca..15be6732d 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 @@ -25,6 +25,7 @@ import com.qonversion.android.sdk.internal.dto.purchase.PurchaseModelInternal import com.qonversion.android.sdk.internal.logger.ConsoleLogger import com.qonversion.android.sdk.internal.logger.ExceptionManager import com.qonversion.android.sdk.internal.provider.AppStateProvider +import com.qonversion.android.sdk.internal.services.QFallbacksService import com.qonversion.android.sdk.internal.storage.SharedPreferencesCache import com.qonversion.android.sdk.listeners.QonversionExperimentAttachCallback import com.qonversion.android.sdk.listeners.QonversionEntitlementsCallback @@ -53,6 +54,7 @@ internal class QonversionInternal( private var sharedPreferencesCache: SharedPreferencesCache private var exceptionManager: ExceptionManager private var remoteConfigManager: QRemoteConfigManager + private var fallbackService: QFallbacksService override var appState = AppState.Background @@ -75,6 +77,8 @@ internal class QonversionInternal( internalConfig.uid = userID + fallbackService = QDependencyInjector.appComponent.fallbacksService() + automationsManager = QDependencyInjector.appComponent.automationsManager() userPropertiesManager = QDependencyInjector.appComponent.userPropertiesManager() @@ -132,7 +136,7 @@ internal class QonversionInternal( override fun onSuccess(launchResult: QLaunchResult) = postToMainThread { automationsManager.onLaunchProcessed() } - override fun onError(error: QonversionError, httpCode: Int?) {} + override fun onError(error: QonversionError) {} }) } @@ -334,6 +338,12 @@ internal class QonversionInternal( userPropertiesManager.userProperties(callback) } + override fun isFallbackFileAccessible(): Boolean { + val fallbackObject = fallbackService.obtainFallbackData() + + return fallbackObject != null + } + override fun setEntitlementsUpdateListener(entitlementsUpdateListener: QEntitlementsUpdateListener) { productCenterManager.setEntitlementsUpdateListener(entitlementsUpdateListener) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/api/ApiErrorMapper.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/api/ApiErrorMapper.kt index ec4db3daf..5103cc806 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/api/ApiErrorMapper.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/api/ApiErrorMapper.kt @@ -48,7 +48,8 @@ internal class ApiErrorMapper @Inject constructor(private val helper: ApiHelper) return QonversionError( qonversionCode, - "HTTP status code=${value.code()}, $errorMessage. ${additionalErrorMessage ?: ""}" + "HTTP status code=${value.code()}, $errorMessage. ${additionalErrorMessage ?: ""}", + value.code() ) } 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 e95b75f0d..14b582319 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 @@ -125,6 +125,7 @@ internal class BillingClientWrapper( val params = QueryPurchaseHistoryParams.newBuilder() .setProductType(productType) .build() + @Suppress("DEPRECATION") queryPurchaseHistoryAsync(params) { billingResult, purchasesList -> onCompleted( billingResult, @@ -142,6 +143,7 @@ internal class BillingClientWrapper( val params = QueryPurchaseHistoryParams.newBuilder() .setProductType(productType.toProductType()) .build() + @Suppress("DEPRECATION") queryPurchaseHistoryAsync(params, onCompleted) } } @@ -288,22 +290,4 @@ internal class BillingClientWrapper( } return this } - - private fun BillingFlowParams.Builder.setSubscriptionUpdateParams( - info: UpdatePurchaseInfo? = null - ): BillingFlowParams.Builder { - if (info != null) { - val updateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder() - updateParamsBuilder.setOldPurchaseToken(info.purchaseToken) - val updateParams = updateParamsBuilder.apply { - info.updatePolicy?.toReplacementMode()?.let { - setSubscriptionReplacementMode(it) - } - }.build() - - setSubscriptionUpdateParams(updateParams) - } - - return this - } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapperBase.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapperBase.kt index 82865fff4..cbda1c220 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapperBase.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/billing/BillingClientWrapperBase.kt @@ -70,4 +70,22 @@ internal abstract class BillingClientWrapperBase( onQueryFailed(BillingError(billingResult.responseCode, errorMessage)) logger.error("queryPurchases() -> $errorMessage") } + + protected fun BillingFlowParams.Builder.setSubscriptionUpdateParams( + info: UpdatePurchaseInfo? = null + ): BillingFlowParams.Builder { + if (info != null) { + val updateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder() + updateParamsBuilder.setOldPurchaseToken(info.purchaseToken) + val updateParams = updateParamsBuilder.apply { + info.updatePolicy?.toReplacementMode()?.let { + setSubscriptionReplacementMode(it) + } + }.build() + + setSubscriptionUpdateParams(updateParams) + } + + return this + } } 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 46661f24e..a999ce576 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 @@ -214,25 +214,6 @@ internal class LegacyBillingClientWrapper( } } - @Suppress("DEPRECATION") - private fun BillingFlowParams.Builder.setSubscriptionUpdateParams( - info: UpdatePurchaseInfo? = null - ): BillingFlowParams.Builder { - if (info != null) { - val updateParamsBuilder = BillingFlowParams.SubscriptionUpdateParams.newBuilder() - updateParamsBuilder.setOldSkuPurchaseToken(info.purchaseToken) - val updateParams = updateParamsBuilder.apply { - info.updatePolicy?.toProrationMode()?.let { - setReplaceSkusProrationMode(it) - } - }.build() - - setSubscriptionUpdateParams(updateParams) - } - - return this - } - private fun logSkuDetails( @Suppress("DEPRECATION") skuDetailsList: List, skuList: List 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 3040d9f0e..5d7c5a612 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 @@ -285,7 +285,7 @@ internal class QonversionBillingService internal constructor( val billingClientWrapper = chooseBillingClientWrapperForProductPurchase(product) ?: run { purchasesListener.onPurchasesFailed( BillingError( - BillingClient.BillingResponseCode.ITEM_NOT_OWNED, + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE, "Store details for purchasing Qonversion product " + "${product.qonversionID} were not found" ) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/component/AppComponent.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/component/AppComponent.kt index cd550ed49..89a68627d 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/component/AppComponent.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/component/AppComponent.kt @@ -16,6 +16,7 @@ import com.qonversion.android.sdk.internal.logger.QExceptionManager import com.qonversion.android.sdk.internal.provider.AppStateProvider import com.qonversion.android.sdk.internal.repository.QRepository import com.qonversion.android.sdk.internal.repository.DefaultRepository +import com.qonversion.android.sdk.internal.services.QFallbacksService import com.qonversion.android.sdk.internal.services.QUserInfoService import com.qonversion.android.sdk.internal.storage.LaunchResultCacheWrapper import com.qonversion.android.sdk.internal.storage.PurchasesCache @@ -45,4 +46,5 @@ internal interface AppComponent { fun appStateProvider(): AppStateProvider fun sharedPreferencesCache(): SharedPreferencesCache fun exceptionManager(): QExceptionManager + fun fallbacksService(): QFallbacksService } 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 c75248b9e..8a6c8b151 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 @@ -8,6 +8,7 @@ import com.qonversion.android.sdk.internal.di.scope.ApplicationScope import com.qonversion.android.sdk.internal.logger.ConsoleLogger import com.qonversion.android.sdk.internal.logger.Logger import com.qonversion.android.sdk.internal.provider.AppStateProvider +import com.qonversion.android.sdk.internal.services.QFallbacksService import com.qonversion.android.sdk.internal.storage.LaunchResultCacheWrapper import com.qonversion.android.sdk.internal.storage.PurchasesCache import com.qonversion.android.sdk.internal.storage.SharedPreferencesCache @@ -69,8 +70,19 @@ internal class AppModule( @Provides fun provideLaunchResultCacheWrapper( moshi: Moshi, - sharedPreferencesCache: SharedPreferencesCache + sharedPreferencesCache: SharedPreferencesCache, + fallbacksService: QFallbacksService ): LaunchResultCacheWrapper { - return LaunchResultCacheWrapper(moshi, sharedPreferencesCache, internalConfig) + return LaunchResultCacheWrapper(moshi, sharedPreferencesCache, internalConfig, fallbacksService) + } + + @ApplicationScope + @Provides + fun provideFallbackService( + context: Application, + moshi: Moshi, + logger: Logger + ): QFallbacksService { + return QFallbacksService(context, internalConfig, moshi, logger) } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt index c181e5fe5..aa4e1ac0a 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt @@ -19,6 +19,7 @@ import com.qonversion.android.sdk.internal.dto.QEntitlementSourceAdapter import com.qonversion.android.sdk.internal.dto.QPermissionsAdapter import com.qonversion.android.sdk.internal.dto.QProductRenewStateAdapter import com.qonversion.android.sdk.internal.dto.QProductsAdapter +import com.qonversion.android.sdk.internal.dto.QRemoteConfigListAdapter import com.qonversion.android.sdk.internal.dto.QRemoteConfigurationSourceAssignmentTypeAdapter import com.qonversion.android.sdk.internal.dto.QRemoteConfigurationSourceTypeAdapter import com.qonversion.android.sdk.internal.dto.QTransactionEnvironmentAdapter @@ -61,6 +62,7 @@ internal class NetworkModule { .add(QOfferingsAdapter()) .add(QOfferingAdapter()) .add(QOfferingTagAdapter()) + .add(QRemoteConfigListAdapter()) .add(QExperimentGroupTypeAdapter()) .add(QRemoteConfigurationSourceTypeAdapter()) .add(QRemoteConfigurationSourceAssignmentTypeAdapter()) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/QonversionMappingAdapters.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/QonversionMappingAdapters.kt index ce5cf469a..47249f8fb 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/QonversionMappingAdapters.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/QonversionMappingAdapters.kt @@ -1,5 +1,7 @@ package com.qonversion.android.sdk.internal.dto +import com.qonversion.android.sdk.dto.QRemoteConfig +import com.qonversion.android.sdk.dto.QRemoteConfigList import com.qonversion.android.sdk.dto.QRemoteConfigurationAssignmentType import com.qonversion.android.sdk.dto.QRemoteConfigurationSourceType import com.qonversion.android.sdk.dto.entitlements.QEntitlementSource @@ -185,6 +187,22 @@ internal class QOfferingTagAdapter { } } +internal class QRemoteConfigListAdapter { + @ToJson + private fun toJson(remoteConfigList: QRemoteConfigList?): List { + return remoteConfigList?.remoteConfigs ?: listOf() + } + + @FromJson + fun fromJson(remoteConfigs: List): QRemoteConfigList? { + if (remoteConfigs.isEmpty()) { + return null + } + + return QRemoteConfigList(remoteConfigs) + } +} + internal class QOfferingsAdapter { @ToJson private fun toJson(offerings: QOfferings?): List { diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/config/CacheConfig.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/config/CacheConfig.kt index 7b428134a..a83703198 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/config/CacheConfig.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/dto/config/CacheConfig.kt @@ -1,7 +1,9 @@ package com.qonversion.android.sdk.internal.dto.config +import androidx.annotation.RawRes import com.qonversion.android.sdk.dto.entitlements.QEntitlementsCacheLifetime internal data class CacheConfig( - val entitlementsCacheLifetime: QEntitlementsCacheLifetime + val entitlementsCacheLifetime: QEntitlementsCacheLifetime, + @RawRes val fallbackFileIdentifier: Int? ) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/errors.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/errors.kt index 979f37b98..99a392b1e 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/errors.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/errors.kt @@ -15,16 +15,16 @@ internal fun BillingError.toQonversionError(): QonversionError { BillingClient.BillingResponseCode.NETWORK_ERROR -> QonversionErrorCode.NetworkConnectionFailed BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> QonversionErrorCode.FeatureNotSupported - BillingClient.BillingResponseCode.OK -> QonversionErrorCode.UnknownError - BillingClient.BillingResponseCode.USER_CANCELED -> QonversionErrorCode.CanceledPurchase + BillingClient.BillingResponseCode.OK -> QonversionErrorCode.Unknown + BillingClient.BillingResponseCode.USER_CANCELED -> QonversionErrorCode.PurchaseCanceled BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> QonversionErrorCode.BillingUnavailable - BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> QonversionErrorCode.ProductUnavailable + BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> QonversionErrorCode.StoreProductNotAvailable BillingClient.BillingResponseCode.DEVELOPER_ERROR -> QonversionErrorCode.PurchaseInvalid BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> QonversionErrorCode.ProductAlreadyOwned BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> QonversionErrorCode.ProductNotOwned - else -> QonversionErrorCode.UnknownError + else -> QonversionErrorCode.Unknown } val additionalMessage = when (errorCode) { QonversionErrorCode.BillingUnavailable -> @@ -40,13 +40,13 @@ internal fun BillingError.toQonversionError(): QonversionError { internal fun Throwable.toQonversionError(): QonversionError { return when (this) { is JSONException -> { - QonversionError(QonversionErrorCode.ParseResponseFailed, localizedMessage ?: "") + QonversionError(QonversionErrorCode.ResponseParsingFailed, localizedMessage ?: "") } is IOException -> { QonversionError(QonversionErrorCode.NetworkConnectionFailed, localizedMessage ?: "") } - else -> QonversionError(QonversionErrorCode.UnknownError, localizedMessage ?: "") + else -> QonversionError(QonversionErrorCode.Unknown, localizedMessage ?: "") } } 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 5caef4c67..56ec92954 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 @@ -573,7 +573,7 @@ internal class DefaultRepository internal constructor( } } } else { - callback.onError(error, errorCode) + callback.onError(error) } } @@ -642,7 +642,7 @@ internal class DefaultRepository internal constructor( } onFailure = { logger.error("restoreRequest - failure - ${it.toQonversionError()}") - callback?.onError(it.toQonversionError(), null) + callback?.onError(it.toQonversionError()) } } } @@ -655,7 +655,7 @@ internal class DefaultRepository internal constructor( if (body != null && body.success) { callback?.onSuccess(body.data) } else { - callback?.onError(errorMapper.getErrorFromResponse(response), response.code()) + callback?.onError(errorMapper.getErrorFromResponse(response)) } } @@ -697,12 +697,12 @@ internal class DefaultRepository internal constructor( if (body != null && body.success) { callback?.onSuccess(body.data) } else { - callback?.onError(errorMapper.getErrorFromResponse(it), it.code()) + callback?.onError(errorMapper.getErrorFromResponse(it)) } } onFailure = { logger.error("initRequest - failure - ${it.toQonversionError()}") - callback?.onError(it.toQonversionError(), null) + callback?.onError(it.toQonversionError()) } } } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt index 4da3ac280..f106a7612 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/repository/RepositoryWithRateLimits.kt @@ -27,7 +27,7 @@ internal class RepositoryWithRateLimits( withRateLimitCheck( RequestType.Init, initRequestData.hashCode(), - { error -> initRequestData.callback?.onError(error, null) } + { error -> initRequestData.callback?.onError(error) } ) { repository.init(initRequestData) } @@ -129,7 +129,7 @@ internal class RepositoryWithRateLimits( withRateLimitCheck( RequestType.Purchase, purchase.hashCode() + (qProductId + installDate).hashCode(), - { error -> callback.onError(error, null) } + { error -> callback.onError(error) } ) { repository.purchase(installDate, purchase, qProductId, callback) } @@ -143,7 +143,7 @@ internal class RepositoryWithRateLimits( withRateLimitCheck( RequestType.Restore, installDate.hashCode() + historyRecords.hashCode(), - { error -> callback?.onError(error, null) } + { error -> callback?.onError(error) } ) { repository.restore(installDate, historyRecords, callback) } diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/services/QFallbacksService.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/services/QFallbacksService.kt new file mode 100644 index 000000000..85f71b8ce --- /dev/null +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/services/QFallbacksService.kt @@ -0,0 +1,62 @@ +package com.qonversion.android.sdk.internal.services + +import android.app.Application +import com.qonversion.android.sdk.dto.QFallbackObject +import com.qonversion.android.sdk.internal.logger.Logger +import com.qonversion.android.sdk.internal.provider.CacheConfigProvider +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader + +private const val FALLBACK_FILE_NAME = "qonversion_android_fallbacks.json" + +internal class QFallbacksService( + private val context: Application, + private val cacheConfigProvider: CacheConfigProvider, + moshi: Moshi, + private val logger: Logger +) { + private val jsonAdapter: JsonAdapter = moshi.adapter(QFallbackObject::class.java) + + fun obtainFallbackData(): QFallbackObject? { + return try { + val json: String = getStringFromFile() + val fallbackData: QFallbackObject? = jsonAdapter.fromJson(json) + + fallbackData + } catch (e: Exception) { + logger.warn("Failed to parse Qonversion fallback file: " + e.message) + null + } + } + + @Throws(java.lang.Exception::class) + fun convertStreamToString(inputStream: InputStream): String { + val reader = BufferedReader(InputStreamReader(inputStream)) + val stringBuilder = StringBuilder() + var line: String? + while (reader.readLine().also { line = it } != null) { + stringBuilder.append(line).append("\n") + } + reader.close() + + return stringBuilder.toString() + } + + @Throws(java.lang.Exception::class) + fun getStringFromFile(): String { + val fallbackFileIdentifier = cacheConfigProvider.cacheConfig.fallbackFileIdentifier + val fileInputStream = if (fallbackFileIdentifier != null) { + context.resources.openRawResource(fallbackFileIdentifier) + } else { + context.assets.open(FALLBACK_FILE_NAME) + } + + val result = convertStreamToString(fileInputStream) + fileInputStream.close() + + return result + } +} diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt index 251a03a19..00898922a 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapper.kt @@ -1,10 +1,14 @@ package com.qonversion.android.sdk.internal.storage +import com.qonversion.android.sdk.dto.QFallbackObject +import com.qonversion.android.sdk.dto.offerings.QOfferings +import com.qonversion.android.sdk.dto.products.QProduct import com.qonversion.android.sdk.internal.milliSecondsToSeconds import com.qonversion.android.sdk.internal.daysToSeconds import com.qonversion.android.sdk.internal.dto.QLaunchResult import com.qonversion.android.sdk.internal.dto.QPermission import com.qonversion.android.sdk.internal.provider.CacheConfigProvider +import com.qonversion.android.sdk.internal.services.QFallbacksService import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types @@ -17,10 +21,14 @@ private const val PERMISSIONS_CACHE_TIMESTAMP_KEY = "permissions_timestamp" internal class LaunchResultCacheWrapper( moshi: Moshi, private val cache: SharedPreferencesCache, - private val cacheConfigProvider: CacheConfigProvider + private val cacheConfigProvider: CacheConfigProvider, + private val fallbacksService: QFallbacksService ) { private val launchResultAdapter: JsonAdapter = moshi.adapter(QLaunchResult::class.java) + private val fallbackData: QFallbackObject? by lazy { + fallbacksService.obtainFallbackData() + } private val permissionsAdapter: JsonAdapter> = moshi.adapter( @@ -31,8 +39,6 @@ internal class LaunchResultCacheWrapper( ) ) - val productPermissions get() = getLaunchResult()?.productPermissions - var sessionLaunchResult: QLaunchResult? = null private set @@ -56,10 +62,28 @@ internal class LaunchResultCacheWrapper( cache.remove(PERMISSIONS_KEY) } - fun getLaunchResult(): QLaunchResult? { + private fun getLaunchResult(): QLaunchResult? { return sessionLaunchResult ?: cache.getObject(LAUNCH_RESULT_KEY, launchResultAdapter) } + fun getActualProducts(): Map? { + val products = getLaunchResult()?.products ?: fallbackData?.products + + return products + } + + fun getProductPermissions(): Map>? { + val productPermissions = getLaunchResult()?.productPermissions ?: fallbackData?.productPermissions + + return productPermissions + } + + fun getActualOfferings(): QOfferings? { + val offerings = getLaunchResult()?.offerings ?: fallbackData?.offerings + + return offerings + } + private fun getPermissions(): Map? { if (permissions == null) { permissions = cache.getObject(PERMISSIONS_KEY, permissionsAdapter) diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/utils.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/utils.kt index d746eb6c6..f2416c4fc 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/utils.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/utils.kt @@ -1,5 +1,13 @@ package com.qonversion.android.sdk.internal +import com.qonversion.android.sdk.dto.QonversionError +import com.qonversion.android.sdk.dto.QonversionErrorCode + internal val Int.daysToSeconds get() = this * 24L * 60 * 60 internal val Int.daysToMs get() = daysToSeconds * 1000 + +internal val QonversionError.shouldFireFallback + get(): Boolean = + this.code == QonversionErrorCode.NetworkConnectionFailed || + this.httpCode?.isInternalServerError() == true diff --git a/sdk/src/main/java/com/qonversion/android/sdk/listeners/QonversionCallback.kt b/sdk/src/main/java/com/qonversion/android/sdk/listeners/QonversionCallback.kt index 6ec342b34..9520770de 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/listeners/QonversionCallback.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/listeners/QonversionCallback.kt @@ -13,7 +13,7 @@ import com.qonversion.android.sdk.dto.properties.QUserProperties internal interface QonversionLaunchCallback { fun onSuccess(launchResult: QLaunchResult) - fun onError(error: QonversionError, httpCode: Int?) + fun onError(error: QonversionError) } interface QonversionProductsCallback { diff --git a/sdk/src/test/java/com/qonversion/android/sdk/QonversionConfigTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/QonversionConfigTest.kt index 2b73b974a..b3578970f 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/QonversionConfigTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/QonversionConfigTest.kt @@ -36,7 +36,7 @@ internal class QonversionConfigTest { private val mockEntitlementsCacheLifetime = mockk() private val mockProxyUrl = "mock url" private val mockPrimaryConfig = PrimaryConfig(projectKey, mockLaunchMode, mockEnvironment, mockProxyUrl) - private val mockCacheConfig = CacheConfig(mockEntitlementsCacheLifetime) + private val mockCacheConfig = CacheConfig(mockEntitlementsCacheLifetime, null) @BeforeEach fun setUp() { @@ -169,7 +169,7 @@ internal class QonversionConfigTest { val builder = QonversionConfig.Builder(mockContext, projectKey, mockLaunchMode) val expPrimaryConfig = PrimaryConfig(projectKey, mockLaunchMode, defaultEnvironment) - val expCacheConfig = CacheConfig(defaultEntitlementsCacheLifetime) + val expCacheConfig = CacheConfig(defaultEntitlementsCacheLifetime, null) val expResult = QonversionConfig( mockApplication, expPrimaryConfig, diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapperTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapperTest.kt index bcb4307dd..d64b12f30 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapperTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/storage/LaunchResultCacheWrapperTest.kt @@ -3,6 +3,7 @@ package com.qonversion.android.sdk.internal.storage import com.qonversion.android.sdk.internal.milliSecondsToSeconds import com.qonversion.android.sdk.internal.dto.QLaunchResult import com.qonversion.android.sdk.internal.provider.CacheConfigProvider +import com.qonversion.android.sdk.internal.services.QFallbacksService import com.qonversion.android.sdk.internal.storage.Util.Companion.buildMoshi import io.mockk.* import org.junit.jupiter.api.BeforeEach @@ -14,6 +15,7 @@ internal class LaunchResultCacheWrapperTest { private val mockMoshi = buildMoshi() private val mockAdapter = mockMoshi.adapter(QLaunchResult::class.java) private val mockCacheConfigProvider = mockk() + private val mockQFallbacksService = mockk() private lateinit var cacheWrapper: LaunchResultCacheWrapper @@ -24,7 +26,7 @@ internal class LaunchResultCacheWrapperTest { fun setUp() { clearAllMocks() - cacheWrapper = LaunchResultCacheWrapper(mockMoshi, mockPrefsCache, mockCacheConfigProvider) + cacheWrapper = LaunchResultCacheWrapper(mockMoshi, mockPrefsCache, mockCacheConfigProvider, mockQFallbacksService) } @Nested