diff --git a/.github/workflows/build+test+deploy.yml b/.github/workflows/build+test+deploy.yml
index 2512c788..4b8bd1ec 100644
--- a/.github/workflows/build+test+deploy.yml
+++ b/.github/workflows/build+test+deploy.yml
@@ -91,7 +91,15 @@ jobs:
GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
GPG_SIGNING_KEY_ID: ${{ secrets.GPG_SIGNING_KEY_ID }}
run: |
- ./gradlew publish \
+ ./gradlew :superwall:publish \
+ -Paws_access_key_id=$AWS_ACCESS_KEY_ID \
+ -Paws_secret_access_key=$AWS_SECRET_ACCESS_KEY \
+ -PsonatypeUsername=$SONATYPE_USERNAME \
+ -PsonatypePassword=$SONATYPE_PASSWORD \
+ -Pgpg_signing_key_passphrase=$GPG_SIGNING_KEY_PASSPHRASE \
+ -Pgpg_signing_key_id=$GPG_SIGNING_KEY_ID
+
+ ./gradlew :superwall-compose:publish \
-Paws_access_key_id=$AWS_ACCESS_KEY_ID \
-Paws_secret_access_key=$AWS_SECRET_ACCESS_KEY \
-PsonatypeUsername=$SONATYPE_USERNAME \
@@ -143,7 +151,7 @@ jobs:
GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
GPG_SIGNING_KEY_ID: ${{ secrets.GPG_SIGNING_KEY_ID }}
run: |
- ./gradlew publish \
+ ./gradlew :superwall:publish \
-Paws_access_key_id=$AWS_ACCESS_KEY_ID \
-Paws_secret_access_key=$AWS_SECRET_ACCESS_KEY \
-PsonatypeUsername=$SONATYPE_USERNAME \
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d406e1fb..d747376f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub.
+## 2.0.0-Alpha
+
+- Removes `PaywallComposable` and Jetpack Compose support from main SDK
+- Adds `Superwall-Compose` module for Jetpack Compose support:
+ - You can find it at `com.superwall.sdk:superwall-compose:2.0.0-alpha`
+- Adds consumer proguard rules to enable consumer minification
+- Removed methods previously marked as Deprecated
+
## 1.5.0
### Enhancements
@@ -14,8 +22,7 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw
### Fixes
-- Fixes concurrency issues with subscriptions triggered in Cordova apps
-
+- Fixes concurrency issues with subscriptions triggered in Cordova apps
## 1.5.0-beta.2
## Enhancements
diff --git a/README.md b/README.md
index 51f8f004..4e6f91b9 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ The preferred installation method is with [Gradle](https://superwall.com/docs/in
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d91aa857..5e919a92 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,7 +11,7 @@ android {
defaultConfig {
applicationId = "com.superwall.superapp"
- minSdk = 26
+ minSdk = 22
targetSdk = 34
versionCode = 2
versionName = "1.0.0"
@@ -80,6 +80,7 @@ dependencies {
// Superwall
implementation(project(":superwall"))
+ implementation(project(":superwall-compose"))
// Test
testImplementation(libs.junit)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
deleted file mode 100644
index 481bb434..00000000
--- a/app/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
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 94b1c8c5..b86f76aa 100644
--- a/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt
+++ b/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt
@@ -1,6 +1,7 @@
@file:Suppress("ktlint:standard:no-empty-file")
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import com.dropbox.dropshots.Dropshots
import com.dropbox.dropshots.ThresholdValidator
import com.example.superapp.utils.CustomComparator
@@ -32,7 +33,7 @@ class FlowScreenshotTestExecutor {
val mainScope = CoroutineScope(Dispatchers.Main)
- /*@Test
+ @Test
@FlakyTest
fun test_paywall_reappers_with_video() =
with(dropshots) {
@@ -44,12 +45,13 @@ class FlowScreenshotTestExecutor {
delayFor(500.milliseconds)
}
step("second_paywall") {
+ it.waitFor { it is SuperwallEvent.PaywallOpen }
awaitUntilWebviewAppears()
delayFor(1.seconds)
}
}
}
-*/
+
@Test
fun test_paywall_presents_regardless_of_subscription() =
with(dropshots) {
@@ -65,7 +67,7 @@ class FlowScreenshotTestExecutor {
Superwall.instance.paywallView
?.webView
?.scrollBy(0, 300) ?: kotlin.run {
- throw IllegalStateException("No viewcontroller found")
+ throw IllegalStateException("No view found")
}
}.await()
// We delay a bit to ensure the button is visible
@@ -76,7 +78,7 @@ class FlowScreenshotTestExecutor {
Superwall.instance.paywallView
?.webView
?.scrollTo(0, 0) ?: kotlin.run {
- throw IllegalStateException("No viewcontroller found")
+ throw IllegalStateException("No view found")
}
}.await()
// We delay a bit to ensure scroll has finished
@@ -123,16 +125,21 @@ class FlowScreenshotTestExecutor {
}
}
}
+}
+
+ /*
+
+ 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.PaywallWebviewLoadComplete }
+ it.waitFor { it is SuperwallEvent.ShimmerViewComplete }
awaitUntilShimmerDisappears()
awaitUntilWebviewAppears()
- delayFor(100.milliseconds)
+ delayFor(300.milliseconds)
mainScope
.async {
// We scroll a bit to display the button
@@ -146,7 +153,7 @@ class FlowScreenshotTestExecutor {
}
}.await()
// We delay a bit to ensure the button is visible
- delayFor(100.milliseconds)
+ delayFor(300.milliseconds)
// We scroll back to the top
mainScope
.async {
@@ -159,10 +166,7 @@ class FlowScreenshotTestExecutor {
// We delay a bit to ensure scroll has finished
delayFor(500.milliseconds)
}
-
- step {
- delayFor(10.seconds)
- }
}
}
}
+ */
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 ebc3adb9..74286b1b 100644
--- a/app/src/androidTest/java/com/example/superapp/test/PresentationRuleTests.kt
+++ b/app/src/androidTest/java/com/example/superapp/test/PresentationRuleTests.kt
@@ -73,7 +73,7 @@ class PresentationRuleTests {
with(dropshots) {
screenshotFlow(UITestHandler.test32Info) {
step("") {
- it.waitFor { it is SuperwallEvent.SubscriptionStatusDidChange }
+ it.waitFor { it is SuperwallEvent.EntitlementStatusDidChange }
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 afa32b74..bdda61fe 100644
--- a/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt
+++ b/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt
@@ -14,7 +14,6 @@ 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.delegate.SubscriptionStatus
import com.superwall.superapp.test.UITestHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -144,19 +143,21 @@ class SimpleScreenshotTestExecutor {
it.waitFor { it is SuperwallEvent.PaywallPresentationRequest }
}
}
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.INACTIVE)
}
- @Test
- fun test_paywall_doesnt_present_without_showing_alert_after_dismiss() =
- with(dropshots) {
- screenshotFlow(UITestHandler.test26Info) {
- step("") {
- it.waitFor { it is SuperwallEvent.PaywallPresentationRequest }
- awaitUntilDialogAppears()
- }
- }
- }
+ /*
+ * Commented out temporarily since Firebase remote lab connection is broken and recording tests is not possible
+ * @Test
+ * fun test_paywall_presents_without_showing_alert_after_dismiss() =
+ * with(dropshots) {
+ * screenshotFlow(UITestHandler.test26Info) {
+ * step("") {
+ * it.waitFor { it is SuperwallEvent.PaywallPresentationRequest }
+ * awaitUntilDialogAppears()
+ * }
+ * }
+ * }
+ * */
@Test
fun test_paywall_doesnt_present_calls_feature_block() =
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 016aa2b2..65b6c672 100644
--- a/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt
+++ b/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt
@@ -17,7 +17,8 @@ import androidx.test.uiautomator.Until
import com.dropbox.dropshots.Dropshots
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.superwall.SuperwallEvent
-import com.superwall.sdk.paywall.vc.ShimmerView
+import com.superwall.sdk.config.models.ConfigurationStatus
+import com.superwall.sdk.paywall.view.ShimmerView
import com.superwall.superapp.MainActivity
import com.superwall.superapp.test.UITestInfo
import kotlinx.coroutines.CoroutineScope
@@ -112,6 +113,7 @@ fun Dropshots.screenshotFlow(
}
runTest(timeout = 5.minutes) {
+ Superwall.instance.configurationStateListener.first { it is ConfigurationStatus.Configured }
try {
flow.steps.forEach {
if (!testReady.value) {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9151da8c..68dcff70 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -51,7 +51,7 @@
diff --git a/app/src/main/java/com/superwall/superapp/ComposeActivity.kt b/app/src/main/java/com/superwall/superapp/ComposeActivity.kt
index cd22b18c..2185f751 100644
--- a/app/src/main/java/com/superwall/superapp/ComposeActivity.kt
+++ b/app/src/main/java/com/superwall/superapp/ComposeActivity.kt
@@ -25,11 +25,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
-import com.superwall.sdk.composable.PaywallComposable
+import com.superwall.sdk.compose.PaywallComposable
import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides
import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult
-import com.superwall.sdk.paywall.vc.PaywallView
-import com.superwall.sdk.paywall.vc.delegate.PaywallViewCallback
+import com.superwall.sdk.paywall.view.PaywallView
+import com.superwall.sdk.paywall.view.delegate.PaywallViewCallback
import com.superwall.superapp.ui.theme.MyApplicationTheme
class ComposeActivity :
diff --git a/app/src/main/java/com/superwall/superapp/MainApplication.kt b/app/src/main/java/com/superwall/superapp/MainApplication.kt
index f5e431b6..d30e853e 100644
--- a/app/src/main/java/com/superwall/superapp/MainApplication.kt
+++ b/app/src/main/java/com/superwall/superapp/MainApplication.kt
@@ -13,6 +13,7 @@ import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.paywall.presentation.register
+import com.superwall.superapp.purchase.RevenueCatPurchaseController
import kotlinx.coroutines.flow.MutableSharedFlow
import java.lang.ref.WeakReference
@@ -62,7 +63,7 @@ class MainApplication :
.build(),
)
- configureWithAutomaticInitialization()
+ configureWithObserverMode()
// configureWithRevenueCatInitialization()
}
@@ -86,6 +87,25 @@ class MainApplication :
// Superwall.instance.options.isGameControllerEnabled = true
}
+ fun configureWithObserverMode() {
+ Superwall.configure(
+ this@MainApplication,
+ CONSTANT_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ shouldObservePurchases = true
+ paywalls =
+ PaywallOptions().apply {
+ shouldPreload = false
+ }
+ },
+ )
+ Superwall.instance.delegate = this@MainApplication
+
+ // Make sure we enable the game controller
+ // Superwall.instance.options.isGameControllerEnabled = true
+ }
+
fun configureWithRevenueCatInitialization() {
val purchaseController = RevenueCatPurchaseController(this)
diff --git a/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt b/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt
index df91cc49..b31053b9 100644
--- a/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt
+++ b/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt
@@ -1,4 +1,4 @@
-package com.superwall.superapp
+package com.superwall.superapp.purchase
import android.app.Activity
import android.content.Context
@@ -24,8 +24,9 @@ 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.SubscriptionStatus
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
// Extension function to convert callback to suspend function
@@ -126,9 +127,16 @@ class RevenueCatPurchaseController(
// Refetch the customer info on load
Purchases.sharedInstance.getCustomerInfoWith {
if (hasAnyActiveEntitlements(it)) {
- setSubscriptionStatus(SubscriptionStatus.ACTIVE)
+ setEntitlementStatus(
+ EntitlementStatus.Active(
+ it.entitlements.active
+ .map {
+ Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL)
+ }.toSet(),
+ ),
+ )
} else {
- setSubscriptionStatus(SubscriptionStatus.INACTIVE)
+ setEntitlementStatus(EntitlementStatus.Inactive)
}
}
}
@@ -138,9 +146,16 @@ class RevenueCatPurchaseController(
*/
override fun onReceived(customerInfo: CustomerInfo) {
if (hasAnyActiveEntitlements(customerInfo)) {
- setSubscriptionStatus(SubscriptionStatus.ACTIVE)
+ setEntitlementStatus(
+ EntitlementStatus.Active(
+ customerInfo.entitlements.active
+ .map {
+ Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL)
+ }.toSet(),
+ ),
+ )
} else {
- setSubscriptionStatus(SubscriptionStatus.INACTIVE)
+ setEntitlementStatus(EntitlementStatus.Inactive)
}
}
@@ -275,9 +290,9 @@ class RevenueCatPurchaseController(
return entitlements.isNotEmpty()
}
- private fun setSubscriptionStatus(subscriptionStatus: SubscriptionStatus) {
+ private fun setEntitlementStatus(entitlementStatus: EntitlementStatus) {
if (Superwall.initialized) {
- Superwall.instance.setSubscriptionStatus(subscriptionStatus)
+ Superwall.instance.setEntitlementStatus(entitlementStatus)
}
}
}
diff --git a/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.kt b/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.kt
new file mode 100644
index 00000000..8a6c4a83
--- /dev/null
+++ b/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.kt
@@ -0,0 +1,105 @@
+package com.superwall.superapp.test
+
+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")
+ .setProductId(id)
+ .setQuantity(1)
+ .setPurchaseToken("opaque-token-up-to-1950-characters")
+ .setPackageName("com.example.app")
+ .setDeveloperPayload("")
+ .setAcknowledged(true)
+ .setAutoRenewing(true)
+ .build()
+ }
+}
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 3b723491..fafa3dd7 100644
--- a/app/src/main/java/com/superwall/superapp/test/UITestActivity.kt
+++ b/app/src/main/java/com/superwall/superapp/test/UITestActivity.kt
@@ -35,6 +35,7 @@ import com.superwall.superapp.test.UITestHandler.tests
import com.superwall.superapp.ui.theme.MyApplicationTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull
@@ -52,6 +53,7 @@ class UITestInfo(
val test: suspend Context.() -> Unit = {
val scope = CoroutineScope(Dispatchers.IO)
+ delay(100)
Superwall.instance.delegate =
object : SuperwallDelegate {
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
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 5dafb43a..80fe7165 100644
--- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt
+++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt
@@ -7,17 +7,18 @@ import android.util.Log
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.superwall.SuperwallEvent
import com.superwall.sdk.analytics.superwall.SuperwallEvent.DeepLink
-import com.superwall.sdk.delegate.SubscriptionStatus
import com.superwall.sdk.identity.identify
import com.superwall.sdk.identity.setUserAttributes
import com.superwall.sdk.misc.AlertControllerFactory
+import com.superwall.sdk.models.entitlements.Entitlement
+import com.superwall.sdk.models.entitlements.EntitlementStatus
import com.superwall.sdk.paywall.presentation.PaywallPresentationHandler
import com.superwall.sdk.paywall.presentation.dismiss
import com.superwall.sdk.paywall.presentation.get_paywall.getPaywall
import com.superwall.sdk.paywall.presentation.get_presentation_result.getPresentationResult
import com.superwall.sdk.paywall.presentation.register
import com.superwall.sdk.paywall.presentation.result.PresentationResult
-import com.superwall.sdk.paywall.vc.SuperwallPaywallActivity
+import com.superwall.sdk.paywall.view.SuperwallPaywallActivity
import com.superwall.sdk.view.fatalAssert
import com.superwall.superapp.ComposeActivity
import kotlinx.coroutines.CoroutineScope
@@ -178,9 +179,9 @@ object UITestHandler {
"Sets subs status to active, paywall should present regardless of this," +
" then it sets the status back to inactive.",
test = { scope, events ->
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test"))))
Superwall.instance.register(event = "present_always")
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.INACTIVE)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
},
)
var test10Info =
@@ -202,20 +203,22 @@ object UITestHandler {
"8 seconds and present again without any name. Then it should present again" +
" with the name Sawyer.",
test = { scope, events ->
-
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
Superwall.instance.setUserAttributes(mapOf("first_name" to "Claire"))
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
Superwall.instance.register(event = "present_data")
- events.first { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ events.first { it is SuperwallEvent.ShimmerViewComplete }
// Dismiss any view controllers
- delay(8.seconds)
+ delay(4.seconds)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
Superwall.instance.setUserAttributes(mapOf("first_name" to null))
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
Superwall.instance.register(event = "present_data")
events.first { it is SuperwallEvent.PaywallOpen }
delay(10.seconds)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
Superwall.instance.setUserAttributes(mapOf("first_name" to "Sawyer"))
Superwall.instance.register(event = "present_data")
@@ -245,11 +248,11 @@ object UITestHandler {
test = { scope, events ->
// Show a paywall
Superwall.instance.register(event = "present_always")
- events.first { it is SuperwallEvent.PaywallWebviewLoadComplete }
+ events.first { it is SuperwallEvent.ShimmerViewComplete }
delay(8000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
},
)
@@ -271,7 +274,7 @@ object UITestHandler {
delay(5000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
Superwall.instance.register(event = "present_always")
@@ -280,7 +283,7 @@ object UITestHandler {
delay(5000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
var handler = PaywallPresentationHandler()
@@ -333,7 +336,7 @@ object UITestHandler {
delay(8000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
// Set identity
@@ -347,7 +350,7 @@ object UITestHandler {
delay(8000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
// Present paywall
@@ -369,14 +372,14 @@ object UITestHandler {
// Create a mock paywall view
val delegate = MockPaywallViewDelegate()
- // Get the paywall view controller instance
- val viewController =
+ // Get the paywall view instance
+ val view =
Superwall.instance.getPaywall(event = "present_urls", delegate = delegate)
// Present using the convenience `SuperwallPaywallActivity` activity and verify test case.
SuperwallPaywallActivity.startWithView(
context = this@UITestInfo,
- view = viewController.getOrThrow(),
+ view = view.getOrThrow(),
)
},
)
@@ -397,14 +400,14 @@ object UITestHandler {
delay(8000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
Superwall.instance.getPresentationResult(event = "present_and_rule_user")
delay(8000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
// Show a paywall
@@ -412,7 +415,7 @@ object UITestHandler {
delay(8000)
- // Dismiss any view controllers
+ // Dismiss any views
Superwall.instance.dismiss()
// Set identity
@@ -472,15 +475,13 @@ object UITestHandler {
"4s later.",
test = { scope, events ->
// Set user as subscribed
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE)
-
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("pro"))))
// Register event - paywall shouldn't appear.
Superwall.instance.register(event = "register_nongated_paywall")
scope.launch {
-
- events.first { it is SuperwallEvent.SubscriptionStatusDidChange }
+ events.first { it is SuperwallEvent.EntitlementStatusDidChange }
delay(4000)
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.INACTIVE)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
}
},
)
@@ -490,16 +491,12 @@ object UITestHandler {
"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 ->
- var currentSubscriptionStatus = Superwall.instance.subscriptionStatus.value
-
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE)
-
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("pro"))))
// Try to present paywall again
Superwall.instance.register(event = "register_nongated_paywall")
scope.launch {
delay(4000)
- Superwall.instance.setSubscriptionStatus(currentSubscriptionStatus)
}
},
)
@@ -509,7 +506,8 @@ object UITestHandler {
"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 ->
- Superwall.instance.register(event = "register_gated_paywalls") {
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
+ Superwall.instance.register(event = "register_gated_paywall") {
val alertController =
AlertControllerFactory.make(
context = this,
@@ -527,10 +525,7 @@ object UITestHandler {
"Tapping the button shouldn't present the paywall but should launch the " +
"feature block - an alert should present.",
test = { scope, events ->
- var currentSubscriptionStatus = Superwall.instance.subscriptionStatus.value
-
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE)
-
+ Superwall.instance.setEntitlementStatus("pro")
Superwall.instance.register(event = "register_gated_paywall") {
val alertController =
AlertControllerFactory.make(
@@ -541,8 +536,8 @@ object UITestHandler {
)
alertController.show()
}
- delay(8000)
- Superwall.instance.setSubscriptionStatus(currentSubscriptionStatus)
+ delay(1000)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
},
)
var test28Info =
@@ -550,9 +545,8 @@ object UITestHandler {
28,
"Should print out \"Paywall(experiment...)\".",
test = { scope, events ->
-
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
scope.launch {
-
val result = Superwall.instance.getPresentationResult("present_data")
val resOrNull = result.getOrNull()
fatalAssert(
@@ -622,15 +616,15 @@ object UITestHandler {
"This sets the subscription status active, prints out \"userIsSubscribed\" " +
"and then returns subscription status to inactive.",
test = { scope, events ->
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test"))))
scope.launch {
val result = Superwall.instance.getPresentationResult("present_data")
fatalAssert(
- result.getOrNull() is PresentationResult.UserIsSubscribed,
+ result.getOrNull() is PresentationResult.NoRuleMatch,
"UserIsSubscribed expected, received $result",
)
println("!!! TEST 32 !!! $result")
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.INACTIVE)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
}
},
)
@@ -668,13 +662,13 @@ object UITestHandler {
"the result type `purchased` is printed to the console. The paywall should dismiss." +
" After doing this, try test 37",
test = { scope, events ->
- // Create a mock paywall view controller
+ // Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
println("!!! TEST 35 !!! Result: $paywallResult, shouldDismiss: $shouldDismiss, paywallVc: $paywallView")
}
- // Get the paywall view controller instance
+ // Get the paywall view instance
val view =
Superwall.instance.getPaywall(event = "present_data", delegate = delegate)
@@ -688,18 +682,21 @@ object UITestHandler {
"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 ->
- // Create a mock paywall view controller
+ // Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
println("!!! TEST 36 !!! Result: $paywallResult, shouldDismiss: $shouldDismiss, paywallVc: $paywallView")
}
- // Get the paywall view controller instance
- val viewController =
+ // Get the paywall view instance
+ val view =
Superwall.instance.getPaywall(event = "present_data", delegate = delegate)
// Present using the convenience `SuperwallPaywallActivity` activity and verify test case.
- SuperwallPaywallActivity.startWithView(context = this, view = viewController.getOrThrow())
+ SuperwallPaywallActivity.startWithView(
+ context = this,
+ view = view.getOrThrow(),
+ )
},
)
var test37Info =
@@ -709,18 +706,18 @@ object UITestHandler {
"paywall and tap \"restore\". The paywall should dismiss and the the console should" +
"print the paywallResult as \"restored\".",
test = { scope, events ->
- // Create a mock paywall view controller
+ // Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
println("!!! TEST 37 !!! Result: $paywallResult, shouldDismiss: $shouldDismiss, paywallVc: $paywallView")
}
- // Get the paywall view controller instance
- val viewController =
+ // Get the paywall view instance
+ val view =
Superwall.instance.getPaywall(event = "restore", delegate = delegate)
// Present using the convenience `SuperwallPaywallActivity` activity and verify test case.
- SuperwallPaywallActivity.startWithView(context = this, view = viewController.getOrThrow())
+ SuperwallPaywallActivity.startWithView(context = this, view = view.getOrThrow())
},
)
@@ -731,11 +728,11 @@ object UITestHandler {
subscribed: Boolean,
gated: Boolean,
) {
- val currentSubscriptionStatus = Superwall.instance.subscriptionStatus.value
+ val currentSubscriptionStatus = Superwall.instance.entitlements.status.value
if (subscribed) {
// Set user subscribed
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test"))))
}
// Determine gating event
@@ -766,7 +763,7 @@ object UITestHandler {
if (subscribed) {
// Reset status
- Superwall.instance.setSubscriptionStatus(currentSubscriptionStatus)
+ Superwall.instance.setEntitlementStatus(currentSubscriptionStatus)
}
}
@@ -1042,18 +1039,18 @@ object UITestHandler {
"Don't have an active subscription, present paywall, tap restore. Check " +
"the \"No Subscription Found\" alert pops up.",
test = { scope, events ->
- // Create a mock paywall view controller
+ // Create a mock paywall view
val delegate = MockPaywallViewDelegate()
delegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
println("!!! TEST 37 !!! Result: $paywallResult, shouldDismiss: $shouldDismiss, paywallVc: $paywallView")
}
- // Get the paywall view controller instance
- val viewController =
+ // Get the paywall view instance
+ val view =
Superwall.instance.getPaywall(event = "restore", delegate = delegate)
// Present using the convenience `SuperwallPaywallActivity` activity and verify test case.
- SuperwallPaywallActivity.startWithView(context = this, view = viewController.getOrThrow())
+ SuperwallPaywallActivity.startWithView(context = this, view = view.getOrThrow())
},
)
var test64Info =
@@ -1256,7 +1253,7 @@ object UITestHandler {
}
}
- // Create a mock paywall view controller
+ // Create a mock paywall view
val paywallDelegate = MockPaywallViewDelegate()
paywallDelegate.paywallViewFinished { paywallView, paywallResult, shouldDismiss ->
println("!!! TEST 70 !!! Result: $paywallResult, shouldDismiss: $shouldDismiss, paywallVc: $paywallView")
@@ -1267,15 +1264,15 @@ object UITestHandler {
}
}
- // Get the paywall view controller instance
- val viewController =
+ // Get the paywall view instance
+ val view =
Superwall.instance.getPaywall(
event = "show_survey_with_other",
delegate = paywallDelegate,
)
// Present using the convenience `SuperwallPaywallActivity` activity and verify test case.
- SuperwallPaywallActivity.startWithView(context = this, view = viewController.getOrThrow())
+ SuperwallPaywallActivity.startWithView(context = this, view = view.getOrThrow())
},
)
var test71Info =
@@ -1352,7 +1349,7 @@ object UITestHandler {
"show. Tap the close button. The paywall will close and the console will print " +
"\"!!! TEST 74 !!! SurveyClose\".",
test = { scope, events ->
- Superwall.instance.setSubscriptionStatus(SubscriptionStatus.INACTIVE)
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
// Create a mock Superwall delegate
val delegate = MockSuperwallDelegate()
@@ -1532,6 +1529,77 @@ object UITestHandler {
},
)
+ var testAndroid100Info =
+ UITestInfo(
+ 100,
+ "Entitlements test: Tap launch button. Paywall should display when user has no entitlements.",
+ testCaseType = TestCaseType.Android,
+ test = { scope, events ->
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive)
+ Superwall.instance.register(event = "entitlements_test_basic")
+ },
+ )
+
+ var testAndroid101Info =
+ UITestInfo(
+ 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 ->
+ Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("basic"))))
+ Superwall.instance.register(event = "entitlements_test_basic") {
+ val alertController =
+ AlertControllerFactory.make(
+ context = this,
+ title = "Feature Launched",
+ message = "The feature block was called",
+ actionTitle = "Ok",
+ )
+ alertController.show()
+ }
+ },
+ )
+
+ var testAndroid102Info =
+ UITestInfo(
+ 102,
+ "Entitlements test: Tap launch button. Paywall should display when user has no `pro` entitlements.",
+ testCaseType = TestCaseType.Android,
+ test = { scope, events ->
+ Superwall.instance.setEntitlementStatus("basic")
+ Superwall.instance.register(event = "entitlements_test_pro") {
+ val alertController =
+ AlertControllerFactory.make(
+ context = this,
+ title = "Feature Launched",
+ message = "The feature block was called",
+ actionTitle = "Ok",
+ )
+ alertController.show()
+ }
+ },
+ )
+
+ var testAndroid103Info =
+ UITestInfo(
+ 103,
+ "Entitlements test: Tap launch button. Paywall should not display when user has `pro` entitlements. Dialog should show.",
+ testCaseType = TestCaseType.Android,
+ test = { scope, events ->
+ Superwall.instance.setEntitlementStatus("pro")
+ Superwall.instance.register(event = "entitlements_test_pro") {
+ val alertController =
+ AlertControllerFactory.make(
+ context = this,
+ title = "Feature Launched",
+ message = "The feature block was called",
+ actionTitle = "Ok",
+ )
+ alertController.show()
+ }
+ },
+ )
+
val tests =
listOf(
UITestHandler.test0Info,
@@ -1609,5 +1677,9 @@ object UITestHandler {
UITestHandler.testAndroid21Info,
UITestHandler.testAndroid22Info,
UITestHandler.testAndroid23Info,
+ UITestHandler.testAndroid100Info,
+ UITestHandler.testAndroid101Info,
+ UITestHandler.testAndroid102Info,
+ UITestHandler.testAndroid103Info,
)
}
diff --git a/app/src/main/java/com/superwall/superapp/test/UITestMocks.kt b/app/src/main/java/com/superwall/superapp/test/UITestMocks.kt
index a25356aa..cbd39d1f 100644
--- a/app/src/main/java/com/superwall/superapp/test/UITestMocks.kt
+++ b/app/src/main/java/com/superwall/superapp/test/UITestMocks.kt
@@ -3,21 +3,12 @@ package com.superwall.superapp.test
import com.superwall.sdk.analytics.superwall.SuperwallEventInfo
import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult
-import com.superwall.sdk.paywall.vc.PaywallView
-import com.superwall.sdk.paywall.vc.delegate.PaywallViewCallback
-
-@Deprecated("Will be removed in the upcoming versions, use MockPaywallViewDelegate instead")
-typealias MockPaywallViewControllerDelegate = MockPaywallViewDelegate
+import com.superwall.sdk.paywall.view.PaywallView
+import com.superwall.sdk.paywall.view.delegate.PaywallViewCallback
class MockPaywallViewDelegate : PaywallViewCallback {
private var paywallViewFinished: ((PaywallView, PaywallResult, Boolean) -> Unit)? = null
- override fun didFinish(
- paywall: PaywallView,
- result: PaywallResult,
- shouldDismiss: Boolean,
- ) = onFinished(paywall, result, shouldDismiss)
-
override fun onFinished(
paywall: PaywallView,
result: PaywallResult,
@@ -29,11 +20,6 @@ class MockPaywallViewDelegate : PaywallViewCallback {
}
}
- @Deprecated("Will be removed in the upcoming versions, use paywallViewFinished instead")
- fun paywallViewControllerDidFinish(handler: (PaywallView, PaywallResult, Boolean) -> Unit) {
- paywallViewFinished(handler)
- }
-
fun paywallViewFinished(handler: (PaywallView, PaywallResult, Boolean) -> Unit) {
paywallViewFinished = handler
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 6628d878..1bda8ebf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.kotlinAndroid) apply false
alias(libs.plugins.serialization) apply false
+ alias(libs.plugins.androidLibrary) apply false
}
true
diff --git a/consumer-rules.pro b/consumer-rules.pro
new file mode 100644
index 00000000..5b17c7b4
--- /dev/null
+++ b/consumer-rules.pro
@@ -0,0 +1,64 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-keep class com.superwall.** { *; }
+-keep class androidx.lifecycle.DefaultLifecycleObserver
+-keep class com.google.gson.reflect.TypeToken
+-keep class * extends com.google.gson.reflect.TypeToken
+-keep public class * implements java.lang.reflect.Type
+
+# Keep `Companion` object fields of serializable classes.
+# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
+-if @kotlinx.serialization.Serializable class **
+-keepclassmembers class <1> {
+ static <1>$Companion Companion;
+}
+
+# Keep `serializer()` on companion objects (both default and named) of serializable classes.
+-if @kotlinx.serialization.Serializable class ** {
+ static **$* *;
+}
+-keepclassmembers class <2>$<3> {
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# Keep `INSTANCE.serializer()` of serializable objects.
+-if @kotlinx.serialization.Serializable class ** {
+ public static ** INSTANCE;
+}
+-keepclassmembers class <1> {
+ public static <1> INSTANCE;
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+
+# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes
+# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900
+-dontnote kotlinx.serialization.**
+
+# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
+# If kotlinx-serialization-cbor is in the classpath, it gets picked as a serialization strategy.
+# Don't warn about these two things.
+-dontwarn java.lang.ClassValue
+-dontwarn org.jetbrains.annotations.ReadOnly
diff --git a/example/app/build.gradle.kts b/example/app/build.gradle.kts
index 13f7735c..42a3d876 100644
--- a/example/app/build.gradle.kts
+++ b/example/app/build.gradle.kts
@@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId = "com.superwall.superapp"
- minSdk = 26
+ minSdk = 22
targetSdk = 34
versionCode = 1
versionName = "1.0.0"
diff --git a/example/app/src/main/AndroidManifest.xml b/example/app/src/main/AndroidManifest.xml
index 8f00d143..a5ea957e 100644
--- a/example/app/src/main/AndroidManifest.xml
+++ b/example/app/src/main/AndroidManifest.xml
@@ -32,7 +32,7 @@
android:label="Home Activity"
android:theme="@style/Theme.SuperwallExampleApp" />
diff --git a/example/app/src/main/java/com/superwall/exampleapp/HomeActivity.kt b/example/app/src/main/java/com/superwall/exampleapp/HomeActivity.kt
index 864f8b2e..b1b431ef 100644
--- a/example/app/src/main/java/com/superwall/exampleapp/HomeActivity.kt
+++ b/example/app/src/main/java/com/superwall/exampleapp/HomeActivity.kt
@@ -46,7 +46,7 @@ class HomeActivity : ComponentActivity() {
val subscriptionStatus by Superwall.instance.subscriptionStatus.collectAsState()
SuperwallExampleAppTheme {
HomeScreen(
- subscriptionStatus = subscriptionStatus,
+ entitlementStatus = subscriptionStatus,
onLogOutClicked = {
finish()
},
@@ -58,12 +58,12 @@ class HomeActivity : ComponentActivity() {
@Composable
fun HomeScreen(
- subscriptionStatus: SubscriptionStatus,
+ entitlementStatus: SubscriptionStatus,
onLogOutClicked: () -> Unit,
) {
val context = LocalContext.current
val subscriptionText =
- when (subscriptionStatus) {
+ when (entitlementStatus) {
SubscriptionStatus.UNKNOWN -> "Loading subscription status."
SubscriptionStatus.ACTIVE ->
"You currently have an active subscription. Therefore, the " +
diff --git a/gradle.properties b/gradle.properties
index b9720b96..04c1b241 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,4 +21,7 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
-android.nonFinalResIds=false
\ No newline at end of file
+android.nonFinalResIds=false
+android.enableR8.debugMode=true
+android.enableR8.fullMode=true
+android.debug.obsoleteApi=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 28f1ae45..dd245c86 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,7 +2,6 @@
billing_version = "6.1.0"
browser_version = "1.5.0"
gradle_plugin_version = "8.4.1"
-javascriptengineVersion = "1.0.0-beta01"
jna_version = "5.14.0@aar"
kotlinxCoroutinesGuavaVersion = "1.8.1"
leakcanaryAndroidVersion = "2.14"
@@ -21,7 +20,7 @@ lifecycle_runtime_ktx_version = "2.8.1"
junit_version = "4.13.2"
kotlinx_coroutines_test_version = "1.8.1"
room_runtime_version = "2.6.1"
-supercel_version = "0.1.16"
+supercel_version = "0.1.17"
test_ext_junit_version = "1.2.1"
espresso_core_version = "3.6.1"
test_runner_version = "1.6.1"
@@ -39,8 +38,6 @@ dropshot_version = "0.4.2"
# SQL
-javascriptengine = { module = "androidx.javascriptengine:javascriptengine", version.ref = "javascriptengineVersion" }
-jna = { module = "net.java.dev.jna:jna", version.ref = "jna_version" }
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroidVersion" }
orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestratorVersion" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room_runtime_version" }
@@ -106,3 +103,4 @@ androidApplication = { id = "com.android.application", version.ref = "gradle_plu
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization_version" }
dropshot = { id = "com.dropbox.dropshots", version.ref = "dropshot_version" }
+androidLibrary = { id = "com.android.library", version.ref = "gradle_plugin_version" }
\ No newline at end of file
diff --git a/superwall/proguard-rules.pro b/proguard-rules.pro
similarity index 100%
rename from superwall/proguard-rules.pro
rename to proguard-rules.pro
diff --git a/settings.gradle b/settings.gradle
index 41b07a90..4df8d6cb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,3 +17,4 @@ include ':app'
include ':superwall'
include 'example:app'
project(':example:app').projectDir = new File('example/app')
+include ':superwall-compose'
diff --git a/superwall-compose/.gitignore b/superwall-compose/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/superwall-compose/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/superwall-compose/build.gradle.kts b/superwall-compose/build.gradle.kts
new file mode 100644
index 00000000..c371146b
--- /dev/null
+++ b/superwall-compose/build.gradle.kts
@@ -0,0 +1,169 @@
+import groovy.json.JsonBuilder
+import java.io.ByteArrayOutputStream
+import java.text.SimpleDateFormat
+import java.util.Date
+
+plugins {
+ alias(libs.plugins.androidLibrary)
+ alias(libs.plugins.kotlinAndroid)
+ id("maven-publish")
+ id("signing")
+}
+
+version = "1.2.4"
+
+android {
+ namespace = "com.superwall.sdk.composable"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 22
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("../consumer-rules.pro")
+
+ val gitSha =
+ project
+ .exec {
+ commandLine("git", "rev-parse", "--short", "HEAD")
+ standardOutput = ByteArrayOutputStream()
+ }.toString()
+ .trim()
+ buildConfigField("String", "GIT_SHA", "\"${gitSha}\"")
+
+ val currentTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date())
+ buildConfigField("String", "BUILD_TIME", "\"${currentTime}\"")
+
+ buildConfigField("String", "SDK_VERSION", "\"${version}\"")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "../proguard-rules.pro",
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.0"
+ }
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ }
+ }
+}
+
+publishing {
+ publications {
+ register("release") {
+ groupId = "com.superwall.sdk"
+ artifactId = "superwall-compose"
+ version = version
+
+ pom {
+ name.set("Superwall Compose")
+ description.set("Remotely configure paywalls without shipping app updates - Jetpack Compose support")
+ url.set("https://superwall.com")
+
+ licenses {
+ license {
+ name.set("MIT License")
+ url.set("https://github.com/superwall/Superwall-Android?tab=MIT-1-ov-file#")
+ }
+ }
+ developers {
+ developer {
+ id.set("ianrumac")
+ name.set("Ian Rumac")
+ email.set("ian@superwall.com")
+ }
+ }
+ scm {
+ connection.set("scm:git:git@github.com:superwall/Superwall-Android.git")
+ developerConnection.set("scm:git:ssh://github.com:superwall/Superwall-Android.git")
+ url.set("scm:git:https://github.com/superwall/Superwall-Android.git")
+ }
+ }
+
+ afterEvaluate {
+ from(components["release"])
+ }
+ }
+ }
+
+ repositories {
+ mavenLocal()
+
+ // Allow us to publish to S3 if we have the credentials
+ // but also allow us to publish locally if we don't
+ val awsAccessKeyId: String? by extra
+ val awsSecretAccessKey: String? by extra
+ val sonatypeUsername: String? by extra
+ val sonatypePassword: String? by extra
+ if (awsAccessKeyId != null && awsSecretAccessKey != null) {
+ maven {
+ url = uri("s3://mvn.superwall.com/release")
+ credentials(AwsCredentials::class.java) {
+ accessKey = awsAccessKeyId
+ secretKey = awsSecretAccessKey
+ }
+ }
+ }
+
+ if (sonatypeUsername != null && sonatypePassword != null) {
+ maven {
+ url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
+ credentials(PasswordCredentials::class.java) {
+ username = sonatypeUsername
+ password = sonatypePassword
+ }
+ }
+ }
+ }
+}
+
+signing {
+ sign(publishing.publications["release"])
+}
+
+tasks.register("generateBuildInfo") {
+ doLast {
+ var buildInfo = mapOf("version" to version)
+ val jsonOutput = JsonBuilder(buildInfo).toPrettyString()
+ val outputFile = File("${project.buildDir}/version.json")
+ outputFile.writeText(jsonOutput)
+ }
+}
+
+dependencies {
+ implementation(platform(libs.compose.bom))
+ implementation(libs.core.ktx)
+ implementation(libs.appcompat)
+ implementation(libs.material)
+
+ // Compose
+ implementation(libs.ui)
+ implementation(libs.ui.graphics)
+ implementation(libs.ui.tooling.preview)
+ implementation(libs.material3)
+ implementation(project(":superwall"))
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.test.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+}
diff --git a/superwall-compose/src/androidTest/java/com/superwall/sdk/compose/ExampleInstrumentedTest.kt b/superwall-compose/src/androidTest/java/com/superwall/sdk/compose/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..cb1468b9
--- /dev/null
+++ b/superwall-compose/src/androidTest/java/com/superwall/sdk/compose/ExampleInstrumentedTest.kt
@@ -0,0 +1,22 @@
+package com.superwall.sdk.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.superwall.superwall.compose.test", appContext.packageName)
+ }
+}
diff --git a/superwall-compose/src/main/AndroidManifest.xml b/superwall-compose/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/superwall-compose/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/superwall/src/main/java/com/superwall/sdk/composable/PaywallComposable.kt b/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt
similarity index 93%
rename from superwall/src/main/java/com/superwall/sdk/composable/PaywallComposable.kt
rename to superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt
index 36333e28..62400e09 100644
--- a/superwall/src/main/java/com/superwall/sdk/composable/PaywallComposable.kt
+++ b/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt
@@ -1,4 +1,4 @@
-package com.superwall.sdk.composable
+package com.superwall.sdk.compose
import android.app.Activity
import androidx.compose.foundation.layout.Arrangement
@@ -19,10 +19,10 @@ import androidx.compose.ui.viewinterop.AndroidView
import com.superwall.sdk.Superwall
import com.superwall.sdk.paywall.presentation.get_paywall.getPaywall
import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides
-import com.superwall.sdk.paywall.vc.LoadingView
-import com.superwall.sdk.paywall.vc.PaywallView
-import com.superwall.sdk.paywall.vc.ShimmerView
-import com.superwall.sdk.paywall.vc.delegate.PaywallViewCallback
+import com.superwall.sdk.paywall.view.LoadingView
+import com.superwall.sdk.paywall.view.PaywallView
+import com.superwall.sdk.paywall.view.ShimmerView
+import com.superwall.sdk.paywall.view.delegate.PaywallViewCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
diff --git a/superwall-compose/src/test/java/com/superwall/sdk/compose/ExampleUnitTest.kt b/superwall-compose/src/test/java/com/superwall/sdk/compose/ExampleUnitTest.kt
new file mode 100644
index 00000000..952aa22c
--- /dev/null
+++ b/superwall-compose/src/test/java/com/superwall/sdk/compose/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package com.superwall.sdk.compose
+
+import org.junit.Assert.*
+import org.junit.Test
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/superwall/build.gradle.kts b/superwall/build.gradle.kts
index c1e1ee32..cc5143fd 100644
--- a/superwall/build.gradle.kts
+++ b/superwall/build.gradle.kts
@@ -18,19 +18,19 @@ plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
- alias(libs.plugins.serialization) // Maven publishing
+ alias(libs.plugins.serialization)
id("maven-publish")
id("signing")
}
-version = "1.5.0"
+version = "2.0.0-alpha.1"
android {
compileSdk = 34
namespace = "com.superwall.sdk"
defaultConfig {
- minSdkVersion(26)
+ minSdkVersion(22)
targetSdkVersion(33)
aarMetadata {
@@ -40,8 +40,6 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
- consumerProguardFile("proguard-rules.pro")
-
val gitSha =
project
.exec {
@@ -58,9 +56,10 @@ android {
}
buildTypes {
- getByName("release") {
+ release {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ consumerProguardFile("../proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "../proguard-rules.pro")
}
}
@@ -70,14 +69,9 @@ android {
}
buildFeatures {
- compose = true
buildConfig = true
}
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.0"
- }
-
kotlinOptions {
jvmTarget = "1.8"
}
@@ -188,7 +182,6 @@ dependencies {
implementation(libs.room.runtime)
implementation(libs.room.ktx)
kapt(libs.room.compiler)
- implementation(libs.javascriptengine)
implementation(libs.kotlinx.coroutines.guava)
implementation(libs.threetenbp)
@@ -208,13 +201,6 @@ dependencies {
// Coroutines
implementation(libs.kotlinx.coroutines.core)
- // Compose
- implementation(platform(libs.compose.bom))
- implementation(libs.ui)
- implementation(libs.ui.graphics)
- implementation(libs.ui.tooling.preview)
- implementation(libs.material3)
-
// Serialization
implementation(libs.kotlinx.serialization.json)
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/ObserverModeTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/ObserverModeTest.kt
new file mode 100644
index 00000000..d3761d96
--- /dev/null
+++ b/superwall/src/androidTest/java/com/superwall/sdk/ObserverModeTest.kt
@@ -0,0 +1,241 @@
+package com.superwall.sdk
+
+import Given
+import Then
+import When
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ProductDetails
+import com.superwall.sdk.analytics.superwall.SuperwallEvent
+import com.superwall.sdk.analytics.superwall.SuperwallEventInfo
+import com.superwall.sdk.billing.BillingError
+import com.superwall.sdk.config.options.PaywallOptions
+import com.superwall.sdk.config.options.SuperwallOptions
+import com.superwall.sdk.delegate.SuperwallDelegate
+import com.superwall.sdk.dependencies.DependencyContainer
+import com.superwall.sdk.store.PurchasingObserverState
+import com.superwall.sdk.store.StoreKitManager
+import com.superwall.sdk.store.abstractions.product.RawStoreProduct
+import com.superwall.sdk.store.abstractions.product.SubscriptionPeriod
+import com.superwall.sdk.store.transactions.TransactionManager
+import com.superwall.sdk.utilities.PurchaseMockBuilder
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.spyk
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.math.BigDecimal
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+class ObserverModeTest {
+ private lateinit var transactionManager: TransactionManager
+ private lateinit var storeKitManager: StoreKitManager
+ private lateinit var dependencyContainer: DependencyContainer
+ val mockPricingPhases =
+ mockk {
+ every { pricingPhaseList } returns
+ listOf(
+ mockk {
+ every { billingPeriod } returns "P1M"
+ every { priceAmountMicros } returns 999000L
+ every { priceCurrencyCode } returns "USD"
+ every { billingCycleCount } returns 1
+ every { this@mockk.formattedPrice } returns "$9.99"
+ every { this@mockk.recurrenceMode } returns 0
+ },
+ )
+ }
+ val mockSubscriptionOfferDetails =
+ mockk {
+ every { basePlanId } returns "test_base_plan"
+ every { offerId } returns "test_offer"
+ every { pricingPhases } returns mockPricingPhases
+ every { this@mockk.offerToken } returns "test_offer_token"
+ every { this@mockk.offerTags } returns listOf("test_offer_tag")
+ }
+
+ val mockProductDetails =
+ mockk {
+ every { productId } returns "test_product"
+ every { productType } returns "subs"
+ every { subscriptionOfferDetails } returns listOf(mockSubscriptionOfferDetails)
+ every { this@mockk.oneTimePurchaseOfferDetails } returns null
+ every { this@mockk.name } returns "Test Product"
+ every { description } returns "Test Product Description"
+ }
+ val mockProduct: RawStoreProduct =
+ spyk(RawStoreProduct.from(mockProductDetails)) {
+ every { underlyingProductDetails } returns mockProductDetails
+ every { fullIdentifier } returns "test_product:test_base_plan:test_offer"
+ every { productIdentifier } returns "test_product"
+ every { hasFreeTrial } returns false
+ every { subscriptionPeriod } returns
+ SubscriptionPeriod(
+ 1,
+ SubscriptionPeriod.Unit.month,
+ )
+ every { localizedSubscriptionPeriod } returns "1 month"
+ every { price } returns BigDecimal.valueOf(9.99)
+ every { localizedPrice } returns "$9.99"
+ every { period } returns "P1M"
+ }
+
+ private lateinit var mockDelegate: MockDelegate
+
+ val CONSTANT_API_KEY = "pk_0ff90006c5c2078e1ce832bd2343ba2f806ca510a0a1696a"
+ var configured = false
+
+ @Before
+ fun setup() {
+ mockkObject(RawStoreProduct.Companion)
+ every { RawStoreProduct.from(any()) } returns mockProduct
+ if (!configured) {
+ Superwall.configure(
+ InstrumentationRegistry.getInstrumentation().context.applicationContext,
+ CONSTANT_API_KEY,
+ options =
+ SuperwallOptions().apply {
+ shouldObservePurchases = true
+ paywalls =
+ PaywallOptions().apply {
+ shouldPreload = false
+ }
+ },
+ completion = {
+ configured = true
+ },
+ )
+ }
+ dependencyContainer = Superwall.instance.dependencyContainer
+ transactionManager = dependencyContainer.transactionManager
+ storeKitManager = dependencyContainer.storeKitManager
+ }
+
+ @After
+ fun tearDown() {
+ Superwall.initialized = false
+ }
+
+ @Test
+ fun test_observe_purchase_will_begin_with_controller() =
+ runTest {
+ setup()
+ Given("a configured Superwall instance with purchase observation enabled") {
+ mockDelegate = MockDelegate(this@runTest)
+ Superwall.instance.delegate = mockDelegate
+
+ When("observing purchase will begin") {
+ Superwall.instance.observe(
+ PurchasingObserverState.PurchaseWillBegin(mockProductDetails),
+ )
+
+ Then("it should delegate to transaction manager and emit transaction start event") {
+ val event =
+ mockDelegate.events.first {
+ it is SuperwallEvent.TransactionStart
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_observe_purchase_complete_with_controller() =
+ runTest {
+ setup()
+ Given("a configured Superwall instance and completed purchase") {
+ mockDelegate = MockDelegate(this@runTest)
+ Superwall.instance.delegate = mockDelegate
+
+ When("observing purchase completion") {
+ Superwall.instance.observe(
+ PurchasingObserverState.PurchaseWillBegin(mockProductDetails),
+ )
+ delayFor(1.seconds)
+ Superwall.instance.observe(
+ PurchasingObserverState.PurchaseResult(
+ result =
+ BillingResult
+ .newBuilder()
+ .setResponseCode(BillingClient.BillingResponseCode.OK)
+ .build(),
+ purchases =
+ listOf(
+ PurchaseMockBuilder.createDefaultPurchase(
+ "test_product:test_base_plan:test_offer",
+ ),
+ ),
+ ),
+ )
+
+ Then("it should handle successful purchase and emit transaction complete event") {
+ mockDelegate.events
+ .onEach {
+ Log.e("test", "event is $it")
+ }.first {
+ it is SuperwallEvent.TransactionComplete
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun test_observe_purchase_failed_with_controller() =
+ runTest {
+ setup()
+ Given("a configured Superwall instance and failed purchase") {
+ mockDelegate = MockDelegate(this@runTest)
+ Superwall.instance.delegate = mockDelegate
+
+ val error = BillingError.BillingNotAvailable("Test error")
+
+ When("observing purchase failure") {
+ Superwall.instance.observe(
+ PurchasingObserverState.PurchaseError(
+ error = error,
+ product = mockProductDetails,
+ ),
+ )
+
+ Then("it should handle failure and emit transaction fail event") {
+ mockDelegate.events.first {
+ it is SuperwallEvent.TransactionFail
+ }
+ }
+ }
+ }
+ }
+}
+
+class MockDelegate(
+ val scope: CoroutineScope,
+) : SuperwallDelegate {
+ val events = MutableSharedFlow(extraBufferCapacity = 20)
+
+ override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
+ Log.e("test", "handle event is ${eventInfo.event}")
+ scope.launch {
+ events.emit(eventInfo.event)
+ }
+ }
+}
+
+suspend fun CoroutineScope.delayFor(duration: Duration) =
+ async(Dispatchers.IO) {
+ delay(duration)
+ }.await()
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt
index a72d8589..c6fd6df3 100644
--- a/superwall/src/androidTest/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt
+++ b/superwall/src/androidTest/java/com/superwall/sdk/analytics/internal/TrackingLogicTest.kt
@@ -1,5 +1,6 @@
package com.superwall.sdk.analytics.internal
+import android.app.Application
import androidx.test.platform.app.InstrumentationRegistry
import com.superwall.sdk.Superwall
import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
@@ -33,7 +34,7 @@ class TrackingLogicTest {
fun should_clean_up_attributes() =
runTest {
val ctx = InstrumentationRegistry.getInstrumentation().context
- Superwall.configure(ctx, "pk_test_1234", null, null, null, null)
+ Superwall.configure(ctx.applicationContext as Application, "pk_test_1234", null, null, null, null)
val deviceHelper =
spyk(
DeviceHelper(
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParamsTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParamsTest.kt
new file mode 100644
index 00000000..736c8d3a
--- /dev/null
+++ b/superwall/src/androidTest/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParamsTest.kt
@@ -0,0 +1,79 @@
+package com.superwall.sdk.billing.observer
+
+import com.android.billingclient.api.ProductDetails
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+class SuperwallBillingFlowParamsTest {
+ @Test
+ fun test_builder_sets_all_parameters_correctly() {
+ // Mock ProductDetails
+ val mockProductDetails = mockk()
+ every { mockProductDetails.oneTimePurchaseOfferDetails } returns
+ mockk {
+ every { zza() } returns "test_offer_token"
+ }
+ every { mockProductDetails.productType } returns "subs"
+ every { mockProductDetails.zza() } returns "test_product_id"
+
+ // Create ProductDetailsParams
+ val productDetailsParams =
+ SuperwallBillingFlowParams.ProductDetailsParams
+ .newBuilder()
+ .setOfferToken("test_offer_token")
+ .setProductDetails(mockProductDetails)
+ .build()
+
+ // Create SubscriptionUpdateParams
+ val subscriptionUpdateParams =
+ SuperwallBillingFlowParams.SubscriptionUpdateParams
+ .newBuilder()
+ .setSubscriptionReplacementMode(SuperwallBillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION)
+ .setOriginalExternalTransactionId("external_transaction_id")
+ .build()
+
+ // Build main params
+ val params =
+ SuperwallBillingFlowParams
+ .newBuilder()
+ .setIsOfferPersonalized(true)
+ .setObfuscatedAccountId("test_account_id")
+ .setObfuscatedProfileId("test_profile_id")
+ .setProductDetailsParamsList(listOf(productDetailsParams))
+ .setSubscriptionUpdateParams(subscriptionUpdateParams)
+ .build()
+
+ // Verify the built object
+ assertNotNull(params)
+ assertNotNull(params.toOriginal())
+ assertEquals(1, params.productDetailsParams.size)
+
+ // Verify ProductDetails
+ val storedProductDetails = params.productDetailsParams[0].details
+ assertEquals(mockProductDetails, storedProductDetails)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun test_product_details_params_builder_throws_when_product_details_is_missing() {
+ SuperwallBillingFlowParams.ProductDetailsParams
+ .newBuilder()
+ .setOfferToken("test_offer_token")
+ .build()
+ }
+
+ @Test
+ fun test_subscription_update_params_replacement_modes() {
+ val params =
+ SuperwallBillingFlowParams.SubscriptionUpdateParams
+ .newBuilder()
+ .setSubscriptionReplacementMode(SuperwallBillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION)
+ .setOldPurchaseToken("test_token")
+ .build()
+
+ assertNotNull(params)
+ assertNotNull(params.toOriginal())
+ }
+}
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt
index 7bb77862..845e23a5 100644
--- a/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt
+++ b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt
@@ -19,6 +19,7 @@ import com.superwall.sdk.models.assignment.Assignment
import com.superwall.sdk.models.assignment.ConfirmableAssignment
import com.superwall.sdk.models.config.Config
import com.superwall.sdk.models.config.RawFeatureFlag
+import com.superwall.sdk.models.entitlements.EntitlementStatus
import com.superwall.sdk.models.geo.GeoInfo
import com.superwall.sdk.models.triggers.Experiment
import com.superwall.sdk.models.triggers.Trigger
@@ -35,7 +36,10 @@ import com.superwall.sdk.storage.LatestGeoInfo
import com.superwall.sdk.storage.LocalStorage
import com.superwall.sdk.storage.Storage
import com.superwall.sdk.storage.StorageMock
-import com.superwall.sdk.store.StoreKitManager
+import com.superwall.sdk.storage.StoredEntitlementStatus
+import com.superwall.sdk.storage.StoredEntitlementsByProductId
+import com.superwall.sdk.store.Entitlements
+import com.superwall.sdk.store.StoreManager
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
@@ -65,7 +69,7 @@ class ConfigManagerUnderTest(
private val storage: Storage,
private val network: SuperwallAPI,
private val paywallManager: PaywallManager,
- private val storeKitManager: StoreKitManager,
+ private val storeManager: StoreManager,
private val factory: Factory,
private val deviceHelper: DeviceHelper,
private val assignments: Assignments,
@@ -76,7 +80,7 @@ class ConfigManagerUnderTest(
storage = storage,
network = network,
paywallManager = paywallManager,
- storeKitManager = storeKitManager,
+ storeManager = storeManager,
factory = factory,
deviceHelper = deviceHelper,
options = SuperwallOptions(),
@@ -84,6 +88,13 @@ class ConfigManagerUnderTest(
paywallPreload = paywallPreload,
ioScope = IOScope(ioScope.coroutineContext),
track = {},
+ entitlements =
+ Entitlements(
+ mockk(relaxUnitFun = true) {
+ every { read(StoredEntitlementStatus) } returns EntitlementStatus.Unknown
+ every { read(StoredEntitlementsByProductId) } returns emptyMap()
+ },
+ ),
) {
suspend fun setConfig(config: Config) {
configState.emit(ConfigState.Retrieved(config))
@@ -134,7 +145,7 @@ class ConfigManagerTests {
storage = storage,
network = network,
paywallManager = dependencyContainer.paywallManager,
- storeKitManager = dependencyContainer.storeKitManager,
+ storeManager = dependencyContainer.storeManager,
factory = dependencyContainer,
deviceHelper = mockDeviceHelper,
assignments = assignments,
@@ -179,7 +190,7 @@ class ConfigManagerTests {
storage = storage,
network = network,
paywallManager = dependencyContainer.paywallManager,
- storeKitManager = dependencyContainer.storeKitManager,
+ storeManager = dependencyContainer.storeManager,
factory = dependencyContainer,
deviceHelper = mockDeviceHelper,
assignments = assignments,
@@ -223,7 +234,7 @@ class ConfigManagerTests {
storage = storage,
network = network,
paywallManager = dependencyContainer.paywallManager,
- storeKitManager = dependencyContainer.storeKitManager,
+ storeManager = dependencyContainer.storeManager,
factory = dependencyContainer,
deviceHelper = mockDeviceHelper,
assignments = assignments,
@@ -269,7 +280,7 @@ class ConfigManagerTests {
storage = storage,
network = network,
paywallManager = dependencyContainer.paywallManager,
- storeKitManager = dependencyContainer.storeKitManager,
+ storeManager = dependencyContainer.storeManager,
factory = dependencyContainer,
deviceHelper = mockDeviceHelper,
assignments = assignmentStore,
@@ -339,7 +350,7 @@ class ConfigManagerTests {
Config.stub().copy(
rawFeatureFlags =
listOf(
- RawFeatureFlag("enable_config_refresh", true),
+ RawFeatureFlag("enable_config_refresh_v2", true),
),
)
@@ -364,7 +375,7 @@ class ConfigManagerTests {
storage,
mockNetwork,
mockPaywallManager,
- dependencyContainer.storeKitManager,
+ dependencyContainer.storeManager,
mockContainer,
mockDeviceHelper,
assignments = assignments,
@@ -412,7 +423,7 @@ class ConfigManagerTests {
Config.stub().copy(
rawFeatureFlags =
listOf(
- RawFeatureFlag("enable_config_refresh", true),
+ RawFeatureFlag("enable_config_refresh_v2", true),
),
)
@@ -437,7 +448,7 @@ class ConfigManagerTests {
storage,
mockNetwork,
mockPaywallManager,
- dependencyContainer.storeKitManager,
+ dependencyContainer.storeManager,
mockContainer,
mockDeviceHelper,
assignments = assignments,
@@ -475,7 +486,7 @@ class ConfigManagerTests {
every { resetCache() } just Runs
}
private val storeKit =
- mockk {
+ mockk {
coEvery { products(any()) } returns emptySet()
}
private val preload =
@@ -499,7 +510,7 @@ class ConfigManagerTests {
val cachedConfig =
Config.stub().copy(
buildId = "cached",
- rawFeatureFlags = listOf(RawFeatureFlag("enable_config_refresh", true)),
+ rawFeatureFlags = listOf(RawFeatureFlag("enable_config_refresh_v2", true)),
)
val newConfig = Config.stub().copy(buildId = "not")
@@ -539,7 +550,7 @@ class ConfigManagerTests {
storage = storage,
network = mockNetwork,
paywallManager = mockContainer.paywallManager,
- storeKitManager = mockContainer.storeKitManager,
+ storeManager = mockContainer.storeManager,
factory = mockContainer,
deviceHelper = mockDeviceHelper,
assignments = assignmentStore,
@@ -595,7 +606,7 @@ class ConfigManagerTests {
storage = storage,
network = mockNetwork,
paywallManager = manager,
- storeKitManager = storeKit,
+ storeManager = storeKit,
factory = dependencyContainer,
deviceHelper = mockDeviceHelper,
assignments = assignmentStore,
@@ -627,7 +638,7 @@ class ConfigManagerTests {
buildId = "cached",
rawFeatureFlags =
listOf(
- RawFeatureFlag("enable_config_refresh", true),
+ RawFeatureFlag("enable_config_refresh_v2", true),
),
)
@@ -654,7 +665,7 @@ class ConfigManagerTests {
storage = storage,
network = mockNetwork,
paywallManager = manager,
- storeKitManager = storeKit,
+ storeManager = storeKit,
factory = dependencyContainer,
deviceHelper = mockDeviceHelper,
assignments = assignmentStore,
@@ -729,7 +740,7 @@ class ConfigManagerTests {
storage = storage,
network = mockNetwork,
paywallManager = manager,
- storeKitManager = storeKit,
+ storeManager = storeKit,
factory = dependencyContainer,
deviceHelper = mockDeviceHelper,
assignments = assignmentStore,
@@ -754,7 +765,7 @@ class ConfigManagerTests {
val cachedConfig =
Config.stub().copy(
buildId = "cached",
- rawFeatureFlags = listOf(RawFeatureFlag("enable_config_refresh", true)),
+ rawFeatureFlags = listOf(RawFeatureFlag("enable_config_refresh_v2", true)),
)
val newConfig = Config.stub().copy(buildId = "not")
val cachedGeo = GeoInfo.stub().copy(country = "cachedCountry")
@@ -795,7 +806,7 @@ class ConfigManagerTests {
storage = storage,
network = mockNetwork,
paywallManager = mockContainer.paywallManager,
- storeKitManager = mockContainer.storeKitManager,
+ storeManager = mockContainer.storeManager,
factory = mockContainer,
deviceHelper = mockDeviceHelper,
assignments = assignmentStore,
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsInstrumentedTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsInstrumentedTest.kt
deleted file mode 100644
index 170513cd..00000000
--- a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsInstrumentedTest.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-package com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator
-
-import org.json.JSONObject
-import org.junit.Test
-import java.util.*
-
-class CombinedExpressionEvaluatorParamsTest {
- @Test
- fun expression_evaluator_params_test() {
- val expected = """
- {
- "expression": "user.id == '123'",
- "values": {
- "user": {
- "id": "123",
- "email": "test@gmail.com"
- },
- "device": {},
- "params": {
- "id": "567"
- }
- }
- }
- """
-
- val jsonValues = JSONObject()
- jsonValues.put("user", JSONObject(mapOf("id" to "123", "email" to "test@gmail.com")))
- jsonValues.put("device", JSONObject(emptyMap()))
- jsonValues.put("params", JSONObject(mapOf("id" to "567")))
-
- val liquidExpressionParams =
- LiquidExpressionEvaluatorParams(
- expression = "user.id == '123'",
- values = jsonValues,
- )
-
- val jsonString = liquidExpressionParams.toJson()
- println("!! jsonString: $jsonString")
-
- // Parse jsonString into a JSONObject
- val parsedJson = JSONObject(jsonString)
-
- // Test top-level properties
- assert(parsedJson.getString("expression") == "user.id == '123'")
-
- // Test nested properties
- val values = parsedJson.getJSONObject("values")
-
- val user = values.getJSONObject("user")
- assert(user.getString("id") == "123")
- assert(user.getString("email") == "test@gmail.com")
-
- val device = values.getJSONObject("device")
- assert(device.names() == null) // Check that device is empty
-
- val params = values.getJSONObject("params")
- assert(params.getString("id") == "567")
-
- val base64String = liquidExpressionParams.toBase64Input()
- // Try to base64 decode the string
- val decodedString = Base64.getDecoder().decode(base64String)
- // Parse the json
- val parsedJson2 = JSONObject(String(decodedString, Charsets.UTF_8))
-
- // Test top-level properties
- assert(parsedJson2.getString("expression") == "user.id == '123'")
-
- // Test nested properties
- val values2 = parsedJson2.getJSONObject("values")
-
- val user2 = values2.getJSONObject("user")
- assert(user2.getString("id") == "123")
- assert(user2.getString("email") == "test@gmail.com")
-
- val device2 = values2.getJSONObject("device")
- assert(device2.names() == null) // Check that device2 is empty
-
- val params2 = values2.getJSONObject("params")
- assert(params2.getString("id") == "567")
- }
-
- @Test
- fun javascript_expression_evaluator_params_test() {
- val expected = """
- {
- "expressionJS": "user.id == '123'",
- "values": {
- "user": {
- "id": "123",
- "email": "test@gmail.com"
- },
- "device": {},
- "params": {
- "id": "567"
- }
- }
- }
- """
-
- val jsonValues = JSONObject()
- jsonValues.put("user", JSONObject(mapOf("id" to "123", "email" to "test@gmail.com")))
- jsonValues.put("device", JSONObject(emptyMap()))
- jsonValues.put("params", JSONObject(mapOf("id" to "567")))
-
- val jsExpressionParams =
- JavascriptExpressionEvaluatorParams(
- expressionJs = "user.id == '123'",
- values = jsonValues,
- )
-
- val jsonString = jsExpressionParams.toJson()
-
- // Parse jsonString into a JSONObject
- val parsedJson = JSONObject(jsonString)
-
- // Test top-level properties
- assert(parsedJson.getString("expressionJS") == "user.id == '123'")
-
- // Test nested properties
- val values = parsedJson.getJSONObject("values")
-
- val user = values.getJSONObject("user")
- assert(user.getString("id") == "123")
- assert(user.getString("email") == "test@gmail.com")
-
- val device = values.getJSONObject("device")
- assert(device.names() == null) // Check that device is empty
-
- val params = values.getJSONObject("params")
- assert(params.getString("id") == "567")
-
- val base64String = jsExpressionParams.toBase64Input()
- // Try to base64 decode the string
- val decodedByteArray = Base64.getDecoder().decode(base64String)
- val decodedString = String(decodedByteArray, Charsets.UTF_8)
- // Parse the json
- val parsedJson2 = JSONObject(decodedString)
-
- // Test top-level properties
- assert(parsedJson2.getString("expressionJS") == "user.id == '123'")
-
- // Test nested properties
- val values2 = parsedJson2.getJSONObject("values")
-
- val user2 = values2.getJSONObject("user")
- assert(user2.getString("id") == "123")
- assert(user2.getString("email") == "test@gmail.com")
-
- val device2 = values2.getJSONObject("device")
- assert(device2.names() == null) // Check that device2 is empty
-
- val params2 = values2.getJSONObject("params")
- assert(params2.getString("id") == "567")
- }
-}
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/JavascriptCombinedExpressionEvaluatorInstrumentedTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/JavascriptCombinedExpressionEvaluatorInstrumentedTest.kt
deleted file mode 100644
index d71009b2..00000000
--- a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/JavascriptCombinedExpressionEvaluatorInstrumentedTest.kt
+++ /dev/null
@@ -1,417 +0,0 @@
-package com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator
-
-import androidx.javascriptengine.JavaScriptSandbox
-import androidx.test.platform.app.InstrumentationRegistry
-import com.superwall.sdk.dependencies.RuleAttributesFactory
-import com.superwall.sdk.misc.IOScope
-import com.superwall.sdk.models.config.ComputedPropertyRequest
-import com.superwall.sdk.models.events.EventData
-import com.superwall.sdk.models.triggers.Experiment
-import com.superwall.sdk.models.triggers.MatchedItem
-import com.superwall.sdk.models.triggers.TriggerPreloadBehavior
-import com.superwall.sdk.models.triggers.TriggerRule
-import com.superwall.sdk.models.triggers.TriggerRuleOutcome
-import com.superwall.sdk.models.triggers.UnmatchedRule
-import com.superwall.sdk.models.triggers.VariantOption
-import com.superwall.sdk.paywall.presentation.rule_logic.cel.SuperscriptEvaluator
-import com.superwall.sdk.paywall.presentation.rule_logic.javascript.SandboxJavascriptEvaluator
-import com.superwall.sdk.storage.LocalStorage
-import com.superwall.sdk.storage.StorageMock
-import com.superwall.sdk.storage.core_data.CoreDataManager
-import junit.framework.TestCase.assertEquals
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.async
-import kotlinx.coroutines.guava.await
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runTest
-import kotlinx.serialization.json.ClassDiscriminatorMode
-import kotlinx.serialization.json.Json
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import java.util.Date
-
-class RuleAttributeFactoryBuilder : RuleAttributesFactory {
- override suspend fun makeRuleAttributes(
- event: EventData?,
- computedPropertyRequests: List,
- ): Map =
- mapOf(
- "user" to
- mapOf(
- "id" to "123",
- "email" to "test@gmail.com",
- ),
- )
-}
-
-class JavascriptCombinedExpressionEvaluatorInstrumentedTest {
- var sandbox: JavaScriptSandbox? = null
-
- @Before
- fun setup() =
- runBlocking {
- sandbox = JavaScriptSandbox.createConnectedInstanceAsync(InstrumentationRegistry.getInstrumentation().targetContext).await()
- }
-
- @After
- fun tearDown() =
- runBlocking {
- sandbox?.killImmediatelyOnThread()
- sandbox?.close()
- sandbox = null
- }
-
- private fun CoroutineScope.evaluatorFor(storage: LocalStorage) =
- SandboxJavascriptEvaluator(
- sandbox ?: error("Sandbox not initialized"),
- storage = storage.coreDataManager,
- ioScope = this,
- )
-
- private fun CoroutineScope.superscriptEval(
- storage: CoreDataManager,
- factoryBuilder: RuleAttributeFactoryBuilder,
- ) = SuperscriptEvaluator(
- storage = storage,
- json =
- Json {
- classDiscriminatorMode = ClassDiscriminatorMode.ALL_JSON_OBJECTS
- classDiscriminator = "type"
- },
- factory = factoryBuilder,
- ioScope = IOScope(this.coroutineContext),
- )
-
- @Test
- fun test_happy_path_evaluator() =
- runTest {
- // get context
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val ruleAttributes = RuleAttributeFactoryBuilder()
- val storage = StorageMock(context = context, coroutineScope = this@runTest)
-
- val expressionEvaluator =
- CombinedExpressionEvaluator(
- evaluator =
- evaluatorFor(
- storage = storage,
- ),
- storage = storage,
- factory = ruleAttributes,
- track = {
- assert(it.jsExpressionResult == it.celExpressionResult)
- },
- superscriptEvaluator =
- superscriptEval(
- storage.coreDataManager,
- ruleAttributes,
- ),
- )
-
- val rule =
- TriggerRule(
- experimentId = "1",
- experimentGroupId = "2",
- variants =
- listOf(
- VariantOption(
- type = Experiment.Variant.VariantType.HOLDOUT,
- id = "3",
- percentage = 20,
- paywallId = null,
- ),
- ),
- expression = "user.id == '123'",
- expressionJs = null,
- preload =
- TriggerRule.TriggerPreload(
- behavior = TriggerPreloadBehavior.ALWAYS,
- requiresReEvaluation = false,
- ),
- )
-
- val result =
- expressionEvaluator.evaluateExpression(
- rule = rule,
- eventData =
- EventData(
- name = "test",
- parameters = mapOf("id" to "123"),
- createdAt = Date(),
- ),
- )
- assertEquals(TriggerRuleOutcome.match(rule = rule), result)
- }
-
- @Test
- fun test_expression_evaluator_expression_js() =
- runTest {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val ruleAttributes = RuleAttributeFactoryBuilder()
- val storage = StorageMock(context = context, coroutineScope = this@runTest)
-
- val expressionEvaluator =
- CombinedExpressionEvaluator(
- evaluator =
- evaluatorFor(
- storage = storage,
- ),
- storage = storage,
- factory = ruleAttributes,
- track = {
- // This is a JS only function
- },
- superscriptEvaluator =
- superscriptEval(
- storage.coreDataManager,
- ruleAttributes,
- ),
- )
-
- val trueRule =
- TriggerRule(
- experimentId = "1",
- experimentGroupId = "2",
- variants =
- listOf(
- VariantOption(
- type = Experiment.Variant.VariantType.HOLDOUT,
- id = "3",
- percentage = 20,
- paywallId = null,
- ),
- ),
- expression = null,
- expressionJs = "function superwallEvaluator(){ return true }; superwallEvaluator",
- preload =
- TriggerRule.TriggerPreload(
- behavior = TriggerPreloadBehavior.ALWAYS,
- requiresReEvaluation = false,
- ),
- )
-
- val falseRule =
- TriggerRule(
- experimentId = "1",
- experimentGroupId = "2",
- variants =
- listOf(
- VariantOption(
- type = Experiment.Variant.VariantType.HOLDOUT,
- id = "3",
- percentage = 20,
- paywallId = null,
- ),
- ),
- expression = null,
- expressionJs = "function superwallEvaluator(){ return false }; superwallEvaluator",
- preload =
- TriggerRule.TriggerPreload(
- behavior = TriggerPreloadBehavior.ALWAYS,
- requiresReEvaluation = false,
- ),
- )
-
- var trueResult =
- expressionEvaluator.evaluateExpression(
- rule = trueRule,
- eventData =
- EventData(
- name = "test",
- parameters = mapOf("id" to "123"),
- createdAt = Date(),
- ),
- )
- assert(trueResult == TriggerRuleOutcome.match(trueRule))
-
- var falseResult =
- expressionEvaluator.evaluateExpression(
- rule = falseRule,
- eventData =
- EventData(
- name = "test",
- parameters = mapOf("id" to "123"),
- createdAt = Date(),
- ),
- )
-
- assert(
- falseResult ==
- TriggerRuleOutcome.noMatch(
- source = UnmatchedRule.Source.EXPRESSION,
- experimentId = "1",
- ),
- )
- }
-
- @Test
- fun multi_threaded() =
- runTest {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val ruleAttributes = RuleAttributeFactoryBuilder()
- val storage = StorageMock(context = context, coroutineScope = this@runTest)
-
- val expressionEvaluator: CombinedExpressionEvaluator =
- CombinedExpressionEvaluator(
- evaluator =
- evaluatorFor(
- storage = storage,
- ),
- storage = storage,
- factory = ruleAttributes,
- track = {
- assert(it.jsExpressionResult == it.celExpressionResult)
- },
- superscriptEvaluator =
- superscriptEval(
- storage.coreDataManager,
- ruleAttributes,
- ),
- )
-
- val trueRule =
- TriggerRule(
- experimentId = "1",
- experimentGroupId = "2",
- variants =
- listOf(
- VariantOption(
- type = Experiment.Variant.VariantType.HOLDOUT,
- id = "3",
- percentage = 20,
- paywallId = null,
- ),
- ),
- expression = "user.id == '123'",
- expressionJs = null,
- preload =
- TriggerRule.TriggerPreload(
- behavior = TriggerPreloadBehavior.ALWAYS,
- requiresReEvaluation = false,
- ),
- )
-
- val falseRule =
- TriggerRule(
- experimentId = "1",
- experimentGroupId = "2",
- variants =
- listOf(
- VariantOption(
- type = Experiment.Variant.VariantType.HOLDOUT,
- id = "3",
- percentage = 20,
- paywallId = null,
- ),
- ),
- expression = null,
- expressionJs = "function() { return false; }",
- preload =
- TriggerRule.TriggerPreload(
- behavior = TriggerPreloadBehavior.ALWAYS,
- requiresReEvaluation = false,
- ),
- )
-
- val trueResult =
- async {
- expressionEvaluator.evaluateExpression(
- rule = trueRule,
- eventData =
- EventData(
- name = "test",
- parameters = mapOf("id" to "123"),
- createdAt = Date(),
- ),
- )
- }
-
- val falseResult =
- async {
- expressionEvaluator.evaluateExpression(
- rule = falseRule,
- eventData =
- EventData(
- name = "test",
- parameters = mapOf("id" to "123"),
- createdAt = Date(),
- ),
- )
- }
-
- // Await all the results
- val results = listOf(trueResult.await(), falseResult.await())
- val expectedResults =
- listOf(
- TriggerRuleOutcome.Match(matchedItem = MatchedItem(rule = trueRule)),
- TriggerRuleOutcome.noMatch(
- source = UnmatchedRule.Source.EXPRESSION,
- experimentId = "1",
- ),
- )
-
- assert(results == expectedResults)
- }
-
- @Test
- fun test_no_expression() =
- runTest {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val ruleAttributes = RuleAttributeFactoryBuilder()
- val storage = StorageMock(context = context, coroutineScope = this@runTest)
-
- val expressionEvaluator: CombinedExpressionEvaluator =
- CombinedExpressionEvaluator(
- evaluator =
- evaluatorFor(
- storage = storage,
- ),
- storage = storage,
- factory = ruleAttributes,
- track = {
- assert(it.jsExpressionResult == it.celExpressionResult)
- },
- superscriptEvaluator =
- superscriptEval(
- storage.coreDataManager,
- ruleAttributes,
- ),
- )
-
- val rule =
- TriggerRule(
- experimentId = "1",
- experimentGroupId = "2",
- variants =
- listOf(
- VariantOption(
- type = Experiment.Variant.VariantType.HOLDOUT,
- id = "3",
- percentage = 20,
- paywallId = null,
- ),
- ),
- expression = null,
- expressionJs = null,
- preload =
- TriggerRule.TriggerPreload(
- behavior = TriggerPreloadBehavior.ALWAYS,
- requiresReEvaluation = false,
- ),
- )
-
- val result =
- expressionEvaluator.evaluateExpression(
- rule = rule,
- eventData =
- EventData(
- name = "test",
- parameters = mapOf("id" to "123"),
- createdAt = Date(),
- ),
- )
-
- assert(result == TriggerRuleOutcome.match(rule = rule))
- }
-}
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvaluatorTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvaluatorTest.kt
deleted file mode 100644
index 3bebc993..00000000
--- a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvaluatorTest.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.superwall.sdk.paywall.presentation.rule_logic.javascript
-
-import android.webkit.WebView
-import androidx.javascriptengine.JavaScriptSandbox
-import androidx.javascriptengine.SandboxDeadException
-import androidx.test.platform.app.InstrumentationRegistry
-import com.superwall.sdk.misc.IOScope
-import com.superwall.sdk.misc.MainScope
-import com.superwall.sdk.models.triggers.TriggerRule
-import com.superwall.sdk.storage.StorageMock
-import io.mockk.every
-import io.mockk.mockkStatic
-import io.mockk.spyk
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.guava.await
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-class DefaultJavascriptEvaluatorTest {
- fun ctx() = InstrumentationRegistry.getInstrumentation().targetContext
-
- @Test
- fun evaulate_succesfully_with_sandbox() =
- runTest {
- val storage = StorageMock(ctx())
- mockkStatic(WebView::class) {
- every { WebView.getCurrentWebViewPackage() } returns null
- }
- val evaulator =
- DefaultJavascriptEvalutor(
- IOScope(this.coroutineContext),
- MainScope(this.coroutineContext),
- ctx(),
- storage = storage,
- )
- evaulator.evaluate("console.assert(true);", TriggerRule.stub())
- evaulator.teardown()
- }
-
- @Test
- fun fail_evaluating_with_sandbox_and_fallback_is_used() =
- runTest {
- val storage =
- StorageMock(
- ctx(),
- coroutineScope = this,
- )
-
- val sandbox = JavaScriptSandbox.createConnectedInstanceAsync(ctx()).await()
-
- val mockSand =
- spyk(sandbox) {
- every { createIsolate() } throws SandboxDeadException()
- }
- val evaulator =
- DefaultJavascriptEvalutor(
- IOScope(this.coroutineContext),
- MainScope(),
- ctx(),
- storage = storage,
- createSandbox = {
- Result.success(sandbox)
- },
- )
- launch(Dispatchers.IO) {
- delay(100)
- evaulator.evaluate("console.assert(true);", TriggerRule.stub())
- }
- mockSand.killImmediatelyOnThread()
- evaulator.teardown()
- }
-}
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/WebviewFallbackClientTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/WebviewFallbackClientTest.kt
index feaf2781..848b8baf 100644
--- a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/WebviewFallbackClientTest.kt
+++ b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/WebviewFallbackClientTest.kt
@@ -13,12 +13,12 @@ import com.superwall.sdk.misc.IOScope
import com.superwall.sdk.misc.MainScope
import com.superwall.sdk.models.paywall.Paywall
import com.superwall.sdk.models.paywall.PaywallWebviewUrl
-import com.superwall.sdk.paywall.vc.web_view.DefaultWebviewClient
-import com.superwall.sdk.paywall.vc.web_view.SWWebView
-import com.superwall.sdk.paywall.vc.web_view.WebviewClientEvent
-import com.superwall.sdk.paywall.vc.web_view.WebviewClientEvent.OnPageFinished
-import com.superwall.sdk.paywall.vc.web_view.WebviewError
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandler
+import com.superwall.sdk.paywall.view.webview.DefaultWebviewClient
+import com.superwall.sdk.paywall.view.webview.SWWebView
+import com.superwall.sdk.paywall.view.webview.WebviewClientEvent
+import com.superwall.sdk.paywall.view.webview.WebviewClientEvent.OnPageFinished
+import com.superwall.sdk.paywall.view.webview.WebviewError
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandler
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -91,6 +91,7 @@ class WebviewFallbackClientTest {
Then("the loading is successful") {
webview
.waitForEvent(mainScope) {
+ println("Got event $it")
it is OnPageFinished
}.let {
assert(it is OnPageFinished)
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/messaging/TestPaywallMessageHandlerDelegate.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/messaging/TestPaywallMessageHandlerDelegate.kt
index f438bd0b..da8f18a9 100644
--- a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/messaging/TestPaywallMessageHandlerDelegate.kt
+++ b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/messaging/TestPaywallMessageHandlerDelegate.kt
@@ -5,9 +5,9 @@ import com.superwall.sdk.models.events.EventData
import com.superwall.sdk.models.paywall.Paywall
import com.superwall.sdk.paywall.presentation.PaywallInfo
import com.superwall.sdk.paywall.presentation.internal.PresentationRequest
-import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandlerDelegate
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent
+import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandlerDelegate
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent
class TestPaywallMessageHandlerDelegate(
override val request: PresentationRequest? = null,
@@ -26,14 +26,6 @@ class TestPaywallMessageHandlerDelegate(
TODO("Not yet implemented")
}
- override fun presentSafariInApp(url: String) {
- super.presentSafariInApp(url)
- }
-
- override fun presentSafariExternal(url: String) {
- super.presentSafariExternal(url)
- }
-
override fun presentBrowserInApp(url: String) {
TODO("Not yet implemented")
}
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/storage/ConfigureSDKTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/storage/ConfigureSDKTest.kt
index 69b4acfb..0024f69c 100644
--- a/superwall/src/androidTest/java/com/superwall/sdk/storage/ConfigureSDKTest.kt
+++ b/superwall/src/androidTest/java/com/superwall/sdk/storage/ConfigureSDKTest.kt
@@ -1,5 +1,6 @@
package com.superwall.sdk.storage
+import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.superwall.sdk.Superwall
@@ -18,7 +19,7 @@ class ConfigureSDKTest {
runTest {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- val superwall = Superwall.configure(appContext, CONSTANT_API_KEY)
+ val superwall = Superwall.configure(appContext.applicationContext as Application, CONSTANT_API_KEY)
val res = Superwall.hasInitialized.first()
assertEquals(true, res)
}
diff --git a/superwall/src/androidTest/java/com/superwall/sdk/utilities/PurchaseMockBuilder.kt b/superwall/src/androidTest/java/com/superwall/sdk/utilities/PurchaseMockBuilder.kt
new file mode 100644
index 00000000..16cb97ec
--- /dev/null
+++ b/superwall/src/androidTest/java/com/superwall/sdk/utilities/PurchaseMockBuilder.kt
@@ -0,0 +1,91 @@
+package com.superwall.sdk.utilities
+
+import com.android.billingclient.api.Purchase
+import org.json.JSONArray
+import org.json.JSONObject
+
+class PurchaseMockBuilder {
+ private val purchaseJson = JSONObject()
+
+ fun setPurchaseState(state: Int): PurchaseMockBuilder {
+ purchaseJson.put("purchaseState", if (state == 2) 4 else state)
+ return this
+ }
+
+ fun setPurchaseTime(time: Long): PurchaseMockBuilder {
+ purchaseJson.put("purchaseTime", time)
+ return this
+ }
+
+ fun setOrderId(orderId: String?): PurchaseMockBuilder {
+ purchaseJson.put("orderId", orderId)
+ return this
+ }
+
+ fun setProductId(productId: String?): PurchaseMockBuilder {
+ val productIds = JSONArray()
+ productIds.put(productId)
+ purchaseJson.put("productIds", productIds)
+ // For backward compatibility
+ purchaseJson.put("productId", productId)
+ return this
+ }
+
+ fun setQuantity(quantity: Int): PurchaseMockBuilder {
+ purchaseJson.put("quantity", quantity)
+ return this
+ }
+
+ fun setPurchaseToken(token: String?): PurchaseMockBuilder {
+ purchaseJson.put("token", token)
+ purchaseJson.put("purchaseToken", token)
+ return this
+ }
+
+ fun setPackageName(packageName: String?): PurchaseMockBuilder {
+ purchaseJson.put("packageName", packageName)
+ return this
+ }
+
+ fun setDeveloperPayload(payload: String?): PurchaseMockBuilder {
+ purchaseJson.put("developerPayload", payload)
+ return this
+ }
+
+ fun setAcknowledged(acknowledged: Boolean): PurchaseMockBuilder {
+ purchaseJson.put("acknowledged", acknowledged)
+ return this
+ }
+
+ fun setAutoRenewing(autoRenewing: Boolean): PurchaseMockBuilder {
+ purchaseJson.put("autoRenewing", autoRenewing)
+ return this
+ }
+
+ fun setAccountIdentifiers(
+ obfuscatedAccountId: String?,
+ obfuscatedProfileId: String?,
+ ): PurchaseMockBuilder {
+ purchaseJson.put("obfuscatedAccountId", obfuscatedAccountId)
+ purchaseJson.put("obfuscatedProfileId", obfuscatedProfileId)
+ return this
+ }
+
+ fun build(): Purchase = Purchase(purchaseJson.toString(), "dummy-signature")
+
+ companion object {
+ fun createDefaultPurchase(id: String): Purchase =
+ PurchaseMockBuilder()
+ .setPurchaseState(Purchase.PurchaseState.PURCHASED)
+ .setPurchaseTime(System.currentTimeMillis())
+ .setOrderId("GPA.1234-5678-9012-34567")
+ .setProductId(id)
+ .setQuantity(1)
+ .setPurchaseToken("opaque-token-up-to-1950-characters")
+ .setPackageName("com.superwall.sdk")
+ .setDeveloperPayload("")
+ .setAcknowledged(true)
+ .setAutoRenewing(true)
+ .build()
+ }
+}
diff --git a/superwall/src/main/AndroidManifest.xml b/superwall/src/main/AndroidManifest.xml
index a3e594af..8288424f 100644
--- a/superwall/src/main/AndroidManifest.xml
+++ b/superwall/src/main/AndroidManifest.xml
@@ -1,6 +1,8 @@
-
+
+
\ No newline at end of file
diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt
index be3d937a..537e4ccd 100644
--- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt
+++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt
@@ -4,13 +4,18 @@ import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.work.WorkManager
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.Purchase
import com.superwall.sdk.analytics.internal.track
import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
import com.superwall.sdk.analytics.superwall.SuperwallEventInfo
+import com.superwall.sdk.billing.toInternalResult
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.models.ConfigurationStatus
import com.superwall.sdk.config.options.SuperwallOptions
-import com.superwall.sdk.delegate.SubscriptionStatus
+import com.superwall.sdk.delegate.InternalPurchaseResult
+import com.superwall.sdk.delegate.PurchaseResult
import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.delegate.SuperwallDelegateJava
import com.superwall.sdk.delegate.subscription_controller.PurchaseController
@@ -27,6 +32,8 @@ import com.superwall.sdk.misc.fold
import com.superwall.sdk.misc.launchWithTracking
import com.superwall.sdk.misc.toResult
import com.superwall.sdk.models.assignment.ConfirmedAssignment
+import com.superwall.sdk.models.entitlements.Entitlement
+import com.superwall.sdk.models.entitlements.EntitlementStatus
import com.superwall.sdk.models.events.EventData
import com.superwall.sdk.network.device.InterfaceStyle
import com.superwall.sdk.paywall.presentation.PaywallCloseReason
@@ -37,19 +44,23 @@ import com.superwall.sdk.paywall.presentation.internal.confirmAssignment
import com.superwall.sdk.paywall.presentation.internal.dismiss
import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo
import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult
-import com.superwall.sdk.paywall.vc.PaywallView
-import com.superwall.sdk.paywall.vc.SuperwallPaywallActivity
-import com.superwall.sdk.paywall.vc.delegate.PaywallViewEventCallback
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent.Closed
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent.Custom
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent.InitiatePurchase
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent.InitiateRestore
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent.OpenedDeepLink
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent.OpenedURL
-import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent.OpenedUrlInChrome
-import com.superwall.sdk.storage.ActiveSubscriptionStatus
-import com.superwall.sdk.store.ExternalNativePurchaseController
+import com.superwall.sdk.paywall.view.PaywallView
+import com.superwall.sdk.paywall.view.SuperwallPaywallActivity
+import com.superwall.sdk.paywall.view.delegate.PaywallViewEventCallback
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.Closed
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.Custom
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.InitiatePurchase
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.InitiateRestore
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.OpenedDeepLink
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.OpenedURL
+import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.OpenedUrlInChrome
+import com.superwall.sdk.storage.StoredEntitlementStatus
+import com.superwall.sdk.store.Entitlements
+import com.superwall.sdk.store.PurchasingObserverState
+import com.superwall.sdk.store.abstractions.product.RawStoreProduct
+import com.superwall.sdk.store.abstractions.product.StoreProduct
+import com.superwall.sdk.store.transactions.TransactionManager
import com.superwall.sdk.utilities.withErrorTracking
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -59,9 +70,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -88,7 +100,11 @@ class Superwall(
internal val presentationItems: PresentationItems = PresentationItems()
private val _events: MutableSharedFlow =
- MutableSharedFlow(0, extraBufferCapacity = 64 * 4, onBufferOverflow = BufferOverflow.SUSPEND)
+ MutableSharedFlow(
+ 0,
+ extraBufferCapacity = 64 * 4,
+ onBufferOverflow = BufferOverflow.SUSPEND,
+ )
/**
* A flow emitting all Superwall events as an alternative to delegate.
@@ -174,7 +190,8 @@ class Superwall(
@JvmName("getDelegate")
fun getJavaDelegate(): SuperwallDelegateJava? = dependencyContainer.delegateAdapter.javaDelegate
- /** A published property that indicates the subscription status of the user.
+ /**
+ * Sets the entitlement status and updates the corresponding entitlement collections.
*
* If you're handling subscription-related logic yourself, you must set this
* property whenever the subscription status of a user changes.
@@ -183,23 +200,41 @@ class Superwall(
* be synced with the user's purchases on device.
*
* Paywalls will not show until the subscription status has been established.
- * On first install, it's value will default to [SubscriptionStatus.UNKNOWN]. Afterwards, it'll
+ * On first install, it's value will default to [EntitlementStatus.Unknown]. Afterwards, it'll
* default to its cached value.
*
- * You can observe [subscriptionStatus] to get notified whenever the user's subscription status
+ * You can observe [entitlements.status] to get notified whenever the user's subscription status
* changes.
*
* Otherwise, you can check the delegate function
- * [SuperwallDelegate.subscriptionStatusDidChange]
+ * [SuperwallDelegate.entitlementStatusDidChange]
* to receive a callback with the new value every time it changes.
*
* To learn more, see
* [Purchases and Subscription Status](https://docs.superwall.com/docs/advanced-configuration).
*
- * @param subscriptionStatus The subscription status of the user.
+ * @param entitlementStatus The entitlement status of the user.
*/
- fun setSubscriptionStatus(subscriptionStatus: SubscriptionStatus) {
- _subscriptionStatus.value = subscriptionStatus
+ fun setEntitlementStatus(entitlementStatus: EntitlementStatus) {
+ entitlements.setEntitlementStatus(entitlementStatus)
+ }
+
+ /**
+ * Simplified version of [Superwall.setEntitlementStatus] that allows
+ * you to set the entitlements by passing in an array of strings.
+ * An empty list is treated as [EntitlementStatus.Inactive].
+ * Example:
+ * `setEntitlementStatus("default", "pro")` equals `EntitlementStatus.Active(setOf(Entitlement("default"), Entitlement("pro")))`
+ * `setEntitlementStatus()` equals `EntitlementStatus.Inactive`
+ *
+ * @param entitlements A list of entitlements.
+ * */
+ fun setEntitlementStatus(vararg entitlements: String) {
+ if (entitlements.isEmpty()) {
+ this.entitlements.setEntitlementStatus(EntitlementStatus.Inactive)
+ } else {
+ this.setEntitlementStatus(EntitlementStatus.Active(entitlements.map { Entitlement(it) }.toSet()))
+ }
}
/**
@@ -236,16 +271,9 @@ class Superwall(
return presentedPaywallInfo ?: presentationItems.paywallInfo
}
- protected var _subscriptionStatus: MutableStateFlow =
- MutableStateFlow(
- SubscriptionStatus.UNKNOWN,
- )
-
- /**
- * A `StateFlow` of the subscription status of the user. Set this using
- * [setSubscriptionStatus].
- */
- val subscriptionStatus: StateFlow get() = _subscriptionStatus
+ val entitlements: Entitlements by lazy {
+ dependencyContainer.entitlements
+ }
/**
* A property that indicates current configuration state of the SDK.
@@ -264,6 +292,16 @@ class Superwall(
}
}
+ val configurationStateListener: Flow
+ get() =
+ dependencyContainer.configManager.configState.asSharedFlow().map {
+ when (it) {
+ is ConfigState.Retrieved -> ConfigurationStatus.Configured
+ is ConfigState.Failed -> ConfigurationStatus.Failed
+ else -> ConfigurationStatus.Pending
+ }
+ }
+
companion object {
/** A variable that is only `true` if ``instance`` is available for use.
* Gets set to `true` immediately after
@@ -298,7 +336,7 @@ class Superwall(
* [sign up for free](https://superwall.com/sign-up).
* @param purchaseController An object that conforms to [PurchaseController]. You must
* implement this to handle all subscription-related logic yourself. You'll need to also
- * call [setSubscriptionStatus] every time the user's subscription status changes. You can
+ * call [setEntitlementStatus] every time the user's subscription status changes. You can
* read more about that in
* [Purchases and Subscription Status](https://docs.superwall.com/docs/advanced-configuration).
* @param options An optional [SuperwallOptions] object which allows you to customise the
@@ -326,9 +364,6 @@ class Superwall(
completion?.invoke(Result.success(Unit))
return
}
- val purchaseController =
- purchaseController
- ?: ExternalNativePurchaseController(context = applicationContext)
_instance =
Superwall(
context = applicationContext,
@@ -361,25 +396,6 @@ class Superwall(
)
})
}
-
- @Deprecated(
- "This constructor is too ambiguous and will be removed in upcoming versions. Use Superwall.configure(Application, ...) instead.",
- )
- fun configure(
- applicationContext: Context,
- apiKey: String,
- purchaseController: PurchaseController? = null,
- options: SuperwallOptions? = null,
- activityProvider: ActivityProvider? = null,
- completion: ((Result) -> Unit)? = null,
- ) = configure(
- applicationContext.applicationContext as Application,
- apiKey,
- purchaseController,
- options,
- activityProvider,
- completion,
- )
}
private lateinit var _dependencyContainer: DependencyContainer
@@ -410,10 +426,10 @@ class Superwall(
throw e
}
- val cachedSubsStatus =
- dependencyContainer.storage.read(ActiveSubscriptionStatus)
- ?: SubscriptionStatus.UNKNOWN
- setSubscriptionStatus(cachedSubsStatus)
+ val cachedEntitlementStatus =
+ dependencyContainer.storage.read(StoredEntitlementStatus)
+ ?: EntitlementStatus.Unknown
+ setEntitlementStatus(cachedEntitlementStatus)
addListeners()
@@ -448,13 +464,13 @@ class Superwall(
// / Listens to config and the subscription status
private fun addListeners() {
ioScope.launchWithTracking {
- subscriptionStatus // Removes duplicates by default
+ entitlements.status // Removes duplicates by default
.drop(1) // Drops the first item
.collect { newValue ->
// Save and handle the new value
- dependencyContainer.storage.write(ActiveSubscriptionStatus, newValue)
- dependencyContainer.delegateAdapter.subscriptionStatusDidChange(newValue)
- val event = InternalSuperwallEvent.SubscriptionStatusDidChange(newValue)
+ dependencyContainer.storage.write(StoredEntitlementStatus, newValue)
+ dependencyContainer.delegateAdapter.entitlementStatusDidChange(newValue)
+ val event = InternalSuperwallEvent.EntitlementStatusDidChange(newValue)
track(event)
}
}
@@ -683,6 +699,224 @@ class Superwall(
}
}
+ /**
+ * Initiates a purchase of `ProductDetails`.
+ *
+ * Use this function to purchase any `ProductDetails`, regardless of whether you
+ * have a paywall or not. Superwall will handle the purchase with `GooglePlayBilling`
+ * and return the `PurchaseResult`. You'll see the data associated with the
+ * purchase on the Superwall dashboard.
+ *
+ * @param product: The `ProductDetails` you wish to purchase.
+ * @return A ``PurchaseResult``.
+ * - Note: You do not need to finish the transaction yourself after this.
+ * ``Superwall`` will handle this for you.
+ */
+ suspend fun purchase(product: ProductDetails): Result =
+ withErrorTracking {
+ dependencyContainer.transactionManager.purchase(
+ TransactionManager.PurchaseSource.ExternalPurchase(
+ StoreProduct(RawStoreProduct.from(product)),
+ ),
+ )
+ }.toResult()
+
+ /**
+ * Initiates a purchase of `StoreProduct`.
+ *
+ * Use this function to purchase any `StoreProduct`, regardless of whether you
+ * have a paywall or not. Superwall will handle the purchase with `GooglePlayBilling`
+ * and return the `PurchaseResult`. You'll see the data associated with the
+ * purchase on the Superwall dashboard.
+ *
+ * @param product: The `StoreProduct` you wish to purchase.
+ * @return A ``PurchaseResult``.
+ * - Note: You do not need to finish the transaction yourself after this.
+ * ``Superwall`` will handle this for you.
+ */
+ suspend fun purchase(product: StoreProduct): Result =
+ withErrorTracking {
+ dependencyContainer.transactionManager.purchase(
+ TransactionManager.PurchaseSource.ExternalPurchase(
+ product,
+ ),
+ )
+ }.toResult()
+
+ /**
+ * Initiates a purchase of a product with the given `productId`.
+ *
+ * Use this function to purchase any product with a given product ID, regardless of whether you
+ * have a paywall or not. Superwall will handle the purchase with `GooglePlayBilling`
+ * and return the `PurchaseResult`. You'll see the data associated with the
+ * purchase on the Superwall dashboard.
+ *
+ * @param product: The `produdctId` you wish to purchase.
+ * @return A ``PurchaseResult``.
+ * - Note: You do not need to finish the transaction yourself after this.
+ * ``Superwall`` will handle this for you.
+ */
+ suspend fun purchase(productId: String): Result =
+ withErrorTracking {
+ getProducts(productId).getOrThrow()[productId]?.let {
+ dependencyContainer.transactionManager.purchase(
+ TransactionManager.PurchaseSource.ExternalPurchase(
+ it,
+ ),
+ )
+ } ?: throw IllegalArgumentException("Product with id $productId not found")
+ }.toResult()
+
+ /**
+ * Given a list of product identifiers, returns a map of identifiers to `StoreProduct` objects.
+ *
+ * @param productIds: A list of full product identifiers.
+ * @return A map of product identifiers to `StoreProduct` objects.
+ */
+ suspend fun getProducts(vararg productIds: String): Result