Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub.

## 2.7.12

## Enhancements
- Add customer info to paywall info and tracked events
- Add stripe/paddle intro offer eligibility

## Fixes
- Fix dismiss animation for bottom sheets and modals on newer Samsung devices

## 2.7.11

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Then
import When
import com.superwall.sdk.misc.Either
import com.superwall.sdk.misc.IOScope
import com.superwall.sdk.models.customer.CustomerInfo
import com.superwall.sdk.models.events.EventData
import com.superwall.sdk.models.paywall.Paywall
import com.superwall.sdk.models.paywall.PaywallIdentifier
Expand Down Expand Up @@ -86,6 +87,7 @@ class PaywallRequestManagerTest {
coEvery { factory.makeDeviceInfo() } returns DeviceInfo("123", "en_US")

coEvery { factory.activePaywallId() } returns null
coEvery { factory.currentCustomerInfo() } returns CustomerInfo.empty()

When("getting the paywall") {
val result = paywallRequestManager.getPaywall(request)
Expand Down Expand Up @@ -130,6 +132,7 @@ class PaywallRequestManagerTest {
coEvery { factory.makeDeviceInfo() } returns DeviceInfo("123", "en_US")

coEvery { factory.activePaywallId() } returns null
coEvery { factory.currentCustomerInfo() } returns CustomerInfo.empty()

// Make first request to cache the paywall
paywallRequestManager.getPaywall(request)
Expand Down Expand Up @@ -177,6 +180,7 @@ class PaywallRequestManagerTest {
coEvery { factory.makeDeviceInfo() } returns DeviceInfo("123", "en_US")

coEvery { factory.activePaywallId() } returns null
coEvery { factory.currentCustomerInfo() } returns CustomerInfo.empty()

When("making multiple concurrent requests") {
val results =
Expand All @@ -203,6 +207,7 @@ class PaywallRequestManagerTest {
fun test_preload_active_paywall() =
runTest {
manager(this@runTest.coroutineContext)
every { factory.currentCustomerInfo() } returns CustomerInfo.empty()
Given("a preload request for an active paywall") {
val paywallId = "test_paywall"
val originalExperiment =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ sealed class TrackingLogic {
input?.let { value ->
when (value) {
is List<*> -> null
is LinkedHashMap<*, *> -> value.mapValues { clean(it.value) }.filterValues { it != null }.toMap()
is Map<*, *> -> value.mapValues { clean(it.value) }.filterValues { it != null }.toMap()
is Map<*, *> -> value
is String -> value
is Int, is Float, is Double, is Long, is Boolean -> value
is JsonElement -> value.convertFromJsonElement()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.superwall.sdk.config.ConfigManager
import com.superwall.sdk.config.PaywallPreload
import com.superwall.sdk.config.options.SuperwallOptions
import com.superwall.sdk.customer.CustomerInfoManager
import com.superwall.sdk.models.customer.CustomerInfo
import com.superwall.sdk.debug.DebugManager
import com.superwall.sdk.debug.DebugView
import com.superwall.sdk.deeplinks.DeepLinkRouter
Expand Down Expand Up @@ -179,6 +180,7 @@ class DependencyContainer(
GoogleBillingWrapper.Factory,
ClassifierDataFactory,
ExperimentalPropertiesFactory,
CustomerInfoFactory,
WebPaywallRedeemer.Factory {
internal val getPaywallComponentsFactory: GetPaywallComponentsFactory by lazy {
DefaultGetPaywallComponentsFactory(Superwall.instance)
Expand Down Expand Up @@ -841,6 +843,8 @@ class DependencyContainer(
?.paywall
?.identifier

override fun currentCustomerInfo(): CustomerInfo = Superwall.instance.getCustomerInfo()

override fun makeDeviceInfo(): DeviceInfo =
DeviceInfo(
appInstalledAtString = deviceHelper.appInstalledAtString,
Expand Down Expand Up @@ -1108,6 +1112,9 @@ class DependencyContainer(

override fun delegate(): SuperwallDelegateAdapter = delegateAdapter

override fun customerInfoFlow(): StateFlow<CustomerInfo> =
Superwall.instance.customerInfo

override fun updatePaywallInfo(paywallInfo: PaywallInfo) {
Superwall.instance.presentationItems.paywallInfo = paywallInfo
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.superwall.sdk.misc.IOScope
import com.superwall.sdk.misc.MainScope
import com.superwall.sdk.models.config.ComputedPropertyRequest
import com.superwall.sdk.models.config.FeatureFlags
import com.superwall.sdk.models.customer.CustomerInfo
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import com.superwall.sdk.models.events.EventData
import com.superwall.sdk.models.paywall.Paywall
Expand Down Expand Up @@ -147,6 +148,10 @@ interface DelegateAdapterFactory {
fun delegate(): SuperwallDelegateAdapter
}

fun interface CustomerInfoFactory {
fun customerInfoFlow(): StateFlow<CustomerInfo>
}

interface PresentationFactory {
fun updatePaywallInfo(paywallInfo: PaywallInfo)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import com.superwall.sdk.models.product.Store
import com.superwall.sdk.storage.LatestDeviceCustomerInfo
import com.superwall.sdk.storage.LatestRedemptionResponse
import com.superwall.sdk.storage.Storage
import com.superwall.sdk.storage.core_data.convertFromJsonElement
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy
import kotlinx.serialization.json.jsonObject

/**
* A class that contains the latest subscription and entitlement info about the customer.
Expand Down Expand Up @@ -46,6 +50,19 @@ data class CustomerInfo(
.map { it.productId }
.toSet()

/**
* Serializes the full CustomerInfo into a params map suitable for templates and analytics.
*
* Returns an empty map when [isPlaceholder] is true — callers should not send partial data
* before CustomerInfo has loaded.
*/
fun toParams(): Map<String, Any?> {
if (isPlaceholder) return emptyMap()
val obj = paramsJson.encodeToJsonElement(serializer(), this).jsonObject
return obj
.mapValues { (_, value) -> value.convertFromJsonElement() }
}
Comment thread
ianrumac marked this conversation as resolved.

override fun toString(): String =
buildString {
appendLine("CustomerInfo(")
Expand All @@ -70,6 +87,16 @@ data class CustomerInfo(
}

companion object {
private val paramsJson = Json {
try {

namingStrategy = JsonNamingStrategy.SnakeCase
} catch (e: Throwable) {
}
encodeDefaults = true;
ignoreUnknownKeys = true
}

/**
* Creates a blank CustomerInfo instance for testing or default states.
*/
Expand Down Expand Up @@ -127,14 +154,16 @@ data class CustomerInfo(
when (subscriptionStatus) {
is SubscriptionStatus.Active ->
subscriptionStatus.entitlements.filter { it.store == Store.PLAY_STORE }

SubscriptionStatus.Inactive,
SubscriptionStatus.Unknown,
-> emptyList()
-> emptyList()
}

// Merge: active from external controller + all web + inactive device
// This gives us complete history while respecting external controller as source of truth
val allEntitlements = externalEntitlements + webCustomerInfo.entitlements + inactiveDeviceEntitlements
val allEntitlements =
externalEntitlements + webCustomerInfo.entitlements + inactiveDeviceEntitlements
val finalEntitlements = mergeEntitlementsPrioritized(allEntitlements).sortedBy { it.id }

return CustomerInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.superwall.sdk.models.paywall

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
enum class IntroOfferEligibility {
@SerialName("eligible")
ELIGIBLE,

@SerialName("ineligible")
INELIGIBLE,

@SerialName("automatic")
AUTOMATIC,
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ data class Paywall(
val isScrollEnabled: Boolean? = true,
@SerialName("reroute_back_button")
val rerouteBackButton: ToggleMode? = null,
@SerialName("intro_offer_eligibility")
val introOfferEligibility: IntroOfferEligibility = IntroOfferEligibility.AUTOMATIC,
) : SerializableEntity {
val playStoreProducts: List<CrossplatformProduct>
get() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.superwall.sdk.logger.Logger
import com.superwall.sdk.misc.camelCaseToSnakeCase
import com.superwall.sdk.models.config.ComputedPropertyRequest
import com.superwall.sdk.models.config.FeatureGatingBehavior
import com.superwall.sdk.models.customer.CustomerInfo
import com.superwall.sdk.models.events.EventData
import com.superwall.sdk.models.paywall.LocalNotification
import com.superwall.sdk.models.paywall.PaywallPresentationInfo
Expand Down Expand Up @@ -62,6 +63,7 @@ data class PaywallInfo(
val isScrollEnabled: Boolean,
@Serializable(with = AnyMapSerializer::class)
val state: Map<String, Any> = emptyMap(),
val customerInfo: CustomerInfo = CustomerInfo.empty(),
) {
constructor(
databaseId: String,
Expand Down Expand Up @@ -96,6 +98,7 @@ data class PaywallInfo(
cacheKey: String,
isScrollEnabled: Boolean,
state: Map<String, Any> = emptyMap(),
customerInfo: CustomerInfo = CustomerInfo.empty(),
) : this(
databaseId = databaseId,
identifier = identifier,
Expand Down Expand Up @@ -183,6 +186,7 @@ data class PaywallInfo(
buildId = buildId,
isScrollEnabled = isScrollEnabled,
state = state,
customerInfo = customerInfo,
)

fun eventParams(
Expand Down Expand Up @@ -217,6 +221,10 @@ data class PaywallInfo(
"is_scroll_enabled" to isScrollEnabled,
"state" to state,
)
val customerParams = customerInfo.toParams()
if (customerParams.isNotEmpty()) {
params["customer_info"] = customerParams
}
params.values.removeAll { it == null }
val filteredParams = params as MutableMap<String, Any>
output.putAll(filteredParams)
Expand Down
Loading
Loading