diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5e919a92..6cafbfd9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -52,8 +52,10 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.0"
}
- packagingOptions {
- resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ packaging {
+ resources.excludes += "META-INF/{AL2.0,LGPL2.1}"
+ resources.excludes += "META-INF/LICENSE.md"
+ resources.excludes += "META-INF/LICENSE-notice.md"
}
}
@@ -97,10 +99,12 @@ dependencies {
androidTestImplementation(libs.ui.test.junit4)
androidTestImplementation(libs.uiautomator)
androidTestImplementation(libs.test.runner)
+ androidTestImplementation(libs.mockk.android)
+ androidTestImplementation(libs.mockk.core)
androidTestUtil(libs.orchestrator)
// Debug
- debugImplementation(libs.leakcanary.android)
+ // debugImplementation(libs.leakcanary.android)
debugImplementation(libs.ui.tooling)
debugImplementation(libs.ui.test.manifest)
}
diff --git a/app/src/androidTest/java/com/example/superapp/test/AlternativeSetupTest.kt b/app/src/androidTest/java/com/example/superapp/test/AlternativeSetupTest.kt
new file mode 100644
index 00000000..80d4de74
--- /dev/null
+++ b/app/src/androidTest/java/com/example/superapp/test/AlternativeSetupTest.kt
@@ -0,0 +1,121 @@
+package com.example.superapp.test
+
+import android.app.Application
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.dropbox.dropshots.Dropshots
+import com.dropbox.dropshots.ThresholdValidator
+import com.example.superapp.utils.CustomComparator
+import com.example.superapp.utils.awaitUntilShimmerDisappears
+import com.example.superapp.utils.awaitUntilWebviewAppears
+import com.example.superapp.utils.clickButtonWith
+import com.example.superapp.utils.delayFor
+import com.example.superapp.utils.goBack
+import com.example.superapp.utils.paywallPresentsFor
+import com.example.superapp.utils.screenshotFlow
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.config.options.SuperwallOptions
+import com.superwall.superapp.Keys
+import com.superwall.superapp.test.UITestHandler
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.time.Duration.Companion.milliseconds
+
+@RunWith(AndroidJUnit4::class)
+class AlternativeSetupTest {
+ @get:Rule
+ val dropshots =
+ Dropshots(
+ resultValidator = ThresholdValidator(0.01f),
+ imageComparator = CustomComparator(),
+ )
+
+ @Test
+ fun test_paywall_displays_on_session_start() =
+ with(dropshots) {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.SESSION_START_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ paywallPresentsFor(UITestHandler.test50Info)
+ }
+
+ @Test
+ fun test_paywall_displays_on_app_install() =
+ with(dropshots) {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.APP_INSTALL_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ paywallPresentsFor(UITestHandler.test52Info)
+ }
+
+ @Test
+ fun test_paywall_displays_on_app_launch() =
+ with(dropshots) {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.APP_LAUNCH_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ paywallPresentsFor(UITestHandler.test53Info)
+ }
+/*
+ @Test
+ fun test_paywall_displays_on_deep_link() =
+ with(dropshots) {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.DEEP_LINK_OPEN_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ paywallPresentsFor(UITestHandler.test57Info)
+ }
+
+ */
+
+ @Test
+ fun test_paywall_displays_on_decline() =
+ with(dropshots) {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.PAYWALL_DECLINE_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ screenshotFlow(UITestHandler.test59Info) {
+ step("1") {
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(300.milliseconds)
+ }
+ step("decline_for_survey") {
+ goBack()
+ delayFor(300.milliseconds)
+ }
+ step("decline_paywall") {
+ clickButtonWith("Too expensive")
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(300.milliseconds)
+ }
+ }
+ }
+}
diff --git a/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt b/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt
index b86f76aa..7f4d7cfa 100644
--- a/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt
+++ b/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt
@@ -1,21 +1,32 @@
@file:Suppress("ktlint:standard:no-empty-file")
+import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.dropbox.dropshots.Dropshots
import com.dropbox.dropshots.ThresholdValidator
import com.example.superapp.utils.CustomComparator
+import com.example.superapp.utils.awaitUntilDialogAppears
import com.example.superapp.utils.awaitUntilShimmerDisappears
import com.example.superapp.utils.awaitUntilWebviewAppears
+import com.example.superapp.utils.clickButtonWith
import com.example.superapp.utils.delayFor
+import com.example.superapp.utils.goBack
import com.example.superapp.utils.screenshotFlow
import com.example.superapp.utils.waitFor
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.superwall.SuperwallEvent
+import com.superwall.sdk.config.options.SuperwallOptions
+import com.superwall.sdk.paywall.presentation.register
+import com.superwall.superapp.Keys
import com.superwall.superapp.test.UITestHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -31,10 +42,22 @@ class FlowScreenshotTestExecutor {
imageComparator = CustomComparator(),
)
+ @Before
+ fun setup() {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.CONSTANT_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ Superwall.instance.reset()
+ }
+
val mainScope = CoroutineScope(Dispatchers.Main)
@Test
- @FlakyTest
fun test_paywall_reappers_with_video() =
with(dropshots) {
screenshotFlow(UITestHandler.test4Info) {
@@ -47,7 +70,7 @@ class FlowScreenshotTestExecutor {
step("second_paywall") {
it.waitFor { it is SuperwallEvent.PaywallOpen }
awaitUntilWebviewAppears()
- delayFor(1.seconds)
+ delayFor(2.seconds)
}
}
}
@@ -125,48 +148,174 @@ class FlowScreenshotTestExecutor {
}
}
}
-}
- /*
+ @Test
+ fun test_invalid_url_doesnt_crash() =
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test62Info) {
+ step {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(300.milliseconds)
+ }
+ step {
+ clickButtonWith("Perform 3")
+ delayFor(300.milliseconds)
+ }
+ }
+ }
- Commented out due to inability to re-record tests until Firebase Android Studio plugin is fixed
+ @Test
+ fun test_restore_alert_shows() {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test63Info) {
+ step {
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ clickButtonWith("Restore")
+ awaitUntilDialogAppears()
+ }
+ }
+ }
+ }
@Test
- fun test_paywall_presents_then_dismisses_without_reappearing() =
+ fun test_ensure_only_one_holdout() {
with(dropshots) {
- screenshotFlow(UITestHandler.test14Info) {
+ screenshotFlow(UITestHandler.test83Info) {
+ step {
+ delayFor(100.milliseconds)
+ }
+ step {
+ Superwall.instance.register(event = "holdout_one_time_occurrence")
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(600.milliseconds)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_ensure_only_one_displayed_with_limit() {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.testAndroid9Info) {
step {
- it.waitFor { it is SuperwallEvent.ShimmerViewComplete }
awaitUntilShimmerDisappears()
awaitUntilWebviewAppears()
delayFor(300.milliseconds)
- mainScope
- .async {
- // We scroll a bit to display the button
- Superwall.instance.paywallView
- ?.webView
- ?.apply {
- // Disable the scrollbar for the test
- // so its not visible in screenshots
- isVerticalScrollBarEnabled = false
- scrollTo(0, 300)
- }
- }.await()
- // We delay a bit to ensure the button is visible
+ }
+ step {
+ goBack()
+ Superwall.instance.register(event = "one_time_occurrence")
delayFor(300.milliseconds)
- // We scroll back to the top
- mainScope
- .async {
- Superwall.instance.paywallView
- ?.webView
- ?.apply {
- scrollTo(0, 0)
- }
- }.await()
- // We delay a bit to ensure scroll has finished
- delayFor(500.milliseconds)
}
}
}
+ }
+
+ @Test
+ fun test_ensure_time_limit_works() {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.testAndroid18Info) {
+ step {
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(300.milliseconds)
+ }
+ step {
+ goBack()
+ Superwall.instance.register(event = "once_a_minute")
+ delayFor(300.milliseconds)
+ }
+ step {
+ Superwall.instance.register(event = "once_a_minute")
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(300.milliseconds)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun should_show_baseplan_with_free_trial() =
+ with(dropshots) {
+ screenshotFlow(UITestHandler.testAndroid21Info) {
+ step {
+ it.waitFor {
+ it is SuperwallEvent.PaywallWebviewLoadComplete
+ }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delay(300.milliseconds)
+ }
+ }
+ }
+
+ @Test
+ fun should_set_attributes_properly() {
+ with(dropshots) {
+ val test = UITestHandler.testAndroid23Info
+ screenshotFlow(test) {
+ step("") {
+ delayFor(300.milliseconds)
+ test.messages().take(2).toList().let {
+ assert(
+ (it.first() as Map).containsKey("first_name"),
+ )
+ assert(
+ !(it.last() as Map).containsKey("first_name"),
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+/*
+
+Commented out due to inability to re-record tests until Firebase Android Studio plugin is fixed
+
+@Test
+fun test_paywall_presents_then_dismisses_without_reappearing() =
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test14Info) {
+ step {
+ it.waitFor { it is SuperwallEvent.ShimmerViewComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(300.milliseconds)
+ mainScope
+ .async {
+ // We scroll a bit to display the button
+ Superwall.instance.paywallView
+ ?.webView
+ ?.apply {
+ // Disable the scrollbar for the test
+ // so its not visible in screenshots
+ isVerticalScrollBarEnabled = false
+ scrollTo(0, 300)
+ }
+ }.await()
+ // We delay a bit to ensure the button is visible
+ delayFor(300.milliseconds)
+ // We scroll back to the top
+ mainScope
+ .async {
+ Superwall.instance.paywallView
+ ?.webView
+ ?.apply {
+ scrollTo(0, 0)
+ }
+ }.await()
+ // We delay a bit to ensure scroll has finished
+ delayFor(500.milliseconds)
+ }
+ }
+ }
}
- */
+*/
diff --git a/app/src/androidTest/java/com/example/superapp/test/NoConnectionTestExecutor.kt b/app/src/androidTest/java/com/example/superapp/test/NoConnectionTestExecutor.kt
new file mode 100644
index 00000000..52eae1bb
--- /dev/null
+++ b/app/src/androidTest/java/com/example/superapp/test/NoConnectionTestExecutor.kt
@@ -0,0 +1,126 @@
+package com.example.superapp.test
+
+import android.app.Application
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.dropbox.dropshots.Dropshots
+import com.dropbox.dropshots.ThresholdValidator
+import com.example.superapp.utils.CustomComparator
+import com.example.superapp.utils.FlowTestConfiguration
+import com.example.superapp.utils.awaitUntilDialogAppears
+import com.example.superapp.utils.paywallDoesntPresentForNoConfig
+import com.example.superapp.utils.screenshotFlow
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.config.options.SuperwallOptions
+import com.superwall.sdk.network.NetworkConsts
+import com.superwall.sdk.network.NetworkConsts.retryCount
+import com.superwall.superapp.Keys
+import com.superwall.superapp.test.UITestHandler
+import io.mockk.every
+import io.mockk.mockkObject
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NoConnectionTestExecutor {
+ @get:Rule
+ val dropshots =
+ Dropshots(
+ resultValidator = ThresholdValidator(0.01f),
+ imageComparator = CustomComparator(),
+ )
+
+ @Before
+ fun setup() {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.CONSTANT_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+
+ getInstrumentation().uiAutomation.executeShellCommand("svc wifi disable")
+ getInstrumentation().uiAutomation.executeShellCommand("svc data disable")
+ }
+
+ @Test
+ fun test_feature_closure_no_config_not_subscribed_not_gated() =
+ runTest {
+ mockkObject(NetworkConsts) {
+ every { retryCount() } returns 0
+ }
+
+ with(dropshots) {
+ val case = UITestHandler.test41Info
+ paywallDoesntPresentForNoConfig(case)
+ case.messages().first { it == false }
+ }
+ }
+
+ @Test
+ fun test_feature_closure_no_config_not_subscribed_gated() =
+ runTest {
+ mockkObject(NetworkConsts) {
+ every { retryCount() } returns 0
+ }
+
+ with(dropshots) {
+ val case = UITestHandler.test42Info
+ paywallDoesntPresentForNoConfig(case)
+ case.messages().first { it == false }
+ }
+ }
+
+ @Test
+ fun test_feature_closure_no_config_subscribed_not_gated() =
+ runTest {
+ // Disable network to simulate no config
+ mockkObject(NetworkConsts) {
+ every { retryCount() } returns 0
+ }
+
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test43Info, FlowTestConfiguration(false)) {
+ step("") {
+ awaitUntilDialogAppears()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_feature_closure_no_config_subscribed_gated() =
+ runTest {
+ mockkObject(NetworkConsts) {
+ every { retryCount() } returns 0
+ }
+
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test44Info, FlowTestConfiguration(false)) {
+ step("") {
+ awaitUntilDialogAppears()
+ }
+ }
+ }
+ }
+
+ @After
+ fun after() {
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .executeShellCommand("svc wifi enable")
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .executeShellCommand("svc data enable")
+ }
+}
diff --git a/app/src/androidTest/java/com/example/superapp/test/PresentationRuleTests.kt b/app/src/androidTest/java/com/example/superapp/test/PresentationRuleTests.kt
index 74286b1b..d0c231e1 100644
--- a/app/src/androidTest/java/com/example/superapp/test/PresentationRuleTests.kt
+++ b/app/src/androidTest/java/com/example/superapp/test/PresentationRuleTests.kt
@@ -1,6 +1,8 @@
package com.example.superapp.test
+import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.dropbox.dropshots.Dropshots
import com.dropbox.dropshots.ThresholdValidator
import com.example.superapp.utils.CustomComparator
@@ -9,7 +11,10 @@ import com.example.superapp.utils.screenshotFlow
import com.example.superapp.utils.waitFor
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.superwall.SuperwallEvent
+import com.superwall.sdk.config.options.SuperwallOptions
+import com.superwall.superapp.Keys
import com.superwall.superapp.test.UITestHandler
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -24,6 +29,18 @@ class PresentationRuleTests {
imageComparator = CustomComparator(),
)
+ @Before
+ fun setup() {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.CONSTANT_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ }
+
@Test
fun test_paywall_doesnt_present_result_experiment() =
with(dropshots) {
@@ -49,7 +66,6 @@ class PresentationRuleTests {
@Test
fun test_paywall_doesnt_present_result_event_not_found() =
with(dropshots) {
- Superwall.instance.reset()
screenshotFlow(UITestHandler.test30Info) {
step("") {
delayFor(1.seconds)
diff --git a/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt b/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt
index bdda61fe..e5fdac1a 100644
--- a/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt
+++ b/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt
@@ -1,9 +1,12 @@
package com.example.superapp.test
+import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.dropbox.dropshots.Dropshots
import com.dropbox.dropshots.ThresholdValidator
import com.example.superapp.utils.CustomComparator
+import com.example.superapp.utils.FlowTestConfiguration
import com.example.superapp.utils.awaitUntilDialogAppears
import com.example.superapp.utils.awaitUntilShimmerDisappears
import com.example.superapp.utils.awaitUntilWebviewAppears
@@ -14,10 +17,14 @@ import com.example.superapp.utils.screenshotFlow
import com.example.superapp.utils.waitFor
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.superwall.SuperwallEvent
+import com.superwall.sdk.config.options.SuperwallOptions
+import com.superwall.superapp.Keys
import com.superwall.superapp.test.UITestHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -33,6 +40,18 @@ class SimpleScreenshotTestExecutor {
imageComparator = CustomComparator(),
)
+ @Before
+ fun setup() {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.CONSTANT_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ },
+ )
+ }
+
@Test
fun test_paywall_displays_with_attribute_first() =
with(dropshots) {
@@ -174,4 +193,59 @@ class SimpleScreenshotTestExecutor {
with(dropshots) {
paywallPresentsFor(UITestHandler.test33Info)
}
+
+ @Test
+ fun test_feature_closure_with_config_not_subscribed_not_gated() =
+ runTest {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test45Info) {
+ step("") {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears() || awaitUntilWebviewAppears()
+ delayFor(2.seconds)
+ }
+ step("") {
+ awaitUntilDialogAppears()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_feature_closure_with_config_not_subscribed_gated() =
+ runTest {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test46Info, FlowTestConfiguration(true)) {
+ step("") {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears() || awaitUntilWebviewAppears()
+ delayFor(2.seconds)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_feature_closure_with_config_subscribed_not_gated() =
+ runTest {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test47Info, FlowTestConfiguration(false)) {
+ step("") {
+ awaitUntilDialogAppears()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_feature_closure_with_config_subscribed_gated() =
+ runTest {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test48Info, FlowTestConfiguration(false)) {
+ step("") {
+ awaitUntilDialogAppears()
+ }
+ }
+ }
+ }
}
diff --git a/app/src/androidTest/java/com/example/superapp/test/SurveyTests.kt b/app/src/androidTest/java/com/example/superapp/test/SurveyTests.kt
new file mode 100644
index 00000000..9d41528c
--- /dev/null
+++ b/app/src/androidTest/java/com/example/superapp/test/SurveyTests.kt
@@ -0,0 +1,168 @@
+package com.example.superapp.test
+
+import com.dropbox.dropshots.Dropshots
+import com.dropbox.dropshots.ThresholdValidator
+import com.example.superapp.utils.CustomComparator
+import org.junit.Rule
+
+class SurveyTests {
+ @get:Rule
+ val dropshots =
+ Dropshots(
+ resultValidator = ThresholdValidator(0.01f),
+ imageComparator = CustomComparator(),
+ )
+
+ /*
+
+ Note: These test are temporarily disabled due to orchestrator flakiness.
+
+ @Before
+ fun setup() {
+ Superwall.configure(
+ getInstrumentation().targetContext.applicationContext as Application,
+ Keys.CONSTANT_API_KEY,
+ options = SuperwallOptions().apply {
+ paywalls.shouldPreload = false
+ }
+ )
+ Superwall.instance.reset()
+ }
+
+ @Test
+ fun test_survey_response_input() {
+ with(dropshots) {
+ val test = UITestHandler.test65Info
+ screenshotFlow(test) {
+ step {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ goBack()
+ }
+
+ step {
+ clickButtonWith("Other")
+ setInput("Test")
+ clickButtonWith("SUBMIT")
+ awaitUntilDialogAppears()
+ }
+ step {
+ val messages = test.messages()
+ .take(2)
+ .toList()
+ assert(messages.first() is SuperwallEvent.SurveyResponse)
+ assert(messages.last() is SuperwallEvent.PaywallClose)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_survey_with_close_button() {
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test74Info) {
+ step {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ goBack()
+ delayFor(100.milliseconds)
+ }
+ step {
+ // Survey should appear
+ awaitUntilDialogAppears()
+ goBack()
+ delayFor(100.milliseconds)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_survey_shows_once() {
+ with(dropshots) {
+ Superwall.instance.identify("survey_2")
+ screenshotFlow(UITestHandler.test64Info) {
+ step {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ goBack()
+ delayFor(100.milliseconds)
+ }
+ step {
+ clickButtonWith("Option")
+ awaitUntilDialogAppears()
+ delayFor(100.milliseconds)
+ }
+ step {
+ goBack()
+ Superwall.instance.register("show_survey_with_other")
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(100.milliseconds)
+ }
+ step {
+ goBack()
+ awaitUntilDialogAppears()
+ delayFor(100.milliseconds)
+ }
+ }
+ }
+ }
+
+
+ @Test
+ fun test_survey_with_campaign_trigger() {
+ Superwall.instance.identify("survey_2")
+
+ with(dropshots) {
+ screenshotFlow(UITestHandler.test68Info) {
+ step {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ goBack()
+ delayFor(100.milliseconds)
+ }
+ step {
+ clickButtonWith("Option 1")
+ delayFor(100.milliseconds)
+ // Verify second paywall appears
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ delayFor(100.milliseconds)
+ }
+ step {
+ goBack()
+ awaitUntilDialogAppears()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_survey_with_delegate() {
+ with(dropshots) {
+ Superwall.instance.identify("survey_3")
+ screenshotFlow(UITestHandler.test70Info) {
+ step {
+ it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ awaitUntilShimmerDisappears()
+ awaitUntilWebviewAppears()
+ goBack()
+ delayFor(100.milliseconds)
+ }
+ step {
+ clickButtonWith("Option")
+ delayFor(100.milliseconds)
+ }
+ }
+ }
+ }
+
+ */
+}
diff --git a/app/src/androidTest/java/com/example/superapp/utils/FlowTestConfiguration.kt b/app/src/androidTest/java/com/example/superapp/utils/FlowTestConfiguration.kt
new file mode 100644
index 00000000..438d7fab
--- /dev/null
+++ b/app/src/androidTest/java/com/example/superapp/utils/FlowTestConfiguration.kt
@@ -0,0 +1,9 @@
+package com.example.superapp.utils
+
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+
+data class FlowTestConfiguration(
+ val waitForConfig: Boolean = true,
+ val timeout: Duration = 5.minutes,
+)
diff --git a/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt b/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt
index 65b6c672..24cc52f7 100644
--- a/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt
+++ b/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt
@@ -36,7 +36,6 @@ import kotlinx.coroutines.test.runTest
import java.util.LinkedList
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
class ScreenshotTestFlow(
@@ -89,6 +88,7 @@ annotation class UiTestDSL
@ScreenshotTestDSL
fun Dropshots.screenshotFlow(
testInfo: UITestInfo,
+ config: FlowTestConfiguration = FlowTestConfiguration(),
flow: ScreenshotTestFlow.() -> Unit,
) {
val flow = ScreenshotTestFlow(testInfo).apply(flow)
@@ -112,8 +112,10 @@ fun Dropshots.screenshotFlow(
}
}
- runTest(timeout = 5.minutes) {
- Superwall.instance.configurationStateListener.first { it is ConfigurationStatus.Configured }
+ runTest(timeout = config.timeout) {
+ if (config.waitForConfig) {
+ Superwall.instance.configurationStateListener.first { it is ConfigurationStatus.Configured }
+ }
try {
flow.steps.forEach {
if (!testReady.value) {
@@ -134,8 +136,11 @@ fun Dropshots.screenshotFlow(
}
@ScreenshotTestDSL
-fun Dropshots.paywallPresentsFor(testInfo: UITestInfo) {
- screenshotFlow(testInfo) {
+fun Dropshots.paywallPresentsFor(
+ testInfo: UITestInfo,
+ config: FlowTestConfiguration = FlowTestConfiguration(),
+) {
+ screenshotFlow(testInfo, config) {
step("") {
it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete }
// Since there is a delay between webview finishing loading and the actual render
@@ -148,8 +153,25 @@ fun Dropshots.paywallPresentsFor(testInfo: UITestInfo) {
}
@ScreenshotTestDSL
-fun Dropshots.paywallDoesntPresentFor(testInfo: UITestInfo) {
- screenshotFlow(testInfo) {
+fun Dropshots.paywallDoesntPresentFor(
+ testInfo: UITestInfo,
+ config: FlowTestConfiguration = FlowTestConfiguration(),
+) {
+ screenshotFlow(testInfo, config) {
+ step("") {
+ it.waitFor { it is SuperwallEvent.PaywallPresentationRequest }
+ // We delay a bit to ensure the paywall doesn't render after presentation request
+ delayFor(1.seconds)
+ }
+ }
+}
+
+@ScreenshotTestDSL
+fun Dropshots.paywallDoesntPresentForNoConfig(
+ testInfo: UITestInfo,
+ config: FlowTestConfiguration = FlowTestConfiguration(false),
+) {
+ screenshotFlow(testInfo, config) {
step("") {
it.waitFor { it is SuperwallEvent.PaywallPresentationRequest }
// We delay a bit to ensure the paywall doesn't render after presentation request
@@ -178,14 +200,50 @@ private fun getWebviewFromPaywall() =
@UiTestDSL
suspend fun awaitUntilWebviewAppears(): Boolean {
- val selector = UiSelector()
val device = UiDevice.getInstance(getInstrumentation())
- device.wait(Until.findObject(By.clazz(WebView::class.java.name)), 10000)
+ device.wait(Until.findObject(By.clazz(WebView::class.java.name)), 1000)
device.waitForIdle()
delay(300.milliseconds)
return true
}
+@UiTestDSL
+suspend fun enableWebviewDebugging() {
+ UiDevice
+ .getInstance(getInstrumentation())
+ .findObject(By.clazz(WebView::class.java))
+ .apply {
+ }
+}
+
+@UiTestDSL
+suspend fun clickButtonWith(text: String) {
+ val selector = UiSelector()
+ val device = UiDevice.getInstance(getInstrumentation())
+ device
+ .findObject(
+ UiSelector().textContains(text),
+ ).click()
+}
+
+@UiTestDSL
+suspend fun setInput(text: String) {
+ val device = UiDevice.getInstance(getInstrumentation())
+ with(
+ device.findObject(
+ UiSelector().focusable(true),
+ ),
+ ) {
+ this.setText(text)
+ }
+}
+
+@UiTestDSL
+suspend fun goBack() {
+ val device = UiDevice.getInstance(getInstrumentation())
+ device.pressBack()
+}
+
@UiTestDSL
suspend fun awaitUntilDialogAppears(): Boolean {
val selector = UiSelector()
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_18.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_18.png
index daee90c4..88bc84b2 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_18.png and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_18.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_1.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_1.png
new file mode 100644
index 00000000..ac52046c
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_2.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_2.png
new file mode 100644
index 00000000..4c27bccc
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_3.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_3.png
new file mode 100644
index 00000000..4c27bccc
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_180_step_3.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_210_step_1.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_210_step_1.png
new file mode 100644
index 00000000..f983efe5
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_210_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_230.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_230.png
new file mode 100644
index 00000000..e00a6242
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_230.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_41.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_41.png
new file mode 100644
index 00000000..8452588f
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_41.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_42.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_42.png
new file mode 100644
index 00000000..8452588f
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_42.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_43.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_43.png
new file mode 100644
index 00000000..8452588f
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_43.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_44.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_44.png
new file mode 100644
index 00000000..6821c81a
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_44.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_45.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_45.png
new file mode 100644
index 00000000..571d2da6
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_45.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_46.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_46.png
new file mode 100644
index 00000000..16690817
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_46.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_47.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_47.png
new file mode 100644
index 00000000..03e18e44
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_47.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_48.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_48.png
new file mode 100644
index 00000000..16690817
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_48.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_4_first_paywall.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_4_first_paywall.png
index 00a1ce71..d516c97a 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_4_first_paywall.png and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_4_first_paywall.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_50.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_50.png
new file mode 100644
index 00000000..b496e7f9
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_50.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_52.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_52.png
new file mode 100644
index 00000000..a901c834
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_52.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_53.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_53.png
new file mode 100644
index 00000000..b496e7f9
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_53.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_1.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_1.png
new file mode 100644
index 00000000..0edbaaed
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_decline_for_survey.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_decline_for_survey.png
new file mode 100644
index 00000000..c6acdf60
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_decline_for_survey.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_decline_paywall.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_decline_paywall.png
new file mode 100644
index 00000000..ccf09601
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_59_decline_paywall.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_62_step_1.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_62_step_1.png
new file mode 100644
index 00000000..ea57ade0
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_62_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_62_step_2.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_62_step_2.png
new file mode 100644
index 00000000..ea57ade0
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_62_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_63_step_1.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_63_step_1.png
new file mode 100644
index 00000000..6513ea69
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_63_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_83_step_1.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_83_step_1.png
new file mode 100644
index 00000000..e00a6242
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_83_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_83_step_2.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_83_step_2.png
new file mode 100644
index 00000000..ebe3f2f8
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_83_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_90_step_1.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_90_step_1.png
new file mode 100644
index 00000000..dffa5819
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_90_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_90_step_2.png b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_90_step_2.png
new file mode 100644
index 00000000..8a5a666b
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_Pixel_7_TestCase_90_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_13_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_13_step_1.png
index 5e8d2fa0..e08014a4 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_13_step_1.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_13_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18.png
index 23009dab..b274ddf3 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_1.png
new file mode 100644
index 00000000..76d7049d
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_2.png
new file mode 100644
index 00000000..926aac16
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_3.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_3.png
new file mode 100644
index 00000000..926aac16
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_180_step_3.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_1.png
new file mode 100644
index 00000000..e4c42186
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_2.png
new file mode 100644
index 00000000..d1edfc47
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_3.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_3.png
new file mode 100644
index 00000000..d1edfc47
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_18_step_3.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_19_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_19_step_1.png
new file mode 100644
index 00000000..ac004980
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_19_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_2.png
index 00d78d75..e08014a4 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_2.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_210_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_210_step_1.png
new file mode 100644
index 00000000..a580841d
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_210_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_21_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_21_step_1.png
new file mode 100644
index 00000000..8e41f2b3
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_21_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_230.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_230.png
new file mode 100644
index 00000000..eb68af91
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_230.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_28.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_28.png
index 5e8d2fa0..11656688 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_28.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_28.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_29.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_29.png
index 5e8d2fa0..381e0d13 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_29.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_29.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_30.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_30.png
index 5e8d2fa0..381e0d13 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_30.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_30.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_31.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_31.png
index 5e8d2fa0..381e0d13 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_31.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_31.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_32.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_32.png
index 5e8d2fa0..381e0d13 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_32.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_32.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_41.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_41.png
new file mode 100644
index 00000000..256a09ea
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_41.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_42.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_42.png
new file mode 100644
index 00000000..22436848
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_42.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_43.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_43.png
new file mode 100644
index 00000000..256a09ea
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_43.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_44.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_44.png
new file mode 100644
index 00000000..9887a2ef
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_44.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_45.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_45.png
new file mode 100644
index 00000000..3473fc52
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_45.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_46.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_46.png
new file mode 100644
index 00000000..8e40c20a
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_46.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_47.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_47.png
new file mode 100644
index 00000000..e08014a4
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_47.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_48.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_48.png
new file mode 100644
index 00000000..8e40c20a
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_48.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_4_first_paywall.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_4_first_paywall.png
index 560b80a0..8988a173 100644
Binary files a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_4_first_paywall.png and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_4_first_paywall.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_58_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_58_1.png
new file mode 100644
index 00000000..b33d9b85
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_58_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_1.png
new file mode 100644
index 00000000..86daa5b5
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_decline_for_survey.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_decline_for_survey.png
new file mode 100644
index 00000000..aa5eebc0
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_decline_for_survey.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_decline_paywall.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_decline_paywall.png
new file mode 100644
index 00000000..918340a1
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_59_decline_paywall.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_62_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_62_step_1.png
new file mode 100644
index 00000000..26a17184
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_62_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_62_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_62_step_2.png
new file mode 100644
index 00000000..deea556e
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_62_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_63_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_63_step_1.png
new file mode 100644
index 00000000..fd18f4e6
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_63_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_1.png
new file mode 100644
index 00000000..031d435b
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_2.png
new file mode 100644
index 00000000..b02a9e53
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_3.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_3.png
new file mode 100644
index 00000000..b02a9e53
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_65_step_3.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_74_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_74_step_1.png
new file mode 100644
index 00000000..724f7d43
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_74_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_74_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_74_step_2.png
new file mode 100644
index 00000000..7355ee4d
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_74_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_83_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_83_step_1.png
new file mode 100644
index 00000000..fab00b6a
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_83_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_83_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_83_step_2.png
new file mode 100644
index 00000000..656da900
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_83_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_90_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_90_step_1.png
new file mode 100644
index 00000000..ce2fd888
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_90_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_90_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_90_step_2.png
new file mode 100644
index 00000000..492b1ddd
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_90_step_2.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_9_step_1.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_9_step_1.png
new file mode 100644
index 00000000..921afd1f
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_9_step_1.png differ
diff --git a/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_9_step_2.png b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_9_step_2.png
new file mode 100644
index 00000000..1b96bd0d
Binary files /dev/null and b/app/src/androidTest/screenshots/SW_Google_sdk_gphone64_arm64_TestCase_9_step_2.png differ
diff --git a/app/src/main/java/com/superwall/superapp/MainApplication.kt b/app/src/main/java/com/superwall/superapp/MainApplication.kt
index d30e853e..f224dda5 100644
--- a/app/src/main/java/com/superwall/superapp/MainApplication.kt
+++ b/app/src/main/java/com/superwall/superapp/MainApplication.kt
@@ -17,28 +17,23 @@ import com.superwall.superapp.purchase.RevenueCatPurchaseController
import kotlinx.coroutines.flow.MutableSharedFlow
import java.lang.ref.WeakReference
+object Keys {
+ const val CONSTANT_API_KEY = "pk_0ff90006c5c2078e1ce832bd2343ba2f806ca510a0a1696a"
+ const val ANDROID_MAIN_SCREEN_API_KEY = "pk_d1f0959f70c761b1d55bb774a03e22b2b6ed290ce6561f85"
+ const val UI_TEST_API_KEY = "pk_0ff90006c5c2078e1ce832bd2343ba2f806ca510a0a1696a"
+ const val DEEP_LINK_OPEN_API_KEY = "pk_3faea4c721179218a245475ea9d378d1ecb9bf059411a0c0"
+ const val APP_LAUNCH_API_KEY = "pk_fb295f846b075fae6619eebb43d126ecddd1e3b18e7028b8"
+ const val APP_INSTALL_API_KEY = "pk_8db958db59cc8460969659822351d5e177d8d65cb295cff2"
+ const val SESSION_START_API_KEY = "pk_6c881299e2f8db59f697646e399397be76432fa0968ca254"
+ const val PAYWALL_DECLINE_API_KEY = "pk_6892bf95fdf329f01b7da4d4b77fcc6c1033d1ec3ef31da2"
+ const val TRANSACTION_ABANDON_API_KEY = "pk_f406422339b71cf568ffe8cba02f849ab27e9791bb9b2ed4"
+ const val TRANSACTION_FAIL_API_KEY = "pk_b6cd945401435766da627080a3fbe349adb2dcd69ab767f3"
+ const val SURVEY_RESPONSE_API_KEY = "pk_3698d9fe123f1e4aa8014ceca111096ca06fd68d31d9e662"
+}
+
class MainApplication :
android.app.Application(),
SuperwallDelegate {
- companion object {
- const val CONSTANT_API_KEY = "pk_0ff90006c5c2078e1ce832bd2343ba2f806ca510a0a1696a"
-
- /*
- Copy and paste the following API keys to switch between apps.
- App API Keys:
- Android Main screen: pk_d1f0959f70c761b1d55bb774a03e22b2b6ed290ce6561f85
- UITest (Android): pk_0ff90006c5c2078e1ce832bd2343ba2f806ca510a0a1696a
- DeepLink Open: pk_3faea4c721179218a245475ea9d378d1ecb9bf059411a0c0
- AppLaunch: pk_fb295f846b075fae6619eebb43d126ecddd1e3b18e7028b8
- AppInstall: pk_8db958db59cc8460969659822351d5e177d8d65cb295cff2
- SessionStart: pk_6c881299e2f8db59f697646e399397be76432fa0968ca254
- PaywallDecline: pk_a1071d541642719e2dc854da9ec717ec967b8908854ede74
- TransactionAbandon: pk_9c99186b023ae795e0189cf9cdcd3e2d2d174289e0800d66
- TransactionFail: pk_b6cd945401435766da627080a3fbe349adb2dcd69ab767f3
- SurveyResponse: pk_3698d9fe123f1e4aa8014ceca111096ca06fd68d31d9e662
- */
- }
-
var activity: WeakReference? = null
override fun onCreate() {
@@ -63,7 +58,9 @@ class MainApplication :
.build(),
)
- configureWithObserverMode()
+ if (!isRunningTest()) {
+ configureWithObserverMode()
+ }
// configureWithRevenueCatInitialization()
}
@@ -72,7 +69,7 @@ class MainApplication :
fun configureWithAutomaticInitialization() {
Superwall.configure(
this,
- CONSTANT_API_KEY,
+ Keys.CONSTANT_API_KEY,
options =
SuperwallOptions().apply {
paywalls =
@@ -90,7 +87,7 @@ class MainApplication :
fun configureWithObserverMode() {
Superwall.configure(
this@MainApplication,
- CONSTANT_API_KEY,
+ Keys.CONSTANT_API_KEY,
options =
SuperwallOptions().apply {
shouldObservePurchases = true
@@ -111,7 +108,7 @@ class MainApplication :
Superwall.configure(
this,
- CONSTANT_API_KEY,
+ Keys.CONSTANT_API_KEY,
purchaseController,
)
Superwall.instance.delegate = this
@@ -159,3 +156,13 @@ class MainApplication :
)
}
}
+
+@Synchronized
+fun isRunningTest(): Boolean =
+ try {
+ // "android.support.test.espresso.Espresso" if you haven't migrated to androidx yet
+ Class.forName("androidx.test.espresso.Espresso")
+ true
+ } catch (e: ClassNotFoundException) {
+ false
+ }
diff --git a/app/src/main/java/com/superwall/superapp/test/UITestActivity.kt b/app/src/main/java/com/superwall/superapp/test/UITestActivity.kt
index fafa3dd7..239d9207 100644
--- a/app/src/main/java/com/superwall/superapp/test/UITestActivity.kt
+++ b/app/src/main/java/com/superwall/superapp/test/UITestActivity.kt
@@ -45,12 +45,15 @@ class UITestInfo(
val number: Int,
val description: String,
val testCaseType: TestCaseType = TestCaseType.iOS,
- test: suspend Context.(testDispatcher: CoroutineScope, events: Flow) -> Unit,
+ test: suspend Context.(testDispatcher: CoroutineScope, events: Flow, message: MutableSharedFlow) -> Unit,
) {
private val events = MutableSharedFlow(extraBufferCapacity = 50)
+ private val message = MutableSharedFlow(extraBufferCapacity = 10, replay = 10)
fun events() = events
+ fun messages() = message
+
val test: suspend Context.() -> Unit = {
val scope = CoroutineScope(Dispatchers.IO)
delay(100)
@@ -67,7 +70,7 @@ class UITestInfo(
}
}
}
- test.invoke(this, scope, events().filterNotNull())
+ test.invoke(this, scope, events().filterNotNull(), message)
}
}
diff --git a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt
index 80fe7165..36356214 100644
--- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt
+++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt
@@ -24,6 +24,7 @@ import com.superwall.superapp.ComposeActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@@ -33,7 +34,7 @@ object UITestHandler {
UITestInfo(
0,
"Uses the identify function. Should see the name 'Jack' in the paywall.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Log.e("Registering event", "present_data")
Superwall.instance.identify(userId = "test0")
Superwall.instance.setUserAttributes(attributes = mapOf("first_name" to "Jack"))
@@ -46,7 +47,7 @@ object UITestHandler {
UITestInfo(
1,
"Uses the identify function. Should see the name 'Kate' in the paywall.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Set identity
Superwall.instance.identify(userId = "test1a")
Superwall.instance.setUserAttributes(mapOf("first_name" to "Jack"))
@@ -63,7 +64,7 @@ object UITestHandler {
UITestInfo(
2,
"Calls `reset()`. No first name should be displayed.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// TODO: The name doesn't get set to begin with so isn't an accurate test.
// Set identity
Superwall.instance.identify(userId = "test2")
@@ -77,7 +78,7 @@ object UITestHandler {
UITestInfo(
3,
"Calls `reset()` multiple times. No first name should be displayed.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Set identity
Superwall.instance.identify(userId = "test3")
Superwall.instance.setUserAttributes(mapOf("first_name" to "Jack"))
@@ -94,7 +95,7 @@ object UITestHandler {
"t0 and a 2 in the video at t2. It will close after 4 seconds. A new paywall " +
"will be presented 1 second after close. This paywall should have a video " +
"playing and should be started from the beginning with a 0 on the screen. ",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Present the paywall.
Superwall.instance.register(event = "present_video")
// Dismiss after 4 seconds
@@ -110,7 +111,7 @@ object UITestHandler {
5,
"Show paywall with override products. Paywall should appear with 2 products:" +
"1 monthly at \$12.99 and 1 annual at \$99.99.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// TODO: Need to get some products from google play console and substitute in.
},
)
@@ -119,7 +120,7 @@ object UITestHandler {
6,
"Paywall should appear with 2 products: 1 monthly at \$4.99 and 1 annual at" +
" \$29.99.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// TODO: This doesn't have the products that it should have - need to add to
// google play console
Superwall.instance.register(event = "present_products")
@@ -131,7 +132,7 @@ object UITestHandler {
"Adds a user attribute to verify rule on `present_and_rule_user` presents: " +
"user.should_display == true and user.some_value > 12. Then dismisses and removes " +
"those attributes. Make sure it's not presented.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.identify(userId = "test7")
Superwall.instance.setUserAttributes(
mapOf(
@@ -160,7 +161,7 @@ object UITestHandler {
8,
"Adds a user attribute to verify rule on `present_and_rule_user`. Verify it" +
" DOES NOT present: user.should_display == true and user.some_value > 12",
- test = { scope, events ->
+ test = { scope, events, _ ->
// TODO: Crashes on no rule match
Superwall.instance.identify(userId = "test7")
Superwall.instance.setUserAttributes(
@@ -178,7 +179,7 @@ object UITestHandler {
9,
"Sets subs status to active, paywall should present regardless of this," +
" then it sets the status back to inactive.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test"))))
Superwall.instance.register(event = "present_always")
Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
@@ -192,7 +193,7 @@ object UITestHandler {
"products: 1 monthly at \$12.99 and 1 annual at \$99.99. After dismiss, paywall " +
"should be presented again with no override products. After dismiss, paywall " +
"should be presented one last time with no override products.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// TODO: Product substitution
},
)
@@ -202,7 +203,7 @@ object UITestHandler {
"Paywall should present with the name Claire. Then it should dismiss after" +
"8 seconds and present again without any name. Then it should present again" +
" with the name Sawyer.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
Superwall.instance.setUserAttributes(mapOf("first_name" to "Claire"))
Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
@@ -228,7 +229,7 @@ object UITestHandler {
UITestInfo(
12,
"Test trigger: off. Paywall shouldn't present. Should print eventNotFound.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "keep_this_trigger_off")
},
)
@@ -236,7 +237,7 @@ object UITestHandler {
UITestInfo(
13,
"Test trigger: not in the dashboard. Paywall shouldn't present.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "i_just_made_this_up_and_it_dne")
},
)
@@ -245,7 +246,7 @@ object UITestHandler {
14,
"Presents the paywall and then dismisses after 8 seconds. The paywall shouldn't " +
"display based on a paywall_close event.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Show a paywall
Superwall.instance.register(event = "present_always")
events.first { it is SuperwallEvent.ShimmerViewComplete }
@@ -264,7 +265,7 @@ object UITestHandler {
"Verify that the console output contains a non-null experimentId. After the third " +
"presentation, manually close the paywall. During this test only 3 paywalls should " +
"event present. If more than 3 present, this test has failed.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "present_always")
Superwall.instance.register(
event = "present_always",
@@ -301,7 +302,7 @@ object UITestHandler {
16,
"The paywall should present. In the console you should see a paywall_open event " +
"from the delegate followed by !!! TEST 16 !!!.",
- test = { scope, events ->
+ test = { scope, events, _ ->
var presentTriggered = false
val paywallPresentationHandler = PaywallPresentationHandler()
paywallPresentationHandler.onPresent { info ->
@@ -329,7 +330,7 @@ object UITestHandler {
" the paywall will dismiss after 8s and one more paywall will display. After " +
"the third presentation, manually close the paywall. During this test only 3 " +
"paywalls should event present. If more than 3 present, this test has failed.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.identify(userId = "test0")
Superwall.instance.setUserAttributes(mapOf("first_name" to "Jack"))
Superwall.instance.register(event = "present_data")
@@ -367,7 +368,7 @@ object UITestHandler {
18,
"Open In-App browser from a manually presented paywall. Once the in-app " +
"browser opens, close it, and verify that the paywall is still showing.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock paywall view
val delegate = MockPaywallViewDelegate()
@@ -389,7 +390,7 @@ object UITestHandler {
"Clusterfucks by Jakeâ„¢. Ths presents a paywall with no name. Then it dismisses" +
" after 8s. Then it presents again with no name, dismisses, and finally presents " +
"with the name Kate.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Set identity
Superwall.instance.identify(userId = "test19a")
Superwall.instance.setUserAttributes(mapOf("first_name" to "Jack"))
@@ -434,7 +435,7 @@ object UITestHandler {
"Verify that external URLs can be opened in native browser from paywall. When" +
" the paywall opens, tap \"Perform\" for \"Open in Safari\". Afterwards, go back " +
"and verify the paywall is still displayed.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Present paywall with URLs
Superwall.instance.register("present_urls")
},
@@ -444,7 +445,7 @@ object UITestHandler {
21,
"Present the paywall and make a purchase. After purchasing completes and " +
"the paywall dismisses, attempt to launch again. The paywall should NOT appear.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "present_data")
},
)
@@ -453,7 +454,7 @@ object UITestHandler {
22,
"This is skipped! (Track an event shortly after another one is beginning to present. The " +
"session should not be cancelled out.)",
- test = { scope, events ->
+ test = { scope, events, _ ->
// TODO: This is skipped in the iOS SDK for now
},
)
@@ -462,7 +463,7 @@ object UITestHandler {
23,
"Case: Unsubscribed user, register event without a gating handler\n" +
"Result: paywall should display",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Register event
Superwall.instance.register(event = "register_nongated_paywall")
},
@@ -473,7 +474,7 @@ object UITestHandler {
"Case: Subscribed user, register event without a gating handler\n" +
"Result: paywall should NOT display. Resets subscription status to inactive " +
"4s later.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Set user as subscribed
Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("pro"))))
// Register event - paywall shouldn't appear.
@@ -490,7 +491,7 @@ object UITestHandler {
25,
"Tapping the button shouldn't present a paywall. These register calls don't " +
"have a feature gate. Differs from iOS in that there is no purchase taking place.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("pro"))))
// Try to present paywall again
Superwall.instance.register(event = "register_nongated_paywall")
@@ -505,7 +506,7 @@ object UITestHandler {
26,
"Registers an event with a gating handler. The paywall should display, you should " +
"NOT see an alert when you close the paywall.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
Superwall.instance.register(event = "register_gated_paywall") {
val alertController =
@@ -524,7 +525,7 @@ object UITestHandler {
27,
"Tapping the button shouldn't present the paywall but should launch the " +
"feature block - an alert should present.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus("pro")
Superwall.instance.register(event = "register_gated_paywall") {
val alertController =
@@ -544,7 +545,7 @@ object UITestHandler {
UITestInfo(
28,
"Should print out \"Paywall(experiment...)\".",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
scope.launch {
val result = Superwall.instance.getPresentationResult("present_data")
@@ -561,7 +562,7 @@ object UITestHandler {
UITestInfo(
29,
"Should print out \"noRuleMatch\".",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setUserAttributes(
mapOf(
"should_display" to null,
@@ -581,7 +582,7 @@ object UITestHandler {
UITestInfo(
30,
"Should print out \"eventNotFound\".",
- test = { scope, events ->
+ test = { scope, events, _ ->
scope.launch {
val result =
@@ -598,7 +599,7 @@ object UITestHandler {
UITestInfo(
31,
"Should print out \"holdout\".",
- test = { scope, events ->
+ test = { scope, events, _ ->
scope.launch {
val result = Superwall.instance.getPresentationResult("holdout")
@@ -615,7 +616,7 @@ object UITestHandler {
32,
"This sets the subscription status active, prints out \"userIsSubscribed\" " +
"and then returns subscription status to inactive.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test"))))
scope.launch {
val result = Superwall.instance.getPresentationResult("present_data")
@@ -632,7 +633,7 @@ object UITestHandler {
UITestInfo(
33,
"Calls identify twice with the same ID before presenting a paywall",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Set identity
Superwall.instance.identify(userId = "test33")
Superwall.instance.identify(userId = "test33")
@@ -644,7 +645,7 @@ object UITestHandler {
UITestInfo(
34,
"Call reset 8s after a paywall is presented – should not cause a crash.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "present_data")
scope.launch {
@@ -661,7 +662,7 @@ object UITestHandler {
"Purchase from the paywall and then check that after the purchase has finished " +
"the result type `purchased` is printed to the console. The paywall should dismiss." +
" After doing this, try test 37",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
@@ -681,7 +682,7 @@ object UITestHandler {
36,
"Close the paywall and check that after the purchase has finished \" " +
"\"the result type \"declined\" is printed to the console. The paywall should close.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
@@ -705,7 +706,7 @@ object UITestHandler {
"Need to have purchased a product before calling this test, then present the " +
"paywall and tap \"restore\". The paywall should dismiss and the the console should" +
"print the paywallResult as \"restored\".",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
@@ -727,6 +728,7 @@ object UITestHandler {
private suspend fun Context.executeRegisterFeatureClosureTest(
subscribed: Boolean,
gated: Boolean,
+ messagePipe: MutableSharedFlow,
) {
val currentSubscriptionStatus = Superwall.instance.entitlements.status.value
@@ -745,6 +747,9 @@ object UITestHandler {
val paywallPresentationHandler = PaywallPresentationHandler()
paywallPresentationHandler.onError { error ->
+ CoroutineScope(Dispatchers.IO).launch {
+ messagePipe.emit(false)
+ }
println("!!! ERROR HANDLER !!! $error")
}
@@ -773,8 +778,12 @@ object UITestHandler {
"Unable to fetch config, not subscribed, and not gated. First disable " +
"internet on device. You should not be subscribed. You SHOULD " +
"see !!! ERROR HANDLER !!! in the console and the alert should NOT show.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = false, gated = false)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = false,
+ gated = false,
+ messagePipe = message,
+ )
},
)
var test42Info =
@@ -783,8 +792,12 @@ object UITestHandler {
"Unable to fetch config, not subscribed, and gated. First disable internet " +
"on device. You should not be subscribed. You SHOULD " +
"see !!! ERROR HANDLER !!! in the console and the alert should NOT show.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = false, gated = true)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = false,
+ gated = true,
+ messagePipe = message,
+ )
},
)
var test43Info =
@@ -793,8 +806,12 @@ object UITestHandler {
"Unable to fetch config, subscribed, and not gated. First disable internet on " +
"device. You should NOT see !!! ERROR HANDLER !!! in the console and the alert " +
"SHOULD show.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = true, gated = false)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = true,
+ gated = false,
+ messagePipe = message,
+ )
},
)
var test44Info =
@@ -803,8 +820,12 @@ object UITestHandler {
"Unable to fetch config, subscribed, and gated. First disable internet on " +
"device. You should NOT see !!! ERROR HANDLER !!! in the console and the alert " +
"SHOULD show.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = true, gated = true)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = true,
+ gated = true,
+ messagePipe = message,
+ )
},
)
var test45Info =
@@ -813,8 +834,12 @@ object UITestHandler {
"Fetched config, not subscribed, and not gated. The paywall should show. On " +
"paywall dismiss you should NOT see !!! ERROR HANDLER !!! in the console and the " +
"alert should show when you dismiss the paywall.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = false, gated = false)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = false,
+ gated = false,
+ messagePipe = message,
+ )
delay(4000)
},
)
@@ -824,8 +849,12 @@ object UITestHandler {
"Fetched config, not subscribed, and gated. The paywall should show. You should " +
"NOT see !!! ERROR HANDLER !!! in the console and the alert should NOT show on " +
"paywall dismiss.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = false, gated = true)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = false,
+ gated = true,
+ messagePipe = message,
+ )
delay(4000)
},
)
@@ -834,8 +863,12 @@ object UITestHandler {
47,
"Fetched config, subscribed, and not gated. The paywall should NOT show. You " +
"should NOT see !!! ERROR HANDLER !!! in the console and the alert SHOULD show.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = true, gated = false)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = true,
+ gated = false,
+ messagePipe = message,
+ )
},
)
var test48Info =
@@ -843,8 +876,12 @@ object UITestHandler {
48,
"Fetched config, subscribed, and gated. The paywall should NOT show. You should" +
" NOT see !!! ERROR HANDLER !!! in the console and the alert SHOULD show.",
- test = { scope, events ->
- executeRegisterFeatureClosureTest(subscribed = true, gated = true)
+ test = { scope, events, message ->
+ executeRegisterFeatureClosureTest(
+ subscribed = true,
+ gated = true,
+ messagePipe = message,
+ )
delay(4000)
},
)
@@ -857,7 +894,7 @@ object UITestHandler {
"Change the API Key to the SessionStart UITest app. Clean install app and the " +
"paywall should show. The button does nothing.",
test =
- { scope, events -> },
+ { scope, events, _ -> },
)
var test52Info =
UITestInfo(
@@ -865,7 +902,7 @@ object UITestHandler {
"Change the API Key to the AppInstall UITest app. Then restart the app and " +
"a paywall should show when the app is launched from a cold start. The button " +
"does nothing.",
- test = { scope, events -> },
+ test = { scope, events, _ -> },
)
var test53Info =
UITestInfo(
@@ -873,14 +910,14 @@ object UITestHandler {
"This covers test 49 too. Change the API Key to the AppLaunch UITest app. " +
"Then restart the app and a paywall should show when the app is launched from a " +
"cold start. Also should happen from a clean install. The button does nothing.",
- test = { scope, events -> },
+ test = { scope, events, _ -> },
)
var test56Info =
UITestInfo(
56,
"The debugger should open when tapping on link. Currently the debugger isn't " +
"implemented.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -911,7 +948,7 @@ object UITestHandler {
"Change API key to DeepLink. Present paywall from implicit trigger: " +
"`deepLink_open`. Verify the `Deep link event received successfully.` in the" +
" console.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -940,24 +977,7 @@ object UITestHandler {
"Change API key to TransactionAbandon. Then, present paywall, try to purchase, " +
"then abandon the transaction. Another paywall will present. In the console " +
"you'll see !!! TEST 58 !!!",
- test = { scope, events ->
- // Create a mock Superwall delegate
- val delegate = MockSuperwallDelegate()
-
- // Set delegate
- Superwall.instance.delegate = delegate
-
- // Respond to Superwall events
- delegate.handleSuperwallEvent { eventInfo ->
- when (eventInfo.event) {
- is SuperwallEvent.TransactionAbandon -> {
- println("!!! TEST 58 !!! TransactionAbandon.")
- }
-
- else -> return@handleSuperwallEvent
- }
- }
-
+ test = { scope, events, _ ->
Superwall.instance.register("campaign_trigger")
},
)
@@ -969,7 +989,7 @@ object UITestHandler {
"then survey_response will be printed in the console. The paywall will dismiss " +
"and PaywallDecline will be printed in the console. Then a paywall should auto " +
"present after the decline.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1002,7 +1022,7 @@ object UITestHandler {
"PurchaseResult.Failed(e). Then present paywall, go to purchase and cancel the " +
"purchase. This will trigger a transaction_fail and it will dismiss the paywall " +
"and present another paywall. In the console you'll see !!! TEST 60 !!!.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1028,17 +1048,18 @@ object UITestHandler {
62,
"Verify that an invalid URL like `#` doesn't crash the app. Manually tap on" +
"the \"Open in-app #\" button.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Present paywall with URLs
Superwall.instance.register(event = "present_urls")
},
)
+
var test63Info =
UITestInfo(
63,
"Don't have an active subscription, present paywall, tap restore. Check " +
"the \"No Subscription Found\" alert pops up.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
@@ -1062,7 +1083,7 @@ object UITestHandler {
"Then open and close the paywall again to make sure survey doesn't show again. " +
"The console will just print out !!! TEST 63!!! with PaywallClose and the feature " +
"block will fire again.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1103,7 +1124,7 @@ object UITestHandler {
"show. Choose the other option and type \"Test\" and tap Submit. The console " +
"will print out !!! TEST 65!!! with PaywallClose and again with SurveyResponse. " +
"The feature block will fire.",
- test = { scope, events ->
+ test = { scope, events, message ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1114,11 +1135,16 @@ object UITestHandler {
delegate.handleSuperwallEvent { eventInfo ->
when (eventInfo.event) {
is SuperwallEvent.PaywallClose -> {
- println("!!! TEST 65 !!! PaywallClose")
+ scope.launch {
+ message.emit(eventInfo.event)
+ }
}
is SuperwallEvent.SurveyResponse -> {
- println("!!! TEST 65 !!! SurveyResponse")
+ scope.launch {
+ message.emit(eventInfo.event)
+ println("!!! TEST 65 !!! SurveyResponse")
+ }
}
else -> return@handleSuperwallEvent
@@ -1143,7 +1169,7 @@ object UITestHandler {
"Delete an reinstall app. Present paywall then tap close button. A survey will " +
"NOT show. The console will print out !!! TEST 66 !!! with PaywallClose. " +
"The feature block will fire.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1188,10 +1214,10 @@ object UITestHandler {
"The console will print out !!! TEST 68 !!! with PaywallClose. Close the second " +
"paywall. The feature block will fire on close of the second paywall.",
test =
- { scope, events ->
+ { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
-
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
// Set delegate
Superwall.instance.delegate = delegate
@@ -1231,7 +1257,7 @@ object UITestHandler {
"Delete an reinstall app. Present paywall then tap close button. Make sure a " +
"survey is displayed. Tap Option 1, make sure it dismisses and the console prints " +
"!!! TEST 70 !!! twice with both PaywallClose and SurveyResponse.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1281,7 +1307,7 @@ object UITestHandler {
"Delete an reinstall app. Present paywall then purchase product. Make sure the " +
"paywall closes and DOES NOT show a survey. The console should NOT print " +
"!!! TEST 71 !!!.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1322,7 +1348,7 @@ object UITestHandler {
"\"!!! seed - 1: 2\", where 2 is a seed number. Then \"!!! user ID: abc\", then " +
" \"!!! seed - 2: 2\".",
test =
- { scope, events ->
+ { scope, events, _ ->
Superwall.instance.identify(userId = "abc")
var seed = Superwall.instance.userAttributes["seed"]
@@ -1348,7 +1374,7 @@ object UITestHandler {
"Delete an reinstall app. Present paywall then close the paywall. A survey will " +
"show. Tap the close button. The paywall will close and the console will print " +
"\"!!! TEST 74 !!! SurveyClose\".",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1376,7 +1402,7 @@ object UITestHandler {
"Present paywall then make a purchase. Make sure !!! TEST 75 !!! with \"false\" " +
"for the transaction being nil, a product id, and a paywall id is printed to the " +
"console.",
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1408,7 +1434,7 @@ object UITestHandler {
UITestInfo(
82,
"Verify that our pricing gets templated in correctly.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "price_readout")
},
)
@@ -1418,63 +1444,63 @@ object UITestHandler {
"The first time launch is tapped you will land in holdout. Check Skipped paywall " +
"for holdout printed in console. Tapping launch again will present paywall. Will " +
"need to delete app to be able to do it again as it uses limits.",
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "holdout_one_time_occurrence")
},
)
var testAndroid4Info =
UITestInfo(
- 4,
+ 40,
"NOTE: Must use `Android Main screen` API key. Launch compose debug screen: " +
"Verify that paywall loads in Tab 0. Go to Tab 2 and press `Another Paywall` button. " +
"Verify that paywall does not load (only 1 paywall can be displayed at once).",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
val intent = Intent(this, ComposeActivity::class.java)
startActivity(intent)
},
)
var testAndroid9Info =
UITestInfo(
- 9,
+ 90,
"Tap launch button. It should display the paywall. Tap again and it should NOT " +
"display again. You will need to delete and reinstall the app to test this again. " +
"This tests the occurrence limit.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "one_time_occurrence")
},
)
var testAndroid18Info =
UITestInfo(
- 18,
+ 180,
"Tap launch button. It should display the paywall. Tap again and it should NOT " +
"display again within a minute, printing a NoRuleMatch in the console. After a " +
"minute, tap again, and it should show. You will need to delete and reinstall " +
"the app to test this again.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "once_a_minute")
},
)
var testAndroid19Info =
UITestInfo(
- 19,
+ 190,
"Tap launch button. The paywall should not display until one day or longer has " +
"passed since the last once_a_minute event. You'll get a NoRuleMatch in the " +
"console.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "one_day_since_last_event")
},
)
var testAndroid20Info =
UITestInfo(
- 20,
+ 200,
"Non-recurring product purchase. Purchase the product and",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1497,35 +1523,43 @@ object UITestHandler {
)
var testAndroid21Info =
UITestInfo(
- 21,
+ 210,
"Tap launch button. Paywall should display. Tests that paywalls with products " +
"that have base plans and offerIds works.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "subs_baseplan_offer_with_free_trial")
},
)
var testAndroid22Info =
UITestInfo(
- 22,
+ 220,
"Tap launch button. Paywall should display. Purchase then quit app. Notification" +
" should appear after a minute.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.register(event = "notifications")
},
)
var testAndroid23Info =
UITestInfo(
- 23,
+ 230,
"Tap button. You should see first_name: Jack printed out in user attributes " +
"then the user attributes shouldn't contain first_name",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, msg ->
Superwall.instance.setUserAttributes(attributes = mapOf("first_name" to "Jack"))
- println(Superwall.instance.userAttributes)
+ val attributes = Superwall.instance.userAttributes
+ println(attributes)
+ scope.launch {
+ msg.emit(attributes)
+ }
Superwall.instance.setUserAttributes(attributes = mapOf("first_name" to null))
- println(Superwall.instance.userAttributes)
+ val next = Superwall.instance.userAttributes
+ println(next)
+ scope.launch {
+ msg.emit(next)
+ }
},
)
@@ -1534,7 +1568,7 @@ object UITestHandler {
100,
"Entitlements test: Tap launch button. Paywall should display when user has no entitlements.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
Superwall.instance.register(event = "entitlements_test_basic")
},
@@ -1545,7 +1579,7 @@ object UITestHandler {
101,
"Entitlements test: Tap launch button. Paywall should not display since user has the entitlement `basic`. Dialog should show.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("basic"))))
Superwall.instance.register(event = "entitlements_test_basic") {
val alertController =
@@ -1565,7 +1599,7 @@ object UITestHandler {
102,
"Entitlements test: Tap launch button. Paywall should display when user has no `pro` entitlements.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus("basic")
Superwall.instance.register(event = "entitlements_test_pro") {
val alertController =
@@ -1585,7 +1619,7 @@ object UITestHandler {
103,
"Entitlements test: Tap launch button. Paywall should not display when user has `pro` entitlements. Dialog should show.",
testCaseType = TestCaseType.Android,
- test = { scope, events ->
+ test = { scope, events, _ ->
Superwall.instance.setEntitlementStatus("pro")
Superwall.instance.register(event = "entitlements_test_pro") {
val alertController =
diff --git a/example/README.md b/example/README.md
index a68d5b66..f5c315e5 100644
--- a/example/README.md
+++ b/example/README.md
@@ -8,10 +8,10 @@ Usually, to integrate Superwall into your app, you first need to have configured
Feature | Sample Project Location
--- | ---
-🕹 Configuring Superwall | [MainActivity.kt](app/src/main/java/com/superwall/exampleapp/MainActivity.kt#L54)
-👉 Presenting a paywall | [HomeActivity.kt](app/src/main/java/com/superwall/exampleapp/HomeActivity.kt#L150)
-👥 Identifying account | [MainActivity.kt](app/src/main/java/com/superwall/exampleapp/MainActivity.kt#L164)
-👥 Resetting account | [HomeActivity.kt](app/src/main/java/com/superwall/exampleapp/HomeActivity.kt#L182)
+🕹 Configuring Superwall | [MainActivity.kt](app/src/main/java/com/superwall/superapp/MainActivity.kt#L54)
+👉 Presenting a paywall | [HomeActivity.kt](app/src/main/java/com/superwall/superapp/HomeActivity.kt#L150)
+👥 Identifying account | [MainActivity.kt](app/src/main/java/com/superwall/superapp/MainActivity.kt#L164)
+👥 Resetting account | [HomeActivity.kt](app/src/main/java/com/superwall/superapp/HomeActivity.kt#L182)
## Requirements
@@ -23,11 +23,11 @@ This example app uses:
## Getting Started
-Clone or download Superwall from the [project home page](https://github.com/superwall/Superwall-Android). Then, open the folder in Android Studio and take a look at the code inside [example/app/kotlin+java/com.superwall.exampleapp](app/src/main/java/com/superwall/exampleapp)`.
+Clone or download Superwall from the [project home page](https://github.com/superwall/Superwall-Android). Then, open the folder in Android Studio and take a look at the code inside [example/app/kotlin+java/com.superwall.exampleapp](app/src/main/java/com/superwall/superapp)`.
-You'll see a [ui.theme](app/src/main/java/com/superwall/exampleapp/ui/theme) folder relating to the design and components used in the app, which you don't need to worry about.
+You'll see a [ui.theme](app/src/main/java/com/superwall/superapp/ui/theme) folder relating to the design and components used in the app, which you don't need to worry about.
-The [MainActivity.kt](app/src/main/java/com/superwall/exampleapp/MainActivity.kt) handles the configuration of the SDK and login, and [HomeActivity.kt](app/src/main/java/com/superwall/exampleapp/HomeActivity.kt) handles the presentation of paywalls.
+The [MainActivity.kt](app/src/main/java/com/superwall/superapp/MainActivity.kt) handles the configuration of the SDK and login, and [HomeActivity.kt](app/src/main/java/com/superwall/superapp/HomeActivity.kt) handles the presentation of paywalls.
Build and run the app and you'll see the welcome screen:
@@ -35,11 +35,11 @@ Build and run the app and you'll see the welcome screen:
-Superwall is [configured](app/src/main/java/com/superwall/exampleapp/MainActivity.kt#L54) on app launch, setting an `apiKey`.
+Superwall is [configured](app/src/main/java/com/superwall/superapp/MainActivity.kt#L54) on app launch, setting an `apiKey`.
## Logging In
-On the welcome screen, enter your name in the **text field**This saves to the Superwall user attributes using [Superwall.instance.setUserAttributes(_:)](app/src/main/java/com/superwall/exampleapp/MainActivity.kt#L159). You don't need to set user attributes, but it can be useful if you want to create a rule to present a paywall based on a specific attribute you've set. You can also recall user attributes on your paywall to personalise the messaging.
+On the welcome screen, enter your name in the **text field**This saves to the Superwall user attributes using [Superwall.instance.setUserAttributes(_:)](app/src/main/java/com/superwall/superapp/MainActivity.kt#L159). You don't need to set user attributes, but it can be useful if you want to create a rule to present a paywall based on a specific attribute you've set. You can also recall user attributes on your paywall to personalise the messaging.
Tap **Log In**. This identifies the user (with a hardcoded userId that we've set), retrieving any paywalls that have already been assigned to them.
@@ -51,7 +51,7 @@ You'll see the home screen:
## Presenting a Paywall
-At the heart of Superwall's SDK lies [Superwall.shared.register(event:params:handler:feature:)](app/src/main/java/com/superwall/exampleapp/HomeActivity.kt#L150).
+At the heart of Superwall's SDK lies [Superwall.shared.register(event:params:handler:feature:)](app/src/main/java/com/superwall/superapp/HomeActivity.kt#L150).
This allows you to register an event to access a feature that may or may not be paywalled later in time. It also allows you to choose whether the user can access the feature even if they don't make a purchase. You can read more about this [in our docs](https://docs.superwall.com/docs).
@@ -59,7 +59,7 @@ On the [Superwall Dashboard](https://superwall.com/dashboard) you add this event
When an event is registered, Superwall evaluates the rules associated with it to determine whether or not to show a paywall.
-By calling [Superwall.shared.register(event:params:handler:feature:)](app/src/main/java/com/superwall/exampleapp/HomeActivity.kt#L150), you present a paywall in response to the event `campaign_trigger`.
+By calling [Superwall.shared.register(event:params:handler:feature:)](app/src/main/java/com/superwall/superapp/HomeActivity.kt#L150), you present a paywall in response to the event `campaign_trigger`.
On screen you'll see some explanatory text and a button to launch a feature that is behind a paywall. Tap the **Launch Feature** button and you'll see the paywall. If the event is disabled on the dashboard, the paywall wouldn't show and the feature would fire immediately. In this case, the feature is just an alert.
diff --git a/example/app/build.gradle.kts b/example/app/build.gradle.kts
index 42a3d876..37cbde6e 100644
--- a/example/app/build.gradle.kts
+++ b/example/app/build.gradle.kts
@@ -4,7 +4,7 @@ plugins {
}
android {
- namespace = "com.superwall.exampleapp"
+ namespace = "com.superwall.superapp"
compileSdk = 34
defaultConfig {
@@ -29,6 +29,24 @@ android {
)
}
}
+ flavorDimensions += "version"
+ productFlavors {
+ create("default") {
+ dimension = "version"
+ }
+ create("entitlements") {
+ dimension = "version"
+ }
+ create("observer") {
+ dimension = "version"
+ }
+ create("purchase") {
+ dimension = "version"
+ }
+ create("revenuecat") {
+ dimension = "version"
+ }
+ }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
@@ -49,6 +67,7 @@ android {
dependencies {
implementation(project(":superwall"))
+ implementation(project(":superwall-compose"))
// Billing
implementation(libs.billing)
diff --git a/example/app/src/main/java/com/superwall/exampleapp/HomeActivity.kt b/example/app/src/default/java/com/superwall/superapp/HomeActivity.kt
similarity index 95%
rename from example/app/src/main/java/com/superwall/exampleapp/HomeActivity.kt
rename to example/app/src/default/java/com/superwall/superapp/HomeActivity.kt
index b1b431ef..cf469547 100644
--- a/example/app/src/main/java/com/superwall/exampleapp/HomeActivity.kt
+++ b/example/app/src/default/java/com/superwall/superapp/HomeActivity.kt
@@ -31,7 +31,7 @@ import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import com.superwall.exampleapp.ui.theme.SuperwallExampleAppTheme
import com.superwall.sdk.Superwall
-import com.superwall.sdk.delegate.SubscriptionStatus
+import com.superwall.sdk.models.entitlements.EntitlementStatus
import com.superwall.sdk.paywall.presentation.PaywallPresentationHandler
import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason
import com.superwall.sdk.paywall.presentation.register
@@ -43,10 +43,11 @@ class HomeActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
- val subscriptionStatus by Superwall.instance.subscriptionStatus.collectAsState()
+ val entitlementStatus by Superwall.instance.entitlements.status
+ .collectAsState()
SuperwallExampleAppTheme {
HomeScreen(
- entitlementStatus = subscriptionStatus,
+ entitlementStatus = entitlementStatus,
onLogOutClicked = {
finish()
},
@@ -58,19 +59,19 @@ class HomeActivity : ComponentActivity() {
@Composable
fun HomeScreen(
- entitlementStatus: SubscriptionStatus,
+ entitlementStatus: EntitlementStatus,
onLogOutClicked: () -> Unit,
) {
val context = LocalContext.current
val subscriptionText =
when (entitlementStatus) {
- SubscriptionStatus.UNKNOWN -> "Loading subscription status."
- SubscriptionStatus.ACTIVE ->
+ is EntitlementStatus.Unknown -> "Loading subscription status."
+ is EntitlementStatus.Active ->
"You currently have an active subscription. Therefore, the " +
"paywall will never show. For the purposes of this app, delete and reinstall the " +
"app to clear subscriptions."
- SubscriptionStatus.INACTIVE ->
+ is EntitlementStatus.Inactive ->
"You do not have an active subscription so the paywall will " +
"show when clicking the button."
}
diff --git a/example/app/src/default/java/com/superwall/superapp/MainApplication.kt b/example/app/src/default/java/com/superwall/superapp/MainApplication.kt
new file mode 100644
index 00000000..c0692219
--- /dev/null
+++ b/example/app/src/default/java/com/superwall/superapp/MainApplication.kt
@@ -0,0 +1,14 @@
+package com.superwall.exampleapp
+
+import android.app.Application
+import com.superwall.sdk.Superwall
+
+class MainApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ Superwall.configure(
+ this,
+ Keys.EXAMPLE_KEY,
+ )
+ }
+}
diff --git a/example/app/src/entitlements/README.md b/example/app/src/entitlements/README.md
new file mode 100644
index 00000000..d0597f10
--- /dev/null
+++ b/example/app/src/entitlements/README.md
@@ -0,0 +1,14 @@
+## Entitlements Example
+
+This application flavor serves as an introduction to using Superwall's entitlements.
+It demonstrates 3 types of features and paywalls:
+
+1. No subscription, no feature-gate
+2. Feature-gate allowing only Pro users
+3. Feature-gate allowing only Diamond users
+
+Entitlement status is controlled either by Superwall automatically during purchase or by your PurchaseController.
+To simplify testing, this example allows you to change the Entitlement status by tapping on the `Change Entitlement Status` Dropdown
+and choosing the desired status.
+
+
diff --git a/example/app/src/entitlements/java/com/superwall/superapp/HomeActivity.kt b/example/app/src/entitlements/java/com/superwall/superapp/HomeActivity.kt
new file mode 100644
index 00000000..eade1d9c
--- /dev/null
+++ b/example/app/src/entitlements/java/com/superwall/superapp/HomeActivity.kt
@@ -0,0 +1,309 @@
+package com.superwall.exampleapp
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.view.WindowCompat
+import com.superwall.exampleapp.ui.theme.SuperwallExampleAppTheme
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.models.entitlements.EntitlementStatus
+import com.superwall.sdk.paywall.presentation.PaywallPresentationHandler
+import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason
+import com.superwall.sdk.paywall.presentation.register
+
+class HomeActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContent {
+ val entitlementStatus by Superwall.instance.entitlements.status
+ .collectAsState()
+ SuperwallExampleAppTheme {
+ HomeScreen(
+ entitlementStatus = entitlementStatus,
+ onLogOutClicked = {
+ finish()
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun HomeScreen(
+ entitlementStatus: EntitlementStatus,
+ onLogOutClicked: () -> Unit,
+) {
+ val context = LocalContext.current
+ val subscriptionText =
+ when (entitlementStatus) {
+ is EntitlementStatus.Unknown -> "Loading entitlement status."
+ is EntitlementStatus.Active ->
+ "You currently have active entitlemements: ${
+ entitlementStatus.entitlements.map { it.id }.joinToString()
+ }."
+
+ is EntitlementStatus.Inactive ->
+ "You do not have any active entitlements so the paywall will " +
+ "show when clicking the button."
+ }
+
+ var dropdownState =
+ remember {
+ mutableStateOf(false)
+ }
+
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = "Presenting a Paywall",
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 50.dp),
+ )
+ Text(
+ text = "Entitlement Status: $subscriptionText",
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center,
+ )
+ Row(
+ Modifier
+ .padding(8.dp)
+ .clickable {
+ dropdownState.value = true
+ },
+ ) {
+ Text(text = "Change entitlement status")
+ Icon(Icons.Default.KeyboardArrowDown, contentDescription = null, tint = Color.White)
+ DropdownMenu(
+ modifier =
+ Modifier
+ .padding(start = 8.dp)
+ .background(Color.DarkGray),
+ expanded = dropdownState.value,
+ onDismissRequest = { dropdownState.value = false },
+ ) {
+ DropdownMenuItem({ Text("Inactive") }, onClick = {
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
+ dropdownState.value = false
+ })
+ DropdownMenuItem({ Text("Pro") }, onClick = {
+ Superwall.instance.setEntitlementStatus("pro")
+ dropdownState.value = false
+ })
+ DropdownMenuItem({ Text("diamond") }, onClick = {
+ Superwall.instance.setEntitlementStatus("diamond")
+ dropdownState.value = false
+ })
+ }
+ }
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text =
+ "Each button below registers a placement. " +
+ "Each placement has been added to a campaign on the Superwall dashboard." +
+ " When the placement is registered, the audience filters in the campaign " +
+ "are evaluated and attempt to show a paywall." +
+ "Each product on the paywall is associated with an entitlement and Pro" +
+ " and Diamond features are gated behind their respective entitlements.",
+ textAlign = TextAlign.Center,
+ )
+ }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ SWButton("Launch Non-gated feature", {
+ Superwall.instance.register("non_gated") {
+ context.dialog("Feature Launched", "The feature block was called")
+ }
+ })
+ SWButton("Launch Pro feature", {
+ Superwall.instance.register("pro") {
+ context.dialog("Pro Feature Launched", "The Pro feature block was called")
+ }
+ })
+ SWButton("Launch Diamond feature", {
+ Superwall.instance.register("diamond") {
+ context.dialog(
+ "Diamond Feature Launched",
+ "The Diamond feature block was called",
+ )
+ }
+ })
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(
+ onClick = {
+ Superwall.instance.reset()
+ onLogOutClicked()
+ },
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 50.dp)
+ .padding(vertical = 8.dp),
+ ) {
+ Text(
+ text = "Log Out",
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+ }
+ }
+ }
+}
+
+fun showPaywall(
+ context: Context,
+ event: String,
+) {
+ val handler = PaywallPresentationHandler()
+ handler.onDismiss { paywallInfo ->
+ println("The paywall dismissed. PaywallInfo: $paywallInfo")
+ }
+ handler.onPresent { paywallInfo ->
+ println("The paywall presented. PaywallInfo: $paywallInfo")
+ }
+ handler.onError { error ->
+ println("The paywall presentation failed with error $error")
+ }
+ handler.onSkip { reason ->
+ when (reason) {
+ is PaywallSkippedReason.EventNotFound -> {
+ print("Paywall not shown because this event isn't part of a campaign.")
+ }
+
+ is PaywallSkippedReason.Holdout -> {
+ print(
+ "Paywall not shown because user is in a holdout group in " +
+ "Experiment: ${reason.experiment.id}",
+ )
+ }
+
+ is PaywallSkippedReason.NoRuleMatch -> {
+ print("Paywall not shown because user doesn't match any rules.")
+ }
+
+ is PaywallSkippedReason.UserIsSubscribed -> {
+ print("Paywall not shown because user is subscribed.")
+ }
+ }
+ }
+
+ Superwall.instance.register(event = "campaign_trigger", handler = handler) {
+ // code in here can be remotely configured to execute. Either
+ // (1) always after presentation or
+ // (2) only if the user pays
+ // code is always executed if no paywall is configured to show
+ val builder =
+ AlertDialog
+ .Builder(context)
+ .setTitle("Feature Launched")
+ .setMessage("The feature block was called")
+
+ builder.setPositiveButton("Ok") { _, _ -> }
+
+ val alertDialog = builder.create()
+ alertDialog.show()
+ }
+}
+
+fun Context.dialog(
+ title: String,
+ text: String,
+) {
+ val builder =
+ AlertDialog
+ .Builder(this)
+ .setTitle(title)
+ .setMessage(text)
+
+ builder.setPositiveButton("Ok") { _, _ -> }
+
+ val alertDialog = builder.create()
+ alertDialog.show()
+}
+
+@Composable
+fun SWButton(
+ text: String,
+ onClick: () -> Unit,
+) {
+ Button(
+ onClick = onClick,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 50.dp)
+ .padding(top = 8.dp),
+ ) {
+ Text(
+ text = text,
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+}
diff --git a/example/app/src/entitlements/java/com/superwall/superapp/MainApplication.kt b/example/app/src/entitlements/java/com/superwall/superapp/MainApplication.kt
new file mode 100644
index 00000000..c0692219
--- /dev/null
+++ b/example/app/src/entitlements/java/com/superwall/superapp/MainApplication.kt
@@ -0,0 +1,14 @@
+package com.superwall.exampleapp
+
+import android.app.Application
+import com.superwall.sdk.Superwall
+
+class MainApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ Superwall.configure(
+ this,
+ Keys.EXAMPLE_KEY,
+ )
+ }
+}
diff --git a/example/app/src/main/AndroidManifest.xml b/example/app/src/main/AndroidManifest.xml
index a5ea957e..36400cdc 100644
--- a/example/app/src/main/AndroidManifest.xml
+++ b/example/app/src/main/AndroidManifest.xml
@@ -9,9 +9,10 @@
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
- android:icon="@mipmap/ic_launcher"
+ android:icon="@drawable/ic_launcher_foreground"
+ android:name=".MainApplication"
android:label="@string/app_name"
- android:roundIcon="@mipmap/ic_launcher_round"
+ android:roundIcon="@drawable/ic_launcher_foreground"
android:supportsRtl="true"
android:theme="@style/Theme.SuperwallExampleApp"
tools:targetApi="31">
diff --git a/example/app/src/main/java/com/superwall/superapp/Keys.kt b/example/app/src/main/java/com/superwall/superapp/Keys.kt
new file mode 100644
index 00000000..62526bc9
--- /dev/null
+++ b/example/app/src/main/java/com/superwall/superapp/Keys.kt
@@ -0,0 +1,5 @@
+package com.superwall.superapp
+
+object Keys {
+ const val EXAMPLE_KEY = "pk_3b18882b1683318b710c741f371f40f54e357c6f0baff1f4"
+}
diff --git a/example/app/src/main/java/com/superwall/exampleapp/MainActivity.kt b/example/app/src/main/java/com/superwall/superapp/MainActivity.kt
similarity index 97%
rename from example/app/src/main/java/com/superwall/exampleapp/MainActivity.kt
rename to example/app/src/main/java/com/superwall/superapp/MainActivity.kt
index 61cbe7a3..c70bd834 100644
--- a/example/app/src/main/java/com/superwall/exampleapp/MainActivity.kt
+++ b/example/app/src/main/java/com/superwall/superapp/MainActivity.kt
@@ -1,4 +1,4 @@
-package com.superwall.exampleapp
+package com.superwall.superapp
import android.content.Context
import android.content.Intent
@@ -44,13 +44,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
-import com.superwall.exampleapp.ui.theme.SuperwallExampleAppTheme
import com.superwall.sdk.Superwall
import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.identity.identify
import com.superwall.sdk.identity.setUserAttributes
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
+import com.superwall.superapp.ui.theme.SuperwallExampleAppTheme
class MainActivity :
ComponentActivity(),
@@ -58,11 +58,6 @@ class MainActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- Superwall.configure(
- applicationContext = application,
- apiKey = "pk_3b18882b1683318b710c741f371f40f54e357c6f0baff1f4",
- )
-
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
SuperwallExampleAppTheme {
diff --git a/example/app/src/main/java/com/superwall/exampleapp/ui/theme/Color.kt b/example/app/src/main/java/com/superwall/superapp/ui/theme/Color.kt
similarity index 85%
rename from example/app/src/main/java/com/superwall/exampleapp/ui/theme/Color.kt
rename to example/app/src/main/java/com/superwall/superapp/ui/theme/Color.kt
index 59ac56fb..ecbdad70 100644
--- a/example/app/src/main/java/com/superwall/exampleapp/ui/theme/Color.kt
+++ b/example/app/src/main/java/com/superwall/superapp/ui/theme/Color.kt
@@ -1,4 +1,4 @@
-package com.superwall.exampleapp.ui.theme
+package com.superwall.superapp.ui.theme
import androidx.compose.ui.graphics.Color
diff --git a/example/app/src/main/java/com/superwall/exampleapp/ui/theme/Theme.kt b/example/app/src/main/java/com/superwall/superapp/ui/theme/Theme.kt
similarity index 98%
rename from example/app/src/main/java/com/superwall/exampleapp/ui/theme/Theme.kt
rename to example/app/src/main/java/com/superwall/superapp/ui/theme/Theme.kt
index 2f381bc5..1d10cb44 100644
--- a/example/app/src/main/java/com/superwall/exampleapp/ui/theme/Theme.kt
+++ b/example/app/src/main/java/com/superwall/superapp/ui/theme/Theme.kt
@@ -1,4 +1,4 @@
-package com.superwall.exampleapp.ui.theme
+package com.superwall.superapp.ui.theme
import android.app.Activity
import android.os.Build
diff --git a/example/app/src/main/java/com/superwall/exampleapp/ui/theme/Type.kt b/example/app/src/main/java/com/superwall/superapp/ui/theme/Type.kt
similarity index 96%
rename from example/app/src/main/java/com/superwall/exampleapp/ui/theme/Type.kt
rename to example/app/src/main/java/com/superwall/superapp/ui/theme/Type.kt
index c78d928e..0267e94d 100644
--- a/example/app/src/main/java/com/superwall/exampleapp/ui/theme/Type.kt
+++ b/example/app/src/main/java/com/superwall/superapp/ui/theme/Type.kt
@@ -1,4 +1,4 @@
-package com.superwall.exampleapp.ui.theme
+package com.superwall.superapp.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
diff --git a/example/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/example/app/src/main/res/mipmap-anydpi/ic_launcher.xml
deleted file mode 100644
index 6f3b755b..00000000
--- a/example/app/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/example/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/example/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
deleted file mode 100644
index 6f3b755b..00000000
--- a/example/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/example/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/example/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78e..00000000
Binary files a/example/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/example/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d1..00000000
Binary files a/example/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/example/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d64..00000000
Binary files a/example/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/example/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611da..00000000
Binary files a/example/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/example/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a3070..00000000
Binary files a/example/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a6956..00000000
Binary files a/example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/example/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77f..00000000
Binary files a/example/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f508..00000000
Binary files a/example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d6427..00000000
Binary files a/example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae37..00000000
Binary files a/example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/example/app/src/observer/README.md b/example/app/src/observer/README.md
new file mode 100644
index 00000000..d4eb94f1
--- /dev/null
+++ b/example/app/src/observer/README.md
@@ -0,0 +1,22 @@
+## Observer mode Example
+
+This application flavor serves as an introduction to Superwall's observer mode.
+It demonstrates 2 different types of transaction observing:
+
+1. Automatic Observation
+2. Manual Observation
+
+Automatic observation can be tested by tapping the first button.
+Otherwise, it can be accomplished manually through belonging methods and their associated buttons.
+
+
+## Setup Note
+
+- Set your Superwall API key in `MainApplication.kt`
+- Change `applicationId` in `build.gradle.kts` to match your `applicationId`
+- Change product names in `./com/superwall/superapp/HomeActivity.kt` to match your products
+- Ensure you are added as a [License Tester](https://developer.android.com/google/play/billing/test) to your app on Google Play Store
+
+
+
+
diff --git a/example/app/src/observer/java/com/superwall/superapp/HomeActivity.kt b/example/app/src/observer/java/com/superwall/superapp/HomeActivity.kt
new file mode 100644
index 00000000..04389abc
--- /dev/null
+++ b/example/app/src/observer/java/com/superwall/superapp/HomeActivity.kt
@@ -0,0 +1,346 @@
+package com.superwall.superapp
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.view.WindowCompat
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingResult
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.billing.observer.SuperwallBillingFlowParams
+import com.superwall.sdk.billing.observer.launchBillingFlowWithSuperwall
+import com.superwall.sdk.models.entitlements.EntitlementStatus
+import com.superwall.sdk.paywall.presentation.PaywallPresentationHandler
+import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason
+import com.superwall.sdk.paywall.presentation.register
+import com.superwall.sdk.store.PurchasingObserverState
+import com.superwall.superapp.ui.theme.SuperwallExampleAppTheme
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class HomeActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContent {
+ val entitlementStatus by Superwall.instance.entitlements.status
+ .collectAsState()
+ SuperwallExampleAppTheme {
+ HomeScreen(
+ entitlementStatus = entitlementStatus,
+ onLogOutClicked = {
+ finish()
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun HomeScreen(
+ entitlementStatus: EntitlementStatus,
+ onLogOutClicked: () -> Unit,
+) {
+ val context = LocalContext.current
+ var dropdownState =
+ remember {
+ mutableStateOf(false)
+ }
+
+ val scope = rememberCoroutineScope()
+
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = "Observing purchases",
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 50.dp),
+ )
+ Row(
+ Modifier
+ .padding(8.dp)
+ .clickable {
+ dropdownState.value = true
+ },
+ ) {
+ Text(text = "Change entitlement status")
+ Icon(Icons.Default.KeyboardArrowDown, contentDescription = null, tint = Color.White)
+ }
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text =
+ "The buttons below trigger the purchase flow. " +
+ "The first button will automatically observe the purchase flow. " +
+ "The other buttons will manually observe the purchase flow." +
+ "Please ensure you have your products set correctly" +
+ " and are added as a tester to the Google Play Console.",
+ textAlign = TextAlign.Center,
+ )
+ }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ SWButton("Purchase with automatic observation", {
+ val billingClient =
+ BillingClient
+ .newBuilder(context)
+ .enablePendingPurchases()
+ .setListener { billingResult, purchases -> }
+ .build()
+ scope.launch(Dispatchers.IO) {
+ // Replace with your product id
+ val product =
+ Superwall.instance
+ .getProducts("com.ui_tests.monthly:com-ui-tests-montly:sw-auto")
+ .getOrNull()!!
+ .values
+ .first()
+
+ // Launch the billing flow with the product and ensure it is observed by superwall
+ billingClient.launchBillingFlowWithSuperwall(
+ (context as Activity),
+ SuperwallBillingFlowParams
+ .newBuilder()
+ .setProductDetailsParamsList(
+ listOf(
+ SuperwallBillingFlowParams.ProductDetailsParams
+ .newBuilder()
+ .setProductDetails(product.rawStoreProduct.underlyingProductDetails)
+ .setOfferToken(
+ product.rawStoreProduct.selectedOffer?.offerToken
+ ?: "",
+ ).build(),
+ ),
+ ).build(),
+ )
+ }
+ })
+ SWButton("Observe purchase start manually", {
+ scope.launch(Dispatchers.IO) {
+ val product =
+ Superwall.instance
+ .getProducts("com.ui_tests.monthly:com-ui-tests-montly:sw-auto")
+ .getOrNull()!!
+ .values
+ .first()
+
+ Superwall.instance.observe(
+ PurchasingObserverState.PurchaseWillBegin(product.rawStoreProduct.underlyingProductDetails),
+ )
+ }
+ })
+ SWButton("Observe purchase complete manually", {
+ scope.launch(Dispatchers.IO) {
+ val product =
+ Superwall.instance
+ .getProducts("com.ui_tests.monthly:com-ui-tests-montly:sw-auto")
+ .getOrNull()!!
+ .values
+ .first()
+
+ Superwall.instance.observe(
+ PurchasingObserverState.PurchaseResult(
+ BillingResult
+ .newBuilder()
+ .setResponseCode(BillingClient.BillingResponseCode.OK)
+ .build(),
+ purchases = listOf(PurchaseMockBuilder.createDefaultPurchase("com.ui_tests.monthly")),
+ ),
+ )
+ }
+ })
+ SWButton("Observe purchase failed manually", {
+ scope.launch(Dispatchers.IO) {
+ val product =
+ Superwall.instance
+ .getProducts("com.ui_tests.monthly:com-ui-tests-montly:sw-auto")
+ .getOrNull()!!
+ .values
+ .first()
+
+ Superwall.instance.observe(
+ PurchasingObserverState.PurchaseError(
+ product.rawStoreProduct.underlyingProductDetails,
+ IllegalStateException("Purchase Abandoned"),
+ ),
+ )
+ }
+ })
+
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(
+ onClick = {
+ Superwall.instance.reset()
+ onLogOutClicked()
+ },
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 50.dp)
+ .padding(vertical = 8.dp),
+ ) {
+ Text(
+ text = "Log Out",
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+ }
+ }
+ }
+}
+
+fun showPaywall(
+ context: Context,
+ event: String,
+) {
+ val handler = PaywallPresentationHandler()
+ handler.onDismiss { paywallInfo ->
+ println("The paywall dismissed. PaywallInfo: $paywallInfo")
+ }
+ handler.onPresent { paywallInfo ->
+ println("The paywall presented. PaywallInfo: $paywallInfo")
+ }
+ handler.onError { error ->
+ println("The paywall presentation failed with error $error")
+ }
+ handler.onSkip { reason ->
+ when (reason) {
+ is PaywallSkippedReason.EventNotFound -> {
+ print("Paywall not shown because this event isn't part of a campaign.")
+ }
+
+ is PaywallSkippedReason.Holdout -> {
+ print(
+ "Paywall not shown because user is in a holdout group in " +
+ "Experiment: ${reason.experiment.id}",
+ )
+ }
+
+ is PaywallSkippedReason.NoRuleMatch -> {
+ print("Paywall not shown because user doesn't match any rules.")
+ }
+
+ is PaywallSkippedReason.UserIsSubscribed -> {
+ print("Paywall not shown because user is subscribed.")
+ }
+ }
+ }
+
+ Superwall.instance.register(event = "campaign_trigger", handler = handler) {
+ // code in here can be remotely configured to execute. Either
+ // (1) always after presentation or
+ // (2) only if the user pays
+ // code is always executed if no paywall is configured to show
+ val builder =
+ AlertDialog
+ .Builder(context)
+ .setTitle("Feature Launched")
+ .setMessage("The feature block was called")
+
+ builder.setPositiveButton("Ok") { _, _ -> }
+
+ val alertDialog = builder.create()
+ alertDialog.show()
+ }
+}
+
+fun Context.dialog(
+ title: String,
+ text: String,
+) {
+ val builder =
+ AlertDialog
+ .Builder(this)
+ .setTitle(title)
+ .setMessage(text)
+
+ builder.setPositiveButton("Ok") { _, _ -> }
+
+ val alertDialog = builder.create()
+ alertDialog.show()
+}
+
+@Composable
+fun SWButton(
+ text: String,
+ onClick: () -> Unit,
+) {
+ Button(
+ onClick = onClick,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 50.dp)
+ .padding(top = 8.dp),
+ ) {
+ Text(
+ text = text,
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+}
diff --git a/example/app/src/observer/java/com/superwall/superapp/MainApplication.kt b/example/app/src/observer/java/com/superwall/superapp/MainApplication.kt
new file mode 100644
index 00000000..9cb37757
--- /dev/null
+++ b/example/app/src/observer/java/com/superwall/superapp/MainApplication.kt
@@ -0,0 +1,19 @@
+package com.superwall.superapp
+
+import android.app.Application
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.config.options.SuperwallOptions
+
+class MainApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ Superwall.configure(
+ this,
+ Keys.EXAMPLE_KEY,
+ options =
+ SuperwallOptions().apply {
+ shouldObservePurchases = true
+ },
+ )
+ }
+}
diff --git a/example/app/src/observer/java/com/superwall/superapp/PurchaseMockBuilder.kt b/example/app/src/observer/java/com/superwall/superapp/PurchaseMockBuilder.kt
new file mode 100644
index 00000000..b6dd8dfb
--- /dev/null
+++ b/example/app/src/observer/java/com/superwall/superapp/PurchaseMockBuilder.kt
@@ -0,0 +1,105 @@
+package com.superwall.superapp
+
+import com.android.billingclient.api.Purchase
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+class PurchaseMockBuilder {
+ private val purchaseJson = JSONObject()
+
+ @Throws(JSONException::class)
+ fun setPurchaseState(state: Int): PurchaseMockBuilder {
+ purchaseJson.put("purchaseState", if (state == 2) 4 else state)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setPurchaseTime(time: Long): PurchaseMockBuilder {
+ purchaseJson.put("purchaseTime", time)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setOrderId(orderId: String?): PurchaseMockBuilder {
+ purchaseJson.put("orderId", orderId)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setProductId(productId: String?): PurchaseMockBuilder {
+ val productIds = JSONArray()
+ productIds.put(productId)
+ purchaseJson.put("productIds", productIds)
+ // For backward compatibility
+ purchaseJson.put("productId", productId)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setQuantity(quantity: Int): PurchaseMockBuilder {
+ purchaseJson.put("quantity", quantity)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setPurchaseToken(token: String?): PurchaseMockBuilder {
+ purchaseJson.put("token", token)
+ purchaseJson.put("purchaseToken", token)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setPackageName(packageName: String?): PurchaseMockBuilder {
+ purchaseJson.put("packageName", packageName)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setDeveloperPayload(payload: String?): PurchaseMockBuilder {
+ purchaseJson.put("developerPayload", payload)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setAcknowledged(acknowledged: Boolean): PurchaseMockBuilder {
+ purchaseJson.put("acknowledged", acknowledged)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setAutoRenewing(autoRenewing: Boolean): PurchaseMockBuilder {
+ purchaseJson.put("autoRenewing", autoRenewing)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun setAccountIdentifiers(
+ obfuscatedAccountId: String?,
+ obfuscatedProfileId: String?,
+ ): PurchaseMockBuilder {
+ purchaseJson.put("obfuscatedAccountId", obfuscatedAccountId)
+ purchaseJson.put("obfuscatedProfileId", obfuscatedProfileId)
+ return this
+ }
+
+ @Throws(JSONException::class)
+ fun build(): Purchase = Purchase(purchaseJson.toString(), "dummy-signature")
+
+ companion object {
+ @Throws(JSONException::class)
+ fun createDefaultPurchase(id: String = "premium_subscription"): Purchase =
+ PurchaseMockBuilder()
+ .setPurchaseState(Purchase.PurchaseState.PURCHASED)
+ .setPurchaseTime(System.currentTimeMillis())
+ .setOrderId("GPA.1234-5678-9012-34567")
+ .setPurchaseToken("opaque-token-up-to-1950-characters")
+ .setPackageName("com.example.app")
+ .setProductId(id)
+ .setQuantity(1)
+ .setDeveloperPayload("")
+ .setAcknowledged(true)
+ .setAutoRenewing(true)
+ .build()
+ }
+}
diff --git a/example/app/src/purchase/README.md b/example/app/src/purchase/README.md
new file mode 100644
index 00000000..85bfbdf6
--- /dev/null
+++ b/example/app/src/purchase/README.md
@@ -0,0 +1,22 @@
+## GetProducts & Purchase API Example
+
+This application flavor serves as an introduction to Superwall's GetProducts & Purchase APIs.
+It demonstrates fetching products with 3 different types:
+
+1. With offer autoselection
+2. With no offer
+3. With a specific offer
+
+Due to Google Play's limitations, you might not be able to purchase the pre-defined products, so you will
+need to replace them with your own. For that, follow the setup.
+
+## Setup
+
+- Set your Superwall API key in `MainApplication.kt`
+- Change `applicationId` in `build.gradle.kts` to match your `applicationId`
+- Change product names in `./com/superwall/superapp/HomeActivity.kt` to match your products
+- Ensure you are added as a [License Tester](https://developer.android.com/google/play/billing/test) to your app on Google Play Store
+
+
+
+
diff --git a/example/app/src/purchase/java/com/superwall/superapp/HomeActivity.kt b/example/app/src/purchase/java/com/superwall/superapp/HomeActivity.kt
new file mode 100644
index 00000000..590d0140
--- /dev/null
+++ b/example/app/src/purchase/java/com/superwall/superapp/HomeActivity.kt
@@ -0,0 +1,298 @@
+package com.superwall.superapp
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.view.WindowCompat
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.models.entitlements.EntitlementStatus
+import com.superwall.sdk.paywall.presentation.PaywallPresentationHandler
+import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason
+import com.superwall.sdk.paywall.presentation.register
+import com.superwall.sdk.store.abstractions.product.StoreProduct
+import com.superwall.superapp.ui.theme.SuperwallExampleAppTheme
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class HomeActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContent {
+ val entitlementStatus by Superwall.instance.entitlements.status
+ .collectAsState()
+ SuperwallExampleAppTheme {
+ HomeScreen(
+ entitlementStatus = entitlementStatus,
+ onLogOutClicked = {
+ finish()
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun HomeScreen(
+ entitlementStatus: EntitlementStatus,
+ onLogOutClicked: () -> Unit,
+) {
+ val context = LocalContext.current
+ var dropdownState =
+ remember {
+ mutableStateOf(false)
+ }
+
+ val scope = rememberCoroutineScope()
+ var products: List by remember {
+ mutableStateOf(emptyList())
+ }
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = "GetProducts & Purchase API",
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 50.dp),
+ )
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text =
+ "The first button will get and show a list of products. Tapping a purchase button will start the purchase flow." +
+ "Tapping restore will restore purchased products. To test with your application, ensure that your `applicationId` and products match store ones.",
+ textAlign = TextAlign.Center,
+ )
+ }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ SWButton(
+ text = "Load products to purchase",
+ onClick = {
+ scope.launch(Dispatchers.IO) {
+ val product =
+ Superwall.instance
+ .getProducts(
+ "com.ui_tests.monthly:com-ui-tests-monthly:sw-auto",
+ "com.ui_tests.monthly:com-ui-tests-monthly:sw-none",
+ "com.ui_tests.quarterly2:com.ui_tests.quarterly2:free-trial-one-week",
+ ).getOrNull()!!
+ .values
+ products = product.toList()
+ }
+ },
+ )
+ products.forEach {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(verticalArrangement = Arrangement.Center) {
+ Text(it.productIdentifier)
+
+ it.fullIdentifier
+ .split(":")
+ .drop(1)
+ .let {
+ Text(
+ "Base plan: ${it.first()}",
+ color = Color.LightGray,
+ fontSize = 12.sp,
+ )
+ Text(
+ it.getOrNull(1)?.let {
+ when (it) {
+ "sw-auto" -> "Automatic offer selection"
+ "sw-none" -> "No offer"
+ else -> "Offer: $it"
+ }
+ } ?: "",
+ color = Color.LightGray,
+ fontSize = 12.sp,
+ )
+ }
+ }
+ SWButton(modifier = Modifier, text = "Purchase", onClick = {
+ scope.launch(Dispatchers.IO) {
+ Superwall.instance.purchase(it)
+ }
+ })
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(
+ onClick = {
+ Superwall.instance.reset()
+ onLogOutClicked()
+ },
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 50.dp)
+ .padding(vertical = 8.dp),
+ ) {
+ Text(
+ text = "Log Out",
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+ }
+ }
+ }
+}
+
+fun showPaywall(
+ context: Context,
+ event: String,
+) {
+ val handler = PaywallPresentationHandler()
+ handler.onDismiss { paywallInfo ->
+ println("The paywall dismissed. PaywallInfo: $paywallInfo")
+ }
+ handler.onPresent { paywallInfo ->
+ println("The paywall presented. PaywallInfo: $paywallInfo")
+ }
+ handler.onError { error ->
+ println("The paywall presentation failed with error $error")
+ }
+ handler.onSkip { reason ->
+ when (reason) {
+ is PaywallSkippedReason.EventNotFound -> {
+ print("Paywall not shown because this event isn't part of a campaign.")
+ }
+
+ is PaywallSkippedReason.Holdout -> {
+ print(
+ "Paywall not shown because user is in a holdout group in " +
+ "Experiment: ${reason.experiment.id}",
+ )
+ }
+
+ is PaywallSkippedReason.NoRuleMatch -> {
+ print("Paywall not shown because user doesn't match any rules.")
+ }
+
+ is PaywallSkippedReason.UserIsSubscribed -> {
+ print("Paywall not shown because user is subscribed.")
+ }
+ }
+ }
+
+ Superwall.instance.register(event = "campaign_trigger", handler = handler) {
+ // code in here can be remotely configured to execute. Either
+ // (1) always after presentation or
+ // (2) only if the user pays
+ // code is always executed if no paywall is configured to show
+ val builder =
+ AlertDialog
+ .Builder(context)
+ .setTitle("Feature Launched")
+ .setMessage("The feature block was called")
+
+ builder.setPositiveButton("Ok") { _, _ -> }
+
+ val alertDialog = builder.create()
+ alertDialog.show()
+ }
+}
+
+fun Context.dialog(
+ title: String,
+ text: String,
+) {
+ val builder =
+ AlertDialog
+ .Builder(this)
+ .setTitle(title)
+ .setMessage(text)
+
+ builder.setPositiveButton("Ok") { _, _ -> }
+
+ val alertDialog = builder.create()
+ alertDialog.show()
+}
+
+@Composable
+fun SWButton(
+ modifier: Modifier = Modifier.fillMaxWidth(),
+ text: String,
+ onClick: () -> Unit,
+) {
+ Button(
+ onClick = onClick,
+ modifier =
+ modifier
+ .height(56.dp)
+ .padding(vertical = 8.dp),
+ ) {
+ Text(
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxHeight(),
+ text = text,
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+}
diff --git a/example/app/src/purchase/java/com/superwall/superapp/MainApplication.kt b/example/app/src/purchase/java/com/superwall/superapp/MainApplication.kt
new file mode 100644
index 00000000..8b9219a3
--- /dev/null
+++ b/example/app/src/purchase/java/com/superwall/superapp/MainApplication.kt
@@ -0,0 +1,14 @@
+package com.superwall.superapp
+
+import android.app.Application
+import com.superwall.sdk.Superwall
+
+class MainApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ Superwall.configure(
+ this,
+ Keys.EXAMPLE_KEY,
+ )
+ }
+}
diff --git a/example/app/src/revenuecat/README.md b/example/app/src/revenuecat/README.md
new file mode 100644
index 00000000..f6924ccd
--- /dev/null
+++ b/example/app/src/revenuecat/README.md
@@ -0,0 +1,17 @@
+## RevenueCat integration Example
+
+This application flavor serves as an introduction to integrating Superwall with Revenuecat.
+It demonstrates how to write a Superwall Purchase Controller with a RevenueCat integration.
+The flavor is the same as the default one, with the exception of passing in a `RevenueCatPurchaseController` during Superwall configuration
+
+
+## Setup Note
+To enable purchasing:
+
+- Set your Superwall API key in `MainApplication.kt`
+- Change `applicationId` in `build.gradle.kts` to match your `applicationId`
+- Ensure you are added as a [License Tester](https://developer.android.com/google/play/billing/test) to your app on Google Play Store
+- Change the trigger used to display a paywall
+
+
+
diff --git a/example/app/src/revenuecat/java/com/superwall/superapp/HomeActivity.kt b/example/app/src/revenuecat/java/com/superwall/superapp/HomeActivity.kt
new file mode 100644
index 00000000..6303b150
--- /dev/null
+++ b/example/app/src/revenuecat/java/com/superwall/superapp/HomeActivity.kt
@@ -0,0 +1,213 @@
+package com.superwall.superapp
+
+import android.app.AlertDialog
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.view.WindowCompat
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.models.entitlements.EntitlementStatus
+import com.superwall.sdk.paywall.presentation.PaywallPresentationHandler
+import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason
+import com.superwall.sdk.paywall.presentation.register
+import com.superwall.superapp.ui.theme.SuperwallExampleAppTheme
+
+class HomeActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContent {
+ val entitlementStatus by Superwall.instance.entitlements.status
+ .collectAsState()
+ SuperwallExampleAppTheme {
+ HomeScreen(
+ entitlementStatus = entitlementStatus,
+ onLogOutClicked = {
+ finish()
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun HomeScreen(
+ entitlementStatus: EntitlementStatus,
+ onLogOutClicked: () -> Unit,
+) {
+ val context = LocalContext.current
+ val subscriptionText =
+ when (entitlementStatus) {
+ is EntitlementStatus.Unknown -> "Loading subscription status."
+ is EntitlementStatus.Active ->
+ "You currently have an active subscription. Therefore, the " +
+ "paywall will never show. For the purposes of this app, delete and reinstall the " +
+ "app to clear subscriptions."
+
+ is EntitlementStatus.Inactive ->
+ "You do not have an active subscription so the paywall will " +
+ "show when clicking the button."
+ }
+
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Text(
+ text = "Presenting a Paywall",
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 50.dp),
+ )
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text =
+ "The Launch Feature button below registers an event \"campaign_trigger\".\n\n" +
+ "This event has been added to a campaign on the Superwall dashboard.\n\n" +
+ "When this event is registered, the rules in the campaign are evaluated.\n\n" +
+ "The rules match and cause a paywall to show.",
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Divider()
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = subscriptionText,
+ textAlign = TextAlign.Center,
+ )
+ }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Button(
+ onClick = {
+ val handler = PaywallPresentationHandler()
+ handler.onDismiss { paywallInfo ->
+ println("The paywall dismissed. PaywallInfo: $paywallInfo")
+ }
+ handler.onPresent { paywallInfo ->
+ println("The paywall presented. PaywallInfo: $paywallInfo")
+ }
+ handler.onError { error ->
+ println("The paywall presentation failed with error $error")
+ }
+ handler.onSkip { reason ->
+ when (reason) {
+ is PaywallSkippedReason.EventNotFound -> {
+ print("Paywall not shown because this event isn't part of a campaign.")
+ }
+ is PaywallSkippedReason.Holdout -> {
+ print(
+ "Paywall not shown because user is in a holdout group in " +
+ "Experiment: ${reason.experiment.id}",
+ )
+ }
+ is PaywallSkippedReason.NoRuleMatch -> {
+ print("Paywall not shown because user doesn't match any rules.")
+ }
+ is PaywallSkippedReason.UserIsSubscribed -> {
+ print("Paywall not shown because user is subscribed.")
+ }
+ }
+ }
+
+ Superwall.instance.register(event = "campaign_trigger", handler = handler) {
+ // code in here can be remotely configured to execute. Either
+ // (1) always after presentation or
+ // (2) only if the user pays
+ // code is always executed if no paywall is configured to show
+ val builder =
+ AlertDialog
+ .Builder(context)
+ .setTitle("Feature Launched")
+ .setMessage("The feature block was called")
+
+ builder.setPositiveButton("Ok") { _, _ -> }
+
+ val alertDialog = builder.create()
+ alertDialog.show()
+ }
+ },
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 50.dp)
+ .padding(top = 8.dp),
+ ) {
+ Text(
+ text = "Launch Feature",
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(
+ onClick = {
+ Superwall.instance.reset()
+ onLogOutClicked()
+ },
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 50.dp)
+ .padding(vertical = 8.dp),
+ ) {
+ Text(
+ text = "Log Out",
+ color = MaterialTheme.colorScheme.onPrimary,
+ style =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 18.sp,
+ ),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/example/app/src/revenuecat/java/com/superwall/superapp/MainApplication.kt b/example/app/src/revenuecat/java/com/superwall/superapp/MainApplication.kt
new file mode 100644
index 00000000..f20642d5
--- /dev/null
+++ b/example/app/src/revenuecat/java/com/superwall/superapp/MainApplication.kt
@@ -0,0 +1,15 @@
+package com.superwall.superapp
+
+import android.app.Application
+import com.superwall.sdk.Superwall
+
+class MainApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ Superwall.configure(
+ this,
+ Keys.EXAMPLE_KEY,
+ purchaseController = RevenueCatPurchaseController(this),
+ )
+ }
+}
diff --git a/example/app/src/revenuecat/java/com/superwall/superapp/RevenueCatPurchaseController.kt b/example/app/src/revenuecat/java/com/superwall/superapp/RevenueCatPurchaseController.kt
new file mode 100644
index 00000000..0692b0bf
--- /dev/null
+++ b/example/app/src/revenuecat/java/com/superwall/superapp/RevenueCatPurchaseController.kt
@@ -0,0 +1,296 @@
+package com.superwall.superapp
+
+import android.app.Activity
+import android.content.Context
+import com.android.billingclient.api.ProductDetails
+import com.revenuecat.purchases.CustomerInfo
+import com.revenuecat.purchases.LogLevel
+import com.revenuecat.purchases.ProductType
+import com.revenuecat.purchases.PurchaseParams
+import com.revenuecat.purchases.Purchases
+import com.revenuecat.purchases.PurchasesConfiguration
+import com.revenuecat.purchases.PurchasesError
+import com.revenuecat.purchases.PurchasesErrorCode
+import com.revenuecat.purchases.getCustomerInfoWith
+import com.revenuecat.purchases.interfaces.GetStoreProductsCallback
+import com.revenuecat.purchases.interfaces.PurchaseCallback
+import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
+import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
+import com.revenuecat.purchases.models.StoreProduct
+import com.revenuecat.purchases.models.StoreTransaction
+import com.revenuecat.purchases.models.SubscriptionOption
+import com.revenuecat.purchases.models.googleProduct
+import com.revenuecat.purchases.purchaseWith
+import com.superwall.sdk.Superwall
+import com.superwall.sdk.delegate.PurchaseResult
+import com.superwall.sdk.delegate.RestorationResult
+import com.superwall.sdk.delegate.subscription_controller.PurchaseController
+import com.superwall.sdk.models.entitlements.Entitlement
+import com.superwall.sdk.models.entitlements.EntitlementStatus
+import kotlinx.coroutines.CompletableDeferred
+
+suspend fun Purchases.awaitProducts(productIds: List): List {
+ val deferred = CompletableDeferred>()
+ getProducts(
+ productIds,
+ object : GetStoreProductsCallback {
+ override fun onReceived(storeProducts: List) {
+ deferred.complete(storeProducts)
+ }
+
+ override fun onError(error: PurchasesError) {
+ deferred.completeExceptionally(Exception(error.message))
+ }
+ },
+ )
+ return deferred.await()
+}
+
+interface PurchaseCompletion {
+ var storeTransaction: StoreTransaction
+ var customerInfo: CustomerInfo
+}
+
+// Create a custom exception class that wraps PurchasesError
+private class PurchasesException(
+ val purchasesError: PurchasesError,
+) : Exception(purchasesError.toString())
+
+suspend fun Purchases.awaitPurchase(
+ activity: Activity,
+ storeProduct: StoreProduct,
+): PurchaseCompletion {
+ val deferred = CompletableDeferred()
+ purchase(
+ PurchaseParams.Builder(activity, storeProduct).build(),
+ object : PurchaseCallback {
+ override fun onCompleted(
+ storeTransaction: StoreTransaction,
+ customerInfo: CustomerInfo,
+ ) {
+ deferred.complete(
+ object : PurchaseCompletion {
+ override var storeTransaction: StoreTransaction = storeTransaction
+ override var customerInfo: CustomerInfo = customerInfo
+ },
+ )
+ }
+
+ override fun onError(
+ error: PurchasesError,
+ p1: Boolean,
+ ) {
+ deferred.completeExceptionally(PurchasesException(error))
+ }
+ },
+ )
+ return deferred.await()
+}
+
+suspend fun Purchases.awaitRestoration(): CustomerInfo {
+ val deferred = CompletableDeferred()
+ restorePurchases(
+ object : ReceiveCustomerInfoCallback {
+ override fun onReceived(purchaserInfo: CustomerInfo) {
+ deferred.complete(purchaserInfo)
+ }
+
+ override fun onError(error: PurchasesError) {
+ deferred.completeExceptionally(error as Throwable)
+ }
+ },
+ )
+ return deferred.await()
+}
+
+class RevenueCatPurchaseController(
+ val context: Context,
+) : PurchaseController,
+ UpdatedCustomerInfoListener {
+ init {
+ Purchases.logLevel = LogLevel.DEBUG
+ Purchases.configure(
+ PurchasesConfiguration
+ .Builder(
+ context,
+ "goog_DCSOujJzRNnPmxdgjOwdOOjwilC",
+ ).build(),
+ )
+
+ // Make sure we get the updates
+ Purchases.sharedInstance.updatedCustomerInfoListener = this
+ }
+
+ fun syncSubscriptionStatus() {
+ // Refetch the customer info on load
+ Purchases.sharedInstance.getCustomerInfoWith {
+ if (hasAnyActiveEntitlements(it)) {
+ setEntitlementStatus(
+ EntitlementStatus.Active(
+ it.entitlements.active
+ .map {
+ Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL)
+ }.toSet(),
+ ),
+ )
+ } else {
+ setEntitlementStatus(EntitlementStatus.Inactive)
+ }
+ }
+ }
+
+ /**
+ * Callback for rc customer updated info
+ */
+ override fun onReceived(customerInfo: CustomerInfo) {
+ if (hasAnyActiveEntitlements(customerInfo)) {
+ setEntitlementStatus(
+ EntitlementStatus.Active(
+ customerInfo.entitlements.active
+ .map {
+ Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL)
+ }.toSet(),
+ ),
+ )
+ } else {
+ setEntitlementStatus(EntitlementStatus.Inactive)
+ }
+ }
+
+ /**
+ * Initiate a purchase
+ */
+ override suspend fun purchase(
+ activity: Activity,
+ productDetails: ProductDetails,
+ basePlanId: String?,
+ offerId: String?,
+ ): PurchaseResult {
+ // Find products matching productId from RevenueCat
+ val products = Purchases.sharedInstance.awaitProducts(listOf(productDetails.productId))
+ // Choose the product which matches the given base plan.
+ // If no base plan set, select first product or fail.
+ val product =
+ products.firstOrNull { it.googleProduct?.basePlanId == basePlanId }
+ ?: products.firstOrNull()
+ ?: return PurchaseResult.Failed("Product not found")
+
+ return when (product.type) {
+ ProductType.SUBS, ProductType.UNKNOWN ->
+ handleSubscription(
+ activity,
+ product,
+ basePlanId,
+ offerId,
+ )
+
+ ProductType.INAPP -> handleInAppPurchase(activity, product)
+ }
+ }
+
+ private fun buildSubscriptionOptionId(
+ basePlanId: String?,
+ offerId: String?,
+ ): String =
+ buildString {
+ basePlanId?.let { append("$it") }
+ offerId?.let { append(":$it") }
+ }
+
+ private suspend fun handleSubscription(
+ activity: Activity,
+ storeProduct: StoreProduct,
+ basePlanId: String?,
+ offerId: String?,
+ ): PurchaseResult {
+ storeProduct.subscriptionOptions?.let { subscriptionOptions ->
+ // If subscription option exists, concatenate base + offer ID.
+ val subscriptionOptionId = buildSubscriptionOptionId(basePlanId, offerId)
+
+ // Find first subscription option that matches the subscription option ID or default
+ // to letting revenuecat choose.
+ val subscriptionOption =
+ subscriptionOptions.firstOrNull { it.id == subscriptionOptionId }
+ ?: subscriptionOptions.defaultOffer
+
+ // Purchase subscription option, otherwise fail.
+ if (subscriptionOption != null) {
+ return purchaseSubscription(activity, subscriptionOption)
+ }
+ }
+ return PurchaseResult.Failed("Valid subscription option not found for product.")
+ }
+
+ private suspend fun purchaseSubscription(
+ activity: Activity,
+ subscriptionOption: SubscriptionOption,
+ ): PurchaseResult {
+ val deferred = CompletableDeferred()
+ Purchases.sharedInstance.purchaseWith(
+ PurchaseParams.Builder(activity, subscriptionOption).build(),
+ onError = { error, userCancelled ->
+ deferred.complete(
+ if (userCancelled) {
+ PurchaseResult.Cancelled()
+ } else {
+ PurchaseResult.Failed(
+ error.message,
+ )
+ },
+ )
+ },
+ onSuccess = { _, _ ->
+ deferred.complete(PurchaseResult.Purchased())
+ },
+ )
+ return deferred.await()
+ }
+
+ private suspend fun handleInAppPurchase(
+ activity: Activity,
+ storeProduct: StoreProduct,
+ ): PurchaseResult =
+ try {
+ Purchases.sharedInstance.awaitPurchase(activity, storeProduct)
+ PurchaseResult.Purchased()
+ } catch (e: PurchasesException) {
+ when (e.purchasesError.code) {
+ PurchasesErrorCode.PurchaseCancelledError -> PurchaseResult.Cancelled()
+ else ->
+ PurchaseResult.Failed(
+ e.message ?: "Purchase failed due to an unknown error",
+ )
+ }
+ }
+
+ /**
+ * Restore purchases
+ */
+ override suspend fun restorePurchases(): RestorationResult {
+ try {
+ if (hasAnyActiveEntitlements(Purchases.sharedInstance.awaitRestoration())) {
+ return RestorationResult.Restored()
+ } else {
+ return RestorationResult.Failed(Exception("No active entitlements"))
+ }
+ } catch (e: Throwable) {
+ return RestorationResult.Failed(e)
+ }
+ }
+
+ /**
+ * Check if the customer has any active entitlements
+ */
+ private fun hasAnyActiveEntitlements(customerInfo: CustomerInfo): Boolean {
+ val entitlements =
+ customerInfo.entitlements.active.values
+ .map { it.identifier }
+ return entitlements.isNotEmpty()
+ }
+
+ private fun setEntitlementStatus(entitlementStatus: EntitlementStatus) {
+ if (Superwall.initialized) {
+ Superwall.instance.setEntitlementStatus(entitlementStatus)
+ }
+ }
+}
diff --git a/superwall/src/main/java/com/superwall/sdk/network/NetworkConsts.kt b/superwall/src/main/java/com/superwall/sdk/network/NetworkConsts.kt
new file mode 100644
index 00000000..68375106
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/network/NetworkConsts.kt
@@ -0,0 +1,5 @@
+package com.superwall.sdk.network
+
+object NetworkConsts {
+ fun retryCount() = 6
+}
diff --git a/superwall/src/main/java/com/superwall/sdk/network/NetworkError.kt b/superwall/src/main/java/com/superwall/sdk/network/NetworkError.kt
index 059d1c41..98c77fcb 100644
--- a/superwall/src/main/java/com/superwall/sdk/network/NetworkError.kt
+++ b/superwall/src/main/java/com/superwall/sdk/network/NetworkError.kt
@@ -12,7 +12,7 @@ sealed class NetworkError(
class Decoding(
cause: Throwable? = null,
- ) : NetworkError("Decoding error.", cause)
+ ) : NetworkError("Decoding error ${cause?.message}", cause)
class NotFound : NetworkError("Not found.")
diff --git a/superwall/src/main/java/com/superwall/sdk/network/NetworkService.kt b/superwall/src/main/java/com/superwall/sdk/network/NetworkService.kt
index 630255c0..bd37e59f 100644
--- a/superwall/src/main/java/com/superwall/sdk/network/NetworkService.kt
+++ b/superwall/src/main/java/com/superwall/sdk/network/NetworkService.kt
@@ -22,7 +22,7 @@ abstract class NetworkService {
queryItems: List? = null,
isForDebugging: Boolean = false,
requestId: String = UUID.randomUUID().toString(),
- retryCount: Int = 6,
+ retryCount: Int = NetworkConsts.retryCount(),
): Either where T : @Serializable Any =
customHttpUrlConnection.request(
buildRequestData = {
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt
index 5f42c9d7..a3672177 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt
@@ -6,7 +6,7 @@ import com.superwall.sdk.models.paywall.PaywallIdentifier
import com.superwall.sdk.network.device.DeviceHelper
import com.superwall.sdk.paywall.view.LoadingView
import com.superwall.sdk.paywall.view.PaywallPurchaseLoadingView
-import com.superwall.sdk.paywall.view.PaywallShimmer
+import com.superwall.sdk.paywall.view.PaywallShimmerView
import com.superwall.sdk.paywall.view.PaywallView
import com.superwall.sdk.paywall.view.ShimmerView
import com.superwall.sdk.paywall.view.ViewStorage
@@ -78,9 +78,9 @@ class PaywallViewCache(
}
}
- fun acquireShimmerView(): PaywallShimmer {
+ fun acquireShimmerView(): PaywallShimmerView {
return store.retrieveView(ShimmerView.TAG)?.let {
- it as PaywallShimmer
+ it as PaywallShimmerView
} ?: run {
val view = ShimmerView(ctx)
store.storeView(ShimmerView.TAG, view)
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/builder/PaywallBuilder.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/builder/PaywallBuilder.kt
index 6c374c5e..1a2a09e4 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/builder/PaywallBuilder.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/builder/PaywallBuilder.kt
@@ -7,26 +7,26 @@ import com.superwall.sdk.paywall.presentation.get_paywall.getPaywall
import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides
import com.superwall.sdk.paywall.view.LoadingView
import com.superwall.sdk.paywall.view.PaywallPurchaseLoadingView
-import com.superwall.sdk.paywall.view.PaywallShimmer
+import com.superwall.sdk.paywall.view.PaywallShimmerView
import com.superwall.sdk.paywall.view.PaywallView
import com.superwall.sdk.paywall.view.ShimmerView
import com.superwall.sdk.paywall.view.delegate.PaywallViewCallback
import java.lang.ref.WeakReference
/**
-* Builder class for creating a PaywallView. This is useful in case you want to present a paywall yourself
-* as an Android View, allowing you to customize the paywall's appearance and behavior,
-* for example by passing in a custom Shimmer or Loading view.
+ * Builder class for creating a PaywallView. This is useful in case you want to present a paywall yourself
+ * as an Android View, allowing you to customize the paywall's appearance and behavior,
+ * for example by passing in a custom Shimmer or Loading view.
*
-* @param event The placement name for the paywall you want to display.
-* @property params the parameters to pass into the event
-* @property paywallOverrides the paywall overrides to apply to the paywall, i.e. substitutions or presentation styles
-* @property delegate the [SuperwallDelegate] to handle the paywall's callbacks
-* @property shimmmerView a view to display while the paywall is loading
-* @property purchaseLoadingView the loading view to display while the purchase/restoration is loading
-* @property activity the activity to attach the paywall to
-* @see PaywallView
-* Example:
+ * @param event The placement name for the paywall you want to display.
+ * @property params the parameters to pass into the event
+ * @property paywallOverrides the paywall overrides to apply to the paywall, i.e. substitutions or presentation styles
+ * @property delegate the [SuperwallDelegate] to handle the paywall's callbacks
+ * @property shimmmerView a view to display while the paywall is loading
+ * @property purchaseLoadingView the loading view to display while the purchase/restoration is loading
+ * @property activity the activity to attach the paywall to
+ * @see PaywallView
+ * Example:
* ```
* val paywallView = PaywallBuilder("event_name")
* .params(mapOf("key" to "value"))
@@ -36,7 +36,7 @@ import java.lang.ref.WeakReference
* .purchaseLoadingView(MyPurchaseLoadingView(context))
* .activity(activity)
* .build()
-*/
+ */
class PaywallBuilder(
val event: String,
@@ -44,7 +44,7 @@ class PaywallBuilder(
private var params: Map? = null
private var paywallOverrides: PaywallOverrides? = null
private var delegate: PaywallViewCallback? = null
- private var shimmmerView: PaywallShimmer? = null
+ private var shimmmerView: PaywallShimmerView? = null
private var purchaseLoadingView: PaywallPurchaseLoadingView? = null
private var activity: Activity? = null
@@ -64,7 +64,7 @@ class PaywallBuilder(
}
fun shimmerView(shimmerView: T): PaywallBuilder
- where T : PaywallShimmer, T : View {
+ where T : PaywallShimmerView, T : View {
this.shimmmerView = shimmerView
return this
}
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/LoadingView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/LoadingView.kt
index 30f72914..fb742f70 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/LoadingView.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/LoadingView.kt
@@ -2,57 +2,74 @@ package com.superwall.sdk.paywall.view
import android.content.Context
import android.graphics.Color
+import android.util.AttributeSet
import android.view.Gravity
+import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
+import android.widget.FrameLayout.GONE
+import android.widget.FrameLayout.VISIBLE
import android.widget.ProgressBar
import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState
-class LoadingView(
- context: Context,
-) : FrameLayout(context),
- PaywallPurchaseLoadingView {
- companion object {
- internal const val TAG = "LoadingView"
- }
+class LoadingView
+ @JvmOverloads
+ constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+ ) : FrameLayout(context),
+ PaywallPurchaseLoadingView {
+ companion object {
+ internal const val TAG = "LoadingView"
+ }
- init {
- setTag(TAG)
- setBackgroundColor(Color.TRANSPARENT)
-
- // Create a ProgressBar with the default spinner style
- val progressBar =
- ProgressBar(context).apply {
- // Ensure it's centered in the LoadingViewController
- val params =
- LayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT,
- Gravity.CENTER,
- )
- layoutParams = params
- }
-
- // Add the ProgressBar to this view
- addView(progressBar)
-
- // Set an OnTouchListener that does nothing but consume touch events
- // to prevent taps from reaching the view this loading indicator obstructs
- setOnTouchListener { _, _ -> true }
- }
+ init {
+ setTag(TAG)
+ setBackgroundColor(Color.TRANSPARENT)
+
+ // Create a ProgressBar with the default spinner style
+ val progressBar =
+ ProgressBar(context).apply {
+ // Ensure it's centered in the LoadingViewController
+ val params =
+ LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER,
+ )
+ layoutParams = params
+ }
+
+ // Add the ProgressBar to this view
+ addView(progressBar)
- override fun setupFor(
- paywallView: PaywallView,
- loadingState: PaywallLoadingState,
- ) {
- (this.parent as? ViewGroup)?.removeView(this)
- paywallView.addView(this)
- visibility =
- when (loadingState) {
- is PaywallLoadingState.LoadingPurchase, is PaywallLoadingState.ManualLoading ->
- VISIBLE
-
- else -> GONE
- }
+ // Set an OnTouchListener that does nothing but consume touch events
+ // to prevent taps from reaching the view this loading indicator obstructs
+ setOnTouchListener { _, _ -> true }
+ }
+
+ override fun showLoading() {
+ visibility = VISIBLE
+ }
+
+ override fun hideLoading() {
+ visibility = GONE
+ }
}
+
+fun T.setupFor(
+ paywallView: PaywallView,
+ loadingState: PaywallLoadingState,
+) where T : View, T : PaywallPurchaseLoadingView {
+ (this.parent as? ViewGroup)?.removeView(this)
+ paywallView.addView(this)
+ visibility =
+ when (loadingState) {
+ is PaywallLoadingState.LoadingPurchase, is PaywallLoadingState.ManualLoading ->
+ VISIBLE
+
+ else -> GONE
+ }
}
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallPurchaseLoadingView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallPurchaseLoadingView.kt
index 9f3de6bd..c5a3fc56 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallPurchaseLoadingView.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallPurchaseLoadingView.kt
@@ -1,10 +1,7 @@
package com.superwall.sdk.paywall.view
-import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState
-
interface PaywallPurchaseLoadingView {
- fun setupFor(
- paywallView: PaywallView,
- loadingState: PaywallLoadingState,
- )
+ fun showLoading()
+
+ fun hideLoading()
}
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallShimmer.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallShimmerView.kt
similarity index 95%
rename from superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallShimmer.kt
rename to superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallShimmerView.kt
index dbd1c5fb..8520fe54 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallShimmer.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallShimmerView.kt
@@ -12,7 +12,7 @@ import com.superwall.sdk.misc.isDarkColor
import com.superwall.sdk.misc.readableOverlayColor
import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState
-interface PaywallShimmer {
+interface PaywallShimmerView {
fun hideShimmer()
fun showShimmer()
@@ -23,7 +23,7 @@ interface PaywallShimmer {
fun T.setupFor(
paywallView: PaywallView,
loadingState: PaywallLoadingState,
-) where T : PaywallShimmer, T : View {
+) where T : PaywallShimmerView, T : View {
(this.parent as? ViewGroup)?.removeView(this)
if (this is ShimmerView && this.background != paywallView.backgroundColor) {
background = paywallView.backgroundColor
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 47156762..ea31c1b0 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
@@ -116,7 +116,7 @@ class PaywallView(
// / The presentation style for the paywall.
private var presentationStyle: PaywallPresentationStyle
- private var shimmerView: PaywallShimmer? = null
+ private var shimmerView: PaywallShimmerView? = null
private var loadingView: PaywallPurchaseLoadingView? = null
@@ -225,7 +225,7 @@ class PaywallView(
this.unsavedOccurrence = unsavedOccurrence
}
- internal fun setupShimmer(shimmerView: PaywallShimmer) {
+ internal fun setupShimmer(shimmerView: PaywallShimmerView) {
this.shimmerView = shimmerView
if (shimmerView is View) {
// Note: This always _is_ true, but the compiler doesn't know that
@@ -235,12 +235,14 @@ class PaywallView(
internal fun setupLoading(loadingView: PaywallPurchaseLoadingView) {
this.loadingView = loadingView
- loadingView.setupFor(this, loadingState)
+ if (loadingView is View) {
+ loadingView.setupFor(this, loadingState)
+ }
}
//region Public functions
fun setupWith(
- shimmerView: PaywallShimmer,
+ shimmerView: PaywallShimmerView,
loadingView: PaywallPurchaseLoadingView,
) {
if (webView.parent == null) {
@@ -559,7 +561,7 @@ class PaywallView(
}
loadingView?.let {
mainScope.launch {
- (it as View).visibility = View.VISIBLE
+ it.showLoading()
}
}
}
@@ -567,7 +569,7 @@ class PaywallView(
private fun hideLoadingView() {
loadingView?.let {
mainScope.launch {
- (it as View).visibility = View.GONE
+ it.hideLoading()
}
}
}
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/ShimmerView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/ShimmerView.kt
index 0d04ae3f..dd725887 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/view/ShimmerView.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/ShimmerView.kt
@@ -16,7 +16,7 @@ class ShimmerView(
context: Context,
attrs: AttributeSet? = null,
) : AppCompatImageView(context, attrs),
- PaywallShimmer {
+ PaywallShimmerView {
private var animator: ValueAnimator? = null
private var vectorDrawable: VectorDrawable? = null
diff --git a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt
index d6a01109..8039d402 100644
--- a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt
+++ b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt
@@ -89,9 +89,9 @@ internal class ErrorTracker(
"com.revenuecat.purchases",
)
) {
- it.map { if (it.isLetter()) "*" else it }
- } else {
it
+ } else {
+ it.map { if (it.isLetter()) "*" else it }
}
}.joinToString("\n")
@@ -123,7 +123,6 @@ internal fun Superwall.trackError(e: Throwable) {
try {
dependencyContainer.errorTracker.trackError(e)
} catch (e: Exception) {
- e.printStackTrace()
Logger.debug(
com.superwall.sdk.logger.LogLevel.error,
com.superwall.sdk.logger.LogScope.all,