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