diff --git a/android/build.gradle b/android/build.gradle index e06b1c61..9359e6a2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,7 +3,6 @@ version '4.4.0' buildscript { ext.kotlin_version = '1.3.50' - ext.qonversion_version = '3.3.0' repositories { google() jcenter() @@ -42,6 +41,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "io.qonversion.android.sdk:sdk:$qonversion_version" + implementation 'io.qonversion.sandwich:sandwich:0.0.11' implementation 'com.google.code.gson:gson:2.8.6' } diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt index 09ecba4d..05a6b290 100644 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt +++ b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/AutomationsPlugin.kt @@ -1,18 +1,21 @@ package com.qonversion.flutter.sdk.qonversion_flutter_sdk import com.google.gson.Gson -import com.qonversion.android.sdk.automations.Automations -import com.qonversion.android.sdk.automations.AutomationsDelegate -import com.qonversion.android.sdk.automations.QActionResult import io.flutter.plugin.common.BinaryMessenger +import io.qonversion.sandwich.AutomationsEventListener +import io.qonversion.sandwich.AutomationsSandwich +import io.qonversion.sandwich.BridgeData -class AutomationsPlugin { +class AutomationsPlugin(messenger: BinaryMessenger) : AutomationsEventListener { private var shownScreensStreamHandler: BaseEventStreamHandler? = null private var startedActionsStreamHandler: BaseEventStreamHandler? = null private var failedActionsStreamHandler: BaseEventStreamHandler? = null private var finishedActionsStreamHandler: BaseEventStreamHandler? = null private var finishedAutomationsStreamHandler: BaseEventStreamHandler? = null - private val automationsDelegate = getAutomationsDelegate() + + private val automationSandwich by lazy { + AutomationsSandwich() + } companion object { private const val EVENT_CHANNEL_SHOWN_SCREENS = "shown_screens" @@ -22,7 +25,7 @@ class AutomationsPlugin { private const val EVENT_CHANNEL_FINISHED_AUTOMATIONS = "finished_automations" } - fun register(messenger: BinaryMessenger) { + init { val shownScreensListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_SHOWN_SCREENS) shownScreensListener.register() shownScreensStreamHandler = shownScreensListener.eventStreamHandler @@ -44,32 +47,19 @@ class AutomationsPlugin { finishedAutomationsStreamHandler = finishedAutomationsListener.eventStreamHandler } - fun setAutomationsDelegate() { - Automations.setDelegate(automationsDelegate) + fun subscribe() { + automationSandwich.subscribe(this) } - private fun getAutomationsDelegate() = object : AutomationsDelegate { - override fun automationsDidShowScreen(screenId: String) { - shownScreensStreamHandler?.eventSink?.success(screenId) + override fun onAutomationEvent(event: AutomationsEventListener.Event, payload: BridgeData?) { + val (data, stream) = when (event) { + AutomationsEventListener.Event.ScreenShown -> Pair(Gson().toJson(payload), shownScreensStreamHandler) + AutomationsEventListener.Event.ActionStarted -> Pair(Gson().toJson(payload), startedActionsStreamHandler) + AutomationsEventListener.Event.ActionFinished -> Pair(Gson().toJson(payload), finishedActionsStreamHandler) + AutomationsEventListener.Event.ActionFailed -> Pair(Gson().toJson(payload), failedActionsStreamHandler) + AutomationsEventListener.Event.AutomationsFinished -> Pair(payload, finishedAutomationsStreamHandler) } - override fun automationsDidStartExecuting(actionResult: QActionResult) { - val payload = Gson().toJson(actionResult.toMap()) - startedActionsStreamHandler?.eventSink?.success(payload) - } - - override fun automationsDidFailExecuting(actionResult: QActionResult) { - val payload = Gson().toJson(actionResult.toMap()) - failedActionsStreamHandler?.eventSink?.success(payload) - } - - override fun automationsDidFinishExecuting(actionResult: QActionResult) { - val payload = Gson().toJson(actionResult.toMap()) - finishedActionsStreamHandler?.eventSink?.success(payload) - } - - override fun automationsFinished() { - finishedAutomationsStreamHandler?.eventSink?.success(null) - } + stream?.eventSink?.success(data) } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/Constants.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/Constants.kt deleted file mode 100644 index e19ef5d5..00000000 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/Constants.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.qonversion.flutter.sdk.qonversion_flutter_sdk - -object ProductFields { - const val ID = "id" - const val STORE_ID = "store_id" - const val TYPE = "type" - const val DURATION = "duration" - const val SKU_DETAILS = "sku_details" - const val PRETTY_PRICE = "pretty_price" - const val TRIAL_DURATION = "trial_duration" - const val OFFERING_ID = "offering_id" -} - -object SkuDetailsFields { - const val ORIGINAL_JSON = "originalJson" -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/FlutterResult+CustomErrors.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/FlutterResult+CustomErrors.kt index 18d1d0ac..66b96622 100644 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/FlutterResult+CustomErrors.kt +++ b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/FlutterResult+CustomErrors.kt @@ -1,7 +1,7 @@ package com.qonversion.flutter.sdk.qonversion_flutter_sdk -import com.qonversion.android.sdk.QonversionError import io.flutter.plugin.common.MethodChannel +import io.qonversion.sandwich.SandwichError private const val passValidValue = "Please make sure you pass a valid value" @@ -37,9 +37,14 @@ fun MethodChannel.Result.noProductIdError() { return this.error("8", "Could not find productId value", "Please provide valid productId") } -fun MethodChannel.Result.qonversionError(error: QonversionError) { +fun MethodChannel.Result.sandwichError(error: SandwichError) { val errorDetails = getErrorDetails(error) - return this.error("9", error.description, errorDetails) + return this.error("9", error.code, errorDetails) +} + +fun MethodChannel.Result.purchaseError(error: SandwichError, isCancelled: Boolean) { + val errorDetails = getErrorDetails(error) + return this.error(if (isCancelled) "PurchaseCancelledByUser" else "9", error.code, errorDetails) } fun MethodChannel.Result.noNewProductIdError() { @@ -50,10 +55,6 @@ fun MethodChannel.Result.noOldProductIdError() { return this.error("11", "Could not find old product id", passValidValue) } -fun MethodChannel.Result.parsingError(message: String?) { - return this.error("12", "Arguments Parsing Error", message) -} - fun MethodChannel.Result.noProperty() { return this.error("13", "Could not find property", passValidValue) } @@ -62,7 +63,7 @@ fun MethodChannel.Result.noPropertyValue() { return this.error("14", "Could not find property value", passValidValue) } -fun MethodChannel.Result.offeringsError(error: QonversionError) { +fun MethodChannel.Result.offeringsError(error: SandwichError) { val errorDetails = getErrorDetails(error) return this.error("Offerings", "Could not get offerings. ${error.description}.", errorDetails) } @@ -83,8 +84,8 @@ fun MethodChannel.Result.jsonSerializationError(details: String?) { return this.error("JSONSerialization", "JSON Serialization Error", details) } -private fun getErrorDetails(error: QonversionError): String { - var result = "Qonversion Error Code: ${error.code}" +private fun getErrorDetails(error: SandwichError): String { + var result = error.description if (error.additionalMessage.isNotEmpty()) { result += ". Additional Message: ${error.additionalMessage}" } diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/Mapper.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/Mapper.kt deleted file mode 100644 index 262f9b59..00000000 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/Mapper.kt +++ /dev/null @@ -1,218 +0,0 @@ -package com.qonversion.flutter.sdk.qonversion_flutter_sdk - -import com.android.billingclient.api.SkuDetails -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException -import com.google.gson.reflect.TypeToken -import com.qonversion.android.sdk.QonversionError -import com.qonversion.android.sdk.QonversionErrorCode -import com.qonversion.android.sdk.automations.AutomationsEvent -import com.qonversion.android.sdk.automations.AutomationsEventType -import com.qonversion.android.sdk.automations.QActionResult -import com.qonversion.android.sdk.automations.QActionResultType -import com.qonversion.android.sdk.dto.QLaunchResult -import com.qonversion.android.sdk.dto.QPermission -import com.qonversion.android.sdk.dto.QPermissionsCacheLifetime -import com.qonversion.android.sdk.dto.eligibility.QEligibility -import com.qonversion.android.sdk.dto.eligibility.QIntroEligibilityStatus -import com.qonversion.android.sdk.dto.offerings.QOffering -import com.qonversion.android.sdk.dto.offerings.QOfferings -import com.qonversion.android.sdk.dto.products.QProduct -import com.qonversion.android.sdk.dto.products.QProductDuration -import com.qonversion.android.sdk.dto.products.QProductType -import com.qonversion.android.sdk.dto.products.QTrialDuration - -data class PurchaseResult(val permissions: Map? = null, val error: QonversionError? = null) { - fun toMap(): Map { - val isUserCancelled = error?.code == QonversionErrorCode.CanceledPurchase - return mapOf( - "permissions" to permissions?.mapValues { it.value.toMap() }, - "error" to error.toMap(), - "is_cancelled" to isUserCancelled - ) - } -} - -fun QonversionError?.toMap(): Map? { - if (this == null) return null - - return mapOf("code" to code.toString(), - "description" to description, - "additionalMessage" to additionalMessage) -} - -fun QLaunchResult.toMap(): Map { - return mapOf( - "uid" to uid, - "timestamp" to date.time.toDouble(), - "products" to products.mapValues { it.value.toMap() }, - "permissions" to permissions.mapValues { it.value.toMap() }, - "user_products" to userProducts.mapValues { it.value.toMap() } - ) -} - -fun QProduct.toMap(): Map { - return mapOf( - ProductFields.ID to qonversionID, - ProductFields.STORE_ID to storeID, - ProductFields.TYPE to type.type, - ProductFields.DURATION to duration?.type, - ProductFields.SKU_DETAILS to skuDetail?.toMap(), - ProductFields.PRETTY_PRICE to prettyPrice, - ProductFields.TRIAL_DURATION to trialDuration?.type, - ProductFields.OFFERING_ID to offeringID - ) -} - -fun QPermission.toMap(): Map { - return mapOf( - "id" to permissionID, - "associated_product" to productID, - "renew_state" to renewState.type, - "started_timestamp" to startedDate.time.toDouble(), - "expiration_timestamp" to expirationDate?.time?.toDouble(), - "active" to isActive() - ) -} - -fun QOfferings.toMap(): Map { - return mapOf( - "main" to main?.toMap(), - "available_offerings" to availableOfferings.map { it.toMap() } - ) -} - -fun QOffering.toMap(): Map { - return mapOf( - "id" to offeringID, - "tag" to tag.tag, - "products" to products.map { it.toMap() } - ) -} - -fun QEligibility.toMap(): Map { - return mapOf("status" to status.toInt()) -} - -fun QIntroEligibilityStatus.toInt(): Int { - return when (this) { - QIntroEligibilityStatus.Unknown -> 0 - QIntroEligibilityStatus.NonIntroProduct -> 1 - QIntroEligibilityStatus.Ineligible -> 2 - QIntroEligibilityStatus.Eligible -> 3 - } -} - -fun SkuDetails.toMap(): Map { - return mapOf( - "title" to title, - "description" to description, - "freeTrialPeriod" to freeTrialPeriod, - "introductoryPrice" to introductoryPrice, - "introductoryPriceAmountMicros" to introductoryPriceAmountMicros, - "introductoryPriceCycles" to introductoryPriceCycles, - "introductoryPricePeriod" to introductoryPricePeriod, - "price" to price, - "priceAmountMicros" to priceAmountMicros, - "priceCurrencyCode" to priceCurrencyCode, - "sku" to sku, - "type" to type, - "subscriptionPeriod" to subscriptionPeriod, - "originalPrice" to originalPrice, - "originalPriceAmountMicros" to originalPriceAmountMicros, - SkuDetailsFields.ORIGINAL_JSON to originalJson - ) -} - -@Throws(JsonSyntaxException::class, IllegalArgumentException::class, ClassCastException::class) -fun mapQProduct(jsonProduct: String): QProduct? { - val mapType = object : TypeToken>() {}.type - val mappedProduct: Map = Gson().fromJson(jsonProduct, mapType) - - val qonversionId = mappedProduct[ProductFields.ID] as? String ?: return null - - val storeId = mappedProduct[ProductFields.STORE_ID] as? String - - val type = mappedProduct[ProductFields.TYPE] as Double - val productType = QProductType.fromType(type.toInt()) - - val duration = mappedProduct[ProductFields.DURATION] as? Double - val productDuration = duration?.toInt()?.let { QProductDuration.fromType(it) } - - val prettyPrice = mappedProduct[ProductFields.PRETTY_PRICE] as? String - - val trialDuration = mappedProduct[ProductFields.TRIAL_DURATION] as? Double - val productTrialDuration = trialDuration?.toInt()?.let { QTrialDuration.fromType(it) } - - val offeringId = mappedProduct[ProductFields.OFFERING_ID] as? String - - val originalSkuDetails = mappedProduct[SkuDetailsFields.ORIGINAL_JSON] as? String - val skuDetails = originalSkuDetails?.let { SkuDetails(it) } - - return QProduct(qonversionId, storeId, productType, productDuration).also { - it.skuDetail = skuDetails - it.offeringID = offeringId - it.prettyPrice = prettyPrice - it.trialDuration = productTrialDuration - } -} - -fun QActionResult.toMap(): Map { - return mapOf("action_type" to type.toInt(), - "parameters" to value, - "error" to error.toMap()) -} - -fun QActionResultType.toInt(): Int { - return when (this) { - QActionResultType.Unknown -> 0 - QActionResultType.Url -> 1 - QActionResultType.DeepLink -> 2 - QActionResultType.Navigation -> 3 - QActionResultType.Purchase -> 4 - QActionResultType.Restore -> 5 - QActionResultType.Close -> 6 - } -} - -fun AutomationsEvent.toMap(): Map { - return mapOf("event_type" to type.toInt(), - "date" to date.time.toDouble()) -} - -fun AutomationsEventType.toInt(): Int { - return when (this) { - AutomationsEventType.Unknown -> 0 - AutomationsEventType.TrialStarted -> 1 - AutomationsEventType.TrialConverted -> 2 - AutomationsEventType.TrialCanceled -> 3 - AutomationsEventType.TrialBillingRetry -> 4 - AutomationsEventType.SubscriptionStarted -> 5 - AutomationsEventType.SubscriptionRenewed -> 6 - AutomationsEventType.SubscriptionRefunded -> 7 - AutomationsEventType.SubscriptionCanceled -> 8 - AutomationsEventType.SubscriptionBillingRetry -> 9 - AutomationsEventType.InAppPurchase -> 10 - AutomationsEventType.SubscriptionUpgraded -> 11 - AutomationsEventType.TrialStillActive -> 12 - AutomationsEventType.TrialExpired -> 13 - AutomationsEventType.SubscriptionExpired -> 14 - AutomationsEventType.SubscriptionDowngraded -> 15 - AutomationsEventType.SubscriptionProductChanged -> 16 - } -} - -fun parsePermissionsCacheLifetime(lifetime: String): QPermissionsCacheLifetime? { - val lifetimes = mapOf( - "Week" to QPermissionsCacheLifetime.WEEK, - "TwoWeeks" to QPermissionsCacheLifetime.TWO_WEEKS, - "Month" to QPermissionsCacheLifetime.MONTH, - "TwoMonths" to QPermissionsCacheLifetime.TWO_MONTHS, - "ThreeMonths" to QPermissionsCacheLifetime.THREE_MONTHS, - "SixMonths" to QPermissionsCacheLifetime.SIX_MONTHS, - "Year" to QPermissionsCacheLifetime.YEAR, - "Unlimited" to QPermissionsCacheLifetime.UNLIMITED - ) - - return lifetimes[lifetime] -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt index 91ce94cc..c0bb5553 100644 --- a/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt +++ b/android/src/main/kotlin/com/qonversion/flutter/sdk/qonversion_flutter_sdk/QonversionFlutterSdkPlugin.kt @@ -2,8 +2,6 @@ package com.qonversion.flutter.sdk.qonversion_flutter_sdk import android.app.Activity import android.app.Application -import android.preference.PreferenceManager -import android.util.Log import androidx.annotation.NonNull import com.google.gson.Gson import com.google.gson.JsonSyntaxException @@ -23,7 +21,7 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.Registrar -import java.lang.Exception +import io.qonversion.sandwich.* /** QonversionFlutterSdkPlugin */ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { @@ -33,6 +31,26 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa private var deferredPurchasesStreamHandler: BaseEventStreamHandler? = null private var automationsPlugin: AutomationsPlugin? = null + private val qonversionSandwich by lazy { + application?.let { + QonversionSandwich( + it, + object : ActivityProvider { + override val currentActivity: Activity? + get() = activity + }, + qonversionEventsListener + ) + } ?: throw IllegalStateException("Failed to initialize Qonversion Sandwich. Application is null.") + } + + private val qonversionEventsListener: QonversionEventsListener = object : QonversionEventsListener { + override fun onPermissionsUpdate(permissions: BridgeData) { + val payload = Gson().toJson(permissions) + deferredPurchasesStreamHandler?.eventSink?.success(payload) + } + } + companion object { const val METHOD_CHANNEL = "qonversion_flutter_sdk" const val EVENT_CHANNEL_PROMO_PURCHASES = "promo_purchases" @@ -88,14 +106,14 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa return restore(result) } "setDebugMode" -> { - Qonversion.setDebugMode() + qonversionSandwich.setDebugMode() return result.success(null) } "offerings" -> { return offerings(result) } "logout" -> { - Qonversion.logout() + qonversionSandwich.logout() return result.success(null) } } @@ -107,7 +125,7 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa when (call.method) { "launch" -> launch(args, result) "purchase" -> purchase(args["productId"] as? String, result) - "purchaseProduct" -> purchaseProduct(args["product"] as? String, result) + "purchaseProduct" -> purchaseProduct(args, result) "updatePurchase" -> updatePurchase(args, result) "updatePurchaseWithProduct" -> updatePurchaseWithProduct(args, result) "setProperty" -> setProperty(args, result) @@ -124,27 +142,9 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa } private fun launch(args: Map, result: Result) { - application?.let { application -> - val apiKey = args["key"] as? String ?: return result.noApiKeyError() - val isObserveMode = args["isObserveMode"] as? Boolean ?: return result.noArgsError() - Qonversion.launch( - application, - apiKey, - isObserveMode, - callback = object : QonversionLaunchCallback { - override fun onSuccess(launchResult: QLaunchResult) { - result.success(launchResult.toMap()) - } - - override fun onError(error: QonversionError) { - result.qonversionError(error) - } - } - ) - startListeningPendingPurchasesEvents() - automationsPlugin?.setAutomationsDelegate() - } - ?: result.error(QonversionErrorCode.UnknownError.name, "Couldn't launch Qonversion. There is no Application context", null) + val projectKey = args["key"] as? String ?: return result.noApiKeyError() + val isObserveMode = args["isObserveMode"] as? Boolean ?: return result.noArgsError() + qonversionSandwich.launch(projectKey, isObserveMode, result.toResultListener()) } private fun identify(userId: String?, result: Result) { @@ -153,51 +153,23 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa return } - Qonversion.identify(userId) + qonversionSandwich.identify(userId) result.success(null) } - private fun startListeningPendingPurchasesEvents() { - Qonversion.setUpdatedPurchasesListener(object : UpdatedPurchasesListener { - override fun onPermissionsUpdate(permissions: Map) { - val payload = Gson().toJson(permissions.mapValues { it.value.toMap() }) - - deferredPurchasesStreamHandler?.eventSink?.success(payload) - } - }) - } - private fun purchase(productId: String?, result: Result) { if (productId == null) { return result.noProductIdError() } - activity?.let { - Qonversion.purchase(it, productId, getPurchasesListener(result)) - } ?: handleMissingActivityOnPurchase(result, object {}.javaClass.enclosingMethod?.name) + qonversionSandwich.purchase(productId, result.toPurchaseResultListener()) } - private fun purchaseProduct(jsonProduct: String?, result: Result) { - if (jsonProduct == null) { - return result.noProduct() - } + private fun purchaseProduct(args: Map, result: Result) { + val productId = args["productId"] as? String ?: return result.noProductIdError() + val offeringId = args["offeringId"] as? String - val funcName = object {}.javaClass.enclosingMethod?.name - try { - val product = mapQProduct(jsonProduct) - ?: return handleMissingProductIdField(result, funcName) - - activity?.let { - Qonversion.purchase(it, product, getPurchasesListener(result)) - } ?: handleMissingActivityOnPurchase(result, funcName) - - } catch (e: JsonSyntaxException) { - handleJsonExceptionOnPurchase(result, e, funcName) - } catch (e: IllegalArgumentException) { - handleExceptionOnPurchase(result, e, funcName) - } catch (e: ClassCastException) { - handleExceptionOnPurchase(result, e, funcName) - } + qonversionSandwich.purchaseProduct(productId, offeringId, result.toPurchaseResultListener()) } private fun updatePurchase(args: Map, result: Result) { @@ -205,106 +177,68 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa val oldProductId = args["oldProductId"] as? String ?: return result.noOldProductIdError() val prorationMode = args["proration_mode"] as? Int - activity?.let { - Qonversion.updatePurchase(it, newProductId, oldProductId, prorationMode, getUpdatePurchasesListener(result)) - } ?: handleMissingActivityOnPurchase(result, object {}.javaClass.enclosingMethod?.name) + qonversionSandwich.updatePurchase(newProductId, oldProductId, prorationMode, result.toPurchaseResultListener()) } private fun updatePurchaseWithProduct(args: Map, result: Result) { - val jsonProduct = args["product"] as? String ?: return result.noProduct() + val newProductId = args["newProductId"] as? String ?: return result.noNewProductIdError() + val offeringId = args["offeringId"] as? String val oldProductId = args["oldProductId"] as? String ?: return result.noOldProductIdError() val prorationMode = args["proration_mode"] as? Int - val funcName = object {}.javaClass.enclosingMethod?.name - try { - val product = mapQProduct(jsonProduct) - ?: return handleMissingProductIdField(result, funcName) - - activity?.let { - Qonversion.updatePurchase(it, product, oldProductId, prorationMode, getUpdatePurchasesListener(result)) - } ?: handleMissingActivityOnPurchase(result, funcName) - } catch (e: JsonSyntaxException) { - handleJsonExceptionOnPurchase(result, e, funcName) - } catch (e: IllegalArgumentException) { - handleExceptionOnPurchase(result, e, funcName) - } catch (e: ClassCastException) { - handleExceptionOnPurchase(result, e, funcName) - } + qonversionSandwich.updatePurchaseWithProduct( + newProductId, + offeringId, + oldProductId, + prorationMode, + result.toPurchaseResultListener() + ) } private fun checkPermissions(result: Result) { - Qonversion.checkPermissions(object : QonversionPermissionsCallback { - override fun onSuccess(permissions: Map) { - result.success(permissions.mapValues { it.value.toMap() }) - } - - override fun onError(error: QonversionError) { - result.qonversionError(error) - } - }) + qonversionSandwich.checkPermissions(result.toResultListener()) } private fun restore(result: Result) { - Qonversion.restore(object : QonversionPermissionsCallback { - override fun onSuccess(permissions: Map) { - result.success(permissions.mapValues { it.value.toMap() }) - } - - override fun onError(error: QonversionError) { - result.qonversionError(error) - } - }) + qonversionSandwich.restore(result.toResultListener()) } private fun offerings(result: Result) { - Qonversion.offerings(callback = object : QonversionOfferingsCallback { - override fun onSuccess(offerings: QOfferings) { - val jsonOfferings = Gson().toJson(offerings.toMap()) - result.success(jsonOfferings) - } - - override fun onError(error: QonversionError) { - result.offeringsError(error) + qonversionSandwich.offerings( + object : ResultListener { + override fun onError(error: SandwichError) { + result.offeringsError(error) + } + + override fun onSuccess(data: Map) { + result.success(Gson().toJson(data)) + } } - }) + ) } private fun products(result: Result) { - Qonversion.products(callback = object : QonversionProductsCallback { - override fun onSuccess(products: Map) { - result.success(products.mapValues { it.value.toMap() }) - } - - override fun onError(error: QonversionError) { - result.qonversionError(error) - } - }) + qonversionSandwich.products(result.toResultListener()) } private fun setProperty(args: Map, result: Result) { val rawProperty = args["property"] as? String ?: return result.noProperty() - val value = args["value"] as? String ?: return result.noPropertyValue() - try { - Qonversion.setProperty(QUserProperties.valueOf(rawProperty), value) - result.success(null) - } catch (e: IllegalArgumentException) { - result.parsingError(e.localizedMessage) - } + qonversionSandwich.setDefinedProperty(rawProperty, value) + result.success(null) } private fun setUserProperty(args: Map, result: Result) { val property = args["property"] as? String ?: return result.noProperty() - val value = args["value"] as? String ?: return result.noPropertyValue() - Qonversion.setUserProperty(property, value) + qonversionSandwich.setCustomProperty(property, value) result.success(null) } private fun syncPurchases(result: Result) { - Qonversion.syncPurchases() + qonversionSandwich.syncPurchases() result.success(null) } @@ -317,28 +251,20 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa val provider = args["provider"] as? String ?: return result.noProviderError() - val castedProvider = when (provider) { - "appsFlyer" -> AttributionSource.AppsFlyer - else -> null - } - ?: return result.success(null) - - Qonversion.attribution(data, castedProvider) - + qonversionSandwich.addAttributionData(provider, data) result.success(null) } private fun checkTrialIntroEligibility(args: Map, result: Result) { val ids = args["ids"] as? List ?: return result.noDataError() - Qonversion.checkTrialIntroEligibilityForProductIds(ids, callback = object : QonversionEligibilityCallback { - override fun onSuccess(eligibilities: Map) { - val jsonEligibilities = Gson().toJson(eligibilities.mapValues { it.value.toMap() }) - result.success(jsonEligibilities) + qonversionSandwich.checkTrialIntroEligibility(ids, object : ResultListener { + override fun onError(error: SandwichError) { + result.sandwichError(error) } - override fun onError(error: QonversionError) { - result.qonversionError(error) + override fun onSuccess(data: Map) { + result.success(Gson().toJson(data)) } }) } @@ -346,18 +272,13 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa private fun setPermissionsCacheLifetime(args: Map, result: Result) { val rawLifetime = args["lifetime"] as? String ?: return result.noLifetime() - val lifetime = parsePermissionsCacheLifetime(rawLifetime) ?: run { - result.parsingError("No permissions cache lifetime associated with the provided value: $rawLifetime") - return - } - - Qonversion.setPermissionsCacheLifetime(lifetime) + qonversionSandwich.setPermissionsCacheLifetime(rawLifetime) result.success(null) } private fun setNotificationsToken(token: String?, result: Result) { token?.let { - Qonversion.setNotificationsToken(it) + qonversionSandwich.setNotificationToken(it) result.success(null) } ?: result.noArgsError() } @@ -369,62 +290,15 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa return result.noDataError() } - val stringsMap: Map = data.mapValues { it.value.toString() } - val isQonversionNotification = Qonversion.handleNotification(stringsMap) + val isQonversionNotification = qonversionSandwich.handleNotification(data) result.success(isQonversionNotification) } - private fun getPurchasesListener(result: Result) = object : QonversionPermissionsCallback { - override fun onSuccess(permissions: Map) = - result.success(PurchaseResult(permissions).toMap()) - - override fun onError(error: QonversionError) = - result.success(PurchaseResult(error = error).toMap()) - } - - private fun getUpdatePurchasesListener(result: Result) = object : QonversionPermissionsCallback { - override fun onSuccess(permissions: Map) = - result.success(permissions.mapValues { it.value.toMap() }) - - override fun onError(error: QonversionError) = - result.qonversionError(error) - } - - private fun handleMissingProductIdField(result: Result, functionName: String?) { - val errorMessage = "Failed to deserialize Qonversion Product. There is no qonversionId" - Log.d("Qonversion", "$functionName() -> $errorMessage") - result.noProductIdField(errorMessage) - } - - private fun handleMissingActivityOnPurchase(result: Result, functionName: String?) { - val errorMessage = "Couldn't make a purchase. There is no Activity context" - Log.d("Qonversion", "$functionName() -> $errorMessage") - result.error(QonversionErrorCode.PurchaseInvalid.name, errorMessage, null) - } - - private fun handleExceptionOnPurchase(result: Result, e: Exception, functionName: String?) { - val errorMessage = "Couldn't make a purchase as an Exception occurred. ${e.localizedMessage}." - Log.d("Qonversion", "$functionName() -> $errorMessage") - result.error(QonversionErrorCode.PurchaseInvalid.name, errorMessage, null) - } - - private fun handleJsonExceptionOnPurchase(result: Result, e: JsonSyntaxException, functionName: String?) { - val errorMessage = "Failed to deserialize Qonversion Product: ${e.localizedMessage}." - Log.d("Qonversion", "$functionName() -> $errorMessage") - result.jsonSerializationError(errorMessage) - } - private fun storeSdkInfo(args: Map, result: Result) { val version = args["version"] as? String ?: return result.noSdkInfo() - val versionKey = args["versionKey"] as? String ?: return result.noSdkInfo() val source = args["source"] as? String ?: return result.noSdkInfo() - val sourceKey = args["sourceKey"] as? String ?: return result.noSdkInfo() - - val editor = PreferenceManager.getDefaultSharedPreferences(application).edit() - editor.putString(sourceKey, source) - editor.putString(versionKey, version) - editor.apply() + qonversionSandwich.storeSdkInfo(source, version) result.success(null) } @@ -441,9 +315,8 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa // Register promo purchases events. Android SDK does not generate any promo purchases yet val promoPurchasesListener = BaseListenerWrapper(messenger, EVENT_CHANNEL_PROMO_PURCHASES) promoPurchasesListener.register() - automationsPlugin = AutomationsPlugin().apply { - register(messenger) - } + + automationsPlugin = AutomationsPlugin(messenger).also { it.subscribe() } } private fun tearDown() { @@ -452,4 +325,28 @@ class QonversionFlutterSdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAwa this.deferredPurchasesStreamHandler = null this.application = null } + + private fun Result.toResultListener(): ResultListener { + return object : ResultListener { + override fun onError(error: SandwichError) { + sandwichError(error) + } + + override fun onSuccess(data: Map) { + success(data) + } + } + } + + private fun Result.toPurchaseResultListener(): PurchaseResultListener { + return object : PurchaseResultListener { + override fun onError(error: SandwichError, isCancelled: Boolean) { + purchaseError(error, isCancelled) + } + + override fun onSuccess(data: Map) { + success(data) + } + } + } } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index b33de893..f35ba1ac 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -41,7 +41,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.qonversion.sample" minSdkVersion 19 - targetSdkVersion 28 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index e04a4f83..de3553b5 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -18,7 +18,8 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true">