Skip to content
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.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.superwall.sdk.config

import And
import Given
import Then
import When
Expand All @@ -10,7 +9,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.Tier
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.options.SuperwallOptions
import com.superwall.sdk.dependencies.DependencyContainer
import com.superwall.sdk.misc.Either
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.superwall.sdk.paywall.view

import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Looper
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.superwall.sdk.misc.ActivityProvider
import com.superwall.sdk.misc.primitives.SequentialActor
import com.superwall.sdk.network.device.DeviceHelper
import com.superwall.sdk.paywall.manager.PaywallCacheState
import com.superwall.sdk.paywall.manager.PaywallViewCache
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.ConcurrentHashMap

/**
* PaywallViewCache builds the shared loading and shimmer views on its actor's
* IO thread, which has no Looper. These tests run on a real device to prove
* that construction is safe there and that the views still attach, draw and
* animate once handed to the main thread.
*/
@RunWith(AndroidJUnit4::class)
class OffMainViewConstructionTest {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val ctx = instrumentation.targetContext

private fun exerciseOnMain(vararg views: View) {
instrumentation.runOnMainSync {
views.forEach { view ->
view.measure(
View.MeasureSpec.makeMeasureSpec(400, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(800, View.MeasureSpec.EXACTLY),
)
view.layout(0, 0, 400, 800)
(view as? PaywallShimmerView)?.showShimmer()
(view as? PaywallPurchaseLoadingView)?.showLoading()
view.draw(Canvas(Bitmap.createBitmap(400, 800, Bitmap.Config.ARGB_8888)))
(view as? PaywallShimmerView)?.hideShimmer()
}
}
}

@Test
fun loadingAndShimmerCanBeBuiltOnAThreadWithoutALooper() {
var error: Throwable? = null
var hadLooper = true
var loading: LoadingView? = null
var shimmer: ShimmerView? = null
val thread =
Thread {
hadLooper = Looper.myLooper() != null
try {
loading = LoadingView(ctx, loadingColor = android.R.color.black)
shimmer = ShimmerView(ctx)
} catch (t: Throwable) {
error = t
}
}
thread.start()
thread.join()

assertTrue("background thread must not have a Looper", !hadLooper)
assertNull("construction off main threw: $error", error)
exerciseOnMain(loading!!, shimmer!!)
}

@Test
fun cacheAcquireFromMainReturnsUsableViewsWithoutDeadlocking() {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
try {
val cache =
PaywallViewCache(
ctx,
object : ViewStorage {
override val views = ConcurrentHashMap<String, View>()
},
mockk<ActivityProvider> { every { getCurrentActivity() } returns null },
mockk<DeviceHelper> { every { locale } returns "en_US" },
actor = SequentialActor(PaywallCacheState(), scope),
)
var loading: PaywallPurchaseLoadingView? = null
var shimmer: PaywallShimmerView? = null
instrumentation.runOnMainSync {
loading = cache.acquireLoadingView()
shimmer = cache.acquireShimmerView()
}
exerciseOnMain(loading as View, shimmer as View)
} finally {
scope.cancel()
}
}
}
16 changes: 9 additions & 7 deletions superwall/src/main/java/com/superwall/sdk/Superwall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent.*
import com.superwall.sdk.analytics.superwall.SuperwallEventInfo
import com.superwall.sdk.billing.toInternalResult
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.ConfigState
import com.superwall.sdk.config.models.ConfigurationStatus
import com.superwall.sdk.config.options.EventTrackingBehavior
import com.superwall.sdk.config.options.SuperwallOptions
Expand Down Expand Up @@ -98,7 +98,6 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -776,12 +775,15 @@ class Superwall(
else -> old::class == new::class
}
}
.drop(1) // Drops the cached/initial emission
.collect { newValue ->
// Pair each status with the one before it. Entitlements persists the
// new status before this collector runs, so storage can't supply `from`.
.scan<SubscriptionStatus, Pair<SubscriptionStatus?, SubscriptionStatus>?>(null) { previous, newStatus ->
Pair(previous?.second, newStatus)
}.filterNotNull()
.filter { it.first != null } // Drops the cached/initial emission
.collect { (previous, newValue) ->
// Save and handle the new value
val oldValue =
dependencyContainer.storage.read(StoredSubscriptionStatus)
?: SubscriptionStatus.Unknown
val oldValue = previous ?: SubscriptionStatus.Unknown
dependencyContainer.storage.write(StoredSubscriptionStatus, newValue)
dependencyContainer.delegateAdapter.subscriptionStatusDidChange(
oldValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.internal.trackable.Trackable
import com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent
import com.superwall.sdk.analytics.superwall.SuperwallEventInfo
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.ConfigState
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.logger.Logger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.internal.track
import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.config.ConfigManager
import com.superwall.sdk.config.models.getConfig
import com.superwall.sdk.config.getConfig
import com.superwall.sdk.dependencies.DeviceHelperFactory
import com.superwall.sdk.dependencies.UserAttributesEventFactory
import com.superwall.sdk.misc.IOScope
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.superwall.sdk.config

import android.content.Context
import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.options.SuperwallOptions
import com.superwall.sdk.identity.IdentityManager
import com.superwall.sdk.misc.primitives.BaseContext
import com.superwall.sdk.models.config.Config
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import com.superwall.sdk.models.triggers.Trigger
import com.superwall.sdk.network.SuperwallAPI
Expand All @@ -33,7 +30,6 @@ interface ConfigContext : BaseContext<ConfigState, ConfigContext> {
val identityManager: (() -> IdentityManager)?
val setSubscriptionStatus: ((SubscriptionStatus) -> Unit)?
val awaitUtilNetwork: suspend () -> Unit
val activateTestMode: suspend (config: Config, justActivated: Boolean) -> Unit

fun setTriggers(triggers: Map<String, Trigger>)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.superwall.sdk.config

import android.content.Context
import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.models.getConfig
import com.superwall.sdk.config.options.SuperwallOptions
import com.superwall.sdk.dependencies.DeviceHelperFactory
import com.superwall.sdk.dependencies.DeviceInfoFactory
Expand Down Expand Up @@ -36,7 +33,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch

open class ConfigManager(
override val context: Context,
Expand All @@ -59,7 +55,6 @@ open class ConfigManager(
override val awaitUtilNetwork: suspend () -> Unit = {
context.awaitUntilNetworkExists()
},
override val activateTestMode: suspend (Config, Boolean) -> Unit = { _, _ -> },
override val actor: StateActor<ConfigContext, ConfigState>,
) : ConfigContext {
interface Factory :
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package com.superwall.sdk.config.models
package com.superwall.sdk.config

import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.config.ConfigContext
import com.superwall.sdk.config.ConfigLogic
import com.superwall.sdk.config.PaywallPreload
import com.superwall.sdk.config.options.computedShouldPreload
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.logger.Logger
import com.superwall.sdk.misc.Either
import com.superwall.sdk.misc.awaitFirstValidConfig
import com.superwall.sdk.misc.fold
import com.superwall.sdk.misc.into
import com.superwall.sdk.misc.onError
Expand Down Expand Up @@ -326,7 +322,7 @@ sealed class ConfigState {
manager.setOverriddenSubscriptionStatus(defaultStatus)
entitlements.setSubscriptionStatus(defaultStatus)
}
scope.launch { activateTestMode(config, testModeJustActivated) }
scope.launch { manager.activate(config, testModeJustActivated) }
} else {
if (wasTestMode) {
manager?.clearTestModeState()
Expand Down Expand Up @@ -357,7 +353,7 @@ sealed class ConfigState {
manager.clearTestModeState()
setSubscriptionStatus?.invoke(SubscriptionStatus.Inactive)
} else if (!wasTestMode && isNowTestMode) {
scope.launch { activateTestMode(config, true) }
scope.launch { manager.activate(config, justActivated = true) }
}
})

Expand Down
4 changes: 2 additions & 2 deletions superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ internal class DebugViewActivity : AppCompatActivity() {
) {
val key = UUID.randomUUID().toString()
Superwall.instance.dependencyContainer
.makeViewStore()
.makeViewRegistry()
.storeView(key, view)

val intent =
Expand Down Expand Up @@ -962,7 +962,7 @@ internal class DebugViewActivity : AppCompatActivity() {
}
val view =
Superwall.instance.dependencyContainer
.makeViewStore()
.makeViewRegistry()
.retrieveView(key) ?: run {
finish() // Close the activity if the view associated with the key is not found
return
Expand Down
Loading