diff --git a/CHANGELOG.md b/CHANGELOG.md index 0179010..b4f0973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Flutter/releases) on GitHub. +## 1.3.5 + +### Enhancements + +- Upgrades iOS SDK to 3.12.0 [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.12.0) +- Updates Android SDK to 1.5.0 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.5.0) + +### Fixes +- Fixes issue with `PaywallInfoBridge` and other bridges throwing NPE when reattaching to activities from deep sleep. + ## 1.3.4 ### Enhancements @@ -24,7 +34,7 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw ### Enhancements - Upgrades Android SDK to 1.3.0 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.3.0) -- Upgrades Android SDK to 3.10.1 [View Android SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.10.1) +- Upgrades iOS SDK to 3.10.1 [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.10.1) - Adds `confirmAllAssignments` method to `Superwall` which confirms assignments for all placements and returns an array of all confirmed experiment assignments. Note that the assignments may be different when a placement is registered due to changes in user, placement, or device parameters used in audience filters. ## 1.3.0 diff --git a/android/build.gradle b/android/build.gradle index 76af696..5c36360 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -74,6 +74,7 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' - implementation "com.superwall.sdk:superwall-android:1.3.1" + implementation "com.superwall.sdk:superwall-android:1.5.0" implementation 'com.android.billingclient:billing:6.1.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' } \ No newline at end of file diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/BridgingCreator.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/BridgingCreator.kt index 2b2bf47..1d2dc14 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/BridgingCreator.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/BridgingCreator.kt @@ -7,57 +7,70 @@ import com.superwall.superwallkit_flutter.bridges.Communicator import com.superwall.superwallkit_flutter.bridges.bridgeClass import com.superwall.superwallkit_flutter.bridges.generateBridgeId import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.filterNotNull import java.util.concurrent.ConcurrentHashMap import io.flutter.embedding.android.FlutterActivity +import kotlinx.coroutines.flow.first -class BridgingCreator(val flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : MethodCallHandler { +class BridgingCreator( + val flutterPluginBinding: suspend () -> FlutterPlugin.FlutterPluginBinding, + val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) +) : MethodCallHandler { private val instances: MutableMap = ConcurrentHashMap() - object Constants { } + object Constants {} companion object { private var _shared: BridgingCreator? = null val shared: BridgingCreator get() = _shared ?: throw IllegalStateException("BridgingCreator not initialized") - private var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null - var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? - get() = _flutterPluginBinding - set(value) { - // Only allow binding to occur once. It appears that if binding is set multiple - // times (due to other SDK interference), we'll lose access to the - // SuperwallKitFlutterPlugin current activity - if (_flutterPluginBinding != null) { - println("WARNING: Attempting to set a flutter plugin binding again.") - return - } + private var _flutterPluginBinding: MutableStateFlow = + MutableStateFlow(null) - // Store for getter + suspend fun waitForPlugin() : FlutterPlugin.FlutterPluginBinding { + return _flutterPluginBinding.filterNotNull().first() + } + fun setFlutterPlugin(binding: FlutterPlugin.FlutterPluginBinding) { + // Only allow binding to occur once. It appears that if binding is set multiple + // times (due to other SDK interference), we'll lose access to the + // SuperwallKitFlutterPlugin current activity + if (_flutterPluginBinding.value != null) { + println("WARNING: Attempting to set a flutter plugin binding again.") + return + } - _flutterPluginBinding = value + // Store for getter - _flutterPluginBinding?.let { - synchronized(BridgingCreator::class.java) { - val bridge = BridgingCreator(it) - _shared = bridge - val communicator = Communicator(it.binaryMessenger, "SWK_BridgingCreator") - communicator.setMethodCallHandler(bridge) - } + + binding?.let { + synchronized(BridgingCreator::class.java) { + val bridge = BridgingCreator({ waitForPlugin() }) + _shared = bridge + _flutterPluginBinding.value = binding + val communicator = Communicator(binding.binaryMessenger, "SWK_BridgingCreator") + communicator.setMethodCallHandler(bridge) } } + + } } fun tearDown() { print("Did tearDown BridgingCreator") _shared = null - _flutterPluginBinding = null + _flutterPluginBinding.value = null } // Generic function to retrieve a bridge instance - fun bridgeInstance(bridgeId: BridgeId): T? { + suspend fun bridgeInstance(bridgeId: BridgeId): T? { BreadCrumbs.append("BridgingCreator.kt: Searching for $bridgeId among ${instances.count()}: ${instances.toFormattedString()}") var instance = instances[bridgeId] as? T @@ -70,25 +83,32 @@ class BridgingCreator(val flutterPluginBinding: FlutterPlugin.FlutterPluginBindi } override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "createBridgeInstance" -> { - val bridgeId = call.argument("bridgeId") - val initializationArgs = call.argument>("args") - - if (bridgeId != null) { - createBridgeInstanceFromBridgeId(bridgeId, initializationArgs) - result.success(null) - } else { - println("WARNING: Unable to create bridge") - result.badArgs(call) + scope.launch { + + when (call.method) { + "createBridgeInstance" -> { + val bridgeId = call.argument("bridgeId") + val initializationArgs = call.argument>("args") + + if (bridgeId != null) { + createBridgeInstanceFromBridgeId(bridgeId, initializationArgs) + result.success(null) + } else { + println("WARNING: Unable to create bridge") + result.badArgs(call) + } } + + else -> result.notImplemented() } - else -> result.notImplemented() } } // Create the bridge instance as instructed from Dart - private fun createBridgeInstanceFromBridgeId(bridgeId: BridgeId, initializationArgs: Map?): BridgeInstance { + private suspend fun createBridgeInstanceFromBridgeId( + bridgeId: BridgeId, + initializationArgs: Map? + ): BridgeInstance { // An existing bridge instance might exist if it were created natively, instead of from Dart val existingBridgeInstance = instances[bridgeId] existingBridgeInstance?.let { @@ -96,10 +116,14 @@ class BridgingCreator(val flutterPluginBinding: FlutterPlugin.FlutterPluginBindi } // Create an instance of the bridge - val bridgeInstance = bridgeInitializers[bridgeId.bridgeClass()]?.invoke(flutterPluginBinding.applicationContext, bridgeId, initializationArgs) + val bridgeInstance = bridgeInitializers[bridgeId.bridgeClass()]?.invoke( + flutterPluginBinding().applicationContext, + bridgeId, + initializationArgs + ) bridgeInstance?.let { bridgeInstance -> instances[bridgeId] = bridgeInstance - bridgeInstance.communicator.setMethodCallHandler(bridgeInstance) + bridgeInstance.communicator().setMethodCallHandler(bridgeInstance) return bridgeInstance } ?: run { throw AssertionError("Unable to find a bridge initializer for ${bridgeId}. Make sure to add to BridgingCreator+Constants.kt.}") @@ -107,7 +131,10 @@ class BridgingCreator(val flutterPluginBinding: FlutterPlugin.FlutterPluginBindi } // Create the bridge instance as instructed from native - fun createBridgeInstanceFromBridgeClass(bridgeClass: BridgeClass, initializationArgs: Map? = null): BridgeInstance { + suspend fun createBridgeInstanceFromBridgeClass( + bridgeClass: BridgeClass, + initializationArgs: Map? = null + ): BridgeInstance { return createBridgeInstanceFromBridgeId(bridgeClass.generateBridgeId(), initializationArgs) } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/SuperwallkitFlutterPlugin.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/SuperwallkitFlutterPlugin.kt index 9c00979..83919ec 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/SuperwallkitFlutterPlugin.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/SuperwallkitFlutterPlugin.kt @@ -36,7 +36,7 @@ class SuperwallkitFlutterPlugin : FlutterPlugin, ActivityAware { } override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - BridgingCreator.flutterPluginBinding = flutterPluginBinding + BridgingCreator.setFlutterPlugin(flutterPluginBinding) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { @@ -70,14 +70,14 @@ fun MethodCall.argumentForKey(key: String): T? { } // Make sure to provide the key for the bridge (which provides the bridgeId) -fun MethodCall.bridgeInstance(key: String): T? { +suspend fun MethodCall.bridgeInstance(key: String): T? { BreadCrumbs.append("SuperwallKitFlutterPlugin.kt: Invoke bridgeInstance(key:) on $this. Key is $key") val bridgeId = this.argument(key) ?: return null BreadCrumbs.append("SuperwallKitFlutterPlugin.kt: Invoke bridgeInstance(key:) in on $this. Found bridgeId $bridgeId") return BridgingCreator.shared.bridgeInstance(bridgeId) } -fun BridgeId.bridgeInstance(): T? { +suspend fun BridgeId.bridgeInstance(): T? { BreadCrumbs.append("SuperwallKitFlutterPlugin.kt: Invoke bridgeInstance() in on $this") return BridgingCreator.shared.bridgeInstance(this) } diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/BridgeInstance.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/BridgeInstance.kt index 6d058f4..6aa04fc 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/BridgeInstance.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/BridgeInstance.kt @@ -5,7 +5,12 @@ import com.superwall.superwallkit_flutter.BridgingCreator import com.superwall.superwallkit_flutter.setBridgeId import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first typealias BridgeClass = String typealias BridgeId = String @@ -14,19 +19,34 @@ typealias Communicator = MethodChannel abstract class BridgeInstance( val context: Context, val bridgeId: BridgeId, - val initializationArgs: Map? = null + val initializationArgs: Map? = null, ) : MethodChannel.MethodCallHandler { + private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.IO) + companion object { fun bridgeClass(): BridgeClass { throw NotImplementedError("Subclasses must implement") } } - val communicator: Communicator by lazy { - val communicator = MethodChannel(BridgingCreator.shared.flutterPluginBinding.binaryMessenger, bridgeId) - communicator.setBridgeId(bridgeId); - communicator + private var communicatorFlow: MutableStateFlow = MutableStateFlow(null) + + suspend fun communicator(): Communicator { + synchronized(this@BridgeInstance) { + if (communicatorFlow.value == null) { + mainScope.launch { + + val communicator = MethodChannel( + BridgingCreator.shared.flutterPluginBinding().binaryMessenger, + bridgeId + ) + communicator.setBridgeId(bridgeId); + communicatorFlow.value = communicator + } + } + } + return communicatorFlow.filterNotNull().first() } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {} diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/CompletionBlockProxyBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/CompletionBlockProxyBridge.kt index 3c857c9..66053f0 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/CompletionBlockProxyBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/CompletionBlockProxyBridge.kt @@ -10,7 +10,7 @@ class CompletionBlockProxyBridge( ) : BridgeInstance(context, bridgeId, initializationArgs) { companion object { fun bridgeClass(): BridgeClass = "CompletionBlockProxyBridge" } - fun callCompletionBlock(value: Any? = null) { - communicator.invokeMethodOnMain("callCompletionBlock", value) + suspend fun callCompletionBlock(value: Any? = null) { + communicator().invokeMethodOnMain("callCompletionBlock", value) } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ConfigurationStatusBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ConfigurationStatusBridge.kt index 954bdca..3b40292 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ConfigurationStatusBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ConfigurationStatusBridge.kt @@ -51,7 +51,7 @@ class ConfigurationStatusConfiguredBridge(context: Context, bridgeId: BridgeId) } -fun ConfigurationStatus.createBridgeId(): BridgeId { +suspend fun ConfigurationStatus.createBridgeId(): BridgeId { val bridgeClass = when (this) { is ConfigurationStatus.Pending -> ConfigurationStatusPendingBridge.bridgeClass() diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ExperimentBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ExperimentBridge.kt index 370a97e..025410a 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ExperimentBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/ExperimentBridge.kt @@ -35,7 +35,7 @@ class ExperimentBridge( } } -fun Experiment.createBridgeId(): BridgeId { +suspend fun Experiment.createBridgeId(): BridgeId { val bridgeInstance = (BridgingCreator.shared.createBridgeInstanceFromBridgeClass( bridgeClass = ExperimentBridge.bridgeClass(), initializationArgs = mapOf("experiment" to this) diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallInfoBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallInfoBridge.kt index 5fcaf9d..da8e668 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallInfoBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallInfoBridge.kt @@ -5,6 +5,9 @@ import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.superwallkit_flutter.BridgingCreator import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import toJson class PaywallInfoBridge( @@ -12,53 +15,62 @@ class PaywallInfoBridge( bridgeId: BridgeId, initializationArgs: Map? = null ) : BridgeInstance(context, bridgeId, initializationArgs) { - companion object { fun bridgeClass(): BridgeClass = "PaywallInfoBridge" } + companion object { + fun bridgeClass(): BridgeClass = "PaywallInfoBridge" + } + lateinit var paywallInfo: PaywallInfo + val scope = CoroutineScope(Dispatchers.IO) init { val paywallInfoArg = initializationArgs?.get("paywallInfo") as? PaywallInfo - paywallInfo = paywallInfoArg ?: throw IllegalArgumentException("Attempting to create `PaywallInfoBridge` without providing `paywallInfo`.") + paywallInfo = paywallInfoArg + ?: throw IllegalArgumentException("Attempting to create `PaywallInfoBridge` without providing `paywallInfo`.") } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "getName" -> result.success(paywallInfo.name) - "getIdentifier" -> result.success(paywallInfo.identifier) - "getExperimentBridgeId" -> result.success(paywallInfo.experiment?.createBridgeId()) - "getTriggerSessionId" -> result.success(paywallInfo.triggerSessionId) - "getProducts" -> result.success(paywallInfo.products.map { it.toJson() }) - "getProductIds" -> result.success(paywallInfo.productIds) - "getUrl" -> result.success(paywallInfo.url.toString()) - "getPresentedByEventWithName" -> result.success(paywallInfo.presentedByEventWithName) - "getPresentedByEventWithId" -> result.success(paywallInfo.presentedByEventWithId) - "getPresentedByEventAt" -> result.success(paywallInfo.presentedByEventAt) - "getPresentedBy" -> result.success(paywallInfo.presentedBy) - "getPresentationSourceType" -> result.success(paywallInfo.presentationSourceType) - "getResponseLoadStartTime" -> result.success(paywallInfo.responseLoadStartTime) - "getResponseLoadCompleteTime" -> result.success(paywallInfo.responseLoadCompleteTime) - "getResponseLoadFailTime" -> result.success(paywallInfo.responseLoadFailTime) - "getResponseLoadDuration" -> result.success(paywallInfo.responseLoadDuration) - "getWebViewLoadStartTime" -> result.success(paywallInfo.webViewLoadStartTime) - "getWebViewLoadCompleteTime" -> result.success(paywallInfo.webViewLoadCompleteTime) - "getWebViewLoadFailTime" -> result.success(paywallInfo.webViewLoadFailTime) - "getWebViewLoadDuration" -> result.success(paywallInfo.webViewLoadDuration) - "getProductsLoadStartTime" -> result.success(paywallInfo.productsLoadStartTime) - "getProductsLoadCompleteTime" -> result.success(paywallInfo.productsLoadCompleteTime) - "getProductsLoadFailTime" -> result.success(paywallInfo.productsLoadFailTime) - "getProductsLoadDuration" -> result.success(paywallInfo.productsLoadDuration) - "getPaywalljsVersion" -> result.success(paywallInfo.paywalljsVersion) - "getIsFreeTrialAvailable" -> result.success(paywallInfo.isFreeTrialAvailable) - "getFeatureGatingBehavior" -> result.success(paywallInfo.featureGatingBehavior.toJson()) - "getCloseReason" -> result.success(paywallInfo.closeReason.toJson()) - "getLocalNotifications" -> result.success(paywallInfo.localNotifications.map { it.toJson() }) - "getComputedPropertyRequests" -> result.success(paywallInfo.computedPropertyRequests.map { it.toJson() }) - "getSurveys" -> result.success(paywallInfo.surveys.map { it.toJson() }) - else -> result.notImplemented() + scope.launch { + + when (call.method) { + "getName" -> result.success(paywallInfo.name) + "getIdentifier" -> result.success(paywallInfo.identifier) + "getExperimentBridgeId" -> result.success(paywallInfo.experiment?.createBridgeId()) + "getTriggerSessionId" -> result.success(paywallInfo.triggerSessionId) + "getProducts" -> result.success(paywallInfo.products.map { it.toJson() }) + "getProductIds" -> result.success(paywallInfo.productIds) + "getUrl" -> result.success(paywallInfo.url.toString()) + "getPresentedByEventWithName" -> result.success(paywallInfo.presentedByEventWithName) + "getPresentedByEventWithId" -> result.success(paywallInfo.presentedByEventWithId) + "getPresentedByEventAt" -> result.success(paywallInfo.presentedByEventAt) + "getPresentedBy" -> result.success(paywallInfo.presentedBy) + "getPresentationSourceType" -> result.success(paywallInfo.presentationSourceType) + "getResponseLoadStartTime" -> result.success(paywallInfo.responseLoadStartTime) + "getResponseLoadCompleteTime" -> result.success(paywallInfo.responseLoadCompleteTime) + "getResponseLoadFailTime" -> result.success(paywallInfo.responseLoadFailTime) + "getResponseLoadDuration" -> result.success(paywallInfo.responseLoadDuration) + "getWebViewLoadStartTime" -> result.success(paywallInfo.webViewLoadStartTime) + "getWebViewLoadCompleteTime" -> result.success(paywallInfo.webViewLoadCompleteTime) + "getWebViewLoadFailTime" -> result.success(paywallInfo.webViewLoadFailTime) + "getWebViewLoadDuration" -> result.success(paywallInfo.webViewLoadDuration) + "getProductsLoadStartTime" -> result.success(paywallInfo.productsLoadStartTime) + "getProductsLoadCompleteTime" -> result.success(paywallInfo.productsLoadCompleteTime) + "getProductsLoadFailTime" -> result.success(paywallInfo.productsLoadFailTime) + "getProductsLoadDuration" -> result.success(paywallInfo.productsLoadDuration) + "getPaywalljsVersion" -> result.success(paywallInfo.paywalljsVersion) + "getIsFreeTrialAvailable" -> result.success(paywallInfo.isFreeTrialAvailable) + "getFeatureGatingBehavior" -> result.success(paywallInfo.featureGatingBehavior.toJson()) + "getCloseReason" -> result.success(paywallInfo.closeReason.toJson()) + "getLocalNotifications" -> result.success(paywallInfo.localNotifications.map { it.toJson() }) + "getComputedPropertyRequests" -> result.success(paywallInfo.computedPropertyRequests.map { it.toJson() }) + "getSurveys" -> result.success(paywallInfo.surveys.map { it.toJson() }) + else -> result.notImplemented() + } } + } } -fun PaywallInfo.createBridgeId(): BridgeId { +suspend fun PaywallInfo.createBridgeId(): BridgeId { val bridgeInstance = (BridgingCreator.shared.createBridgeInstanceFromBridgeClass( bridgeClass = PaywallInfoBridge.bridgeClass(), initializationArgs = mapOf("paywallInfo" to this) diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallPresentationHandlerProxyBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallPresentationHandlerProxyBridge.kt index 4134498..81043bb 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallPresentationHandlerProxyBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallPresentationHandlerProxyBridge.kt @@ -3,28 +3,55 @@ package com.superwall.superwallkit_flutter.bridges import android.content.Context import com.superwall.sdk.paywall.presentation.PaywallPresentationHandler import com.superwall.superwallkit_flutter.invokeMethodOnMain +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class PaywallPresentationHandlerProxyBridge( context: Context, bridgeId: BridgeId, - initializationArgs: Map? = null + initializationArgs: Map? = null, ) : BridgeInstance(context, bridgeId, initializationArgs) { - companion object { fun bridgeClass(): BridgeClass = "PaywallPresentationHandlerProxyBridge" } + val scope = CoroutineScope(Dispatchers.IO) + + companion object { + fun bridgeClass(): BridgeClass = "PaywallPresentationHandlerProxyBridge" + } val handler: PaywallPresentationHandler by lazy { PaywallPresentationHandler().apply { onPresent { - communicator.invokeMethodOnMain("onPresent", mapOf("paywallInfoBridgeId" to it.createBridgeId())) + scope.launch { + communicator().invokeMethodOnMain( + "onPresent", + mapOf("paywallInfoBridgeId" to it.createBridgeId()) + ) + } } onDismiss { - communicator.invokeMethodOnMain("onDismiss", mapOf("paywallInfoBridgeId" to it.createBridgeId())) + scope.launch { + communicator().invokeMethodOnMain( + "onDismiss", + mapOf("paywallInfoBridgeId" to it.createBridgeId()) + ) + } } onError { - communicator.invokeMethodOnMain("onError", mapOf("errorString" to it.toString())) + scope.launch { + communicator().invokeMethodOnMain( + "onError", + mapOf("errorString" to it.toString()) + ) + } } onSkip { - communicator.invokeMethodOnMain("onSkip", mapOf("paywallSkippedReasonBridgeId" to it.createBridgeId())) + scope.launch { + communicator().invokeMethodOnMain( + "onSkip", + mapOf("paywallSkippedReasonBridgeId" to it.createBridgeId()) + ) + } } } } diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallSkippedReasonBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallSkippedReasonBridge.kt index b3ce519..7275ef3 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallSkippedReasonBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PaywallSkippedReasonBridge.kt @@ -5,6 +5,10 @@ import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReaso import com.superwall.superwallkit_flutter.BridgingCreator import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch abstract class PaywallSkippedReasonBridge( context: Context, @@ -19,6 +23,7 @@ abstract class PaywallSkippedReasonBridge( val description = reason.toString() result.success(description) } + else -> result.notImplemented() } } @@ -29,25 +34,35 @@ class PaywallSkippedReasonHoldoutBridge( bridgeId: BridgeId, initializationArgs: Map? = null ) : PaywallSkippedReasonBridge(context, bridgeId, initializationArgs) { - companion object { fun bridgeClass(): BridgeClass = "PaywallSkippedReasonHoldoutBridge" } + private val scope = CoroutineScope(Dispatchers.IO) + + companion object { + fun bridgeClass(): BridgeClass = "PaywallSkippedReasonHoldoutBridge" + } - override val reason: PaywallSkippedReason = initializationArgs?.get("reason") as? PaywallSkippedReason - ?: throw IllegalArgumentException("Attempting to create a `PaywallSkippedReasonHoldoutBridge` without providing a reason.") + override val reason: PaywallSkippedReason = + initializationArgs?.get("reason") as? PaywallSkippedReason + ?: throw IllegalArgumentException("Attempting to create a `PaywallSkippedReasonHoldoutBridge` without providing a reason.") override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { "getExperimentBridgeId" -> { - if (reason is PaywallSkippedReason.Holdout) { - val experiment = reason.experiment - val experimentBridgeId = BridgingCreator.shared.createBridgeInstanceFromBridgeClass( - ExperimentBridge.bridgeClass(), - mapOf("experiment" to experiment) - ) - result.success(experimentBridgeId) - } else { - result.notImplemented() + scope.launch { + if (reason is PaywallSkippedReason.Holdout) { + val experiment = reason.experiment + val experimentBridgeId = + BridgingCreator.shared.createBridgeInstanceFromBridgeClass( + ExperimentBridge.bridgeClass(), + mapOf("experiment" to experiment) + ) + result.success(experimentBridgeId) + } else { + result.notImplemented() + } } } + else -> super.onMethodCall(call, result) } } @@ -80,7 +95,7 @@ class PaywallSkippedReasonUserIsSubscribedBridge( override val reason: PaywallSkippedReason = PaywallSkippedReason.UserIsSubscribed() } -fun PaywallSkippedReason.createBridgeId(): BridgeId { +suspend fun PaywallSkippedReason.createBridgeId(): BridgeId { return when (this) { is PaywallSkippedReason.Holdout -> { val bridgeInstance = BridgingCreator.shared.createBridgeInstanceFromBridgeClass( @@ -89,6 +104,7 @@ fun PaywallSkippedReason.createBridgeId(): BridgeId { ) return bridgeInstance.bridgeId } + is PaywallSkippedReason.NoRuleMatch -> { val bridgeInstance = BridgingCreator.shared.createBridgeInstanceFromBridgeClass( PaywallSkippedReasonNoRuleMatchBridge.bridgeClass(), @@ -96,6 +112,7 @@ fun PaywallSkippedReason.createBridgeId(): BridgeId { ) return bridgeInstance.bridgeId } + is PaywallSkippedReason.EventNotFound -> { val bridgeInstance = BridgingCreator.shared.createBridgeInstanceFromBridgeClass( PaywallSkippedReasonEventNotFoundBridge.bridgeClass(), @@ -103,6 +120,7 @@ fun PaywallSkippedReason.createBridgeId(): BridgeId { ) return bridgeInstance.bridgeId } + is PaywallSkippedReason.UserIsSubscribed -> { val bridgeInstance = BridgingCreator.shared.createBridgeInstanceFromBridgeClass( PaywallSkippedReasonUserIsSubscribedBridge.bridgeClass(), diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PurchaseControllerProxyBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PurchaseControllerProxyBridge.kt index d63f7e1..93ca17e 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PurchaseControllerProxyBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/PurchaseControllerProxyBridge.kt @@ -27,7 +27,7 @@ class PurchaseControllerProxyBridge( "offerId" to offerId ) - val purchaseResultBridgeId = communicator.asyncInvokeMethodOnMain("purchaseFromGooglePlay", attributes) as? BridgeId + val purchaseResultBridgeId = communicator().asyncInvokeMethodOnMain("purchaseFromGooglePlay", attributes) as? BridgeId val purchaseResultBridge = purchaseResultBridgeId?.bridgeInstance() as? PurchaseResultBridge if (purchaseResultBridge == null) { @@ -42,7 +42,7 @@ class PurchaseControllerProxyBridge( override suspend fun restorePurchases(): RestorationResult { print("Attempting to invoke restorePurchases method internally") - val restorationResultBridgeId = communicator.asyncInvokeMethodOnMain("restorePurchases") as? BridgeId + val restorationResultBridgeId = communicator().asyncInvokeMethodOnMain("restorePurchases") as? BridgeId val restorationResultBridge = restorationResultBridgeId?.bridgeInstance() as? RestorationResultBridge if (restorationResultBridge == null) { diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SubscriptionStatusBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SubscriptionStatusBridge.kt index 7b45be1..999b176 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SubscriptionStatusBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SubscriptionStatusBridge.kt @@ -60,7 +60,7 @@ class SubscriptionStatusUnknownBridge( get() = SubscriptionStatus.UNKNOWN } -fun SubscriptionStatus.createBridgeId(): BridgeId { +suspend fun SubscriptionStatus.createBridgeId(): BridgeId { val bridgeClass = when (this) { SubscriptionStatus.ACTIVE -> SubscriptionStatusActiveBridge.bridgeClass() SubscriptionStatus.INACTIVE -> SubscriptionStatusInactiveBridge.bridgeClass() diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallBridge.kt index 17020ca..b2c05b1 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallBridge.kt @@ -31,291 +31,302 @@ import toJson class SuperwallBridge( context: Context, bridgeId: BridgeId, - initializationArgs: Map? = null + initializationArgs: Map? = null, + val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) ) : BridgeInstance(context, bridgeId, initializationArgs), ActivityProvider { companion object { fun bridgeClass(): BridgeClass = "SuperwallBridge" } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "setDelegate" -> { - val delegateProxyBridge = - call.bridgeInstance("delegateProxyBridgeId") - delegateProxyBridge?.let { - Superwall.instance.delegate = it - result.success(null) - } ?: run { - result.badArgs(call) + scope.launch { + when (call.method) { + "setDelegate" -> { + val delegateProxyBridge = + call.bridgeInstance("delegateProxyBridgeId") + delegateProxyBridge?.let { + Superwall.instance.delegate = it + result.success(null) + } ?: run { + result.badArgs(call) + } } - } - "getLogLevel" -> { - val logLevel = Superwall.instance.logLevel - result.success(logLevel.toJson()) - } + "getLogLevel" -> { + val logLevel = Superwall.instance.logLevel + result.success(logLevel.toJson()) + } - "setLogLevel" -> { - val logLevelJson = call.argumentForKey("logLevel") - val logLevel = logLevelJson?.let { JsonExtensions.logLevelFromJson(it) } + "setLogLevel" -> { + val logLevelJson = call.argumentForKey("logLevel") + val logLevel = logLevelJson?.let { JsonExtensions.logLevelFromJson(it) } - logLevel?.let { - Superwall.instance.logLevel = it - result.success(null) - } ?: run { - result.badArgs(call) + logLevel?.let { + Superwall.instance.logLevel = it + result.success(null) + } ?: run { + result.badArgs(call) + } } - } - "getUserAttributes" -> { - val attributes = Superwall.instance.userAttributes - result.success(attributes) - } + "getUserAttributes" -> { + val attributes = Superwall.instance.userAttributes + result.success(attributes) + } - "setUserAttributes" -> { - val userAttributes = call.argument>("userAttributes") - userAttributes?.let { - Superwall.instance.setUserAttributes(userAttributes) - result.success(null) - } ?: run { - result.badArgs(call) + "setUserAttributes" -> { + val userAttributes = call.argument>("userAttributes") + userAttributes?.let { + Superwall.instance.setUserAttributes(userAttributes) + result.success(null) + } ?: run { + result.badArgs(call) + } } - } - "getLocaleIdentifier" -> { - val identifier = Superwall.instance.localeIdentifier - result.success(identifier) - } + "getLocaleIdentifier" -> { + val identifier = Superwall.instance.localeIdentifier + result.success(identifier) + } - "setLocaleIdentifier" -> { - val localeIdentifier = call.argument("localeIdentifier") - Superwall.instance.localeIdentifier = localeIdentifier - result.success(null) - } + "setLocaleIdentifier" -> { + val localeIdentifier = call.argument("localeIdentifier") + Superwall.instance.localeIdentifier = localeIdentifier + result.success(null) + } - "getUserId" -> { - // Implement logic to get the current user's id - val userId = Superwall.instance.userId - result.success(userId) - } + "getUserId" -> { + // Implement logic to get the current user's id + val userId = Superwall.instance.userId + result.success(userId) + } - "getIsLoggedIn" -> { - val isLoggedIn = Superwall.instance.isLoggedIn - result.success(isLoggedIn) - } + "getIsLoggedIn" -> { + val isLoggedIn = Superwall.instance.isLoggedIn + result.success(isLoggedIn) + } - "getIsInitialized" -> { - val isInitialized = Superwall.initialized - result.success(isInitialized) - } + "getIsInitialized" -> { + val isInitialized = Superwall.initialized + result.success(isInitialized) + } - "getPresentedViewController" -> { - // TODO: Since UIViewController cannot be returned directly to Dart, handle appropriately - result.notImplemented() - } + "getPresentedViewController" -> { + // TODO: Since UIViewController cannot be returned directly to Dart, handle appropriately + result.notImplemented() + } - "getLatestPaywallInfoBridgeId" -> { - val paywallInfo = Superwall.instance.latestPaywallInfo - result.success(paywallInfo?.createBridgeId()) - } + "getLatestPaywallInfoBridgeId" -> { + val paywallInfo = Superwall.instance.latestPaywallInfo + result.success(paywallInfo?.createBridgeId()) + } - "getSubscriptionStatusBridgeId" -> { - val subscriptionStatusBridgeId = - Superwall.instance.subscriptionStatus.value.createBridgeId() - result.success(subscriptionStatusBridgeId) - } + "getSubscriptionStatusBridgeId" -> { + val subscriptionStatusBridgeId = + Superwall.instance.subscriptionStatus.value.createBridgeId() + result.success(subscriptionStatusBridgeId) + } - "setSubscriptionStatus" -> { - val subscriptionStatusBridge = - call.bridgeInstance("subscriptionStatusBridgeId") - subscriptionStatusBridge?.let { - Superwall.instance.setSubscriptionStatus(it.status) - - val updatedValue = Superwall.instance.subscriptionStatus.value; - val valid = - (updatedValue == SubscriptionStatus.ACTIVE || updatedValue == SubscriptionStatus.INACTIVE) && (updatedValue != SubscriptionStatus.UNKNOWN); - result.success( - mapOf( - "providedStatus" to it.status.toString(), - "updatedValue" to updatedValue.toString(), - "valid" to valid + "setSubscriptionStatus" -> { + val subscriptionStatusBridge = + call.bridgeInstance("subscriptionStatusBridgeId") + subscriptionStatusBridge?.let { + Superwall.instance.setSubscriptionStatus(it.status) + + val updatedValue = Superwall.instance.subscriptionStatus.value; + val valid = + (updatedValue == SubscriptionStatus.ACTIVE || updatedValue == SubscriptionStatus.INACTIVE) && (updatedValue != SubscriptionStatus.UNKNOWN); + result.success( + mapOf( + "providedStatus" to it.status.toString(), + "updatedValue" to updatedValue.toString(), + "valid" to valid + ) ) - ) - } ?: run { - result.badArgs(call) + } ?: run { + result.badArgs(call) + } } - } - - "getConfigurationStatusBridgeId" -> { - val configurationStatusBridgeId = - Superwall.instance.configurationState.createBridgeId() - result.success(configurationStatusBridgeId) - } - - "getIsConfigured" -> { - - // TODO: Add to Android -// val isConfigured = Superwall.instance.isConfigured -// result.success(isConfigured) - result.notImplemented() - } - "setIsConfigured" -> { - // TODO: Add to Android -// val configured = call.argumentForKey("configured") -// configured?.let { -// Superwall.instance.isConfigured = it -// } -// result.success(null) - result.notImplemented() - } + "getConfigurationStatusBridgeId" -> { + val configurationStatusBridgeId = + Superwall.instance.configurationState.createBridgeId() + result.success(configurationStatusBridgeId) + } - "getIsPaywallPresented" -> { - val isPaywallPresented = Superwall.instance.isPaywallPresented - result.success(isPaywallPresented) - } + "getIsConfigured" -> { - "preloadAllPaywalls" -> { - Superwall.instance.preloadAllPaywalls() - result.success(null) - } + // TODO: Add to Android + // val isConfigured = Superwall.instance.isConfigured + // result.success(isConfigured) + result.notImplemented() + } - "preloadPaywallsForEvents" -> { - val eventNames = call.argumentForKey>("eventNames")?.toSet() - eventNames?.let { - Superwall.instance.preloadPaywalls(it) + "setIsConfigured" -> { + // TODO: Add to Android + // val configured = call.argumentForKey("configured") + // configured?.let { + // Superwall.instance.isConfigured = it + // } + // result.success(null) + result.notImplemented() } - result.success(null) - } - "handleDeepLink" -> { - val urlString = call.argumentForKey("url") - urlString?.let { - try { - val uri = Uri.parse(it) - val handled = Superwall.instance.handleDeepLink(uri) - result.success(handled) - } catch (e: Exception) { - // Handle any other exceptions during parsing - result.badArgs(call) - } - } ?: run { - // urlString is null - result.badArgs(call) + "getIsPaywallPresented" -> { + val isPaywallPresented = Superwall.instance.isPaywallPresented + result.success(isPaywallPresented) } - } - "togglePaywallSpinner" -> { - val isHidden = call.argumentForKey("isHidden") - isHidden?.let { - Superwall.instance.togglePaywallSpinner(isHidden = it) + "preloadAllPaywalls" -> { + Superwall.instance.preloadAllPaywalls() result.success(null) - } ?: run { - result.badArgs(call) } - } - "reset" -> { - Superwall.instance.reset() - result.success(null) - } - - "configure" -> { - val apiKey = call.argumentForKey("apiKey") - apiKey?.let { apiKey -> - val purchaseControllerProxyBridge = - call.bridgeInstance("purchaseControllerProxyBridgeId") - - val options: SuperwallOptions? = - call.argument>("options")?.let { optionsValue -> - JsonExtensions.superwallOptionsFromJson(optionsValue) - } + "preloadPaywallsForEvents" -> { + val eventNames = call.argumentForKey>("eventNames")?.toSet() + eventNames?.let { + Superwall.instance.preloadPaywalls(it) + } + result.success(null) + } - Superwall.configure( - applicationContext = this@SuperwallBridge.context, - apiKey = apiKey, - purchaseController = purchaseControllerProxyBridge, - options = options, - activityProvider = this@SuperwallBridge, - completion = { - val completionBlockProxyBridge = - call.bridgeInstance("completionBlockProxyBridgeId") - completionBlockProxyBridge?.callCompletionBlock() + "handleDeepLink" -> { + val urlString = call.argumentForKey("url") + urlString?.let { + try { + val uri = Uri.parse(it) + val handled = Superwall.instance.handleDeepLink(uri) + result.success(handled) + } catch (e: Exception) { + // Handle any other exceptions during parsing + result.badArgs(call) } - ) - - // Set the platform wrapper - Superwall.instance.setPlatformWrapper( - "Flutter", - call.argumentForKey("sdkVersion") ?: "" - ); + } ?: run { + // urlString is null + result.badArgs(call) + } + } - // Returning nil instead of the result from configure because we want to use the Dart - // instance of Superwall, not a native variant - result.success(null) - } ?: run { - result.badArgs(call.method) + "togglePaywallSpinner" -> { + val isHidden = call.argumentForKey("isHidden") + isHidden?.let { + Superwall.instance.togglePaywallSpinner(isHidden = it) + result.success(null) + } ?: run { + result.badArgs(call) + } } - } - "dismiss" -> { - CoroutineScope(Dispatchers.Main).launch { - Superwall.instance.dismiss() + "reset" -> { + Superwall.instance.reset() result.success(null) } - } - "registerEvent" -> { - val event = call.argumentForKey("event") - event?.let { event -> - val params = call.argument>("params") + "configure" -> { + val apiKey = call.argumentForKey("apiKey") + apiKey?.let { apiKey -> + val purchaseControllerProxyBridge = + call.bridgeInstance("purchaseControllerProxyBridgeId") + + val options: SuperwallOptions? = + call.argument>("options")?.let { optionsValue -> + JsonExtensions.superwallOptionsFromJson(optionsValue) + } + + Superwall.configure( + applicationContext = this@SuperwallBridge.context, + apiKey = apiKey, + purchaseController = purchaseControllerProxyBridge, + options = options, + activityProvider = this@SuperwallBridge, + completion = { + scope.launch { + + val completionBlockProxyBridge = + call.bridgeInstance("completionBlockProxyBridgeId") + completionBlockProxyBridge?.callCompletionBlock() + } + } + ) - BreadCrumbs.append("SuperwallBridge.kt: Invoke registerEvent") - val handlerProxyBridge = - call.bridgeInstance("handlerProxyBridgeId") - BreadCrumbs.append("SuperwallBridge.kt: Found handlerProxyBridge instance: $handlerProxyBridge") + // Set the platform wrapper + Superwall.instance.setPlatformWrapper( + "Flutter", + call.argumentForKey("sdkVersion") ?: "" + ); + + // Returning nil instead of the result from configure because we want to use the Dart + // instance of Superwall, not a native variant + result.success(null) + } ?: run { + result.badArgs(call.method) + } + } - val handler: PaywallPresentationHandler? = handlerProxyBridge?.handler - BreadCrumbs.append("SuperwallBridge.kt: Found handler: $handler") + "dismiss" -> { + CoroutineScope(Dispatchers.Main).launch { + Superwall.instance.dismiss() + result.success(null) + } + } - Superwall.instance.register(event, params, handler) { - val featureBlockProxyBridge = - call.bridgeInstance("featureBlockProxyBridgeId") - featureBlockProxyBridge?.let { - it.callCompletionBlock() + "registerEvent" -> { + val event = call.argumentForKey("event") + event?.let { event -> + val params = call.argument>("params") + BreadCrumbs.append("SuperwallBridge.kt: Invoke registerEvent") + val handlerProxyBridge = + call.bridgeInstance("handlerProxyBridgeId") + BreadCrumbs.append("SuperwallBridge.kt: Found handlerProxyBridge instance: $handlerProxyBridge") + + val handler: PaywallPresentationHandler? = handlerProxyBridge?.handler + BreadCrumbs.append("SuperwallBridge.kt: Found handler: $handler") + + Superwall.instance.register(event, params, handler) { + scope.launch { + val featureBlockProxyBridge = + call.bridgeInstance("featureBlockProxyBridgeId") + featureBlockProxyBridge?.let { + it.callCompletionBlock() + } + } } + result.success(true) + } ?: run { + result.badArgs(call) } - result.success(true) - } ?: run { - result.badArgs(call) } - } - - "identify" -> { - val userId = call.argumentForKey("userId") - userId?.let { - val identityOptionsJson = call.argument>("identityOptions") - val identityOptions = - identityOptionsJson?.let { JsonExtensions.identityOptionsFromJson(it) } - Superwall.instance.identify(userId, identityOptions) - result.success(null) - } ?: run { - result.badArgs(call) + "identify" -> { + val userId = call.argumentForKey("userId") + userId?.let { + val identityOptionsJson = + call.argument>("identityOptions") + val identityOptions = + identityOptionsJson?.let { JsonExtensions.identityOptionsFromJson(it) } + + Superwall.instance.identify(userId, identityOptions) + result.success(null) + } ?: run { + result.badArgs(call) + } } - } - "confirmAllAssignments" -> { - CoroutineScope(Dispatchers.IO).launch { - val assignments = Superwall.instance.confirmAllAssignments() - .map { it.toJson() } - result.success(assignments) + "confirmAllAssignments" -> { + CoroutineScope(Dispatchers.IO).launch { + Superwall.instance.confirmAllAssignments() + .fold({ + result.success(it.map { it.toJson() }) + }, { + result.badArgs(call) + }) + } } - } - else -> result.notImplemented() + else -> result.notImplemented() + } } } diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallDelegateProxyBridge.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallDelegateProxyBridge.kt index 1b39674..053b02b 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallDelegateProxyBridge.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/bridges/SuperwallDelegateProxyBridge.kt @@ -8,49 +8,96 @@ import com.superwall.sdk.delegate.SuperwallDelegate import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.superwallkit_flutter.invokeMethodOnMain import toJson -import java.net.URL +import java.net.URI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class SuperwallDelegateProxyBridge( context: Context, bridgeId: BridgeId, - initializationArgs: Map? = null + initializationArgs: Map? = null, + val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) ) : BridgeInstance(context, bridgeId, initializationArgs), SuperwallDelegate { - companion object { fun bridgeClass(): BridgeClass = "SuperwallDelegateProxyBridge" } + companion object { + fun bridgeClass(): BridgeClass = "SuperwallDelegateProxyBridge" + } override fun willPresentPaywall(paywallInfo: PaywallInfo) { - communicator.invokeMethodOnMain("willPresentPaywall", mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId())) + coroutineScope.launch { + communicator().invokeMethodOnMain( + "willPresentPaywall", + mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId()) + ) + } } override fun didPresentPaywall(paywallInfo: PaywallInfo) { - communicator.invokeMethodOnMain("didPresentPaywall", mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId())) + coroutineScope.launch { + + communicator().invokeMethodOnMain( + "didPresentPaywall", + mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId()) + ) + } } override fun willDismissPaywall(paywallInfo: PaywallInfo) { - communicator.invokeMethodOnMain("willDismissPaywall", mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId())) + coroutineScope.launch { + + communicator().invokeMethodOnMain( + "willDismissPaywall", + mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId()) + ) + } } override fun didDismissPaywall(paywallInfo: PaywallInfo) { - communicator.invokeMethodOnMain("didDismissPaywall", mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId())) + coroutineScope.launch { + communicator().invokeMethodOnMain( + "didDismissPaywall", + mapOf("paywallInfoBridgeId" to paywallInfo.createBridgeId()) + ) + } } override fun handleCustomPaywallAction(name: String) { - communicator.invokeMethodOnMain("handleCustomPaywallAction", mapOf("name" to name)) + coroutineScope.launch { + communicator().invokeMethodOnMain("handleCustomPaywallAction", mapOf("name" to name)) + } } override fun subscriptionStatusDidChange(newValue: SubscriptionStatus) { - communicator.invokeMethodOnMain("subscriptionStatusDidChange", mapOf("subscriptionStatusBridgeId" to newValue.createBridgeId())) + coroutineScope.launch { + communicator().invokeMethodOnMain( + "subscriptionStatusDidChange", + mapOf("subscriptionStatusBridgeId" to newValue.createBridgeId()) + ) + } } override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { - communicator.invokeMethodOnMain("handleSuperwallEvent", mapOf("eventInfo" to eventInfo.toJson())) + coroutineScope.launch { + communicator().invokeMethodOnMain( + "handleSuperwallEvent", + mapOf("eventInfo" to eventInfo.toJson()) + ) + } } - override fun paywallWillOpenURL(url: URL) { - communicator.invokeMethodOnMain("paywallWillOpenURL", mapOf("url" to url.toString())) + override fun paywallWillOpenURL(url: URI) { + coroutineScope.launch { + communicator().invokeMethodOnMain("paywallWillOpenURL", mapOf("url" to url.toString())) + } } override fun paywallWillOpenDeepLink(url: Uri) { - communicator.invokeMethodOnMain("paywallWillOpenDeepLink", mapOf("url" to url.toString())) + coroutineScope.launch { + communicator().invokeMethodOnMain( + "paywallWillOpenDeepLink", + mapOf("url" to url.toString()) + ) + } } override fun handleLog( @@ -60,27 +107,29 @@ class SuperwallDelegateProxyBridge( info: Map?, error: Throwable? ) { - val transformedInfo = info?.mapValues { (_, value) -> - when (value) { - is String -> value - is Int -> value - is Map<*, *> -> value - is List<*> -> value - is Boolean -> value - is Set<*> -> value.toList() - else -> null + coroutineScope.launch { + val transformedInfo = info?.mapValues { (_, value) -> + when (value) { + is String -> value + is Int -> value + is Map<*, *> -> value + is List<*> -> value + is Boolean -> value + is Set<*> -> value.toList() + else -> null + } } - } - val arguments: Map = mapOf( - "level" to level, - "scope" to scope, - "message" to message, - "info" to transformedInfo, - "error" to error?.localizedMessage - ) + val arguments: Map = mapOf( + "level" to level, + "scope" to scope, + "message" to message, + "info" to transformedInfo, + "error" to error?.localizedMessage + ) - communicator.invokeMethodOnMain("handleLog", arguments) + communicator().invokeMethodOnMain("handleLog", arguments) + } } } diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/PaywallPresentationRequestStatus+Json.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/PaywallPresentationRequestStatus+Json.kt index 7018544..dec96de 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/PaywallPresentationRequestStatus+Json.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/PaywallPresentationRequestStatus+Json.kt @@ -10,7 +10,7 @@ fun PaywallPresentationRequestStatus.toJson(): Map = when (this) PaywallPresentationRequestStatus.Timeout -> mapOf("status" to "timeout") } -fun PaywallPresentationRequestStatusReason.toJson(): Map = when (this) { +suspend fun PaywallPresentationRequestStatusReason.toJson(): Map = when (this) { is PaywallPresentationRequestStatusReason.DebuggerPresented -> mapOf("reason" to "debuggerPresented") is PaywallPresentationRequestStatusReason.PaywallAlreadyPresented -> mapOf("reason" to "paywallAlreadyPresented") is PaywallPresentationRequestStatusReason.UserIsSubscribed -> mapOf("reason" to "userIsSubscribed") diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/SuperwallEventInfo+Json.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/SuperwallEventInfo+Json.kt index cf786ca..120be85 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/SuperwallEventInfo+Json.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/SuperwallEventInfo+Json.kt @@ -4,14 +4,14 @@ import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import com.superwall.superwallkit_flutter.bridges.createBridgeId import com.superwall.superwallkit_flutter.json.toJson -fun SuperwallEventInfo.toJson(): Map { +suspend fun SuperwallEventInfo.toJson(): Map { return mapOf( "event" to event.toJson(), "params" to params ) } -fun SuperwallEvent.toJson(): Map { +suspend fun SuperwallEvent.toJson(): Map { return when (this) { is SuperwallEvent.FirstSeen -> mapOf("event" to "firstSeen") is SuperwallEvent.AppOpen -> mapOf("event" to "appOpen") @@ -214,9 +214,11 @@ fun SuperwallEvent.toJson(): Map { "params" to this.params, "paywallInfoBridgeId" to this.paywallInfo.createBridgeId() ) - is SuperwallEvent.ErrorThrown -> mapOf("event" to "errorThrown") is SuperwallEvent.ConfigFail -> mapOf("event" to "configFail") is SuperwallEvent.ConfirmAllAssignments -> mapOf("event" to "confirmAllAssignments") + is SuperwallEvent.PaywallResourceLoadFail -> mapOf("event" to "paywallResourceLoadFail") + is SuperwallEvent.ShimmerViewComplete -> mapOf("event" to "shimmerViewComplete") + else -> {mapOf()} } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/TriggerResult+Json.kt b/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/TriggerResult+Json.kt index a432bdc..fdc74dc 100644 --- a/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/TriggerResult+Json.kt +++ b/android/src/main/kotlin/com/superwall/superwallkit_flutter/json/TriggerResult+Json.kt @@ -1,7 +1,7 @@ import com.superwall.sdk.models.triggers.TriggerResult import com.superwall.superwallkit_flutter.bridges.createBridgeId -fun TriggerResult.toJson(): Map { +suspend fun TriggerResult.toJson(): Map { return when (this) { is TriggerResult.EventNotFound -> mapOf("result" to "eventNotFound") is TriggerResult.NoRuleMatch -> mapOf("result" to "noRuleMatch") diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ded53a2..85944d0 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,10 +8,12 @@ PODS: - PurchasesHybridCommon (10.9.0): - RevenueCat (= 4.43.0) - RevenueCat (4.43.0) - - SuperwallKit (3.10.1) + - Superscript (0.1.16) + - SuperwallKit (3.12.0): + - Superscript (= 0.1.16) - superwallkit_flutter (0.0.1): - Flutter - - SuperwallKit (= 3.10.1) + - SuperwallKit (= 3.12.0) DEPENDENCIES: - Flutter (from `Flutter`) @@ -23,6 +25,7 @@ SPEC REPOS: trunk: - PurchasesHybridCommon - RevenueCat + - Superscript - SuperwallKit EXTERNAL SOURCES: @@ -41,8 +44,9 @@ SPEC CHECKSUMS: purchases_flutter: a10ab0c4bb4effbc9b78be6513658ae659e67a20 PurchasesHybridCommon: 0e157d11d04fdfdd434b5c6d05acb58fd1cc9db8 RevenueCat: f7b90c52f4b7e322bc9e7adaf734030523534b06 - SuperwallKit: 89cfaba57c139f97a578efc6bbb1f3963d2aa6b2 - superwallkit_flutter: 0b14c04ddbd9cfd008acfca685bbef67155c5f52 + Superscript: 17e2597de5e1ddfa132e217b33d1eb8eddf13e0f + SuperwallKit: 9abe07053249c596af9875a3b857236068a79edc + superwallkit_flutter: 56ac7886c6e750f9759174ecb42e4c1472f11764 PODFILE CHECKSUM: 1959d098c91d8a792531a723c4a9d7e9f6a01e38 diff --git a/example/pubspec.lock b/example/pubspec.lock index 193e599..7a1f56d 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -134,18 +134,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -174,18 +174,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" path: dependency: transitive description: @@ -198,10 +198,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -277,7 +277,7 @@ packages: path: ".." relative: true source: path - version: "1.3.0" + version: "1.3.4" sync_http: dependency: transitive description: @@ -298,10 +298,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -330,10 +330,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" webdriver: dependency: transitive description: diff --git a/ios/Classes/Json/SuperwallEventInfo+Json.swift b/ios/Classes/Json/SuperwallEventInfo+Json.swift index fd30238..242e99e 100644 --- a/ios/Classes/Json/SuperwallEventInfo+Json.swift +++ b/ios/Classes/Json/SuperwallEventInfo+Json.swift @@ -128,6 +128,10 @@ extension SuperwallEvent { return ["event": "adServicesTokenRequestFail", "error": error.localizedDescription] case .adServicesTokenRequestComplete(token: let token): return ["event": "adServicesTokenRequestComplete", "token": token] + case .shimmerViewStart: + return ["event": "shimmerViewStart"] + case .shimmerViewComplete: + return ["event": "shimmerViewComplete"] } } } diff --git a/ios/Classes/Json/SuperwallOptions+Json.swift b/ios/Classes/Json/SuperwallOptions+Json.swift index 06c440d..dbd18ac 100644 --- a/ios/Classes/Json/SuperwallOptions+Json.swift +++ b/ios/Classes/Json/SuperwallOptions+Json.swift @@ -9,8 +9,7 @@ extension SuperwallOptions { let networkEnvironment = NetworkEnvironment.fromJson(networkEnvironmentValue), let isExternalDataCollectionEnabled = dictionary["isExternalDataCollectionEnabled"] as? Bool, let loggingValue = dictionary["logging"] as? [String: Any], - let logging = Logging.fromJson(loggingValue), - let collectAdServicesAttribution = dictionary["collectAdServicesAttribution"] as? Bool + let logging = Logging.fromJson(loggingValue) else { return nil } @@ -25,7 +24,6 @@ extension SuperwallOptions { superwallOptions.localeIdentifier = localeIdentifier superwallOptions.isGameControllerEnabled = isGameControllerEnabled superwallOptions.logging = logging - superwallOptions.collectAdServicesAttribution = collectAdServicesAttribution return superwallOptions } diff --git a/ios/superwallkit_flutter.podspec b/ios/superwallkit_flutter.podspec index 35f7901..9a8e7df 100644 --- a/ios/superwallkit_flutter.podspec +++ b/ios/superwallkit_flutter.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'SuperwallKit', '3.10.1' + s.dependency 'SuperwallKit', '3.12.0' s.platform = :ios, '14.0' # Flutter.framework does not contain a i386 slice. diff --git a/lib/src/public/SuperwallEventInfo.dart b/lib/src/public/SuperwallEventInfo.dart index cd4193a..8a1b8cc 100644 --- a/lib/src/public/SuperwallEventInfo.dart +++ b/lib/src/public/SuperwallEventInfo.dart @@ -70,7 +70,9 @@ enum EventType { configFail, adServicesTokenRequestStart, adServicesTokenRequestFail, - adServicesTokenRequestComplete + adServicesTokenRequestComplete, + shimmerViewStart, + shimmerViewComplete } class SuperwallEvent { @@ -342,6 +344,10 @@ class SuperwallEvent { return SuperwallEvent._( type: EventType.adServicesTokenRequestComplete, token: json['token']); + case 'shimmerViewStart': + return SuperwallEvent._(type: EventType.shimmerViewStart); + case 'shimmerViewComplete': + return SuperwallEvent._(type: EventType.shimmerViewComplete); default: throw ArgumentError('Invalid event type'); } diff --git a/lib/src/public/SuperwallOptions.dart b/lib/src/public/SuperwallOptions.dart index d03441d..32af753 100644 --- a/lib/src/public/SuperwallOptions.dart +++ b/lib/src/public/SuperwallOptions.dart @@ -25,11 +25,6 @@ class SuperwallOptions { /// The log scope and level to print to the console. Logging logging = Logging(); - /// Collects the `AdServices` attributes on iOS and attaches them to the user attributes. - /// - /// Defaults to `false`. - bool collectAdServicesAttribution = false; - /// Enables passing identifier to the Play Store as AccountId's. Defaults to `false`. bool passIdentifiersToPlayStore = false; } @@ -43,8 +38,7 @@ extension SuperwallOptionsJson on SuperwallOptions { 'localeIdentifier': localeIdentifier, 'isGameControllerEnabled': isGameControllerEnabled, 'logging': logging.toJson(), - 'collectAdServicesAttribution': collectAdServicesAttribution, - 'passIdentifiersToPlayStore' : passIdentifiersToPlayStore + 'passIdentifiersToPlayStore': passIdentifiersToPlayStore }; } } @@ -84,9 +78,10 @@ class Logging { LogLevel level = LogLevel.info; /// Defines the scope of logs to print to the console. Defaults to .all. - Set scopes = { LogScope.all }; + Set scopes = {LogScope.all}; - void handleLogRecord(LogLevel level, String message, [Object? error, StackTrace? trace]) { + void handleLogRecord(LogLevel level, String message, + [Object? error, StackTrace? trace]) { if (level.index < this.level.index) { return; } @@ -103,14 +98,14 @@ class Logging { } } - void debug(String message, [Object? error, StackTrace? trace]) - => handleLogRecord(LogLevel.debug, message, error, trace); - void info(String message, [Object? error, StackTrace? trace]) - => handleLogRecord(LogLevel.info, message, error, trace); - void warn(String message, [Object? error, StackTrace? trace]) - => handleLogRecord(LogLevel.warn, message, error, trace); - void error(String message, [Object? error, StackTrace? trace]) - => handleLogRecord(LogLevel.error, message, error, trace); + void debug(String message, [Object? error, StackTrace? trace]) => + handleLogRecord(LogLevel.debug, message, error, trace); + void info(String message, [Object? error, StackTrace? trace]) => + handleLogRecord(LogLevel.info, message, error, trace); + void warn(String message, [Object? error, StackTrace? trace]) => + handleLogRecord(LogLevel.warn, message, error, trace); + void error(String message, [Object? error, StackTrace? trace]) => + handleLogRecord(LogLevel.error, message, error, trace); } extension LoggingJson on Logging { diff --git a/pubspec.yaml b/pubspec.yaml index f9117c7..6ce84fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: superwallkit_flutter description: "Remotely configure every aspect of your paywall and double your revenue." -version: 1.3.4 +version: 1.3.5 homepage: "https://superwall.com" environment: