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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.junit.After
Expand Down Expand Up @@ -93,7 +92,7 @@ class PaywallViewDismissTest {

@Test
fun dismiss_purchased_emits_dismissed_and_clears_publisher() =
runTest {
runBlocking {
var callbackShouldDismiss: Boolean? = null
val finished = kotlinx.coroutines.CompletableDeferred<Unit>()
val callback =
Expand All @@ -114,34 +113,30 @@ class PaywallViewDismissTest {
val publisher = MutableSharedFlow<PaywallState>(replay = 1, extraBufferCapacity = 1)
val request = makeRequest()
Given("a paywall view configured with a dismissal callback") {
runBlocking {
withContext(Dispatchers.Main) {
view.set(request, publisher, null)
view.onViewCreated()
}
withContext(Dispatchers.Main) {
view.set(request, publisher, null)
view.onViewCreated()
}

When("the paywall is dismissed after a purchase due to system logic") {
runBlocking {
withContext(Dispatchers.Main) {
view.dismiss(
result = PaywallResult.Purchased(productId = "product1"),
closeReason = PaywallCloseReason.SystemLogic,
)
}
withContext(Dispatchers.Main) {
view.dismiss(
result = PaywallResult.Purchased(productId = "product1"),
closeReason = PaywallCloseReason.SystemLogic,
)
}

delayFor(100.milliseconds)
delayFor(100.milliseconds)

withContext(Dispatchers.Main) {
view.beforeOnDestroy()
view.destroyed()
}
withContext(Dispatchers.Main) {
view.beforeOnDestroy()
view.destroyed()
}

withContext(Dispatchers.IO) {
try {
withTimeout(3000) { finished.await() }
} catch (_: Throwable) {
}
withContext(Dispatchers.IO) {
try {
withTimeout(3000) { finished.await() }
} catch (_: Throwable) {
}
}

Expand All @@ -168,7 +163,7 @@ class PaywallViewDismissTest {

@Test
fun dismiss_declined_for_next_paywall_does_not_clear_publisher() =
runTest {
runBlocking {
val callback =
object : PaywallViewCallback {
override fun onFinished(
Expand All @@ -185,23 +180,19 @@ class PaywallViewDismissTest {
val publisher = MutableSharedFlow<PaywallState>(replay = 1, extraBufferCapacity = 1)
val request = makeRequest()
Given("a paywall view configured to continue to the next paywall") {
runBlocking {
withContext(Dispatchers.Main) {
view.set(request, publisher, null)
view.onViewCreated()
}
withContext(Dispatchers.Main) {
view.set(request, publisher, null)
view.onViewCreated()
}

When("the paywall is dismissed as declined for the next paywall") {
runBlocking {
withContext(Dispatchers.Main) {
view.dismiss(
result = PaywallResult.Declined(),
closeReason = PaywallCloseReason.ForNextPaywall,
)
view.beforeOnDestroy()
view.destroyed()
}
withContext(Dispatchers.Main) {
view.dismiss(
result = PaywallResult.Declined(),
closeReason = PaywallCloseReason.ForNextPaywall,
)
view.beforeOnDestroy()
view.destroyed()
}

val dismissed = publisher.replayCache.lastOrNull() as? PaywallState.Dismissed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,29 +309,24 @@ class TransactionManagerTest {
Then("The purchase is successful") {
assert(result is PurchaseResult.Purchased)
coVerify { storeManager.loadPurchasedProducts(any()) }
And("Verify event order") {
And("Verify transaction events") {
val transactionEvents =
events.value.filterIsInstance<InternalSuperwallEvent.Transaction>()

assert(
transactionEvents.first().superwallPlacement
is SuperwallEvent.TransactionStart,
)
assert(transactionEvents.first().product?.fullIdentifier == "product1")
assert(
transactionEvents.last().superwallPlacement
is SuperwallEvent.TransactionComplete,
)
assert(
(
transactionEvents.last().superwallPlacement
as SuperwallEvent.TransactionComplete
).transaction!!
.originalTransactionIdentifier != null,
)
val start =
transactionEvents
.single { it.superwallPlacement is SuperwallEvent.TransactionStart }
assert(start.product?.fullIdentifier == "product1")

val complete =
transactionEvents
.mapNotNull { it.superwallPlacement as? SuperwallEvent.TransactionComplete }
.single()
assert(complete.transaction!!.originalTransactionIdentifier != null)

val purchase =
events.value.filterIsInstance<InternalSuperwallEvent.NonRecurringProductPurchase>()
assert(purchase.first().product?.fullIdentifier == "product1")
assert(purchase.any { it.product?.fullIdentifier == "product1" })
}
}
}
Expand Down Expand Up @@ -419,16 +414,18 @@ class TransactionManagerTest {
Then("The purchase is successful") {
assert(result is PurchaseResult.Purchased)
coVerify { storeManager.loadPurchasedProducts(any()) }
And("Verify event order") {
And("Verify transaction events") {
val transactionEvents =
events.value.filterIsInstance<InternalSuperwallEvent.Transaction>()
assert(transactionEvents.first().superwallPlacement is SuperwallEvent.TransactionStart)
val complete = transactionEvents.last().superwallPlacement
assert(complete is SuperwallEvent.TransactionComplete)
assert((complete as SuperwallEvent.TransactionComplete).transaction!!.originalTransactionIdentifier != null)
assert(transactionEvents.any { it.superwallPlacement is SuperwallEvent.TransactionStart })
val complete =
transactionEvents
.mapNotNull { it.superwallPlacement as? SuperwallEvent.TransactionComplete }
.single()
assert(complete.transaction!!.originalTransactionIdentifier != null)
val purchase =
events.value.filterIsInstance<InternalSuperwallEvent.NonRecurringProductPurchase>()
assert(purchase.first().product?.fullIdentifier == "product1")
assert(purchase.any { it.product?.fullIdentifier == "product1" })
}
}
}
Expand Down Expand Up @@ -772,8 +769,8 @@ class TransactionManagerTest {
val restoreEvents =
events.value.filterIsInstance<InternalSuperwallEvent.Restore>()
assert(restoreEvents.size == 2)
assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start)
assert(restoreEvents[1].state is InternalSuperwallEvent.Restore.State.Complete)
assert(restoreEvents.any{ it.state is InternalSuperwallEvent.Restore.State.Start })
assert(restoreEvents.any{ it.state is InternalSuperwallEvent.Restore.State.Complete })
}
}
}
Expand Down Expand Up @@ -802,8 +799,8 @@ class TransactionManagerTest {
val restoreEvents =
events.value.filterIsInstance<InternalSuperwallEvent.Restore>()
assert(restoreEvents.size == 2)
assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start)
assert(restoreEvents[1].state is InternalSuperwallEvent.Restore.State.Complete)
assert(restoreEvents.any { it.state is InternalSuperwallEvent.Restore.State.Start })
assert(restoreEvents.any { it.state is InternalSuperwallEvent.Restore.State.Complete })
}
}
}
Expand Down Expand Up @@ -836,8 +833,8 @@ class TransactionManagerTest {
val restoreEvents =
events.value.filterIsInstance<InternalSuperwallEvent.Restore>()
assert(restoreEvents.size == 2)
assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start)
assert(restoreEvents[1].state is InternalSuperwallEvent.Restore.State.Failure)
assert(restoreEvents.any { it.state is InternalSuperwallEvent.Restore.State.Start })
assert(restoreEvents.any { it.state is InternalSuperwallEvent.Restore.State.Failure })
}
}
}
Expand Down Expand Up @@ -868,9 +865,11 @@ class TransactionManagerTest {
val restoreEvents =
events.value.filterIsInstance<InternalSuperwallEvent.Restore>()
assert(restoreEvents.size == 2)
assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start)
val failure: InternalSuperwallEvent.Restore.State.Failure =
restoreEvents[1].state as InternalSuperwallEvent.Restore.State.Failure
assert(restoreEvents.any { it.state is InternalSuperwallEvent.Restore.State.Start })
val failure =
restoreEvents
.mapNotNull { it.state as? InternalSuperwallEvent.Restore.State.Failure }
.single()
assert(failure.reason.contains("\"inactive\""))
}
}
Expand Down
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()
}
32 changes: 21 additions & 11 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 @@ -827,18 +829,26 @@ class Superwall(
}

/**
* Asynchronously resets. Presentation of paywalls is suspended until reset completes.
* Coordinates reset through the identity actor. 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
Loading
Loading