diff --git a/CHANGELOG.md b/CHANGELOG.md
index f046cb86..35f06598 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub.
+## 2.6.7
+
+### Enhancements
+- Adds permission granting and callbacks to/from paywalls
+- Adds `PaywallPreloadStart` and `PaywallPreloadComplete` events
+
+### Fixes
+- Fix handling of deep links when paywall is detached
+- Enables permission granting from paywall and callbacks
+- Fix crash when handling drawer style paywalls with 100% height
+
## 2.6.6
## Enhancements
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 20fe5a62..eb4c16a2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,16 @@
+
+
+
+
+
+
+
+
{
+ Logger.debug(
+ LogLevel.debug,
+ LogScope.paywallView,
+ message = "Permission requested: ${paywallEvent.permissionType.rawValue}",
+ )
+ }
}
}
}
diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt
index 1fa5c2c7..0d00fb5b 100644
--- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt
+++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt
@@ -1174,4 +1174,72 @@ sealed class InternalSuperwallEvent(
)
}
}
+
+ data class Permission(
+ val state: State,
+ val permissionName: String,
+ val paywallIdentifier: String,
+ ) : InternalSuperwallEvent(
+ SuperwallEvent.PermissionRequested(permissionName, paywallIdentifier),
+ ) {
+ enum class State {
+ Requested,
+ Granted,
+ Denied,
+ }
+
+ override val superwallPlacement: SuperwallEvent
+ get() =
+ when (state) {
+ State.Requested ->
+ SuperwallEvent.PermissionRequested(
+ permissionName = permissionName,
+ paywallIdentifier = paywallIdentifier,
+ )
+ State.Granted ->
+ SuperwallEvent.PermissionGranted(
+ permissionName = permissionName,
+ paywallIdentifier = paywallIdentifier,
+ )
+ State.Denied ->
+ SuperwallEvent.PermissionDenied(
+ permissionName = permissionName,
+ paywallIdentifier = paywallIdentifier,
+ )
+ }
+
+ override val audienceFilterParams: Map = emptyMap()
+
+ override suspend fun getSuperwallParameters(): Map =
+ mapOf(
+ "permission_name" to permissionName,
+ "paywall_identifier" to paywallIdentifier,
+ )
+ }
+
+ data class PaywallPreload(
+ val state: State,
+ val paywallCount: Int,
+ ) : InternalSuperwallEvent(
+ SuperwallEvent.PaywallPreloadStart(paywallCount),
+ ) {
+ enum class State {
+ Start,
+ Complete,
+ }
+
+ override val superwallPlacement: SuperwallEvent
+ get() =
+ when (state) {
+ State.Start -> SuperwallEvent.PaywallPreloadStart(paywallCount)
+ State.Complete -> SuperwallEvent.PaywallPreloadComplete(paywallCount)
+ }
+
+ override val audienceFilterParams: Map = emptyMap()
+
+ override suspend fun getSuperwallParameters(): Map =
+ mapOf(
+ "paywall_count" to paywallCount,
+ )
+ }
}
diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt
index 63b93806..acd9b6ed 100644
--- a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt
+++ b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt
@@ -521,6 +521,49 @@ sealed class SuperwallEvent {
get() = SuperwallEvents.CustomerInfoDidChange.rawName
}
+ // / When a permission is requested from a paywall.
+ data class PermissionRequested(
+ val permissionName: String,
+ val paywallIdentifier: String,
+ ) : SuperwallEvent() {
+ override val rawName: String
+ get() = SuperwallEvents.PermissionRequested.rawName
+ }
+
+ // / When a permission is granted after being requested from a paywall.
+ data class PermissionGranted(
+ val permissionName: String,
+ val paywallIdentifier: String,
+ ) : SuperwallEvent() {
+ override val rawName: String
+ get() = SuperwallEvents.PermissionGranted.rawName
+ }
+
+ // / When a permission is denied after being requested from a paywall.
+ data class PermissionDenied(
+ val permissionName: String,
+ val paywallIdentifier: String,
+ ) : SuperwallEvent() {
+ override val rawName: String
+ get() = SuperwallEvents.PermissionDenied.rawName
+ }
+
+ // / When paywall preloading starts.
+ data class PaywallPreloadStart(
+ val paywallCount: Int,
+ ) : SuperwallEvent() {
+ override val rawName: String
+ get() = SuperwallEvents.PaywallPreloadStart.rawName
+ }
+
+ // / When paywall preloading completes.
+ data class PaywallPreloadComplete(
+ val paywallCount: Int,
+ ) : SuperwallEvent() {
+ override val rawName: String
+ get() = SuperwallEvents.PaywallPreloadComplete.rawName
+ }
+
open val rawName: String
get() = this.toString()
diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt
index 4abc03fd..e0c6a3a7 100644
--- a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt
+++ b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt
@@ -58,4 +58,9 @@ enum class SuperwallEvents(
ReviewDenied("review_denied"),
IntegrationAttributes("integration_attributes"),
CustomerInfoDidChange("customerInfo_didChange"),
+ PermissionRequested("permission_requested"),
+ PermissionGranted("permission_granted"),
+ PermissionDenied("permission_denied"),
+ PaywallPreloadStart("paywallPreload_start"),
+ PaywallPreloadComplete("paywallPreload_complete"),
}
diff --git a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt
index b2e91c8c..ca328669 100644
--- a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt
+++ b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt
@@ -1,6 +1,7 @@
package com.superwall.sdk.config
import android.content.Context
+import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.dependencies.RequestFactory
import com.superwall.sdk.dependencies.RuleAttributesFactory
import com.superwall.sdk.misc.IOScope
@@ -25,6 +26,7 @@ class PaywallPreload(
val storage: LocalStorage,
val assignments: Assignments,
val paywallManager: PaywallManager,
+ private val track: suspend (InternalSuperwallEvent) -> Unit,
) {
interface Factory :
RequestFactory,
@@ -57,8 +59,8 @@ class PaywallPreload(
unconfirmedAssignments = assignments.unconfirmedAssignments,
expressionEvaluator = expressionEvaluator,
)
- preloadPaywalls(paywallIdentifiers = paywallIds)
+ preloadPaywalls(paywallIdentifiers = paywallIds)
currentPreloadingTask = null
}
}
@@ -79,6 +81,14 @@ class PaywallPreload(
// Preloads paywalls referenced by triggers.
private suspend fun preloadPaywalls(paywallIdentifiers: Set) {
+ val paywallCount = paywallIdentifiers.size
+ track(
+ InternalSuperwallEvent.PaywallPreload(
+ state = InternalSuperwallEvent.PaywallPreload.State.Start,
+ paywallCount = paywallCount,
+ ),
+ )
+
val webviewExists = webViewExists()
if (webviewExists) {
scope.launchWithTracking {
@@ -116,6 +126,12 @@ class PaywallPreload(
}
// Await all tasks
tasks.awaitAll()
+ track(
+ InternalSuperwallEvent.PaywallPreload(
+ state = InternalSuperwallEvent.PaywallPreload.State.Complete,
+ paywallCount = paywallCount,
+ ),
+ )
}
}
}
diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt
index 52a0ba48..297ec2d7 100644
--- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt
+++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt
@@ -98,6 +98,8 @@ import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandler
import com.superwall.sdk.paywall.view.webview.templating.models.JsonVariables
import com.superwall.sdk.paywall.view.webview.templating.models.Variables
import com.superwall.sdk.paywall.view.webview.webViewExists
+import com.superwall.sdk.permissions.UserPermissions
+import com.superwall.sdk.permissions.UserPermissionsImpl
import com.superwall.sdk.review.MockReviewManager
import com.superwall.sdk.review.ReviewManager
import com.superwall.sdk.review.ReviewManagerImpl
@@ -187,6 +189,7 @@ class DependencyContainer(
val transactionManager: TransactionManager
val googleBillingWrapper: GoogleBillingWrapper
internal val reviewManager: ReviewManager
+ internal val userPermissions: UserPermissions
var entitlements: Entitlements
internal lateinit var customerInfoManager: CustomerInfoManager
@@ -381,6 +384,9 @@ class DependencyContainer(
assignments = assignments,
paywallManager = paywallManager,
scope = ioScope,
+ track = {
+ Superwall.instance.track(it)
+ },
)
configManager =
@@ -590,6 +596,8 @@ class DependencyContainer(
})
}
+ userPermissions = UserPermissionsImpl(context)
+
deepLinkRouter =
DeepLinkRouter(
reedemer,
@@ -699,6 +707,8 @@ class DependencyContainer(
encodeToB64 = {
Base64.encodeToString(it.toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP)
},
+ userPermissions = userPermissions,
+ getActivity = { activityProvider?.getCurrentActivity() },
)
val state =
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt
index ce3ba0e5..f05f06e8 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt
@@ -1,6 +1,7 @@
package com.superwall.sdk.paywall.view
import android.app.Activity
+import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
@@ -991,11 +992,24 @@ class PaywallView(
}
override fun openDeepLink(url: String) {
- var uri = url.toUri()
+ val uri = url.toUri()
eventDidOccur(PaywallWebEvent.OpenedDeepLink(uri))
- val context = encapsulatingActivity?.get()
- val deepLinkIntent = Intent(Intent.ACTION_VIEW, uri)
- context?.startActivity(deepLinkIntent)
+ val activityContext = encapsulatingActivity?.get()
+ val deepLinkIntent =
+ Intent(Intent.ACTION_VIEW, uri).apply {
+ if (activityContext == null) {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ }
+ try {
+ (activityContext ?: context).startActivity(deepLinkIntent)
+ } catch (e: ActivityNotFoundException) {
+ Logger.debug(
+ logLevel = LogLevel.warn,
+ scope = LogScope.paywallView,
+ message = "No activity found to handle deep link: $url",
+ )
+ }
}
//region GameController
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt
index 0829e952..0378e28b 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt
@@ -520,8 +520,9 @@ class SuperwallPaywallActivity : AppCompatActivity() {
val content = contentView as ViewGroup
val bottomSheetBehavior = BottomSheetBehavior.from(content.getChildAt(0))
if (!isModal) {
- bottomSheetBehavior.halfExpandedRatio =
- (if (height > 1.0) height / 100 else height).toFloat()
+ val normalizedHeight = (if (height > 1.0) height / 100 else height).toFloat()
+ // Clamp to (0, 1) since 0.0 = STATE_COLLAPSED and 1.0 = STATE_EXPANDED
+ bottomSheetBehavior.halfExpandedRatio = normalizedHeight.coerceIn(0.01f, 0.99f)
} else {
// If it's a Modal, we want it to cover only 95% of the screen when expanded
content.updateLayoutParams {
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt
index 9bbbee7c..9a97608a 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt
@@ -173,7 +173,7 @@ class SWWebView(
addJavascriptInterface(messageHandler, "SWAndroid")
val webSettings = this.settings
- setWebContentsDebuggingEnabled(false)
+ setWebContentsDebuggingEnabled(true)
webSettings.javaScriptEnabled = true
webSettings.setSupportZoom(false)
webSettings.builtInZoomControls = false
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt
index 63566ff8..dc9a8d38 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt
@@ -4,6 +4,7 @@ import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.logger.Logger
import com.superwall.sdk.models.paywall.LocalNotificationType
+import com.superwall.sdk.permissions.PermissionType
import com.superwall.sdk.storage.core_data.convertFromJsonElement
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@@ -108,6 +109,11 @@ sealed class PaywallMessage {
val body: String,
val delay: Long,
) : PaywallMessage()
+
+ data class RequestPermission(
+ val permissionType: PermissionType,
+ val requestId: String,
+ ) : PaywallMessage()
}
fun parseWrappedPaywallMessages(jsonString: String): Result =
@@ -198,6 +204,22 @@ private fun parsePaywallMessage(json: JsonObject): PaywallMessage {
delay = json["delay"]?.jsonPrimitive?.longOrNull ?: 0L,
)
+ "request_permission" -> {
+ val permissionTypeRaw =
+ json["permission_type"]?.jsonPrimitive?.contentOrNull
+ ?: throw IllegalArgumentException("request_permission missing permission_type")
+ val permissionType =
+ PermissionType.fromRaw(permissionTypeRaw)
+ ?: throw IllegalArgumentException("Unknown permission_type: $permissionTypeRaw")
+ val requestId =
+ json["request_id"]?.jsonPrimitive?.contentOrNull
+ ?: throw IllegalArgumentException("request_permission missing request_id")
+ PaywallMessage.RequestPermission(
+ permissionType = permissionType,
+ requestId = requestId,
+ )
+ }
+
else -> {
throw IllegalArgumentException("Unknown event name: $eventName")
}
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt
index 9563a49f..a919c1e7 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt
@@ -1,6 +1,7 @@
package com.superwall.sdk.paywall.view.webview.messaging
import TemplateLogic
+import android.app.Activity
import android.webkit.JavascriptInterface
import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent
@@ -17,6 +18,8 @@ import com.superwall.sdk.paywall.view.PaywallView
import com.superwall.sdk.paywall.view.PaywallViewState
import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState
import com.superwall.sdk.paywall.view.webview.SendPaywallMessages
+import com.superwall.sdk.permissions.PermissionStatus
+import com.superwall.sdk.permissions.UserPermissions
import com.superwall.sdk.storage.core_data.convertToJsonElement
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -63,6 +66,8 @@ class PaywallMessageHandler(
private val ioScope: CoroutineScope,
private val json: Json = Json { encodeDefaults = true },
private val encodeToB64: (String) -> String,
+ private val userPermissions: UserPermissions,
+ private val getActivity: () -> Activity?,
) : SendPaywallMessages {
private companion object {
val selectionString =
@@ -226,6 +231,8 @@ class PaywallMessageHandler(
),
)
+ is PaywallMessage.RequestPermission -> handleRequestPermission(message)
+
else -> {
Logger.debug(
LogLevel.error,
@@ -487,6 +494,130 @@ class PaywallMessageHandler(
)
}
+ private fun handleRequestPermission(request: PaywallMessage.RequestPermission) {
+ val activity = getActivity()
+ val paywallIdentifier = messageHandler?.state?.paywall?.identifier ?: ""
+ val permissionName = request.permissionType.rawValue
+
+ messageHandler?.eventDidOccur(
+ PaywallWebEvent.RequestPermission(
+ permissionType = request.permissionType,
+ requestId = request.requestId,
+ ),
+ )
+
+ // Track permission requested event
+ ioScope.launch {
+ track(
+ InternalSuperwallEvent.Permission(
+ state = InternalSuperwallEvent.Permission.State.Requested,
+ permissionName = permissionName,
+ paywallIdentifier = paywallIdentifier,
+ ),
+ )
+ }
+
+ if (activity == null) {
+ Logger.debug(
+ LogLevel.error,
+ LogScope.superwallCore,
+ "Cannot request permission - no activity available",
+ )
+ // Send unsupported status back to webview since we can't request
+ ioScope.launch {
+ sendPermissionResult(
+ requestId = request.requestId,
+ permissionType = request.permissionType,
+ status = PermissionStatus.UNSUPPORTED,
+ )
+ }
+ return
+ }
+
+ ioScope.launch {
+ val status =
+ try {
+ userPermissions.requestPermission(activity, request.permissionType)
+ } catch (e: Exception) {
+ Logger.debug(
+ LogLevel.error,
+ LogScope.superwallCore,
+ "Error requesting permission: ${e.message}",
+ error = e,
+ )
+ PermissionStatus.UNSUPPORTED
+ }
+
+ // Track permission result event
+ when (status) {
+ PermissionStatus.GRANTED -> {
+ track(
+ InternalSuperwallEvent.Permission(
+ state = InternalSuperwallEvent.Permission.State.Granted,
+ permissionName = permissionName,
+ paywallIdentifier = paywallIdentifier,
+ ),
+ )
+ }
+ PermissionStatus.DENIED, PermissionStatus.UNSUPPORTED -> {
+ track(
+ InternalSuperwallEvent.Permission(
+ state = InternalSuperwallEvent.Permission.State.Denied,
+ permissionName = permissionName,
+ paywallIdentifier = paywallIdentifier,
+ ),
+ )
+ }
+ }
+
+ sendPermissionResult(
+ requestId = request.requestId,
+ permissionType = request.permissionType,
+ status = status,
+ )
+ }
+ }
+
+ /**
+ * Send a permission_result message back to the webview
+ */
+ private suspend fun sendPermissionResult(
+ requestId: String,
+ permissionType: com.superwall.sdk.permissions.PermissionType,
+ status: PermissionStatus,
+ ) {
+ val eventList =
+ listOf(
+ mapOf(
+ "event_name" to "permission_result",
+ "permission_type" to permissionType.rawValue,
+ "request_id" to requestId,
+ "status" to status.rawValue,
+ ),
+ )
+
+ val jsonString =
+ try {
+ json.encodeToString(eventList.convertToJsonElement())
+ } catch (e: Throwable) {
+ Logger.debug(
+ LogLevel.error,
+ LogScope.superwallCore,
+ "Error encoding permission result: ${e.message}",
+ error = e,
+ )
+ return
+ }
+
+ Logger.debug(
+ LogLevel.debug,
+ LogScope.superwallCore,
+ "Sending permission_result: $jsonString",
+ )
+
+ passMessageToWebView(base64String = encodeToB64(jsonString))
+ }
+
private fun detectHiddenPaywallEvent(
eventName: String,
userInfo: Map? = null,
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallWebEvent.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallWebEvent.kt
index f0e7e4ee..71b0ca9f 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallWebEvent.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallWebEvent.kt
@@ -2,6 +2,7 @@ package com.superwall.sdk.paywall.view.webview.messaging
import android.net.Uri
import com.superwall.sdk.models.paywall.LocalNotification
+import com.superwall.sdk.permissions.PermissionType
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@@ -63,4 +64,10 @@ sealed class PaywallWebEvent {
EXTERNAL("external"),
}
}
+
+ @SerialName("request_permission")
+ data class RequestPermission(
+ val permissionType: PermissionType,
+ val requestId: String,
+ ) : PaywallWebEvent()
}
diff --git a/superwall/src/main/java/com/superwall/sdk/permissions/PermissionStatus.kt b/superwall/src/main/java/com/superwall/sdk/permissions/PermissionStatus.kt
new file mode 100644
index 00000000..2fc89607
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/permissions/PermissionStatus.kt
@@ -0,0 +1,23 @@
+package com.superwall.sdk.permissions
+
+/**
+ * Permission status values matching OS-level permission states.
+ * Maps to the paywall schema permission_result status values.
+ */
+enum class PermissionStatus(
+ val rawValue: String,
+) {
+ /** User granted permission */
+ GRANTED("granted"),
+
+ /** User denied permission */
+ DENIED("denied"),
+
+ /** Platform doesn't support this permission */
+ UNSUPPORTED("unsupported"),
+ ;
+
+ companion object {
+ fun fromRaw(raw: String): PermissionStatus? = values().find { it.rawValue == raw }
+ }
+}
diff --git a/superwall/src/main/java/com/superwall/sdk/permissions/PermissionType.kt b/superwall/src/main/java/com/superwall/sdk/permissions/PermissionType.kt
new file mode 100644
index 00000000..76c3a32f
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/permissions/PermissionType.kt
@@ -0,0 +1,65 @@
+package com.superwall.sdk.permissions
+
+import android.Manifest
+import android.os.Build
+
+/**
+ * Permission types that can be requested from the host app.
+ * Maps to the paywall schema permission_type values.
+ */
+enum class PermissionType(
+ val rawValue: String,
+) {
+ NOTIFICATION("notification"),
+ LOCATION("location"),
+ BACKGROUND_LOCATION("background_location"),
+ READ_IMAGES("read_images"),
+ CONTACTS("contacts"),
+ READ_VIDEO("read_video"),
+ CAMERA("camera"),
+ ;
+
+ /**
+ * Get the Android manifest permission string for this permission type.
+ * Returns null if the permission is not available on the current API level.
+ */
+ fun toManifestPermission(): String? =
+ when (this) {
+ NOTIFICATION ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Manifest.permission.POST_NOTIFICATIONS
+ } else {
+ null // Notifications don't require runtime permission before API 33
+ }
+ LOCATION -> Manifest.permission.ACCESS_FINE_LOCATION
+ BACKGROUND_LOCATION ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ } else {
+ null // Background location not available before API 29
+ }
+ READ_IMAGES ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Manifest.permission.READ_MEDIA_IMAGES
+ } else {
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ }
+ CONTACTS -> Manifest.permission.READ_CONTACTS
+ READ_VIDEO ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Manifest.permission.READ_MEDIA_VIDEO
+ } else {
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ }
+ CAMERA -> Manifest.permission.CAMERA
+ }
+
+ companion object {
+ /**
+ * Find a PermissionType by its raw value from the paywall schema
+ * @param raw The permission type string (e.g., "notification")
+ * @return The corresponding PermissionType or null if not found
+ */
+ fun fromRaw(raw: String): PermissionType? = values().find { it.rawValue == raw }
+ }
+}
diff --git a/superwall/src/main/java/com/superwall/sdk/permissions/UserPermissions.kt b/superwall/src/main/java/com/superwall/sdk/permissions/UserPermissions.kt
new file mode 100644
index 00000000..6d569e65
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/permissions/UserPermissions.kt
@@ -0,0 +1,27 @@
+package com.superwall.sdk.permissions
+
+import android.app.Activity
+
+/**
+ * Interface for managing user permissions in a testable way.
+ * Handles permission requests from paywalls and returns results to be sent back.
+ */
+interface UserPermissions {
+ /**
+ * Check if a specific permission is granted
+ * @param permission The permission to check
+ * @return The current status of the permission
+ */
+ fun hasPermission(permission: PermissionType): PermissionStatus
+
+ /**
+ * Request a specific permission from the user
+ * @param activity The activity to use for the permission request
+ * @param permission The permission to request
+ * @return The result of the permission request
+ */
+ suspend fun requestPermission(
+ activity: Activity,
+ permission: PermissionType,
+ ): PermissionStatus
+}
diff --git a/superwall/src/main/java/com/superwall/sdk/permissions/UserPermissionsImpl.kt b/superwall/src/main/java/com/superwall/sdk/permissions/UserPermissionsImpl.kt
new file mode 100644
index 00000000..f4bc19ad
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/permissions/UserPermissionsImpl.kt
@@ -0,0 +1,230 @@
+package com.superwall.sdk.permissions
+
+import android.app.Activity
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+
+/**
+ * Implementation of UserPermissions using Android's permission system
+ */
+internal class UserPermissionsImpl(
+ private val context: Context,
+) : UserPermissions {
+ override fun hasPermission(permission: PermissionType): PermissionStatus =
+ when (permission) {
+ PermissionType.NOTIFICATION -> checkNotificationPermission()
+ PermissionType.LOCATION,
+ PermissionType.READ_IMAGES,
+ PermissionType.CONTACTS,
+ PermissionType.READ_VIDEO,
+ PermissionType.CAMERA,
+ -> checkRuntimePermission(permission)
+ PermissionType.BACKGROUND_LOCATION -> checkBackgroundLocationPermission()
+ }
+
+ private fun checkNotificationPermission(): PermissionStatus {
+ // On API 33+, check the runtime permission
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val granted =
+ ContextCompat.checkSelfPermission(
+ context,
+ android.Manifest.permission.POST_NOTIFICATIONS,
+ ) == PackageManager.PERMISSION_GRANTED
+ return if (granted) PermissionStatus.GRANTED else PermissionStatus.DENIED
+ }
+
+ // On older APIs, check if notifications are enabled in system settings
+ val notificationManager = NotificationManagerCompat.from(context)
+ return if (notificationManager.areNotificationsEnabled()) {
+ PermissionStatus.GRANTED
+ } else {
+ PermissionStatus.DENIED
+ }
+ }
+
+ private fun checkRuntimePermission(permission: PermissionType): PermissionStatus {
+ val manifestPermission =
+ permission.toManifestPermission()
+ ?: return PermissionStatus.UNSUPPORTED
+
+ val granted =
+ ContextCompat.checkSelfPermission(
+ context,
+ manifestPermission,
+ ) == PackageManager.PERMISSION_GRANTED
+ return if (granted) PermissionStatus.GRANTED else PermissionStatus.DENIED
+ }
+
+ private fun checkBackgroundLocationPermission(): PermissionStatus {
+ // Background location requires API 29+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ return PermissionStatus.UNSUPPORTED
+ }
+
+ // First check if foreground location is granted
+ val foregroundGranted =
+ ContextCompat.checkSelfPermission(
+ context,
+ android.Manifest.permission.ACCESS_FINE_LOCATION,
+ ) == PackageManager.PERMISSION_GRANTED
+
+ if (!foregroundGranted) {
+ return PermissionStatus.DENIED
+ }
+
+ // Then check background location
+ val backgroundGranted =
+ ContextCompat.checkSelfPermission(
+ context,
+ android.Manifest.permission.ACCESS_BACKGROUND_LOCATION,
+ ) == PackageManager.PERMISSION_GRANTED
+
+ return if (backgroundGranted) PermissionStatus.GRANTED else PermissionStatus.DENIED
+ }
+
+ override suspend fun requestPermission(
+ activity: Activity,
+ permission: PermissionType,
+ ): PermissionStatus =
+ when (permission) {
+ PermissionType.NOTIFICATION -> requestNotificationPermission(activity)
+ PermissionType.BACKGROUND_LOCATION -> requestBackgroundLocationPermission(activity)
+ PermissionType.LOCATION,
+ PermissionType.READ_IMAGES,
+ PermissionType.CONTACTS,
+ PermissionType.READ_VIDEO,
+ PermissionType.CAMERA,
+ -> requestStandardPermission(activity, permission)
+ }
+
+ private suspend fun requestNotificationPermission(activity: Activity): PermissionStatus {
+ // Check current status first
+ val currentStatus = hasPermission(PermissionType.NOTIFICATION)
+ if (currentStatus == PermissionStatus.GRANTED) {
+ return PermissionStatus.GRANTED
+ }
+
+ // On API < 33, we can't request notification permission at runtime
+ // The user needs to enable it in system settings
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ // Can't request at runtime - return current status
+ return currentStatus
+ }
+
+ // On API 33+, request the POST_NOTIFICATIONS permission
+ val manifestPermission =
+ PermissionType.NOTIFICATION.toManifestPermission()
+ ?: return PermissionStatus.UNSUPPORTED
+
+ return requestRuntimePermission(activity, manifestPermission)
+ }
+
+ private suspend fun requestStandardPermission(
+ activity: Activity,
+ permission: PermissionType,
+ ): PermissionStatus {
+ // Check current status first
+ val currentStatus = hasPermission(permission)
+ if (currentStatus == PermissionStatus.GRANTED) {
+ return PermissionStatus.GRANTED
+ }
+
+ val manifestPermission =
+ permission.toManifestPermission()
+ ?: return PermissionStatus.UNSUPPORTED
+
+ return requestRuntimePermission(activity, manifestPermission)
+ }
+
+ private suspend fun requestBackgroundLocationPermission(activity: Activity): PermissionStatus {
+ // Background location requires API 29+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ return PermissionStatus.UNSUPPORTED
+ }
+
+ // Check if already granted
+ val currentStatus = hasPermission(PermissionType.BACKGROUND_LOCATION)
+ if (currentStatus == PermissionStatus.GRANTED) {
+ return PermissionStatus.GRANTED
+ }
+
+ // First ensure foreground location is granted
+ val foregroundStatus = hasPermission(PermissionType.LOCATION)
+ if (foregroundStatus != PermissionStatus.GRANTED) {
+ // Request foreground location first
+ val foregroundResult = requestStandardPermission(activity, PermissionType.LOCATION)
+ if (foregroundResult != PermissionStatus.GRANTED) {
+ return PermissionStatus.DENIED
+ }
+ }
+
+ // Now request background location
+ val manifestPermission =
+ PermissionType.BACKGROUND_LOCATION.toManifestPermission()
+ ?: return PermissionStatus.UNSUPPORTED
+
+ return requestRuntimePermission(activity, manifestPermission)
+ }
+
+ private suspend fun requestRuntimePermission(
+ activity: Activity,
+ manifestPermission: String,
+ ): PermissionStatus =
+ suspendCancellableCoroutine { continuation ->
+ // Check if we can use the modern ActivityResult API
+ if (activity is ComponentActivity) {
+ // Use a one-shot launcher pattern
+ var launcher: ActivityResultLauncher? = null
+ launcher =
+ activity.activityResultRegistry.register(
+ "permission_request_${System.currentTimeMillis()}",
+ ActivityResultContracts.RequestPermission(),
+ ) { isGranted ->
+ launcher?.unregister()
+ val status = if (isGranted) PermissionStatus.GRANTED else PermissionStatus.DENIED
+ if (continuation.isActive) {
+ continuation.resume(status)
+ }
+ }
+
+ continuation.invokeOnCancellation {
+ launcher.unregister()
+ }
+
+ launcher.launch(manifestPermission)
+ } else {
+ // Fallback for non-ComponentActivity (legacy approach)
+ // This won't wait for result, so we can only check current state
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf(manifestPermission),
+ PERMISSION_REQUEST_CODE,
+ )
+
+ // Since we can't wait for result with legacy API, check after a delay
+ // This is not ideal but provides fallback compatibility
+ val isGranted =
+ ContextCompat.checkSelfPermission(
+ activity,
+ manifestPermission,
+ ) == PackageManager.PERMISSION_GRANTED
+
+ if (continuation.isActive) {
+ continuation.resume(if (isGranted) PermissionStatus.GRANTED else PermissionStatus.DENIED)
+ }
+ }
+ }
+
+ companion object {
+ private const val PERMISSION_REQUEST_CODE = 10001
+ }
+}
diff --git a/superwall/src/test/java/com/superwall/sdk/analytics/internal/trackable/InternalSuperwallEventTest.kt b/superwall/src/test/java/com/superwall/sdk/analytics/internal/trackable/InternalSuperwallEventTest.kt
index 2cefb3be..6c9f3d0b 100644
--- a/superwall/src/test/java/com/superwall/sdk/analytics/internal/trackable/InternalSuperwallEventTest.kt
+++ b/superwall/src/test/java/com/superwall/sdk/analytics/internal/trackable/InternalSuperwallEventTest.kt
@@ -850,6 +850,56 @@ class InternalSuperwallEventTest {
}
}
+ @Test
+ fun paywallPreload_startReportsPaywallCount() =
+ runTest {
+ Given("a paywall preload start event") {
+ val paywallCount = 5
+ val event =
+ InternalSuperwallEvent.PaywallPreload(
+ state = InternalSuperwallEvent.PaywallPreload.State.Start,
+ paywallCount = paywallCount,
+ )
+
+ When("parameters are requested") {
+ val params = event.getSuperwallParameters()
+
+ Then("paywall count is included") {
+ assertEquals(paywallCount, params["paywall_count"])
+ }
+
+ And("the superwall placement is paywallPreload_start") {
+ assertEquals("paywallPreload_start", event.superwallPlacement.rawName)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun paywallPreload_completeReportsPaywallCount() =
+ runTest {
+ Given("a paywall preload complete event") {
+ val paywallCount = 3
+ val event =
+ InternalSuperwallEvent.PaywallPreload(
+ state = InternalSuperwallEvent.PaywallPreload.State.Complete,
+ paywallCount = paywallCount,
+ )
+
+ When("parameters are requested") {
+ val params = event.getSuperwallParameters()
+
+ Then("paywall count is included") {
+ assertEquals(paywallCount, params["paywall_count"])
+ }
+
+ And("the superwall placement is paywallPreload_complete") {
+ assertEquals("paywallPreload_complete", event.superwallPlacement.rawName)
+ }
+ }
+ }
+ }
+
private fun stubPaywallInfo(): PaywallInfo =
PaywallInfo.empty().copy(
databaseId = "db_1",
diff --git a/superwall/src/test/java/com/superwall/sdk/config/PaywallPreloadTest.kt b/superwall/src/test/java/com/superwall/sdk/config/PaywallPreloadTest.kt
index 3f36cafd..25d1565f 100644
--- a/superwall/src/test/java/com/superwall/sdk/config/PaywallPreloadTest.kt
+++ b/superwall/src/test/java/com/superwall/sdk/config/PaywallPreloadTest.kt
@@ -59,6 +59,7 @@ class PaywallPreloadTest {
storage = storage,
assignments = assignments,
paywallManager = paywallManager,
+ track = {},
)
val config = Config.stub()
@@ -121,6 +122,7 @@ class PaywallPreloadTest {
storage = storage,
assignments = assignments,
paywallManager = paywallManager,
+ track = {},
)
When("removeUnusedPaywallVCsFromCache runs") {
diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt
index 3daa577b..147396cd 100644
--- a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt
+++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt
@@ -1,6 +1,7 @@
package com.superwall.sdk.paywall.view
import TemplateLogic
+import android.app.Activity
import android.view.View
import android.view.ViewGroup
import com.superwall.sdk.Given
@@ -20,6 +21,9 @@ import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandler
import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent
import com.superwall.sdk.paywall.view.webview.templating.models.JsonVariables
import com.superwall.sdk.paywall.view.webview.templating.models.Variables
+import com.superwall.sdk.permissions.PermissionStatus
+import com.superwall.sdk.permissions.PermissionType
+import com.superwall.sdk.permissions.UserPermissions
import com.superwall.sdk.storage.LocalStorage
import com.superwall.sdk.web.WebPaywallRedeemer
import io.mockk.coEvery
@@ -280,6 +284,15 @@ class PaywallMessageHandlerTest {
lateinit var viewRef: PaywallView
val scopeContext = scope.coroutineContext
+ val fakeUserPermissions =
+ object : UserPermissions {
+ override fun hasPermission(permission: PermissionType): PermissionStatus = PermissionStatus.GRANTED
+
+ override suspend fun requestPermission(
+ activity: Activity,
+ permission: PermissionType,
+ ): PermissionStatus = PermissionStatus.GRANTED
+ }
val messageHandler =
PaywallMessageHandler(
factory = TestVariablesFactory,
@@ -296,6 +309,8 @@ class PaywallMessageHandlerTest {
},
json = Json { encodeDefaults = true },
encodeToB64 = { it },
+ userPermissions = fakeUserPermissions,
+ getActivity = { null },
)
val state =
diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt
index 6b374f8a..dbed7919 100644
--- a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt
+++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt
@@ -1,6 +1,7 @@
package com.superwall.sdk.paywall.view
import TemplateLogic
+import android.app.Activity
import android.view.View
import android.view.ViewGroup
import com.superwall.sdk.Given
@@ -25,6 +26,9 @@ import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandler
import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent
import com.superwall.sdk.paywall.view.webview.templating.models.JsonVariables
import com.superwall.sdk.paywall.view.webview.templating.models.Variables
+import com.superwall.sdk.permissions.PermissionStatus
+import com.superwall.sdk.permissions.PermissionType
+import com.superwall.sdk.permissions.UserPermissions
import com.superwall.sdk.storage.LocalStorage
import com.superwall.sdk.web.WebPaywallRedeemer
import io.mockk.Runs
@@ -417,6 +421,15 @@ class PaywallViewTest {
lateinit var viewRef: PaywallView
val scopeContext = scope.coroutineContext
+ val fakeUserPermissions =
+ object : UserPermissions {
+ override fun hasPermission(permission: PermissionType): PermissionStatus = PermissionStatus.GRANTED
+
+ override suspend fun requestPermission(
+ activity: Activity,
+ permission: PermissionType,
+ ): PermissionStatus = PermissionStatus.GRANTED
+ }
val messageHandler =
PaywallMessageHandler(
factory = TestVariablesFactory,
@@ -433,6 +446,8 @@ class PaywallViewTest {
},
json = Json { encodeDefaults = true },
encodeToB64 = { it },
+ userPermissions = fakeUserPermissions,
+ getActivity = { null },
)
val state =
diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/PaywallMessageHandlerTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/PaywallMessageHandlerTest.kt
index 6ac2565e..615ac5a5 100644
--- a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/PaywallMessageHandlerTest.kt
+++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/PaywallMessageHandlerTest.kt
@@ -1,5 +1,6 @@
package com.superwall.sdk.paywall.view.webview
+import android.app.Activity
import com.superwall.sdk.Given
import com.superwall.sdk.Then
import com.superwall.sdk.When
@@ -21,6 +22,10 @@ import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent
import com.superwall.sdk.paywall.view.webview.messaging.parseWrappedPaywallMessages
import com.superwall.sdk.paywall.view.webview.templating.models.JsonVariables
import com.superwall.sdk.paywall.view.webview.templating.models.Variables
+import com.superwall.sdk.permissions.PermissionStatus
+import com.superwall.sdk.permissions.PermissionType
+import com.superwall.sdk.permissions.UserPermissions
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -86,6 +91,37 @@ class PaywallMessageHandlerTest {
): JsonVariables = JsonVariables("template_variables", Variables(emptyMap(), emptyMap(), emptyMap()))
}
+ private class FakeUserPermissions : UserPermissions {
+ override fun hasPermission(permission: PermissionType): PermissionStatus = PermissionStatus.GRANTED
+
+ override suspend fun requestPermission(
+ activity: Activity,
+ permission: PermissionType,
+ ): PermissionStatus = PermissionStatus.GRANTED
+ }
+
+ private fun createHandler(
+ track: suspend (com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent) -> Unit = { _ -> },
+ setAttributes: (Map) -> Unit = { _ -> },
+ ioScope: CoroutineScope = IOScope(Dispatchers.Unconfined),
+ encodeToB64: (String) -> String = { it },
+ ): PaywallMessageHandler =
+ PaywallMessageHandler(
+ factory = FakeVariablesFactory(),
+ options =
+ object : OptionsFactory {
+ override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
+ },
+ track = track,
+ setAttributes = setAttributes,
+ getView = { null },
+ mainScope = MainScope(Dispatchers.Unconfined),
+ ioScope = ioScope,
+ encodeToB64 = encodeToB64,
+ userPermissions = FakeUserPermissions(),
+ getActivity = { null },
+ )
+
@Test
fun onReady_setsVersion_via_updateState() =
runTest {
@@ -94,23 +130,12 @@ class PaywallMessageHandlerTest {
val state = PaywallViewState(paywall = paywall, locale = "en-US")
val delegate = FakeDelegate(state)
// Use a cancelled IO scope so didLoadWebView doesn't run (avoids tracking).
- val cancelledIoScope = IOScope(Dispatchers.Unconfined)
val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { _ -> },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
+ createHandler(
ioScope =
- object : kotlinx.coroutines.CoroutineScope {
+ object : CoroutineScope {
override val coroutineContext = kotlinx.coroutines.Job().apply { cancel() } + Dispatchers.Unconfined
},
- encodeToB64 = { it },
)
handler.messageHandler = delegate
@@ -134,20 +159,7 @@ class PaywallMessageHandlerTest {
val paywall = Paywall.stub()
val state = PaywallViewState(paywall = paywall, locale = "en-US")
val delegate = FakeDelegate(state)
- val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { _ -> },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
- ioScope = IOScope(Dispatchers.Unconfined),
- encodeToB64 = { it },
- )
+ val handler = createHandler()
handler.messageHandler = delegate
When("OnReady is handled and async flows complete") {
@@ -205,20 +217,7 @@ class PaywallMessageHandlerTest {
}
val tracked = mutableListOf()
- val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { evt -> tracked.add(evt.superwallPlacement.rawName) },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
- ioScope = IOScope(Dispatchers.Unconfined),
- encodeToB64 = { it },
- )
+ val handler = createHandler(track = { evt -> tracked.add(evt.superwallPlacement.rawName) })
handler.messageHandler = delegate
When("messages are handled before readiness (queue), then after ready") {
@@ -316,20 +315,7 @@ class PaywallMessageHandlerTest {
resultCallback?.invoke(null)
}
}
- val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { _ -> },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
- ioScope = IOScope(Dispatchers.Unconfined),
- encodeToB64 = { it },
- )
+ val handler = createHandler()
handler.messageHandler = delegate
When("OnReady is handled then RestoreFailed arrives") {
@@ -366,20 +352,7 @@ class PaywallMessageHandlerTest {
deepLinks.add(url)
}
}
- val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { _ -> },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
- ioScope = IOScope(Dispatchers.Unconfined),
- encodeToB64 = { it },
- )
+ val handler = createHandler()
handler.messageHandler = delegate
When("Custom message arrives") {
@@ -410,20 +383,7 @@ class PaywallMessageHandlerTest {
resultCallback?.invoke(null)
}
}
- val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { _ -> },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
- ioScope = IOScope(Dispatchers.Unconfined),
- encodeToB64 = { it },
- )
+ val handler = createHandler()
handler.messageHandler = delegate
When("OnReady then PaywallOpen/PaywallClose are handled") {
@@ -459,20 +419,7 @@ class PaywallMessageHandlerTest {
events.add(paywallWebEvent)
}
}
- val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { _ -> },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
- ioScope = IOScope(Dispatchers.Unconfined),
- encodeToB64 = { it },
- )
+ val handler = createHandler()
handler.messageHandler = delegate
When("OpenUrl, OpenUrlInBrowser, OpenDeepLink arrive") {
@@ -505,20 +452,7 @@ class PaywallMessageHandlerTest {
events.add(paywallWebEvent)
}
}
- val handler =
- PaywallMessageHandler(
- factory = FakeVariablesFactory(),
- options =
- object : OptionsFactory {
- override fun makeSuperwallOptions(): SuperwallOptions = SuperwallOptions()
- },
- track = { _ -> },
- setAttributes = { _ -> },
- getView = { null },
- mainScope = MainScope(Dispatchers.Unconfined),
- ioScope = IOScope(Dispatchers.Unconfined),
- encodeToB64 = { it },
- )
+ val handler = createHandler()
handler.messageHandler = delegate
When("RequestReview external arrives") {
@@ -543,20 +477,7 @@ class PaywallMessageHandlerTest {
val state = PaywallViewState(paywall = paywall, locale = "en-US")
val delegate = FakeDelegate(state)
val capturedAttributes = mutableListOf