Skip to content
Open
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
33 changes: 33 additions & 0 deletions superwall/src/main/java/com/superwall/sdk/SdkContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.superwall.sdk

import com.superwall.sdk.config.ConfigManager
import com.superwall.sdk.misc.awaitFirstValidConfig
import com.superwall.sdk.models.config.Config

/**
* Cross-slice bridge used by the identity actor to call into other managers.
*
* Keeps the identity slice decoupled from concrete manager types.
*/
interface SdkContext {
fun reevaluateTestMode(appUserId: String?, aliasId: String?)

suspend fun fetchAssignments()

suspend fun awaitConfig(): Config?
}

class SdkContextImpl(
private val configManager: () -> ConfigManager,
) : SdkContext {
override fun reevaluateTestMode(appUserId: String?, aliasId: String?) {
configManager().reevaluateTestMode(appUserId = appUserId, aliasId = aliasId)
}

override suspend fun fetchAssignments() {
configManager().getAssignments()
}

override suspend fun awaitConfig(): Config? =
configManager().configState.awaitFirstValidConfig()
}
29 changes: 19 additions & 10 deletions superwall/src/main/java/com/superwall/sdk/Superwall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,9 @@ class Superwall(
}
// Implicitly wait
dependencyContainer.configManager.fetchConfiguration()
dependencyContainer.identityManager.configure()
dependencyContainer.identityManager.configure(
neverCalledStaticConfig = dependencyContainer.storage.neverCalledStaticConfig,
)
}.toResult().fold({
CoroutineScope(Dispatchers.Main).launch {
completion?.invoke(Result.success(Unit))
Expand Down Expand Up @@ -829,16 +831,23 @@ class Superwall(
/**
* Asynchronously resets. Presentation of paywalls is suspended until reset completes.
*/
internal fun reset(duringIdentify: Boolean) {
internal fun reset(duringIdentify: Boolean = false) {
withErrorTracking {
dependencyContainer.identityManager.reset(duringIdentify)
dependencyContainer.storage.reset()
dependencyContainer.paywallManager.resetCache()
presentationItems.reset()
dependencyContainer.configManager.reset()
dependencyContainer.reedemer.clear(RedemptionOwnershipType.AppUser)
ioScope.launch {
track(InternalSuperwallEvent.Reset)
if (!duringIdentify) {
// Public reset — delegate to identity actor which coordinates
// dropping readiness, running cleanup, and restoring readiness.
dependencyContainer.identityManager.reset()
} else {
// Called from identity actor's completeReset during identify
// or full reset — just do cleanup without touching identity.
dependencyContainer.storage.reset()
dependencyContainer.paywallManager.resetCache()
presentationItems.reset()
dependencyContainer.configManager.reset()
dependencyContainer.reedemer.clear(RedemptionOwnershipType.AppUser)
ioScope.launch {
track(InternalSuperwallEvent.Reset)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import android.webkit.WebSettings
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.ViewModelProvider
import com.android.billingclient.api.Purchase
import com.superwall.sdk.SdkContextImpl
import com.superwall.sdk.SdkContext
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.AttributionManager
import com.superwall.sdk.analytics.ClassifierDataFactory
Expand All @@ -34,8 +36,12 @@ import com.superwall.sdk.debug.DebugView
import com.superwall.sdk.deeplinks.DeepLinkRouter
import com.superwall.sdk.delegate.SuperwallDelegateAdapter
import com.superwall.sdk.delegate.subscription_controller.PurchaseController
import com.superwall.sdk.identity.IdentityContext
import com.superwall.sdk.identity.IdentityInfo
import com.superwall.sdk.identity.IdentityManager
import com.superwall.sdk.identity.IdentityPersistenceInterceptor
import com.superwall.sdk.identity.IdentityState
import com.superwall.sdk.identity.createInitialIdentityState
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.logger.Logger
Expand All @@ -44,6 +50,8 @@ import com.superwall.sdk.misc.AppLifecycleObserver
import com.superwall.sdk.misc.CurrentActivityTracker
import com.superwall.sdk.misc.IOScope
import com.superwall.sdk.misc.MainScope
import com.superwall.sdk.misc.primitives.DebugInterceptor
import com.superwall.sdk.misc.primitives.SequentialActor
import com.superwall.sdk.models.config.ComputedPropertyRequest
import com.superwall.sdk.models.config.FeatureFlags
import com.superwall.sdk.models.entitlements.SubscriptionStatus
Expand Down Expand Up @@ -125,7 +133,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.ClassDiscriminatorMode
import kotlinx.serialization.json.Json
import java.lang.ref.WeakReference
Expand Down Expand Up @@ -254,7 +261,13 @@ class DependencyContainer(
this,
)
storage =
LocalStorage(context = context, ioScope = ioScope(), factory = this, json = json(), _apiKey = apiKey)
LocalStorage(
context = context,
ioScope = ioScope(),
factory = this,
json = json(),
_apiKey = apiKey
)
entitlements = Entitlements(storage)
testModeManager = TestModeManager(storage)
testModeTransactionHandler =
Expand Down Expand Up @@ -400,6 +413,18 @@ class DependencyContainer(
},
)

// Identity actor setup
val initialIdentity = createInitialIdentityState(storage, deviceHelper.appInstalledAtString)
val identityActor = SequentialActor<IdentityContext, IdentityState>(initialIdentity)

DebugInterceptor.install(identityActor, name = "Identity")
IdentityPersistenceInterceptor.install(identityActor, storage)

val sdkContext: SdkContext =
SdkContextImpl(
configManager = { configManager },
)

configManager =
ConfigManager(
context = context,
Expand Down Expand Up @@ -427,14 +452,12 @@ class DependencyContainer(
entitlements.setSubscriptionStatus(status)
},
)

identityManager =
IdentityManager(
storage = storage,
deviceHelper = deviceHelper,
configManager = configManager,
neverCalledStaticConfig = {
storage.neverCalledStaticConfig
},
options = { options },
ioScope = ioScope,
stringToSha = {
val bytes = this.toString().toByteArray()
Expand All @@ -445,6 +468,8 @@ class DependencyContainer(
notifyUserChange = {
delegate().userAttributesDidChange(it)
},
actor = identityActor,
sdkContext = sdkContext,
)

reedemer =
Expand Down Expand Up @@ -591,9 +616,9 @@ class DependencyContainer(

val paywallActivity =
(
paywallView.encapsulatingActivity?.get()
?: activityProvider?.getCurrentActivity()
) as? SuperwallPaywallActivity
paywallView.encapsulatingActivity?.get()
?: activityProvider?.getCurrentActivity()
) as? SuperwallPaywallActivity

if (paywallActivity != null) {
ioScope.launch {
Expand All @@ -610,7 +635,12 @@ class DependencyContainer(
)
}
// Await message delivery to ensure webview has time to process before dismiss
paywallView.webView.messageHandler.handle(PaywallMessage.TrialStarted(trialEndDate, id))
paywallView.webView.messageHandler.handle(
PaywallMessage.TrialStarted(
trialEndDate,
id
)
)
}
},
)
Expand Down Expand Up @@ -702,8 +732,8 @@ class DependencyContainer(
"X-Low-Power-Mode" to deviceHelper.isLowPowerModeEnabled.toString(),
"X-Is-Sandbox" to deviceHelper.isSandbox.toString(),
"X-Entitlement-Status" to
Superwall.instance.entitlements.status.value
.toString(),
Superwall.instance.entitlements.status.value
.toString(),
"Content-Type" to "application/json",
"X-Current-Time" to dateFormat(DateUtils.ISO_MILLIS).format(Date()),
"X-Static-Config-Build-Id" to (configManager.config?.buildId ?: ""),
Expand Down Expand Up @@ -800,7 +830,8 @@ class DependencyContainer(
return view
}

override fun makeCache(): PaywallViewCache = PaywallViewCache(context, makeViewStore(), activityProvider!!, deviceHelper)
override fun makeCache(): PaywallViewCache =
PaywallViewCache(context, makeViewStore(), activityProvider!!, deviceHelper)

override fun activePaywallId(): String? =
paywallManager.currentView
Expand Down Expand Up @@ -835,9 +866,11 @@ class DependencyContainer(
audienceFilterParams = HashMap(identityManager.userAttributes),
)

override fun makeHasExternalPurchaseController(): Boolean = storeManager.purchaseController.hasExternalPurchaseController
override fun makeHasExternalPurchaseController(): Boolean =
storeManager.purchaseController.hasExternalPurchaseController

override fun makeHasInternalPurchaseController(): Boolean = storeManager.purchaseController.hasInternalPurchaseController
override fun makeHasInternalPurchaseController(): Boolean =
storeManager.purchaseController.hasInternalPurchaseController

override fun isWebToAppEnabled(): Boolean = configManager.config?.featureFlags?.web2App ?: false

Expand Down Expand Up @@ -927,7 +960,8 @@ class DependencyContainer(

override fun makeFeatureFlags(): FeatureFlags? = configManager.config?.featureFlags

override fun makeComputedPropertyRequests(): List<ComputedPropertyRequest> = configManager.config?.allComputedProperties ?: emptyList()
override fun makeComputedPropertyRequests(): List<ComputedPropertyRequest> =
configManager.config?.allComputedProperties ?: emptyList()

override suspend fun makeIdentityInfo(): IdentityInfo =
IdentityInfo(
Expand Down Expand Up @@ -965,7 +999,8 @@ class DependencyContainer(
appSessionId = appSessionManager.appSession.id,
)

override suspend fun activeProductIds(): List<String> = storeManager.receiptManager.purchases.toList()
override suspend fun activeProductIds(): List<String> =
storeManager.receiptManager.purchases.toList()

override suspend fun makeIdentityManager(): IdentityManager = identityManager

Expand All @@ -992,7 +1027,8 @@ class DependencyContainer(
get() = ViewModelFactory()
private val vmProvider = ViewModelProvider(storeOwner, vmFactory)

override fun makeViewStore(): ViewStorageViewModel = vmProvider[ViewStorageViewModel::class.java]
override fun makeViewStore(): ViewStorageViewModel =
vmProvider[ViewStorageViewModel::class.java]

private var _mainScope: MainScope? = null
private var _ioScope: IOScope? = null
Expand Down Expand Up @@ -1056,15 +1092,17 @@ class DependencyContainer(

override fun context(): Context = context

override fun experimentalProperties(): Map<String, Any> = storeManager.receiptManager.experimentalProperties()
override fun experimentalProperties(): Map<String, Any> =
storeManager.receiptManager.experimentalProperties()

override fun getCurrentUserAttributes(): Map<String, Any> = identityManager.userAttributes

override fun demandTier(): String? = deviceHelper.demandTier

override fun demandScore(): Int? = deviceHelper.demandScore

override suspend fun track(event: TrackableSuperwallEvent): Result<TrackingResult> = Superwall.instance.track(event)
override suspend fun track(event: TrackableSuperwallEvent): Result<TrackingResult> =
Superwall.instance.track(event)

override fun delegate(): SuperwallDelegateAdapter = delegateAdapter

Expand Down Expand Up @@ -1098,7 +1136,8 @@ class DependencyContainer(
delegateAdapter.didRedeemLink(redemptionResult)
}

override fun maxAge(): Long = configManager.config?.webToAppConfig?.entitlementsMaxAgeMs ?: 86400000L
override fun maxAge(): Long =
configManager.config?.webToAppConfig?.entitlementsMaxAgeMs ?: 86400000L

override fun getActiveDeviceEntitlements(): Set<com.superwall.sdk.models.entitlements.Entitlement> =
entitlements.activeDeviceEntitlements
Expand Down Expand Up @@ -1140,7 +1179,8 @@ class DependencyContainer(
?.flatMap { entitlements.byProductId(it) }
?.toSet() ?: emptySet()

override fun getPaywallInfo(): PaywallInfo = Superwall.instance.paywallView?.info ?: PaywallInfo.empty()
override fun getPaywallInfo(): PaywallInfo =
Superwall.instance.paywallView?.info ?: PaywallInfo.empty()

override fun trackRestorationFailed(message: String) {
trackRestorationFailure(message)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.superwall.sdk.identity

import com.superwall.sdk.SdkContext
import com.superwall.sdk.analytics.internal.trackable.Trackable
import com.superwall.sdk.misc.primitives.BaseContext
import com.superwall.sdk.network.device.DeviceHelper
import com.superwall.sdk.store.testmode.TestModeManager
import com.superwall.sdk.web.WebPaywallRedeemer

/**
* All dependencies available to identity [IdentityState.Actions].
*
* Cross-slice dispatch goes through [sdkContext]. Config reads use [configManager].
*/
interface IdentityContext : BaseContext<IdentityState, IdentityContext> {
val sdkContext: SdkContext
val webPaywallRedeemer: (() -> WebPaywallRedeemer)?
val testModeManager: TestModeManager?
val deviceHelper: DeviceHelper
val completeReset: () -> Unit
val track: suspend (Trackable) -> Unit
val notifyUserChange: ((Map<String, Any>) -> Unit)?
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,11 @@ object IdentityLogic {
transformedKey = transformedKey.replace("\$", "")
}

when (val value = entry.value) {
is List<*> -> mergedAttributes[transformedKey] = value.filterNotNull()
is Map<*, *> -> {
val nonNullMap = value.filterValues { it != null }
mergedAttributes[transformedKey] = nonNullMap.filterKeys { it != null }
}
null -> mergedAttributes.remove(transformedKey)
else -> mergedAttributes[transformedKey] = value
val value = entry.value
if (value != null) {
mergedAttributes[transformedKey] = value
} else {
mergedAttributes.remove(transformedKey)
}
}

Expand Down
Loading
Loading