From 3a23f9d0e494881d6a8504555a57c660416e26ff Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Fri, 6 Sep 2024 21:33:09 +0200 Subject: [PATCH 01/37] Extract compose, add consumer proguard rules --- CHANGELOG.md | 10 +- app/build.gradle.kts | 1 + app/proguard-rules.pro | 21 --- .../com/superwall/superapp/ComposeActivity.kt | 2 +- build.gradle.kts | 1 + consumer-rules.pro | 64 +++++++ gradle.properties | 5 +- gradle/libs.versions.toml | 1 + .../proguard-rules.pro => proguard-rules.pro | 0 settings.gradle | 1 + superwall-compose/.gitignore | 1 + superwall-compose/build.gradle.kts | 169 ++++++++++++++++++ .../sdk/compose/ExampleInstrumentedTest.kt | 22 +++ .../src/main/AndroidManifest.xml | 4 + .../sdk/compose}/PaywallComposable.kt | 2 +- .../superwall/sdk/compose/ExampleUnitTest.kt | 16 ++ superwall/build.gradle.kts | 21 +-- 17 files changed, 298 insertions(+), 43 deletions(-) delete mode 100644 app/proguard-rules.pro create mode 100644 consumer-rules.pro rename superwall/proguard-rules.pro => proguard-rules.pro (100%) create mode 100644 superwall-compose/.gitignore create mode 100644 superwall-compose/build.gradle.kts create mode 100644 superwall-compose/src/androidTest/java/com/superwall/sdk/compose/ExampleInstrumentedTest.kt create mode 100644 superwall-compose/src/main/AndroidManifest.xml rename {superwall/src/main/java/com/superwall/sdk/composable => superwall-compose/src/main/java/com/superwall/sdk/compose}/PaywallComposable.kt (99%) create mode 100644 superwall-compose/src/test/java/com/superwall/sdk/compose/ExampleUnitTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d406e1fb..451537c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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 + ## 1.5.0 ### Enhancements @@ -14,8 +21,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/app/build.gradle.kts b/app/build.gradle.kts index d91aa857..db564f1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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/main/java/com/superwall/superapp/ComposeActivity.kt b/app/src/main/java/com/superwall/superapp/ComposeActivity.kt index cd22b18c..a8a2b53f 100644 --- a/app/src/main/java/com/superwall/superapp/ComposeActivity.kt +++ b/app/src/main/java/com/superwall/superapp/ComposeActivity.kt @@ -25,7 +25,7 @@ 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 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/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..aa4bead9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -106,3 +106,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 99% 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..ff2c0043 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 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..d6394a4c 100644 --- a/superwall/build.gradle.kts +++ b/superwall/build.gradle.kts @@ -18,7 +18,7 @@ plugins { id("com.android.library") kotlin("android") kotlin("kapt") - alias(libs.plugins.serialization) // Maven publishing + alias(libs.plugins.serialization) id("maven-publish") id("signing") } @@ -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" } @@ -208,13 +202,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) From 26a25f1d92beeca24b6d25c73e4a355cc497317c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 16 Sep 2024 13:28:52 +0200 Subject: [PATCH 02/37] Add compose to deploy script --- .github/workflows/build+test+deploy.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 \ From d5f5ea3fc474c1ad0ee69326a65bbebe8263ca75 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 19 Sep 2024 22:11:08 +0200 Subject: [PATCH 03/37] Downgrade minSDK to version 22 --- app/build.gradle.kts | 2 +- superwall/build.gradle.kts | 2 +- superwall/src/main/AndroidManifest.xml | 4 +- .../sdk/analytics/internal/TrackingLogic.kt | 4 +- .../sdk/contrib/threeteen/AmountFormats.kt | 66 +++--- .../sdk/network/device/DeviceHelper.kt | 57 ++--- .../ExpressionEvaluatorParams.kt | 4 +- .../paywall/vc/SuperwallPaywallActivity.kt | 40 ++-- .../vc/web_view/DefaultWebviewClient.kt | 19 +- .../sdk/paywall/vc/web_view/SWWebView.kt | 37 ++- .../messaging/PaywallMessageHandler.kt | 5 +- .../vc/web_view/templating/TemplateLogic.kt | 5 +- .../sdk/storage/core_data/CoreDataManager.kt | 2 +- .../abstractions/product/RawStoreProduct.kt | 2 +- .../sdk/contrib/AmountFormatsTest.kt | 215 ++++++++++++++++++ .../src/test/java/com/superwall/sdk/utils.kt | 54 +++++ 16 files changed, 409 insertions(+), 109 deletions(-) create mode 100644 superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index db564f1f..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" diff --git a/superwall/build.gradle.kts b/superwall/build.gradle.kts index d6394a4c..9f3d6144 100644 --- a/superwall/build.gradle.kts +++ b/superwall/build.gradle.kts @@ -30,7 +30,7 @@ android { namespace = "com.superwall.sdk" defaultConfig { - minSdkVersion(26) + minSdkVersion(22) targetSdkVersion(33) aarMetadata { 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/analytics/internal/TrackingLogic.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt index d57190c5..61716a08 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt @@ -16,8 +16,8 @@ import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.net.URI -import java.time.LocalDateTime -import java.time.ZoneOffset +import org.threeten.bp.LocalDateTime +import org.threeten.bp.ZoneOffset import java.util.* sealed class TrackingLogic { diff --git a/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt b/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt index af53fed4..0d124f03 100644 --- a/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt +++ b/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt @@ -4,13 +4,10 @@ import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import org.threeten.bp.Period -import java.time.Duration -import java.time.format.DateTimeParseException +import org.threeten.bp.Duration +import org.threeten.bp.format.DateTimeParseException import java.util.* -import java.util.function.Function -import java.util.function.IntPredicate import java.util.regex.Pattern -import java.util.stream.Stream /* * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos @@ -55,6 +52,11 @@ import java.util.stream.Stream * This class is immutable and thread-safe. */ object AmountFormats { + + + fun interface IntPredicate { + fun test(value: Int): Boolean + } /** * The number of days per week. */ @@ -172,7 +174,7 @@ object AmountFormats { * List of DurationUnit values ordered by longest suffix first. */ private val DURATION_UNITS = - Arrays.asList( + listOf( DurationUnit("ns", Duration.ofNanos(1)), DurationUnit("µs", Duration.ofNanos(1000)), // U+00B5 = micro symbol DurationUnit("μs", Duration.ofNanos(1000)), // U+03BC = Greek letter mu @@ -431,23 +433,23 @@ object AmountFormats { // consume the leading sign - or + if one is present. var sign = 1 var updatedText = consumePrefix(durationText, '-') - if (updatedText.isPresent) { + if (updatedText.isSuccess) { sign = -1 offset += 1 - durationText = updatedText.get() + durationText = updatedText.getOrNull()!! } else { updatedText = consumePrefix(durationText, '+') - if (updatedText.isPresent) { + if (updatedText.isSuccess) { offset += 1 } - durationText = updatedText.orElse(durationText) + durationText = updatedText.getOrNull() ?: durationText } // special case for a string of "0" if (durationText == "0") { return Duration.ZERO } // special case, empty string as an invalid duration. - if (durationText.length == 0) { + if (durationText.isEmpty()) { throw DateTimeParseException("Not a numeric value", original, 0) } var value = Duration.ZERO @@ -459,9 +461,9 @@ object AmountFormats { val leadingInt: DurationScalar = integerPart var fraction: DurationScalar = EMPTY_FRACTION val dot = consumePrefix(durationText, '.') - if (dot.isPresent) { + if (dot.isSuccess) { offset += 1 - durationText = dot.get() + durationText = dot.getOrNull()!! val fractionPart = consumeDurationFraction(durationText, original, offset) // update the remaining string and fraction. offset += durationText.length - fractionPart.remainingText().length @@ -469,14 +471,14 @@ object AmountFormats { fraction = fractionPart } val optUnit = findUnit(durationText) - if (!optUnit.isPresent) { + if (optUnit.isFailure) { throw DateTimeParseException( "Invalid duration unit", original, offset, ) } - val unit = optUnit.get() + val unit = optUnit.getOrNull()!! try { var unitValue = leadingInt.applyTo(unit) val fractionValue = fraction.applyTo(unit) @@ -591,27 +593,24 @@ object AmountFormats { } // find the duration unit at the beginning of the input text, if present. - private fun findUnit(text: CharSequence): Optional = + private fun findUnit(text: CharSequence): Result = DURATION_UNITS - .stream() - .sequential() - .filter { du: DurationUnit -> + .firstOrNull { du: DurationUnit -> du.prefixMatchesUnit( text, ) - }.findFirst() + }?.let { Result.success(it) } ?: Result.failure(Exception("No matching duration unit found")) // consume the indicated {@code prefix} if it exists at the beginning of the - // text, returning the - // remaining string if the prefix was consumed. + // text, returning the remaining string if the prefix was consumed. private fun consumePrefix( text: CharSequence, prefix: Char, - ): Optional = - if (text.length > 0 && text[0] == prefix) { - Optional.of(text.subSequence(1, text.length)) + ): Result = + if (text.isNotEmpty() && text[0] == prefix) { + Result.success(text.subSequence(1, text.length)) } else { - Optional.empty() + Result.failure(Exception("Prefix not found")) } // ------------------------------------------------------------------------- @@ -694,12 +693,9 @@ object AmountFormats { init { check(predicateStrs.size + 1 == text.size) { "Invalid word-based resource" } - predicates = - Stream - .of(*predicateStrs) - .map { predicateStr -> - findPredicate(predicateStr) - }.toArray { size -> arrayOfNulls(size) } + predicates = predicateStrs.map { predicateStr -> + findPredicate(predicateStr!!) + }.toTypedArray() this.text = text } @@ -723,8 +719,8 @@ object AmountFormats { } buf.append(value).append(text[predicates.size]) } - } + } // ------------------------------------------------------------------------- // data holder for a duration unit string and its associated Duration value. internal class DurationUnit constructor( @@ -740,7 +736,7 @@ object AmountFormats { // scale the unit by the input scalingFunction, returning a value if // one is produced, or an empty result when the operation results in an // arithmetic overflow. - fun scaleBy(scaleFunc: Function): Duration? = scaleFunc.apply(value) + fun scaleBy(scaleFunc: (Duration) -> Duration?): Duration? = scaleFunc(value) } // interface for computing a duration from a duration unit and a scalar. @@ -774,7 +770,7 @@ object AmountFormats { // data holder for the fractional floating point value of a duration // scalar. - internal class FractionScalarPart constructor( + internal class FractionScalarPart( private val value: Long, private val scale: Long, ) : DurationScalar { diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index 87ed3d6b..5bbc8932 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -39,7 +39,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.Json import java.text.SimpleDateFormat -import java.time.Duration import java.util.Currency import java.util.Date import java.util.Locale @@ -113,41 +112,37 @@ class DeviceHelper( private val daysSinceInstall: Int get() { - val fromDate = appInstallDate - val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() - val duration = Duration.between(fromInstant, toInstant) + val fromDate = org.threeten.bp.Instant.ofEpochMilli(appInstallDate.time) + val toDate = org.threeten.bp.Instant.now() + val duration = org.threeten.bp.Duration.between(fromDate, toDate) return duration.toDays().toInt() } private val minutesSinceInstall: Int get() { - val fromDate = appInstallDate - val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() - val duration = Duration.between(fromInstant, toInstant) + val fromDate = org.threeten.bp.Instant.ofEpochMilli(appInstallDate.time) + val toDate = org.threeten.bp.Instant.now() + val duration = org.threeten.bp.Duration.between(fromDate, toDate) return duration.toMinutes().toInt() } private val daysSinceLastPaywallView: Int? get() { - val fromDate = storage.read(LastPaywallView) ?: return null - val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() - val duration = Duration.between(fromInstant, toInstant) + val fromDate = + storage.read(LastPaywallView)?.let { org.threeten.bp.Instant.ofEpochMilli(it.time) } + ?: return null + val toDate = org.threeten.bp.Instant.now() + val duration = org.threeten.bp.Duration.between(fromDate, toDate) return duration.toDays().toInt() } private val minutesSinceLastPaywallView: Int? get() { - val fromDate = storage.read(LastPaywallView) ?: return null - val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() - val duration = Duration.between(fromInstant, toInstant) + val fromDate = + storage.read(LastPaywallView)?.let { org.threeten.bp.Instant.ofEpochMilli(it.time) } + ?: return null + val toDate = org.threeten.bp.Instant.now() + val duration = org.threeten.bp.Duration.between(fromDate, toDate) return duration.toMinutes().toInt() } @@ -238,12 +233,20 @@ class DeviceHelper( return "" } - val networkCapabilities = - connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - return when { - networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "Cellular" - networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "Wifi" - else -> "" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkCapabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + return when { + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "Cellular" + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "Wifi" + else -> "" + } + } else { + when(connectivityManager.activeNetworkInfo?.type){ + ConnectivityManager.TYPE_MOBILE -> return "Cellular" + ConnectivityManager.TYPE_WIFI -> return "Wifi" + else -> return "" + } } } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt index 07867e18..41d6203b 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt @@ -1,11 +1,11 @@ package com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator +import android.util.Base64 import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import kotlinx.serialization.SerializationException import org.json.JSONObject -import java.util.* data class LiquidExpressionEvaluatorParams( val expression: String, @@ -51,4 +51,4 @@ data class JavascriptExpressionEvaluatorParams( } } -fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) +fun ByteArray.toBase64(): String = Base64.encodeToString(this, Base64.DEFAULT) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt index 39095d35..c41fcba2 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt @@ -23,6 +23,7 @@ import android.widget.FrameLayout import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -497,6 +498,7 @@ class SuperwallPaywallActivity : AppCompatActivity() { return@suspendCoroutine } + createNotificationChannel() notificationPermissionCallback = @@ -517,20 +519,22 @@ class SuperwallPaywallActivity : AppCompatActivity() { } private fun createNotificationChannel() { - val importance = NotificationManager.IMPORTANCE_DEFAULT - val channel = - NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_NAME, - importance, - ).apply { - description = NOTIFICATION_CHANNEL_DESCRIPTION - } - channel.setShowBadge(false) - // Register the channel with the system - val notificationManager: NotificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = + NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_NAME, + importance, + ).apply { + description = NOTIFICATION_CHANNEL_DESCRIPTION + } + channel.setShowBadge(false) + // Register the channel with the system + val notificationManager: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } } private fun checkAndRequestNotificationPermissions( @@ -569,9 +573,11 @@ class SuperwallPaywallActivity : AppCompatActivity() { private fun areNotificationsEnabled(context: Context): Boolean { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val channel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) - if (channel?.importance == NotificationManager.IMPORTANCE_NONE) { - return false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) + if (channel?.importance == NotificationManager.IMPORTANCE_NONE) { + return false + } } return NotificationManagerCompat.from(context).areNotificationsEnabled() } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt index 22565c6b..7e30bcdb 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt @@ -1,6 +1,7 @@ package com.superwall.sdk.paywall.vc.web_view import android.graphics.Bitmap +import android.os.Build import android.webkit.RenderProcessGoneDetail import android.webkit.WebResourceError import android.webkit.WebResourceRequest @@ -82,11 +83,19 @@ internal open class DefaultWebviewClient( ioScope.launch { webviewClientEvents.emit( WebviewClientEvent.OnError( - WebviewError.NetworkError( - error.errorCode, - error.description.toString(), - forUrl, - ), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + WebviewError.NetworkError( + error.errorCode, + error.description.toString(), + forUrl, + ) + } else { + WebviewError.NetworkError( + -1, + "Error description unavailable, Android API version < 23", + forUrl, + ) + } ), ) } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt index f39be417..967dcdfb 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt @@ -12,8 +12,10 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.webkit.ConsoleMessage import android.webkit.RenderProcessGoneDetail +import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebView +import android.webkit.WebViewClient import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent @@ -75,6 +77,7 @@ class SWWebView( return true } } + private var lastWebViewClient : WebViewClient? = null internal fun prepareWebview() { addJavascriptInterface(messageHandler, "SWAndroid") @@ -122,7 +125,10 @@ class SWWebView( onCrashed = onRenderProcessCrashed, ) this.webViewClient = client - listenToWebviewClientEvents(this.webViewClient as DefaultWebviewClient) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + lastWebViewClient = client + } + listenToWebviewClientEvents(client) client.loadWithFallback() } @@ -152,12 +158,17 @@ class SWWebView( override fun loadUrl(url: String) { prepareWebview() - this.webViewClient = - DefaultWebviewClient( - forUrl = url, - ioScope = CoroutineScope(Dispatchers.IO), - onWebViewCrash = onRenderProcessCrashed, - ) + val client = DefaultWebviewClient( + forUrl = url, + ioScope = CoroutineScope(Dispatchers.IO), + onWebViewCrash = onRenderProcessCrashed, + ) + this.webViewClient = client + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + lastWebViewClient = client + } + listenToWebviewClientEvents(this.webViewClient as DefaultWebviewClient) // Parse the url and add the query parameter val uri = Uri.parse(url) @@ -186,7 +197,11 @@ class SWWebView( .takeWhile { mainScope .async { - webViewClient == client + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + webViewClient == client + } else { + lastWebViewClient == client + } }.await() }.collect { mainScope.launch { @@ -336,7 +351,11 @@ class SWWebView( internal fun webViewExists(): Boolean = try { - WebView.getCurrentWebViewPackage() != null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + WebView.getCurrentWebViewPackage()!=null + } else { + runCatching { CookieManager.getInstance() }.isSuccess + } } catch (e: Throwable) { false } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt index fd77c2d8..22e47978 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt @@ -1,7 +1,7 @@ package com.superwall.sdk.paywall.vc.web_view.messaging import TemplateLogic -import android.net.Uri +import android.util.Base64 import android.webkit.JavascriptInterface import android.webkit.WebView import com.superwall.sdk.Superwall @@ -30,7 +30,6 @@ import kotlinx.serialization.json.Json import org.json.JSONObject import java.net.URI import java.nio.charset.StandardCharsets -import java.util.Base64 import java.util.Date import java.util.LinkedList import java.util.Queue @@ -203,7 +202,7 @@ class PaywallMessageHandler( // Encode the JSON string to Base64 val base64Event = - encoder.encodeToString(jsonString.toByteArray(StandardCharsets.UTF_8)) + Base64.encodeToString(jsonString.toByteArray(StandardCharsets.UTF_8), Base64.DEFAULT) passMessageToWebView(base64String = base64Event) } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt index e1957a04..f15cc7aa 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt @@ -7,14 +7,11 @@ import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.paywall.vc.web_view.templating.models.FreeTrialTemplate import com.superwall.sdk.paywall.vc.web_view.templating.models.JsonVariables import com.superwall.sdk.paywall.view_controller.web_view.templating.models.ProductTemplate -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import java.util.* object TemplateLogic { suspend fun getBase64EncodedTemplates( json: Json, - base64: Base64.Encoder, paywall: Paywall, event: EventData?, factory: VariablesFactory, @@ -59,7 +56,7 @@ object TemplateLogic { "!!! Template Logic: $templatesString", ) - return base64.encodeToString(templatesData) + return android.util.Base64.encodeToString(templatesData, android.util.Base64.DEFAULT) } // private fun swProductTemplate( diff --git a/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt b/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt index fecc7473..20969710 100644 --- a/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt @@ -1,7 +1,7 @@ package com.superwall.sdk.storage.core_data import android.content.Context -import android.icu.util.Calendar +import java.util.Calendar import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt index 8e714a81..e93113ef 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt @@ -8,7 +8,7 @@ import com.superwall.sdk.utilities.dateFormat import kotlinx.serialization.Transient import java.math.BigDecimal import java.math.RoundingMode -import java.time.Period +import org.threeten.bp.Period import java.util.Calendar import java.util.Currency import java.util.Locale diff --git a/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt new file mode 100644 index 00000000..6380c0a6 --- /dev/null +++ b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt @@ -0,0 +1,215 @@ +package com.superwall.sdk.contrib + +import com.superwall.sdk.Given +import com.superwall.sdk.Then +import com.superwall.sdk.When +import com.superwall.sdk.contrib.threeteen.AmountFormats +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test +import org.threeten.bp.Duration +import org.threeten.bp.Period +import org.threeten.bp.format.DateTimeParseException +import java.util.* + +class AmountFormatsTest { + + @Test + fun testIso8601Format() { + Given("a period and duration") { + val period = Period.of(1, 2, 3) + val duration = Duration.ofHours(4).plusMinutes(5).plusSeconds(6) + + When("formatting to ISO-8601") { + val result = AmountFormats.iso8601(period, duration) + + Then("it should return the correct ISO-8601 string") { + assertEquals("P1Y2M3DT4H5M6S", result) + } + } + } + + Given("a zero period and non-zero duration") { + val period = Period.ZERO + val duration = Duration.ofMinutes(30) + + When("formatting to ISO-8601") { + val result = AmountFormats.iso8601(period, duration) + + Then("it should return only the duration string") { + assertEquals("PT30M", result) + } + } + } + + Given("a non-zero period and zero duration") { + val period = Period.ofMonths(3) + val duration = Duration.ZERO + + When("formatting to ISO-8601") { + val result = AmountFormats.iso8601(period, duration) + + Then("it should return only the period string") { + assertEquals("P3M", result) + } + } + } + } + + @Test + fun testWordBasedPeriod() { + Given("a period with multiple units") { + val period = Period.of(2, 3, 15) + + When("formatting to word-based with English locale") { + val result = AmountFormats.wordBased(period, Locale.ENGLISH) + + Then("it should return the correct word-based string") { + assertEquals("2 years, 3 months and 15 days", result) + } + } + } + + Given("a period with opposite signs") { + val period = Period.of(1, -2, 0) + + When("formatting to word-based") { + val result = AmountFormats.wordBased(period, Locale.ENGLISH) + + Then("it should normalize the period") { + assertEquals("10 months", result) + } + } + } + } + + @Test + fun testWordBasedDuration() { + Given("a duration with multiple units") { + val duration = Duration.ofHours(25).plusMinutes(30).plusSeconds(45).plusMillis(500) + + When("formatting to word-based with English locale") { + val result = AmountFormats.wordBased(duration, Locale.ENGLISH) + + Then("it should return the correct word-based string") { + assertEquals("25 hours, 30 minutes, 45 seconds and 500 milliseconds", result) + } + } + } + } + + @Test + fun testWordBasedPeriodAndDuration() { + Given("a period and duration with multiple units") { + val period = Period.of(1, 2, 3) + val duration = Duration.ofHours(4).plusMinutes(5).plusSeconds(6) + + When("formatting to word-based with English locale") { + val result = AmountFormats.wordBased(period, duration, Locale.ENGLISH) + + Then("it should return the correct word-based string") { + assertEquals("1 year, 2 months, 3 days, 4 hours, 5 minutes and 6 seconds", result) + } + } + } + } + + @Test + fun testParseUnitBasedDuration() { + Given("a valid duration string") { + val durationString = "2h45m30s" + + When("parsing the unit-based duration") { + val result = AmountFormats.parseUnitBasedDuration(durationString) + + Then("it should return the correct Duration") { + assertEquals(Duration.ofHours(2).plusMinutes(45).plusSeconds(30), result) + } + } + } + + Given("a duration string with a negative value") { + val durationString = "-1.5h" + + When("parsing the unit-based duration") { + val result = AmountFormats.parseUnitBasedDuration(durationString) + + Then("it should return the correct negative Duration") { + assertEquals(Duration.ofMinutes(-90), result) + } + } + } + + Given("a duration string with mixed units") { + val durationString = "2h30m500ms" + + When("parsing the unit-based duration") { + val result = AmountFormats.parseUnitBasedDuration(durationString) + + Then("it should return the correct Duration") { + assertEquals(Duration.ofHours(2).plusMinutes(30).plusMillis(500), result) + } + } + } + + Given("an invalid duration string") { + val durationString = "2h30x" + + When("parsing the unit-based duration") { + Then("it should throw a DateTimeParseException") { + assertThrows(DateTimeParseException::class.java) { + AmountFormats.parseUnitBasedDuration(durationString) + } + } + } + } + + Given("a duration string with an empty value") { + val durationString = "" + + When("parsing the unit-based duration") { + Then("it should throw a DateTimeParseException") { + assertThrows(DateTimeParseException::class.java) { + AmountFormats.parseUnitBasedDuration(durationString) + } + } + } + } + + Given("a duration string with only a zero") { + val durationString = "0" + + When("parsing the unit-based duration") { + val result = AmountFormats.parseUnitBasedDuration(durationString) + + Then("it should return Duration.ZERO") { + assertEquals(Duration.ZERO, result) + } + } + } + + Given("a duration string with a very large value") { + val durationString = "9223372036854775807ns" + + When("parsing the unit-based duration") { + val result = AmountFormats.parseUnitBasedDuration(durationString) + + Then("it should return the correct Duration") { + assertEquals(Duration.ofNanos(9223372036854775807), result) + } + } + } + + Given("a duration string that exceeds the valid range") { + val durationString = "9223372036854775808ns" + + When("parsing the unit-based duration") { + Then("it should throw a DateTimeParseException") { + assertThrows(DateTimeParseException::class.java) { + AmountFormats.parseUnitBasedDuration(durationString) + } + } + } + } + } +} \ No newline at end of file diff --git a/superwall/src/test/java/com/superwall/sdk/utils.kt b/superwall/src/test/java/com/superwall/sdk/utils.kt index 740afb21..78a4a6b6 100644 --- a/superwall/src/test/java/com/superwall/sdk/utils.kt +++ b/superwall/src/test/java/com/superwall/sdk/utils.kt @@ -1,3 +1,4 @@ +@file:Suppress("ktlint:standard:function-naming") package com.superwall.sdk fun assertTrue(value: Boolean) { @@ -11,3 +12,56 @@ fun assertFalse(value: Boolean) { throw AssertionError("Expected false, got true") } } + + +@DslMarker annotation class TestingDSL + +class GivenWhenThenScope( + val text: MutableList, +) + +@TestingDSL +inline fun Given( + what: String, + block: GivenWhenThenScope.() -> Unit, +) { + val scope = GivenWhenThenScope(mutableListOf("Given $what")) + try { + block(scope) + } catch (e: Throwable) { + e.printStackTrace() + println(scope.text.joinToString("\n")) + throw e + } +} + +@TestingDSL +inline fun GivenWhenThenScope.When( + what: String, + block: GivenWhenThenScope.() -> T, +): T { + text.add("\tWhen $what") + try { + return block(this) + } catch (e: Throwable) { + throw e + } +} + +@TestingDSL +inline fun GivenWhenThenScope.Then( + what: String, + block: GivenWhenThenScope.() -> Unit, +) { + text.add("\t\tThen $what") + block() +} + +@TestingDSL +inline fun GivenWhenThenScope.And( + what: String, + block: GivenWhenThenScope.() -> Unit, +) { + text.add("\t\t\tAnd $what") + block() +} From 44c05c71b0c7c20e2278fd74f367b2ac12a998f7 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 14 Oct 2024 14:59:19 +0200 Subject: [PATCH 04/37] Add purchase and basic restore method --- .../main/java/com/superwall/sdk/Superwall.kt | 68 +- .../models/transactions/SavedTransaction.kt | 23 + .../sdk/paywall/presentation/PaywallInfo.kt | 45 +- .../com/superwall/sdk/storage/CacheKeys.kt | 10 + .../store/coordinator/CoordinatorProtocols.kt | 5 + .../store/transactions/TransactionManager.kt | 695 ++++++++++++------ .../superwall/sdk/utilities/ErrorTracking.kt | 4 + 7 files changed, 609 insertions(+), 241 deletions(-) create mode 100644 superwall/src/main/java/com/superwall/sdk/models/transactions/SavedTransaction.kt diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index be3d937a..916b95ba 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -10,6 +10,7 @@ import com.superwall.sdk.analytics.superwall.SuperwallEventInfo 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.PurchaseResult import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.delegate.SuperwallDelegate import com.superwall.sdk.delegate.SuperwallDelegateJava @@ -50,6 +51,8 @@ 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.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 @@ -683,6 +686,63 @@ class Superwall( } } + /** + *Initiates a purchase of a `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): PurchaseResult = + dependencyContainer.transactionManager.purchase( + TransactionManager.PurchaseSource.External( + product, + ), + ) + + /** + * Initiates a purchase of a `StoreProduct` with a callback. + * + * 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` in `onFinished`. You'll see the data associated with the + * purchase on the Superwall dashboard. + * + * @param product: The `StoreProduct` you wish to purchase. + * @param onFinished: A callback that will receive the `PurchaseResult`. + * - Note: You do not need to finish the transaction yourself after this. + * ``Superwall`` will handle this for you. + */ + + fun purchase( + product: StoreProduct, + onFinished: (PurchaseResult) -> Unit, + ) { + ioScope.launch { + val res = + dependencyContainer.transactionManager.purchase( + TransactionManager.PurchaseSource.External( + product, + ), + ) + onFinished(res) + } + } + + /** + * Restores purchases + * + * Use this function to restore purchases made by the user. + * */ + suspend fun restorePurchases() = dependencyContainer.transactionManager.tryToRestorePurchases(null) + override suspend fun eventDidOccur( paywallEvent: PaywallWebEvent, paywallView: PaywallView, @@ -713,8 +773,10 @@ class Superwall( launch { try { dependencyContainer.transactionManager.purchase( - paywallEvent.productId, - paywallView, + TransactionManager.PurchaseSource.Internal( + paywallEvent.productId, + paywallView, + ), ) } finally { // Ensure the task is cleared once the purchase is complete or if an error occurs @@ -724,7 +786,7 @@ class Superwall( } is InitiateRestore -> { - dependencyContainer.transactionManager.tryToRestore(paywallView) + dependencyContainer.transactionManager.tryToRestorePurchases(paywallView) } is OpenedURL -> { diff --git a/superwall/src/main/java/com/superwall/sdk/models/transactions/SavedTransaction.kt b/superwall/src/main/java/com/superwall/sdk/models/transactions/SavedTransaction.kt new file mode 100644 index 00000000..42fff803 --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/models/transactions/SavedTransaction.kt @@ -0,0 +1,23 @@ +package com.superwall.sdk.models.transactions + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * The transaction to save to storage + * @param id The id of the transaction + * @param date The date of the transaction as unix epoch time + * @param hasExternalPurchaseController Whether the transaction has an external purchase controller + * @param isExternal Whether the transaction is external + **/ +@Serializable +class SavedTransaction( + @SerialName("id") + val id: String, + @SerialName("date") + val date: Long, + @SerialName("hasExternalPurchaseController") + val hasExternalPurchaseController: Boolean, + @SerialName("isExternal") + val isExternal: Boolean, +) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index 90b11793..eda55321 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -11,6 +11,8 @@ import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.paywall.LocalNotification import com.superwall.sdk.models.paywall.PaywallPresentationInfo import com.superwall.sdk.models.paywall.PaywallURL +import com.superwall.sdk.models.paywall.PaywallPresentationStyle +import com.superwall.sdk.models.paywall.PresentationCondition import com.superwall.sdk.models.product.Product import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.triggers.Experiment @@ -292,7 +294,48 @@ data class PaywallInfo( return output.filter { (_, value) -> value != null } as MutableMap } - private companion object { + companion object { private val json = Json { } + + fun empty() = + PaywallInfo( + databaseId = "", + identifier = "", + name = "", + url = PaywallURL(""), + experiment = null, + triggerSessionId = "", + products = emptyList(), + productItems = emptyList(), + productIds = emptyList(), + presentedByEventWithName = null, + presentedByEventWithId = null, + presentedByEventAt = null, + presentedBy = "", + presentationSourceType = null, + responseLoadStartTime = null, + responseLoadCompleteTime = null, + responseLoadFailTime = null, + responseLoadDuration = null, + webViewLoadStartTime = null, + webViewLoadCompleteTime = null, + webViewLoadFailTime = null, + webViewLoadDuration = null, + productsLoadStartTime = null, + productsLoadCompleteTime = null, + productsLoadFailTime = null, + productsLoadDuration = null, + paywalljsVersion = null, + isFreeTrialAvailable = false, + featureGatingBehavior = FeatureGatingBehavior.NonGated, + closeReason = PaywallCloseReason.None, + localNotifications = emptyList(), + computedPropertyRequests = emptyList(), + surveys = emptyList(), + presentation = PaywallPresentationInfo(PaywallPresentationStyle.NONE, PresentationCondition.ALWAYS, 0), + buildId = "", + cacheKey = "", + isScrollEnabled = true + ) } } diff --git a/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt b/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt index a5fd1d88..5ed49a5f 100644 --- a/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt +++ b/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt @@ -5,6 +5,7 @@ import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.models.config.Config import com.superwall.sdk.models.geo.GeoInfo import com.superwall.sdk.models.serialization.AnySerializer +import com.superwall.sdk.models.transactions.SavedTransaction import com.superwall.sdk.models.triggers.Experiment import com.superwall.sdk.models.triggers.ExperimentID import com.superwall.sdk.store.abstractions.transactions.StoreTransaction @@ -15,6 +16,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException import kotlinx.serialization.Serializer import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor @@ -269,6 +271,14 @@ internal object LatestGeoInfo : Storable { get() = GeoInfo.serializer() } +internal object SavedTransactions : Storable> { + override val key: String + get() = "store.savedTransactions" + override val directory: SearchPathDirectory + get() = SearchPathDirectory.APP_SPECIFIC_DOCUMENTS + override val serializer: KSerializer> + get() = SetSerializer(SavedTransaction.serializer()) +} //endregion // region Serializers diff --git a/superwall/src/main/java/com/superwall/sdk/store/coordinator/CoordinatorProtocols.kt b/superwall/src/main/java/com/superwall/sdk/store/coordinator/CoordinatorProtocols.kt index ec74072d..a062047a 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/coordinator/CoordinatorProtocols.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/coordinator/CoordinatorProtocols.kt @@ -21,3 +21,8 @@ interface TransactionRestorer { // obtaining the restored transactions suspend fun restorePurchases(): RestorationResult } + +interface Purchasing : + ProductPurchaser, + ProductsFetcher, + TransactionRestorer diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index 8fb62a0a..8e4f479e 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -22,6 +22,7 @@ import com.superwall.sdk.logger.Logger import com.superwall.sdk.misc.ActivityProvider import com.superwall.sdk.misc.launchWithTracking import com.superwall.sdk.models.paywall.LocalNotificationType +import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.sdk.paywall.presentation.internal.dismiss import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult import com.superwall.sdk.paywall.vc.PaywallView @@ -31,6 +32,7 @@ import com.superwall.sdk.storage.EventsQueue import com.superwall.sdk.store.StoreKitManager import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -44,6 +46,19 @@ class TransactionManager( private val factory: Factory, private val context: Context, ) { + sealed class PurchaseSource { + data class Internal( + val productId: String, + val paywallView: PaywallView, + ) : PurchaseSource() + + data class External( + val product: StoreProduct, + ) : PurchaseSource() + } + + val scope = CoroutineScope(Dispatchers.IO) + interface Factory : OptionsFactory, TriggerFactory, @@ -54,19 +69,23 @@ class TransactionManager( private var lastPaywallView: PaywallView? = null - suspend fun purchase( - productId: String, - paywallView: PaywallView, - ) { + suspend fun purchase(purchaseSource: PurchaseSource): PurchaseResult { val product = - storeKitManager.productsByFullId[productId] ?: run { - Logger.debug( - logLevel = LogLevel.error, - scope = LogScope.paywallTransactions, - message = - "Trying to purchase ($productId) but the product has failed to load. Visit https://superwall.com/l/missing-products to diagnose.", - ) - return + when (purchaseSource) { + is PurchaseSource.Internal -> + storeKitManager.productsByFullId[purchaseSource.productId] ?: run { + Logger.debug( + logLevel = LogLevel.error, + scope = LogScope.paywallTransactions, + message = + "Trying to purchase (${purchaseSource.productId}) but the product has failed to load. Visit https://superwall.com/l/missing-products to diagnose.", + ) + return PurchaseResult.Failed("Product not found") + } + + is PurchaseSource.External -> { + purchaseSource.product + } } val rawStoreProduct = product.rawStoreProduct @@ -76,9 +95,11 @@ class TransactionManager( "!!! Purchasing product ${rawStoreProduct.hasFreeTrial}", ) val productDetails = rawStoreProduct.underlyingProductDetails - val activity = activityProvider.getCurrentActivity() ?: return + val activity = + activityProvider.getCurrentActivity() + ?: return PurchaseResult.Failed("Activity not found - required for starting the billing flow") - prepareToStartTransaction(product, paywallView) + prepareToPurchase(product, purchaseSource) val result = storeKitManager.purchaseController.purchase( @@ -88,15 +109,17 @@ class TransactionManager( basePlanId = rawStoreProduct.basePlanId, ) + val isEligibleForTrial = rawStoreProduct.selectedOffer != null + when (result) { is PurchaseResult.Purchased -> { - didPurchase(product, paywallView) + didPurchase(product, purchaseSource, isEligibleForTrial) } is PurchaseResult.Restored -> { didRestore( product = product, - paywallView = paywallView, + purchaseSource = purchaseSource, ) } @@ -112,36 +135,39 @@ class TransactionManager( trackFailure( result.errorMessage, product, - paywallView, + purchaseSource, ) presentAlert( Error(result.errorMessage), product, - paywallView, + purchaseSource, ) } else { trackFailure( result.errorMessage, product, - paywallView, + purchaseSource, ) - return paywallView.togglePaywallSpinner(isHidden = true) + if (purchaseSource is PurchaseSource.Internal) { + purchaseSource.paywallView.togglePaywallSpinner(isHidden = true) + } } } is PurchaseResult.Pending -> { - handlePendingTransaction(paywallView) + handlePendingTransaction(purchaseSource) } is PurchaseResult.Cancelled -> { - trackCancelled(product, paywallView) + trackCancelled(product, purchaseSource) } } + return result } private suspend fun didRestore( product: StoreProduct? = null, - paywallView: PaywallView, + purchaseSource: PurchaseSource, ) { val purchasingCoordinator = factory.makeTransactionVerifier() var transaction: StoreTransaction? @@ -159,206 +185,351 @@ class TransactionManager( restoreType = RestoreType.ViaRestore } - val paywallInfo = paywallView.info - val trackedEvent = InternalSuperwallEvent.Transaction( state = InternalSuperwallEvent.Transaction.State.Restore(restoreType), - paywallInfo = paywallInfo, + paywallInfo = if (purchaseSource is PurchaseSource.Internal) purchaseSource.paywallView.info else PaywallInfo.empty(), product = product, model = null, ) Superwall.instance.track(trackedEvent) val superwallOptions = factory.makeSuperwallOptions() - if (superwallOptions.paywalls.automaticallyDismiss) { - Superwall.instance.dismiss(paywallView, result = PaywallResult.Restored()) + if (superwallOptions.paywalls.automaticallyDismiss && purchaseSource is PurchaseSource.Internal) { + Superwall.instance.dismiss( + purchaseSource.paywallView, + result = PaywallResult.Restored(), + ) } } private fun trackFailure( errorMessage: String, product: StoreProduct, - paywallView: PaywallView, + purchaseSource: PurchaseSource, ) { - // Log the error - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.paywallTransactions, - message = "Transaction Error: $errorMessage", - info = - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to paywallView, - ), - ) + when (purchaseSource) { + is PurchaseSource.Internal -> { + // Log the error + Logger.debug( + logLevel = LogLevel.debug, + scope = LogScope.paywallTransactions, + message = "Transaction Error: $errorMessage", + info = + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to purchaseSource.paywallView, + ), + ) - // Launch a coroutine to handle async tasks - factory.ioScope().launchWithTracking { - val paywallInfo = paywallView.info - val trackedEvent = - InternalSuperwallEvent.Transaction( - state = - InternalSuperwallEvent.Transaction.State.Fail( - TransactionError.Failure( - errorMessage, - product, + factory.ioScope().launchWithTracking { + val paywallInfo = purchaseSource.paywallView.info + val trackedEvent = + InternalSuperwallEvent.Transaction( + state = + InternalSuperwallEvent.Transaction.State.Fail( + TransactionError.Failure( + errorMessage, + product, + ), ), - ), - paywallInfo = paywallInfo, - product = product, - model = null, + paywallInfo = paywallInfo, + product = product, + model = null, + ) + + Superwall.instance.track(trackedEvent) + + } + } + + is PurchaseSource.External -> { + Logger.debug( + logLevel = LogLevel.debug, + scope = LogScope.paywallTransactions, + message = "Transaction Error: $errorMessage", + info = + mapOf( + "product_id" to product.fullIdentifier, + ), ) + factory.ioScope().launch { + val trackedEvent = + InternalSuperwallEvent.Transaction( + state = + InternalSuperwallEvent.Transaction.State.Fail( + TransactionError.Failure( + errorMessage, + product, + ), + ), + paywallInfo = PaywallInfo.empty(), + product = product, + model = null, + ) - // Assuming Superwall.instance.track and sessionEventsManager.triggerSession.trackTransactionError are suspend functions - Superwall.instance.track(trackedEvent) + Superwall.instance.track(trackedEvent) + } + } } } - private suspend fun prepareToStartTransaction( + private suspend fun prepareToPurchase( product: StoreProduct, - paywallView: PaywallView, + source: PurchaseSource, ) { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Purchasing", - mapOf("paywall_vc" to paywallView), - null, - ) - } + when (source) { + is PurchaseSource.Internal -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Purchasing", + mapOf("paywall_vc" to source), + null, + ) + } - val paywallInfo = paywallView.info - val trackedEvent = - InternalSuperwallEvent.Transaction( - InternalSuperwallEvent.Transaction.State.Start(product), - paywallInfo, - product, - null, - ) - Superwall.instance.track(trackedEvent) + val paywallInfo = source.paywallView.info + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Start(product), + paywallInfo, + product, + null, + ) + Superwall.instance.track(trackedEvent) - withContext(Dispatchers.Main) { - paywallView.loadingState = PaywallLoadingState.LoadingPurchase() - } + withContext(Dispatchers.Main) { + source.paywallView.loadingState = PaywallLoadingState.LoadingPurchase() + } + + lastPaywallView = source.paywallView + } + + is PurchaseSource.External -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "External Transaction Purchasing", + null, + ) + } - lastPaywallView = paywallView + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Start(product), + PaywallInfo.empty(), + product, + null, + ) + Superwall.instance.track(trackedEvent) + } + } } - // ... Remaining functions translated in a similar fashion ... private suspend fun didPurchase( product: StoreProduct, - paywallView: PaywallView, + purchaseSource: PurchaseSource, + didStartFreeTrial: Boolean, ) { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Succeeded", - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to paywallView, - ), - null, - ) - } + when (purchaseSource) { + is PurchaseSource.Internal -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Succeeded", + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to purchaseSource.paywallView, + ), + null, + ) + } - val transactionVerifier = factory.makeTransactionVerifier() - val transaction = - transactionVerifier.getLatestTransaction( - factory = factory, - ) + val transactionVerifier = factory.makeTransactionVerifier() + val transaction = + transactionVerifier.getLatestTransaction( + factory = factory, + ) - transaction?.let { - sessionEventsManager.enqueue(it) - } + transaction?.let { + sessionEventsManager.enqueue(it) + } - storeKitManager.loadPurchasedProducts() + storeKitManager.loadPurchasedProducts() - trackTransactionDidSucceed(transaction, product) + trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) - if (Superwall.instance.options.paywalls.automaticallyDismiss) { - Superwall.instance.dismiss( - paywallView, - PaywallResult.Purchased(product.fullIdentifier), - ) + if (Superwall.instance.options.paywalls.automaticallyDismiss) { + Superwall.instance.dismiss( + purchaseSource.paywallView, + PaywallResult.Purchased(product.fullIdentifier), + ) + } + } + + is PurchaseSource.External -> { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Succeeded", + mapOf( + "product_id" to product.fullIdentifier, + ), + null, + ) + val transactionVerifier = factory.makeTransactionVerifier() + val transaction = + transactionVerifier.getLatestTransaction( + factory = factory, + ) + + transaction?.let { + sessionEventsManager.enqueue(it) + } + + storeKitManager.loadPurchasedProducts() + + trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) + } } } private suspend fun trackCancelled( product: StoreProduct, - paywallView: PaywallView, + purchaseSource: PurchaseSource, ) { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Abandoned", - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to paywallView, - ), - null, - ) - } + when (purchaseSource) { + is PurchaseSource.Internal -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Abandoned", + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to purchaseSource.paywallView, + ), + null, + ) + } - val paywallInfo = paywallView.info - val trackedEvent = - InternalSuperwallEvent.Transaction( - InternalSuperwallEvent.Transaction.State.Abandon(product), - paywallInfo, - product, - null, - ) - Superwall.instance.track(trackedEvent) + val paywallInfo = purchaseSource.paywallView.info + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Abandon(product), + paywallInfo, + product, + null, + ) + Superwall.instance.track(trackedEvent) - withContext(Dispatchers.Main) { - paywallView.loadingState = PaywallLoadingState.Ready() + withContext(Dispatchers.Main) { + purchaseSource.paywallView.loadingState = PaywallLoadingState.Ready() + } + } + + is PurchaseSource.External -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Abandoned", + mapOf( + "product_id" to product.fullIdentifier, + ), + null, + ) + } + + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Abandon(product), + PaywallInfo.empty(), + product, + null, + ) + Superwall.instance.track(trackedEvent) + } } } - private suspend fun handlePendingTransaction(paywallView: PaywallView) { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Pending", - mapOf("paywall_vc" to paywallView), - null, - ) - } + private suspend fun handlePendingTransaction(purchaseSource: PurchaseSource) { + when (purchaseSource) { + is PurchaseSource.Internal -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Pending", + mapOf("paywall_vc" to purchaseSource.paywallView), + null, + ) + } - val paywallInfo = paywallView.info + val paywallInfo = purchaseSource.paywallView.info - val trackedEvent = - InternalSuperwallEvent.Transaction( - InternalSuperwallEvent.Transaction.State.Fail(TransactionError.Pending("Needs parental approval")), - paywallInfo, - null, - null, - ) - Superwall.instance.track(trackedEvent) + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Fail(TransactionError.Pending("Needs parental approval")), + paywallInfo, + null, + null, + ) + Superwall.instance.track(trackedEvent) - paywallView.showAlert( - "Waiting for Approval", - "Thank you! This purchase is pending approval from your parent. Please try again once it is approved.", - ) + purchaseSource.paywallView.showAlert( + "Waiting for Approval", + "Thank you! This purchase is pending approval from your parent. Please try again once it is approved.", + ) + } + + is PurchaseSource.External -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Pending", + null, + ) + } + + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Fail(TransactionError.Pending("Needs parental approval")), + PaywallInfo.empty(), + null, + null, + ) + Superwall.instance.track(trackedEvent) + } + } } - suspend fun tryToRestore(paywallView: PaywallView) { + /** + * Attempt to restore purchases. + * + * @param paywallView The paywall view that initiated the restore or null if initiated externally. + * @return A [RestorationResult] indicating the result of the restoration. + */ + suspend fun tryToRestorePurchases(paywallView: PaywallView?): RestorationResult { Logger.debug( logLevel = LogLevel.debug, scope = LogScope.paywallTransactions, message = "Attempting Restore", ) - paywallView.loadingState = PaywallLoadingState.LoadingPurchase() + val paywallInfo = paywallView?.info ?: PaywallInfo.empty() + + paywallView?.loadingState = PaywallLoadingState.LoadingPurchase() Superwall.instance.track( InternalSuperwallEvent.Restore( state = InternalSuperwallEvent.Restore.State.Start, - paywallInfo = paywallView.info, + paywallInfo = paywallInfo, ), ) val restorationResult = purchaseController.restorePurchases() @@ -376,15 +547,17 @@ class TransactionManager( Superwall.instance.track( InternalSuperwallEvent.Restore( state = InternalSuperwallEvent.Restore.State.Complete, - paywallInfo = paywallView.info, + paywallInfo = paywallView?.info ?: PaywallInfo.empty(), ), ) - didRestore(paywallView = paywallView) + if (paywallView != null) { + didRestore(null, PurchaseSource.Internal("", paywallView)) + } } else { val msg = "Transactions Failed to Restore.${ if (hasRestored && !isUserSubscribed) { " The user's subscription status is \"inactive\", but the restoration result is \"restored\"." + - " Ensure the subscription status is active before confirming successful restoration." + " Ensure the subscription status is active before confirming successful restoration." } else { " Original restoration error message: ${ when (restorationResult) { @@ -403,111 +576,159 @@ class TransactionManager( Superwall.instance.track( InternalSuperwallEvent.Restore( state = InternalSuperwallEvent.Restore.State.Failure(msg), - paywallInfo = paywallView.info, + paywallInfo = paywallView?.info ?: PaywallInfo.empty(), ), ) - paywallView.showAlert( + paywallView?.showAlert( title = Superwall.instance.options.paywalls.restoreFailed.title, message = Superwall.instance.options.paywalls.restoreFailed.message, closeActionTitle = Superwall.instance.options.paywalls.restoreFailed.closeButtonTitle, ) } + return restorationResult } private suspend fun presentAlert( error: Error, product: StoreProduct, - paywallView: PaywallView, + source: PurchaseSource, ) { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Error", - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to paywallView, - ), - error, - ) - } + when (source) { + is PurchaseSource.Internal -> { + factory.ioScope().launch { + Logger.debug( + LogLevel.debug, + LogScope.paywallTransactions, + "Transaction Error", + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to source.paywallView, + ), + error, + ) + } - val paywallInfo = paywallView.info + val paywallInfo = source.paywallView.info - val trackedEvent = - InternalSuperwallEvent.Transaction( - InternalSuperwallEvent.Transaction.State.Fail( - TransactionError.Failure( - error.message ?: "", + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Fail( + TransactionError.Failure( + error.message ?: "", + product, + ), + ), + paywallInfo, product, - ), - ), - paywallInfo, - product, - null, - ) - Superwall.instance.track(trackedEvent) + null, + ) + Superwall.instance.track(trackedEvent) - paywallView.showAlert( - "An error occurred", - error.message ?: "Unknown error", - ) + source.paywallView.showAlert( + "An error occurred", + error.message ?: "Unknown error", + ) + } + + is PurchaseSource.External -> { + // TODO: Implement displaying an alert here + } + } } - // ... and so on for the other methods ... private suspend fun trackTransactionDidSucceed( transaction: StoreTransaction?, product: StoreProduct, + purchaseSource: PurchaseSource, + didStartFreeTrial: Boolean, ) { - val paywallView = lastPaywallView ?: return + when (purchaseSource) { + is PurchaseSource.Internal -> { + val paywallView = lastPaywallView ?: return - val paywallShowingFreeTrial = paywallView.paywall.isFreeTrialAvailable == true - val didStartFreeTrial = product.hasFreeTrial && paywallShowingFreeTrial + val paywallShowingFreeTrial = paywallView.paywall.isFreeTrialAvailable == true + val didStartFreeTrial = product.hasFreeTrial && paywallShowingFreeTrial - val paywallInfo = paywallView.info + val paywallInfo = paywallView.info - val trackedEvent = - InternalSuperwallEvent.Transaction( - InternalSuperwallEvent.Transaction.State.Complete(product, transaction), - paywallInfo, - product, - transaction, - ) - Superwall.instance.track(trackedEvent) + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Complete(product, transaction), + paywallInfo, + product, + transaction, + ) + Superwall.instance.track(trackedEvent) + + // Immediately flush the events queue on transaction complete. + eventsQueue.flushInternal() + + if (product.subscriptionPeriod == null) { + val nonRecurringEvent = + InternalSuperwallEvent.NonRecurringProductPurchase( + paywallInfo, + product, + ) + Superwall.instance.track(nonRecurringEvent) + } else { + if (didStartFreeTrial) { + val freeTrialEvent = + InternalSuperwallEvent.FreeTrialStart(paywallInfo, product) + Superwall.instance.track(freeTrialEvent) + + val notifications = + paywallInfo.localNotifications.filter { it.type == LocalNotificationType.TrialStarted } + val paywallActivity = + paywallView.encapsulatingActivity?.get() as? SuperwallPaywallActivity + ?: return + paywallActivity.attemptToScheduleNotifications( + notifications = notifications, + factory = factory, + context = context, + ) + } else { + val subscriptionEvent = + InternalSuperwallEvent.SubscriptionStart(paywallInfo, product) + Superwall.instance.track(subscriptionEvent) + } + } - // Immediately flush the events queue on transaction complete. - eventsQueue.flushInternal() + lastPaywallView = null + } - if (product.subscriptionPeriod == null) { - val nonRecurringEvent = - InternalSuperwallEvent.NonRecurringProductPurchase( - paywallInfo, - product, - ) - Superwall.instance.track(nonRecurringEvent) - } else { - if (didStartFreeTrial) { - val freeTrialEvent = InternalSuperwallEvent.FreeTrialStart(paywallInfo, product) - Superwall.instance.track(freeTrialEvent) - - val notifications = - paywallInfo.localNotifications.filter { it.type == LocalNotificationType.TrialStarted } - val paywallActivity = - paywallView.encapsulatingActivity?.get() as? SuperwallPaywallActivity - ?: return - paywallActivity.attemptToScheduleNotifications( - notifications = notifications, - factory = factory, - context = context, - ) - } else { - val subscriptionEvent = - InternalSuperwallEvent.SubscriptionStart(paywallInfo, product) - Superwall.instance.track(subscriptionEvent) + is PurchaseSource.External -> { + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Complete(product, transaction), + PaywallInfo.empty(), + product, + transaction, + ) + Superwall.instance.track(trackedEvent) + eventsQueue.flushInternal() + if (product.subscriptionPeriod == null) { + val nonRecurringEvent = + InternalSuperwallEvent.NonRecurringProductPurchase( + PaywallInfo.empty(), + product, + ) + Superwall.instance.track(nonRecurringEvent) + } else { + if (didStartFreeTrial) { + val freeTrialEvent = + InternalSuperwallEvent.FreeTrialStart(PaywallInfo.empty(), product) + Superwall.instance.track(freeTrialEvent) + } else { + val subscriptionEvent = + InternalSuperwallEvent.SubscriptionStart( + PaywallInfo.empty(), + product, + ) + Superwall.instance.track(subscriptionEvent) + } + } } } - - lastPaywallView = null } } diff --git a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt index 690f158a..71ff17ab 100644 --- a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt +++ b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt @@ -1,5 +1,6 @@ package com.superwall.sdk.utilities +import android.util.Log import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent @@ -57,6 +58,9 @@ internal class ErrorTracker( val exists = cache.read(ErrorLog) if (exists != null) { scope.launch { + Log.e("ErrorTracker", "Error occurred in the SDK") + Log.e("ErrorTracker", exists.message) + Log.e("ErrorTracker", "\n\n ------------------------- \n ${exists.stacktrace} \n\n -------------------------\n") track( InternalSuperwallEvent.ErrorThrown( exists.message, From 000fd4e982c9864a2602d70db7b29b03d9cfa388 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 24 Oct 2024 12:51:05 +0200 Subject: [PATCH 05/37] Extract interfaces properly for transaction code, add tests --- .../main/java/com/superwall/sdk/Superwall.kt | 11 +- .../java/com/superwall/sdk/billing/Billing.kt | 11 + .../sdk/billing/GoogleBillingWrapper.kt | 75 +- .../sdk/billing/QueryProductDetailsUseCase.kt | 40 +- .../sdk/dependencies/DependencyContainer.kt | 15 +- .../sdk/paywall/presentation/PaywallInfo.kt | 2 +- .../paywall/vc/SuperwallPaywallActivity.kt | 3 +- .../store/ExternalNativePurchaseController.kt | 11 +- .../java/com/superwall/sdk/store/StoreKit.kt | 23 + .../superwall/sdk/store/StoreKitManager.kt | 106 +- .../abstractions/product/RawStoreProduct.kt | 8 +- .../store/transactions/TransactionManager.kt | 355 +++--- .../sdk/store/StoreKitManagerTest.kt | 245 ++++ .../transactions/TransactionManagerTest.kt | 1045 +++++++++++++++++ 14 files changed, 1604 insertions(+), 346 deletions(-) create mode 100644 superwall/src/main/java/com/superwall/sdk/billing/Billing.kt create mode 100644 superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt create mode 100644 superwall/src/test/java/com/superwall/sdk/store/StoreKitManagerTest.kt create mode 100644 superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 916b95ba..c03dc6df 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -51,6 +51,7 @@ 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.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 @@ -331,7 +332,7 @@ class Superwall( } val purchaseController = purchaseController - ?: ExternalNativePurchaseController(context = applicationContext) + ?: ExternalNativePurchaseController(context = applicationContext, scope = IOScope()) _instance = Superwall( context = applicationContext, @@ -700,10 +701,10 @@ class Superwall( * ``Superwall`` will handle this for you. */ - suspend fun purchase(product: StoreProduct): PurchaseResult = + suspend fun purchase(product: RawStoreProduct): PurchaseResult = dependencyContainer.transactionManager.purchase( TransactionManager.PurchaseSource.External( - product, + StoreProduct(product), ), ) @@ -722,14 +723,14 @@ class Superwall( */ fun purchase( - product: StoreProduct, + product: RawStoreProduct, onFinished: (PurchaseResult) -> Unit, ) { ioScope.launch { val res = dependencyContainer.transactionManager.purchase( TransactionManager.PurchaseSource.External( - product, + StoreProduct(product), ), ) onFinished(res) diff --git a/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt b/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt new file mode 100644 index 00000000..1acfac06 --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt @@ -0,0 +1,11 @@ +package com.superwall.sdk.billing + +import com.superwall.sdk.dependencies.StoreTransactionFactory +import com.superwall.sdk.store.abstractions.product.StoreProduct +import com.superwall.sdk.store.abstractions.transactions.StoreTransaction + +interface Billing { + suspend fun awaitGetProducts(identifiers: Set): Set + + suspend fun getLatestTransaction(factory: StoreTransactionFactory): StoreTransaction? +} diff --git a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt index fddd134f..68150f47 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt @@ -1,8 +1,6 @@ package com.superwall.sdk.billing import android.content.Context -import android.os.Handler -import android.os.Looper import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.ProductType import com.android.billingclient.api.BillingClientStateListener @@ -16,10 +14,12 @@ import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.misc.AppLifecycleObserver import com.superwall.sdk.misc.Either +import com.superwall.sdk.misc.IOScope import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter @@ -37,14 +37,17 @@ internal const val RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 16L * 1000L class GoogleBillingWrapper( val context: Context, - val mainHandler: Handler = Handler(Looper.getMainLooper()), + val ioScope: IOScope, val appLifecycleObserver: AppLifecycleObserver, ) : PurchasesUpdatedListener, - BillingClientStateListener { + BillingClientStateListener, + Billing { companion object { private val productsCache = ConcurrentHashMap>() } + private val threadHandler = Handler(ioScope) + @get:Synchronized @set:Synchronized @Volatile @@ -63,23 +66,44 @@ class GoogleBillingWrapper( // Setup mutable state flow for purchase results private val purchaseResults = MutableStateFlow(null) - internal val IN_APP_BILLING_LESS_THAN_3_ERROR_MESSAGE = "Google Play In-app Billing API version is less than 3" + internal val IN_APP_BILLING_LESS_THAN_3_ERROR_MESSAGE = + "Google Play In-app Billing API version is less than 3" init { startConnectionOnMainThread() } + internal class Handler( + val scope: CoroutineScope, + ) { + fun post(action: () -> Unit) { + scope.launch { + action() + } + } + + fun postDelayed( + action: () -> Unit, + delayMilliseconds: Long, + ) { + scope.launch { + delay(delayMilliseconds) + action() + } + } + } + private fun executePendingRequests() { synchronized(this@GoogleBillingWrapper) { while (billingClient?.isReady == true) { serviceRequests.poll()?.let { (request, delayMilliseconds) -> if (delayMilliseconds != null) { - mainHandler.postDelayed( + threadHandler.postDelayed( { request(null) }, delayMilliseconds, ) } else { - mainHandler.post { request(null) } + threadHandler.post { request(null) } } } ?: break } @@ -87,7 +111,7 @@ class GoogleBillingWrapper( } fun startConnectionOnMainThread(delayMilliseconds: Long = 0) { - mainHandler.postDelayed( + threadHandler.postDelayed( { startConnection() }, delayMilliseconds, ) @@ -141,7 +165,7 @@ class GoogleBillingWrapper( */ @JvmSynthetic @Throws(Throwable::class) - suspend fun awaitGetProducts(fullProductIds: Set): Set { + override suspend fun awaitGetProducts(fullProductIds: Set): Set { // Get the cached products. If any are a failure, we throw an error. val cachedProducts = fullProductIds @@ -160,7 +184,8 @@ class GoogleBillingWrapper( } // Determine which product IDs are not in cache - val missingFullProductIds = fullProductIds - cachedProducts.map { it.fullIdentifier }.toSet() + val missingFullProductIds = + fullProductIds - cachedProducts.map { it.fullIdentifier }.toSet() return suspendCoroutine { continuation -> getProducts( @@ -175,9 +200,12 @@ class GoogleBillingWrapper( } // Identify and handle missing products - missingFullProductIds.filterNot { it in foundProductIds }.forEach { fullProductId -> - productsCache[fullProductId] = Either.Failure(Exception("Failed to query product details for $fullProductId")) - } + missingFullProductIds + .filterNot { it in foundProductIds } + .forEach { fullProductId -> + productsCache[fullProductId] = + Either.Failure(Exception("Failed to query product details for $fullProductId")) + } // Combine cached products (now including the newly fetched ones) with the fetched products val allProducts = cachedProducts + storeProducts @@ -274,12 +302,7 @@ class GoogleBillingWrapper( } private fun dispatch(action: () -> Unit) { - if (Thread.currentThread() != Looper.getMainLooper().thread) { - val handler = mainHandler ?: Handler(Looper.getMainLooper()) - handler.post(action) - } else { - action() - } + threadHandler.post(action) } private fun queryProductDetailsAsync( @@ -362,7 +385,7 @@ class GoogleBillingWrapper( } override fun onBillingSetupFinished(billingResult: BillingResult) { - mainHandler.post { + threadHandler.post { when (billingResult.responseCode) { BillingClient.BillingResponseCode.OK -> { Logger.debug( @@ -411,7 +434,8 @@ class GoogleBillingWrapper( Logger.debug( LogLevel.error, LogScope.productsManager, - error.message ?: "Billing is not available in this device. ${billingResult.debugMessage}", + error.message + ?: "Billing is not available in this device. ${billingResult.debugMessage}", ) // The calls will fail with an error that will be surfaced. We want to surface these errors // Can't call executePendingRequests because it will not do anything since it checks for isReady() @@ -509,16 +533,18 @@ class GoogleBillingWrapper( } } - suspend fun getLatestTransaction(factory: StoreTransactionFactory): StoreTransaction? { + override suspend fun getLatestTransaction(factory: StoreTransactionFactory): StoreTransaction? { // Get the latest from purchaseResults purchaseResults.asStateFlow().filter { it != null }.first().let { purchaseResult -> return when (purchaseResult) { is InternalPurchaseResult.Purchased -> { return factory.makeStoreTransaction(purchaseResult.purchase) } + is InternalPurchaseResult.Cancelled -> { null } + else -> { null } @@ -530,7 +556,7 @@ class GoogleBillingWrapper( private fun sendErrorsToAllPendingRequests(error: BillingError) { while (true) { serviceRequests.poll()?.let { (serviceRequest, _) -> - mainHandler.post { + threadHandler.post { serviceRequest(error) } } ?: break @@ -538,7 +564,8 @@ class GoogleBillingWrapper( } private fun trackProductDetailsNotSupportedIfNeeded() { - val billingResult = billingClient?.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS) + val billingResult = + billingClient?.isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS) if ( billingResult != null && billingResult.responseCode == BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED diff --git a/superwall/src/main/java/com/superwall/sdk/billing/QueryProductDetailsUseCase.kt b/superwall/src/main/java/com/superwall/sdk/billing/QueryProductDetailsUseCase.kt index dcc1fcae..859dd9b9 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/QueryProductDetailsUseCase.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/QueryProductDetailsUseCase.kt @@ -28,15 +28,18 @@ internal class QueryProductDetailsUseCase( val withConnectedClient: (BillingClient.() -> Unit) -> Unit, executeRequestOnUIThread: ExecuteRequestOnUIThreadFunction, ) : BillingClientUseCase>(useCaseParams, onError, executeRequestOnUIThread) { + private fun log(msg: String) = + Logger.debug( + logLevel = LogLevel.debug, + scope = LogScope.productsManager, + message = msg, + ) + override fun executeAsync() { val nonEmptyProductIds = useCaseParams.subscriptionIds.filter { it.isNotEmpty() }.toSet() if (nonEmptyProductIds.isEmpty()) { - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.productsManager, - message = "productId list is empty, skipping queryProductDetailsAsync call", - ) + log("productId list is empty, skipping queryProductDetailsAsync call") onReceive(emptyList()) return } @@ -57,22 +60,10 @@ internal class QueryProductDetailsUseCase( * `StoreProduct`. */ override fun onOk(received: List) { - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.productsManager, - message = "Products request finished for ${useCaseParams.subscriptionIds.joinToString()}", - ) - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.productsManager, - message = "Retrieved productDetailsList: ${received.joinToString { it.toString() }}", - ) + log("Products request finished for ${useCaseParams.subscriptionIds.joinToString()}") + log("Retrieved productDetailsList: ${received.joinToString { it.toString() }}") received.takeUnless { it.isEmpty() }?.forEach { - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.productsManager, - message = "${it.productId} - $it", - ) + log("${it.productId} - $it") } val storeProducts = @@ -102,12 +93,9 @@ internal class QueryProductDetailsUseCase( val hasResponded = AtomicBoolean(false) billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList -> if (hasResponded.getAndSet(true)) { - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.productsManager, - message = - "BillingClient queryProductDetails has returned more than once, " + - "with result ${billingResult.responseCode}", + log( + "BillingClient queryProductDetails has returned more than once, " + + "with result ${billingResult.responseCode}", ) return@queryProductDetailsAsync } diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index 5dc49901..5c2a1ecb 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -49,8 +49,10 @@ import com.superwall.sdk.network.device.DeviceInfo import com.superwall.sdk.network.session.CustomHttpUrlConnection import com.superwall.sdk.paywall.manager.PaywallManager import com.superwall.sdk.paywall.manager.PaywallViewCache +import com.superwall.sdk.paywall.presentation.dismiss import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType +import com.superwall.sdk.paywall.presentation.internal.dismiss import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.presentation.rule_logic.cel.SuperscriptEvaluator @@ -203,7 +205,7 @@ class DependencyContainer( } googleBillingWrapper = - GoogleBillingWrapper(context, appLifecycleObserver = appLifecycleObserver) + GoogleBillingWrapper(context, ioScope, appLifecycleObserver = appLifecycleObserver) var purchaseController = InternalPurchaseController( @@ -211,7 +213,7 @@ class DependencyContainer( javaPurchaseController = null, context, ) - storeKitManager = StoreKitManager(context, purchaseController, googleBillingWrapper) + storeKitManager = StoreKitManager(purchaseController, googleBillingWrapper) delegateAdapter = SuperwallDelegateAdapter() storage = LocalStorage(context = context, ioScope = ioScope(), factory = this, json = json()) @@ -358,11 +360,16 @@ class DependencyContainer( TransactionManager( storeKitManager = storeKitManager, purchaseController = purchaseController, - sessionEventsManager, eventsQueue = eventsQueue, activityProvider, factory = this, - context = context, + track = { + Superwall.instance.track(it) + }, + dismiss = { it, et -> + Superwall.instance.dismiss(it, et) + }, + ioScope = ioScope(), ) /** diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index eda55321..a99a9667 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -10,8 +10,8 @@ import com.superwall.sdk.models.config.FeatureGatingBehavior import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.paywall.LocalNotification import com.superwall.sdk.models.paywall.PaywallPresentationInfo -import com.superwall.sdk.models.paywall.PaywallURL import com.superwall.sdk.models.paywall.PaywallPresentationStyle +import com.superwall.sdk.models.paywall.PaywallURL import com.superwall.sdk.models.paywall.PresentationCondition import com.superwall.sdk.models.product.Product import com.superwall.sdk.models.product.ProductItem diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt index c41fcba2..c980a64c 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt @@ -491,7 +491,6 @@ class SuperwallPaywallActivity : AppCompatActivity() { suspend fun attemptToScheduleNotifications( notifications: List, factory: DeviceHelperFactory, - context: Context, ) = suspendCoroutine { continuation -> if (notifications.isEmpty()) { continuation.resume(Unit) // Resume immediately as there's nothing to schedule @@ -508,7 +507,7 @@ class SuperwallPaywallActivity : AppCompatActivity() { NotificationScheduler.scheduleNotifications( notifications = notifications, factory = factory, - context = context, + context = this@SuperwallPaywallActivity, ) } continuation.resume(Unit) // Resume coroutine after processing diff --git a/superwall/src/main/java/com/superwall/sdk/store/ExternalNativePurchaseController.kt b/superwall/src/main/java/com/superwall/sdk/store/ExternalNativePurchaseController.kt index 71ced155..07250e27 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/ExternalNativePurchaseController.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/ExternalNativePurchaseController.kt @@ -13,6 +13,7 @@ import com.superwall.sdk.delegate.subscription_controller.PurchaseController import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger +import com.superwall.sdk.misc.IOScope import com.superwall.sdk.store.abstractions.product.OfferType import com.superwall.sdk.store.abstractions.product.RawStoreProduct import kotlinx.coroutines.CompletableDeferred @@ -26,6 +27,7 @@ import kotlin.math.min class ExternalNativePurchaseController( var context: Context, + val scope: IOScope, ) : PurchaseController, PurchasesUpdatedListener { private var billingClient: BillingClient = @@ -34,6 +36,7 @@ class ExternalNativePurchaseController( .setListener(this) .enablePendingPurchases() .build() + private val isConnected = MutableStateFlow(false) private val purchaseResults = MutableStateFlow(null) @@ -43,7 +46,7 @@ class ExternalNativePurchaseController( //region Initialization init { - CoroutineScope(Dispatchers.IO).launch { + scope.launch { startConnection() } } @@ -95,8 +98,8 @@ class ExternalNativePurchaseController( //region Public - fun syncSubscriptionStatus() { - CoroutineScope(Dispatchers.IO).launch { + private fun syncSubscriptionStatus() { + scope.launch { Superwall.hasInitialized.first { it } syncSubscriptionStatusAndWait() } @@ -248,7 +251,7 @@ class ExternalNativePurchaseController( } } - CoroutineScope(Dispatchers.IO).launch { + scope.launch { // Emit the purchase result to any observers purchaseResults.emit(result) diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt new file mode 100644 index 00000000..6080b51a --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt @@ -0,0 +1,23 @@ +package com.superwall.sdk.store + +import com.superwall.sdk.models.paywall.Paywall +import com.superwall.sdk.models.product.ProductVariable +import com.superwall.sdk.paywall.request.PaywallRequest +import com.superwall.sdk.store.abstractions.product.StoreProduct + +interface StoreKit { + suspend fun getProductVariables( + paywall: Paywall, + request: PaywallRequest, + ): List + + suspend fun getProducts( + substituteProducts: Map? = null, + paywall: Paywall, + request: PaywallRequest? = null, + ): GetProductsResponse + + suspend fun refreshReceipt() + + suspend fun loadPurchasedProducts() +} diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManager.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManager.kt index d6c3bd84..3c6f45ba 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManager.kt @@ -1,12 +1,11 @@ package com.superwall.sdk.store -import android.content.Context import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent +import com.superwall.sdk.billing.Billing import com.superwall.sdk.billing.BillingError import com.superwall.sdk.billing.DecomposedProductIds -import com.superwall.sdk.billing.GoogleBillingWrapper import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger @@ -22,87 +21,14 @@ import com.superwall.sdk.store.abstractions.product.receipt.ReceiptManager import com.superwall.sdk.store.coordinator.ProductsFetcher import java.util.Date -/* -class StoreKitManager(private val context: Context) : StoreKitManagerInterface { - private val fetcher: GooglePlayProductsFetcher = GooglePlayProductsFetcher(context) - - override val productsById: Map - get() = TODO("Not yet implemented") - - override suspend fun getProductVariables(paywall: Paywall): List { - TODO("Not yet implemented") - } - -// data class GetProductsResponse( -// val productsById: Map, -// val products: List -// ) - - override suspend fun getProducts( - responseProductIds: List, - paywallName: String?, - responseProducts: List, - substituteProducts: PaywallProducts? - ): GetProductsResponse { - var productsById = mutableMapOf() - println("!! responseProductIds: $responseProductIds") - val products = fetcher.products(responseProductIds) - println("!! products: $products") - - for (product in products) { - when(product.value) { - is GooglePlayProductsFetcher.Result.Success -> { - val rawStoreProduct = (product.value as GooglePlayProductsFetcher.Result.Success).value - println("!! rawStoreProduct: $rawStoreProduct") - productsById[product.key] = StoreProduct(rawStoreProduct) - } else -> { - // TODO: ?? - } - } - } - - return GetProductsResponse(productsById, responseProducts) - } - - override suspend fun tryToRestore(paywallViewController: PaywallViewController) { -// TODO("Not yet implemented") - } - - override suspend fun processRestoration( - restorationResult: RestorationResult, - paywallViewController: PaywallViewController - ) { - TODO("Not yet implemented") - } - - override suspend fun refreshReceipt() { - TODO("Not yet implemented") - } - - override suspend fun loadPurchasedProducts() { - TODO("Not yet implemented") - } - - override suspend fun isFreeTrialAvailable(product: StoreProduct): Boolean { - // TODO: Implement this - return false - } - - override suspend fun products( - identifiers: Set, - paywallName: String? - ): Set { - TODO("Not yet implemented") - } -} -*/ - class StoreKitManager( - private val context: Context, val purchaseController: InternalPurchaseController, - val billingWrapper: GoogleBillingWrapper, - // val productFetcher: GooglePlayProductsFetcher -) : ProductsFetcher { + private val billing: Billing, + private val track: suspend (InternalSuperwallEvent) -> Unit = { + Superwall.instance.track(it) + }, +) : ProductsFetcher, + StoreKit { private val receiptManager by lazy { ReceiptManager(delegate = this) } var productsByFullId: MutableMap = mutableMapOf() @@ -113,7 +39,7 @@ class StoreKitManager( val productItems: List, ) - suspend fun getProductVariables( + override suspend fun getProductVariables( paywall: Paywall, request: PaywallRequest, ): List { @@ -136,10 +62,10 @@ class StoreKitManager( return productAttributes } - suspend fun getProducts( - substituteProducts: Map? = null, + override suspend fun getProducts( + substituteProducts: Map?, paywall: Paywall, - request: PaywallRequest? = null, + request: PaywallRequest?, ): GetProductsResponse { val processingResult = removeAndStore( @@ -150,7 +76,7 @@ class StoreKitManager( var products: Set = setOf() try { - products = billingWrapper.awaitGetProducts(processingResult.fullProductIdsToLoad) + products = billing.awaitGetProducts(processingResult.fullProductIdsToLoad) } catch (error: Throwable) { paywall.productsLoadingInfo.failAt = Date() val paywallInfo = paywall.getInfo(request?.eventData) @@ -160,7 +86,7 @@ class StoreKitManager( paywallInfo = paywallInfo, eventData = request?.eventData, ) - Superwall.instance.track(productLoadEvent) + track(productLoadEvent) // If billing isn't available, make it call the onError handler when requesting // a paywall. @@ -261,7 +187,7 @@ class StoreKitManager( ) } - suspend fun refreshReceipt() { + override suspend fun refreshReceipt() { Logger.debug( logLevel = LogLevel.debug, scope = LogScope.storeKitManager, // Rename this scope to reflect Billing Manager @@ -270,7 +196,7 @@ class StoreKitManager( receiptManager.refreshReceipt() } - suspend fun loadPurchasedProducts() { + override suspend fun loadPurchasedProducts() { Logger.debug( logLevel = LogLevel.debug, scope = LogScope.storeKitManager, // Rename this scope to reflect Billing Manager @@ -280,5 +206,5 @@ class StoreKitManager( } @Throws(Throwable::class) - override suspend fun products(identifiers: Set): Set = billingWrapper.awaitGetProducts(identifiers) + override suspend fun products(identifiers: Set): Set = billing.awaitGetProducts(identifiers) } diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt index e93113ef..ea0bc9e3 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt @@ -277,7 +277,10 @@ class RawStoreProduct( val offersForBasePlan = subscriptionOfferDetails.filter { it.basePlanId == basePlanId } // In offers that match base plan, if there's only 1 pricing phase then this offer represents the base plan. - val basePlan = offersForBasePlan.firstOrNull { it.pricingPhases.pricingPhaseList.size == 1 } ?: return null + val basePlan = + offersForBasePlan.firstOrNull { + it.pricingPhases.pricingPhaseList.size == 1 + } ?: return null return when (offerType) { is OfferType.Auto -> { @@ -485,7 +488,8 @@ class RawStoreProduct( .billingPeriod try { - SubscriptionPeriod.from(baseBillingPeriod) + SubscriptionPeriod.from(baseBillingPeriod).also { + } } catch (e: Throwable) { null } diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index 8e4f479e..ac7ee5b7 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -1,10 +1,9 @@ package com.superwall.sdk.store.transactions -import android.content.Context import com.superwall.sdk.Superwall -import com.superwall.sdk.analytics.SessionEventsManager import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent +import com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent import com.superwall.sdk.analytics.superwall.SuperwallEvents import com.superwall.sdk.delegate.PurchaseResult import com.superwall.sdk.delegate.RestorationResult @@ -13,17 +12,16 @@ import com.superwall.sdk.delegate.subscription_controller.PurchaseController import com.superwall.sdk.dependencies.DeviceHelperFactory import com.superwall.sdk.dependencies.OptionsFactory import com.superwall.sdk.dependencies.StoreTransactionFactory -import com.superwall.sdk.dependencies.SuperwallScopeFactory import com.superwall.sdk.dependencies.TransactionVerifierFactory import com.superwall.sdk.dependencies.TriggerFactory import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.misc.ActivityProvider +import com.superwall.sdk.misc.IOScope import com.superwall.sdk.misc.launchWithTracking import com.superwall.sdk.models.paywall.LocalNotificationType import com.superwall.sdk.paywall.presentation.PaywallInfo -import com.superwall.sdk.paywall.presentation.internal.dismiss import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult import com.superwall.sdk.paywall.vc.PaywallView import com.superwall.sdk.paywall.vc.SuperwallPaywallActivity @@ -32,19 +30,22 @@ import com.superwall.sdk.storage.EventsQueue import com.superwall.sdk.store.StoreKitManager import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class TransactionManager( private val storeKitManager: StoreKitManager, private val purchaseController: PurchaseController, - private val sessionEventsManager: SessionEventsManager, private val eventsQueue: EventsQueue, private val activityProvider: ActivityProvider, private val factory: Factory, - private val context: Context, + private val ioScope: IOScope, + private val track: suspend (TrackableSuperwallEvent) -> Unit = { + Superwall.instance.track(it) + }, + private val dismiss: suspend (paywallView: PaywallView, result: PaywallResult) -> Unit, + private val subscriptionStatus: () -> SubscriptionStatus = { + Superwall.instance.subscriptionStatus.value + }, ) { sealed class PurchaseSource { data class Internal( @@ -57,15 +58,12 @@ class TransactionManager( ) : PurchaseSource() } - val scope = CoroutineScope(Dispatchers.IO) - interface Factory : OptionsFactory, TriggerFactory, TransactionVerifierFactory, StoreTransactionFactory, - DeviceHelperFactory, - SuperwallScopeFactory + DeviceHelperFactory private var lastPaywallView: PaywallView? = null @@ -74,10 +72,8 @@ class TransactionManager( when (purchaseSource) { is PurchaseSource.Internal -> storeKitManager.productsByFullId[purchaseSource.productId] ?: run { - Logger.debug( - logLevel = LogLevel.error, - scope = LogScope.paywallTransactions, - message = + log( + LogLevel.error, "Trying to purchase (${purchaseSource.productId}) but the product has failed to load. Visit https://superwall.com/l/missing-products to diagnose.", ) return PurchaseResult.Failed("Product not found") @@ -89,10 +85,9 @@ class TransactionManager( } val rawStoreProduct = product.rawStoreProduct - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "!!! Purchasing product ${rawStoreProduct.hasFreeTrial}", + log( + message = + "!!! Purchasing product ${rawStoreProduct.hasFreeTrial}", ) val productDetails = rawStoreProduct.underlyingProductDetails val activity = @@ -113,7 +108,7 @@ class TransactionManager( when (result) { is PurchaseResult.Purchased -> { - didPurchase(product, purchaseSource, isEligibleForTrial) + didPurchase(product, purchaseSource, isEligibleForTrial && product.hasFreeTrial) } is PurchaseResult.Restored -> { @@ -192,13 +187,13 @@ class TransactionManager( product = product, model = null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) val superwallOptions = factory.makeSuperwallOptions() if (superwallOptions.paywalls.automaticallyDismiss && purchaseSource is PurchaseSource.Internal) { - Superwall.instance.dismiss( + dismiss( purchaseSource.paywallView, - result = PaywallResult.Restored(), + PaywallResult.Restored(), ) } } @@ -210,65 +205,59 @@ class TransactionManager( ) { when (purchaseSource) { is PurchaseSource.Internal -> { - // Log the error - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.paywallTransactions, - message = "Transaction Error: $errorMessage", + log( + message = + + "Transaction Error: $errorMessage", info = - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to purchaseSource.paywallView, - ), + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to purchaseSource.paywallView, + ), ) - factory.ioScope().launchWithTracking { + ioScope.launchWithTracking { val paywallInfo = purchaseSource.paywallView.info val trackedEvent = InternalSuperwallEvent.Transaction( state = - InternalSuperwallEvent.Transaction.State.Fail( - TransactionError.Failure( - errorMessage, - product, + InternalSuperwallEvent.Transaction.State.Fail( + TransactionError.Failure( + errorMessage, + product, + ), ), - ), paywallInfo = paywallInfo, product = product, model = null, ) - Superwall.instance.track(trackedEvent) - + track(trackedEvent) } } is PurchaseSource.External -> { - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.paywallTransactions, + log( message = "Transaction Error: $errorMessage", - info = - mapOf( - "product_id" to product.fullIdentifier, - ), + info = mapOf("product_id" to product.fullIdentifier), + error = Error(errorMessage), ) - factory.ioScope().launch { + ioScope.launch { val trackedEvent = InternalSuperwallEvent.Transaction( state = - InternalSuperwallEvent.Transaction.State.Fail( - TransactionError.Failure( - errorMessage, - product, + InternalSuperwallEvent.Transaction.State.Fail( + TransactionError.Failure( + errorMessage, + product, + ), ), - ), paywallInfo = PaywallInfo.empty(), product = product, model = null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) } } } @@ -280,13 +269,12 @@ class TransactionManager( ) { when (source) { is PurchaseSource.Internal -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Purchasing", - mapOf("paywall_vc" to source), - null, + ioScope.launch { + log( + message = + + "Transaction Purchasing", + info = mapOf("paywall_vc" to source), ) } @@ -298,22 +286,19 @@ class TransactionManager( product, null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) - withContext(Dispatchers.Main) { - source.paywallView.loadingState = PaywallLoadingState.LoadingPurchase() - } + source.paywallView.loadingState = PaywallLoadingState.LoadingPurchase() lastPaywallView = source.paywallView } is PurchaseSource.External -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "External Transaction Purchasing", - null, + ioScope.launch { + log( + message = + + "External Transaction Purchasing", ) } @@ -324,7 +309,7 @@ class TransactionManager( product, null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) } } } @@ -336,16 +321,14 @@ class TransactionManager( ) { when (purchaseSource) { is PurchaseSource.Internal -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Succeeded", - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to purchaseSource.paywallView, - ), - null, + ioScope.launch { + log( + message = "Transaction Succeeded", + info = + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to purchaseSource.paywallView, + ), ) } @@ -355,16 +338,12 @@ class TransactionManager( factory = factory, ) - transaction?.let { - sessionEventsManager.enqueue(it) - } - storeKitManager.loadPurchasedProducts() trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) - if (Superwall.instance.options.paywalls.automaticallyDismiss) { - Superwall.instance.dismiss( + if (factory.makeSuperwallOptions().paywalls.automaticallyDismiss) { + dismiss( purchaseSource.paywallView, PaywallResult.Purchased(product.fullIdentifier), ) @@ -372,14 +351,9 @@ class TransactionManager( } is PurchaseSource.External -> { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Succeeded", - mapOf( - "product_id" to product.fullIdentifier, - ), - null, + log( + message = "Transaction Succeeded", + info = mapOf("product_id" to product.fullIdentifier), ) val transactionVerifier = factory.makeTransactionVerifier() val transaction = @@ -387,10 +361,6 @@ class TransactionManager( factory = factory, ) - transaction?.let { - sessionEventsManager.enqueue(it) - } - storeKitManager.loadPurchasedProducts() trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) @@ -404,16 +374,14 @@ class TransactionManager( ) { when (purchaseSource) { is PurchaseSource.Internal -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Abandoned", - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to purchaseSource.paywallView, - ), - null, + ioScope.launch { + log( + message = "Transaction Abandoned", + info = + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to purchaseSource.paywallView, + ), ) } @@ -425,23 +393,16 @@ class TransactionManager( product, null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) - withContext(Dispatchers.Main) { - purchaseSource.paywallView.loadingState = PaywallLoadingState.Ready() - } + purchaseSource.paywallView.loadingState = PaywallLoadingState.Ready() } is PurchaseSource.External -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Abandoned", - mapOf( - "product_id" to product.fullIdentifier, - ), - null, + ioScope.launch { + log( + message = "Transaction Abandoned", + info = mapOf("product_id" to product.fullIdentifier), ) } @@ -452,7 +413,7 @@ class TransactionManager( product, null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) } } } @@ -460,13 +421,10 @@ class TransactionManager( private suspend fun handlePendingTransaction(purchaseSource: PurchaseSource) { when (purchaseSource) { is PurchaseSource.Internal -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Pending", - mapOf("paywall_vc" to purchaseSource.paywallView), - null, + ioScope.launch { + log( + message = "Transaction Pending", + info = mapOf("paywall_vc" to purchaseSource.paywallView), ) } @@ -479,7 +437,7 @@ class TransactionManager( null, null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) purchaseSource.paywallView.showAlert( "Waiting for Approval", @@ -488,13 +446,8 @@ class TransactionManager( } is PurchaseSource.External -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Pending", - null, - ) + ioScope.launch { + log(message = "Transaction Pending") } val trackedEvent = @@ -504,7 +457,7 @@ class TransactionManager( null, null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) } } } @@ -516,17 +469,13 @@ class TransactionManager( * @return A [RestorationResult] indicating the result of the restoration. */ suspend fun tryToRestorePurchases(paywallView: PaywallView?): RestorationResult { - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.paywallTransactions, - message = "Attempting Restore", - ) + log(message = "Attempting Restore") val paywallInfo = paywallView?.info ?: PaywallInfo.empty() paywallView?.loadingState = PaywallLoadingState.LoadingPurchase() - Superwall.instance.track( + track( InternalSuperwallEvent.Restore( state = InternalSuperwallEvent.Restore.State.Start, paywallInfo = paywallInfo, @@ -536,15 +485,11 @@ class TransactionManager( val hasRestored = restorationResult is RestorationResult.Restored val isUserSubscribed = - Superwall.instance.subscriptionStatus.value == SubscriptionStatus.ACTIVE + subscriptionStatus() == SubscriptionStatus.ACTIVE if (hasRestored && isUserSubscribed) { - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.paywallTransactions, - message = "Transactions Restored", - ) - Superwall.instance.track( + log(message = "Transactions Restored") + track( InternalSuperwallEvent.Restore( state = InternalSuperwallEvent.Restore.State.Complete, paywallInfo = paywallView?.info ?: PaywallInfo.empty(), @@ -557,7 +502,7 @@ class TransactionManager( val msg = "Transactions Failed to Restore.${ if (hasRestored && !isUserSubscribed) { " The user's subscription status is \"inactive\", but the restoration result is \"restored\"." + - " Ensure the subscription status is active before confirming successful restoration." + " Ensure the subscription status is active before confirming successful restoration." } else { " Original restoration error message: ${ when (restorationResult) { @@ -568,12 +513,8 @@ class TransactionManager( } }" - Logger.debug( - logLevel = LogLevel.debug, - scope = LogScope.paywallTransactions, - message = msg, - ) - Superwall.instance.track( + log(message = msg) + track( InternalSuperwallEvent.Restore( state = InternalSuperwallEvent.Restore.State.Failure(msg), paywallInfo = paywallView?.info ?: PaywallInfo.empty(), @@ -581,9 +522,18 @@ class TransactionManager( ) paywallView?.showAlert( - title = Superwall.instance.options.paywalls.restoreFailed.title, - message = Superwall.instance.options.paywalls.restoreFailed.message, - closeActionTitle = Superwall.instance.options.paywalls.restoreFailed.closeButtonTitle, + title = + factory + .makeSuperwallOptions() + .paywalls.restoreFailed.title, + message = + factory + .makeSuperwallOptions() + .paywalls.restoreFailed.message, + closeActionTitle = + factory + .makeSuperwallOptions() + .paywalls.restoreFailed.closeButtonTitle, ) } return restorationResult @@ -596,16 +546,15 @@ class TransactionManager( ) { when (source) { is PurchaseSource.Internal -> { - factory.ioScope().launch { - Logger.debug( - LogLevel.debug, - LogScope.paywallTransactions, - "Transaction Error", - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to source.paywallView, - ), - error, + ioScope.launch { + log( + message = "Transaction Error", + info = + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to source.paywallView, + ), + error = error, ) } @@ -623,7 +572,7 @@ class TransactionManager( product, null, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) source.paywallView.showAlert( "An error occurred", @@ -632,7 +581,26 @@ class TransactionManager( } is PurchaseSource.External -> { - // TODO: Implement displaying an alert here + ioScope.launch { + log( + message = "Transaction Error", + error = error, + ) + } + + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Fail( + TransactionError.Failure( + error.message ?: "", + product, + ), + ), + PaywallInfo.empty(), + product, + null, + ) + track(trackedEvent) } } } @@ -659,9 +627,7 @@ class TransactionManager( product, transaction, ) - Superwall.instance.track(trackedEvent) - - // Immediately flush the events queue on transaction complete. + track(trackedEvent) eventsQueue.flushInternal() if (product.subscriptionPeriod == null) { @@ -670,12 +636,12 @@ class TransactionManager( paywallInfo, product, ) - Superwall.instance.track(nonRecurringEvent) + track(nonRecurringEvent) } else { if (didStartFreeTrial) { val freeTrialEvent = InternalSuperwallEvent.FreeTrialStart(paywallInfo, product) - Superwall.instance.track(freeTrialEvent) + track(freeTrialEvent) val notifications = paywallInfo.localNotifications.filter { it.type == LocalNotificationType.TrialStarted } @@ -685,12 +651,11 @@ class TransactionManager( paywallActivity.attemptToScheduleNotifications( notifications = notifications, factory = factory, - context = context, ) } else { val subscriptionEvent = InternalSuperwallEvent.SubscriptionStart(paywallInfo, product) - Superwall.instance.track(subscriptionEvent) + track(subscriptionEvent) } } @@ -705,30 +670,44 @@ class TransactionManager( product, transaction, ) - Superwall.instance.track(trackedEvent) + track(trackedEvent) eventsQueue.flushInternal() + if (product.subscriptionPeriod == null) { val nonRecurringEvent = InternalSuperwallEvent.NonRecurringProductPurchase( PaywallInfo.empty(), product, ) - Superwall.instance.track(nonRecurringEvent) + track(nonRecurringEvent) } else { if (didStartFreeTrial) { val freeTrialEvent = InternalSuperwallEvent.FreeTrialStart(PaywallInfo.empty(), product) - Superwall.instance.track(freeTrialEvent) + track(freeTrialEvent) } else { val subscriptionEvent = InternalSuperwallEvent.SubscriptionStart( PaywallInfo.empty(), product, ) - Superwall.instance.track(subscriptionEvent) + track(subscriptionEvent) } } } } } + + private fun log( + logLevel: LogLevel = LogLevel.debug, + message: String, + info: Map? = null, + error: Throwable? = null, + ) = Logger.debug( + logLevel = logLevel, + scope = LogScope.paywallTransactions, + message = message, + info = info, + error = error, + ) } diff --git a/superwall/src/test/java/com/superwall/sdk/store/StoreKitManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/StoreKitManagerTest.kt new file mode 100644 index 00000000..c05d8292 --- /dev/null +++ b/superwall/src/test/java/com/superwall/sdk/store/StoreKitManagerTest.kt @@ -0,0 +1,245 @@ +package com.superwall.sdk.store + +import com.superwall.sdk.And +import com.superwall.sdk.Given +import com.superwall.sdk.Then +import com.superwall.sdk.When +import com.superwall.sdk.assertTrue +import com.superwall.sdk.billing.Billing +import com.superwall.sdk.billing.BillingError +import com.superwall.sdk.models.paywall.Paywall +import com.superwall.sdk.models.product.Offer +import com.superwall.sdk.models.product.PlayStoreProduct +import com.superwall.sdk.models.product.ProductItem +import com.superwall.sdk.models.product.Store +import com.superwall.sdk.paywall.request.PaywallRequest +import com.superwall.sdk.store.abstractions.product.StoreProduct +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test + +class StoreKitManagerTest { + private lateinit var purchaseController: InternalPurchaseController + private lateinit var billing: Billing + private lateinit var storeKitManager: StoreKitManager + + @Before + fun setup() { + purchaseController = mockk() + billing = mockk() + storeKitManager = + StoreKitManager( + purchaseController, + billing, + track = {}, + ) + } + + @Test + fun test_getProductVariables_with_successful_product_fetch() = + runTest { + Given("a paywall with product items") { + val paywall = + Paywall.stub().copy( + productIds = listOf("product1", "product2"), + _productItems = + listOf( + ProductItem( + "Item1", + ProductItem.StoreProductType.PlayStore( + PlayStoreProduct( + store = Store.PLAY_STORE, + productIdentifier = "product1", + basePlanIdentifier = "basePlan1", + offer = Offer.Automatic(), + ), + ), + ), + ProductItem( + "Item2", + type = + ProductItem.StoreProductType.PlayStore( + PlayStoreProduct( + store = Store.PLAY_STORE, + productIdentifier = "product2", + basePlanIdentifier = "basePlan1", + offer = Offer.Automatic(), + ), + ), + ), + ), + ) + val request = mockk() + val storeProducts = + setOf( + mockk { + every { fullIdentifier } returns "product1:basePlan1:sw-auto" + every { attributes } returns mapOf("attr1" to "value1") + }, + mockk { + every { fullIdentifier } returns "product2:basePlan1:sw-auto" + every { attributes } returns mapOf("attr2" to "value2") + }, + ) + + coEvery { billing.awaitGetProducts(any()) } returns storeProducts + + When("getProductVariables is called") { + val result = storeKitManager.getProductVariables(paywall, request) + + Then("it should return the correct product variables") { + assertEquals(2, result.size) + assertEquals("Item1", result[0].name) + assertEquals(mapOf("attr1" to "value1"), result[0].attributes) + assertEquals("Item2", result[1].name) + assertEquals(mapOf("attr2" to "value2"), result[1].attributes) + } + } + } + } + + @Test + fun test_getProducts_with_substitute_products() = + runTest { + Given("a paywall and substitute products") { + val paywall = + Paywall.stub().copy( + productIds = listOf("product1", "product2"), + _productItems = + listOf( + ProductItem( + "Item1", + ProductItem.StoreProductType.PlayStore( + PlayStoreProduct( + store = Store.PLAY_STORE, + productIdentifier = "product1", + basePlanIdentifier = "basePlan1", + offer = Offer.Automatic(), + ), + ), + ), + ProductItem( + "Item2", + type = + ProductItem.StoreProductType.PlayStore( + PlayStoreProduct( + store = Store.PLAY_STORE, + productIdentifier = "product2", + basePlanIdentifier = "basePlan1", + offer = Offer.Automatic(), + ), + ), + ), + ), + ) + val substituteProducts = + mapOf( + "Item1" to + mockk { + every { fullIdentifier } returns "substitute1" + every { attributes } returns mapOf("attr1" to "value1") + }, + ) + + coEvery { billing.awaitGetProducts(any()) } returns + setOf( + mockk { + every { fullIdentifier } returns "product2" + every { attributes } returns mapOf("attr2" to "value2") + }, + ) + + When("getProducts is called with substitute products") { + val result = storeKitManager.getProducts(substituteProducts, paywall, null) + + Then("it should use the substitute product and fetch the remaining product") { + assertEquals(2, result.productsByFullId.size) + assertTrue(result.productsByFullId.containsKey("substitute1")) + assertTrue(result.productsByFullId.containsKey("product2")) + } + + And("it should update the product items accordingly") { + assertEquals(2, result.productItems.size) + assertEquals("Item1", result.productItems[0].name) + assertEquals("Item2", result.productItems[1].name) + } + } + } + } + + @Test + fun `test getProducts with billing error`() = + runTest { + Given("a paywall and a billing error") { + val paywall = + Paywall.stub().copy( + productIds = listOf("product1"), + _productItems = + listOf( + ProductItem( + "Item1", + ProductItem.StoreProductType.PlayStore( + PlayStoreProduct( + store = Store.PLAY_STORE, + productIdentifier = "product1", + basePlanIdentifier = "basePlan1", + offer = Offer.Automatic(), + ), + ), + ), + ProductItem( + "Item2", + type = + ProductItem.StoreProductType.PlayStore( + PlayStoreProduct( + store = Store.PLAY_STORE, + productIdentifier = "product2", + basePlanIdentifier = "basePlan1", + offer = Offer.Automatic(), + ), + ), + ), + ), + ) + + coEvery { billing.awaitGetProducts(any()) } throws BillingError.BillingNotAvailable("Billing not available") + + When("getProducts is called") { + Then("it should throw a BillingNotAvailable error") { + assertThrows(BillingError.BillingNotAvailable::class.java) { + runBlocking { storeKitManager.getProducts(null, paywall, null) } + } + } + } + } + } + + @Test + fun `test products method`() = + runTest { + Given("a set of product identifiers") { + val identifiers = setOf("product1", "product2") + val expectedProducts = + setOf( + mockk { every { fullIdentifier } returns "product1" }, + mockk { every { fullIdentifier } returns "product2" }, + ) + + coEvery { billing.awaitGetProducts(identifiers) } returns expectedProducts + + When("products method is called") { + val result = storeKitManager.products(identifiers) + + Then("it should return the correct set of StoreProducts") { + assertEquals(expectedProducts, result) + } + } + } + } +} diff --git a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt new file mode 100644 index 00000000..f3547b82 --- /dev/null +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -0,0 +1,1045 @@ +package com.superwall.sdk.store.transactions + +import com.android.billingclient.api.ProductDetails +import com.superwall.sdk.And +import com.superwall.sdk.Given +import com.superwall.sdk.Then +import com.superwall.sdk.When +import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent +import com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent +import com.superwall.sdk.analytics.superwall.SuperwallEvent +import com.superwall.sdk.billing.Billing +import com.superwall.sdk.config.options.SuperwallOptions +import com.superwall.sdk.delegate.PurchaseResult +import com.superwall.sdk.delegate.RestorationResult +import com.superwall.sdk.delegate.SubscriptionStatus +import com.superwall.sdk.misc.ActivityProvider +import com.superwall.sdk.misc.IOScope +import com.superwall.sdk.models.paywall.Paywall +import com.superwall.sdk.models.product.Offer +import com.superwall.sdk.models.product.PlayStoreProduct +import com.superwall.sdk.models.product.Product +import com.superwall.sdk.models.product.ProductItem +import com.superwall.sdk.models.product.ProductType +import com.superwall.sdk.paywall.presentation.PaywallInfo +import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult +import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState +import com.superwall.sdk.products.mockPricingPhase +import com.superwall.sdk.products.mockSubscriptionOfferDetails +import com.superwall.sdk.storage.EventsQueue +import com.superwall.sdk.store.InternalPurchaseController +import com.superwall.sdk.store.StoreKitManager +import com.superwall.sdk.store.abstractions.product.OfferType +import com.superwall.sdk.store.abstractions.product.RawStoreProduct +import com.superwall.sdk.store.abstractions.product.StoreProduct +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.lang.ref.WeakReference + +class TransactionManagerTest { + private val playProduct = + mockk { + every { productId } returns "product1" + every { oneTimePurchaseOfferDetails } returns + mockk { + every { priceAmountMicros } returns 1000000 + every { priceCurrencyCode } returns "USD" + } + } + + private val mockProduct = + RawStoreProduct( + playProduct, + "product1", + "basePlan", + OfferType.Auto, + ) + + private val pwInfo = + PaywallInfo.empty().copy( + products = + listOf( + Product(ProductType.PRIMARY, "product1"), + ), + ) + + private val mockItems = + listOf( + ProductItem( + "Item1", + ProductItem.StoreProductType.PlayStore( + PlayStoreProduct( + productIdentifier = "product1", + basePlanIdentifier = "basePlan", + offer = Offer.Automatic(), + ), + ), + ), + ) + + private val mockedPaywall: Paywall = + mockk { + every { getInfo(any()) } returns pwInfo + every { productIds } returns listOf("product1") + every { productItems } returns mockItems + every { isFreeTrialAvailable } returns true + } + private val paywallView = + mockk(relaxUnitFun = true) { + every { info } returns pwInfo + every { paywall } returns mockedPaywall + } + + private var purchaseController = mockk() + private var billing: Billing = + mockk { + coEvery { awaitGetProducts(any()) } returns + setOf( + StoreProduct( + mockProduct, + ), + ) + } + private var storeKitManager = spyk(StoreKitManager(purchaseController, billing)) + private var activityProvider = + mockk { + every { getCurrentActivity() } returns mockk() + } + + private var eventsQueue = mockk(relaxUnitFun = true) + private var transactionManagerFactory = + mockk { + every { makeTransactionVerifier() } returns + mockk { + coEvery { getLatestTransaction(any()) } returns mockk() + } + } + + fun TestScope.manager( + track: (TrackableSuperwallEvent) -> Unit = {}, + dismiss: (paywallView: PaywallView, result: PaywallResult) -> Unit = { _, _ -> }, + subscriptionStatus: () -> SubscriptionStatus = { + SubscriptionStatus.ACTIVE + }, + options: SuperwallOptions.() -> Unit = {}, + ) = TransactionManager( + purchaseController = purchaseController, + storeKitManager = storeKitManager, + activityProvider = activityProvider, + subscriptionStatus = subscriptionStatus, + track = { track(it) }, + dismiss = { i, e -> dismiss(i, e) }, + eventsQueue = eventsQueue, + factory = transactionManagerFactory, + ioScope = IOScope(this.coroutineContext), + ).also { + coEvery { transactionManagerFactory.makeSuperwallOptions() } returns + SuperwallOptions().apply( + options, + ) + } + + @Test + fun test_purchase_internal_product_not_found() = + runTest { + Given("We try to purchase a product that does not exist") { + val transactionManager: TransactionManager = manager() + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase fails") { + assert(result is PurchaseResult.Failed && result.errorMessage == "Product not found") + } + } + } + } + + @Test + fun test_purchase_activity_not_found() = + runTest { + Given("We have loaded products but no activity") { + storeKitManager.getProducts(paywall = mockedPaywall) + every { activityProvider.getCurrentActivity() } returns null + val transactionManager: TransactionManager = manager() + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase fails") { + assert( + result is PurchaseResult.Failed && + result.errorMessage == "Activity not found - required for starting the billing flow", + ) + } + } + } + } + + @Test + fun test_purchase_successful_internal() = + runTest { + val events = MutableStateFlow(emptyList()) + Given("We have loaded products and we can purchase successfully") { + // Pretend a paywall loaded a producy + storeKitManager.getProducts(paywall = mockedPaywall) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { + it + e + } + }) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Purchased() + + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify event order") { + val transactionEvents = + events.value.filterIsInstance() + + assert(transactionEvents.first().superwallEvent is SuperwallEvent.TransactionStart) + assert(transactionEvents.first().product?.fullIdentifier == "product1") + assert(transactionEvents.last().superwallEvent is SuperwallEvent.TransactionComplete) + + val purchase = + events.value.filterIsInstance() + assert(purchase.first().product?.fullIdentifier == "product1") + } + } + } + } + } + + @Test + fun test_purchase_successful_external() = + runTest { + Given("We have loaded products and we can purchase successfully externally") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }, options = { + paywalls.automaticallyDismiss = true + }) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Purchased() + + When("We try to purchase a product externally") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.External( + StoreProduct(mockProduct), + ), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify event order") { + val transactionEvents = + events.value.filterIsInstance() + assert(transactionEvents.first().superwallEvent is SuperwallEvent.TransactionStart) + assert(transactionEvents.last().superwallEvent is SuperwallEvent.TransactionComplete) + + val purchase = + events.value.filterIsInstance() + assert(purchase.first().product?.fullIdentifier == "product1") + } + } + } + } + } + + @Test + fun test_purchase_restored_internal() = + runTest { + Given("We have loaded products and a purchase results in restoration") { + storeKitManager.getProducts(paywall = mockedPaywall) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager( + track = { e -> + events.update { it + e } + }, + options = { + paywalls.automaticallyDismiss = true + }, + dismiss = { view, res -> + assert(view == paywallView) + assert(res is PaywallResult.Restored) + }, + ) + coEvery { + purchaseController.restorePurchases() + } returns RestorationResult.Restored() + + When("We try to restore a product from the paywall") { + val result = + transactionManager.tryToRestorePurchases( + paywallView, + ) + Then("The restore results in restoration") { + assert(result is RestorationResult.Restored) + And("Verify restoration event") { + val restorationEvent = + events.value + .filterIsInstance() + .find { it.state is InternalSuperwallEvent.Transaction.State.Restore } + assert(restorationEvent != null) + } + } + } + } + } + + @Test + fun test_purchase_restored_external() = + runTest { + Given("We have not loaded products and have products to restore") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + coEvery { + purchaseController.restorePurchases() + } returns RestorationResult.Restored() + + When("We try to restore a product from the paywall") { + val result = transactionManager.tryToRestorePurchases(null) + Then("The purchase results in restoration") { + assert(result is RestorationResult.Restored) + And("Verify restoration event") { + advanceUntilIdle() + val restorationEvent = + events.value + .filterIsInstance() + .find { it.state is InternalSuperwallEvent.Restore.State.Complete } + assert(restorationEvent != null) + } + } + } + } + } + + @Test + fun test_purchase_failed_with_alert() = + runTest { + Given("We have loaded products and a purchase fails") { + storeKitManager.getProducts(paywall = mockedPaywall) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }, options = { + paywalls.shouldShowPurchaseFailureAlert = true + }) + coEvery { transactionManagerFactory.makeTriggers() } returns + events.value + .map { it.rawName } + .toSet() + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Failed("Test failure") + + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase fails and an alert is shown") { + assert(result is PurchaseResult.Failed) + coVerify { + paywallView.showAlert( + any(), + any(), + any(), + any(), + isNull(), + isNull(), + ) + } + And("Verify failure event") { + val failureEvent = + events.value + .filterIsInstance() + .find { it.state is InternalSuperwallEvent.Transaction.State.Fail } + assert(failureEvent != null) + } + } + } + } + } + + @Test + fun test_purchase_failed_without_alert() = + runTest { + Given("We have loaded products and a purchase fails") { + storeKitManager.getProducts(paywall = mockedPaywall) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }, options = { + paywalls.shouldShowPurchaseFailureAlert = false + }) + coEvery { transactionManagerFactory.makeTriggers() } returns + events.value + .map { it.rawName } + .toSet() + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Failed("Test failure") + + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase fails and no alert is shown") { + assert(result is PurchaseResult.Failed) + coVerify(exactly = 0) { + paywallView.showAlert( + any(), + any(), + any(), + any(), + isNull(), + isNull(), + ) + } + And("Verify failure event") { + advanceUntilIdle() + val failureEvent = + events.value + .filterIsInstance() + .find { it.state is InternalSuperwallEvent.Transaction.State.Fail } + assert(failureEvent != null) + } + } + } + } + } + + @Test + fun test_purchase_pending() = + runTest { + Given("We have loaded products and a purchase is pending") { + storeKitManager.getProducts(paywall = mockedPaywall) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Pending() + + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase is pending") { + assert(result is PurchaseResult.Pending) + coVerify { paywallView.showAlert(any(), any()) } + And("Verify pending event") { + val pendingEvent = + events.value + .filterIsInstance() + .find { + it.state is InternalSuperwallEvent.Transaction.State.Fail && + (it.state as InternalSuperwallEvent.Transaction.State.Fail).error is TransactionError.Pending + } + assert(pendingEvent != null) + } + } + } + } + } + + @Test + fun test_purchase_cancelled_internal() = + runTest { + Given("We have loaded products and a purchase is pending") { + storeKitManager.getProducts(paywall = mockedPaywall) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Cancelled() + + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase is pending") { + assert(result is PurchaseResult.Cancelled) + verify { paywallView setProperty "loadingState" value any(PaywallLoadingState.Ready::class) } + And("Verify pending event") { + val pendingEvent = + events.value + .filterIsInstance() + .find { it.state is InternalSuperwallEvent.Transaction.State.Abandon } + assert(pendingEvent != null) + } + } + } + } + } + + @Test + fun test_purchase_cancelled_external() = + runTest { + Given("We have loaded products and a purchase is pending") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + coEvery { + purchaseController.purchase( + any(), + any(), + any(), + any(), + ) + } returns PurchaseResult.Cancelled() + + When("We try to purchase a product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.External( + StoreProduct(mockProduct), + ), + ) + Then("The purchase is pending") { + assert(result is PurchaseResult.Cancelled) + verify(exactly = 0) { + paywallView setProperty "loadingState" value + any( + PaywallLoadingState.Ready::class, + ) + } + And("Verify pending event") { + val pendingEvent = + events.value + .filterIsInstance() + .find { it.state is InternalSuperwallEvent.Transaction.State.Abandon } + assert(pendingEvent != null) + } + } + } + } + } + + @Test + fun test_try_to_restore_purchases_success() = + runTest { + Given("We can successfully restore purchases from a paywall") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager( + track = { e -> + events.update { it + e } + }, + subscriptionStatus = { SubscriptionStatus.ACTIVE }, + ) + coEvery { purchaseController.restorePurchases() } returns RestorationResult.Restored() + + When("We try to restore purchases") { + val result = transactionManager.tryToRestorePurchases(paywallView) + Then("The restoration is successful") { + assert(result is RestorationResult.Restored) + And("Verify restoration events") { + val restoreEvents = + events.value.filterIsInstance() + assert(restoreEvents.size == 2) + assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start) + assert(restoreEvents[1].state is InternalSuperwallEvent.Restore.State.Complete) + } + } + } + } + } + + @Test + fun test_try_to_restore_purchases_success_externally() = + runTest { + Given("We can successfully restore purchases without a paywall") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager( + track = { e -> + events.update { it + e } + }, + subscriptionStatus = { SubscriptionStatus.ACTIVE }, + ) + coEvery { purchaseController.restorePurchases() } returns RestorationResult.Restored() + + When("We try to restore purchases") { + val result = transactionManager.tryToRestorePurchases(null) + Then("The restoration is successful") { + assert(result is RestorationResult.Restored) + And("Verify restoration events") { + val restoreEvents = + events.value.filterIsInstance() + assert(restoreEvents.size == 2) + assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start) + assert(restoreEvents[1].state is InternalSuperwallEvent.Restore.State.Complete) + } + } + } + } + } + + @Test + fun test_try_to_restore_purchases_failure() = + runTest { + Given("Restoration of purchases fails") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager( + track = { e -> + events.update { it + e } + }, + subscriptionStatus = { SubscriptionStatus.ACTIVE }, + ) + + coEvery { purchaseController.restorePurchases() } returns + RestorationResult.Failed( + Exception("Test failure"), + ) + + When("We try to restore purchases") { + val result = transactionManager.tryToRestorePurchases(paywallView) + Then("The restoration fails") { + assert(result is RestorationResult.Failed) + coVerify { + paywallView.showAlert( + any(), + any(), + any(), + any(), + isNull(), + isNull(), + ) + } + And("Verify restoration events") { + val restoreEvents = + events.value.filterIsInstance() + assert(restoreEvents.size == 2) + assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start) + assert(restoreEvents[1].state is InternalSuperwallEvent.Restore.State.Failure) + } + } + } + } + } + + @Test + fun test_try_to_restore_purchases_restored_but_inactive() = + runTest { + Given("Restoration of purchases fails") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager( + track = { e -> + events.update { it + e } + }, + subscriptionStatus = { SubscriptionStatus.INACTIVE }, + ) + + coEvery { purchaseController.restorePurchases() } returns RestorationResult.Restored() + + When("We try to restore purchases") { + val result = transactionManager.tryToRestorePurchases(paywallView) + Then("The restoration fails because subscription is inactive") { + assert(result is RestorationResult.Restored) + coVerify { + paywallView.showAlert( + any(), + any(), + any(), + any(), + isNull(), + isNull(), + ) + } + And("Verify restoration events") { + val restoreEvents = + events.value.filterIsInstance() + assert(restoreEvents.size == 2) + assert(restoreEvents[0].state is InternalSuperwallEvent.Restore.State.Start) + val failure: InternalSuperwallEvent.Restore.State.Failure = + restoreEvents[1].state as InternalSuperwallEvent.Restore.State.Failure + assert(failure.reason.contains("\"inactive\"")) + } + } + } + } + } + + @Test + fun test_purchase_with_free_trial_internal() = + runTest { + Given("We have loaded products with a free trial and we can purchase successfully") { + storeKitManager.getProducts(paywall = mockedPaywall) + every { paywallView.encapsulatingActivity } returns WeakReference(mockk()) + every { playProduct.oneTimePurchaseOfferDetails } returns null + every { playProduct.subscriptionOfferDetails } returns + listOf( + mockSubscriptionOfferDetails( + basePlanId = "basePlan", + pricingPhases = listOf(mockPricingPhase()), + ), + mockSubscriptionOfferDetails( + offerId = "offer1", + basePlanId = "basePlan", + pricingPhases = listOf(mockPricingPhase(0.0), mockPricingPhase()), + ), + ) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Purchased() + When("We try to purchase a product with a free trial from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify free trial start event") { + val freeTrialStartEvent = + events.value.filterIsInstance() + assert(freeTrialStartEvent.isNotEmpty()) + assert(freeTrialStartEvent.first().product?.fullIdentifier == "product1") + } + } + } + } + } + + @Test + fun test_purchase_with_free_trial_external() = + runTest { + Given("We can purchase a product with a free trial externally") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + every { playProduct.oneTimePurchaseOfferDetails } returns null + every { playProduct.subscriptionOfferDetails } returns + listOf( + mockSubscriptionOfferDetails( + basePlanId = "basePlan", + pricingPhases = listOf(mockPricingPhase()), + ), + mockSubscriptionOfferDetails( + offerId = "offer1", + basePlanId = "basePlan", + pricingPhases = listOf(mockPricingPhase(0.0), mockPricingPhase()), + ), + ) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Purchased() + + When("We try to purchase a product with a free trial externally") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.External( + StoreProduct(mockProduct), + ), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify free trial start event") { + val freeTrialStartEvent = + events.value.filterIsInstance() + assert(freeTrialStartEvent.isNotEmpty()) + assert(freeTrialStartEvent.first().product?.fullIdentifier == "product1") + } + } + } + } + } + + @Test + fun test_purchase_non_recurring_product_internal() = + runTest { + Given("We have loaded a non-recurring product and we can purchase successfully") { + storeKitManager.getProducts(paywall = mockedPaywall) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + every { playProduct.oneTimePurchaseOfferDetails } returns null + every { playProduct.subscriptionOfferDetails } returns null + coEvery { + purchaseController.purchase( + any(), + any(), + any(), + any(), + ) + } returns PurchaseResult.Purchased() + + When("We try to purchase a non-recurring product from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify non-recurring product purchase event") { + val nonRecurringPurchaseEvent = + events.value.filterIsInstance() + assert(nonRecurringPurchaseEvent.isNotEmpty()) + assert(nonRecurringPurchaseEvent.first().product?.fullIdentifier == "product1") + } + } + } + } + } + + @Test + fun test_purchase_non_recurring_product_external() = + runTest { + Given("We can purchase a non-recurring product externally") { + every { playProduct.oneTimePurchaseOfferDetails } returns null + every { playProduct.subscriptionOfferDetails } returns null + + val nonRecurringStoreProduct = StoreProduct(mockProduct) + + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + coEvery { + purchaseController.purchase( + any(), + any(), + any(), + any(), + ) + } returns PurchaseResult.Purchased() + + When("We try to purchase a non-recurring product externally") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.External(nonRecurringStoreProduct), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify non-recurring product purchase event") { + val nonRecurringPurchaseEvent = + events.value.filterIsInstance() + assert(nonRecurringPurchaseEvent.isNotEmpty()) + assert(nonRecurringPurchaseEvent.first().product?.fullIdentifier == "product1") + } + } + } + } + } + + @Test + fun test_purchase_subscription_without_trial_internal() = + runTest { + Given("We have loaded a subscription product without trial and we can purchase successfully") { + storeKitManager.getProducts(paywall = mockedPaywall) + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + every { playProduct.oneTimePurchaseOfferDetails } returns null + every { playProduct.subscriptionOfferDetails } returns + listOf( + mockSubscriptionOfferDetails( + basePlanId = "basePlan", + pricingPhases = listOf(mockPricingPhase()), + ), + ) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Purchased() + + When("We try to purchase a subscription without trial from the paywall") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.Internal( + "product1", + paywallView, + ), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify subscription start event") { + val subscriptionStartEvent = + events.value.filterIsInstance() + assert(subscriptionStartEvent.isNotEmpty()) + assert(subscriptionStartEvent.first().product?.fullIdentifier == "product1") + } + } + } + } + } + + @Test + fun test_purchase_subscription_without_trial_external() = + runTest { + Given("We can purchase a subscription product without trial externally") { + val events = MutableStateFlow(emptyList()) + val transactionManager: TransactionManager = + manager(track = { e -> + events.update { it + e } + }) + every { playProduct.oneTimePurchaseOfferDetails } returns null + every { playProduct.subscriptionOfferDetails } returns + listOf( + mockSubscriptionOfferDetails( + basePlanId = "basePlan", + pricingPhases = listOf(mockPricingPhase()), + ), + ) + coEvery { + purchaseController.purchase( + any(), + any(), + "basePlan", + any(), + ) + } returns PurchaseResult.Purchased() + + When("We try to purchase a subscription without trial externally") { + val result = + transactionManager.purchase( + TransactionManager.PurchaseSource.External( + StoreProduct(mockProduct), + ), + ) + Then("The purchase is successful") { + assert(result is PurchaseResult.Purchased) + coVerify { storeKitManager.loadPurchasedProducts() } + And("Verify subscription start event") { + val subscriptionStartEvent = + events.value.filterIsInstance() + assert(subscriptionStartEvent.isNotEmpty()) + assert(subscriptionStartEvent.first().product?.fullIdentifier == "product1") + } + } + } + } + } +} From 84d7cfcfdcf9c0918dccd96f30e9c1f48e507e59 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 24 Oct 2024 13:28:40 +0200 Subject: [PATCH 06/37] Update tests --- .../sdk/store/transactions/TransactionManagerTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt index f3547b82..8678bbc2 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -200,7 +200,7 @@ class TransactionManagerTest { runTest { val events = MutableStateFlow(emptyList()) Given("We have loaded products and we can purchase successfully") { - // Pretend a paywall loaded a producy + // Pretend a paywall loaded a product storeKitManager.getProducts(paywall = mockedPaywall) val transactionManager: TransactionManager = manager(track = { e -> @@ -335,7 +335,7 @@ class TransactionManagerTest { @Test fun test_purchase_restored_external() = runTest { - Given("We have not loaded products and have products to restore") { + Given("We want to restore a product externally") { val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -567,7 +567,7 @@ class TransactionManagerTest { @Test fun test_purchase_cancelled_external() = runTest { - Given("We have loaded products and a purchase is pending") { + Given("An external purchase was cancelled") { val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -589,7 +589,7 @@ class TransactionManagerTest { StoreProduct(mockProduct), ), ) - Then("The purchase is pending") { + Then("The purchase is cancelled") { assert(result is PurchaseResult.Cancelled) verify(exactly = 0) { paywallView setProperty "loadingState" value @@ -841,7 +841,7 @@ class TransactionManagerTest { ) } returns PurchaseResult.Purchased() - When("We try to purchase a product with a free trial externally") { + When("We try to purchase the product") { val result = transactionManager.purchase( TransactionManager.PurchaseSource.External( @@ -928,7 +928,7 @@ class TransactionManagerTest { ) } returns PurchaseResult.Purchased() - When("We try to purchase a non-recurring product externally") { + When("We try to purchase the product") { val result = transactionManager.purchase( TransactionManager.PurchaseSource.External(nonRecurringStoreProduct), From b76e56a165a5bd0194a678793633afd41ba802db Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 24 Oct 2024 15:10:46 +0200 Subject: [PATCH 07/37] Wrap purchase methods into Result --- .../main/java/com/superwall/sdk/Superwall.kt | 38 ++++++++++++------- .../superwall/sdk/utilities/ErrorTracking.kt | 4 -- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index c03dc6df..24a7a4d6 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -332,7 +332,10 @@ class Superwall( } val purchaseController = purchaseController - ?: ExternalNativePurchaseController(context = applicationContext, scope = IOScope()) + ?: ExternalNativePurchaseController( + context = applicationContext, + scope = IOScope(), + ) _instance = Superwall( context = applicationContext, @@ -701,12 +704,14 @@ class Superwall( * ``Superwall`` will handle this for you. */ - suspend fun purchase(product: RawStoreProduct): PurchaseResult = - dependencyContainer.transactionManager.purchase( - TransactionManager.PurchaseSource.External( - StoreProduct(product), - ), - ) + suspend fun purchase(product: RawStoreProduct): Result = + withErrorTracking { + dependencyContainer.transactionManager.purchase( + TransactionManager.PurchaseSource.External( + StoreProduct(product), + ), + ) + }.toResult() /** * Initiates a purchase of a `StoreProduct` with a callback. @@ -724,15 +729,17 @@ class Superwall( fun purchase( product: RawStoreProduct, - onFinished: (PurchaseResult) -> Unit, + onFinished: (Result) -> Unit, ) { ioScope.launch { val res = - dependencyContainer.transactionManager.purchase( - TransactionManager.PurchaseSource.External( - StoreProduct(product), - ), - ) + withErrorTracking { + dependencyContainer.transactionManager.purchase( + TransactionManager.PurchaseSource.External( + StoreProduct(product), + ), + ) + }.toResult() onFinished(res) } } @@ -742,7 +749,10 @@ class Superwall( * * Use this function to restore purchases made by the user. * */ - suspend fun restorePurchases() = dependencyContainer.transactionManager.tryToRestorePurchases(null) + suspend fun restorePurchases() = + withErrorTracking { + dependencyContainer.transactionManager.tryToRestorePurchases(null) + }.toResult() override suspend fun eventDidOccur( paywallEvent: PaywallWebEvent, diff --git a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt index 71ff17ab..690f158a 100644 --- a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt +++ b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt @@ -1,6 +1,5 @@ package com.superwall.sdk.utilities -import android.util.Log import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent @@ -58,9 +57,6 @@ internal class ErrorTracker( val exists = cache.read(ErrorLog) if (exists != null) { scope.launch { - Log.e("ErrorTracker", "Error occurred in the SDK") - Log.e("ErrorTracker", exists.message) - Log.e("ErrorTracker", "\n\n ------------------------- \n ${exists.stacktrace} \n\n -------------------------\n") track( InternalSuperwallEvent.ErrorThrown( exists.message, From 32c1fd01103c9d463df39a7f0aea520329201594 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 11 Nov 2024 14:30:59 +0100 Subject: [PATCH 08/37] Clear up after rebase, fix transaction tracking and add source to event --- .../sdk/analytics/internal/TrackingLogic.kt | 2 +- .../trackable/TrackableSuperwallEvent.kt | 8 +++ .../sdk/contrib/threeteen/AmountFormats.kt | 15 +++--- .../sdk/dependencies/DependencyContainer.kt | 3 -- .../sdk/network/device/DeviceHelper.kt | 52 ++++++++++++++----- .../sdk/paywall/presentation/PaywallInfo.kt | 2 +- .../paywall/vc/SuperwallPaywallActivity.kt | 2 - .../vc/web_view/DefaultWebviewClient.kt | 2 +- .../messaging/PaywallMessageHandler.kt | 4 +- .../abstractions/product/RawStoreProduct.kt | 2 +- .../store/transactions/TransactionManager.kt | 30 ++++++++++- .../sdk/contrib/AmountFormatsTest.kt | 10 ++-- .../src/test/java/com/superwall/sdk/utils.kt | 2 +- 13 files changed, 96 insertions(+), 38 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt index 61716a08..7c0c29a3 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt @@ -15,9 +15,9 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import java.net.URI import org.threeten.bp.LocalDateTime import org.threeten.bp.ZoneOffset +import java.net.URI import java.util.* sealed class TrackingLogic { diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt index 0f547c73..332293cb 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt @@ -434,7 +434,15 @@ sealed class InternalSuperwallEvent( val paywallInfo: PaywallInfo, val product: StoreProduct?, val model: StoreTransaction?, + val source: TransactionSource, ) : TrackableSuperwallEvent { + enum class TransactionSource( + val raw: String, + ) { + INTERNAL("SUPERWALL"), + EXTERNAL("APP"), + } + sealed class State { class Start( val product: StoreProduct, diff --git a/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt b/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt index 0d124f03..8da3c253 100644 --- a/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt +++ b/superwall/src/main/java/com/superwall/sdk/contrib/threeteen/AmountFormats.kt @@ -3,8 +3,8 @@ package com.superwall.sdk.contrib.threeteen import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger -import org.threeten.bp.Period import org.threeten.bp.Duration +import org.threeten.bp.Period import org.threeten.bp.format.DateTimeParseException import java.util.* import java.util.regex.Pattern @@ -52,11 +52,10 @@ import java.util.regex.Pattern * This class is immutable and thread-safe. */ object AmountFormats { - - fun interface IntPredicate { fun test(value: Int): Boolean } + /** * The number of days per week. */ @@ -693,9 +692,11 @@ object AmountFormats { init { check(predicateStrs.size + 1 == text.size) { "Invalid word-based resource" } - predicates = predicateStrs.map { predicateStr -> - findPredicate(predicateStr!!) - }.toTypedArray() + predicates = + predicateStrs + .map { predicateStr -> + findPredicate(predicateStr!!) + }.toTypedArray() this.text = text } @@ -719,8 +720,8 @@ object AmountFormats { } buf.append(value).append(text[predicates.size]) } - } + // ------------------------------------------------------------------------- // data holder for a duration unit string and its associated Duration value. internal class DurationUnit constructor( diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index 5c2a1ecb..f9fce745 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -90,7 +90,6 @@ import kotlinx.coroutines.launch import kotlinx.serialization.json.ClassDiscriminatorMode import kotlinx.serialization.json.Json import java.lang.ref.WeakReference -import java.util.Base64 import java.util.Date class DependencyContainer( @@ -430,7 +429,6 @@ class DependencyContainer( } private val paywallJson = Json { encodeDefaults = true } - private val encoder = Base64.getEncoder() override suspend fun makePaywallView( paywall: Paywall, @@ -442,7 +440,6 @@ class DependencyContainer( sessionEventsManager = sessionEventsManager, factory = this@DependencyContainer, ioScope = ioScope, - encoder = encoder, json = paywallJson, mainScope = mainScope(), ) diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index 5bbc8932..8667dde1 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -112,37 +112,63 @@ class DeviceHelper( private val daysSinceInstall: Int get() { - val fromDate = org.threeten.bp.Instant.ofEpochMilli(appInstallDate.time) - val toDate = org.threeten.bp.Instant.now() - val duration = org.threeten.bp.Duration.between(fromDate, toDate) + val fromDate = + org.threeten.bp.Instant + .ofEpochMilli(appInstallDate.time) + val toDate = + org.threeten.bp.Instant + .now() + val duration = + org.threeten.bp.Duration + .between(fromDate, toDate) return duration.toDays().toInt() } private val minutesSinceInstall: Int get() { - val fromDate = org.threeten.bp.Instant.ofEpochMilli(appInstallDate.time) - val toDate = org.threeten.bp.Instant.now() - val duration = org.threeten.bp.Duration.between(fromDate, toDate) + val fromDate = + org.threeten.bp.Instant + .ofEpochMilli(appInstallDate.time) + val toDate = + org.threeten.bp.Instant + .now() + val duration = + org.threeten.bp.Duration + .between(fromDate, toDate) return duration.toMinutes().toInt() } private val daysSinceLastPaywallView: Int? get() { val fromDate = - storage.read(LastPaywallView)?.let { org.threeten.bp.Instant.ofEpochMilli(it.time) } + storage.read(LastPaywallView)?.let { + org.threeten.bp.Instant + .ofEpochMilli(it.time) + } ?: return null - val toDate = org.threeten.bp.Instant.now() - val duration = org.threeten.bp.Duration.between(fromDate, toDate) + val toDate = + org.threeten.bp.Instant + .now() + val duration = + org.threeten.bp.Duration + .between(fromDate, toDate) return duration.toDays().toInt() } private val minutesSinceLastPaywallView: Int? get() { val fromDate = - storage.read(LastPaywallView)?.let { org.threeten.bp.Instant.ofEpochMilli(it.time) } + storage.read(LastPaywallView)?.let { + org.threeten.bp.Instant + .ofEpochMilli(it.time) + } ?: return null - val toDate = org.threeten.bp.Instant.now() - val duration = org.threeten.bp.Duration.between(fromDate, toDate) + val toDate = + org.threeten.bp.Instant + .now() + val duration = + org.threeten.bp.Duration + .between(fromDate, toDate) return duration.toMinutes().toInt() } @@ -242,7 +268,7 @@ class DeviceHelper( else -> "" } } else { - when(connectivityManager.activeNetworkInfo?.type){ + when (connectivityManager.activeNetworkInfo?.type) { ConnectivityManager.TYPE_MOBILE -> return "Cellular" ConnectivityManager.TYPE_WIFI -> return "Wifi" else -> return "" diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index a99a9667..a90b7035 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -335,7 +335,7 @@ data class PaywallInfo( presentation = PaywallPresentationInfo(PaywallPresentationStyle.NONE, PresentationCondition.ALWAYS, 0), buildId = "", cacheKey = "", - isScrollEnabled = true + isScrollEnabled = true, ) } } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt index c980a64c..567d6fa7 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt @@ -23,7 +23,6 @@ import android.widget.FrameLayout import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -497,7 +496,6 @@ class SuperwallPaywallActivity : AppCompatActivity() { return@suspendCoroutine } - createNotificationChannel() notificationPermissionCallback = diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt index 7e30bcdb..220070e4 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt @@ -95,7 +95,7 @@ internal open class DefaultWebviewClient( "Error description unavailable, Android API version < 23", forUrl, ) - } + }, ), ) } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt index 22e47978..6f11795a 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt @@ -1,6 +1,7 @@ package com.superwall.sdk.paywall.vc.web_view.messaging import TemplateLogic +import android.net.Uri import android.util.Base64 import android.webkit.JavascriptInterface import android.webkit.WebView @@ -62,7 +63,6 @@ class PaywallMessageHandler( private val factory: VariablesFactory, private val mainScope: MainScope, private val ioScope: CoroutineScope, - private val encoder: Base64.Encoder = Base64.getEncoder(), private val json: Json = Json { encodeDefaults = true }, ) { private companion object { @@ -217,7 +217,6 @@ class PaywallMessageHandler( event = eventData, factory = factory, json = json, - base64 = encoder, ) passMessageToWebView(base64String = templates) } @@ -283,7 +282,6 @@ class PaywallMessageHandler( event = eventData, factory = factory, json = json, - base64 = encoder, ) val scriptSrc = """ window.paywall.accept64('$templates'); diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt index ea0bc9e3..01b7d186 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt @@ -6,9 +6,9 @@ import com.superwall.sdk.contrib.threeteen.AmountFormats import com.superwall.sdk.utilities.DateUtils import com.superwall.sdk.utilities.dateFormat import kotlinx.serialization.Transient +import org.threeten.bp.Period import java.math.BigDecimal import java.math.RoundingMode -import org.threeten.bp.Period import java.util.Calendar import java.util.Currency import java.util.Locale diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index ac7ee5b7..0af8bb0c 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -3,6 +3,7 @@ package com.superwall.sdk.store.transactions import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent +import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent.Transaction.TransactionSource import com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent import com.superwall.sdk.analytics.superwall.SuperwallEvents import com.superwall.sdk.delegate.PurchaseResult @@ -10,6 +11,7 @@ import com.superwall.sdk.delegate.RestorationResult import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.delegate.subscription_controller.PurchaseController import com.superwall.sdk.dependencies.DeviceHelperFactory +import com.superwall.sdk.dependencies.HasExternalPurchaseControllerFactory import com.superwall.sdk.dependencies.OptionsFactory import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.dependencies.TransactionVerifierFactory @@ -63,7 +65,8 @@ class TransactionManager( TriggerFactory, TransactionVerifierFactory, StoreTransactionFactory, - DeviceHelperFactory + DeviceHelperFactory, + HasExternalPurchaseControllerFactory private var lastPaywallView: PaywallView? = null @@ -104,6 +107,10 @@ class TransactionManager( basePlanId = rawStoreProduct.basePlanId, ) + if (purchaseSource is PurchaseSource.External && factory.makeHasExternalPurchaseController()) { + return result + } + val isEligibleForTrial = rawStoreProduct.selectedOffer != null when (result) { @@ -186,6 +193,11 @@ class TransactionManager( paywallInfo = if (purchaseSource is PurchaseSource.Internal) purchaseSource.paywallView.info else PaywallInfo.empty(), product = product, model = null, + source = + when (purchaseSource) { + is PurchaseSource.External -> TransactionSource.EXTERNAL + is PurchaseSource.Internal -> TransactionSource.INTERNAL + }, ) track(trackedEvent) @@ -230,6 +242,7 @@ class TransactionManager( paywallInfo = paywallInfo, product = product, model = null, + source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -255,8 +268,8 @@ class TransactionManager( paywallInfo = PaywallInfo.empty(), product = product, model = null, + source = TransactionSource.EXTERNAL, ) - track(trackedEvent) } } @@ -285,6 +298,7 @@ class TransactionManager( paywallInfo, product, null, + source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -294,6 +308,9 @@ class TransactionManager( } is PurchaseSource.External -> { + if (factory.makeHasExternalPurchaseController()) { + return + } ioScope.launch { log( message = @@ -308,6 +325,7 @@ class TransactionManager( PaywallInfo.empty(), product, null, + source = TransactionSource.EXTERNAL, ) track(trackedEvent) } @@ -392,6 +410,7 @@ class TransactionManager( paywallInfo, product, null, + source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -412,6 +431,7 @@ class TransactionManager( PaywallInfo.empty(), product, null, + source = TransactionSource.EXTERNAL, ) track(trackedEvent) } @@ -436,6 +456,7 @@ class TransactionManager( paywallInfo, null, null, + source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -456,6 +477,7 @@ class TransactionManager( PaywallInfo.empty(), null, null, + source = TransactionSource.EXTERNAL, ) track(trackedEvent) } @@ -571,6 +593,7 @@ class TransactionManager( paywallInfo, product, null, + source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -599,6 +622,7 @@ class TransactionManager( PaywallInfo.empty(), product, null, + source = TransactionSource.EXTERNAL, ) track(trackedEvent) } @@ -626,6 +650,7 @@ class TransactionManager( paywallInfo, product, transaction, + source = TransactionSource.INTERNAL, ) track(trackedEvent) eventsQueue.flushInternal() @@ -669,6 +694,7 @@ class TransactionManager( PaywallInfo.empty(), product, transaction, + source = TransactionSource.EXTERNAL, ) track(trackedEvent) eventsQueue.flushInternal() diff --git a/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt index 6380c0a6..4f382d96 100644 --- a/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt @@ -13,7 +13,6 @@ import org.threeten.bp.format.DateTimeParseException import java.util.* class AmountFormatsTest { - @Test fun testIso8601Format() { Given("a period and duration") { @@ -86,7 +85,12 @@ class AmountFormatsTest { @Test fun testWordBasedDuration() { Given("a duration with multiple units") { - val duration = Duration.ofHours(25).plusMinutes(30).plusSeconds(45).plusMillis(500) + val duration = + Duration + .ofHours(25) + .plusMinutes(30) + .plusSeconds(45) + .plusMillis(500) When("formatting to word-based with English locale") { val result = AmountFormats.wordBased(duration, Locale.ENGLISH) @@ -212,4 +216,4 @@ class AmountFormatsTest { } } } -} \ No newline at end of file +} diff --git a/superwall/src/test/java/com/superwall/sdk/utils.kt b/superwall/src/test/java/com/superwall/sdk/utils.kt index 78a4a6b6..adba8e65 100644 --- a/superwall/src/test/java/com/superwall/sdk/utils.kt +++ b/superwall/src/test/java/com/superwall/sdk/utils.kt @@ -1,4 +1,5 @@ @file:Suppress("ktlint:standard:function-naming") + package com.superwall.sdk fun assertTrue(value: Boolean) { @@ -13,7 +14,6 @@ fun assertFalse(value: Boolean) { } } - @DslMarker annotation class TestingDSL class GivenWhenThenScope( From 6b0dc6b6599a66c9363dc91cc4220067e68b53ec Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 19 Sep 2024 22:11:08 +0200 Subject: [PATCH 09/37] Downgrade minSDK to version 22 --- .../sdk/paywall/vc/SuperwallPaywallActivity.kt | 2 ++ .../sdk/paywall/vc/web_view/DefaultWebviewClient.kt | 2 +- .../com/superwall/sdk/contrib/AmountFormatsTest.kt | 10 +++------- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt index 567d6fa7..c980a64c 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt @@ -23,6 +23,7 @@ import android.widget.FrameLayout import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -496,6 +497,7 @@ class SuperwallPaywallActivity : AppCompatActivity() { return@suspendCoroutine } + createNotificationChannel() notificationPermissionCallback = diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt index 220070e4..7e30bcdb 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt @@ -95,7 +95,7 @@ internal open class DefaultWebviewClient( "Error description unavailable, Android API version < 23", forUrl, ) - }, + } ), ) } diff --git a/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt index 4f382d96..6380c0a6 100644 --- a/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt @@ -13,6 +13,7 @@ import org.threeten.bp.format.DateTimeParseException import java.util.* class AmountFormatsTest { + @Test fun testIso8601Format() { Given("a period and duration") { @@ -85,12 +86,7 @@ class AmountFormatsTest { @Test fun testWordBasedDuration() { Given("a duration with multiple units") { - val duration = - Duration - .ofHours(25) - .plusMinutes(30) - .plusSeconds(45) - .plusMillis(500) + val duration = Duration.ofHours(25).plusMinutes(30).plusSeconds(45).plusMillis(500) When("formatting to word-based with English locale") { val result = AmountFormats.wordBased(duration, Locale.ENGLISH) @@ -216,4 +212,4 @@ class AmountFormatsTest { } } } -} +} \ No newline at end of file From 321f21dab42e94383e47862c1bfdf84f12c9e2c4 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 11 Nov 2024 14:30:59 +0100 Subject: [PATCH 10/37] Clear up after rebase, fix transaction tracking and add source to event --- .../sdk/paywall/vc/SuperwallPaywallActivity.kt | 2 -- .../sdk/paywall/vc/web_view/DefaultWebviewClient.kt | 2 +- .../com/superwall/sdk/contrib/AmountFormatsTest.kt | 10 +++++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt index c980a64c..567d6fa7 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt @@ -23,7 +23,6 @@ import android.widget.FrameLayout import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -497,7 +496,6 @@ class SuperwallPaywallActivity : AppCompatActivity() { return@suspendCoroutine } - createNotificationChannel() notificationPermissionCallback = diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt index 7e30bcdb..220070e4 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt @@ -95,7 +95,7 @@ internal open class DefaultWebviewClient( "Error description unavailable, Android API version < 23", forUrl, ) - } + }, ), ) } diff --git a/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt index 6380c0a6..4f382d96 100644 --- a/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/contrib/AmountFormatsTest.kt @@ -13,7 +13,6 @@ import org.threeten.bp.format.DateTimeParseException import java.util.* class AmountFormatsTest { - @Test fun testIso8601Format() { Given("a period and duration") { @@ -86,7 +85,12 @@ class AmountFormatsTest { @Test fun testWordBasedDuration() { Given("a duration with multiple units") { - val duration = Duration.ofHours(25).plusMinutes(30).plusSeconds(45).plusMillis(500) + val duration = + Duration + .ofHours(25) + .plusMinutes(30) + .plusSeconds(45) + .plusMillis(500) When("formatting to word-based with English locale") { val result = AmountFormats.wordBased(duration, Locale.ENGLISH) @@ -212,4 +216,4 @@ class AmountFormatsTest { } } } -} \ No newline at end of file +} From d6bf0541dcd27898a3f2cdc91b5c5356c051b718 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 11 Nov 2024 19:24:10 +0100 Subject: [PATCH 11/37] Add observer mode to the purchases --- .../main/java/com/superwall/sdk/Superwall.kt | 48 +++++- .../trackable/TrackableSuperwallEvent.kt | 4 +- .../sdk/billing/GoogleBillingWrapper.kt | 29 +++- .../sdk/config/options/SuperwallOptions.kt | 2 + .../superwall/sdk/delegate/PurchaseResult.kt | 2 +- .../sdk/dependencies/DependencyContainer.kt | 11 +- .../sdk/paywall/vc/web_view/SWWebView.kt | 18 ++- .../com/superwall/sdk/storage/CacheKeys.kt | 9 ++ .../sdk/storage/core_data/CoreDataManager.kt | 2 +- .../sdk/store/PurchasingObserverState.kt | 21 +++ .../abstractions/product/RawStoreProduct.kt | 13 ++ .../store/transactions/TransactionManager.kt | 144 ++++++++++++++++-- .../transactions/TransactionManagerTest.kt | 10 +- 13 files changed, 280 insertions(+), 33 deletions(-) create mode 100644 superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 24a7a4d6..8a875ad1 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -6,10 +6,12 @@ import android.net.Uri import androidx.work.WorkManager import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent +import com.superwall.sdk.billing.toInternalResult import com.superwall.sdk.analytics.superwall.SuperwallEventInfo 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.InternalPurchaseResult import com.superwall.sdk.delegate.PurchaseResult import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.delegate.SuperwallDelegate @@ -51,6 +53,7 @@ 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.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 @@ -707,7 +710,7 @@ class Superwall( suspend fun purchase(product: RawStoreProduct): Result = withErrorTracking { dependencyContainer.transactionManager.purchase( - TransactionManager.PurchaseSource.External( + TransactionManager.PurchaseSource.ExternalPurchase( StoreProduct(product), ), ) @@ -735,7 +738,7 @@ class Superwall( val res = withErrorTracking { dependencyContainer.transactionManager.purchase( - TransactionManager.PurchaseSource.External( + TransactionManager.PurchaseSource.ExternalPurchase( StoreProduct(product), ), ) @@ -744,6 +747,47 @@ class Superwall( } } + fun observe(state: PurchasingObserverState) { + ioScope.launchWithTracking { + if (!options.shouldObservePurchases) { + Logger.debug( + logLevel = LogLevel.error, + scope = LogScope.superwallCore, + message = + "You are trying to observe purchases but the SuperwallOption shouldObservePurchases is " + + "false. Please set it to true to be able to observe purchases.", + ) + return@launchWithTracking + } + when (state) { + is PurchasingObserverState.PurchaseWillBegin -> { + val product = StoreProduct(RawStoreProduct.from(state.productId)) + dependencyContainer.transactionManager.prepareToPurchase( + product, + source = TransactionManager.PurchaseSource.ObserverMode(product), + ) + } + + is PurchasingObserverState.PurchaseResult -> { + val result = (state.result to state.purchases).toInternalResult() + for (internalPurchaseResult in result) { + dependencyContainer.transactionManager.handle( + internalPurchaseResult, + state, + ) + } + } + + is PurchasingObserverState.PurchaseError -> { + dependencyContainer.transactionManager.handle( + InternalPurchaseResult.Failed(state.error), + state, + ) + } + } + } + } + /** * Restores purchases * diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt index 332293cb..952c8147 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt @@ -435,11 +435,13 @@ sealed class InternalSuperwallEvent( val product: StoreProduct?, val model: StoreTransaction?, val source: TransactionSource, + val isObserved: Boolean, ) : TrackableSuperwallEvent { enum class TransactionSource( val raw: String, ) { INTERNAL("SUPERWALL"), + OBSERVER("OBSERVER"), EXTERNAL("APP"), } @@ -519,7 +521,7 @@ sealed class InternalSuperwallEvent( get() = superwallEvent.rawName override val canImplicitlyTriggerPaywall: Boolean - get() = superwallEvent.canImplicitlyTriggerPaywall + get() = if (isObserved) false else superwallEvent.canImplicitlyTriggerPaywall override suspend fun getSuperwallParameters(): HashMap { return when (state) { diff --git a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt index 68150f47..68f69160 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt @@ -8,6 +8,8 @@ import com.android.billingclient.api.BillingResult import com.android.billingclient.api.Purchase import com.android.billingclient.api.PurchasesUpdatedListener import com.superwall.sdk.delegate.InternalPurchaseResult +import com.superwall.sdk.dependencies.HasExternalPurchaseControllerFactory +import com.superwall.sdk.dependencies.OptionsFactory import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope @@ -39,6 +41,7 @@ class GoogleBillingWrapper( val context: Context, val ioScope: IOScope, val appLifecycleObserver: AppLifecycleObserver, + val factory: Factory, ) : PurchasesUpdatedListener, BillingClientStateListener, Billing { @@ -46,7 +49,14 @@ class GoogleBillingWrapper( private val productsCache = ConcurrentHashMap>() } + interface Factory : + HasExternalPurchaseControllerFactory, + OptionsFactory + private val threadHandler = Handler(ioScope) + private val shouldFinishTransactions: Boolean + get() = + !factory.makeHasExternalPurchaseController() && !factory.makeSuperwallOptions().shouldObservePurchases @get:Synchronized @set:Synchronized @@ -538,7 +548,11 @@ class GoogleBillingWrapper( purchaseResults.asStateFlow().filter { it != null }.first().let { purchaseResult -> return when (purchaseResult) { is InternalPurchaseResult.Purchased -> { - return factory.makeStoreTransaction(purchaseResult.purchase) + if (shouldFinishTransactions) { + return factory.makeStoreTransaction(purchaseResult.purchase) + } else { + null + } } is InternalPurchaseResult.Cancelled -> { @@ -578,3 +592,16 @@ class GoogleBillingWrapper( } } } + +fun Pair?>.toInternalResult(): List { + val (result, purchases) = this + return if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { + purchases.map { + InternalPurchaseResult.Purchased(it) + } + } else if (result.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { + listOf(InternalPurchaseResult.Cancelled) + } else { + listOf(InternalPurchaseResult.Failed(Exception(result.responseCode.toString()))) + } +} diff --git a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt index 1193203a..12b3a2bf 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt @@ -12,6 +12,8 @@ class SuperwallOptions { // Configures the appearance and behavior of paywalls. var paywalls: PaywallOptions = PaywallOptions() + var shouldObservePurchases = false + // **WARNING**: The different network environments that the SDK should use. // Only use this enum to set ``SuperwallOptions/networkEnvironment-swift.property`` // if told so explicitly by the Superwall team. diff --git a/superwall/src/main/java/com/superwall/sdk/delegate/PurchaseResult.kt b/superwall/src/main/java/com/superwall/sdk/delegate/PurchaseResult.kt index bf8192d1..ac37af06 100644 --- a/superwall/src/main/java/com/superwall/sdk/delegate/PurchaseResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/delegate/PurchaseResult.kt @@ -14,7 +14,7 @@ sealed class InternalPurchaseResult { object Pending : InternalPurchaseResult() data class Failed( - val error: Exception, + val error: Throwable, ) : InternalPurchaseResult() } diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index f9fce745..d9ff9b22 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -124,7 +124,8 @@ class DependencyContainer( ConfigAttributesFactory, PaywallPreload.Factory, ViewStoreFactory, - SuperwallScopeFactory { + SuperwallScopeFactory, + GoogleBillingWrapper.Factory { var network: Network override var api: Api override var deviceHelper: DeviceHelper @@ -204,7 +205,12 @@ class DependencyContainer( } googleBillingWrapper = - GoogleBillingWrapper(context, ioScope, appLifecycleObserver = appLifecycleObserver) + GoogleBillingWrapper( + context, + ioScope, + appLifecycleObserver = appLifecycleObserver, + this, + ) var purchaseController = InternalPurchaseController( @@ -360,6 +366,7 @@ class DependencyContainer( storeKitManager = storeKitManager, purchaseController = purchaseController, eventsQueue = eventsQueue, + storage = storage, activityProvider, factory = this, track = { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt index 967dcdfb..51c31a78 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt @@ -11,8 +11,8 @@ import android.view.inputmethod.BaseInputConnection import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.webkit.ConsoleMessage -import android.webkit.RenderProcessGoneDetail import android.webkit.CookieManager +import android.webkit.RenderProcessGoneDetail import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient @@ -77,7 +77,8 @@ class SWWebView( return true } } - private var lastWebViewClient : WebViewClient? = null + + private var lastWebViewClient: WebViewClient? = null internal fun prepareWebview() { addJavascriptInterface(messageHandler, "SWAndroid") @@ -158,11 +159,12 @@ class SWWebView( override fun loadUrl(url: String) { prepareWebview() - val client = DefaultWebviewClient( - forUrl = url, - ioScope = CoroutineScope(Dispatchers.IO), - onWebViewCrash = onRenderProcessCrashed, - ) + val client = + DefaultWebviewClient( + forUrl = url, + ioScope = CoroutineScope(Dispatchers.IO), + onWebViewCrash = onRenderProcessCrashed, + ) this.webViewClient = client if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { @@ -352,7 +354,7 @@ class SWWebView( internal fun webViewExists(): Boolean = try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - WebView.getCurrentWebViewPackage()!=null + WebView.getCurrentWebViewPackage() != null } else { runCatching { CookieManager.getInstance() }.isSuccess } diff --git a/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt b/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt index 5ed49a5f..a0d1b92b 100644 --- a/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt +++ b/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt @@ -279,6 +279,15 @@ internal object SavedTransactions : Storable> { override val serializer: KSerializer> get() = SetSerializer(SavedTransaction.serializer()) } + +internal object PurchasingProductdIds : Storable> { + override val key: String + get() = "store.purchasingProductIds" + override val directory: SearchPathDirectory + get() = SearchPathDirectory.APP_SPECIFIC_DOCUMENTS + override val serializer: KSerializer> + get() = SetSerializer(String.serializer()) +} //endregion // region Serializers diff --git a/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt b/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt index 20969710..d4a62b82 100644 --- a/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/storage/core_data/CoreDataManager.kt @@ -1,7 +1,6 @@ package com.superwall.sdk.storage.core_data import android.content.Context -import java.util.Calendar import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger @@ -13,6 +12,7 @@ import com.superwall.sdk.storage.core_data.entities.ManagedTriggerRuleOccurrence import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.util.Calendar import java.util.Date import kotlin.coroutines.CoroutineContext diff --git a/superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt b/superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt new file mode 100644 index 00000000..bff5dc6c --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt @@ -0,0 +1,21 @@ +package com.superwall.sdk.store + +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase + +sealed class PurchasingObserverState { + class PurchaseWillBegin( + val productId: ProductDetails, + ) : PurchasingObserverState() + + class PurchaseResult( + val result: BillingResult, + val purchases: List?, + ) : PurchasingObserverState() + + class PurchaseError( + val product: ProductDetails, + val error: Throwable, + ) : PurchasingObserverState() +} diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt index 01b7d186..cdb29799 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt @@ -2,6 +2,7 @@ package com.superwall.sdk.store.abstractions.product import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.ProductDetails.PricingPhase import com.android.billingclient.api.ProductDetails.SubscriptionOfferDetails +import com.superwall.sdk.billing.DecomposedProductIds import com.superwall.sdk.contrib.threeteen.AmountFormats import com.superwall.sdk.utilities.DateUtils import com.superwall.sdk.utilities.dateFormat @@ -19,6 +20,18 @@ class RawStoreProduct( val basePlanId: String?, private val offerType: OfferType?, ) : StoreProductType { + companion object { + fun from(details: ProductDetails): RawStoreProduct { + val ids = DecomposedProductIds.from(details.productId) + return RawStoreProduct( + underlyingProductDetails = details, + fullIdentifier = details.productId, + basePlanId = ids.basePlanId, + offerType = ids.offerType, + ) + } + } + @Transient private val priceFormatterProvider = PriceFormatterProvider() diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index 0af8bb0c..b638593f 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -6,10 +6,12 @@ import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent.Transaction.TransactionSource import com.superwall.sdk.analytics.internal.trackable.TrackableSuperwallEvent import com.superwall.sdk.analytics.superwall.SuperwallEvents +import com.superwall.sdk.delegate.InternalPurchaseResult 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.dependencies.CacheFactory import com.superwall.sdk.dependencies.DeviceHelperFactory import com.superwall.sdk.dependencies.HasExternalPurchaseControllerFactory import com.superwall.sdk.dependencies.OptionsFactory @@ -29,7 +31,11 @@ import com.superwall.sdk.paywall.vc.PaywallView import com.superwall.sdk.paywall.vc.SuperwallPaywallActivity import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState import com.superwall.sdk.storage.EventsQueue +import com.superwall.sdk.storage.PurchasingProductdIds +import com.superwall.sdk.storage.Storage +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.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import kotlinx.coroutines.launch @@ -38,6 +44,7 @@ class TransactionManager( private val storeKitManager: StoreKitManager, private val purchaseController: PurchaseController, private val eventsQueue: EventsQueue, + private val storage: Storage, private val activityProvider: ActivityProvider, private val factory: Factory, private val ioScope: IOScope, @@ -55,7 +62,11 @@ class TransactionManager( val paywallView: PaywallView, ) : PurchaseSource() - data class External( + data class ExternalPurchase( + val product: StoreProduct, + ) : PurchaseSource() + + data class ObserverMode( val product: StoreProduct, ) : PurchaseSource() } @@ -66,10 +77,80 @@ class TransactionManager( TransactionVerifierFactory, StoreTransactionFactory, DeviceHelperFactory, + CacheFactory, HasExternalPurchaseControllerFactory + private val shouldFinishTransactions = + !factory.makeHasExternalPurchaseController() && + !factory.makeSuperwallOptions().shouldObservePurchases + private var lastPaywallView: PaywallView? = null + internal suspend fun handle( + result: InternalPurchaseResult, + state: PurchasingObserverState, + ) { + when (result) { + is InternalPurchaseResult.Purchased -> { + val state = state as PurchasingObserverState.PurchaseResult + state.purchases?.forEach { purchase -> + purchase.products.map { + storeKitManager.productsByFullId[it] ?.let { product -> + didPurchase(product, PurchaseSource.ObserverMode(product), product.hasFreeTrial) + } + } + } + } + + InternalPurchaseResult.Cancelled -> { + val state = state as PurchasingObserverState.PurchaseError + val product = StoreProduct(RawStoreProduct.from(state.product)) + trackCancelled( + product = product, + purchaseSource = PurchaseSource.ObserverMode(product), + ) + } + is InternalPurchaseResult.Failed -> { + val state = state as PurchasingObserverState.PurchaseError + val product = StoreProduct(RawStoreProduct.from(state.product)) + trackFailure( + state.error.localizedMessage ?: "Unknown error", + product, + PurchaseSource.ObserverMode(product), + ) + } + InternalPurchaseResult.Pending -> { + val result = state as PurchasingObserverState.PurchaseResult + result.purchases?.forEach { purchase -> + purchase.products.map { + storeKitManager.productsByFullId[it] ?.let { product -> + handlePendingTransaction(PurchaseSource.ObserverMode(product)) + } + } + } + } + InternalPurchaseResult.Restored -> { + val state = state as PurchasingObserverState.PurchaseResult + state.purchases?.forEach { purchase -> + purchase.products.map { + storeKitManager.productsByFullId[it] ?.let { product -> + didRestore(product, PurchaseSource.ObserverMode(product)) + } + } + } + } + } + } + + fun updatePaymentQueue(removedTransactions: List) { + var stored = storage.read(PurchasingProductdIds) + val remainingTransactions = + stored?.filter { transaction -> + !removedTransactions.any { it == transaction } + } ?: emptyList() + storage.write(PurchasingProductdIds, remainingTransactions.toSet()) + } + suspend fun purchase(purchaseSource: PurchaseSource): PurchaseResult { val product = when (purchaseSource) { @@ -82,9 +163,10 @@ class TransactionManager( return PurchaseResult.Failed("Product not found") } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase -> { purchaseSource.product } + is PurchaseSource.ObserverMode -> purchaseSource.product } val rawStoreProduct = product.rawStoreProduct @@ -107,7 +189,7 @@ class TransactionManager( basePlanId = rawStoreProduct.basePlanId, ) - if (purchaseSource is PurchaseSource.External && factory.makeHasExternalPurchaseController()) { + if (purchaseSource is PurchaseSource.ExternalPurchase && factory.makeHasExternalPurchaseController()) { return result } @@ -193,10 +275,12 @@ class TransactionManager( paywallInfo = if (purchaseSource is PurchaseSource.Internal) purchaseSource.paywallView.info else PaywallInfo.empty(), product = product, model = null, + isObserved = factory.makeSuperwallOptions().shouldObservePurchases, source = when (purchaseSource) { - is PurchaseSource.External -> TransactionSource.EXTERNAL + is PurchaseSource.ExternalPurchase -> TransactionSource.EXTERNAL is PurchaseSource.Internal -> TransactionSource.INTERNAL + is PurchaseSource.ObserverMode -> TransactionSource.OBSERVER }, ) track(trackedEvent) @@ -242,6 +326,7 @@ class TransactionManager( paywallInfo = paywallInfo, product = product, model = null, + isObserved = factory.makeSuperwallOptions().shouldObservePurchases, source = TransactionSource.INTERNAL, ) @@ -249,7 +334,7 @@ class TransactionManager( } } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase, is PurchaseSource.ObserverMode -> { log( message = "Transaction Error: $errorMessage", info = mapOf("product_id" to product.fullIdentifier), @@ -268,6 +353,7 @@ class TransactionManager( paywallInfo = PaywallInfo.empty(), product = product, model = null, + isObserved = purchaseSource is PurchaseSource.ObserverMode, source = TransactionSource.EXTERNAL, ) track(trackedEvent) @@ -276,10 +362,13 @@ class TransactionManager( } } - private suspend fun prepareToPurchase( + internal suspend fun prepareToPurchase( product: StoreProduct, source: PurchaseSource, ) { + val isObserved = + source is PurchaseSource.ObserverMode + when (source) { is PurchaseSource.Internal -> { ioScope.launch { @@ -298,6 +387,7 @@ class TransactionManager( paywallInfo, product, null, + isObserved = isObserved, source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -307,10 +397,17 @@ class TransactionManager( lastPaywallView = source.paywallView } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase, is PurchaseSource.ObserverMode -> { if (factory.makeHasExternalPurchaseController()) { return } + // If an external purchase controller is being used, skip because this will + // get called by the purchase function of the purchase controller. + val options = factory.makeSuperwallOptions() + if (!options.shouldObservePurchases && factory.makeHasExternalPurchaseController()) { + return + } + ioScope.launch { log( message = @@ -325,6 +422,7 @@ class TransactionManager( PaywallInfo.empty(), product, null, + isObserved = isObserved, source = TransactionSource.EXTERNAL, ) track(trackedEvent) @@ -368,7 +466,7 @@ class TransactionManager( } } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase, is PurchaseSource.ObserverMode -> { log( message = "Transaction Succeeded", info = mapOf("product_id" to product.fullIdentifier), @@ -390,6 +488,9 @@ class TransactionManager( product: StoreProduct, purchaseSource: PurchaseSource, ) { + val isObserved = + purchaseSource is PurchaseSource.ObserverMode + when (purchaseSource) { is PurchaseSource.Internal -> { ioScope.launch { @@ -410,6 +511,7 @@ class TransactionManager( paywallInfo, product, null, + isObserved = isObserved, source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -417,7 +519,7 @@ class TransactionManager( purchaseSource.paywallView.loadingState = PaywallLoadingState.Ready() } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase, is PurchaseSource.ObserverMode -> { ioScope.launch { log( message = "Transaction Abandoned", @@ -431,6 +533,7 @@ class TransactionManager( PaywallInfo.empty(), product, null, + isObserved = isObserved, source = TransactionSource.EXTERNAL, ) track(trackedEvent) @@ -439,6 +542,9 @@ class TransactionManager( } private suspend fun handlePendingTransaction(purchaseSource: PurchaseSource) { + val isObserved = + purchaseSource is PurchaseSource.ObserverMode + when (purchaseSource) { is PurchaseSource.Internal -> { ioScope.launch { @@ -456,6 +562,7 @@ class TransactionManager( paywallInfo, null, null, + isObserved = factory.makeSuperwallOptions().shouldObservePurchases, source = TransactionSource.INTERNAL, ) track(trackedEvent) @@ -466,7 +573,9 @@ class TransactionManager( ) } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase, + is PurchaseSource.ObserverMode, + -> { ioScope.launch { log(message = "Transaction Pending") } @@ -477,6 +586,7 @@ class TransactionManager( PaywallInfo.empty(), null, null, + isObserved = isObserved, source = TransactionSource.EXTERNAL, ) track(trackedEvent) @@ -594,6 +704,7 @@ class TransactionManager( product, null, source = TransactionSource.INTERNAL, + isObserved = factory.makeSuperwallOptions().shouldObservePurchases, ) track(trackedEvent) @@ -603,7 +714,7 @@ class TransactionManager( ) } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase -> { ioScope.launch { log( message = "Transaction Error", @@ -623,9 +734,13 @@ class TransactionManager( product, null, source = TransactionSource.EXTERNAL, + isObserved = factory.makeSuperwallOptions().shouldObservePurchases, ) track(trackedEvent) } + is PurchaseSource.ObserverMode -> { + // No-op + } } } @@ -635,6 +750,9 @@ class TransactionManager( purchaseSource: PurchaseSource, didStartFreeTrial: Boolean, ) { + val isObserved = + purchaseSource is PurchaseSource.ObserverMode + when (purchaseSource) { is PurchaseSource.Internal -> { val paywallView = lastPaywallView ?: return @@ -651,6 +769,7 @@ class TransactionManager( product, transaction, source = TransactionSource.INTERNAL, + isObserved = isObserved, ) track(trackedEvent) eventsQueue.flushInternal() @@ -687,7 +806,7 @@ class TransactionManager( lastPaywallView = null } - is PurchaseSource.External -> { + is PurchaseSource.ExternalPurchase, is PurchaseSource.ObserverMode -> { val trackedEvent = InternalSuperwallEvent.Transaction( InternalSuperwallEvent.Transaction.State.Complete(product, transaction), @@ -695,6 +814,7 @@ class TransactionManager( product, transaction, source = TransactionSource.EXTERNAL, + isObserved = isObserved, ) track(trackedEvent) eventsQueue.flushInternal() diff --git a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt index 8678bbc2..780f459b 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -268,7 +268,7 @@ class TransactionManagerTest { When("We try to purchase a product externally") { val result = transactionManager.purchase( - TransactionManager.PurchaseSource.External( + TransactionManager.PurchaseSource.ExternalPurchase( StoreProduct(mockProduct), ), ) @@ -585,7 +585,7 @@ class TransactionManagerTest { When("We try to purchase a product from the paywall") { val result = transactionManager.purchase( - TransactionManager.PurchaseSource.External( + TransactionManager.PurchaseSource.ExternalPurchase( StoreProduct(mockProduct), ), ) @@ -844,7 +844,7 @@ class TransactionManagerTest { When("We try to purchase the product") { val result = transactionManager.purchase( - TransactionManager.PurchaseSource.External( + TransactionManager.PurchaseSource.ExternalPurchase( StoreProduct(mockProduct), ), ) @@ -931,7 +931,7 @@ class TransactionManagerTest { When("We try to purchase the product") { val result = transactionManager.purchase( - TransactionManager.PurchaseSource.External(nonRecurringStoreProduct), + TransactionManager.PurchaseSource.ExternalPurchase(nonRecurringStoreProduct), ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) @@ -1025,7 +1025,7 @@ class TransactionManagerTest { When("We try to purchase a subscription without trial externally") { val result = transactionManager.purchase( - TransactionManager.PurchaseSource.External( + TransactionManager.PurchaseSource.ExternalPurchase( StoreProduct(mockProduct), ), ) From 2464724f9012a3096a83efbbe7084102e6e2fd0f Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Sat, 16 Nov 2024 20:42:33 +0100 Subject: [PATCH 12/37] Fix base64 handling --- .../expression_evaluator/ExpressionEvaluatorParams.kt | 2 +- .../sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt | 2 +- .../sdk/paywall/vc/web_view/templating/TemplateLogic.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt index 41d6203b..a71043a1 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt @@ -51,4 +51,4 @@ data class JavascriptExpressionEvaluatorParams( } } -fun ByteArray.toBase64(): String = Base64.encodeToString(this, Base64.DEFAULT) +fun ByteArray.toBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt index 6f11795a..fe35e1f7 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt @@ -202,7 +202,7 @@ class PaywallMessageHandler( // Encode the JSON string to Base64 val base64Event = - Base64.encodeToString(jsonString.toByteArray(StandardCharsets.UTF_8), Base64.DEFAULT) + Base64.encodeToString(jsonString.toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) passMessageToWebView(base64String = base64Event) } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt index f15cc7aa..38a6e4ae 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt @@ -56,7 +56,7 @@ object TemplateLogic { "!!! Template Logic: $templatesString", ) - return android.util.Base64.encodeToString(templatesData, android.util.Base64.DEFAULT) + return android.util.Base64.encodeToString(templatesData, android.util.Base64.NO_WRAP) } // private fun swProductTemplate( From 38961f5fba139513f25e0ba9dda93cb1df18266c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 12 Dec 2024 17:44:37 +0100 Subject: [PATCH 13/37] Update superscript version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa4bead9..fe662d66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,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" From c504ef711f66e476a9778e26c42b140f2b5f5815 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 12 Dec 2024 17:38:06 +0100 Subject: [PATCH 14/37] Remove deprecated naming and cleanup the code --- CHANGELOG.md | 1 + README.md | 2 +- .../test/FlowScreenshotTestExecutor.kt | 4 +- .../example/superapp/utils/TestingUtils.kt | 2 +- app/src/main/AndroidManifest.xml | 2 +- .../com/superwall/superapp/ComposeActivity.kt | 4 +- .../superwall/superapp/test/UITestHandler.kt | 66 +++++++++--------- .../superwall/superapp/test/UITestMocks.kt | 18 +---- example/app/src/main/AndroidManifest.xml | 2 +- .../sdk/compose/PaywallComposable.kt | 8 +-- .../config/ConfigManagerInstrumentedTest.kt | 30 ++++---- .../vc/webview/WebviewFallbackClientTest.kt | 12 ++-- .../TestPaywallMessageHandlerDelegate.kt | 6 +- .../main/java/com/superwall/sdk/Superwall.kt | 43 ++++-------- .../sdk/analytics/internal/TrackingLogic.kt | 2 +- .../trackable/TrackableSuperwallEvent.kt | 4 +- .../sdk/analytics/superwall/SuperwallEvent.kt | 2 +- .../analytics/superwall/SuperwallEvents.kt | 3 - .../com/superwall/sdk/config/ConfigManager.kt | 6 +- .../superwall/sdk/config/PaywallPreload.kt | 2 +- .../com/superwall/sdk/debug/DebugManager.kt | 14 +--- .../java/com/superwall/sdk/debug/DebugView.kt | 12 ++-- .../sdk/dependencies/DependencyContainer.kt | 39 ++++++----- .../sdk/dependencies/FactoryProtocols.kt | 30 ++------ .../sdk/game/GameControllerManager.kt | 4 +- .../sdk/models/triggers/TriggerResult.kt | 4 +- .../sdk/network/device/DeviceHelper.kt | 3 +- .../sdk/paywall/manager/PaywallManager.kt | 27 +------- .../sdk/paywall/manager/PaywallViewCache.kt | 11 ++- .../sdk/paywall/presentation/PaywallInfo.kt | 2 - .../get_paywall/InternalGetPaywall.kt | 7 +- .../get_paywall/PublicGetPaywall.kt | 6 +- .../internal/GetPaywallComponents.kt | 2 +- .../internal/InternalPresentation.kt | 2 +- .../PaywallPresentationRequestStatus.kt | 2 - .../internal/operators/GetExperiment.kt | 2 +- .../internal/operators/GetPaywallVC.kt | 4 +- .../internal/operators/GetPresenter.kt | 2 +- .../internal/operators/PresentPaywall.kt | 25 ++----- .../internal/request/PresentationRequest.kt | 5 +- .../presentation/result/PresentationResult.kt | 2 +- .../javascript/DefaultJavascriptEvalutor.kt | 2 +- .../paywall/request/PaywallRequestManager.kt | 6 +- .../vc/delegate/PaywallViewCallback.kt | 47 ------------- .../sdk/paywall/{vc => view}/LoadingView.kt | 10 +-- .../sdk/paywall/{vc => view}/PaywallView.kt | 68 ++++++------------- .../sdk/paywall/{vc => view}/ShimmerView.kt | 12 ++-- .../{vc => view}/SuperwallPaywallActivity.kt | 4 +- .../{vc => view}/SuperwallStoreOwner.kt | 2 +- .../{vc => view}/Survey/SurveyManager.kt | 6 +- .../Survey/SurveyPresentationResult.kt | 2 +- .../sdk/paywall/{vc => view}/ViewStorage.kt | 2 +- .../{vc => view}/ViewStorageViewModel.kt | 2 +- .../view/delegate/PaywallViewCallback.kt | 32 +++++++++ .../delegate/PaywallViewDelegateAdapter.kt | 14 +--- .../webview}/DefaultWebviewClient.kt | 2 +- .../web_view => view/webview}/SWWebView.kt | 6 +- .../webview}/ScrollDisabled.kt | 2 +- .../webview}/WebviewClientEvent.kt | 2 +- .../webview}/WebviewFallbackClient.kt | 2 +- .../webview}/messaging/PaywallMessage.kt | 2 +- .../messaging/PaywallMessageHandler.kt | 19 ++---- .../webview}/messaging/PaywallWebEvent.kt | 2 +- .../messaging/RawWebMessageHandler.kt | 6 +- .../webview}/templating/TemplateLogic.kt | 4 +- .../templating/models/DeviceTemplate.kt | 2 +- .../templating/models/FreeTrialTemplate.kt | 2 +- .../templating/models/ProductTemplate.kt | 0 .../webview}/templating/models/Variables.kt | 2 +- .../sdk/store/StoreKitManagerInterface.kt | 2 +- .../{StoreKitManager.kt => StoreManager.kt} | 2 +- .../store/transactions/TransactionManager.kt | 24 +++---- .../notifications/NotificationScheduler.kt | 2 +- .../notifications/NotificationWorker.kt | 2 +- .../webview}/PaywallMessageTest.kt | 2 +- .../templating/models/DeviceTemplateTest.kt | 2 +- ...eKitManagerTest.kt => StoreManagerTest.kt} | 16 ++--- .../transactions/TransactionManagerTest.kt | 46 ++++++------- 78 files changed, 305 insertions(+), 479 deletions(-) delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/vc/delegate/PaywallViewCallback.kt rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/LoadingView.kt (85%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/PaywallView.kt (91%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/ShimmerView.kt (93%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/SuperwallPaywallActivity.kt (99%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/SuperwallStoreOwner.kt (94%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/Survey/SurveyManager.kt (98%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/Survey/SurveyPresentationResult.kt (74%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/ViewStorage.kt (91%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/ViewStorageViewModel.kt (88%) create mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/view/delegate/PaywallViewCallback.kt rename superwall/src/main/java/com/superwall/sdk/paywall/{vc => view}/delegate/PaywallViewDelegateAdapter.kt (50%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/DefaultWebviewClient.kt (98%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/SWWebView.kt (98%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/ScrollDisabled.kt (90%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/WebviewClientEvent.kt (96%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/WebviewFallbackClient.kt (99%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/messaging/PaywallMessage.kt (98%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/messaging/PaywallMessageHandler.kt (94%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/messaging/PaywallWebEvent.kt (94%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/messaging/RawWebMessageHandler.kt (90%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/templating/TemplateLogic.kt (94%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/templating/models/DeviceTemplate.kt (97%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/templating/models/FreeTrialTemplate.kt (77%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/templating/models/ProductTemplate.kt (100%) rename superwall/src/main/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/templating/models/Variables.kt (96%) rename superwall/src/main/java/com/superwall/sdk/store/{StoreKitManager.kt => StoreManager.kt} (99%) rename superwall/src/test/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/PaywallMessageTest.kt (96%) rename superwall/src/test/java/com/superwall/sdk/paywall/{vc/web_view => view/webview}/templating/models/DeviceTemplateTest.kt (98%) rename superwall/src/test/java/com/superwall/sdk/store/{StoreKitManagerTest.kt => StoreManagerTest.kt} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 451537c1..d747376f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw - 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 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/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt b/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt index 94b1c8c5..bf0d7013 100644 --- a/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt +++ b/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt @@ -65,7 +65,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 +76,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 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..6229b3c4 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,7 @@ 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.paywall.view.ShimmerView import com.superwall.superapp.MainActivity import com.superwall.superapp.test.UITestInfo import kotlinx.coroutines.CoroutineScope 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 a8a2b53f..2185f751 100644 --- a/app/src/main/java/com/superwall/superapp/ComposeActivity.kt +++ b/app/src/main/java/com/superwall/superapp/ComposeActivity.kt @@ -28,8 +28,8 @@ import androidx.compose.ui.unit.dp 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/test/UITestHandler.kt b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt index 5dafb43a..0d9f5a29 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -17,7 +17,7 @@ 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 @@ -206,16 +206,16 @@ object UITestHandler { Superwall.instance.setUserAttributes(mapOf("first_name" to "Claire")) Superwall.instance.register(event = "present_data") events.first { it is SuperwallEvent.PaywallWebviewLoadComplete } - // Dismiss any view controllers + // Dismiss any views delay(8.seconds) - // Dismiss any view controllers + // Dismiss any views Superwall.instance.dismiss() Superwall.instance.setUserAttributes(mapOf("first_name" to null)) 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") @@ -249,7 +249,7 @@ object UITestHandler { delay(8000) - // Dismiss any view controllers + // Dismiss any views Superwall.instance.dismiss() }, ) @@ -271,7 +271,7 @@ object UITestHandler { delay(5000) - // Dismiss any view controllers + // Dismiss any views Superwall.instance.dismiss() Superwall.instance.register(event = "present_always") @@ -280,7 +280,7 @@ object UITestHandler { delay(5000) - // Dismiss any view controllers + // Dismiss any views Superwall.instance.dismiss() var handler = PaywallPresentationHandler() @@ -333,7 +333,7 @@ object UITestHandler { delay(8000) - // Dismiss any view controllers + // Dismiss any views Superwall.instance.dismiss() // Set identity @@ -347,7 +347,7 @@ object UITestHandler { delay(8000) - // Dismiss any view controllers + // Dismiss any views Superwall.instance.dismiss() // Present paywall @@ -369,14 +369,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 +397,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 +412,7 @@ object UITestHandler { delay(8000) - // Dismiss any view controllers + // Dismiss any views Superwall.instance.dismiss() // Set identity @@ -668,13 +668,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 +688,18 @@ 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 +709,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()) }, ) @@ -1042,18 +1042,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 +1256,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 +1267,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 = 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/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/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt b/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt index ff2c0043..62400e09 100644 --- a/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt +++ b/superwall-compose/src/main/java/com/superwall/sdk/compose/PaywallComposable.kt @@ -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/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt index 7bb77862..dda287bd 100644 --- a/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt +++ b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt @@ -35,7 +35,7 @@ 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.store.StoreManager import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify @@ -65,7 +65,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 +76,7 @@ class ConfigManagerUnderTest( storage = storage, network = network, paywallManager = paywallManager, - storeKitManager = storeKitManager, + storeManager = storeManager, factory = factory, deviceHelper = deviceHelper, options = SuperwallOptions(), @@ -134,7 +134,7 @@ class ConfigManagerTests { storage = storage, network = network, paywallManager = dependencyContainer.paywallManager, - storeKitManager = dependencyContainer.storeKitManager, + storeManager = dependencyContainer.storeManager, factory = dependencyContainer, deviceHelper = mockDeviceHelper, assignments = assignments, @@ -179,7 +179,7 @@ class ConfigManagerTests { storage = storage, network = network, paywallManager = dependencyContainer.paywallManager, - storeKitManager = dependencyContainer.storeKitManager, + storeManager = dependencyContainer.storeManager, factory = dependencyContainer, deviceHelper = mockDeviceHelper, assignments = assignments, @@ -223,7 +223,7 @@ class ConfigManagerTests { storage = storage, network = network, paywallManager = dependencyContainer.paywallManager, - storeKitManager = dependencyContainer.storeKitManager, + storeManager = dependencyContainer.storeManager, factory = dependencyContainer, deviceHelper = mockDeviceHelper, assignments = assignments, @@ -269,7 +269,7 @@ class ConfigManagerTests { storage = storage, network = network, paywallManager = dependencyContainer.paywallManager, - storeKitManager = dependencyContainer.storeKitManager, + storeManager = dependencyContainer.storeManager, factory = dependencyContainer, deviceHelper = mockDeviceHelper, assignments = assignmentStore, @@ -364,7 +364,7 @@ class ConfigManagerTests { storage, mockNetwork, mockPaywallManager, - dependencyContainer.storeKitManager, + dependencyContainer.storeManager, mockContainer, mockDeviceHelper, assignments = assignments, @@ -437,7 +437,7 @@ class ConfigManagerTests { storage, mockNetwork, mockPaywallManager, - dependencyContainer.storeKitManager, + dependencyContainer.storeManager, mockContainer, mockDeviceHelper, assignments = assignments, @@ -475,7 +475,7 @@ class ConfigManagerTests { every { resetCache() } just Runs } private val storeKit = - mockk { + mockk { coEvery { products(any()) } returns emptySet() } private val preload = @@ -539,7 +539,7 @@ class ConfigManagerTests { storage = storage, network = mockNetwork, paywallManager = mockContainer.paywallManager, - storeKitManager = mockContainer.storeKitManager, + storeManager = mockContainer.storeManager, factory = mockContainer, deviceHelper = mockDeviceHelper, assignments = assignmentStore, @@ -595,7 +595,7 @@ class ConfigManagerTests { storage = storage, network = mockNetwork, paywallManager = manager, - storeKitManager = storeKit, + storeManager = storeKit, factory = dependencyContainer, deviceHelper = mockDeviceHelper, assignments = assignmentStore, @@ -654,7 +654,7 @@ class ConfigManagerTests { storage = storage, network = mockNetwork, paywallManager = manager, - storeKitManager = storeKit, + storeManager = storeKit, factory = dependencyContainer, deviceHelper = mockDeviceHelper, assignments = assignmentStore, @@ -729,7 +729,7 @@ class ConfigManagerTests { storage = storage, network = mockNetwork, paywallManager = manager, - storeKitManager = storeKit, + storeManager = storeKit, factory = dependencyContainer, deviceHelper = mockDeviceHelper, assignments = assignmentStore, @@ -795,7 +795,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/vc/webview/WebviewFallbackClientTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/vc/webview/WebviewFallbackClientTest.kt index feaf2781..b13de2fb 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 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..41e2bbf5 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, diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 8a875ad1..d94f59b5 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -6,8 +6,8 @@ import android.net.Uri import androidx.work.WorkManager import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent -import com.superwall.sdk.billing.toInternalResult 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 @@ -40,17 +40,17 @@ 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.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.ActiveSubscriptionStatus import com.superwall.sdk.store.ExternalNativePurchaseController import com.superwall.sdk.store.PurchasingObserverState @@ -371,25 +371,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 diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt index 7c0c29a3..602b3f02 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/TrackingLogic.kt @@ -8,7 +8,7 @@ import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.models.paywall.PaywallURL -import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt index 952c8147..0cdce3df 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt @@ -17,8 +17,8 @@ import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatus import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType -import com.superwall.sdk.paywall.vc.Survey.SurveyPresentationResult -import com.superwall.sdk.paywall.vc.web_view.WebviewError +import com.superwall.sdk.paywall.view.Survey.SurveyPresentationResult +import com.superwall.sdk.paywall.view.webview.WebviewError import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import com.superwall.sdk.store.abstractions.transactions.StoreTransactionType diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt index ec2be1f5..0272e411 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt @@ -7,7 +7,7 @@ import com.superwall.sdk.models.triggers.TriggerResult import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatus import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason -import com.superwall.sdk.paywall.vc.web_view.WebviewError +import com.superwall.sdk.paywall.view.webview.WebviewError import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransactionType import com.superwall.sdk.store.transactions.RestoreType diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt index 5a6dcead..52d29321 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt @@ -1,8 +1,5 @@ package com.superwall.sdk.analytics.superwall -@Deprecated("Will be removed in the upcoming versions, use SuperwallEvents instead") -typealias SuperwallEventObjc = SuperwallEvents - enum class SuperwallEvents( val rawName: String, ) { diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt index 63acb629..36861e0f 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt @@ -33,7 +33,7 @@ import com.superwall.sdk.storage.DisableVerboseEvents import com.superwall.sdk.storage.LatestConfig import com.superwall.sdk.storage.LatestGeoInfo import com.superwall.sdk.storage.Storage -import com.superwall.sdk.store.StoreKitManager +import com.superwall.sdk.store.StoreManager import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow @@ -48,7 +48,7 @@ import java.util.concurrent.atomic.AtomicInteger // TODO: Re-enable those params open class ConfigManager( private val context: Context, - private val storeKitManager: StoreKitManager, + private val storeManager: StoreManager, private val storage: Storage, private val network: SuperwallAPI, private val deviceHelper: DeviceHelper, @@ -221,7 +221,7 @@ open class ConfigManager( if (options.paywalls.shouldPreload) { val productIds = it.paywalls.flatMap { it.productIds }.toSet() try { - storeKitManager.products(productIds) + storeManager.products(productIds) } catch (e: Throwable) { Logger.debug( logLevel = LogLevel.error, diff --git a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt index 1d95f133..79c0b781 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt @@ -12,7 +12,7 @@ import com.superwall.sdk.models.triggers.Trigger import com.superwall.sdk.paywall.manager.PaywallManager import com.superwall.sdk.paywall.presentation.rule_logic.javascript.JavascriptEvaluator import com.superwall.sdk.paywall.request.ResponseIdentifiers -import com.superwall.sdk.paywall.vc.web_view.webViewExists +import com.superwall.sdk.paywall.view.webview.webViewExists import com.superwall.sdk.storage.LocalStorage import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job diff --git a/superwall/src/main/java/com/superwall/sdk/debug/DebugManager.kt b/superwall/src/main/java/com/superwall/sdk/debug/DebugManager.kt index 0dea405f..55ce6a9e 100644 --- a/superwall/src/main/java/com/superwall/sdk/debug/DebugManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/debug/DebugManager.kt @@ -80,28 +80,20 @@ class DebugManager( currentView, ) } else { - val newViewController = factory.makeDebugViewController(paywallDatabaseId) + val newView = factory.makeDebugView(paywallDatabaseId) DebugViewActivity.startWithView( context, - newViewController, + newView, ) - view = newViewController + view = newView } } @MainThread suspend fun closeDebugger(animated: Boolean) { - // suspend fun dismissViewController() { view?.encapsulatingActivity?.finish() view = null isDebuggerLaunched = false -// } -// -// -// viewController?.presentedViewController?.let { -// it.dismiss(animated) -// dismissViewController() -// } ?: dismissViewController() } } diff --git a/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt b/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt index ab87a2b3..b59ece95 100644 --- a/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt +++ b/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt @@ -49,8 +49,8 @@ import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReaso import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.paywall.request.PaywallRequestManager import com.superwall.sdk.paywall.request.ResponseIdentifiers -import com.superwall.sdk.paywall.vc.ActivityEncapsulatable -import com.superwall.sdk.store.StoreKitManager +import com.superwall.sdk.paywall.view.ActivityEncapsulatable +import com.superwall.sdk.store.StoreManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -65,7 +65,7 @@ interface AppCompatActivityEncapsulatable { class DebugView( private val context: Context, - private val storeKitManager: StoreKitManager, + private val storeManager: StoreManager, private val network: Network, private val paywallRequestManager: PaywallRequestManager, private val paywallManager: PaywallManager, @@ -83,7 +83,7 @@ class DebugView( val style: Int = AlertDialog.BUTTON_POSITIVE, ) - // The full screen activity instance if this view controller has been presented in one. + // The full screen activity instance if this view has been presented in one. override var encapsulatingActivity: AppCompatActivity? = null internal var paywallDatabaseId: String? = null @@ -566,7 +566,7 @@ class DebugView( var paywall = paywallRequestManager.getPaywall(request).toResult().getOrThrow() val productVariables = - storeKitManager.getProductVariables( + storeManager.getProductVariables( paywall, request = request, ) @@ -737,7 +737,7 @@ class DebugView( try { val (productsById, _) = - storeKitManager.getProducts( + storeManager.getProducts( paywall = paywall, ) diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index d9ff9b22..c0afc11d 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -49,7 +49,6 @@ import com.superwall.sdk.network.device.DeviceInfo import com.superwall.sdk.network.session.CustomHttpUrlConnection import com.superwall.sdk.paywall.manager.PaywallManager import com.superwall.sdk.paywall.manager.PaywallViewCache -import com.superwall.sdk.paywall.presentation.dismiss import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType import com.superwall.sdk.paywall.presentation.internal.dismiss @@ -64,20 +63,20 @@ import com.superwall.sdk.paywall.request.PaywallRequest import com.superwall.sdk.paywall.request.PaywallRequestManager import com.superwall.sdk.paywall.request.PaywallRequestManagerDepFactory import com.superwall.sdk.paywall.request.ResponseIdentifiers -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.SuperwallStoreOwner -import com.superwall.sdk.paywall.vc.ViewModelFactory -import com.superwall.sdk.paywall.vc.ViewStorageViewModel -import com.superwall.sdk.paywall.vc.delegate.PaywallViewDelegateAdapter -import com.superwall.sdk.paywall.vc.web_view.SWWebView -import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandler -import com.superwall.sdk.paywall.vc.web_view.templating.models.JsonVariables -import com.superwall.sdk.paywall.vc.web_view.templating.models.Variables -import com.superwall.sdk.paywall.vc.web_view.webViewExists +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.SuperwallStoreOwner +import com.superwall.sdk.paywall.view.ViewModelFactory +import com.superwall.sdk.paywall.view.ViewStorageViewModel +import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter +import com.superwall.sdk.paywall.view.webview.SWWebView +import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandler +import com.superwall.sdk.paywall.view.webview.templating.models.JsonVariables +import com.superwall.sdk.paywall.view.webview.templating.models.Variables +import com.superwall.sdk.paywall.view.webview.webViewExists import com.superwall.sdk.storage.EventsQueue import com.superwall.sdk.storage.LocalStorage import com.superwall.sdk.store.InternalPurchaseController -import com.superwall.sdk.store.StoreKitManager +import com.superwall.sdk.store.StoreManager import com.superwall.sdk.store.abstractions.transactions.GoogleBillingPurchaseTransaction import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import com.superwall.sdk.store.transactions.TransactionManager @@ -140,7 +139,7 @@ class DependencyContainer( var debugManager: DebugManager var paywallManager: PaywallManager var paywallRequestManager: PaywallRequestManager - var storeKitManager: StoreKitManager + var storeManager: StoreManager val transactionManager: TransactionManager val googleBillingWrapper: GoogleBillingWrapper private val uiScope @@ -218,7 +217,7 @@ class DependencyContainer( javaPurchaseController = null, context, ) - storeKitManager = StoreKitManager(purchaseController, googleBillingWrapper) + storeManager = StoreManager(purchaseController, googleBillingWrapper) delegateAdapter = SuperwallDelegateAdapter() storage = LocalStorage(context = context, ioScope = ioScope(), factory = this, json = json()) @@ -263,7 +262,7 @@ class DependencyContainer( errorTracker = ErrorTracker(scope = ioScope, cache = storage) paywallRequestManager = PaywallRequestManager( - storeKitManager = storeKitManager, + storeManager = storeManager, network = network, factory = this, ioScope = ioScope, @@ -301,7 +300,7 @@ class DependencyContainer( configManager = ConfigManager( context = context, - storeKitManager = storeKitManager, + storeManager = storeManager, storage = storage, network = network, options = options, @@ -363,7 +362,7 @@ class DependencyContainer( transactionManager = TransactionManager( - storeKitManager = storeKitManager, + storeManager = storeManager, purchaseController = purchaseController, eventsQueue = eventsQueue, storage = storage, @@ -483,11 +482,11 @@ class DependencyContainer( return paywallView } - override fun makeDebugViewController(id: String?): DebugView { + override fun makeDebugView(id: String?): DebugView { val view = DebugView( context = context, - storeKitManager = storeKitManager, + storeManager = storeManager, network = network, paywallRequestManager = paywallRequestManager, paywallManager = paywallManager, @@ -528,7 +527,7 @@ class DependencyContainer( audienceFilterParams = HashMap(identityManager.userAttributes), ) - override fun makeHasExternalPurchaseController(): Boolean = storeKitManager.purchaseController.hasExternalPurchaseController + override fun makeHasExternalPurchaseController(): Boolean = storeManager.purchaseController.hasExternalPurchaseController override suspend fun didUpdateAppSession(appSession: AppSession) { } diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt index a1af19f0..2762d176 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt @@ -29,10 +29,10 @@ import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.request.PaywallRequest import com.superwall.sdk.paywall.request.ResponseIdentifiers -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.ViewStorage -import com.superwall.sdk.paywall.vc.delegate.PaywallViewDelegateAdapter -import com.superwall.sdk.paywall.vc.web_view.templating.models.JsonVariables +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.ViewStorage +import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter +import com.superwall.sdk.paywall.view.webview.templating.models.JsonVariables import com.superwall.sdk.storage.LocalStorage import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import kotlinx.coroutines.flow.StateFlow @@ -135,26 +135,8 @@ interface ViewFactory { delegate: PaywallViewDelegateAdapter?, ): PaywallView - fun makeDebugViewController(id: String?): DebugView -} - -// ViewControllerFactory & CacheFactory & DeviceInfoFactory, -// interface ViewControllerCacheDevice { -// suspend fun makePaywallViewController( -// paywall: Paywall, -// cache: PaywallViewControllerCache?, -// delegate: PaywallViewControllerDelegate? -// ): PaywallViewController -// -// // TODO: (Debug) -// // fun makeDebugViewController(id: String?): DebugViewController -// -// // Mark - device -// fun makeDeviceInfo(): DeviceInfo -// -// // Mark - cache -// fun makeCache(): PaywallViewControllerCache -// } + fun makeDebugView(id: String?): DebugView +} interface CacheFactory { fun makeCache(): PaywallViewCache diff --git a/superwall/src/main/java/com/superwall/sdk/game/GameControllerManager.kt b/superwall/src/main/java/com/superwall/sdk/game/GameControllerManager.kt index b83b2a0d..a96bb2bb 100644 --- a/superwall/src/main/java/com/superwall/sdk/game/GameControllerManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/game/GameControllerManager.kt @@ -4,7 +4,7 @@ import android.view.KeyEvent import android.view.MotionEvent interface GameControllerDelegate { - fun gameControllerEventDidOccur(event: GameControllerEvent) + fun gameControllerEventOccured(event: GameControllerEvent) } class GameControllerManager { @@ -39,7 +39,7 @@ class GameControllerManager { y = y.toDouble(), directional = directional, ) - delegate?.gameControllerEventDidOccur(event) + delegate?.gameControllerEventOccured(event) } fun dispatchKeyEvent(event: KeyEvent): Boolean { diff --git a/superwall/src/main/java/com/superwall/sdk/models/triggers/TriggerResult.kt b/superwall/src/main/java/com/superwall/sdk/models/triggers/TriggerResult.kt index 1f8ee4aa..c2d0968e 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/triggers/TriggerResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/triggers/TriggerResult.kt @@ -39,7 +39,7 @@ sealed class TriggerResult { // An error occurred and the user will not be shown a paywall. // - // If the error code is `101`, it means that no view controller could be found to present on. Otherwise a network failure may have occurred. + // If the error code is `101`, it means that no view could be found to present on. Otherwise a network failure may have occurred. // // In these instances, consider falling back to a native paywall. @Serializable @@ -92,7 +92,7 @@ sealed class InternalTriggerResult { /** * An error occurred and the user will not be shown a paywall. * - * If the error code is `101`, it means that no view controller could be found to present on. Otherwise a network failure may have occurred. + * If the error code is `101`, it means that no view could be found to present on. Otherwise a network failure may have occurred. * * In these instances, consider falling back to a native paywall. */ diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index 8667dde1..367ef42b 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -18,7 +18,6 @@ import com.superwall.sdk.dependencies.LocaleIdentifierFactory import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger -import com.superwall.sdk.misc.fold import com.superwall.sdk.misc.then import com.superwall.sdk.misc.toResult import com.superwall.sdk.models.config.ComputedPropertyRequest @@ -26,7 +25,7 @@ import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.geo.GeoInfo import com.superwall.sdk.network.JsonFactory import com.superwall.sdk.network.SuperwallAPI -import com.superwall.sdk.paywall.vc.web_view.templating.models.DeviceTemplate +import com.superwall.sdk.paywall.view.webview.templating.models.DeviceTemplate import com.superwall.sdk.storage.LastPaywallView import com.superwall.sdk.storage.LatestGeoInfo import com.superwall.sdk.storage.LocalStorage diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt index 9e02137f..5e51cd4d 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt @@ -7,13 +7,12 @@ import com.superwall.sdk.dependencies.ViewFactory import com.superwall.sdk.misc.Either import com.superwall.sdk.misc.launchWithTracking import com.superwall.sdk.misc.mapAsync -import com.superwall.sdk.misc.toResult import com.superwall.sdk.models.paywall.PaywallIdentifier import com.superwall.sdk.paywall.request.PaywallRequest import com.superwall.sdk.paywall.request.PaywallRequestManager -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState -import com.superwall.sdk.paywall.vc.delegate.PaywallViewDelegateAdapter +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState +import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter class PaywallManager( private val factory: PaywallManager.Factory, @@ -28,13 +27,6 @@ class PaywallManager( var currentView: PaywallView? = null get() = cache.activePaywallView - @Deprecated("Will be removed in the upcoming versions, use curentView instead") - var presentedViewController: PaywallView? - get() = currentView - set(value) { - currentView = value - } - private var _cache: PaywallViewCache? = null private val cache: PaywallViewCache @@ -51,11 +43,6 @@ class PaywallManager( return cache } - @Deprecated("Will be removed in the upcoming versions, use removePaywallView instead") - fun removePaywallViewController(forKey: String) { - removePaywallView(forKey) - } - fun removePaywallView(identifier: PaywallIdentifier) { cache.removePaywallView(identifier) } @@ -81,14 +68,6 @@ class PaywallManager( } } - @Deprecated("Will be removed in the upcoming versions, use getPaywallView instead") - suspend fun getPaywallViewController( - request: PaywallRequest, - isForPresentation: Boolean, - isPreloading: Boolean, - delegate: PaywallViewDelegateAdapter?, - ): Result = getPaywallView(request, isForPresentation, isPreloading, delegate).toResult() - suspend fun getPaywallView( request: PaywallRequest, isForPresentation: Boolean, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt index 2a3549e7..c80ac542 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallViewCache.kt @@ -4,10 +4,10 @@ import android.content.Context import com.superwall.sdk.misc.ActivityProvider import com.superwall.sdk.models.paywall.PaywallIdentifier import com.superwall.sdk.network.device.DeviceHelper -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.ViewStorage +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.ViewStorage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -37,9 +37,6 @@ class PaywallViewCache( .map { it.key to it.value } .toMap() - @Deprecated("Will be removed in the upcoming versions in favor of `getPaywallViews`") - fun getAllPaywallViewControllers(): List = getAllPaywallViews() - fun getAllPaywallViews(): List = runBlocking(singleThreadContext) { store.all().filterIsInstance().toList() diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index a90b7035..1d795903 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -29,8 +29,6 @@ data class PaywallInfo( val name: String, val url: PaywallURL, val experiment: Experiment?, - @Deprecated("This will always be an empty string and will be removed in the next major update of the SDK.") - val triggerSessionId: String = "", @Deprecated( message = "Use productItems because a paywall can support more than three products", ReplaceWith("productsItems"), diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt index c9d6aa17..fd4a51af 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt @@ -8,7 +8,7 @@ import com.superwall.sdk.paywall.presentation.internal.getPaywallComponents import com.superwall.sdk.paywall.presentation.internal.operators.logErrors import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.paywall.presentation.rule_logic.RuleEvaluationOutcome -import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.flow.MutableSharedFlow data class PaywallComponents( @@ -16,10 +16,7 @@ data class PaywallComponents( val presenter: Activity?, val rulesOutcome: RuleEvaluationOutcome, val debugInfo: Map, -) { - @Deprecated("Will be removed in the upcoming versions, use PaywallComponents.view instead") - val viewController: PaywallView = view -} +) @Throws(Throwable::class) internal suspend fun Superwall.getPaywall( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/PublicGetPaywall.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/PublicGetPaywall.kt index df082f58..c3c94914 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/PublicGetPaywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/PublicGetPaywall.kt @@ -8,9 +8,9 @@ import com.superwall.sdk.misc.toResult import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.delegate.PaywallViewCallback -import com.superwall.sdk.paywall.vc.delegate.PaywallViewDelegateAdapter +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.delegate.PaywallViewCallback +import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt index 331937f4..4a128286 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow * @param request The presentation request. * @param publisher A `MutableStateFlow` that gets sent `PaywallState` objects. * @return A `PaywallComponents` object that contains objects associated with the - * paywall view controller. + * paywall view. * @throws PresentationPipelineError object associated with stages of the pipeline. */ @Throws(Throwable::class) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt index 31bcd89b..21bbb32f 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt @@ -5,7 +5,7 @@ import com.superwall.sdk.paywall.presentation.PaywallCloseReason import com.superwall.sdk.paywall.presentation.internal.operators.* import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult import com.superwall.sdk.paywall.presentation.internal.state.PaywallState -import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt index 7be81925..cd61e0f2 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt @@ -54,6 +54,4 @@ sealed class PaywallPresentationRequestStatusReason( class SubscriptionStatusTimeout : PaywallPresentationRequestStatusReason("subscription_status_timeout") } -@Deprecated("Will be removed in the upcoming versions, use NoPaywallView instead") -typealias NoPaywallController = PaywallPresentationRequestStatusReason.NoPaywallView typealias PresentationPipelineError = PaywallPresentationRequestStatusReason diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt index f2985536..9aad0b0c 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt @@ -67,7 +67,7 @@ suspend fun Superwall.getExperiment( Logger.debug( logLevel = LogLevel.error, scope = LogScope.paywallPresentation, - message = "Error Getting Paywall View Controller", + message = "Error Getting Paywall view", info = debugInfo, error = rulesOutcome.triggerResult.error, ) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt index bedbd863..7d0f6452 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt @@ -17,8 +17,8 @@ import com.superwall.sdk.paywall.presentation.internal.userIsSubscribed import com.superwall.sdk.paywall.presentation.rule_logic.RuleEvaluationOutcome import com.superwall.sdk.paywall.request.PaywallRequest import com.superwall.sdk.paywall.request.ResponseIdentifiers -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.web_view.webViewExists +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.webview.webViewExists import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt index a93f83b9..fd926dbf 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt @@ -18,7 +18,7 @@ import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.paywall.presentation.rule_logic.RuleEvaluationOutcome -import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt index bfb8947e..19ea8772 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt @@ -13,18 +13,18 @@ import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationReques import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.state.PaywallState -import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** - * Presents the paywall view controller, stores the presentation request for future use, + * Presents the paywall view, stores the presentation request for future use, * and sends back a `presented` state to the paywall state publisher. * - * @param paywallView The paywall view controller to present. - * @param presenter The view controller to present the paywall on. + * @param paywallView The paywall view to present. + * @param presenter The view to present the paywall on. * @param unsavedOccurrence The trigger rule occurrence to save, if available. * @param debugInfo Information to help with debugging. * @param request The request to present the paywall. @@ -88,20 +88,3 @@ suspend fun Superwall.presentPaywallView( throw error } } - -@Deprecated("Will be removed in the upcoming versions, use `presentPaywallView` instead.") -suspend fun Superwall.presentPaywallViewController( - paywallView: PaywallView, - presenter: Activity, - unsavedOccurrence: TriggerRuleOccurrence?, - debugInfo: Map, - request: PresentationRequest, - paywallStatePublisher: MutableSharedFlow, -) = presentPaywallView( - paywallView = paywallView, - presenter = presenter, - unsavedOccurrence = unsavedOccurrence, - debugInfo = debugInfo, - request = request, - paywallStatePublisher = paywallStatePublisher, -) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt index 60a2605c..1b0f8836 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt @@ -4,7 +4,7 @@ import android.app.Activity import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo -import com.superwall.sdk.paywall.vc.delegate.PaywallViewDelegateAdapter +import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter import kotlinx.coroutines.flow.StateFlow import java.lang.ref.WeakReference @@ -32,9 +32,6 @@ sealed class PresentationRequestType { else -> "Unknown" } - @Deprecated("Will be removed in the upcoming versions, use paywallViewDelegateAdapter instead") - val paywallVcDelegateAdapter: PaywallViewDelegateAdapter? = paywallViewDelegateAdapter - val paywallViewDelegateAdapter: PaywallViewDelegateAdapter? get() = if (this is GetPaywall) this.adapter else null diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt index 109b50c2..a81e56f2 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt @@ -41,6 +41,6 @@ sealed class PresentationResult { // behavior in the paywall editor. class UserIsSubscribed : PresentationResult() - // No view controller could be found to present on. + // No view could be found to present on. class PaywallNotAvailable : PresentationResult() } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt index c55faf9b..4d49d42e 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt @@ -14,7 +14,7 @@ import com.superwall.sdk.misc.toResult 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.paywall.vc.web_view.webViewExists +import com.superwall.sdk.paywall.view.webview.webViewExists import com.superwall.sdk.storage.LocalStorage import kotlinx.coroutines.Deferred import kotlinx.coroutines.async diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt index 31423ded..ca08e070 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt @@ -18,7 +18,7 @@ import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.network.Network import com.superwall.sdk.paywall.presentation.PaywallInfo -import com.superwall.sdk.store.StoreKitManager +import com.superwall.sdk.store.StoreManager import com.superwall.sdk.utilities.withErrorTracking import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred @@ -30,7 +30,7 @@ interface PaywallRequestManagerDepFactory : ConfigManagerFactory class PaywallRequestManager( - private val storeKitManager: StoreKitManager, + private val storeManager: StoreManager, private val network: Network, private val factory: PaywallRequestManagerDepFactory, private val ioScope: IOScope, @@ -228,7 +228,7 @@ class PaywallRequestManager( var paywall = paywall val result = - storeKitManager.getProducts( + storeManager.getProducts( substituteProducts = request.overrides.products, paywall = paywall, request = request, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/delegate/PaywallViewCallback.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/delegate/PaywallViewCallback.kt deleted file mode 100644 index 886ec67e..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/delegate/PaywallViewCallback.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.superwall.sdk.paywall.vc.delegate - -import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent - -@Deprecated("Will be removed in the upcoming versions, use PaywallViewDelegate instead") -typealias PaywallViewControllerDelegate = PaywallViewCallback - -@Deprecated("Will be removed in the upcoming versions, use PaywallViewEventDelegate instead") -typealias PaywallViewControllerEventDelegate = PaywallViewEventCallback - -interface PaywallViewCallback { - // TODO: missing `shouldDismiss` - - @Deprecated("Will be removed in the upcoming versions, use onFinish instead") - fun didFinish( - paywall: PaywallView, - result: PaywallResult, - shouldDismiss: Boolean, - ) = onFinished(paywall, result, shouldDismiss) - - fun onFinished( - paywall: PaywallView, - result: PaywallResult, - shouldDismiss: Boolean, - ) -} - -fun interface PaywallViewEventCallback { - suspend fun eventDidOccur( - paywallEvent: PaywallWebEvent, - paywallView: PaywallView, - ) -} - -sealed class PaywallLoadingState { - class Unknown : PaywallLoadingState() - - class LoadingPurchase : PaywallLoadingState() - - class LoadingURL : PaywallLoadingState() - - class ManualLoading : PaywallLoadingState() - - class Ready : PaywallLoadingState() -} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/LoadingView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/LoadingView.kt similarity index 85% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/LoadingView.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/LoadingView.kt index 3f51aa73..e58e5392 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/LoadingView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/LoadingView.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc +package com.superwall.sdk.paywall.view import android.content.Context import android.graphics.Color @@ -6,13 +6,13 @@ import android.view.Gravity import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ProgressBar -import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState class LoadingView( context: Context, ) : FrameLayout(context) { companion object { - internal const val TAG = "LoadingViewController" + internal const val TAG = "LoadingView" } init { @@ -41,11 +41,11 @@ class LoadingView( } fun setupFor( - paywallViewController: PaywallView, + paywallView: PaywallView, loadingState: PaywallLoadingState, ) { (this.parent as? ViewGroup)?.removeView(this) - paywallViewController.addView(this) + paywallView.addView(this) visibility = when (loadingState) { is PaywallLoadingState.LoadingPurchase, is PaywallLoadingState.ManualLoading -> diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/PaywallView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt similarity index 91% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/PaywallView.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt index 93463a17..f22cbf98 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/PaywallView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc +package com.superwall.sdk.paywall.view import android.app.Activity import android.content.Context @@ -45,16 +45,16 @@ import com.superwall.sdk.paywall.presentation.internal.operators.storePresentati import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.paywall.presentation.result.PresentationResult -import com.superwall.sdk.paywall.vc.Survey.SurveyManager -import com.superwall.sdk.paywall.vc.Survey.SurveyPresentationResult -import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState -import com.superwall.sdk.paywall.vc.delegate.PaywallViewDelegateAdapter -import com.superwall.sdk.paywall.vc.delegate.PaywallViewEventCallback -import com.superwall.sdk.paywall.vc.web_view.PaywallMessage -import com.superwall.sdk.paywall.vc.web_view.SWWebView -import com.superwall.sdk.paywall.vc.web_view.SWWebViewDelegate -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.Survey.SurveyManager +import com.superwall.sdk.paywall.view.Survey.SurveyPresentationResult +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState +import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter +import com.superwall.sdk.paywall.view.delegate.PaywallViewEventCallback +import com.superwall.sdk.paywall.view.webview.PaywallMessage +import com.superwall.sdk.paywall.view.webview.SWWebView +import com.superwall.sdk.paywall.view.webview.SWWebViewDelegate +import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandlerDelegate +import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent import com.superwall.sdk.storage.LocalStorage import com.superwall.sdk.utilities.withErrorTracking import kotlinx.coroutines.delay @@ -118,11 +118,11 @@ class PaywallView( private var shimmerView: ShimmerView? = null - private var loadingViewController: LoadingView? = null + private var loadingView: LoadingView? = null var paywallStatePublisher: MutableSharedFlow? = null - // The full screen activity instance if this view controller has been presented in one. + // The full screen activity instance if this view has been presented in one. override var encapsulatingActivity: WeakReference? = null // / Stores the ``PaywallResult`` on dismiss of paywall. @@ -165,7 +165,7 @@ class PaywallView( override val isActive: Boolean get() = isPresented - // / Defines whether the view controller is being presented or not. + // / Defines whether the view is being presented or not. private var isPresented = false private var presentationWillPrepare = true private var presentationDidFinishPrepare = false @@ -233,7 +233,7 @@ class PaywallView( } internal fun setupLoading(loadingView: LoadingView) { - this.loadingViewController = loadingView + this.loadingView = loadingView loadingView.setupFor(this, loadingState) } @@ -247,7 +247,7 @@ class PaywallView( LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) } this.shimmerView = shimmerView - this.loadingViewController = loadingView + this.loadingView = loadingView } fun present( @@ -282,9 +282,6 @@ class PaywallView( viewCreatedCompletion = completion } - @Deprecated("Will be removed in the upcoming versions, use beforeViewCreated instead") - fun viewWillAppear() = beforeViewCreated() - fun beforeViewCreated() { if (isBrowserViewPresented) { return @@ -471,19 +468,15 @@ class PaywallView( super.onAttachedToWindow() // Assert if no `request` - // fatalAssert(request != null, "Must be presenting a PaywallViewController with a `request` instance.") + // fatalAssert(request != null, "Must be presenting a Paywallview with a `request` instance.") if (loadingState is PaywallLoadingState.Unknown) { loadWebView() } } - // / Lets the view controller know that presentation has finished. + // Lets the view know that presentation has finished. // Only called once per presentation. - - @Deprecated("Will be removed in the upcoming versions, use onViewCreated instead") - fun viewDidAppear() = onViewCreated() - fun onViewCreated() { viewCreatedCompletion?.invoke(true) viewCreatedCompletion = null @@ -557,7 +550,7 @@ class PaywallView( if (transactionBackgroundView != PaywallOptions.TransactionBackgroundView.SPINNER) { return } - loadingViewController?.let { + loadingView?.let { mainScope.launch { it.visibility = View.VISIBLE } @@ -565,7 +558,7 @@ class PaywallView( } private fun hideLoadingView() { - loadingViewController?.let { + loadingView?.let { mainScope.launch { it.visibility = View.GONE } @@ -627,19 +620,6 @@ class PaywallView( // TODO: Implement this } - @Deprecated( - "Will be removed in the upcoming versions, use presentAlert instead", - ReplaceWith("showAlert(title, message, actionTitle, closeActionTitle, action, onClose)"), - ) - fun presentAlert( - title: String? = null, - message: String? = null, - actionTitle: String? = null, - closeActionTitle: String = "Done", - action: (() -> Unit)? = null, - onClose: (() -> Unit)? = null, - ) = showAlert(title, message, actionTitle, closeActionTitle, action, onClose) - fun showAlert( title: String? = null, message: String? = null, @@ -833,14 +813,8 @@ class PaywallView( context?.startActivity(deepLinkIntent) } - @Deprecated("Will be removed in the upcoming versions, use presentBrowserInApp instead") - override fun presentSafariInApp(url: String) = presentBrowserInApp(url) - - @Deprecated("Will be removed in the upcoming versions, use presentBrowserExternal instead") - override fun presentSafariExternal(url: String) = presentBrowserExternal(url) - //region GameController - override fun gameControllerEventDidOccur(event: GameControllerEvent) { + override fun gameControllerEventOccured(event: GameControllerEvent) { val payload = try { gameControllerJson.encodeToString(event) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/ShimmerView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/ShimmerView.kt similarity index 93% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/ShimmerView.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/ShimmerView.kt index 5ce08f87..ce8e4971 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/ShimmerView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/ShimmerView.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc +package com.superwall.sdk.paywall.view import android.animation.ValueAnimator import android.content.Context @@ -18,7 +18,7 @@ import androidx.core.graphics.BlendModeCompat import com.superwall.sdk.R import com.superwall.sdk.misc.isDarkColor import com.superwall.sdk.misc.readableOverlayColor -import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState class ShimmerView( context: Context, @@ -52,12 +52,12 @@ class ShimmerView( private var tintColor: Int = 0 fun setupFor( - paywallViewController: PaywallView, + paywallView: PaywallView, loadingState: PaywallLoadingState, ) { (this.parent as? ViewGroup)?.removeView(this) - if (background != paywallViewController.backgroundColor) { - background = paywallViewController.backgroundColor + if (background != paywallView.backgroundColor) { + background = paywallView.backgroundColor setBackgroundColor(background) isLightBackground = !background.isDarkColor() tintColor = background.readableOverlayColor() @@ -75,7 +75,7 @@ class ShimmerView( else -> GONE } - paywallViewController.addView(this) + paywallView.addView(this) layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) checkForOrientationChanges() diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt similarity index 99% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt index 567d6fa7..4d97d486 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc +package com.superwall.sdk.paywall.view import android.Manifest import android.animation.ArgbEvaluator @@ -49,7 +49,7 @@ import com.superwall.sdk.models.paywall.LocalNotification import com.superwall.sdk.models.paywall.PaywallPresentationStyle import com.superwall.sdk.paywall.presentation.PaywallCloseReason import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult -import com.superwall.sdk.paywall.vc.web_view.SWWebView +import com.superwall.sdk.paywall.view.webview.SWWebView import com.superwall.sdk.store.transactions.notifications.NotificationScheduler import com.superwall.sdk.utilities.withErrorTracking import kotlinx.coroutines.CoroutineScope diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallStoreOwner.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallStoreOwner.kt similarity index 94% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallStoreOwner.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallStoreOwner.kt index 4315d50c..f95b3f44 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/SuperwallStoreOwner.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallStoreOwner.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc +package com.superwall.sdk.paywall.view import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/Survey/SurveyManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/Survey/SurveyManager.kt similarity index 98% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/Survey/SurveyManager.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/Survey/SurveyManager.kt index efb2b597..0c76f84b 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/Survey/SurveyManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/Survey/SurveyManager.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.Survey +package com.superwall.sdk.paywall.view.Survey import android.app.Activity import android.text.Editable @@ -25,8 +25,8 @@ import com.superwall.sdk.logger.Logger import com.superwall.sdk.paywall.presentation.PaywallCloseReason import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState import com.superwall.sdk.storage.LocalStorage import com.superwall.sdk.storage.SurveyAssignmentKey import kotlinx.coroutines.CoroutineScope diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/Survey/SurveyPresentationResult.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/Survey/SurveyPresentationResult.kt similarity index 74% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/Survey/SurveyPresentationResult.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/Survey/SurveyPresentationResult.kt index e8debf1c..754e140b 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/Survey/SurveyPresentationResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/Survey/SurveyPresentationResult.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.Survey +package com.superwall.sdk.paywall.view.Survey enum class SurveyPresentationResult( val rawValue: String, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/ViewStorage.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/ViewStorage.kt similarity index 91% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/ViewStorage.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/ViewStorage.kt index ae6370a2..22ccb4ab 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/ViewStorage.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/ViewStorage.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc +package com.superwall.sdk.paywall.view import android.view.View import java.util.concurrent.ConcurrentHashMap diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/ViewStorageViewModel.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/ViewStorageViewModel.kt similarity index 88% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/ViewStorageViewModel.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/ViewStorageViewModel.kt index 1d2c727f..3ef52962 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/ViewStorageViewModel.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/ViewStorageViewModel.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc +package com.superwall.sdk.paywall.view import android.view.View import androidx.lifecycle.ViewModel diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/delegate/PaywallViewCallback.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/delegate/PaywallViewCallback.kt new file mode 100644 index 00000000..8377edad --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/delegate/PaywallViewCallback.kt @@ -0,0 +1,32 @@ +package com.superwall.sdk.paywall.view.delegate + +import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent + +interface PaywallViewCallback { + fun onFinished( + paywall: PaywallView, + result: PaywallResult, + shouldDismiss: Boolean, + ) +} + +fun interface PaywallViewEventCallback { + suspend fun eventDidOccur( + paywallEvent: PaywallWebEvent, + paywallView: PaywallView, + ) +} + +sealed class PaywallLoadingState { + class Unknown : PaywallLoadingState() + + class LoadingPurchase : PaywallLoadingState() + + class LoadingURL : PaywallLoadingState() + + class ManualLoading : PaywallLoadingState() + + class Ready : PaywallLoadingState() +} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/delegate/PaywallViewDelegateAdapter.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/delegate/PaywallViewDelegateAdapter.kt similarity index 50% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/delegate/PaywallViewDelegateAdapter.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/delegate/PaywallViewDelegateAdapter.kt index 06d67d4d..2770dfe7 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/delegate/PaywallViewDelegateAdapter.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/delegate/PaywallViewDelegateAdapter.kt @@ -1,7 +1,7 @@ -package com.superwall.sdk.paywall.vc.delegate +package com.superwall.sdk.paywall.view.delegate import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult -import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -11,13 +11,6 @@ class PaywallViewDelegateAdapter( val hasJavaDelegate: Boolean get() = false - @Deprecated("Will be removed in the upcoming versions, use onFinished instead") - suspend fun didFinish( - paywall: PaywallView, - result: PaywallResult, - shouldDismiss: Boolean, - ) = onFinished(paywall, result, shouldDismiss) - suspend fun onFinished( paywall: PaywallView, result: PaywallResult, @@ -26,6 +19,3 @@ class PaywallViewDelegateAdapter( kotlinDelegate?.onFinished(paywall, result, shouldDismiss) } } - -@Deprecated("Will be removed in the upcoming versions, use PaywallViewDelegateAdapter instead") -typealias PaywallViewControllerDelegateAdapter = PaywallViewDelegateAdapter diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DefaultWebviewClient.kt similarity index 98% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DefaultWebviewClient.kt index 220070e4..9b262097 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/DefaultWebviewClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DefaultWebviewClient.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view +package com.superwall.sdk.paywall.view.webview import android.graphics.Bitmap import android.os.Build diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt similarity index 98% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt index 51c31a78..e1dd8722 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view +package com.superwall.sdk.paywall.view.webview import android.content.Context import android.graphics.Color @@ -28,8 +28,8 @@ import com.superwall.sdk.misc.IOScope import com.superwall.sdk.misc.MainScope import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.paywall.presentation.PaywallInfo -import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandler -import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandlerDelegate +import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandler +import com.superwall.sdk.paywall.view.webview.messaging.PaywallMessageHandlerDelegate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/ScrollDisabled.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/ScrollDisabled.kt similarity index 90% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/ScrollDisabled.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/ScrollDisabled.kt index 6193a1d1..860ac4ee 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/ScrollDisabled.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/ScrollDisabled.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view +package com.superwall.sdk.paywall.view.webview import android.view.GestureDetector import android.view.MotionEvent diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebviewClientEvent.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewClientEvent.kt similarity index 96% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebviewClientEvent.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewClientEvent.kt index ff66266b..3cabad4e 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebviewClientEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewClientEvent.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view +package com.superwall.sdk.paywall.view.webview sealed class WebviewClientEvent { data class OnPageFinished( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebviewFallbackClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewFallbackClient.kt similarity index 99% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebviewFallbackClient.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewFallbackClient.kt index cc0c2fc2..b3e7fda5 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebviewFallbackClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewFallbackClient.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view +package com.superwall.sdk.paywall.view.webview import android.graphics.Bitmap import android.webkit.RenderProcessGoneDetail diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessage.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt similarity index 98% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessage.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt index 937cff1a..5235c293 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessage.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessage.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view +package com.superwall.sdk.paywall.view.webview import android.net.Uri import com.superwall.sdk.logger.LogLevel diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt similarity index 94% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt index fe35e1f7..a78293a2 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallMessageHandler.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallMessageHandler.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view.messaging +package com.superwall.sdk.paywall.view.webview.messaging import TemplateLogic import android.net.Uri @@ -18,10 +18,10 @@ import com.superwall.sdk.misc.MainScope 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.PaywallMessage -import com.superwall.sdk.paywall.vc.web_view.WrappedPaywallMessages -import com.superwall.sdk.paywall.vc.web_view.parseWrappedPaywallMessages +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState +import com.superwall.sdk.paywall.view.webview.PaywallMessage +import com.superwall.sdk.paywall.view.webview.WrappedPaywallMessages +import com.superwall.sdk.paywall.view.webview.parseWrappedPaywallMessages import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -47,12 +47,6 @@ interface PaywallMessageHandlerDelegate { fun openDeepLink(url: String) - @Deprecated("Will be removed in the upcoming versions, use presentBrowserInApp instead") - fun presentSafariInApp(url: String) = presentBrowserInApp(url) - - @Deprecated("Will be removed in the upcoming versions, use presentBrowserExternal instead") - fun presentSafariExternal(url: String) = presentBrowserExternal(url) - fun presentBrowserInApp(url: String) fun presentBrowserExternal(url: String) @@ -339,9 +333,6 @@ class PaywallMessageHandler( delegate?.presentBrowserInApp(url.toString()) } - @Deprecated("Will be removed in the upcoming versions, use openUrlInChrome instead") - private fun openUrlInSafari(url: URI) = openUrlInBrowser(url) - private fun openUrlInBrowser(url: URI) { detectHiddenPaywallEvent( "openUrlInSafari", diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallWebEvent.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallWebEvent.kt similarity index 94% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallWebEvent.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallWebEvent.kt index 60f6611c..bb13f2d2 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/PaywallWebEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/PaywallWebEvent.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view.messaging +package com.superwall.sdk.paywall.view.webview.messaging import android.net.Uri import kotlinx.serialization.SerialName diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/RawWebMessageHandler.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/RawWebMessageHandler.kt similarity index 90% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/RawWebMessageHandler.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/RawWebMessageHandler.kt index b63f67d0..31bda972 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/messaging/RawWebMessageHandler.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/messaging/RawWebMessageHandler.kt @@ -1,12 +1,12 @@ -package com.superwall.sdk.paywall.vc.web_view.messaging +package com.superwall.sdk.paywall.view.webview.messaging import android.webkit.JavascriptInterface import android.webkit.WebViewClient import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger -import com.superwall.sdk.paywall.vc.web_view.PaywallMessage -import com.superwall.sdk.paywall.vc.web_view.parseWrappedPaywallMessages +import com.superwall.sdk.paywall.view.webview.PaywallMessage +import com.superwall.sdk.paywall.view.webview.parseWrappedPaywallMessages import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/TemplateLogic.kt similarity index 94% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/TemplateLogic.kt index 38a6e4ae..d8642fd2 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/TemplateLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/TemplateLogic.kt @@ -4,8 +4,8 @@ import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.paywall.Paywall -import com.superwall.sdk.paywall.vc.web_view.templating.models.FreeTrialTemplate -import com.superwall.sdk.paywall.vc.web_view.templating.models.JsonVariables +import com.superwall.sdk.paywall.view.webview.templating.models.FreeTrialTemplate +import com.superwall.sdk.paywall.view.webview.templating.models.JsonVariables import com.superwall.sdk.paywall.view_controller.web_view.templating.models.ProductTemplate import kotlinx.serialization.json.Json diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/DeviceTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt similarity index 97% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/DeviceTemplate.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt index beb5083f..d593cc26 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/DeviceTemplate.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view.templating.models +package com.superwall.sdk.paywall.view.webview.templating.models import com.superwall.sdk.storage.core_data.toNullableTypedMap import kotlinx.serialization.SerialName diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/FreeTrialTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/FreeTrialTemplate.kt similarity index 77% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/FreeTrialTemplate.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/FreeTrialTemplate.kt index 37175ce7..a34c5158 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/FreeTrialTemplate.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/FreeTrialTemplate.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view.templating.models +package com.superwall.sdk.paywall.view.webview.templating.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/ProductTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/ProductTemplate.kt similarity index 100% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/ProductTemplate.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/ProductTemplate.kt diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/Variables.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/Variables.kt similarity index 96% rename from superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/Variables.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/Variables.kt index 907eada0..ecf86471 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/templating/models/Variables.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/Variables.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view.templating.models +package com.superwall.sdk.paywall.view.webview.templating.models import com.superwall.sdk.models.product.ProductVariable import com.superwall.sdk.models.serialization.AnySerializer diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt index d7b193d3..53c274aa 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt @@ -6,7 +6,7 @@ import com.superwall.sdk.models.paywall.PaywallProducts import com.superwall.sdk.models.product.Product import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.product.ProductVariable -import com.superwall.sdk.paywall.vc.PaywallView +import com.superwall.sdk.paywall.view.PaywallView import com.superwall.sdk.store.abstractions.product.StoreProduct data class GetProductsResponse( diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManager.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt similarity index 99% rename from superwall/src/main/java/com/superwall/sdk/store/StoreKitManager.kt rename to superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt index 3c6f45ba..92ad1c7f 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt @@ -21,7 +21,7 @@ import com.superwall.sdk.store.abstractions.product.receipt.ReceiptManager import com.superwall.sdk.store.coordinator.ProductsFetcher import java.util.Date -class StoreKitManager( +class StoreManager( val purchaseController: InternalPurchaseController, private val billing: Billing, private val track: suspend (InternalSuperwallEvent) -> Unit = { diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index b638593f..ef2809d1 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -27,21 +27,21 @@ import com.superwall.sdk.misc.launchWithTracking import com.superwall.sdk.models.paywall.LocalNotificationType import com.superwall.sdk.paywall.presentation.PaywallInfo 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.PaywallLoadingState +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.SuperwallPaywallActivity +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState import com.superwall.sdk.storage.EventsQueue import com.superwall.sdk.storage.PurchasingProductdIds import com.superwall.sdk.storage.Storage import com.superwall.sdk.store.PurchasingObserverState -import com.superwall.sdk.store.StoreKitManager +import com.superwall.sdk.store.StoreManager import com.superwall.sdk.store.abstractions.product.RawStoreProduct import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import kotlinx.coroutines.launch class TransactionManager( - private val storeKitManager: StoreKitManager, + private val storeManager: StoreManager, private val purchaseController: PurchaseController, private val eventsQueue: EventsQueue, private val storage: Storage, @@ -95,7 +95,7 @@ class TransactionManager( val state = state as PurchasingObserverState.PurchaseResult state.purchases?.forEach { purchase -> purchase.products.map { - storeKitManager.productsByFullId[it] ?.let { product -> + storeManager.productsByFullId[it] ?.let { product -> didPurchase(product, PurchaseSource.ObserverMode(product), product.hasFreeTrial) } } @@ -123,7 +123,7 @@ class TransactionManager( val result = state as PurchasingObserverState.PurchaseResult result.purchases?.forEach { purchase -> purchase.products.map { - storeKitManager.productsByFullId[it] ?.let { product -> + storeManager.productsByFullId[it] ?.let { product -> handlePendingTransaction(PurchaseSource.ObserverMode(product)) } } @@ -133,7 +133,7 @@ class TransactionManager( val state = state as PurchasingObserverState.PurchaseResult state.purchases?.forEach { purchase -> purchase.products.map { - storeKitManager.productsByFullId[it] ?.let { product -> + storeManager.productsByFullId[it] ?.let { product -> didRestore(product, PurchaseSource.ObserverMode(product)) } } @@ -155,7 +155,7 @@ class TransactionManager( val product = when (purchaseSource) { is PurchaseSource.Internal -> - storeKitManager.productsByFullId[purchaseSource.productId] ?: run { + storeManager.productsByFullId[purchaseSource.productId] ?: run { log( LogLevel.error, "Trying to purchase (${purchaseSource.productId}) but the product has failed to load. Visit https://superwall.com/l/missing-products to diagnose.", @@ -182,7 +182,7 @@ class TransactionManager( prepareToPurchase(product, purchaseSource) val result = - storeKitManager.purchaseController.purchase( + storeManager.purchaseController.purchase( activity = activity, productDetails = productDetails, offerId = rawStoreProduct.offerId, @@ -454,7 +454,7 @@ class TransactionManager( factory = factory, ) - storeKitManager.loadPurchasedProducts() + storeManager.loadPurchasedProducts() trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) @@ -477,7 +477,7 @@ class TransactionManager( factory = factory, ) - storeKitManager.loadPurchasedProducts() + storeManager.loadPurchasedProducts() trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) } diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt index ceb48757..75d5b1b3 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt @@ -9,7 +9,7 @@ import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.models.paywall.LocalNotification -import com.superwall.sdk.paywall.vc.SuperwallPaywallActivity +import com.superwall.sdk.paywall.view.SuperwallPaywallActivity import java.util.concurrent.TimeUnit internal class NotificationScheduler { diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt index b12fcf9f..3ab9cbde 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt @@ -8,7 +8,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.work.Worker import androidx.work.WorkerParameters -import com.superwall.sdk.paywall.vc.SuperwallPaywallActivity +import com.superwall.sdk.paywall.view.SuperwallPaywallActivity internal class NotificationWorker( val context: Context, diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/vc/web_view/PaywallMessageTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/PaywallMessageTest.kt similarity index 96% rename from superwall/src/test/java/com/superwall/sdk/paywall/vc/web_view/PaywallMessageTest.kt rename to superwall/src/test/java/com/superwall/sdk/paywall/view/webview/PaywallMessageTest.kt index d0a8ae28..d1a251a1 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/vc/web_view/PaywallMessageTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/PaywallMessageTest.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view +package com.superwall.sdk.paywall.view.webview class PaywallMessageTest { // private val jsonFormat = Json { ignoreUnknownKeys = true } diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/vc/web_view/templating/models/DeviceTemplateTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt similarity index 98% rename from superwall/src/test/java/com/superwall/sdk/paywall/vc/web_view/templating/models/DeviceTemplateTest.kt rename to superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt index d5b71c21..848a58c3 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/vc/web_view/templating/models/DeviceTemplateTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt @@ -1,4 +1,4 @@ -package com.superwall.sdk.paywall.vc.web_view.templating.models +package com.superwall.sdk.paywall.view.webview.templating.models import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject diff --git a/superwall/src/test/java/com/superwall/sdk/store/StoreKitManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt similarity index 95% rename from superwall/src/test/java/com/superwall/sdk/store/StoreKitManagerTest.kt rename to superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt index c05d8292..ff0a8304 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/StoreKitManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt @@ -24,17 +24,17 @@ import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test -class StoreKitManagerTest { +class StoreManagerTest { private lateinit var purchaseController: InternalPurchaseController private lateinit var billing: Billing - private lateinit var storeKitManager: StoreKitManager + private lateinit var storeManager: StoreManager @Before fun setup() { purchaseController = mockk() billing = mockk() - storeKitManager = - StoreKitManager( + storeManager = + StoreManager( purchaseController, billing, track = {}, @@ -91,7 +91,7 @@ class StoreKitManagerTest { coEvery { billing.awaitGetProducts(any()) } returns storeProducts When("getProductVariables is called") { - val result = storeKitManager.getProductVariables(paywall, request) + val result = storeManager.getProductVariables(paywall, request) Then("it should return the correct product variables") { assertEquals(2, result.size) @@ -156,7 +156,7 @@ class StoreKitManagerTest { ) When("getProducts is called with substitute products") { - val result = storeKitManager.getProducts(substituteProducts, paywall, null) + val result = storeManager.getProducts(substituteProducts, paywall, null) Then("it should use the substitute product and fetch the remaining product") { assertEquals(2, result.productsByFullId.size) @@ -213,7 +213,7 @@ class StoreKitManagerTest { When("getProducts is called") { Then("it should throw a BillingNotAvailable error") { assertThrows(BillingError.BillingNotAvailable::class.java) { - runBlocking { storeKitManager.getProducts(null, paywall, null) } + runBlocking { storeManager.getProducts(null, paywall, null) } } } } @@ -234,7 +234,7 @@ class StoreKitManagerTest { coEvery { billing.awaitGetProducts(identifiers) } returns expectedProducts When("products method is called") { - val result = storeKitManager.products(identifiers) + val result = storeManager.products(identifiers) Then("it should return the correct set of StoreProducts") { assertEquals(expectedProducts, result) diff --git a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt index 780f459b..a5156146 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -23,13 +23,13 @@ import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.product.ProductType import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult -import com.superwall.sdk.paywall.vc.PaywallView -import com.superwall.sdk.paywall.vc.delegate.PaywallLoadingState +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState import com.superwall.sdk.products.mockPricingPhase import com.superwall.sdk.products.mockSubscriptionOfferDetails import com.superwall.sdk.storage.EventsQueue import com.superwall.sdk.store.InternalPurchaseController -import com.superwall.sdk.store.StoreKitManager +import com.superwall.sdk.store.StoreManager import com.superwall.sdk.store.abstractions.product.OfferType import com.superwall.sdk.store.abstractions.product.RawStoreProduct import com.superwall.sdk.store.abstractions.product.StoreProduct @@ -111,7 +111,7 @@ class TransactionManagerTest { ), ) } - private var storeKitManager = spyk(StoreKitManager(purchaseController, billing)) + private var storeManager = spyk(StoreManager(purchaseController, billing)) private var activityProvider = mockk { every { getCurrentActivity() } returns mockk() @@ -135,7 +135,7 @@ class TransactionManagerTest { options: SuperwallOptions.() -> Unit = {}, ) = TransactionManager( purchaseController = purchaseController, - storeKitManager = storeKitManager, + storeManager = storeManager, activityProvider = activityProvider, subscriptionStatus = subscriptionStatus, track = { track(it) }, @@ -174,7 +174,7 @@ class TransactionManagerTest { fun test_purchase_activity_not_found() = runTest { Given("We have loaded products but no activity") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) every { activityProvider.getCurrentActivity() } returns null val transactionManager: TransactionManager = manager() When("We try to purchase a product from the paywall") { @@ -201,7 +201,7 @@ class TransactionManagerTest { val events = MutableStateFlow(emptyList()) Given("We have loaded products and we can purchase successfully") { // Pretend a paywall loaded a product - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val transactionManager: TransactionManager = manager(track = { e -> events.update { @@ -227,7 +227,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify event order") { val transactionEvents = events.value.filterIsInstance() @@ -274,7 +274,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify event order") { val transactionEvents = events.value.filterIsInstance() @@ -294,7 +294,7 @@ class TransactionManagerTest { fun test_purchase_restored_internal() = runTest { Given("We have loaded products and a purchase results in restoration") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager( @@ -366,7 +366,7 @@ class TransactionManagerTest { fun test_purchase_failed_with_alert() = runTest { Given("We have loaded products and a purchase fails") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -423,7 +423,7 @@ class TransactionManagerTest { fun test_purchase_failed_without_alert() = runTest { Given("We have loaded products and a purchase fails") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -481,7 +481,7 @@ class TransactionManagerTest { fun test_purchase_pending() = runTest { Given("We have loaded products and a purchase is pending") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -526,7 +526,7 @@ class TransactionManagerTest { fun test_purchase_cancelled_internal() = runTest { Given("We have loaded products and a purchase is pending") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -760,7 +760,7 @@ class TransactionManagerTest { fun test_purchase_with_free_trial_internal() = runTest { Given("We have loaded products with a free trial and we can purchase successfully") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) every { paywallView.encapsulatingActivity } returns WeakReference(mockk()) every { playProduct.oneTimePurchaseOfferDetails } returns null every { playProduct.subscriptionOfferDetails } returns @@ -798,7 +798,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify free trial start event") { val freeTrialStartEvent = events.value.filterIsInstance() @@ -850,7 +850,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify free trial start event") { val freeTrialStartEvent = events.value.filterIsInstance() @@ -866,7 +866,7 @@ class TransactionManagerTest { fun test_purchase_non_recurring_product_internal() = runTest { Given("We have loaded a non-recurring product and we can purchase successfully") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -893,7 +893,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify non-recurring product purchase event") { val nonRecurringPurchaseEvent = events.value.filterIsInstance() @@ -935,7 +935,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify non-recurring product purchase event") { val nonRecurringPurchaseEvent = events.value.filterIsInstance() @@ -951,7 +951,7 @@ class TransactionManagerTest { fun test_purchase_subscription_without_trial_internal() = runTest { Given("We have loaded a subscription product without trial and we can purchase successfully") { - storeKitManager.getProducts(paywall = mockedPaywall) + storeManager.getProducts(paywall = mockedPaywall) val events = MutableStateFlow(emptyList()) val transactionManager: TransactionManager = manager(track = { e -> @@ -984,7 +984,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify subscription start event") { val subscriptionStartEvent = events.value.filterIsInstance() @@ -1031,7 +1031,7 @@ class TransactionManagerTest { ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) - coVerify { storeKitManager.loadPurchasedProducts() } + coVerify { storeManager.loadPurchasedProducts() } And("Verify subscription start event") { val subscriptionStartEvent = events.value.filterIsInstance() From b260e8abc8948c5e59807e6bb8317f2a6a10cac0 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Fri, 13 Dec 2024 10:46:45 +0100 Subject: [PATCH 15/37] Minor fixes --- example/app/build.gradle.kts | 2 +- .../sdk/network/device/DeviceHelper.kt | 66 +++++++------------ .../sdk/paywall/presentation/PaywallInfo.kt | 3 +- 3 files changed, 27 insertions(+), 44 deletions(-) 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/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index 367ef42b..8acc148f 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -37,6 +37,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.Json +import org.threeten.bp.Duration +import org.threeten.bp.Instant import java.text.SimpleDateFormat import java.util.Currency import java.util.Date @@ -76,8 +78,8 @@ class DeviceHelper( fun daysSince(date: Date): Int { val fromDate = date val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() + val fromInstant = Instant.ofEpochMilli(fromDate.time) + val toInstant = Instant.ofEpochMilli(toDate.time) val duration = Duration.between(fromInstant, toInstant) return duration.toDays().toInt() } @@ -85,8 +87,8 @@ class DeviceHelper( fun minutesSince(date: Date): Int { val fromDate = date val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() + val fromInstant = Instant.ofEpochMilli(fromDate.time) + val toInstant = Instant.ofEpochMilli(toDate.time) val duration = Duration.between(fromInstant, toInstant) return duration.toMinutes().toInt() } @@ -94,8 +96,8 @@ class DeviceHelper( fun hoursSince(date: Date): Int { val fromDate = date val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() + val fromInstant = Instant.ofEpochMilli(fromDate.time) + val toInstant = Instant.ofEpochMilli(toDate.time) val duration = Duration.between(fromInstant, toInstant) return duration.toHours().toInt() } @@ -103,37 +105,25 @@ class DeviceHelper( fun monthsSince(date: Date): Int { val fromDate = date val toDate = Date() - val fromInstant = fromDate.toInstant() - val toInstant = toDate.toInstant() + val fromInstant = Instant.ofEpochMilli(fromDate.time) + val toInstant = Instant.ofEpochMilli(toDate.time) val duration = Duration.between(fromInstant, toInstant) return duration.toDays().toInt() / 30 } private val daysSinceInstall: Int get() { - val fromDate = - org.threeten.bp.Instant - .ofEpochMilli(appInstallDate.time) - val toDate = - org.threeten.bp.Instant - .now() - val duration = - org.threeten.bp.Duration - .between(fromDate, toDate) + val fromDate = Instant.ofEpochMilli(appInstallDate.time) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toDays().toInt() } private val minutesSinceInstall: Int get() { - val fromDate = - org.threeten.bp.Instant - .ofEpochMilli(appInstallDate.time) - val toDate = - org.threeten.bp.Instant - .now() - val duration = - org.threeten.bp.Duration - .between(fromDate, toDate) + val fromDate = Instant.ofEpochMilli(appInstallDate.time) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toMinutes().toInt() } @@ -141,16 +131,12 @@ class DeviceHelper( get() { val fromDate = storage.read(LastPaywallView)?.let { - org.threeten.bp.Instant + Instant .ofEpochMilli(it.time) } ?: return null - val toDate = - org.threeten.bp.Instant - .now() - val duration = - org.threeten.bp.Duration - .between(fromDate, toDate) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toDays().toInt() } @@ -158,16 +144,12 @@ class DeviceHelper( get() { val fromDate = storage.read(LastPaywallView)?.let { - org.threeten.bp.Instant + Instant .ofEpochMilli(it.time) } ?: return null - val toDate = - org.threeten.bp.Instant - .now() - val duration = - org.threeten.bp.Duration - .between(fromDate, toDate) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toMinutes().toInt() } @@ -402,10 +384,10 @@ class DeviceHelper( val sdkVersion: String get() = BuildConfig.SDK_VERSION - val buildTime: String? + val buildTime: String get() = BuildConfig.BUILD_TIME - val gitSha: String? + val gitSha: String get() = BuildConfig.GIT_SHA suspend fun getDeviceAttributes( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index 1d795903..0b237d6e 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -302,7 +302,6 @@ data class PaywallInfo( name = "", url = PaywallURL(""), experiment = null, - triggerSessionId = "", products = emptyList(), productItems = emptyList(), productIds = emptyList(), @@ -323,6 +322,8 @@ data class PaywallInfo( productsLoadCompleteTime = null, productsLoadFailTime = null, productsLoadDuration = null, + shimmerLoadStartTime = null, + shimmerLoadCompleteTime = null, paywalljsVersion = null, isFreeTrialAvailable = false, featureGatingBehavior = FeatureGatingBehavior.NonGated, From 11ad604d39f1f3d437114fb9c21371a249a03cee Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Fri, 13 Dec 2024 13:58:07 +0100 Subject: [PATCH 16/37] Fix UI tests and build issues after rebase --- .../test/FlowScreenshotTestExecutor.kt | 10 +++-- .../analytics/internal/TrackingLogicTest.kt | 3 +- .../config/ConfigManagerInstrumentedTest.kt | 10 ++--- ...inedExpressionEvaluatorInstrumentedTest.kt | 9 ++++- .../DefaultJavascriptEvaluatorTest.kt | 2 +- .../TestPaywallMessageHandlerDelegate.kt | 8 ---- .../superwall/sdk/storage/ConfigureSDKTest.kt | 3 +- .../transactions/TransactionManagerTest.kt | 39 ++++++++++++------- 8 files changed, 50 insertions(+), 34 deletions(-) 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 bf0d7013..c33780d0 100644 --- a/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt +++ b/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt @@ -123,6 +123,11 @@ 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() = @@ -159,10 +164,7 @@ class FlowScreenshotTestExecutor { // We delay a bit to ensure scroll has finished delayFor(500.milliseconds) } - - step { - delayFor(10.seconds) - } } } } + */ 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/config/ConfigManagerInstrumentedTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt index dda287bd..aac59a69 100644 --- a/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt +++ b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt @@ -339,7 +339,7 @@ class ConfigManagerTests { Config.stub().copy( rawFeatureFlags = listOf( - RawFeatureFlag("enable_config_refresh", true), + RawFeatureFlag("enable_config_refresh_v2", true), ), ) @@ -412,7 +412,7 @@ class ConfigManagerTests { Config.stub().copy( rawFeatureFlags = listOf( - RawFeatureFlag("enable_config_refresh", true), + RawFeatureFlag("enable_config_refresh_v2", true), ), ) @@ -499,7 +499,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") @@ -627,7 +627,7 @@ class ConfigManagerTests { buildId = "cached", rawFeatureFlags = listOf( - RawFeatureFlag("enable_config_refresh", true), + RawFeatureFlag("enable_config_refresh_v2", true), ), ) @@ -754,7 +754,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") 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 index d71009b2..c404358f 100644 --- 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 @@ -51,7 +51,10 @@ class JavascriptCombinedExpressionEvaluatorInstrumentedTest { @Before fun setup() = runBlocking { - sandbox = JavaScriptSandbox.createConnectedInstanceAsync(InstrumentationRegistry.getInstrumentation().targetContext).await() + sandbox = + JavaScriptSandbox + .createConnectedInstanceAsync(InstrumentationRegistry.getInstrumentation().targetContext) + .await() } @After @@ -107,6 +110,7 @@ class JavascriptCombinedExpressionEvaluatorInstrumentedTest { storage.coreDataManager, ruleAttributes, ), + shouldTraceResults = false, ) val rule = @@ -167,6 +171,7 @@ class JavascriptCombinedExpressionEvaluatorInstrumentedTest { storage.coreDataManager, ruleAttributes, ), + shouldTraceResults = false, ) val trueRule = @@ -268,6 +273,7 @@ class JavascriptCombinedExpressionEvaluatorInstrumentedTest { storage.coreDataManager, ruleAttributes, ), + shouldTraceResults = false, ) val trueRule = @@ -377,6 +383,7 @@ class JavascriptCombinedExpressionEvaluatorInstrumentedTest { storage.coreDataManager, ruleAttributes, ), + shouldTraceResults = false, ) val 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 index 3bebc993..f1028930 100644 --- 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 @@ -24,7 +24,7 @@ class DefaultJavascriptEvaluatorTest { @Test fun evaulate_succesfully_with_sandbox() = runTest { - val storage = StorageMock(ctx()) + val storage = StorageMock(ctx(), coroutineScope = this) mockkStatic(WebView::class) { every { WebView.getCurrentWebViewPackage() } returns null } 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 41e2bbf5..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 @@ -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/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt index a5156146..e829b87e 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -28,6 +28,7 @@ import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState import com.superwall.sdk.products.mockPricingPhase import com.superwall.sdk.products.mockSubscriptionOfferDetails import com.superwall.sdk.storage.EventsQueue +import com.superwall.sdk.storage.Storage import com.superwall.sdk.store.InternalPurchaseController import com.superwall.sdk.store.StoreManager import com.superwall.sdk.store.abstractions.product.OfferType @@ -118,12 +119,15 @@ class TransactionManagerTest { } private var eventsQueue = mockk(relaxUnitFun = true) + private var storage = mockk() private var transactionManagerFactory = mockk { + every { makeHasExternalPurchaseController() } returns false every { makeTransactionVerifier() } returns mockk { coEvery { getLatestTransaction(any()) } returns mockk() } + every { makeSuperwallOptions() } } fun TestScope.manager( @@ -133,21 +137,23 @@ class TransactionManagerTest { SubscriptionStatus.ACTIVE }, options: SuperwallOptions.() -> Unit = {}, - ) = TransactionManager( - purchaseController = purchaseController, - storeManager = storeManager, - activityProvider = activityProvider, - subscriptionStatus = subscriptionStatus, - track = { track(it) }, - dismiss = { i, e -> dismiss(i, e) }, - eventsQueue = eventsQueue, - factory = transactionManagerFactory, - ioScope = IOScope(this.coroutineContext), - ).also { + ): TransactionManager { coEvery { transactionManagerFactory.makeSuperwallOptions() } returns SuperwallOptions().apply( options, ) + return TransactionManager( + purchaseController = purchaseController, + storeManager = storeManager, + activityProvider = activityProvider, + subscriptionStatus = subscriptionStatus, + track = { track(it) }, + dismiss = { i, e -> dismiss(i, e) }, + eventsQueue = eventsQueue, + factory = transactionManagerFactory, + ioScope = IOScope(this.coroutineContext), + storage = storage, + ) } @Test @@ -551,7 +557,12 @@ class TransactionManagerTest { ) Then("The purchase is pending") { assert(result is PurchaseResult.Cancelled) - verify { paywallView setProperty "loadingState" value any(PaywallLoadingState.Ready::class) } + verify { + paywallView setProperty "loadingState" value + any( + PaywallLoadingState.Ready::class, + ) + } And("Verify pending event") { val pendingEvent = events.value @@ -931,7 +942,9 @@ class TransactionManagerTest { When("We try to purchase the product") { val result = transactionManager.purchase( - TransactionManager.PurchaseSource.ExternalPurchase(nonRecurringStoreProduct), + TransactionManager.PurchaseSource.ExternalPurchase( + nonRecurringStoreProduct, + ), ) Then("The purchase is successful") { assert(result is PurchaseResult.Purchased) From d0ad909817fd85d1090ccf31a08ac8ed2649c93e Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Fri, 13 Dec 2024 14:11:04 +0100 Subject: [PATCH 17/37] Censor error-tracking --- .../superwall/sdk/utilities/ErrorTracking.kt | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt index 690f158a..e78d2fb9 100644 --- a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt +++ b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt @@ -71,11 +71,36 @@ internal class ErrorTracker( } } + fun String.replaceNonSuperwallPackages() = + lines() + .map { + if (it.containsAny( + "com.superwall.sdk", + "com.superwall.supercel", + "java.lang", + "net.java.dev.jna", + "kotlin.", + "android.os", + "androidx.os", + "com.android.", + "com.google.", + "org.threeten.", + "com.revenuecat.purchases", + ) + ) { + it.map { if (it.isLetter()) "*" else it } + } else { + it + } + }.joinToString("\n") + + fun String.containsAny(vararg strings: String) = strings.any { this.contains(it) } + override fun trackError(throwable: Throwable) { val errorOccurence = ErrorTracking.ErrorOccurence( message = throwable.message ?: "", - stacktrace = throwable.stackTraceToString(), + stacktrace = throwable.stackTraceToString().replaceNonSuperwallPackages(), timestamp = System.currentTimeMillis(), isFatal = throwable.isFatal(), type = throwable.javaClass.simpleName, From 94dca9582687ec0c4b1bdb4970644da5fff7e929 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 19 Sep 2024 22:11:08 +0200 Subject: [PATCH 18/37] Downgrade minSDK to version 22 --- .../com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt index 4d97d486..8c7534f5 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt @@ -23,6 +23,7 @@ import android.widget.FrameLayout import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -496,6 +497,7 @@ class SuperwallPaywallActivity : AppCompatActivity() { return@suspendCoroutine } + createNotificationChannel() notificationPermissionCallback = From 84f98e7f6da531c67dafb44b01801c5de4b8dd07 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 11 Nov 2024 14:30:59 +0100 Subject: [PATCH 19/37] Clear up after rebase, fix transaction tracking and add source to event --- .../com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt index 8c7534f5..4d97d486 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt @@ -23,7 +23,6 @@ import android.widget.FrameLayout import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -497,7 +496,6 @@ class SuperwallPaywallActivity : AppCompatActivity() { return@suspendCoroutine } - createNotificationChannel() notificationPermissionCallback = From b1266c3da09804a4e394c341f662af10221b427c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 25 Nov 2024 16:53:50 +0100 Subject: [PATCH 20/37] Migrate to entitlements from subscriptions --- .../superapp/test/PresentationRuleTests.kt | 2 +- .../test/SimpleScreenshotTestExecutor.kt | 4 +- .../purchase/RevenueCatPurchaseController.kt | 29 ++- .../superwall/superapp/test/UITestHandler.kt | 37 ++-- .../com/superwall/exampleapp/HomeActivity.kt | 6 +- .../config/ConfigManagerInstrumentedTest.kt | 11 ++ .../vc/webview/WebviewFallbackClientTest.kt | 1 + .../main/java/com/superwall/sdk/Superwall.kt | 54 +++-- .../sdk/analytics/internal/Tracking.kt | 4 +- .../trackable/TrackableSuperwallEvent.kt | 10 +- .../sdk/analytics/superwall/SuperwallEvent.kt | 4 +- .../analytics/superwall/SuperwallEvents.kt | 2 +- .../com/superwall/sdk/config/ConfigLogic.kt | 7 + .../com/superwall/sdk/config/ConfigManager.kt | 5 + .../superwall/sdk/config/PaywallPreload.kt | 1 - .../java/com/superwall/sdk/debug/DebugView.kt | 7 +- .../sdk/delegate/SuperwallDelegate.kt | 3 +- .../sdk/delegate/SuperwallDelegateAdapter.kt | 7 +- .../sdk/delegate/SuperwallDelegateJava.kt | 3 +- .../sdk/dependencies/DependencyContainer.kt | 29 +-- .../sdk/dependencies/FactoryProtocols.kt | 5 +- .../sdk/models/entitlements/Entitlement.kt | 10 + .../models/entitlements/EntitlementStatus.kt | 19 ++ .../superwall/sdk/models/paywall/Paywall.kt | 34 +--- .../models/paywall/PaywallPresentationInfo.kt | 3 - .../models/paywall/PresentationCondition.kt | 13 -- .../superwall/sdk/models/product/Product.kt | 22 +-- .../sdk/models/product/ProductItem.kt | 2 + .../sdk/network/device/DeviceHelper.kt | 6 +- .../sdk/paywall/presentation/PaywallInfo.kt | 18 +- .../InternalGetPresentationResult.kt | 2 +- .../internal/GetPaywallComponents.kt | 13 +- .../internal/InternalPresentationLogic.kt | 41 +--- .../PaywallPresentationRequestStatus.kt | 6 +- .../internal/PresentationErrors.kt | 2 +- .../operators/CheckUserSubscription.kt | 28 --- .../internal/operators/GetPaywallVC.kt | 35 +--- .../internal/operators/GetPresenter.kt | 29 +-- .../operators/WaitForSubsStatusAndConfig.kt | 82 ++++---- .../internal/request/PresentationRequest.kt | 4 +- .../superwall/sdk/paywall/view/PaywallView.kt | 9 +- .../sdk/paywall/view/webview/SWWebView.kt | 15 +- .../templating/models/DeviceTemplate.kt | 2 +- .../java/com/superwall/sdk/storage/Cache.kt | 1 - .../com/superwall/sdk/storage/CacheKeys.kt | 41 +++- ...ller.kt => AutomaticPurchaseController.kt} | 26 ++- .../com/superwall/sdk/store/Entitlements.kt | 96 +++++++++ .../sdk/store/StoreKitManagerInterface.kt | 3 +- .../com/superwall/sdk/store/StoreManager.kt | 2 + .../store/transactions/TransactionManager.kt | 14 +- .../sdk/models/paywall/PaywallProductTest.kt | 23 ++- .../templating/models/DeviceTemplateTest.kt | 4 +- .../superwall/sdk/store/EntitlementsTest.kt | 185 ++++++++++++++++++ .../superwall/sdk/store/StoreManagerTest.kt | 13 +- .../transactions/TransactionManagerTest.kt | 42 ++-- 55 files changed, 650 insertions(+), 426 deletions(-) create mode 100644 superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt create mode 100644 superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt delete mode 100644 superwall/src/main/java/com/superwall/sdk/models/paywall/PresentationCondition.kt delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckUserSubscription.kt rename superwall/src/main/java/com/superwall/sdk/store/{ExternalNativePurchaseController.kt => AutomaticPurchaseController.kt} (92%) create mode 100644 superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt create mode 100644 superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt 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..5f3b19f1 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,7 @@ 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.sdk.models.entitlements.EntitlementStatus import com.superwall.superapp.test.UITestHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -144,7 +144,7 @@ class SimpleScreenshotTestExecutor { it.waitFor { it is SuperwallEvent.PaywallPresentationRequest } } } - Superwall.instance.setSubscriptionStatus(SubscriptionStatus.INACTIVE) + Superwall.instance.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) } @Test 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..d26e1628 100644 --- a/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt +++ b/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt @@ -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) + }.toSet(), + ), + ) } else { - setSubscriptionStatus(SubscriptionStatus.INACTIVE) + setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) } } } @@ -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) + }.toSet(), + ), + ) } else { - setSubscriptionStatus(SubscriptionStatus.INACTIVE) + setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) } } @@ -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/UITestHandler.kt b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt index 0d9f5a29..91e588cd 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -7,10 +7,11 @@ 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 @@ -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.NoActiveEntitlements) }, ) var test10Info = @@ -472,15 +473,15 @@ object UITestHandler { "4s later.", test = { scope, events -> // Set user as subscribed - Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) // 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.NoActiveEntitlements) } }, ) @@ -490,16 +491,16 @@ 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 + var currentSubscriptionStatus = Superwall.instance.entitlementStatus.value - Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) // Try to present paywall again Superwall.instance.register(event = "register_nongated_paywall") scope.launch { delay(4000) - Superwall.instance.setSubscriptionStatus(currentSubscriptionStatus) + Superwall.instance.setEntitlementStatus(currentSubscriptionStatus) } }, ) @@ -527,9 +528,9 @@ 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 + var currentSubscriptionStatus = Superwall.instance.entitlementStatus.value - Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) Superwall.instance.register(event = "register_gated_paywall") { val alertController = @@ -542,7 +543,7 @@ object UITestHandler { alertController.show() } delay(8000) - Superwall.instance.setSubscriptionStatus(currentSubscriptionStatus) + Superwall.instance.setEntitlementStatus(currentSubscriptionStatus) }, ) var test28Info = @@ -622,7 +623,7 @@ 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( @@ -630,7 +631,7 @@ object UITestHandler { "UserIsSubscribed expected, received $result", ) println("!!! TEST 32 !!! $result") - Superwall.instance.setSubscriptionStatus(SubscriptionStatus.INACTIVE) + Superwall.instance.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) } }, ) @@ -731,11 +732,11 @@ object UITestHandler { subscribed: Boolean, gated: Boolean, ) { - val currentSubscriptionStatus = Superwall.instance.subscriptionStatus.value + val currentSubscriptionStatus = Superwall.instance.entitlementStatus.value if (subscribed) { // Set user subscribed - Superwall.instance.setSubscriptionStatus(SubscriptionStatus.ACTIVE) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) } // Determine gating event @@ -766,7 +767,7 @@ object UITestHandler { if (subscribed) { // Reset status - Superwall.instance.setSubscriptionStatus(currentSubscriptionStatus) + Superwall.instance.setEntitlementStatus(currentSubscriptionStatus) } } @@ -1352,7 +1353,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.NoActiveEntitlements) // Create a mock Superwall delegate val delegate = MockSuperwallDelegate() 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/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt index aac59a69..4509b90b 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 @@ -36,6 +37,9 @@ import com.superwall.sdk.storage.LocalStorage import com.superwall.sdk.storage.Storage import com.superwall.sdk.storage.StorageMock import com.superwall.sdk.store.StoreManager +import com.superwall.sdk.storage.StoredEntitlementStatus +import com.superwall.sdk.storage.StoredEntitlementsByProductId +import com.superwall.sdk.store.Entitlements import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify @@ -84,6 +88,13 @@ class ConfigManagerUnderTest( paywallPreload = paywallPreload, ioScope = IOScope(ioScope.coroutineContext), track = {}, + entitlements = + Entitlements( + mockk(relaxUnitFun = true) { + every { read(StoredEntitlementStatus) } returns EntitlementStatus.Unkown + every { read(StoredEntitlementsByProductId) } returns emptyMap() + }, + ), ) { suspend fun setConfig(config: Config) { configState.emit(ConfigState.Retrieved(config)) 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 b13de2fb..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 @@ -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/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index d94f59b5..ecee3e83 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -13,7 +13,6 @@ import com.superwall.sdk.config.models.ConfigurationStatus import com.superwall.sdk.config.options.SuperwallOptions import com.superwall.sdk.delegate.InternalPurchaseResult import com.superwall.sdk.delegate.PurchaseResult -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.delegate.SuperwallDelegate import com.superwall.sdk.delegate.SuperwallDelegateJava import com.superwall.sdk.delegate.subscription_controller.PurchaseController @@ -30,6 +29,7 @@ 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.EntitlementStatus import com.superwall.sdk.models.events.EventData import com.superwall.sdk.network.device.InterfaceStyle import com.superwall.sdk.paywall.presentation.PaywallCloseReason @@ -40,6 +40,8 @@ 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.storage.StoredEntitlementStatus +import com.superwall.sdk.store.Entitlements import com.superwall.sdk.paywall.view.PaywallView import com.superwall.sdk.paywall.view.SuperwallPaywallActivity import com.superwall.sdk.paywall.view.delegate.PaywallViewEventCallback @@ -51,8 +53,6 @@ import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.Initiate 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.ActiveSubscriptionStatus -import com.superwall.sdk.store.ExternalNativePurchaseController import com.superwall.sdk.store.PurchasingObserverState import com.superwall.sdk.store.abstractions.product.RawStoreProduct import com.superwall.sdk.store.abstractions.product.StoreProduct @@ -190,14 +190,14 @@ 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 * 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 @@ -205,8 +205,8 @@ class Superwall( * * @param subscriptionStatus The subscription status of the user. */ - fun setSubscriptionStatus(subscriptionStatus: SubscriptionStatus) { - _subscriptionStatus.value = subscriptionStatus + fun setEntitlementStatus(entitlementStatus: EntitlementStatus) { + entitlements.setEntitlementStatus(entitlementStatus) } /** @@ -243,16 +243,18 @@ class Superwall( return presentedPaywallInfo ?: presentationItems.paywallInfo } - protected var _subscriptionStatus: MutableStateFlow = - MutableStateFlow( - SubscriptionStatus.UNKNOWN, - ) + val entitlements: Entitlements by lazy { + dependencyContainer.entitlements + } /** - * A `StateFlow` of the subscription status of the user. Set this using - * [setSubscriptionStatus]. + * A `StateFlow` of the entitlement status of the user. Set this using + * [setEntitlementStatus]. */ - val subscriptionStatus: StateFlow get() = _subscriptionStatus + + val entitlementStatus: StateFlow by lazy { + entitlements.status + } /** * A property that indicates current configuration state of the SDK. @@ -305,7 +307,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 @@ -333,12 +335,6 @@ class Superwall( completion?.invoke(Result.success(Unit)) return } - val purchaseController = - purchaseController - ?: ExternalNativePurchaseController( - context = applicationContext, - scope = IOScope(), - ) _instance = Superwall( context = applicationContext, @@ -401,10 +397,10 @@ class Superwall( throw e } - val cachedSubsStatus = - dependencyContainer.storage.read(ActiveSubscriptionStatus) - ?: SubscriptionStatus.UNKNOWN - setSubscriptionStatus(cachedSubsStatus) + val cachedEntitlementStatus = + dependencyContainer.storage.read(StoredEntitlementStatus) + ?: EntitlementStatus.Unkown + setEntitlementStatus(cachedEntitlementStatus) addListeners() @@ -439,13 +435,13 @@ class Superwall( // / Listens to config and the subscription status private fun addListeners() { ioScope.launchWithTracking { - subscriptionStatus // Removes duplicates by default + entitlementStatus // 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) } } diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt index 7fc0a687..94391a69 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt @@ -14,7 +14,7 @@ import com.superwall.sdk.paywall.presentation.dismissForNextPaywall import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType import com.superwall.sdk.paywall.presentation.internal.internallyPresent import com.superwall.sdk.paywall.presentation.internal.operators.logErrors -import com.superwall.sdk.paywall.presentation.internal.operators.waitForSubsStatusAndConfig +import com.superwall.sdk.paywall.presentation.internal.operators.waitForEntitlementsAndConfig import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.storage.DisableVerboseEvents @@ -130,7 +130,7 @@ private suspend fun Superwall.internallyHandleImplicitTrigger( ) try { - waitForSubsStatusAndConfig(request, null) + waitForEntitlementsAndConfig(request, null) } catch (e: Throwable) { logErrors(request, e) return@withErrorTracking diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt index 0cdce3df..9ad1ffe5 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt @@ -7,10 +7,10 @@ import com.superwall.sdk.config.models.Survey import com.superwall.sdk.config.models.SurveyOption import com.superwall.sdk.config.options.SuperwallOptions import com.superwall.sdk.config.options.toMap -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.dependencies.ComputedPropertyRequestsFactory import com.superwall.sdk.dependencies.FeatureFlagsFactory import com.superwall.sdk.dependencies.RuleAttributesFactory +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.triggers.InternalTriggerResult import com.superwall.sdk.paywall.presentation.PaywallInfo @@ -255,13 +255,13 @@ sealed class InternalSuperwallEvent( } } - class SubscriptionStatusDidChange( - val subscriptionStatus: SubscriptionStatus, + class EntitlementStatusDidChange( + val entitlementStatus: EntitlementStatus, override var audienceFilterParams: HashMap = HashMap(), - ) : InternalSuperwallEvent(SuperwallEvent.SubscriptionStatusDidChange()) { + ) : InternalSuperwallEvent(SuperwallEvent.EntitlementStatusDidChange()) { override suspend fun getSuperwallParameters(): HashMap = hashMapOf( - "subscription_status" to subscriptionStatus.toString(), + "entitlement_status" to entitlementStatus.toString(), ) } diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt index 0272e411..5895e678 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvent.kt @@ -77,9 +77,9 @@ sealed class SuperwallEvent { } // / When the user's subscription status changes. - class SubscriptionStatusDidChange : SuperwallEvent() { + class EntitlementStatusDidChange : SuperwallEvent() { override val rawName: String - get() = "subscriptionStatus_didChange" + get() = "entitlementStatus_didChange" } // / Anytime the app leaves the foreground. diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt index 52d29321..ef6f346f 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/superwall/SuperwallEvents.kt @@ -23,7 +23,7 @@ enum class SuperwallEvents( SubscriptionStart("subscription_start"), SurveyResponse("survey_response"), SurveyClose("survey_close"), - SubscriptionStatusDidChange("subscriptionStatus_didChange"), + EntitlementStatusDidChange("entitlementStatus_didChange"), FreeTrialStart("freeTrial_start"), UserAttributes("user_attributes"), NonRecurringProductPurchase("nonRecurringProduct_purchase"), diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt index c1bddda0..e6a04453 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt @@ -316,4 +316,11 @@ object ConfigLogic { val triggers = from return triggers.associateBy { it.eventName } } + + // Returns entitlements mapped by product ID + fun extractEntitlementsByProductId(from: List) = + from + .flatMap { it.products } + .map { it.fullProductId to it.entitlements } + .toMap() } diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt index 36861e0f..0c9adb7a 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt @@ -34,6 +34,7 @@ import com.superwall.sdk.storage.LatestConfig import com.superwall.sdk.storage.LatestGeoInfo import com.superwall.sdk.storage.Storage import com.superwall.sdk.store.StoreManager +import com.superwall.sdk.store.Entitlements import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow @@ -49,6 +50,7 @@ import java.util.concurrent.atomic.AtomicInteger open class ConfigManager( private val context: Context, private val storeManager: StoreManager, + private val entitlements: Entitlements, private val storage: Storage, private val network: SuperwallAPI, private val deviceHelper: DeviceHelper, @@ -305,6 +307,9 @@ open class ConfigManager( } triggersByEventName = ConfigLogic.getTriggersByEventName(config.triggers) assignments.choosePaywallVariants(config.triggers) + ConfigLogic.extractEntitlementsByProductId(config.paywalls).let { + entitlements.addEntitlementsByProductId(it) + } } // Preloading Paywalls diff --git a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt index 79c0b781..145eb5ff 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt @@ -101,7 +101,6 @@ class PaywallPreload( overrides = null, isDebuggerLaunched = false, presentationSourceType = null, - retryCount = 6, ) try { paywallManager.getPaywallView( diff --git a/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt b/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt index b59ece95..946d47de 100644 --- a/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt +++ b/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt @@ -31,7 +31,6 @@ import com.superwall.sdk.BuildConfig import com.superwall.sdk.R import com.superwall.sdk.Superwall import com.superwall.sdk.debug.localizations.SWLocalizationActivity -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.dependencies.RequestFactory import com.superwall.sdk.dependencies.ViewFactory import com.superwall.sdk.logger.LogLevel @@ -39,6 +38,7 @@ import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.misc.fold import com.superwall.sdk.misc.toResult +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.network.Network import com.superwall.sdk.paywall.manager.PaywallManager @@ -561,7 +561,6 @@ class DebugView( overrides = null, isDebuggerLaunched = true, presentationSourceType = null, - retryCount = 6, ) var paywall = paywallRequestManager.getPaywall(request).toResult().getOrThrow() @@ -836,7 +835,7 @@ class DebugView( // bottomButton.setImageDrawable(null) // bottomButton.showLoading = true - val inactiveSubscriptionPublisher = MutableStateFlow(SubscriptionStatus.INACTIVE) + val inactiveSubscriptionPublisher = MutableStateFlow(EntitlementStatus.NoActiveEntitlements) val presentationRequest = factory.makePresentationRequest( @@ -847,7 +846,7 @@ class DebugView( paywallOverrides = null, presenter = encapsulatingActivity, isDebuggerLaunched = true, - subscriptionStatus = inactiveSubscriptionPublisher, + entitlementStatus = inactiveSubscriptionPublisher, isPaywallPresented = Superwall.instance.isPaywallPresented, type = PresentationRequestType.Presentation, ) diff --git a/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegate.kt b/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegate.kt index f80e2edc..38d8783c 100644 --- a/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegate.kt +++ b/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegate.kt @@ -2,11 +2,12 @@ package com.superwall.sdk.delegate import android.net.Uri import com.superwall.sdk.analytics.superwall.SuperwallEventInfo +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.paywall.presentation.PaywallInfo import java.net.URI interface SuperwallDelegate { - fun subscriptionStatusDidChange(to: SubscriptionStatus) {} + fun entitlementStatusDidChange(to: EntitlementStatus) {} fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {} diff --git a/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateAdapter.kt b/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateAdapter.kt index f0aaa7a3..e8c93469 100644 --- a/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateAdapter.kt +++ b/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateAdapter.kt @@ -2,6 +2,7 @@ package com.superwall.sdk.delegate import android.net.Uri import com.superwall.sdk.analytics.superwall.SuperwallEventInfo +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.paywall.presentation.PaywallInfo import java.net.URI @@ -49,9 +50,9 @@ class SuperwallDelegateAdapter { ?: javaDelegate?.handleSuperwallEvent(eventInfo) } - fun subscriptionStatusDidChange(newValue: SubscriptionStatus) { - kotlinDelegate?.subscriptionStatusDidChange(newValue) - ?: javaDelegate?.subscriptionStatusDidChange(newValue) + fun entitlementStatusDidChange(newValue: EntitlementStatus) { + kotlinDelegate?.entitlementStatusDidChange(newValue) + ?: javaDelegate?.entitlementStatusDidChange(newValue) } fun handleLog( diff --git a/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateJava.kt b/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateJava.kt index 2cb84214..f659334f 100644 --- a/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateJava.kt +++ b/superwall/src/main/java/com/superwall/sdk/delegate/SuperwallDelegateJava.kt @@ -2,6 +2,7 @@ package com.superwall.sdk.delegate import android.net.Uri import com.superwall.sdk.analytics.superwall.SuperwallEventInfo +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.paywall.presentation.PaywallInfo import java.net.URI @@ -22,7 +23,7 @@ interface SuperwallDelegateJava { fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {} - fun subscriptionStatusDidChange(newValue: SubscriptionStatus) {} + fun entitlementStatusDidChange(newValue: EntitlementStatus) {} fun handleLog( level: String, diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index c0afc11d..765135fb 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -22,7 +22,6 @@ import com.superwall.sdk.config.PaywallPreload import com.superwall.sdk.config.options.SuperwallOptions import com.superwall.sdk.debug.DebugManager import com.superwall.sdk.debug.DebugView -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.delegate.SuperwallDelegateAdapter import com.superwall.sdk.delegate.subscription_controller.PurchaseController import com.superwall.sdk.identity.IdentityInfo @@ -34,6 +33,7 @@ import com.superwall.sdk.misc.IOScope import com.superwall.sdk.misc.MainScope import com.superwall.sdk.models.config.ComputedPropertyRequest import com.superwall.sdk.models.config.FeatureFlags +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.models.product.ProductVariable @@ -75,6 +75,8 @@ import com.superwall.sdk.paywall.view.webview.templating.models.Variables import com.superwall.sdk.paywall.view.webview.webViewExists import com.superwall.sdk.storage.EventsQueue import com.superwall.sdk.storage.LocalStorage +import com.superwall.sdk.store.AutomaticPurchaseController +import com.superwall.sdk.store.Entitlements import com.superwall.sdk.store.InternalPurchaseController import com.superwall.sdk.store.StoreManager import com.superwall.sdk.store.abstractions.transactions.GoogleBillingPurchaseTransaction @@ -142,6 +144,9 @@ class DependencyContainer( var storeManager: StoreManager val transactionManager: TransactionManager val googleBillingWrapper: GoogleBillingWrapper + + var entitlements: Entitlements + private val uiScope get() = mainScope() private val ioScope @@ -181,8 +186,6 @@ class DependencyContainer( internal val errorTracker: ErrorTracker init { - // TODO: Add delegate adapter - // For tracking when the app enters the background. uiScope.launch { ProcessLifecycleOwner.get().lifecycle.addObserver(appLifecycleObserver) @@ -210,17 +213,18 @@ class DependencyContainer( appLifecycleObserver = appLifecycleObserver, this, ) + storage = LocalStorage(context = context, ioScope = ioScope(), factory = this, json = json()) + entitlements = Entitlements(storage) var purchaseController = InternalPurchaseController( - kotlinPurchaseController = purchaseController, + kotlinPurchaseController = purchaseController ?: AutomaticPurchaseController(context, ioScope, entitlements), javaPurchaseController = null, context, ) storeManager = StoreManager(purchaseController, googleBillingWrapper) delegateAdapter = SuperwallDelegateAdapter() - storage = LocalStorage(context = context, ioScope = ioScope(), factory = this, json = json()) val httpConnection = CustomHttpUrlConnection( json = json(), @@ -309,11 +313,11 @@ class DependencyContainer( deviceHelper = deviceHelper, assignments = assignments, ioScope = ioScope, - paywallPreload = - paywallPreload, + paywallPreload = paywallPreload, track = { Superwall.instance.track(it) }, + entitlements = entitlements, ) eventsQueue = @@ -423,8 +427,8 @@ class DependencyContainer( "X-Bundle-ID" to deviceHelper.bundleId, "X-Low-Power-Mode" to deviceHelper.isLowPowerModeEnabled.toString(), "X-Is-Sandbox" to deviceHelper.isSandbox.toString(), - "X-Subscription-Status" to - Superwall.instance.subscriptionStatus.value + "X-Entitlement-Status" to + Superwall.instance.entitlementStatus.value .toString(), "Content-Type" to "application/json", "X-Current-Time" to dateFormat(DateUtils.ISO_MILLIS).format(Date()), @@ -541,7 +545,6 @@ class DependencyContainer( overrides: PaywallRequest.Overrides?, isDebuggerLaunched: Boolean, presentationSourceType: String?, - retryCount: Int, ): PaywallRequest = PaywallRequest( @@ -550,7 +553,7 @@ class DependencyContainer( overrides = overrides ?: PaywallRequest.Overrides(products = null, isFreeTrial = null), isDebuggerLaunched = isDebuggerLaunched, presentationSourceType = presentationSourceType, - retryCount = retryCount, + retryCount = 6, ) override fun makePresentationRequest( @@ -558,7 +561,7 @@ class DependencyContainer( paywallOverrides: PaywallOverrides?, presenter: Activity?, isDebuggerLaunched: Boolean?, - subscriptionStatus: StateFlow?, + entitlementStatus: StateFlow?, isPaywallPresented: Boolean, type: PresentationRequestType, ): PresentationRequest = @@ -570,7 +573,7 @@ class DependencyContainer( PresentationRequest.Flags( isDebuggerLaunched = isDebuggerLaunched ?: debugManager.isDebuggerLaunched, // TODO: (PresentationCritical) Fix subscription status - subscriptionStatus = subscriptionStatus ?: Superwall.instance.subscriptionStatus, + entitlements = entitlementStatus ?: Superwall.instance.entitlementStatus, // subscriptionStatus = subscriptionStatus!!, isPaywallPresented = isPaywallPresented, type = type, diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt index 2762d176..cc586276 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt @@ -7,7 +7,6 @@ import com.superwall.sdk.billing.GoogleBillingWrapper import com.superwall.sdk.config.ConfigManager import com.superwall.sdk.config.options.SuperwallOptions import com.superwall.sdk.debug.DebugView -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.identity.IdentityInfo import com.superwall.sdk.identity.IdentityManager import com.superwall.sdk.misc.AppLifecycleObserver @@ -15,6 +14,7 @@ import com.superwall.sdk.misc.IOScope import com.superwall.sdk.misc.MainScope import com.superwall.sdk.models.config.ComputedPropertyRequest import com.superwall.sdk.models.config.FeatureFlags +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.models.events.EventData import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.models.product.ProductVariable @@ -74,7 +74,6 @@ interface RequestFactory { overrides: PaywallRequest.Overrides?, isDebuggerLaunched: Boolean, presentationSourceType: String?, - retryCount: Int, ): PaywallRequest fun makePresentationRequest( @@ -82,7 +81,7 @@ interface RequestFactory { paywallOverrides: PaywallOverrides? = null, presenter: Activity? = null, isDebuggerLaunched: Boolean? = null, - subscriptionStatus: StateFlow? = null, + entitlementStatus: StateFlow? = null, isPaywallPresented: Boolean, type: PresentationRequestType, ): PresentationRequest diff --git a/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt b/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt new file mode 100644 index 00000000..ac87afbd --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt @@ -0,0 +1,10 @@ +package com.superwall.sdk.models.entitlements + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Entitlement( + @SerialName("id") + val id: String, +) diff --git a/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt new file mode 100644 index 00000000..37eb0066 --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt @@ -0,0 +1,19 @@ +package com.superwall.sdk.models.entitlements + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed class EntitlementStatus { + @Serializable + object Unkown : EntitlementStatus() + + @Serializable + object NoActiveEntitlements : EntitlementStatus() + + @Serializable + data class Active( + @SerialName("entitlements") + val entitlements: Set, + ) : EntitlementStatus() +} diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt index d306eeeb..602235da 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt @@ -10,10 +10,8 @@ import com.superwall.sdk.models.SerializableEntity import com.superwall.sdk.models.config.ComputedPropertyRequest import com.superwall.sdk.models.config.FeatureGatingBehavior import com.superwall.sdk.models.events.EventData -import com.superwall.sdk.models.product.Product import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.product.ProductItemsDeserializer -import com.superwall.sdk.models.product.ProductType import com.superwall.sdk.models.product.ProductVariable import com.superwall.sdk.models.serialization.DateSerializer import com.superwall.sdk.models.triggers.Experiment @@ -64,7 +62,6 @@ data class Paywall( "Unknown or unsupported presentation style: $presentationStyle", ) }, - condition = PresentationCondition.valueOf(presentationCondition.uppercase()), delay = presentationDelay, ), @SerialName("background_color_hex") @@ -73,7 +70,7 @@ data class Paywall( val darkBackgroundColorHex: String? = null, // Declared as private to prevent direct access @kotlinx.serialization.Transient() - private var _products: List = emptyList(), + private var _products: List = emptyList(), @Serializable(with = ProductItemsDeserializer::class) @SerialName("products_v2") private var _productItems: List, @@ -131,11 +128,11 @@ data class Paywall( _productItems = value // Automatically update related properties when productItems is set productIds = value.map { it.fullProductId } - _products = makeProducts(value) // Assuming makeProducts is a function that generates products based on product items + _products = value // Assuming makeProducts is a function that generates products based on product items } // Public getter for products to allow access but not direct modification - val products: List + val products: List get() = _products val backgroundColor: Int by lazy { @@ -200,7 +197,6 @@ data class Paywall( url = url, products = products, productIds = productIds, - productItems = productItems, eventData = fromEvent, responseLoadStartTime = responseLoadingInfo.startAt, responseLoadCompleteTime = responseLoadingInfo.endAt, @@ -229,29 +225,6 @@ data class Paywall( ) companion object { - private fun makeProducts(productItems: List): List { - val output = mutableListOf() - - for (productItem in productItems) { - when (productItem.name) { - "primary" -> - output.add( - Product(type = ProductType.PRIMARY, id = productItem.fullProductId), - ) - "secondary" -> - output.add( - Product(type = ProductType.SECONDARY, id = productItem.fullProductId), - ) - "tertiary" -> - output.add( - Product(type = ProductType.TERTIARY, id = productItem.fullProductId), - ) - } - } - - return output - } - fun stub(): Paywall = Paywall( databaseId = "id", @@ -262,7 +235,6 @@ data class Paywall( presentation = PaywallPresentationInfo( PaywallPresentationStyle.MODAL, - PresentationCondition.CHECK_USER_SUBSCRIPTION, 300, ), presentationStyle = "MODAL", diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationInfo.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationInfo.kt index 2c522845..aaf8be6e 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationInfo.kt @@ -9,9 +9,6 @@ data class PaywallPresentationInfo( // The presentation style of the paywall @SerialName("style") val style: PaywallPresentationStyle, - // The condition for when a paywall should present. - @SerialName("condition") - val condition: PresentationCondition, // The delay in milliseconds before switching from the loading view to // the paywall view. @SerialName("delay") diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/PresentationCondition.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/PresentationCondition.kt deleted file mode 100644 index 833af4e9..00000000 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/PresentationCondition.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.superwall.sdk.models.paywall - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -enum class PresentationCondition { - @SerialName("ALWAYS") - ALWAYS, - - @SerialName("CHECK_USER_SUBSCRIPTION") - CHECK_USER_SUBSCRIPTION, -} diff --git a/superwall/src/main/java/com/superwall/sdk/models/product/Product.kt b/superwall/src/main/java/com/superwall/sdk/models/product/Product.kt index 997d4685..a0712370 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/product/Product.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/product/Product.kt @@ -1,19 +1,7 @@ package com.superwall.sdk.models.product -import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonEncoder -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive @Serializable enum class ProductType { @@ -30,13 +18,7 @@ enum class ProductType { override fun toString() = name.lowercase() } - -@Serializable(with = ProductSerializer::class) -data class Product( - val type: ProductType, - val id: String, -) - +/* @Serializer(forClass = Product::class) object ProductSerializer : KSerializer { override fun serialize( @@ -70,3 +52,5 @@ object ProductSerializer : KSerializer { return Product(type, id) } } +* + */ diff --git a/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt b/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt index f3c5e8f4..741a8eaf 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt @@ -127,6 +127,7 @@ data class ProductItem( @SerialName("reference_name") val name: String, val type: StoreProductType, + val entitlements: Set, ) { sealed class StoreProductType { data class PlayStore( @@ -179,6 +180,7 @@ object ProductItemSerializer : KSerializer { return ProductItem( name = name, type = ProductItem.StoreProductType.PlayStore(storeProduct), + entitlements = emptySet(), ) } } diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index 8acc148f..ea4e09be 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -486,9 +486,9 @@ class DeviceHelper( utcDateTime = utcDateTimeString, localDateTime = localDateTimeString, isSandbox = isSandbox.toString(), - subscriptionStatus = - Superwall.instance.subscriptionStatus.value - .toString(), + activeEntitlements = + Superwall.instance.entitlements.active + .map { it.id }, isFirstAppOpen = isFirstAppOpen, sdkVersion = sdkVersion, sdkVersionPadded = sdkVersionPadded, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index 0b237d6e..7b16ac0f 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -12,8 +12,6 @@ import com.superwall.sdk.models.paywall.LocalNotification import com.superwall.sdk.models.paywall.PaywallPresentationInfo import com.superwall.sdk.models.paywall.PaywallPresentationStyle import com.superwall.sdk.models.paywall.PaywallURL -import com.superwall.sdk.models.paywall.PresentationCondition -import com.superwall.sdk.models.product.Product import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.triggers.Experiment import com.superwall.sdk.store.abstractions.product.StoreProduct @@ -29,12 +27,7 @@ data class PaywallInfo( val name: String, val url: PaywallURL, val experiment: Experiment?, - @Deprecated( - message = "Use productItems because a paywall can support more than three products", - ReplaceWith("productsItems"), - ) - val products: List, - val productItems: List, + val products: List, val productIds: List, val presentedByEventWithName: String?, val presentedByEventWithId: String?, @@ -72,8 +65,7 @@ data class PaywallInfo( identifier: String, name: String, url: PaywallURL, - products: List, - productItems: List, + products: List, productIds: List, eventData: EventData?, responseLoadStartTime: Date?, @@ -111,7 +103,6 @@ data class PaywallInfo( experiment = experiment, paywalljsVersion = paywalljsVersion, products = products, - productItems = productItems, productIds = productIds, isFreeTrialAvailable = isFreeTrialAvailable, featureGatingBehavior = featureGatingBehavior, @@ -279,7 +270,7 @@ data class PaywallInfo( output["secondary_product_id"] = "" output["tertiary_product_id"] = "" - productItems.forEachIndexed { index, product -> + products.forEachIndexed { index, product -> when (index) { 0 -> output["primary_product_id"] = product.fullProductId 1 -> output["secondary_product_id"] = product.fullProductId @@ -303,7 +294,6 @@ data class PaywallInfo( url = PaywallURL(""), experiment = null, products = emptyList(), - productItems = emptyList(), productIds = emptyList(), presentedByEventWithName = null, presentedByEventWithId = null, @@ -331,7 +321,7 @@ data class PaywallInfo( localNotifications = emptyList(), computedPropertyRequests = emptyList(), surveys = emptyList(), - presentation = PaywallPresentationInfo(PaywallPresentationStyle.NONE, PresentationCondition.ALWAYS, 0), + presentation = PaywallPresentationInfo(PaywallPresentationStyle.NONE, 0), buildId = "", cacheKey = "", isScrollEnabled = true, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt index 95d20c5d..4d9d5e60 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt @@ -50,7 +50,7 @@ private fun handle( is PaywallPresentationRequestStatusReason.NoPresenter, is PaywallPresentationRequestStatusReason.PaywallAlreadyPresented, is PaywallPresentationRequestStatusReason.NoConfig, - is PaywallPresentationRequestStatusReason.SubscriptionStatusTimeout, + is PaywallPresentationRequestStatusReason.EntitlementStatusTimeout, -> PresentationResult.PaywallNotAvailable() } } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt index 4a128286..ecf93394 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt @@ -6,13 +6,12 @@ import com.superwall.sdk.misc.toResult import com.superwall.sdk.models.assignment.ConfirmedAssignment import com.superwall.sdk.paywall.presentation.get_paywall.PaywallComponents import com.superwall.sdk.paywall.presentation.internal.operators.checkDebuggerPresentation -import com.superwall.sdk.paywall.presentation.internal.operators.checkUserSubscription import com.superwall.sdk.paywall.presentation.internal.operators.confirmHoldoutAssignment import com.superwall.sdk.paywall.presentation.internal.operators.confirmPaywallAssignment import com.superwall.sdk.paywall.presentation.internal.operators.evaluateRules import com.superwall.sdk.paywall.presentation.internal.operators.getPaywallView import com.superwall.sdk.paywall.presentation.internal.operators.getPresenterIfNecessary -import com.superwall.sdk.paywall.presentation.internal.operators.waitForSubsStatusAndConfig +import com.superwall.sdk.paywall.presentation.internal.operators.waitForEntitlementsAndConfig import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.utilities.withErrorTracking import kotlinx.coroutines.flow.MutableSharedFlow @@ -32,7 +31,7 @@ suspend fun Superwall.getPaywallComponents( publisher: MutableSharedFlow? = null, ): Result = withErrorTracking { - waitForSubsStatusAndConfig(request, publisher) + waitForEntitlementsAndConfig(request, publisher) // TODO: // val debugInfo = logPresentation(request) val debugInfo = emptyMap() @@ -42,12 +41,6 @@ suspend fun Superwall.getPaywallComponents( val rulesOutcome = evaluateRules(request) val outcome = rulesOutcome.getOrThrow() - checkUserSubscription( - request = request, - triggerResult = outcome.triggerResult, - paywallStatePublisher = publisher, - ) - confirmHoldoutAssignment(request = request, rulesOutcome = outcome) val paywallView = @@ -71,7 +64,7 @@ suspend fun Superwall.getPaywallComponents( internal suspend fun Superwall.confirmAssignment(request: PresentationRequest): Either { return withErrorTracking { - waitForSubsStatusAndConfig(request) + waitForEntitlementsAndConfig(request) val rules = evaluateRules(request) if (rules.isFailure) { throw rules.exceptionOrNull()!! diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentationLogic.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentationLogic.kt index 26145825..e61d5593 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentationLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentationLogic.kt @@ -1,49 +1,10 @@ package com.superwall.sdk.paywall.presentation.internal -import com.superwall.sdk.models.paywall.PresentationCondition - object InternalPresentationLogic { - data class UserSubscriptionOverrides( - val isDebuggerLaunched: Boolean, - val shouldIgnoreSubscriptionStatus: Boolean?, - var presentationCondition: PresentationCondition?, - ) - - fun userSubscribedAndNotOverridden( - isUserSubscribed: Boolean, - overrides: UserSubscriptionOverrides, - ): Boolean { - if (overrides.isDebuggerLaunched) { - return false - } - - fun checkSubscriptionStatus(): Boolean { - if (!isUserSubscribed) { - return false - } - if (overrides.shouldIgnoreSubscriptionStatus == true) { - return false - } - return true - } - - val presentationCondition = overrides.presentationCondition ?: return checkSubscriptionStatus() - - if (presentationCondition == PresentationCondition.ALWAYS) { - return false - } - - return checkSubscriptionStatus() - } - fun presentationError( domain: String, code: Int, title: String, value: String, - ): Throwable { - // In Kotlin, we usually throw exceptions rather than errors - // Kotlin does not have a built-in equivalent to NSError - return RuntimeException("$domain: $code, $title - $value") - } + ): Throwable = RuntimeException("$domain: $code, $title - $value") } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt index cd61e0f2..30771718 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt @@ -48,10 +48,10 @@ sealed class PaywallPresentationRequestStatusReason( class NoConfig : PaywallPresentationRequestStatusReason("no_config") /** - * The subscription status timed out. - * This happens when the subscriptionStatus stays unknown for more than 5 seconds. + * The entitlement status timed out. + * This happens when the entitlementStatus stays unknown for more than 5 seconds. */ - class SubscriptionStatusTimeout : PaywallPresentationRequestStatusReason("subscription_status_timeout") + class EntitlementStatusTimeout : PaywallPresentationRequestStatusReason("subscription_status_timeout") } typealias PresentationPipelineError = PaywallPresentationRequestStatusReason diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt index 85f90311..28e18ecf 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt @@ -5,7 +5,7 @@ import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReaso import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import kotlinx.coroutines.flow.MutableSharedFlow -suspend fun Superwall.userIsSubscribed(paywallStatePublisher: MutableSharedFlow?): PresentationPipelineError { +suspend fun Superwall.userHasEntitlements(paywallStatePublisher: MutableSharedFlow?): PresentationPipelineError { val state = PaywallState.Skipped(PaywallSkippedReason.UserIsSubscribed()) paywallStatePublisher?.emit(state) return PaywallPresentationRequestStatusReason.UserIsSubscribed() diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckUserSubscription.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckUserSubscription.kt deleted file mode 100644 index c363126c..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckUserSubscription.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.superwall.sdk.paywall.presentation.internal.operators - -import com.superwall.sdk.Superwall -import com.superwall.sdk.delegate.SubscriptionStatus -import com.superwall.sdk.models.triggers.InternalTriggerResult -import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason -import com.superwall.sdk.paywall.presentation.internal.PresentationRequest -import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason -import com.superwall.sdk.paywall.presentation.internal.state.PaywallState -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.first - -suspend fun Superwall.checkUserSubscription( - request: PresentationRequest, - triggerResult: InternalTriggerResult, - paywallStatePublisher: MutableSharedFlow? = null, -) { - when (triggerResult) { - is InternalTriggerResult.Paywall -> return - else -> { - val subscriptionStatus = request.flags.subscriptionStatus.first() - if (subscriptionStatus == SubscriptionStatus.ACTIVE) { - paywallStatePublisher?.emit(PaywallState.Skipped(PaywallSkippedReason.UserIsSubscribed())) - throw PaywallPresentationRequestStatusReason.UserIsSubscribed() - } - } - } -} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt index 7d0f6452..9e443226 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPaywallVC.kt @@ -1,26 +1,21 @@ package com.superwall.sdk.paywall.presentation.internal.operators import com.superwall.sdk.Superwall -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.dependencies.DependencyContainer import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.misc.toResult -import com.superwall.sdk.paywall.presentation.internal.InternalPresentationLogic import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType -import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason import com.superwall.sdk.paywall.presentation.internal.state.PaywallState -import com.superwall.sdk.paywall.presentation.internal.userIsSubscribed import com.superwall.sdk.paywall.presentation.rule_logic.RuleEvaluationOutcome import com.superwall.sdk.paywall.request.PaywallRequest import com.superwall.sdk.paywall.request.ResponseIdentifiers import com.superwall.sdk.paywall.view.PaywallView import com.superwall.sdk.paywall.view.webview.webViewExists import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.first internal suspend fun Superwall.getPaywallView( request: PresentationRequest, @@ -44,13 +39,6 @@ internal suspend fun Superwall.getPaywallView( experiment = experiment, ) - var requestRetryCount = 6 - - val subscriptionStatus = request.flags.subscriptionStatus.first() - if (subscriptionStatus == SubscriptionStatus.ACTIVE) { - requestRetryCount = 0 - } - val paywallRequest = dependencyContainer.makePaywallRequest( eventData = request.presentationInfo.eventData, @@ -62,7 +50,6 @@ internal suspend fun Superwall.getPaywallView( ), isDebuggerLaunched = request.flags.isDebuggerLaunched, presentationSourceType = request.presentationSourceType, - retryCount = requestRetryCount, ) return try { val isForPresentation = @@ -96,35 +83,15 @@ internal suspend fun Superwall.getPaywallView( Result.failure(PaywallPresentationRequestStatusReason.NoPaywallView()) } } catch (e: Throwable) { - if (subscriptionStatus == SubscriptionStatus.ACTIVE) { - Result.failure(userIsSubscribed(paywallStatePublisher)) - } else { - Result.failure(presentationFailure(e, request, debugInfo, paywallStatePublisher)) - } + Result.failure(presentationFailure(e, debugInfo, paywallStatePublisher)) } } private suspend fun presentationFailure( error: Throwable, - request: PresentationRequest, debugInfo: Map, paywallStatePublisher: MutableSharedFlow?, ): Throwable { - val subscriptionStatus = request.flags.subscriptionStatus.first() - if (InternalPresentationLogic.userSubscribedAndNotOverridden( - isUserSubscribed = subscriptionStatus == SubscriptionStatus.ACTIVE, - overrides = - InternalPresentationLogic.UserSubscriptionOverrides( - isDebuggerLaunched = request.flags.isDebuggerLaunched, - shouldIgnoreSubscriptionStatus = request.paywallOverrides?.ignoreSubscriptionStatus, - presentationCondition = null, - ), - ) - ) { - paywallStatePublisher?.emit(PaywallState.Skipped(PaywallSkippedReason.UserIsSubscribed())) - return PaywallPresentationRequestStatusReason.UserIsSubscribed() - } - Logger.debug( logLevel = LogLevel.error, scope = LogScope.paywallPresentation, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt index fd926dbf..82a8bc50 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt @@ -4,30 +4,19 @@ import android.app.Activity import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger -import com.superwall.sdk.models.assignment.ConfirmableAssignment import com.superwall.sdk.models.triggers.InternalTriggerResult import com.superwall.sdk.paywall.presentation.internal.InternalPresentationLogic import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo -import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.paywall.presentation.rule_logic.RuleEvaluationOutcome import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.first - -data class PresentablePipelineOutput( - val debugInfo: Map, - val paywallView: PaywallView, - val presenter: Activity, - val confirmableAssignment: ConfirmableAssignment?, -) suspend fun Superwall.getPresenterIfNecessary( paywallView: PaywallView, @@ -35,21 +24,6 @@ suspend fun Superwall.getPresenterIfNecessary( request: PresentationRequest, paywallStatePublisher: MutableSharedFlow? = null, ): Activity? { - val subscriptionStatus = request.flags.subscriptionStatus.first() - if (InternalPresentationLogic.userSubscribedAndNotOverridden( - isUserSubscribed = subscriptionStatus == SubscriptionStatus.ACTIVE, - overrides = - InternalPresentationLogic.UserSubscriptionOverrides( - isDebuggerLaunched = request.flags.isDebuggerLaunched, - shouldIgnoreSubscriptionStatus = request.paywallOverrides?.ignoreSubscriptionStatus, - presentationCondition = paywallView.paywall.presentation.condition, - ), - ) - ) { - paywallStatePublisher?.emit(PaywallState.Skipped(PaywallSkippedReason.UserIsSubscribed())) - throw PaywallPresentationRequestStatusReason.UserIsSubscribed() - } - when (request.flags.type) { is PresentationRequestType.GetPaywall -> { val sessionId = @@ -64,6 +38,7 @@ suspend fun Superwall.getPresenterIfNecessary( is PresentationRequestType.GetPresentationResult, is PresentationRequestType.ConfirmAllAssignments, -> return null + is PresentationRequestType.Presentation -> Unit else -> Unit } @@ -108,9 +83,11 @@ suspend fun Superwall.attemptTriggerFire( when (triggerResult) { is InternalTriggerResult.Error, is InternalTriggerResult.EventNotFound -> return + else -> {} // No-op } } + is PresentationInfo.FromIdentifier -> {} // No-op } val trackedEvent = diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt index 3e5e8fa6..12366d42 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt @@ -4,18 +4,16 @@ import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent import com.superwall.sdk.config.models.ConfigState -import com.superwall.sdk.config.models.getConfig -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.dependencies.DependencyContainer import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.paywall.presentation.internal.InternalPresentationLogic import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatus import com.superwall.sdk.paywall.presentation.internal.PaywallPresentationRequestStatusReason import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.state.PaywallState -import com.superwall.sdk.paywall.presentation.internal.userIsSubscribed import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.StateFlow @@ -25,7 +23,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlin.time.Duration.Companion.seconds -internal suspend fun Superwall.waitForSubsStatusAndConfig( +internal suspend fun Superwall.waitForEntitlementsAndConfig( request: PresentationRequest, paywallStatePublisher: MutableSharedFlow? = null, dependencyContainer: DependencyContainer? = null, @@ -35,8 +33,8 @@ internal suspend fun Superwall.waitForSubsStatusAndConfig( try { withTimeout(5.seconds) { - request.flags.subscriptionStatus - .filter { it != SubscriptionStatus.UNKNOWN } + request.flags.entitlements + .filter { it !is EntitlementStatus.Unkown } .first() } } catch (e: TimeoutCancellationException) { @@ -47,7 +45,7 @@ internal suspend fun Superwall.waitForSubsStatusAndConfig( eventData = request.presentationInfo.eventData, type = request.flags.type, status = PaywallPresentationRequestStatus.Timeout, - statusReason = PaywallPresentationRequestStatusReason.SubscriptionStatusTimeout(), + statusReason = PaywallPresentationRequestStatusReason.EntitlementStatusTimeout(), factory = dependencyContainer, ) track(trackedEvent) @@ -56,7 +54,7 @@ internal suspend fun Superwall.waitForSubsStatusAndConfig( logLevel = LogLevel.info, scope = LogScope.paywallPresentation, message = - "Timeout: Superwall.instance.subscriptionStatus has been \"unknown\" for " + + "Timeout: Superwall.instance.entitlementStatus has been \"unknown\" for " + "over 5 seconds resulting in a failure.", ) val error = @@ -64,10 +62,10 @@ internal suspend fun Superwall.waitForSubsStatusAndConfig( domain = "SWKPresentationError", code = 105, title = "Timeout", - value = "The subscription status failed to change from \"unknown\".", + value = "The entitlement status failed to change from \"unknown\".", ) paywallStatePublisher?.emit(PaywallState.PresentationError(error)) - throw PaywallPresentationRequestStatusReason.SubscriptionStatusTimeout() + throw PaywallPresentationRequestStatusReason.EntitlementStatusTimeout() } val configState = dependencyContainer.configManager.configState @@ -79,53 +77,53 @@ internal suspend fun Superwall.waitForSubsStatusAndConfig( } } - val subscriptionIsActive = subscriptionStatus.value == SubscriptionStatus.ACTIVE when { // Config is still retrieving, wait for <=1 second. // At 1s we cancel the task and check config again. - subscriptionIsActive && - configState.value is ConfigState.Retrieving -> { + configState.value is ConfigState.Retrieving -> { try { withTimeout(1.seconds) { configState .configOrThrow() } } catch (e: TimeoutCancellationException) { - ioScope.launch { - val trackedEvent = - InternalSuperwallEvent.PresentationRequest( - eventData = request.presentationInfo.eventData, - type = request.flags.type, - status = PaywallPresentationRequestStatus.Timeout, - statusReason = PaywallPresentationRequestStatusReason.NoConfig(), - factory = dependencyContainer, + try { + // Check config again just in case + configState.configOrThrow() + } catch (e: Exception) { + ioScope.launch { + val trackedEvent = + InternalSuperwallEvent.PresentationRequest( + eventData = request.presentationInfo.eventData, + type = request.flags.type, + status = PaywallPresentationRequestStatus.Timeout, + statusReason = PaywallPresentationRequestStatusReason.NoConfig(), + factory = dependencyContainer, + ) + track(trackedEvent) + } + Logger.debug( + logLevel = LogLevel.info, + scope = LogScope.paywallPresentation, + message = "Timeout: The config could not be retrieved in a reasonable time for a subscribed user.", + ) + val state = + PaywallState.PresentationError( + InternalPresentationLogic.presentationError( + domain = "SWKPresentationError", + code = 104, + title = "No Config", + value = "Trying to present paywall without the superwall config.", + ), ) - track(trackedEvent) + paywallStatePublisher?.emit(state) + throw PaywallPresentationRequestStatusReason.NoConfig() } - Logger.debug( - logLevel = LogLevel.info, - scope = LogScope.paywallPresentation, - message = "Timeout: The config could not be retrieved in a reasonable time for a subscribed user.", - ) - throw userIsSubscribed(paywallStatePublisher) } } - // If the user is subscribed and there's no config (for whatever reason), - // just call the feature block. - subscriptionIsActive && - configState.value.getConfig() == null && - configState.value !is ConfigState.Retrieving -> { - throw userIsSubscribed(paywallStatePublisher) - } - - subscriptionIsActive && - configState.value.getConfig() != null -> { - // If the user is subscribed and there is config, continue. - } - // User is not subscribed, so we either wait for config and show the paywall - // Or we show the paywall without config. else -> { + // Try to get the config and continue or throw an error try { configState.configOrThrow() } catch (e: Throwable) { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt index 1b0f8836..c6cd24d2 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/request/PresentationRequest.kt @@ -1,7 +1,7 @@ package com.superwall.sdk.paywall.presentation.internal import android.app.Activity -import com.superwall.sdk.delegate.SubscriptionStatus +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter @@ -55,7 +55,7 @@ data class PresentationRequest( ) { data class Flags( var isDebuggerLaunched: Boolean, - var subscriptionStatus: StateFlow, + var entitlements: StateFlow, var isPaywallPresented: Boolean, var type: PresentationRequestType, ) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt index f22cbf98..46d1417a 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt @@ -309,7 +309,9 @@ class PaywallView( Superwall.instance.dependencyContainer.delegateAdapter .willPresentPaywall(info) - webView.setRendererPriorityPolicy(RENDERER_PRIORITY_IMPORTANT, true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + webView.setRendererPriorityPolicy(RENDERER_PRIORITY_IMPORTANT, true) + } webView.scrollTo(0, 0) if (loadingState is PaywallLoadingState.Ready) { webView.messageHandler.handle(PaywallMessage.TemplateParamsAndUserAttributes) @@ -721,12 +723,15 @@ class PaywallView( } webView.onRenderProcessCrashed = { + val isOverO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O Logger.debug( logLevel = LogLevel.error, scope = LogScope.paywallView, message = "Webview Process has crashed for paywall with identifier: ${paywall.identifier}.\n" + - "Crashed by the system: ${it.didCrash()} - priority ${it.rendererPriorityAtExit()}", + "Crashed by the system: ${ + if (isOverO)it.didCrash() else "Unknown"} - priority ${ + if (isOverO)it.rendererPriorityAtExit() else "Unknown"}", ) recreateWebview() } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt index e1dd8722..9e3f8301 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt @@ -118,7 +118,7 @@ class SWWebView( mainScope = mainScope, ioScope = ioScope, loadUrl = { - loadUrl(it.url) + super.loadUrl(transformUri(it.url)) }, stopLoading = { stopLoading() @@ -134,7 +134,9 @@ class SWWebView( } fun enableOffscreenRender() { - settings.offscreenPreRaster = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + settings.offscreenPreRaster = true + } } // ??? @@ -171,7 +173,12 @@ class SWWebView( lastWebViewClient = client } - listenToWebviewClientEvents(this.webViewClient as DefaultWebviewClient) + listenToWebviewClientEvents(client) + + super.loadUrl(transformUri(url)) + } + + private fun transformUri(url: String): String { // Parse the url and add the query parameter val uri = Uri.parse(url) @@ -190,7 +197,7 @@ class SWWebView( LogScope.paywallView, "SWWebView.loadUrl: $urlString", ) - super.loadUrl(urlString) + return urlString } private fun listenToWebviewClientEvents(client: DefaultWebviewClient) { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt index d593cc26..688d79f4 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt @@ -44,7 +44,7 @@ data class DeviceTemplate( val utcDateTime: String, val localDateTime: String, val isSandbox: String, - val subscriptionStatus: String, + val activeEntitlements: List, val isFirstAppOpen: Boolean, val sdkVersion: String, val sdkVersionPadded: String, diff --git a/superwall/src/main/java/com/superwall/sdk/storage/Cache.kt b/superwall/src/main/java/com/superwall/sdk/storage/Cache.kt index 0d405c4a..1ff8f225 100644 --- a/superwall/src/main/java/com/superwall/sdk/storage/Cache.kt +++ b/superwall/src/main/java/com/superwall/sdk/storage/Cache.kt @@ -34,7 +34,6 @@ class Cache( fun read(storable: Storable): T? { var data = memCache[storable.key] as? T - if (data == null) { runBlocking(ioQueue) { val file = storable.file(context = context) diff --git a/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt b/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt index a0d1b92b..8c444496 100644 --- a/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt +++ b/superwall/src/main/java/com/superwall/sdk/storage/CacheKeys.kt @@ -1,8 +1,9 @@ package com.superwall.sdk.storage import android.content.Context -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.models.config.Config +import com.superwall.sdk.models.entitlements.Entitlement +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.models.geo.GeoInfo import com.superwall.sdk.models.serialization.AnySerializer import com.superwall.sdk.models.transactions.SavedTransaction @@ -43,8 +44,17 @@ enum class SearchPathDirectory { fun fileDirectory(context: Context): File = when (this) { CACHE -> context.cacheDir - USER_SPECIFIC_DOCUMENTS -> context.getDir("user_specific_document_dir", Context.MODE_PRIVATE) - APP_SPECIFIC_DOCUMENTS -> context.getDir("app_specific_document_dir", Context.MODE_PRIVATE) + USER_SPECIFIC_DOCUMENTS -> + context.getDir( + "user_specific_document_dir", + Context.MODE_PRIVATE, + ) + + APP_SPECIFIC_DOCUMENTS -> + context.getDir( + "app_specific_document_dir", + Context.MODE_PRIVATE, + ) } } @@ -211,15 +221,26 @@ object SdkVersion : Storable { get() = String.serializer() } -object ActiveSubscriptionStatus : Storable { +object StoredEntitlementStatus : Storable { override val key: String - get() = "store.subscriptionStatus" + get() = "store.entitlementStatus" override val directory: SearchPathDirectory get() = SearchPathDirectory.APP_SPECIFIC_DOCUMENTS - override val serializer: KSerializer - get() = SubscriptionStatus.serializer() + override val serializer: KSerializer + get() = EntitlementStatus.serializer() +} + +object StoredEntitlementsByProductId : Storable>> { + override val key: String + get() = "store.entitlementByProductId" + + override val directory: SearchPathDirectory + get() = SearchPathDirectory.APP_SPECIFIC_DOCUMENTS + + override val serializer: KSerializer>> + get() = MapSerializer(String.serializer(), SetSerializer(Entitlement.serializer())) } object SurveyAssignmentKey : Storable { @@ -299,7 +320,8 @@ object DateSerializer : KSerializer { timeZone = TimeZone.getTimeZone("UTC") } - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) override fun serialize( encoder: Encoder, @@ -311,7 +333,8 @@ object DateSerializer : KSerializer { override fun deserialize(decoder: Decoder): Date { val dateString = decoder.decodeString() - return format.parse(dateString) ?: throw SerializationException("Invalid date format: $dateString") + return format.parse(dateString) + ?: throw SerializationException("Invalid date format: $dateString") } } diff --git a/superwall/src/main/java/com/superwall/sdk/store/ExternalNativePurchaseController.kt b/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt similarity index 92% rename from superwall/src/main/java/com/superwall/sdk/store/ExternalNativePurchaseController.kt rename to superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt index 07250e27..d88546a7 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/ExternalNativePurchaseController.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt @@ -8,12 +8,12 @@ import com.superwall.sdk.billing.RECONNECT_TIMER_MAX_TIME_MILLISECONDS import com.superwall.sdk.billing.RECONNECT_TIMER_START_MILLISECONDS 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.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger import com.superwall.sdk.misc.IOScope +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.store.abstractions.product.OfferType import com.superwall.sdk.store.abstractions.product.RawStoreProduct import kotlinx.coroutines.CompletableDeferred @@ -25,9 +25,10 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlin.math.min -class ExternalNativePurchaseController( +class AutomaticPurchaseController( var context: Context, val scope: IOScope, + val entitlementsInfo: Entitlements, ) : PurchaseController, PurchasesUpdatedListener { private var billingClient: BillingClient = @@ -271,8 +272,23 @@ class ExternalNativePurchaseController( val hasActivePurchaseOrSubscription = allPurchases.any { it.purchaseState == Purchase.PurchaseState.PURCHASED } - val status: SubscriptionStatus = - if (hasActivePurchaseOrSubscription) SubscriptionStatus.ACTIVE else SubscriptionStatus.INACTIVE + val status: EntitlementStatus = + if (hasActivePurchaseOrSubscription) { + subscriptionPurchases + .flatMap { it.products } + .toSet() + .flatMap { entitlementsInfo.byProductId(it) } + .toSet() + .let { entitlements -> + if (entitlements.isNotEmpty()) { + EntitlementStatus.Active(entitlements) + } else { + EntitlementStatus.NoActiveEntitlements + } + } + } else { + EntitlementStatus.NoActiveEntitlements + } if (!Superwall.initialized) { Logger.debug( @@ -283,7 +299,7 @@ class ExternalNativePurchaseController( return } - Superwall.instance.setSubscriptionStatus(status) + Superwall.instance.setEntitlementStatus(status) } private suspend fun queryPurchasesOfType(productType: String): List { diff --git a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt new file mode 100644 index 00000000..1d624dac --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt @@ -0,0 +1,96 @@ +package com.superwall.sdk.store + +import com.superwall.sdk.models.entitlements.Entitlement +import com.superwall.sdk.models.entitlements.EntitlementStatus +import com.superwall.sdk.storage.Storage +import com.superwall.sdk.storage.StoredEntitlementStatus +import com.superwall.sdk.storage.StoredEntitlementsByProductId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap + +class Entitlements( + private val storage: Storage, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default), +) { + private val _entitlementsByProduct = ConcurrentHashMap>() + + val _status: MutableStateFlow = + MutableStateFlow(EntitlementStatus.Unkown) + + val status: StateFlow + get() = _status.asStateFlow() + + // Mutable backing fields for entitlements + private val _all = mutableSetOf() + private val _active = mutableSetOf() + private val _inactive = mutableSetOf() + + // Exposed properties for entitlements + val all: Set + get() = _all.toSet() + val active: Set + get() = _active.toSet() + val inactive: Set + get() = _inactive.toSet() + + init { + storage.read(StoredEntitlementStatus)?.let { + setEntitlementStatus(it) + } + storage.read(StoredEntitlementsByProductId)?.let { + _entitlementsByProduct.putAll(it) + } + + scope.launch { + status.collect { + storage.write(StoredEntitlementStatus, it) + } + } + } + + fun setEntitlementStatus(value: EntitlementStatus) { + when (value) { + is EntitlementStatus.Active -> { + if (value.entitlements.isEmpty()) { + setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + } else { + _all.addAll(value.entitlements) + _active.addAll(value.entitlements) + _inactive.removeAll(value.entitlements) + _status.value = value + } + } + + is EntitlementStatus.NoActiveEntitlements -> { + _active.clear() + _inactive.clear() + _status.value = value + } + + is EntitlementStatus.Unkown -> { + _all.clear() + _active.clear() + _inactive.clear() + _status.value = value + } + } + } + + internal fun byProductId(id: String): Set = _entitlementsByProduct[id] ?: emptySet() + + internal fun addEntitlementsByProductId(idToEntitlements: Map>) { + _entitlementsByProduct.putAll( + idToEntitlements.mapValues { (_, entitlements) -> + entitlements.map { Entitlement(it) }.toSet() + }, + ) + _all.clear() + _all.addAll(_entitlementsByProduct.values.flatten()) + storage.write(StoredEntitlementsByProductId, _entitlementsByProduct) + } +} diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt index 53c274aa..56ace8d4 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt @@ -3,7 +3,6 @@ package com.superwall.sdk.store import com.superwall.sdk.delegate.RestorationResult import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.models.paywall.PaywallProducts -import com.superwall.sdk.models.product.Product import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.product.ProductVariable import com.superwall.sdk.paywall.view.PaywallView @@ -23,7 +22,7 @@ interface StoreKitManagerInterface { suspend fun getProducts( responseProductIds: List, paywallName: String? = null, - responseProducts: List = listOf(), + responseProducts: List = listOf(), substituteProducts: PaywallProducts? = null, ): GetProductsResponse diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt index 92ad1c7f..07b8c9a8 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt @@ -137,6 +137,7 @@ class StoreManager( productItems[index] = ProductItem( name = productItems[index].name, + entitlements = productItems[index].entitlements, type = ProductItem.StoreProductType.PlayStore( PlayStoreProduct( @@ -157,6 +158,7 @@ class StoreManager( productItems.add( ProductItem( name = name, + entitlements = emptySet(), type = ProductItem.StoreProductType.PlayStore( PlayStoreProduct( diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index ef2809d1..f35ca9aa 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -9,7 +9,6 @@ import com.superwall.sdk.analytics.superwall.SuperwallEvents import com.superwall.sdk.delegate.InternalPurchaseResult 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.dependencies.CacheFactory import com.superwall.sdk.dependencies.DeviceHelperFactory @@ -24,6 +23,7 @@ import com.superwall.sdk.logger.Logger import com.superwall.sdk.misc.ActivityProvider import com.superwall.sdk.misc.IOScope import com.superwall.sdk.misc.launchWithTracking +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.models.paywall.LocalNotificationType import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult @@ -52,8 +52,8 @@ class TransactionManager( Superwall.instance.track(it) }, private val dismiss: suspend (paywallView: PaywallView, result: PaywallResult) -> Unit, - private val subscriptionStatus: () -> SubscriptionStatus = { - Superwall.instance.subscriptionStatus.value + private val entitlementStatus: () -> EntitlementStatus = { + Superwall.instance.entitlementStatus.value }, ) { sealed class PurchaseSource { @@ -616,10 +616,10 @@ class TransactionManager( val restorationResult = purchaseController.restorePurchases() val hasRestored = restorationResult is RestorationResult.Restored - val isUserSubscribed = - subscriptionStatus() == SubscriptionStatus.ACTIVE + val hasEntitlements = + entitlementStatus() is EntitlementStatus.Active - if (hasRestored && isUserSubscribed) { + if (hasRestored && hasEntitlements) { log(message = "Transactions Restored") track( InternalSuperwallEvent.Restore( @@ -632,7 +632,7 @@ class TransactionManager( } } else { val msg = "Transactions Failed to Restore.${ - if (hasRestored && !isUserSubscribed) { + if (hasRestored && !hasEntitlements) { " The user's subscription status is \"inactive\", but the restoration result is \"restored\"." + " Ensure the subscription status is active before confirming successful restoration." } else { diff --git a/superwall/src/test/java/com/superwall/sdk/models/paywall/PaywallProductTest.kt b/superwall/src/test/java/com/superwall/sdk/models/paywall/PaywallProductTest.kt index f6bfbe5e..ee701ba6 100644 --- a/superwall/src/test/java/com/superwall/sdk/models/paywall/PaywallProductTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/models/paywall/PaywallProductTest.kt @@ -1,7 +1,6 @@ package com.superwall.sdk.models.paywall -import com.superwall.sdk.models.product.Product -import com.superwall.sdk.models.product.ProductType +import com.superwall.sdk.models.product.ProductItem import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy import org.junit.Test @@ -11,7 +10,18 @@ class PaywallProductTest { fun `test parsing of config`() { val productString = """ - {"product": "primary", "product_id": "abc-def:ghi:jkl", "product_id_android": "abc-def:ghi:jkl"} + { + "reference_name": "primary", + "store_product": { + "store": "PLAY_STORE", + "product_identifier": "abc-def", + "base_plan_identifier": "ghi", + "offer": { + "type": "SPECIFIED", + "offer_identifier": "jkl" + } + } + } """.trimIndent() val json = @@ -19,9 +29,10 @@ class PaywallProductTest { ignoreUnknownKeys = true namingStrategy = JsonNamingStrategy.SnakeCase } - val product = json.decodeFromString(productString) + val product = json.decodeFromString(productString) assert(product != null) - assert(product.id == "abc-def:ghi:jkl") - assert(product.type == ProductType.PRIMARY) + assert(product.fullProductId == "abc-def:ghi:jkl") + assert(product.name == "primary") + assert(product.entitlements.isEmpty()) } } diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt index 848a58c3..a1c2dc50 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt @@ -51,7 +51,7 @@ class DeviceTemplateTest { utcDateTime = "2024-03-20T10:00:00", localDateTime = "2024-03-20T02:00:00", isSandbox = "true", - subscriptionStatus = "active", + activeEntitlements = listOf("active"), isFirstAppOpen = false, sdkVersion = "1.0.0", sdkVersionPadded = "001.000.000", @@ -128,7 +128,7 @@ class DeviceTemplateTest { utcDateTime = "2024-03-20T10:00:00", localDateTime = "2024-03-20T02:00:00", isSandbox = "true", - subscriptionStatus = "none", + activeEntitlements = listOf(), isFirstAppOpen = true, sdkVersion = "1.0.0", sdkVersionPadded = "001.000.000", diff --git a/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt b/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt new file mode 100644 index 00000000..9186a020 --- /dev/null +++ b/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt @@ -0,0 +1,185 @@ +package com.superwall.sdk.store + +import com.superwall.sdk.And +import com.superwall.sdk.Given +import com.superwall.sdk.Then +import com.superwall.sdk.When +import com.superwall.sdk.models.entitlements.Entitlement +import com.superwall.sdk.models.entitlements.EntitlementStatus +import com.superwall.sdk.storage.Storage +import com.superwall.sdk.storage.StoredEntitlementStatus +import com.superwall.sdk.storage.StoredEntitlementsByProductId +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class EntitlementsTest { + private val storage: Storage = mockk(relaxUnitFun = true) + + private lateinit var entitlements: Entitlements + + @Test + fun `test initialization with stored entitlement status`() = + runTest { + Given("storage contains entitlement status") { + val storedEntitlements = setOf(Entitlement("test_entitlement")) + val storedStatus = EntitlementStatus.Active(storedEntitlements) + every { storage.read(StoredEntitlementStatus) } returns storedStatus + every { storage.read(StoredEntitlementsByProductId) } returns + mapOf( + "test" to + setOf( + Entitlement("test_entitlement"), + ), + ) + entitlements = Entitlements(storage) + + When("Entitlements is initialized") { + val entitlements = Entitlements(storage) + + Then("it should load the stored status") { + assertEquals(storedStatus, entitlements.status.value) + assertEquals(storedEntitlements, entitlements.active) + assertEquals(storedEntitlements, entitlements.all) + assertTrue(entitlements.inactive.isEmpty()) + } + } + } + } + + @Test + fun `test setEntitlementStatus with active entitlements`() = + runTest { + Given("an Entitlements instance") { + val activeEntitlements = + setOf( + Entitlement("entitlement1"), + Entitlement("entitlement2"), + ) + every { storage.read(StoredEntitlementStatus) } returns null + every { storage.read(StoredEntitlementsByProductId) } returns null + entitlements = Entitlements(storage) + + When("setting active entitlement status") { + entitlements.setEntitlementStatus(EntitlementStatus.Active(activeEntitlements)) + + Then("it should update all collections correctly") { + assertEquals(activeEntitlements, entitlements.active) + assertEquals(activeEntitlements, entitlements.all) + assertTrue(entitlements.inactive.isEmpty()) + assertTrue(entitlements.status.value is EntitlementStatus.Active) + } + + And("it should store the status") { + verify { + storage.write( + StoredEntitlementStatus, + EntitlementStatus.Active(activeEntitlements), + ) + } + } + } + } + } + + @Test + fun `test setEntitlementStatus with empty active entitlements`() = + runTest { + Given("an Entitlements instance") { + every { storage.read(StoredEntitlementStatus) } returns null + every { storage.read(StoredEntitlementsByProductId) } returns null + entitlements = Entitlements(storage) + + When("setting active entitlement status with empty set") { + entitlements.setEntitlementStatus(EntitlementStatus.Active(emptySet())) + + Then("it should convert to NoActiveEntitlements status") { + assertTrue(entitlements.status.value is EntitlementStatus.NoActiveEntitlements) + assertTrue(entitlements.active.isEmpty()) + assertTrue(entitlements.inactive.isEmpty()) + } + } + } + } + + @Test + fun `test setEntitlementStatus with no active entitlements`() = + runTest { + Given("an Entitlements instance") { + every { storage.read(StoredEntitlementStatus) } returns null + every { storage.read(StoredEntitlementsByProductId) } returns null + entitlements = Entitlements(storage) + When("setting NoActiveEntitlements status") { + entitlements.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + + Then("it should clear all collections") { + assertTrue(entitlements.active.isEmpty()) + assertTrue(entitlements.inactive.isEmpty()) + assertTrue(entitlements.status.value is EntitlementStatus.NoActiveEntitlements) + } + } + } + } + + @Test + fun `test setEntitlementStatus with unknown status`() = + runTest { + Given("an Entitlements instance") { + every { storage.read(StoredEntitlementStatus) } returns null + every { storage.read(StoredEntitlementsByProductId) } returns null + entitlements = Entitlements(storage) + When("setting Unknown status") { + entitlements.setEntitlementStatus(EntitlementStatus.Unkown) + + Then("it should clear all collections") { + assertTrue(entitlements.active.isEmpty()) + assertTrue(entitlements.inactive.isEmpty()) + assertTrue(entitlements.all.isEmpty()) + assertTrue(entitlements.status.value is EntitlementStatus.Unkown) + } + } + } + } + + @Test + fun `test byProductId functionality`() = + runTest { + Given("storage contains entitlements by product ID") { + val productEntitlements = + mapOf( + "product1" to setOf(Entitlement("entitlement1")), + "product2" to setOf(Entitlement("entitlement2")), + ) + every { storage.read(StoredEntitlementStatus) } returns + EntitlementStatus.Active( + setOf( + Entitlement("entitlement1"), + Entitlement("entitlement2"), + ), + ) + every { storage.read(StoredEntitlementsByProductId) } returns productEntitlements + entitlements = Entitlements(storage) + + When("creating a new Entitlements instance") { + Then("it should return correct entitlements for each product") { + assertEquals( + productEntitlements["product1"], + entitlements.byProductId("product1"), + ) + assertEquals( + productEntitlements["product2"], + entitlements.byProductId("product2"), + ) + } + + And("it should return empty set for unknown product") { + assertTrue(entitlements.byProductId("unknown_product").isEmpty()) + } + } + } + } +} diff --git a/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt index ff0a8304..26fbd8b7 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt @@ -7,6 +7,7 @@ import com.superwall.sdk.When import com.superwall.sdk.assertTrue import com.superwall.sdk.billing.Billing import com.superwall.sdk.billing.BillingError +import com.superwall.sdk.models.entitlements.Entitlement import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.models.product.Offer import com.superwall.sdk.models.product.PlayStoreProduct @@ -28,6 +29,7 @@ class StoreManagerTest { private lateinit var purchaseController: InternalPurchaseController private lateinit var billing: Billing private lateinit var storeManager: StoreManager + private val entitlementsBasic = setOf(Entitlement("entitlement1")) @Before fun setup() { @@ -60,6 +62,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), + entitlements = entitlementsBasic.map { it.id }.toSet(), ), ProductItem( "Item2", @@ -72,6 +75,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), + entitlements = entitlementsBasic.map { it.id }.toSet(), ), ), ) @@ -123,6 +127,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), + entitlements = entitlementsBasic.map { it.id }.toSet(), ), ProductItem( "Item2", @@ -135,6 +140,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), + entitlements = entitlementsBasic.map { it.id }.toSet(), ), ), ) @@ -192,6 +198,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), + entitlements = entitlementsBasic.map { it.id }.toSet(), ), ProductItem( "Item2", @@ -204,11 +211,15 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), + entitlements = entitlementsBasic.map { it.id }.toSet(), ), ), ) - coEvery { billing.awaitGetProducts(any()) } throws BillingError.BillingNotAvailable("Billing not available") + coEvery { billing.awaitGetProducts(any()) } throws + BillingError.BillingNotAvailable( + "Billing not available", + ) When("getProducts is called") { Then("it should throw a BillingNotAvailable error") { diff --git a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt index e829b87e..6ce2ee07 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -12,15 +12,14 @@ import com.superwall.sdk.billing.Billing import com.superwall.sdk.config.options.SuperwallOptions import com.superwall.sdk.delegate.PurchaseResult import com.superwall.sdk.delegate.RestorationResult -import com.superwall.sdk.delegate.SubscriptionStatus import com.superwall.sdk.misc.ActivityProvider import com.superwall.sdk.misc.IOScope +import com.superwall.sdk.models.entitlements.Entitlement +import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.models.product.Offer import com.superwall.sdk.models.product.PlayStoreProduct -import com.superwall.sdk.models.product.Product import com.superwall.sdk.models.product.ProductItem -import com.superwall.sdk.models.product.ProductType import com.superwall.sdk.paywall.presentation.PaywallInfo import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult import com.superwall.sdk.paywall.view.PaywallView @@ -59,6 +58,8 @@ class TransactionManagerTest { } } + val entitlements = setOf("test-entitlement").map { Entitlement(it) }.toSet() + private val mockProduct = RawStoreProduct( playProduct, @@ -67,14 +68,6 @@ class TransactionManagerTest { OfferType.Auto, ) - private val pwInfo = - PaywallInfo.empty().copy( - products = - listOf( - Product(ProductType.PRIMARY, "product1"), - ), - ) - private val mockItems = listOf( ProductItem( @@ -86,9 +79,16 @@ class TransactionManagerTest { offer = Offer.Automatic(), ), ), + entitlements = entitlements.map { it.id }.toSet(), ), ) + private val pwInfo = + PaywallInfo.empty().copy( + products = + mockItems, + ) + private val mockedPaywall: Paywall = mockk { every { getInfo(any()) } returns pwInfo @@ -119,7 +119,6 @@ class TransactionManagerTest { } private var eventsQueue = mockk(relaxUnitFun = true) - private var storage = mockk() private var transactionManagerFactory = mockk { every { makeHasExternalPurchaseController() } returns false @@ -127,14 +126,17 @@ class TransactionManagerTest { mockk { coEvery { getLatestTransaction(any()) } returns mockk() } - every { makeSuperwallOptions() } + every { makeHasExternalPurchaseController() } returns false + every { makeSuperwallOptions() } returns SuperwallOptions() } + private var storage = mockk(relaxUnitFun = true) + fun TestScope.manager( track: (TrackableSuperwallEvent) -> Unit = {}, dismiss: (paywallView: PaywallView, result: PaywallResult) -> Unit = { _, _ -> }, - subscriptionStatus: () -> SubscriptionStatus = { - SubscriptionStatus.ACTIVE + entitlementStatus: () -> EntitlementStatus = { + EntitlementStatus.Active(entitlements) }, options: SuperwallOptions.() -> Unit = {}, ): TransactionManager { @@ -146,7 +148,7 @@ class TransactionManagerTest { purchaseController = purchaseController, storeManager = storeManager, activityProvider = activityProvider, - subscriptionStatus = subscriptionStatus, + entitlementStatus = entitlementStatus, track = { track(it) }, dismiss = { i, e -> dismiss(i, e) }, eventsQueue = eventsQueue, @@ -630,7 +632,7 @@ class TransactionManagerTest { track = { e -> events.update { it + e } }, - subscriptionStatus = { SubscriptionStatus.ACTIVE }, + entitlementStatus = { EntitlementStatus.Active(entitlements) }, ) coEvery { purchaseController.restorePurchases() } returns RestorationResult.Restored() @@ -660,7 +662,7 @@ class TransactionManagerTest { track = { e -> events.update { it + e } }, - subscriptionStatus = { SubscriptionStatus.ACTIVE }, + entitlementStatus = { EntitlementStatus.Active(entitlements) }, ) coEvery { purchaseController.restorePurchases() } returns RestorationResult.Restored() @@ -690,7 +692,7 @@ class TransactionManagerTest { track = { e -> events.update { it + e } }, - subscriptionStatus = { SubscriptionStatus.ACTIVE }, + entitlementStatus = { EntitlementStatus.NoActiveEntitlements }, ) coEvery { purchaseController.restorePurchases() } returns @@ -734,7 +736,7 @@ class TransactionManagerTest { track = { e -> events.update { it + e } }, - subscriptionStatus = { SubscriptionStatus.INACTIVE }, + entitlementStatus = { EntitlementStatus.NoActiveEntitlements }, ) coEvery { purchaseController.restorePurchases() } returns RestorationResult.Restored() From 84363d06affff9fcd0937b0548ff4d8f9c7bd151 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Fri, 29 Nov 2024 15:52:18 +0100 Subject: [PATCH 21/37] Improve tests, fix issues with not observing when PC is not defined --- .../com/superwall/superapp/MainApplication.kt | 21 +- .../superapp/test/PurchaseMockBuilder.java | 94 +++++++ .../superwall/superapp/test/UITestHandler.kt | 10 +- .../com/superwall/sdk/ObserverModeTest.kt | 241 ++++++++++++++++++ .../sdk/utilities/PurchaseMockBuilder.kt | 91 +++++++ .../main/java/com/superwall/sdk/Superwall.kt | 72 +++++- .../java/com/superwall/sdk/billing/Billing.kt | 4 + .../sdk/billing/GoogleBillingWrapper.kt | 8 +- .../sdk/dependencies/DependencyContainer.kt | 2 + .../sdk/dependencies/FactoryProtocols.kt | 4 + .../superwall/sdk/models/paywall/Paywall.kt | 4 +- .../paywall/request/PaywallRequestManager.kt | 4 +- .../sdk/store/InternalPurchaseController.kt | 3 + .../java/com/superwall/sdk/store/StoreKit.kt | 5 + .../sdk/store/StoreKitManagerInterface.kt | 2 +- .../com/superwall/sdk/store/StoreManager.kt | 31 ++- .../store/transactions/TransactionManager.kt | 53 +++- 17 files changed, 629 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.java create mode 100644 superwall/src/androidTest/java/com/superwall/sdk/ObserverModeTest.kt create mode 100644 superwall/src/androidTest/java/com/superwall/sdk/utilities/PurchaseMockBuilder.kt diff --git a/app/src/main/java/com/superwall/superapp/MainApplication.kt b/app/src/main/java/com/superwall/superapp/MainApplication.kt index f5e431b6..9cdfdf4c 100644 --- a/app/src/main/java/com/superwall/superapp/MainApplication.kt +++ b/app/src/main/java/com/superwall/superapp/MainApplication.kt @@ -62,7 +62,7 @@ class MainApplication : .build(), ) - configureWithAutomaticInitialization() + configureWithObserverMode() // configureWithRevenueCatInitialization() } @@ -86,6 +86,25 @@ class MainApplication : // Superwall.instance.options.isGameControllerEnabled = true } + fun configureWithObserverMode() { + Superwall.configure( + this, + CONSTANT_API_KEY, + options = + SuperwallOptions().apply { + shouldObservePurchases = true + paywalls = + PaywallOptions().apply { + shouldPreload = false + } + }, + ) + Superwall.instance.delegate = this + + // 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/test/PurchaseMockBuilder.java b/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.java new file mode 100644 index 00000000..9b5c794a --- /dev/null +++ b/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.java @@ -0,0 +1,94 @@ +package com.superwall.superapp.test; + +import com.android.billingclient.api.Purchase; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class PurchaseMockBuilder { + private JSONObject purchaseJson; + + public PurchaseMockBuilder() { + purchaseJson = new JSONObject(); + } + + public static Purchase createDefaultPurchase() throws JSONException { + return new PurchaseMockBuilder() + .setPurchaseState(Purchase.PurchaseState.PURCHASED) + .setPurchaseTime(System.currentTimeMillis()) + .setOrderId("GPA.1234-5678-9012-34567") + .setProductId("premium_subscription") + .setQuantity(1) + .setPurchaseToken("opaque-token-up-to-1950-characters") + .setPackageName("com.example.app") + .setDeveloperPayload("") + .setAcknowledged(true) + .setAutoRenewing(true) + .build(); + } + + public PurchaseMockBuilder setPurchaseState(int state) throws JSONException { + purchaseJson.put("purchaseState", state == 2 ? 4 : state); + return this; + } + + public PurchaseMockBuilder setPurchaseTime(long time) throws JSONException { + purchaseJson.put("purchaseTime", time); + return this; + } + + public PurchaseMockBuilder setOrderId(String orderId) throws JSONException { + purchaseJson.put("orderId", orderId); + return this; + } + + public PurchaseMockBuilder setProductId(String productId) throws JSONException { + JSONArray productIds = new JSONArray(); + productIds.put(productId); + purchaseJson.put("productIds", productIds); + // For backward compatibility + purchaseJson.put("productId", productId); + return this; + } + + public PurchaseMockBuilder setQuantity(int quantity) throws JSONException { + purchaseJson.put("quantity", quantity); + return this; + } + + public PurchaseMockBuilder setPurchaseToken(String token) throws JSONException { + purchaseJson.put("token", token); + purchaseJson.put("purchaseToken", token); + return this; + } + + public PurchaseMockBuilder setPackageName(String packageName) throws JSONException { + purchaseJson.put("packageName", packageName); + return this; + } + + public PurchaseMockBuilder setDeveloperPayload(String payload) throws JSONException { + purchaseJson.put("developerPayload", payload); + return this; + } + + public PurchaseMockBuilder setAcknowledged(boolean acknowledged) throws JSONException { + purchaseJson.put("acknowledged", acknowledged); + return this; + } + + public PurchaseMockBuilder setAutoRenewing(boolean autoRenewing) throws JSONException { + purchaseJson.put("autoRenewing", autoRenewing); + return this; + } + + public PurchaseMockBuilder setAccountIdentifiers(String obfuscatedAccountId, String obfuscatedProfileId) throws JSONException { + purchaseJson.put("obfuscatedAccountId", obfuscatedAccountId); + purchaseJson.put("obfuscatedProfileId", obfuscatedProfileId); + return this; + } + + public Purchase build() throws JSONException { + return new Purchase(purchaseJson.toString(), "dummy-signature"); + } +} \ No newline at end of file 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 91e588cd..556a1e23 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -4,6 +4,9 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.Purchase import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.superwall.SuperwallEvent import com.superwall.sdk.analytics.superwall.SuperwallEvent.DeepLink @@ -19,6 +22,8 @@ import com.superwall.sdk.paywall.presentation.get_presentation_result.getPresent import com.superwall.sdk.paywall.presentation.register import com.superwall.sdk.paywall.presentation.result.PresentationResult import com.superwall.sdk.paywall.view.SuperwallPaywallActivity +import com.superwall.sdk.store.PurchasingObserverState +import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.view.fatalAssert import com.superwall.superapp.ComposeActivity import kotlinx.coroutines.CoroutineScope @@ -700,7 +705,10 @@ object UITestHandler { Superwall.instance.getPaywall(event = "present_data", delegate = delegate) // Present using the convenience `SuperwallPaywallActivity` activity and verify test case. - SuperwallPaywallActivity.startWithView(context = this, view = view.getOrThrow()) + SuperwallPaywallActivity.startWithView( + context = this, + view = view.getOrThrow(), + ) }, ) var test37Info = 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/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/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index ecee3e83..52b3737c 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.Context import android.net.Uri import androidx.work.WorkManager +import com.android.billingclient.api.ProductDetails import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent import com.superwall.sdk.analytics.superwall.SuperwallEventInfo @@ -671,7 +672,30 @@ class Superwall( } /** - *Initiates a purchase of a `StoreProduct`. + *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` @@ -684,15 +708,52 @@ class Superwall( * ``Superwall`` will handle this for you. */ - suspend fun purchase(product: RawStoreProduct): Result = + suspend fun purchase(product: StoreProduct): Result = withErrorTracking { dependencyContainer.transactionManager.purchase( TransactionManager.PurchaseSource.ExternalPurchase( - StoreProduct(product), + 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> = + withErrorTracking { + dependencyContainer.storeKitManager.getProductsWithoutPaywall(productIds.toList()) + }.toResult() + /** * Initiates a purchase of a `StoreProduct` with a callback. * @@ -724,6 +785,11 @@ class Superwall( } } + /** + * Observe purchases made without using Paywalls + * + * */ + fun observe(state: PurchasingObserverState) { ioScope.launchWithTracking { if (!options.shouldObservePurchases) { diff --git a/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt b/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt index 1acfac06..ecca8456 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt @@ -1,10 +1,14 @@ package com.superwall.sdk.billing +import com.superwall.sdk.delegate.InternalPurchaseResult import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction +import kotlinx.coroutines.flow.MutableStateFlow interface Billing { + val purchaseResults: MutableStateFlow + suspend fun awaitGetProducts(identifiers: Set): Set suspend fun getLatestTransaction(factory: StoreTransactionFactory): StoreTransaction? diff --git a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt index 68f69160..91c56092 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt @@ -74,7 +74,7 @@ class GoogleBillingWrapper( private var reconnectionAlreadyScheduled = false // Setup mutable state flow for purchase results - private val purchaseResults = MutableStateFlow(null) + override val purchaseResults = MutableStateFlow(null) internal val IN_APP_BILLING_LESS_THAN_3_ERROR_MESSAGE = "Google Play In-app Billing API version is less than 3" @@ -543,6 +543,12 @@ class GoogleBillingWrapper( } } + /** + * Get the latest transaction from the purchaseResults flow. + * + * @param factory StoreTransactionFactory to create the StoreTransaction + * @return StoreTransaction if the purchase was finished by Superwall, null otherwise + */ override suspend fun getLatestTransaction(factory: StoreTransactionFactory): StoreTransaction? { // Get the latest from purchaseResults purchaseResults.asStateFlow().filter { it != null }.first().let { purchaseResult -> diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index 765135fb..957c8c0b 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -533,6 +533,8 @@ class DependencyContainer( override fun makeHasExternalPurchaseController(): Boolean = storeManager.purchaseController.hasExternalPurchaseController + override fun makeHasInternalPurchaseController(): Boolean = storeKitManager.purchaseController.hasInternalPurchaseController + override suspend fun didUpdateAppSession(appSession: AppSession) { } diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt index cc586276..12a15382 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt @@ -126,6 +126,10 @@ interface HasExternalPurchaseControllerFactory { fun makeHasExternalPurchaseController(): Boolean } +interface HasInternalPurchaseControllerFactory { + fun makeHasInternalPurchaseController(): Boolean +} + interface ViewFactory { // NOTE: THIS MUST BE EXECUTED ON THE MAIN THREAD (no way to enforce in Kotlin) suspend fun makePaywallView( diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt index 602235da..06efac26 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt @@ -25,7 +25,9 @@ import java.util.* @JvmInline value class PaywallURL( val value: String, -) +) { + override fun toString(): String = value +} @Serializable data class Paywalls( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt index ca08e070..3aabcd71 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/request/PaywallRequestManager.kt @@ -233,7 +233,9 @@ class PaywallRequestManager( paywall = paywall, request = request, ) - paywall = result.paywall + if (result.paywall != null) { + paywall = result.paywall + } paywall.productItems = result.productItems val outcome = diff --git a/superwall/src/main/java/com/superwall/sdk/store/InternalPurchaseController.kt b/superwall/src/main/java/com/superwall/sdk/store/InternalPurchaseController.kt index bee9ee70..d2ed9055 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/InternalPurchaseController.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/InternalPurchaseController.kt @@ -18,6 +18,9 @@ class InternalPurchaseController( val hasExternalPurchaseController: Boolean get() = kotlinPurchaseController != null || javaPurchaseController != null + val hasInternalPurchaseController: Boolean + get() = hasExternalPurchaseController && kotlinPurchaseController is AutomaticPurchaseController + override suspend fun purchase( activity: Activity, productDetails: ProductDetails, diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt index 6080b51a..77811a33 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreKit.kt @@ -17,6 +17,11 @@ interface StoreKit { request: PaywallRequest? = null, ): GetProductsResponse + suspend fun getProductsWithoutPaywall( + productIds: List, + substituteProducts: Map? = null, + ): Map + suspend fun refreshReceipt() suspend fun loadPurchasedProducts() diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt index 56ace8d4..fd686c83 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreKitManagerInterface.kt @@ -11,7 +11,7 @@ import com.superwall.sdk.store.abstractions.product.StoreProduct data class GetProductsResponse( val productsByFullId: Map, val productItems: List, - val paywall: Paywall, + val paywall: Paywall? = null, ) interface StoreKitManagerInterface { diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt index 07b8c9a8..ad3c7432 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt @@ -23,7 +23,7 @@ import java.util.Date class StoreManager( val purchaseController: InternalPurchaseController, - private val billing: Billing, + val billing: Billing, private val track: suspend (InternalSuperwallEvent) -> Unit = { Superwall.instance.track(it) }, @@ -62,6 +62,35 @@ class StoreManager( return productAttributes } + override suspend fun getProductsWithoutPaywall( + productIds: List, + substituteProducts: Map?, + ): Map { + val processingResult = + removeAndStore( + substituteProductsByName = substituteProducts, + fullProductIds = productIds, + productItems = emptyList(), + ) + + val products: Set + try { + products = billing.awaitGetProducts(processingResult.fullProductIdsToLoad) + } catch (error: Throwable) { + throw error + } + + val productsById = processingResult.substituteProductsById.toMutableMap() + + for (product in products) { + val fullProductIdentifier = product.fullIdentifier + productsById[fullProductIdentifier] = product + this.productsByFullId[fullProductIdentifier] = product + } + + return products.map { it.fullIdentifier to it }.toMap() + } + override suspend fun getProducts( substituteProducts: Map?, paywall: Paywall, diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index f35ca9aa..833b0208 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -1,5 +1,6 @@ package com.superwall.sdk.store.transactions +import com.android.billingclient.api.Purchase import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent @@ -13,6 +14,7 @@ import com.superwall.sdk.delegate.subscription_controller.PurchaseController import com.superwall.sdk.dependencies.CacheFactory import com.superwall.sdk.dependencies.DeviceHelperFactory import com.superwall.sdk.dependencies.HasExternalPurchaseControllerFactory +import com.superwall.sdk.dependencies.HasInternalPurchaseControllerFactory import com.superwall.sdk.dependencies.OptionsFactory import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.dependencies.TransactionVerifierFactory @@ -78,14 +80,13 @@ class TransactionManager( StoreTransactionFactory, DeviceHelperFactory, CacheFactory, - HasExternalPurchaseControllerFactory - - private val shouldFinishTransactions = - !factory.makeHasExternalPurchaseController() && - !factory.makeSuperwallOptions().shouldObservePurchases + HasExternalPurchaseControllerFactory, + HasInternalPurchaseControllerFactory private var lastPaywallView: PaywallView? = null + private var transactionsInProgress: MutableSet = mutableSetOf() + internal suspend fun handle( result: InternalPurchaseResult, state: PurchasingObserverState, @@ -95,8 +96,13 @@ class TransactionManager( val state = state as PurchasingObserverState.PurchaseResult state.purchases?.forEach { purchase -> purchase.products.map { - storeManager.productsByFullId[it] ?.let { product -> - didPurchase(product, PurchaseSource.ObserverMode(product), product.hasFreeTrial) + storeManager.productsByFullId[it]?.let { product -> + didPurchase( + product, + PurchaseSource.ObserverMode(product), + product.hasFreeTrial, + purchase, + ) } } } @@ -110,6 +116,7 @@ class TransactionManager( purchaseSource = PurchaseSource.ObserverMode(product), ) } + is InternalPurchaseResult.Failed -> { val state = state as PurchasingObserverState.PurchaseError val product = StoreProduct(RawStoreProduct.from(state.product)) @@ -119,6 +126,7 @@ class TransactionManager( PurchaseSource.ObserverMode(product), ) } + InternalPurchaseResult.Pending -> { val result = state as PurchasingObserverState.PurchaseResult result.purchases?.forEach { purchase -> @@ -129,6 +137,7 @@ class TransactionManager( } } } + InternalPurchaseResult.Restored -> { val state = state as PurchasingObserverState.PurchaseResult state.purchases?.forEach { purchase -> @@ -166,9 +175,10 @@ class TransactionManager( is PurchaseSource.ExternalPurchase -> { purchaseSource.product } + is PurchaseSource.ObserverMode -> purchaseSource.product } - + transactionsInProgress.add(product.fullIdentifier) val rawStoreProduct = product.rawStoreProduct log( message = @@ -189,7 +199,10 @@ class TransactionManager( basePlanId = rawStoreProduct.basePlanId, ) - if (purchaseSource is PurchaseSource.ExternalPurchase && factory.makeHasExternalPurchaseController()) { + if (purchaseSource is PurchaseSource.ExternalPurchase && + factory.makeHasExternalPurchaseController() && + !factory.makeHasInternalPurchaseController() + ) { return result } @@ -198,6 +211,7 @@ class TransactionManager( when (result) { is PurchaseResult.Purchased -> { didPurchase(product, purchaseSource, isEligibleForTrial && product.hasFreeTrial) + transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Restored -> { @@ -205,6 +219,7 @@ class TransactionManager( product = product, purchaseSource = purchaseSource, ) + transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Failed -> { @@ -236,14 +251,17 @@ class TransactionManager( purchaseSource.paywallView.togglePaywallSpinner(isHidden = true) } } + transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Pending -> { handlePendingTransaction(purchaseSource) + transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Cancelled -> { trackCancelled(product, purchaseSource) + transactionsInProgress.remove(product.fullIdentifier) } } return result @@ -379,6 +397,7 @@ class TransactionManager( info = mapOf("paywall_vc" to source), ) } + transactionsInProgress.add(product.fullIdentifier) val paywallInfo = source.paywallView.info val trackedEvent = @@ -398,7 +417,12 @@ class TransactionManager( } is PurchaseSource.ExternalPurchase, is PurchaseSource.ObserverMode -> { - if (factory.makeHasExternalPurchaseController()) { + transactionsInProgress.add(product.fullIdentifier) + if (!storeManager.productsByFullId.contains(product.fullIdentifier)) { + storeManager.productsByFullId[product.fullIdentifier] = product + } + + if (factory.makeHasExternalPurchaseController() && !factory.makeHasInternalPurchaseController()) { return } // If an external purchase controller is being used, skip because this will @@ -434,6 +458,7 @@ class TransactionManager( product: StoreProduct, purchaseSource: PurchaseSource, didStartFreeTrial: Boolean, + purchase: Purchase? = null, ) { when (purchaseSource) { is PurchaseSource.Internal -> { @@ -477,6 +502,13 @@ class TransactionManager( factory = factory, ) + if (purchase != null) { + factory.makeStoreTransaction(purchase) + } else { + transactionVerifier.getLatestTransaction( + factory = factory, + ) + } storeManager.loadPurchasedProducts() trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) @@ -738,6 +770,7 @@ class TransactionManager( ) track(trackedEvent) } + is PurchaseSource.ObserverMode -> { // No-op } From ef27d75d9f673961e7218c5236e39c643bc6b9ff Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 2 Dec 2024 17:11:39 +0100 Subject: [PATCH 22/37] Fix tests for entitlements, fix CEL ignoring evaluation --- .../test/FlowScreenshotTestExecutor.kt | 4 +-- .../com/superwall/superapp/MainApplication.kt | 6 ++-- .../purchase/RevenueCatPurchaseController.kt | 4 +-- .../superwall/superapp/test/UITestHandler.kt | 5 +-- .../sdk/config/options/SuperwallOptions.kt | 2 +- .../sdk/models/entitlements/Entitlement.kt | 12 +++++-- .../sdk/models/product/ProductItem.kt | 13 +++++-- .../sdk/network/device/DeviceHelper.kt | 34 +++++++------------ .../sdk/paywall/presentation/PaywallInfo.kt | 2 ++ .../InternalGetPresentationResult.kt | 1 - .../PaywallPresentationRequestStatus.kt | 3 -- .../internal/PresentationErrors.kt | 12 ------- .../presentation/result/PresentationResult.kt | 12 +------ .../rule_logic/cel/SuperscriptEvaluator.kt | 9 ++--- .../CombinedExpressionEvaluator.kt | 24 +++---------- .../templating/models/DeviceTemplate.kt | 2 +- .../com/superwall/sdk/store/Entitlements.kt | 4 +-- 17 files changed, 55 insertions(+), 94 deletions(-) delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt 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 c33780d0..ba776d62 100644 --- a/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt +++ b/app/src/androidTest/java/com/example/superapp/test/FlowScreenshotTestExecutor.kt @@ -137,7 +137,7 @@ class FlowScreenshotTestExecutor { it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete } awaitUntilShimmerDisappears() awaitUntilWebviewAppears() - delayFor(100.milliseconds) + delayFor(300.milliseconds) mainScope .async { // We scroll a bit to display the button @@ -151,7 +151,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 { diff --git a/app/src/main/java/com/superwall/superapp/MainApplication.kt b/app/src/main/java/com/superwall/superapp/MainApplication.kt index 9cdfdf4c..d377e86a 100644 --- a/app/src/main/java/com/superwall/superapp/MainApplication.kt +++ b/app/src/main/java/com/superwall/superapp/MainApplication.kt @@ -62,7 +62,7 @@ class MainApplication : .build(), ) - configureWithObserverMode() + configureWithAutomaticInitialization() // configureWithRevenueCatInitialization() } @@ -88,7 +88,7 @@ class MainApplication : fun configureWithObserverMode() { Superwall.configure( - this, + this@MainApplication, CONSTANT_API_KEY, options = SuperwallOptions().apply { @@ -99,7 +99,7 @@ class MainApplication : } }, ) - Superwall.instance.delegate = this + Superwall.instance.delegate = this@MainApplication // Make sure we enable the game controller // Superwall.instance.options.isGameControllerEnabled = true 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 d26e1628..de8f3531 100644 --- a/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt +++ b/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt @@ -131,7 +131,7 @@ class RevenueCatPurchaseController( EntitlementStatus.Active( it.entitlements.active .map { - Entitlement(it.key) + Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL) }.toSet(), ), ) @@ -150,7 +150,7 @@ class RevenueCatPurchaseController( EntitlementStatus.Active( customerInfo.entitlements.active .map { - Entitlement(it.key) + Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL) }.toSet(), ), ) 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 556a1e23..852b3863 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -4,9 +4,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log -import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.BillingResult -import com.android.billingclient.api.Purchase import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.superwall.SuperwallEvent import com.superwall.sdk.analytics.superwall.SuperwallEvent.DeepLink @@ -632,7 +629,7 @@ object UITestHandler { 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") diff --git a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt index 12b3a2bf..9f886612 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt @@ -51,7 +51,7 @@ class SuperwallOptions { // **WARNING:**: Determines which network environment your SDK should use. // Defaults to `.release`. You should under no circumstance change this unless you // received the go-ahead from the Superwall team. - var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Release() + var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Developer() // Enables the sending of non-Superwall tracked events and properties back to the Superwall servers. // Defaults to `true`. diff --git a/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt b/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt index ac87afbd..f19b9fee 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt @@ -5,6 +5,14 @@ import kotlinx.serialization.Serializable @Serializable data class Entitlement( - @SerialName("id") + @SerialName("identifier") val id: String, -) + @SerialName("type") + val type: Type = Type.SERVICE_LEVEL, +) { + @Serializable + enum class Type { + @SerialName("SERVICE_LEVEL") + SERVICE_LEVEL, + } +} diff --git a/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt b/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt index 741a8eaf..46de00a3 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt @@ -1,5 +1,6 @@ package com.superwall.sdk.models.product +import com.superwall.sdk.models.entitlements.Entitlement import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -126,8 +127,10 @@ object PlayStoreProductSerializer : KSerializer { data class ProductItem( @SerialName("reference_name") val name: String, + @SerialName("store_product") val type: StoreProductType, - val entitlements: Set, + @SerialName("entitlements") + val entitlements: Set, ) { sealed class StoreProductType { data class PlayStore( @@ -173,6 +176,12 @@ object ProductItemSerializer : KSerializer { val storeProductJsonObject = jsonObject["store_product"]?.jsonObject ?: throw SerializationException("Missing store_product") + val entitlements = + jsonObject["entitlements"] + ?.jsonArray + ?.map { + Json.decodeFromJsonElement(it) + }?.toSet() ?: emptySet() // Deserialize 'storeProduct' JSON object into the expected Kotlin data class val storeProduct = Json.decodeFromJsonElement(storeProductJsonObject) @@ -180,7 +189,7 @@ object ProductItemSerializer : KSerializer { return ProductItem( name = name, type = ProductItem.StoreProductType.PlayStore(storeProduct), - entitlements = emptySet(), + entitlements = entitlements, ) } } diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index ea4e09be..a01731fb 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -76,38 +76,30 @@ class DeviceHelper( private val appInstallDate = Date(appInfo.firstInstallTime) fun daysSince(date: Date): Int { - val fromDate = date - val toDate = Date() - val fromInstant = Instant.ofEpochMilli(fromDate.time) - val toInstant = Instant.ofEpochMilli(toDate.time) - val duration = Duration.between(fromInstant, toInstant) + val fromDate = Instant.ofEpochMilli(date.time) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toDays().toInt() } fun minutesSince(date: Date): Int { - val fromDate = date - val toDate = Date() - val fromInstant = Instant.ofEpochMilli(fromDate.time) - val toInstant = Instant.ofEpochMilli(toDate.time) - val duration = Duration.between(fromInstant, toInstant) + val fromDate = Instant.ofEpochMilli(date.time) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toMinutes().toInt() } fun hoursSince(date: Date): Int { - val fromDate = date - val toDate = Date() - val fromInstant = Instant.ofEpochMilli(fromDate.time) - val toInstant = Instant.ofEpochMilli(toDate.time) - val duration = Duration.between(fromInstant, toInstant) + val fromDate = Instant.ofEpochMilli(date.time) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toHours().toInt() } fun monthsSince(date: Date): Int { - val fromDate = date - val toDate = Date() - val fromInstant = Instant.ofEpochMilli(fromDate.time) - val toInstant = Instant.ofEpochMilli(toDate.time) - val duration = Duration.between(fromInstant, toInstant) + val fromDate = Instant.ofEpochMilli(date.time) + val toDate = Instant.now() + val duration = Duration.between(fromDate, toDate) return duration.toDays().toInt() / 30 } @@ -488,7 +480,7 @@ class DeviceHelper( isSandbox = isSandbox.toString(), activeEntitlements = Superwall.instance.entitlements.active - .map { it.id }, + .map { mapOf("identifier" to it.id) }, isFirstAppOpen = isFirstAppOpen, sdkVersion = sdkVersion, sdkVersionPadded = sdkVersionPadded, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index 7b16ac0f..739a99d1 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -325,6 +325,8 @@ data class PaywallInfo( buildId = "", cacheKey = "", isScrollEnabled = true, + shimmerLoadCompleteTime = null, + shimmerLoadStartTime = null, ) } } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt index 4d9d5e60..0e0e2b16 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/InternalGetPresentationResult.kt @@ -41,7 +41,6 @@ private fun handle( } return when (error) { - is PaywallPresentationRequestStatusReason.UserIsSubscribed -> PresentationResult.UserIsSubscribed() is PaywallPresentationRequestStatusReason.NoPaywallView -> PresentationResult.PaywallNotAvailable() is PaywallPresentationRequestStatusReason.NoRuleMatch -> PresentationResult.NoRuleMatch() is PaywallPresentationRequestStatusReason.Holdout -> PresentationResult.Holdout(error.experiment) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt index 30771718..ea243222 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PaywallPresentationRequestStatus.kt @@ -24,9 +24,6 @@ sealed class PaywallPresentationRequestStatusReason( /** There's already a paywall presented. */ class PaywallAlreadyPresented : PaywallPresentationRequestStatusReason("paywall_already_presented") - /** The user is subscribed. */ - class UserIsSubscribed : PaywallPresentationRequestStatusReason("user_is_subscribed") - /** The user is in a holdout group. */ data class Holdout( val experiment: Experiment, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt deleted file mode 100644 index 28e18ecf..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/PresentationErrors.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.superwall.sdk.paywall.presentation.internal - -import com.superwall.sdk.Superwall -import com.superwall.sdk.paywall.presentation.internal.state.PaywallSkippedReason -import com.superwall.sdk.paywall.presentation.internal.state.PaywallState -import kotlinx.coroutines.flow.MutableSharedFlow - -suspend fun Superwall.userHasEntitlements(paywallStatePublisher: MutableSharedFlow?): PresentationPipelineError { - val state = PaywallState.Skipped(PaywallSkippedReason.UserIsSubscribed()) - paywallStatePublisher?.emit(state) - return PaywallPresentationRequestStatusReason.UserIsSubscribed() -} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt index a81e56f2..629d7674 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/result/PresentationResult.kt @@ -31,16 +31,6 @@ sealed class PresentationResult { val experiment: Experiment, ) : PresentationResult() - // The user is subscribed. - // - // This means ``Superwall/subscriptionStatus`` is set to `.active`. If you're - // letting Superwall handle subscription-related logic, it will be based on the on-device - // receipts. Otherwise it'll be based on the value you've set. - // - // By default, paywalls do not show to users who are already subscribed. You can override this - // behavior in the paywall editor. - class UserIsSubscribed : PresentationResult() - - // No view could be found to present on. + // No view controller could be found to present on. class PaywallNotAvailable : PresentationResult() } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt index 02b7a22e..68d8c60a 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/cel/SuperscriptEvaluator.kt @@ -47,7 +47,7 @@ internal class SuperscriptEvaluator( rule: TriggerRule, eventData: EventData?, ): TriggerRuleOutcome { - if (rule.expressionJs == null && rule.expression == null) { + if (rule.expressionCEL == null) { return rule.tryToMatchOccurrence(storage, true) } @@ -56,12 +56,7 @@ internal class SuperscriptEvaluator( val factory = factory.makeRuleAttributes(eventData, rule.computedPropertyRequests) val userAttributes = factory.toPassableValue() val expression = - ( - rule.expressionCEL - ?: run { - rule.expression ?: "".replace("and", "&&").replace("or", "||") - } - ).replace("device.", "computed.") + rule.expressionCEL val executionContext = ExecutionContext( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt index 81096640..04528911 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt @@ -32,35 +32,19 @@ internal class CombinedExpressionEvaluator( eventData: EventData?, ): TriggerRuleOutcome { // Expression matches all - if (rule.expressionJs == null && rule.expression == null) { + if (rule.expressionJs == null && rule.expression == null && rule.expressionCEL == null) { return rule.tryToMatchOccurrence(storage.coreDataManager, true) } - val base64Params = - getBase64Params(rule, eventData) ?: return TriggerRuleOutcome.noMatch( - UnmatchedRule.Source.EXPRESSION, - rule.experiment.id, - ) - - val result = evaluator.evaluate(base64Params, rule) + // If we are evaluating JS/Liquid, we encode rules, otherwise we return null + // and evaluate superscript only val celEvaluation = try { superscriptEvaluator.evaluateExpression(rule, eventData) } catch (e: Exception) { TriggerRuleOutcome.noMatch(UnmatchedRule.Source.EXPRESSION, rule.experiment.id) } - if (shouldTraceResults) { - track( - InternalSuperwallEvent.ExpressionResult( - liquidExpression = rule.expression, - celExpression = rule.expressionCEL, - celExpressionResult = if (celEvaluation is TriggerRuleOutcome.Match) true else false, - jsExpression = rule.expressionJs, - jsExpressionResult = if (result is TriggerRuleOutcome.Match) true else false, - ), - ) - } - return result + return celEvaluation } private suspend fun getBase64Params( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt index 688d79f4..22e82e12 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt @@ -44,7 +44,7 @@ data class DeviceTemplate( val utcDateTime: String, val localDateTime: String, val isSandbox: String, - val activeEntitlements: List, + val activeEntitlements: List>, val isFirstAppOpen: Boolean, val sdkVersion: String, val sdkVersionPadded: String, diff --git a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt index 1d624dac..9ba20a4e 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt @@ -83,10 +83,10 @@ class Entitlements( internal fun byProductId(id: String): Set = _entitlementsByProduct[id] ?: emptySet() - internal fun addEntitlementsByProductId(idToEntitlements: Map>) { + internal fun addEntitlementsByProductId(idToEntitlements: Map>) { _entitlementsByProduct.putAll( idToEntitlements.mapValues { (_, entitlements) -> - entitlements.map { Entitlement(it) }.toSet() + entitlements.toSet() }, ) _all.clear() From 185d704f6575159d2767c44bae02d15bcad138a8 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 9 Dec 2024 11:52:29 +0100 Subject: [PATCH 23/37] Update how billing flow and observations work, minor entitlement refactor, test fixing --- .../test/SimpleScreenshotTestExecutor.kt | 2 +- .../purchase/RevenueCatPurchaseController.kt | 4 +- .../superwall/superapp/test/UITestHandler.kt | 8 +- .../main/java/com/superwall/sdk/Superwall.kt | 24 ++- .../LaunchBillingFlowWithSuperwall.kt | 45 +++++ .../observer/SuperwallBillingFlowParams.kt | 162 ++++++++++++++++++ .../sdk/config/options/SuperwallOptions.kt | 2 +- .../java/com/superwall/sdk/debug/DebugView.kt | 2 +- .../models/entitlements/EntitlementStatus.kt | 2 +- .../sdk/store/AutomaticPurchaseController.kt | 4 +- .../com/superwall/sdk/store/Entitlements.kt | 6 +- .../store/transactions/TransactionManager.kt | 43 ++++- .../superwall/sdk/store/EntitlementsTest.kt | 6 +- .../transactions/TransactionManagerTest.kt | 4 +- 14 files changed, 291 insertions(+), 23 deletions(-) create mode 100644 superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt create mode 100644 superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt 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 5f3b19f1..c8fca9dd 100644 --- a/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt +++ b/app/src/androidTest/java/com/example/superapp/test/SimpleScreenshotTestExecutor.kt @@ -144,7 +144,7 @@ class SimpleScreenshotTestExecutor { it.waitFor { it is SuperwallEvent.PaywallPresentationRequest } } } - Superwall.instance.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive) } @Test 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 de8f3531..225c36de 100644 --- a/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt +++ b/app/src/main/java/com/superwall/superapp/purchase/RevenueCatPurchaseController.kt @@ -136,7 +136,7 @@ class RevenueCatPurchaseController( ), ) } else { - setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + setEntitlementStatus(EntitlementStatus.Inactive) } } } @@ -155,7 +155,7 @@ class RevenueCatPurchaseController( ), ) } else { - setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + setEntitlementStatus(EntitlementStatus.Inactive) } } 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 852b3863..6bf7750d 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -183,7 +183,7 @@ object UITestHandler { test = { scope, events -> Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) Superwall.instance.register(event = "present_always") - Superwall.instance.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive) }, ) var test10Info = @@ -483,7 +483,7 @@ object UITestHandler { events.first { it is SuperwallEvent.EntitlementStatusDidChange } delay(4000) - Superwall.instance.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive) } }, ) @@ -633,7 +633,7 @@ object UITestHandler { "UserIsSubscribed expected, received $result", ) println("!!! TEST 32 !!! $result") - Superwall.instance.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive) } }, ) @@ -1358,7 +1358,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.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive) // Create a mock Superwall delegate val delegate = MockSuperwallDelegate() diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 52b3737c..281d7f70 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -4,7 +4,9 @@ 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 @@ -769,7 +771,7 @@ class Superwall( */ fun purchase( - product: RawStoreProduct, + product: StoreProduct, onFinished: (Result) -> Unit, ) { ioScope.launch { @@ -777,7 +779,7 @@ class Superwall( withErrorTracking { dependencyContainer.transactionManager.purchase( TransactionManager.PurchaseSource.ExternalPurchase( - StoreProduct(product), + product, ), ) }.toResult() @@ -831,6 +833,24 @@ class Superwall( } } + fun observePurchaseStart(product: ProductDetails) { + observe(PurchasingObserverState.PurchaseWillBegin(product)) + } + + fun observePurchaseError( + product: ProductDetails, + error: Throwable, + ) { + observe(PurchasingObserverState.PurchaseWillBegin(product)) + } + + fun observePurchaseResult( + billingResult: BillingResult, + purchases: List, + ) { + observe(PurchasingObserverState.PurchaseResult(billingResult, purchases)) + } + /** * Restores purchases * diff --git a/superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt b/superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt new file mode 100644 index 00000000..e0076df1 --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt @@ -0,0 +1,45 @@ +package com.superwall.sdk.billing.observer + +import android.app.Activity +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingResult +import com.superwall.sdk.Superwall +import com.superwall.sdk.logger.LogLevel +import com.superwall.sdk.logger.LogScope +import com.superwall.sdk.logger.Logger +import com.superwall.sdk.store.PurchasingObserverState +import com.superwall.sdk.store.abstractions.product.RawStoreProduct + +fun BillingClient.launchBillingFlowWithSuperwall( + activity: Activity, + params: SuperwallBillingFlowParams, +): BillingResult { + if (Superwall.initialized.not()) { + throw IllegalStateException("Superwall SDK is not initialized") + } + if (Superwall.instance.options.shouldObservePurchases + .not() + ) { + Logger.debug( + LogLevel.error, + LogScope.superwallCore, + "Observer mode is not enabled. In order to observe purchases, please enable it in the SuperwallOptions by setting `shouldObservePurchases` to true.", + mapOf( + "method" to "launchBillingFlowWithSuperwall", + "products" to + params.productDetailsParams + .map { it.details.productId } + .joinToString(", "), + ), + ) + return launchBillingFlow(activity, params.toOriginal()) + } + + params.productDetailsParams.forEach { + val product = RawStoreProduct.from(it.details) + Superwall.instance.observe( + PurchasingObserverState.PurchaseWillBegin(product.underlyingProductDetails), + ) + } + return launchBillingFlow(activity, params.toOriginal()) +} diff --git a/superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt b/superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt new file mode 100644 index 00000000..91e42f17 --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt @@ -0,0 +1,162 @@ +package com.superwall.sdk.billing.observer + +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.SkuDetails + +class SuperwallBillingFlowParams private constructor( + internal val params: BillingFlowParams, + internal val productDetailsParams: MutableList = mutableListOf(), +) { + companion object { + fun newBuilder(): Builder = Builder() + } + + fun toOriginal() = params + + class Builder { + private val builder = BillingFlowParams.newBuilder() + internal val productDetailsParams: MutableList = mutableListOf() + + fun setIsOfferPersonalized(isOfferPersonalized: Boolean): Builder = + apply { + builder.setIsOfferPersonalized(isOfferPersonalized) + } + + fun setObfuscatedAccountId(obfuscatedAccountId: String): Builder = + apply { + builder.setObfuscatedAccountId(obfuscatedAccountId) + } + + fun setObfuscatedProfileId(obfuscatedProfileId: String): Builder = + apply { + builder.setObfuscatedProfileId(obfuscatedProfileId) + } + + fun setProductDetailsParamsList(productDetailsParamsList: List): Builder = + apply { + productDetailsParams.addAll(productDetailsParamsList) + builder.setProductDetailsParamsList(productDetailsParamsList.map { it.toOriginal() }) + } + + @Deprecated("Use setProductDetailsParamsList instead") + fun setSkuDetails(skuDetails: SkuDetails): Builder = + apply { + builder.setSkuDetails(skuDetails) + } + + fun setSubscriptionUpdateParams(params: SubscriptionUpdateParams): Builder = + apply { + builder.setSubscriptionUpdateParams(params.toOriginal()) + } + + fun build(): SuperwallBillingFlowParams = SuperwallBillingFlowParams(builder.build(), productDetailsParams) + } + + class ProductDetailsParams private constructor( + private val params: BillingFlowParams.ProductDetailsParams, + internal val details: ProductDetails, + ) { + fun toOriginal() = params + + companion object { + fun newBuilder(): ProductDetailsParamsBuilder = ProductDetailsParamsBuilder() + } + + class ProductDetailsParamsBuilder { + private val builder = BillingFlowParams.ProductDetailsParams.newBuilder() + internal var details: ProductDetails? = null + + fun setOfferToken(offerToken: String): ProductDetailsParamsBuilder = + apply { + builder.setOfferToken(offerToken) + } + + fun setProductDetails(productDetails: ProductDetails): ProductDetailsParamsBuilder = + apply { + details = productDetails + builder.setProductDetails(productDetails) + } + + fun build(): ProductDetailsParams = + ProductDetailsParams( + builder.build(), + details ?: throw IllegalArgumentException("ProductDetails are required"), + ) + } + } + + class SubscriptionUpdateParams private constructor( + private val params: BillingFlowParams.SubscriptionUpdateParams, + ) { + fun toOriginal() = params + + companion object { + fun newBuilder(): SubscriptionUpdateParamsBuilder = SubscriptionUpdateParamsBuilder() + } + + class SubscriptionUpdateParamsBuilder { + private val builder = BillingFlowParams.SubscriptionUpdateParams.newBuilder() + + fun setOldPurchaseToken(purchaseToken: String): SubscriptionUpdateParamsBuilder = + apply { + builder.setOldPurchaseToken(purchaseToken) + } + + @Deprecated("Use setOldPurchaseToken instead") + fun setOldSkuPurchaseToken(purchaseToken: String): SubscriptionUpdateParamsBuilder = + apply { + builder.setOldSkuPurchaseToken(purchaseToken) + } + + fun setOriginalExternalTransactionId(externalTransactionId: String): SubscriptionUpdateParamsBuilder = + apply { + builder.setOriginalExternalTransactionId(externalTransactionId) + } + + @Deprecated("Use setSubscriptionReplacementMode instead") + fun setReplaceProrationMode(replaceSkusProrationMode: Int): SubscriptionUpdateParamsBuilder = + apply { + builder.setReplaceProrationMode(replaceSkusProrationMode) + } + + @Deprecated("Use setSubscriptionReplacementMode instead") + fun setReplaceSkusProrationMode(replaceSkusProrationMode: Int): SubscriptionUpdateParamsBuilder = + apply { + builder.setReplaceSkusProrationMode(replaceSkusProrationMode) + } + + fun setSubscriptionReplacementMode(subscriptionReplacementMode: Int): SubscriptionUpdateParamsBuilder = + apply { + builder.setSubscriptionReplacementMode(subscriptionReplacementMode) + } + + fun build(): SubscriptionUpdateParams = SubscriptionUpdateParams(builder.build()) + } + + @Retention(AnnotationRetention.SOURCE) + annotation class ReplacementMode { + companion object { + const val UNKNOWN_REPLACEMENT_MODE = 0 + const val WITH_TIME_PRORATION = 1 + const val CHARGE_PRORATED_PRICE = 2 + const val WITHOUT_PRORATION = 3 + const val CHARGE_FULL_PRICE = 5 + const val DEFERRED = 6 + } + } + } + + @Deprecated("Use SubscriptionUpdateParams.ReplacementMode instead") + @Retention(AnnotationRetention.SOURCE) + annotation class ProrationMode { + companion object { + const val UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY = 0 + const val IMMEDIATE_WITH_TIME_PRORATION = 1 + const val IMMEDIATE_AND_CHARGE_PRORATED_PRICE = 2 + const val IMMEDIATE_WITHOUT_PRORATION = 3 + const val DEFERRED = 4 + const val IMMEDIATE_AND_CHARGE_FULL_PRICE = 5 + } + } +} diff --git a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt index 9f886612..12b3a2bf 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt @@ -51,7 +51,7 @@ class SuperwallOptions { // **WARNING:**: Determines which network environment your SDK should use. // Defaults to `.release`. You should under no circumstance change this unless you // received the go-ahead from the Superwall team. - var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Developer() + var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Release() // Enables the sending of non-Superwall tracked events and properties back to the Superwall servers. // Defaults to `true`. diff --git a/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt b/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt index 946d47de..dcc1e70d 100644 --- a/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt +++ b/superwall/src/main/java/com/superwall/sdk/debug/DebugView.kt @@ -835,7 +835,7 @@ class DebugView( // bottomButton.setImageDrawable(null) // bottomButton.showLoading = true - val inactiveSubscriptionPublisher = MutableStateFlow(EntitlementStatus.NoActiveEntitlements) + val inactiveSubscriptionPublisher = MutableStateFlow(EntitlementStatus.Inactive) val presentationRequest = factory.makePresentationRequest( diff --git a/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt index 37eb0066..26dc2f15 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt @@ -9,7 +9,7 @@ sealed class EntitlementStatus { object Unkown : EntitlementStatus() @Serializable - object NoActiveEntitlements : EntitlementStatus() + object Inactive : EntitlementStatus() @Serializable data class Active( diff --git a/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt b/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt index d88546a7..eb719c0c 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt @@ -283,11 +283,11 @@ class AutomaticPurchaseController( if (entitlements.isNotEmpty()) { EntitlementStatus.Active(entitlements) } else { - EntitlementStatus.NoActiveEntitlements + EntitlementStatus.Inactive } } } else { - EntitlementStatus.NoActiveEntitlements + EntitlementStatus.Inactive } if (!Superwall.initialized) { diff --git a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt index 9ba20a4e..cd5a0fa8 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt @@ -19,7 +19,7 @@ class Entitlements( ) { private val _entitlementsByProduct = ConcurrentHashMap>() - val _status: MutableStateFlow = + private val _status: MutableStateFlow = MutableStateFlow(EntitlementStatus.Unkown) val status: StateFlow @@ -57,7 +57,7 @@ class Entitlements( when (value) { is EntitlementStatus.Active -> { if (value.entitlements.isEmpty()) { - setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + setEntitlementStatus(EntitlementStatus.Inactive) } else { _all.addAll(value.entitlements) _active.addAll(value.entitlements) @@ -66,7 +66,7 @@ class Entitlements( } } - is EntitlementStatus.NoActiveEntitlements -> { + is EntitlementStatus.Inactive -> { _active.clear() _inactive.clear() _status.value = value diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index 833b0208..434cc4d6 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -1,5 +1,7 @@ package com.superwall.sdk.store.transactions +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingResult import com.android.billingclient.api.Purchase import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track @@ -40,7 +42,11 @@ import com.superwall.sdk.store.StoreManager import com.superwall.sdk.store.abstractions.product.RawStoreProduct import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch +import java.util.LinkedHashSet class TransactionManager( private val storeManager: StoreManager, @@ -85,7 +91,42 @@ class TransactionManager( private var lastPaywallView: PaywallView? = null - private var transactionsInProgress: MutableSet = mutableSetOf() + private var transactionsInProgress: LinkedHashSet = LinkedHashSet() + + private val shouldObserveTransactionFinishingAutomatically: Boolean + get() = factory.makeSuperwallOptions().shouldObservePurchases + + init { + if (shouldObserveTransactionFinishingAutomatically + ) { + ioScope.launch { + storeKitManager.billing.purchaseResults + .asSharedFlow() + .filterNotNull() + .collectLatest { it: InternalPurchaseResult -> + val id = transactionsInProgress.last() + val state = + when (it) { + is InternalPurchaseResult.Purchased -> { + PurchasingObserverState.PurchaseResult( + BillingResult + .newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .build(), + listOf(it.purchase), + ) + } + else -> + PurchasingObserverState.PurchaseError( + error = (it as? InternalPurchaseResult.Failed)?.error ?: Throwable("Unknown error"), + product = storeKitManager.productsByFullId[id]?.rawStoreProduct!!.underlyingProductDetails, + ) + } + handle(it, state) + } + } + } + } internal suspend fun handle( result: InternalPurchaseResult, diff --git a/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt b/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt index 9186a020..47604e01 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt @@ -98,7 +98,7 @@ class EntitlementsTest { entitlements.setEntitlementStatus(EntitlementStatus.Active(emptySet())) Then("it should convert to NoActiveEntitlements status") { - assertTrue(entitlements.status.value is EntitlementStatus.NoActiveEntitlements) + assertTrue(entitlements.status.value is EntitlementStatus.Inactive) assertTrue(entitlements.active.isEmpty()) assertTrue(entitlements.inactive.isEmpty()) } @@ -114,12 +114,12 @@ class EntitlementsTest { every { storage.read(StoredEntitlementsByProductId) } returns null entitlements = Entitlements(storage) When("setting NoActiveEntitlements status") { - entitlements.setEntitlementStatus(EntitlementStatus.NoActiveEntitlements) + entitlements.setEntitlementStatus(EntitlementStatus.Inactive) Then("it should clear all collections") { assertTrue(entitlements.active.isEmpty()) assertTrue(entitlements.inactive.isEmpty()) - assertTrue(entitlements.status.value is EntitlementStatus.NoActiveEntitlements) + assertTrue(entitlements.status.value is EntitlementStatus.Inactive) } } } diff --git a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt index 6ce2ee07..b2525534 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -692,7 +692,7 @@ class TransactionManagerTest { track = { e -> events.update { it + e } }, - entitlementStatus = { EntitlementStatus.NoActiveEntitlements }, + entitlementStatus = { EntitlementStatus.Inactive }, ) coEvery { purchaseController.restorePurchases() } returns @@ -736,7 +736,7 @@ class TransactionManagerTest { track = { e -> events.update { it + e } }, - entitlementStatus = { EntitlementStatus.NoActiveEntitlements }, + entitlementStatus = { EntitlementStatus.Inactive }, ) coEvery { purchaseController.restorePurchases() } returns RestorationResult.Restored() From 5edcaf09b20f2eb93a99981807d278b5b7f1454a Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 12 Dec 2024 17:03:35 +0100 Subject: [PATCH 24/37] Fix entitlement bugs, update testing suite, cleanup some code --- .../test/FlowScreenshotTestExecutor.kt | 8 +- .../test/SimpleScreenshotTestExecutor.kt | 25 +-- .../example/superapp/utils/TestingUtils.kt | 4 +- .../com/superwall/superapp/MainApplication.kt | 2 +- .../superapp/test/PurchaseMockBuilder.java | 94 ---------- .../superapp/test/PurchaseMockBuilder.kt | 105 +++++++++++ .../superwall/superapp/test/UITestActivity.kt | 2 + .../superwall/superapp/test/UITestHandler.kt | 109 +++++++++-- .../SuperwallBillingFlowParamsTest.kt | 79 ++++++++ .../main/java/com/superwall/sdk/Superwall.kt | 39 +++- .../observer/SuperwallBillingFlowParams.kt | 10 +- .../com/superwall/sdk/config/ConfigLogic.kt | 8 +- .../com/superwall/sdk/config/ConfigManager.kt | 7 +- .../sdk/config/models/ConfigState.kt | 4 +- .../sdk/config/options/SuperwallOptions.kt | 2 +- .../sdk/dependencies/DependencyContainer.kt | 13 -- .../sdk/misc/Config+AwaitFirstValidConfig.kt | 2 +- .../com/superwall/sdk/models/config/Config.kt | 3 + .../sdk/models/product/ProductItem.kt | 3 +- .../CombinedExpressionEvaluator.kt | 36 ---- .../sdk/store/AutomaticPurchaseController.kt | 24 ++- .../com/superwall/sdk/store/Entitlements.kt | 20 +- .../sdk/store/PurchasingObserverState.kt | 16 +- .../store/transactions/TransactionManager.kt | 174 +++++++++--------- .../templating/models/DeviceTemplateTest.kt | 2 +- .../superwall/sdk/store/StoreManagerTest.kt | 12 +- .../transactions/TransactionManagerTest.kt | 2 +- 27 files changed, 502 insertions(+), 303 deletions(-) delete mode 100644 app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.java create mode 100644 app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.kt create mode 100644 superwall/src/androidTest/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParamsTest.kt 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 ba776d62..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) { @@ -134,7 +136,7 @@ class FlowScreenshotTestExecutor { with(dropshots) { screenshotFlow(UITestHandler.test14Info) { step { - it.waitFor { it is SuperwallEvent.PaywallWebviewLoadComplete } + it.waitFor { it is SuperwallEvent.ShimmerViewComplete } awaitUntilShimmerDisappears() awaitUntilWebviewAppears() delayFor(300.milliseconds) 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 c8fca9dd..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.models.entitlements.EntitlementStatus 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.setEntitlementStatus(EntitlementStatus.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 6229b3c4..20e4a000 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.view.ShimmerView +import com.superwall.sdk.config.models.ConfigurationStatus +import com.superwall.sdk.paywall.vc.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/java/com/superwall/superapp/MainApplication.kt b/app/src/main/java/com/superwall/superapp/MainApplication.kt index d377e86a..ac66945e 100644 --- a/app/src/main/java/com/superwall/superapp/MainApplication.kt +++ b/app/src/main/java/com/superwall/superapp/MainApplication.kt @@ -62,7 +62,7 @@ class MainApplication : .build(), ) - configureWithAutomaticInitialization() + configureWithObserverMode() // configureWithRevenueCatInitialization() } diff --git a/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.java b/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.java deleted file mode 100644 index 9b5c794a..00000000 --- a/app/src/main/java/com/superwall/superapp/test/PurchaseMockBuilder.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.superwall.superapp.test; - -import com.android.billingclient.api.Purchase; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -public class PurchaseMockBuilder { - private JSONObject purchaseJson; - - public PurchaseMockBuilder() { - purchaseJson = new JSONObject(); - } - - public static Purchase createDefaultPurchase() throws JSONException { - return new PurchaseMockBuilder() - .setPurchaseState(Purchase.PurchaseState.PURCHASED) - .setPurchaseTime(System.currentTimeMillis()) - .setOrderId("GPA.1234-5678-9012-34567") - .setProductId("premium_subscription") - .setQuantity(1) - .setPurchaseToken("opaque-token-up-to-1950-characters") - .setPackageName("com.example.app") - .setDeveloperPayload("") - .setAcknowledged(true) - .setAutoRenewing(true) - .build(); - } - - public PurchaseMockBuilder setPurchaseState(int state) throws JSONException { - purchaseJson.put("purchaseState", state == 2 ? 4 : state); - return this; - } - - public PurchaseMockBuilder setPurchaseTime(long time) throws JSONException { - purchaseJson.put("purchaseTime", time); - return this; - } - - public PurchaseMockBuilder setOrderId(String orderId) throws JSONException { - purchaseJson.put("orderId", orderId); - return this; - } - - public PurchaseMockBuilder setProductId(String productId) throws JSONException { - JSONArray productIds = new JSONArray(); - productIds.put(productId); - purchaseJson.put("productIds", productIds); - // For backward compatibility - purchaseJson.put("productId", productId); - return this; - } - - public PurchaseMockBuilder setQuantity(int quantity) throws JSONException { - purchaseJson.put("quantity", quantity); - return this; - } - - public PurchaseMockBuilder setPurchaseToken(String token) throws JSONException { - purchaseJson.put("token", token); - purchaseJson.put("purchaseToken", token); - return this; - } - - public PurchaseMockBuilder setPackageName(String packageName) throws JSONException { - purchaseJson.put("packageName", packageName); - return this; - } - - public PurchaseMockBuilder setDeveloperPayload(String payload) throws JSONException { - purchaseJson.put("developerPayload", payload); - return this; - } - - public PurchaseMockBuilder setAcknowledged(boolean acknowledged) throws JSONException { - purchaseJson.put("acknowledged", acknowledged); - return this; - } - - public PurchaseMockBuilder setAutoRenewing(boolean autoRenewing) throws JSONException { - purchaseJson.put("autoRenewing", autoRenewing); - return this; - } - - public PurchaseMockBuilder setAccountIdentifiers(String obfuscatedAccountId, String obfuscatedProfileId) throws JSONException { - purchaseJson.put("obfuscatedAccountId", obfuscatedAccountId); - purchaseJson.put("obfuscatedProfileId", obfuscatedProfileId); - return this; - } - - public Purchase build() throws JSONException { - return new Purchase(purchaseJson.toString(), "dummy-signature"); - } -} \ No newline at end of file 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 6bf7750d..0fa6c08d 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -205,16 +205,18 @@ 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 } - // Dismiss any views - delay(8.seconds) + events.first { it is SuperwallEvent.ShimmerViewComplete } + // Dismiss any view controllers + delay(4.seconds) // 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) @@ -248,7 +250,7 @@ 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) @@ -475,12 +477,10 @@ object UITestHandler { "4s later.", test = { scope, events -> // Set user as subscribed - Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) - + 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.EntitlementStatusDidChange } delay(4000) Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive) @@ -493,16 +493,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.entitlementStatus.value - - Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) - + 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.setEntitlementStatus(currentSubscriptionStatus) } }, ) @@ -512,7 +508,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, @@ -530,10 +527,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.entitlementStatus.value - - Superwall.instance.setEntitlementStatus(EntitlementStatus.Active(setOf(Entitlement("test")))) - + Superwall.instance.setEntitlementStatus("pro") Superwall.instance.register(event = "register_gated_paywall") { val alertController = AlertControllerFactory.make( @@ -544,8 +538,8 @@ object UITestHandler { ) alertController.show() } - delay(8000) - Superwall.instance.setEntitlementStatus(currentSubscriptionStatus) + delay(1000) + Superwall.instance.setEntitlementStatus(EntitlementStatus.Inactive) }, ) var test28Info = @@ -1538,6 +1532,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, @@ -1615,5 +1680,9 @@ object UITestHandler { UITestHandler.testAndroid21Info, UITestHandler.testAndroid22Info, UITestHandler.testAndroid23Info, + UITestHandler.testAndroid100Info, + UITestHandler.testAndroid101Info, + UITestHandler.testAndroid102Info, + UITestHandler.testAndroid103Info, ) } 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/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 281d7f70..8ab4c172 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -32,6 +32,7 @@ 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 @@ -70,8 +71,10 @@ 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 @@ -98,7 +101,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. @@ -212,6 +219,22 @@ class Superwall( 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")` + * + * @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())) + } + } + /** * Properties stored about the user, set using `setUserAttributes`. */ @@ -276,6 +299,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 @@ -806,7 +839,7 @@ class Superwall( } when (state) { is PurchasingObserverState.PurchaseWillBegin -> { - val product = StoreProduct(RawStoreProduct.from(state.productId)) + val product = StoreProduct(RawStoreProduct.from(state.product)) dependencyContainer.transactionManager.prepareToPurchase( product, source = TransactionManager.PurchaseSource.ObserverMode(product), @@ -841,7 +874,7 @@ class Superwall( product: ProductDetails, error: Throwable, ) { - observe(PurchasingObserverState.PurchaseWillBegin(product)) + observe(PurchasingObserverState.PurchaseError(product, error)) } fun observePurchaseResult( diff --git a/superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt b/superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt index 91e42f17..2bcd6e72 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/observer/SuperwallBillingFlowParams.kt @@ -78,11 +78,15 @@ class SuperwallBillingFlowParams private constructor( builder.setProductDetails(productDetails) } - fun build(): ProductDetailsParams = - ProductDetailsParams( + fun build(): ProductDetailsParams { + val details = + details ?: throw IllegalArgumentException("ProductDetails are required") + + return ProductDetailsParams( builder.build(), - details ?: throw IllegalArgumentException("ProductDetails are required"), + details, ) + } } } diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt index e6a04453..b6ec50d0 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigLogic.kt @@ -8,6 +8,7 @@ import com.superwall.sdk.models.assignment.ConfirmableAssignment import com.superwall.sdk.models.config.Config import com.superwall.sdk.models.config.PreloadingDisabled import com.superwall.sdk.models.paywall.Paywall +import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.triggers.* import com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator.ExpressionEvaluating import java.util.* @@ -260,6 +261,7 @@ object ConfigLogic { skippedExperimentIds.add(rule.experiment.id) } } + TriggerPreloadBehavior.ALWAYS -> {} TriggerPreloadBehavior.NEVER -> skippedExperimentIds.add(rule.experiment.id) } @@ -318,9 +320,5 @@ object ConfigLogic { } // Returns entitlements mapped by product ID - fun extractEntitlementsByProductId(from: List) = - from - .flatMap { it.products } - .map { it.fullProductId to it.entitlements } - .toMap() + fun extractEntitlementsByProductId(from: List) = from.associate { it.fullProductId to it.entitlements } } diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt index 0c9adb7a..54743a86 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt @@ -72,7 +72,7 @@ open class ConfigManager( JavascriptEvaluator.Factory // The configuration of the Superwall dashboard - val configState = MutableStateFlow(ConfigState.None) + internal val configState = MutableStateFlow(ConfigState.None) // Convenience variable to access config val config: Config? @@ -145,6 +145,7 @@ open class ConfigManager( } } } catch (e: Throwable) { + e.printStackTrace() // If fetching config fails, default to the cached version // Note: Only a timeout exception is possible here oldConfig?.let { @@ -307,7 +308,7 @@ open class ConfigManager( } triggersByEventName = ConfigLogic.getTriggersByEventName(config.triggers) assignments.choosePaywallVariants(config.triggers) - ConfigLogic.extractEntitlementsByProductId(config.paywalls).let { + ConfigLogic.extractEntitlementsByProductId(config.products).let { entitlements.addEntitlementsByProductId(it) } } @@ -378,7 +379,7 @@ open class ConfigManager( return } - var retryCount: AtomicInteger = AtomicInteger(0) + val retryCount: AtomicInteger = AtomicInteger(0) val startTime = System.currentTimeMillis() network .getConfig { diff --git a/superwall/src/main/java/com/superwall/sdk/config/models/ConfigState.kt b/superwall/src/main/java/com/superwall/sdk/config/models/ConfigState.kt index b6b981a4..4affe957 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/models/ConfigState.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/models/ConfigState.kt @@ -2,7 +2,7 @@ package com.superwall.sdk.config.models import com.superwall.sdk.models.config.Config -sealed class ConfigState { +internal sealed class ConfigState { object None : ConfigState() object Retrieving : ConfigState() @@ -18,7 +18,7 @@ sealed class ConfigState { ) : ConfigState() } -fun ConfigState.getConfig(): Config? = +internal fun ConfigState.getConfig(): Config? = when (this) { is ConfigState.Retrieved -> config else -> null diff --git a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt index 12b3a2bf..9f886612 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt @@ -51,7 +51,7 @@ class SuperwallOptions { // **WARNING:**: Determines which network environment your SDK should use. // Defaults to `.release`. You should under no circumstance change this unless you // received the go-ahead from the Superwall team. - var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Release() + var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Developer() // Enables the sending of non-Superwall tracked events and properties back to the Superwall servers. // Defaults to `true`. diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index 957c8c0b..e16c4a5a 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -57,7 +57,6 @@ import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.presentation.rule_logic.cel.SuperscriptEvaluator import com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator.CombinedExpressionEvaluator import com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator.ExpressionEvaluating -import com.superwall.sdk.paywall.presentation.rule_logic.javascript.DefaultJavascriptEvalutor import com.superwall.sdk.paywall.presentation.rule_logic.javascript.JavascriptEvaluator import com.superwall.sdk.paywall.request.PaywallRequest import com.superwall.sdk.paywall.request.PaywallRequestManager @@ -154,14 +153,6 @@ class DependencyContainer( private val evaluator by lazy { CombinedExpressionEvaluator( storage = storage, - factory = this, - evaluator = - DefaultJavascriptEvalutor( - ioScope = ioScope, - uiScope = uiScope, - context = context, - storage = storage, - ), superscriptEvaluator = SuperscriptEvaluator( json = @@ -173,10 +164,6 @@ class DependencyContainer( factory = this, ioScope = ioScope, ), - track = { - Superwall.instance.track(it) - }, - shouldTraceResults = makeFeatureFlags()?.enableCELLogging ?: false, ) } diff --git a/superwall/src/main/java/com/superwall/sdk/misc/Config+AwaitFirstValidConfig.kt b/superwall/src/main/java/com/superwall/sdk/misc/Config+AwaitFirstValidConfig.kt index a9c3b2ed..539c4698 100644 --- a/superwall/src/main/java/com/superwall/sdk/misc/Config+AwaitFirstValidConfig.kt +++ b/superwall/src/main/java/com/superwall/sdk/misc/Config+AwaitFirstValidConfig.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first -suspend fun Flow.awaitFirstValidConfig(): Config = +internal suspend fun Flow.awaitFirstValidConfig(): Config = try { filterIsInstance() .first() diff --git a/superwall/src/main/java/com/superwall/sdk/models/config/Config.kt b/superwall/src/main/java/com/superwall/sdk/models/config/Config.kt index b829106f..a406cefa 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/config/Config.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/config/Config.kt @@ -3,6 +3,7 @@ package com.superwall.sdk.models.config import com.superwall.sdk.models.SerializableEntity import com.superwall.sdk.models.paywall.Paywall import com.superwall.sdk.models.postback.PostbackRequest +import com.superwall.sdk.models.product.ProductItem import com.superwall.sdk.models.triggers.Trigger import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,6 +17,7 @@ data class Config( var postback: PostbackRequest, @SerialName("appSessionTimeoutMs") var appSessionTimeout: Long, @SerialName("toggles") var rawFeatureFlags: List, + @SerialName("products") val products: List, @SerialName("disablePreload") var preloadingDisabled: PreloadingDisabled, @SerialName("localization") var localizationConfig: LocalizationConfig, var requestId: String? = null, @@ -72,6 +74,7 @@ data class Config( preloadingDisabled = PreloadingDisabled.stub(), localizationConfig = LocalizationConfig(locales = emptyList()), buildId = "stub-build-id", + products = emptyList(), ) } } diff --git a/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt b/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt index 46de00a3..7ccac177 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/product/ProductItem.kt @@ -125,6 +125,7 @@ object PlayStoreProductSerializer : KSerializer { @Serializable(with = ProductItemSerializer::class) data class ProductItem( + // Note: This is used only by paywall as a reference to the object. Otherwise, it is empty. @SerialName("reference_name") val name: String, @SerialName("store_product") @@ -172,7 +173,7 @@ object ProductItemSerializer : KSerializer { val jsonObject = jsonInput.decodeJsonElement().jsonObject // Extract fields using the expected names during deserialization - val name = jsonObject["reference_name"]?.jsonPrimitive?.content ?: throw SerializationException("Missing reference_name") + val name = jsonObject["reference_name"]?.jsonPrimitive?.content ?: "" val storeProductJsonObject = jsonObject["store_product"]?.jsonObject ?: throw SerializationException("Missing store_product") diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt index 04528911..f7158158 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/CombinedExpressionEvaluator.kt @@ -1,16 +1,12 @@ package com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator -import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent -import com.superwall.sdk.dependencies.RuleAttributesFactory import com.superwall.sdk.models.events.EventData 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.paywall.presentation.rule_logic.cel.SuperscriptEvaluator -import com.superwall.sdk.paywall.presentation.rule_logic.javascript.JavascriptEvaluator import com.superwall.sdk.paywall.presentation.rule_logic.tryToMatchOccurrence import com.superwall.sdk.storage.LocalStorage -import org.json.JSONObject interface ExpressionEvaluating { suspend fun evaluateExpression( @@ -21,11 +17,7 @@ interface ExpressionEvaluating { internal class CombinedExpressionEvaluator( private val storage: LocalStorage, - private val factory: RuleAttributesFactory, - private val evaluator: JavascriptEvaluator, - private val shouldTraceResults: Boolean, private val superscriptEvaluator: SuperscriptEvaluator, - private val track: suspend (InternalSuperwallEvent.ExpressionResult) -> Unit, ) : ExpressionEvaluating { override suspend fun evaluateExpression( rule: TriggerRule, @@ -46,32 +38,4 @@ internal class CombinedExpressionEvaluator( } return celEvaluation } - - private suspend fun getBase64Params( - rule: TriggerRule, - eventData: EventData?, - ): String? { - val jsonAttributes = - factory.makeRuleAttributes(eventData, rule.computedPropertyRequests) - - rule.expressionJs?.let { expressionJs -> - JavascriptExpressionEvaluatorParams( - expressionJs, - JSONObject(jsonAttributes), - ).toBase64Input()?.let { base64Params -> - return "\n SuperwallSDKJS.evaluateJS64('$base64Params');" - } - } - - rule.expression?.let { expression -> - LiquidExpressionEvaluatorParams( - expression, - JSONObject(jsonAttributes), - ).toBase64Input()?.let { base64Params -> - return "\n SuperwallSDKJS.evaluate64('$base64Params');" - } - } - - return null - } } diff --git a/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt b/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt index eb719c0c..ba718ef6 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt @@ -2,10 +2,19 @@ package com.superwall.sdk.store import android.app.Activity import android.content.Context -import com.android.billingclient.api.* +import com.android.billingclient.api.AcknowledgePurchaseParams +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.QueryPurchasesParams import com.superwall.sdk.Superwall import com.superwall.sdk.billing.RECONNECT_TIMER_MAX_TIME_MILLISECONDS import com.superwall.sdk.billing.RECONNECT_TIMER_START_MILLISECONDS +import com.superwall.sdk.config.models.ConfigurationStatus import com.superwall.sdk.delegate.PurchaseResult import com.superwall.sdk.delegate.RestorationResult import com.superwall.sdk.delegate.subscription_controller.PurchaseController @@ -266,6 +275,8 @@ class AutomaticPurchaseController( //region Private private suspend fun syncSubscriptionStatusAndWait() { + // We await for configuration to be set so our entitlements are available + Superwall.instance.configurationStateListener.first { it is ConfigurationStatus.Configured } val subscriptionPurchases = queryPurchasesOfType(BillingClient.ProductType.SUBS) val inAppPurchases = queryPurchasesOfType(BillingClient.ProductType.INAPP) val allPurchases = subscriptionPurchases + inAppPurchases @@ -275,10 +286,13 @@ class AutomaticPurchaseController( val status: EntitlementStatus = if (hasActivePurchaseOrSubscription) { subscriptionPurchases - .flatMap { it.products } - .toSet() - .flatMap { entitlementsInfo.byProductId(it) } - .toSet() + .flatMap { + it.products + }.toSet() + .flatMap { + val res = entitlementsInfo.byProductId(it) + res + }.toSet() .let { entitlements -> if (entitlements.isNotEmpty()) { EntitlementStatus.Active(entitlements) diff --git a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt index cd5a0fa8..f8a8a929 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt @@ -1,5 +1,6 @@ package com.superwall.sdk.store +import com.superwall.sdk.billing.DecomposedProductIds import com.superwall.sdk.models.entitlements.Entitlement import com.superwall.sdk.models.entitlements.EntitlementStatus import com.superwall.sdk.storage.Storage @@ -59,6 +60,7 @@ class Entitlements( if (value.entitlements.isEmpty()) { setEntitlementStatus(EntitlementStatus.Inactive) } else { + _active.clear() _all.addAll(value.entitlements) _active.addAll(value.entitlements) _inactive.removeAll(value.entitlements) @@ -81,7 +83,23 @@ class Entitlements( } } - internal fun byProductId(id: String): Set = _entitlementsByProduct[id] ?: emptySet() + internal fun byProductId(id: String): Set { + val decomposedProductIds = DecomposedProductIds.from(id) + listOf( + decomposedProductIds.fullId, + "${decomposedProductIds.subscriptionId}:${decomposedProductIds.basePlanId}", + decomposedProductIds.subscriptionId, + ).forEach { id -> + _entitlementsByProduct.entries + .firstOrNull { it.key.contains(id) && it.value.isNotEmpty() } + .let { + if (it != null) { + return it.value + } + } + } + return emptySet() + } internal fun addEntitlementsByProductId(idToEntitlements: Map>) { _entitlementsByProduct.putAll( diff --git a/superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt b/superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt index bff5dc6c..3e69b76e 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/PurchasingObserverState.kt @@ -5,15 +5,29 @@ import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.Purchase sealed class PurchasingObserverState { + /** + * Tracks a beginning of a purchase flow for a product, equalt to Transaction Start event. + * @param product The product that is being purchased. + * */ class PurchaseWillBegin( - val productId: ProductDetails, + val product: ProductDetails, ) : PurchasingObserverState() + /** + * Tracks a successful purchase flow for a product, equal to Transaction Success event. + * @param result The result of the purchase flow. + * @param purchases The list of purchases that were made. + */ class PurchaseResult( val result: BillingResult, val purchases: List?, ) : PurchasingObserverState() + /** + * Tracks a failed purchase flow for a product, equal to Transaction Fail event. + * @param product The product that was being purchased. + * @param error The error that caused the purchase to fail. + */ class PurchaseError( val product: ProductDetails, val error: Throwable, diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index 434cc4d6..d8b155c8 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -2,6 +2,7 @@ package com.superwall.sdk.store.transactions import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.Purchase import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track @@ -44,9 +45,10 @@ import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch -import java.util.LinkedHashSet +import java.util.concurrent.ConcurrentHashMap class TransactionManager( private val storeManager: StoreManager, @@ -91,7 +93,8 @@ class TransactionManager( private var lastPaywallView: PaywallView? = null - private var transactionsInProgress: LinkedHashSet = LinkedHashSet() + private var transactionsInProgress: ConcurrentHashMap = + ConcurrentHashMap() private val shouldObserveTransactionFinishingAutomatically: Boolean get() = factory.makeSuperwallOptions().shouldObservePurchases @@ -102,12 +105,16 @@ class TransactionManager( ioScope.launch { storeKitManager.billing.purchaseResults .asSharedFlow() - .filterNotNull() + .dropWhile { + transactionsInProgress.isEmpty() + }.filterNotNull() .collectLatest { it: InternalPurchaseResult -> - val id = transactionsInProgress.last() val state = when (it) { is InternalPurchaseResult.Purchased -> { + it.purchase.products.forEach { + transactionsInProgress.remove(it) + } PurchasingObserverState.PurchaseResult( BillingResult .newBuilder() @@ -116,11 +123,29 @@ class TransactionManager( listOf(it.purchase), ) } - else -> + + is InternalPurchaseResult.Cancelled -> { + val last = transactionsInProgress.entries.last() + transactionsInProgress.remove(last.key) + PurchasingObserverState.PurchaseResult( + BillingResult + .newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.USER_CANCELED) + .build(), + emptyList(), + ) + } + + else -> { + val last = transactionsInProgress.entries.last() + transactionsInProgress.remove(last.key) PurchasingObserverState.PurchaseError( - error = (it as? InternalPurchaseResult.Failed)?.error ?: Throwable("Unknown error"), - product = storeKitManager.productsByFullId[id]?.rawStoreProduct!!.underlyingProductDetails, + error = + (it as? InternalPurchaseResult.Failed)?.error + ?: Throwable("Unknown error"), + product = last.value, ) + } } handle(it, state) } @@ -132,6 +157,12 @@ class TransactionManager( result: InternalPurchaseResult, state: PurchasingObserverState, ) { + if (transactionsInProgress.isEmpty()) { + return + } else { + transactionsInProgress.clear() + } + when (result) { is InternalPurchaseResult.Purchased -> { val state = state as PurchasingObserverState.PurchaseResult @@ -150,8 +181,8 @@ class TransactionManager( } InternalPurchaseResult.Cancelled -> { - val state = state as PurchasingObserverState.PurchaseError - val product = StoreProduct(RawStoreProduct.from(state.product)) + val lastProduct = transactionsInProgress.entries.last() + val product = StoreProduct(RawStoreProduct.from(lastProduct.value)) trackCancelled( product = product, purchaseSource = PurchaseSource.ObserverMode(product), @@ -219,7 +250,6 @@ class TransactionManager( is PurchaseSource.ObserverMode -> purchaseSource.product } - transactionsInProgress.add(product.fullIdentifier) val rawStoreProduct = product.rawStoreProduct log( message = @@ -231,7 +261,6 @@ class TransactionManager( ?: return PurchaseResult.Failed("Activity not found - required for starting the billing flow") prepareToPurchase(product, purchaseSource) - val result = storeManager.purchaseController.purchase( activity = activity, @@ -252,7 +281,6 @@ class TransactionManager( when (result) { is PurchaseResult.Purchased -> { didPurchase(product, purchaseSource, isEligibleForTrial && product.hasFreeTrial) - transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Restored -> { @@ -260,7 +288,6 @@ class TransactionManager( product = product, purchaseSource = purchaseSource, ) - transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Failed -> { @@ -270,18 +297,19 @@ class TransactionManager( val triggers = factory.makeTriggers() val transactionFailExists = triggers.contains(SuperwallEvents.TransactionFail.rawName) - if (shouldShowPurchaseFailureAlert && !transactionFailExists) { trackFailure( result.errorMessage, product, purchaseSource, ) - presentAlert( - Error(result.errorMessage), - product, - purchaseSource, - ) + if (purchaseSource is PurchaseSource.Internal) { + presentAlert( + Error(result.errorMessage), + product, + purchaseSource.paywallView, + ) + } } else { trackFailure( result.errorMessage, @@ -292,17 +320,14 @@ class TransactionManager( purchaseSource.paywallView.togglePaywallSpinner(isHidden = true) } } - transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Pending -> { handlePendingTransaction(purchaseSource) - transactionsInProgress.remove(product.fullIdentifier) } is PurchaseResult.Cancelled -> { trackCancelled(product, purchaseSource) - transactionsInProgress.remove(product.fullIdentifier) } } return result @@ -438,7 +463,6 @@ class TransactionManager( info = mapOf("paywall_vc" to source), ) } - transactionsInProgress.add(product.fullIdentifier) val paywallInfo = source.paywallView.info val trackedEvent = @@ -458,7 +482,12 @@ class TransactionManager( } is PurchaseSource.ExternalPurchase, is PurchaseSource.ObserverMode -> { - transactionsInProgress.add(product.fullIdentifier) + if (isObserved) { + transactionsInProgress.put( + product.fullIdentifier, + product.rawStoreProduct.underlyingProductDetails, + ) + } if (!storeManager.productsByFullId.contains(product.fullIdentifier)) { storeManager.productsByFullId[product.fullIdentifier] = product } @@ -747,75 +776,42 @@ class TransactionManager( private suspend fun presentAlert( error: Error, product: StoreProduct, - source: PurchaseSource, + paywallView: PaywallView, ) { - when (source) { - is PurchaseSource.Internal -> { - ioScope.launch { - log( - message = "Transaction Error", - info = - mapOf( - "product_id" to product.fullIdentifier, - "paywall_vc" to source.paywallView, - ), - error = error, - ) - } - - val paywallInfo = source.paywallView.info - - val trackedEvent = - InternalSuperwallEvent.Transaction( - InternalSuperwallEvent.Transaction.State.Fail( - TransactionError.Failure( - error.message ?: "", - product, - ), - ), - paywallInfo, - product, - null, - source = TransactionSource.INTERNAL, - isObserved = factory.makeSuperwallOptions().shouldObservePurchases, - ) - track(trackedEvent) - - source.paywallView.showAlert( - "An error occurred", - error.message ?: "Unknown error", - ) - } + ioScope.launch { + log( + message = "Transaction Error", + info = + mapOf( + "product_id" to product.fullIdentifier, + "paywall_vc" to paywallView, + ), + error = error, + ) + } - is PurchaseSource.ExternalPurchase -> { - ioScope.launch { - log( - message = "Transaction Error", - error = error, - ) - } + val paywallInfo = paywallView.info - val trackedEvent = - InternalSuperwallEvent.Transaction( - InternalSuperwallEvent.Transaction.State.Fail( - TransactionError.Failure( - error.message ?: "", - product, - ), - ), - PaywallInfo.empty(), + val trackedEvent = + InternalSuperwallEvent.Transaction( + InternalSuperwallEvent.Transaction.State.Fail( + TransactionError.Failure( + error.message ?: "", product, - null, - source = TransactionSource.EXTERNAL, - isObserved = factory.makeSuperwallOptions().shouldObservePurchases, - ) - track(trackedEvent) - } + ), + ), + paywallInfo, + product, + null, + source = TransactionSource.INTERNAL, + isObserved = factory.makeSuperwallOptions().shouldObservePurchases, + ) + track(trackedEvent) - is PurchaseSource.ObserverMode -> { - // No-op - } - } + paywallView.showAlert( + "An error occurred", + error.message ?: "Unknown error", + ) } private suspend fun trackTransactionDidSucceed( @@ -843,7 +839,7 @@ class TransactionManager( product, transaction, source = TransactionSource.INTERNAL, - isObserved = isObserved, + isObserved = false, ) track(trackedEvent) eventsQueue.flushInternal() diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt index a1c2dc50..c7f455ba 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt @@ -51,7 +51,7 @@ class DeviceTemplateTest { utcDateTime = "2024-03-20T10:00:00", localDateTime = "2024-03-20T02:00:00", isSandbox = "true", - activeEntitlements = listOf("active"), + activeEntitlements = listOf(mapOf("identifier" to "active")), isFirstAppOpen = false, sdkVersion = "1.0.0", sdkVersionPadded = "001.000.000", diff --git a/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt index 26fbd8b7..f9d13435 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/StoreManagerTest.kt @@ -62,7 +62,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), - entitlements = entitlementsBasic.map { it.id }.toSet(), + entitlements = entitlementsBasic.toSet(), ), ProductItem( "Item2", @@ -75,7 +75,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), - entitlements = entitlementsBasic.map { it.id }.toSet(), + entitlements = entitlementsBasic.toSet(), ), ), ) @@ -127,7 +127,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), - entitlements = entitlementsBasic.map { it.id }.toSet(), + entitlements = entitlementsBasic.toSet(), ), ProductItem( "Item2", @@ -140,7 +140,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), - entitlements = entitlementsBasic.map { it.id }.toSet(), + entitlements = entitlementsBasic.toSet(), ), ), ) @@ -198,7 +198,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), - entitlements = entitlementsBasic.map { it.id }.toSet(), + entitlements = entitlementsBasic.toSet(), ), ProductItem( "Item2", @@ -211,7 +211,7 @@ class StoreManagerTest { offer = Offer.Automatic(), ), ), - entitlements = entitlementsBasic.map { it.id }.toSet(), + entitlements = entitlementsBasic.toSet(), ), ), ) diff --git a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt index b2525534..b3549aed 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/transactions/TransactionManagerTest.kt @@ -79,7 +79,7 @@ class TransactionManagerTest { offer = Offer.Automatic(), ), ), - entitlements = entitlements.map { it.id }.toSet(), + entitlements = entitlements.toSet(), ), ) From 31268fdb5598d7ad0bbfc01ac92615f36c2617c1 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 30 Sep 2024 18:14:19 +0200 Subject: [PATCH 25/37] Add sync version of methods, update KDoc and hide internals --- .../sdk/analytics/internal/Tracking.kt | 25 ++++++++++ .../paywall/PaywallPresentationStyle.kt | 2 +- .../presentation/PublicPresentation.kt | 48 +++++++++++++++++++ .../get_paywall/InternalGetPaywall.kt | 37 ++++++++++++++ .../PublicGetPresentationResult.kt | 40 ++++++++++++++++ .../internal/GetPaywallComponents.kt | 19 ++++++++ .../internal/InternalPresentation.kt | 11 ++++- .../operators/CheckDebuggerPresentation.kt | 6 +++ .../CheckNoPaywallAlreadyPresented.kt | 2 +- .../internal/operators/EvaluateRules.kt | 7 +++ .../internal/operators/GetExperiment.kt | 2 +- .../internal/operators/GetPresenter.kt | 15 +++++- .../internal/operators/LogErrors.kt | 2 +- .../internal/operators/PresentPaywall.kt | 32 +++++++++++++ 14 files changed, 240 insertions(+), 8 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt index 94391a69..bdc2a581 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/Tracking.kt @@ -24,9 +24,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.util.Date +/** + * Tracks an analytical event by sending it to the server and, for internal Superwall events, the delegate. + * + * @param event The event you want to track. + * @return TrackingResult The result of the tracking operation. + */ suspend fun Superwall.track(event: Trackable): Result { return withErrorTracking { // Wait for the SDK to be fully initialized @@ -106,6 +113,24 @@ suspend fun Superwall.track(event: Trackable): Result { }.toResult() } +/** + * Tracks an analytical event synchronously. + * Warning: This blocks the calling thread. + * @param event The event you want to track. + * @return TrackingResult The result of the tracking operation. + */ +fun Superwall.trackSync(event: Trackable): Result = + runBlocking { + track(event) + } + +/** + * Attempts to implicitly trigger a paywall for a given analytical event. + * + * @param event The tracked event. + * @param eventData The event data that could trigger a paywall. + */ + suspend fun Superwall.handleImplicitTrigger( event: Trackable, eventData: EventData, diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt index ce6b7eac..234670b7 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt @@ -14,7 +14,7 @@ enum class PaywallPresentationStyle( FULLSCREEN("FULLSCREEN"), @SerialName("NO_ANIMATION") - FULLSCREEN_NO_ANIMATION("NO_ANIMATION"), + FULLSCREEN_NO_ANIMATION("FULLSCREEN_NO_ANIMATION"), @SerialName("PUSH") PUSH("PUSH"), diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PublicPresentation.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PublicPresentation.kt index 6bc25e23..d45437cd 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PublicPresentation.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PublicPresentation.kt @@ -23,8 +23,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +/** + * Dismisses the presented paywall, if one exists. + */ suspend fun Superwall.dismiss() = withContext(Dispatchers.Main) { val completionSignal = CompletableDeferred() @@ -40,6 +44,19 @@ suspend fun Superwall.dismiss() = completionSignal.await() } +/** + * Dismisses the presented paywall synchronously, if one exists. + * Warning: This blocks the calling thread. + */ +fun Superwall.dismissSync() { + runBlocking { + dismiss() + } +} + +/** + * Dismisses the presented paywall, if it exists, in order to present a different one. + */ suspend fun Superwall.dismissForNextPaywall() = withContext(Dispatchers.Main) { val completionSignal = CompletableDeferred() @@ -58,6 +75,37 @@ suspend fun Superwall.dismissForNextPaywall() = completionSignal.await() } +/** + * Dismisses the presented paywall synchronously, if it exists, in order to present a different one. + * Warning: This blocks the calling thread. + */ +fun Superwall.dismissSyncForNextPaywall() = + runBlocking { + dismissForNextPaywall() + } + +/** + * Registers an event to access a feature. When the event is added to a campaign on the Superwall dashboard, it can show a paywall. + * + * This shows a paywall to the user when: An event you provide is added to a campaign on the [Superwall Dashboard](https://superwall.com/dashboard); + * the user matches a rule in the campaign; and the user doesn't have an active subscription. + * + * Before using this method, you'll first need to create a campaign and add the event to the campaign on the [Superwall Dashboard](https://superwall.com/dashboard). + * + * The paywall shown to the user is determined by the rules defined in the campaign. When a user is assigned a paywall within a rule, + * they will continue to see that paywall unless you remove the paywall from the rule or reset assignments to the paywall. + * + * @param event The name of the event you wish to register. + * @param params Optional parameters you'd like to pass with your event. These can be referenced within the rules of your campaign. + * Keys beginning with `$` are reserved for Superwall and will be dropped. Values can be any JSON encodable value, URLs or Dates. + * Arrays and dictionaries as values are not supported at this time, and will be dropped. Defaults to `null`. + * @param handler An optional handler whose functions provide status updates for a paywall. Defaults to `null`. + * @param feature A completion block containing a feature that you wish to paywall. Access to this block is remotely configurable via the + * [Superwall Dashboard](https://superwall.com/dashboard). If the paywall is set to _Non Gated_, this will be called when + * the paywall is dismissed or if the user is already paying. If the paywall is _Gated_, this will be called only if the user + * is already paying or if they begin paying. If no paywall is configured, this gets called immediately. This will not be called + * in the event of an error, which you can detect via the `handler`. + */ fun Superwall.register( event: String, params: Map? = null, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt index fd4a51af..e70317c3 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt @@ -10,6 +10,9 @@ import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.paywall.presentation.rule_logic.RuleEvaluationOutcome import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking data class PaywallComponents( val view: PaywallView, @@ -18,6 +21,14 @@ data class PaywallComponents( val debugInfo: Map, ) +/** + * Gets a paywall to present, publishing [PaywallState] objects that provide updates on the lifecycle of the paywall. + * + * @param request A presentation request of type [PresentationRequest] to feed into a presentation pipeline. + * @param publisher A [MutableSharedFlow] that emits [PaywallState] objects. + * @return A [PaywallView] to present. + * @throws Throwable if an error occurs during the process. + */ @Throws(Throwable::class) internal suspend fun Superwall.getPaywall( request: PresentationRequest, @@ -34,3 +45,29 @@ internal suspend fun Superwall.getPaywall( logErrors(request, error = it) Either.Failure(it) }) + +/** + * Gets a paywall to present synchronously, providing updates on the lifecycle of the paywall through a callback. + * Warning: This blocks the calling thread until the paywall is returned. + * + * @param request A presentation request of type [PresentationRequest] to feed into a presentation pipeline. + * @param onStateChanged A callback function that receives [PaywallState] updates. + * @return A [PaywallView] to present. + * @throws Throwable if an error occurs during the process. + */ +@Throws(Throwable::class) +fun Superwall.getPaywallSync( + request: PresentationRequest, + onStateChanged: (PaywallState) -> Unit = {}, +): PaywallView { + val scope = Superwall.instance.ioScope + val publisher = MutableSharedFlow() + scope.launch { + publisher.collectLatest { + onStateChanged(it) + } + } + return runBlocking { + getPaywall(request, publisher) + } +} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/PublicGetPresentationResult.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/PublicGetPresentationResult.kt index 87115c57..cacf2a3e 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/PublicGetPresentationResult.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_presentation_result/PublicGetPresentationResult.kt @@ -10,9 +10,23 @@ import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.presentation.result.PresentationResult import com.superwall.sdk.utilities.withErrorTracking +import kotlinx.coroutines.runBlocking import java.util.Date import java.util.HashMap +/** + * Preemptively gets the result of registering an event. + * + * This helps you determine whether a particular event will present a paywall + * in the future. + * + * Note that this method does not present a paywall. To do that, use + * `register(event:params:handler:feature:)`. + * + * @param event The name of the event you want to register. + * @param params Optional parameters you'd like to pass with your event. + * @return A [PresentationResult] that indicates the result of registering an event. + */ suspend fun Superwall.getPresentationResult( event: String, params: Map? = null, @@ -32,6 +46,32 @@ suspend fun Superwall.getPresentationResult( ) }.toResult() +/** + * Synchronously preemptively gets the result of registering an event. + * + * This helps you determine whether a particular event will present a paywall + * in the future. + * + * Note that this method does not present a paywall. To do that, use + * `register(event:params:handler:feature:)`. + * + * Warning: This blocks the calling thread. + * + * @param event The name of the event you want to register. + * @param params Optional parameters you'd like to pass with your event. + * @return A [PresentationResult] that indicates the result of registering an event. + */ +fun Superwall.getPresentationResultSync( + event: String, + params: Map? = null, +): Result = + runBlocking { + getPresentationResult( + event, + params, + ) + } + internal suspend fun Superwall.internallyGetPresentationResult( event: Trackable, isImplicit: Boolean, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt index ecf93394..077c3e91 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt @@ -15,6 +15,7 @@ import com.superwall.sdk.paywall.presentation.internal.operators.waitForEntitlem import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.utilities.withErrorTracking import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.runBlocking /** * Runs a pipeline of operations to get a paywall to present and associated components. @@ -81,3 +82,21 @@ internal suspend fun Superwall.confirmAssignment(request: PresentationRequest): } } } + +/** + * Synchronously runs a pipeline of operations to get a paywall to present and associated components. + * + * @param request The presentation request. + * @param publisher A `MutableStateFlow` that gets sent `PaywallState` objects. + * @return A `PaywallComponents` object that contains objects associated with the + * paywall view controller. + * @throws PresentationPipelineError object associated with stages of the pipeline. + */ + +fun Superwall.getPaywallComponentsSync( + request: PresentationRequest, + publisher: MutableSharedFlow? = null, +): PaywallComponents = + runBlocking { + getPaywallComponents(request, publisher) + } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt index 21bbb32f..8b2749e6 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/InternalPresentation.kt @@ -13,8 +13,15 @@ import kotlinx.coroutines.withContext typealias PaywallStatePublisher = Flow +/** + * Runs a background task to present a paywall, publishing [PaywallState] objects that provide updates on the lifecycle of the paywall. + * + * @param request A presentation request of type [PresentationRequest] to feed into a presentation pipeline. + * @param publisher A publisher fed into the pipeline that sends state updates. + */ + @Throws(Throwable::class) -suspend fun Superwall.internallyPresent( +internal suspend fun Superwall.internallyPresent( request: PresentationRequest, publisher: MutableSharedFlow, ) { @@ -48,7 +55,7 @@ suspend fun Superwall.internallyPresent( } } -suspend fun Superwall.dismiss( +internal suspend fun Superwall.dismiss( paywallView: PaywallView, result: PaywallResult, closeReason: PaywallCloseReason = PaywallCloseReason.SystemLogic, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckDebuggerPresentation.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckDebuggerPresentation.kt index 398c9a8c..ec63b4a1 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckDebuggerPresentation.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckDebuggerPresentation.kt @@ -10,6 +10,12 @@ import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import kotlinx.coroutines.flow.MutableSharedFlow +/** + * Cancels the state publisher if the debugger is already launched. + * + * @param request The presentation request. + * @param paywallStatePublisher A [MutableSharedFlow] that gets sent [PaywallState] objects. + */ suspend fun Superwall.checkDebuggerPresentation( request: PresentationRequest, paywallStatePublisher: MutableSharedFlow?, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckNoPaywallAlreadyPresented.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckNoPaywallAlreadyPresented.kt index b9240db5..1ce2f4e5 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckNoPaywallAlreadyPresented.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/CheckNoPaywallAlreadyPresented.kt @@ -10,7 +10,7 @@ import com.superwall.sdk.paywall.presentation.internal.PresentationRequest import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import kotlinx.coroutines.flow.MutableSharedFlow -suspend fun Superwall.checkNoPaywallAlreadyPresented( +internal suspend fun Superwall.checkNoPaywallAlreadyPresented( request: PresentationRequest, paywallStatePublisher: MutableSharedFlow, ) { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/EvaluateRules.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/EvaluateRules.kt index b40e4660..92c37b16 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/EvaluateRules.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/EvaluateRules.kt @@ -18,6 +18,13 @@ data class AssignmentPipelineOutput( val debugInfo: Map, ) +/** + * Evaluates the rules from the campaign that the event belongs to. + * + * @param request The presentation request + * @return A [RuleEvaluationOutcome] object containing the trigger result, + * confirmable assignment, and unsaved occurrence. + */ suspend fun Superwall.evaluateRules(request: PresentationRequest): Result { val eventData = request.presentationInfo.eventData diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt index 9aad0b0c..cb2643a3 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetExperiment.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow * @return A data class that contains info for the next operation. */ @Throws(Throwable::class) -suspend fun Superwall.getExperiment( +internal suspend fun Superwall.getExperiment( request: PresentationRequest, rulesOutcome: RuleEvaluationOutcome, debugInfo: Map, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt index 82a8bc50..2aaaaf3a 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/GetPresenter.kt @@ -18,7 +18,18 @@ import com.superwall.sdk.paywall.presentation.rule_logic.RuleEvaluationOutcome import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.flow.MutableSharedFlow -suspend fun Superwall.getPresenterIfNecessary( +/** + * Checks conditions for whether the paywall can present before accessing a window on + * which the paywall can present. + * + * @param paywallView The [PaywallView] to present. + * @param rulesOutcome The output from evaluating rules. + * @param request The presentation request. + * @param paywallStatePublisher A [MutableSharedFlow] that gets sent [PaywallState] objects. + * + * @return An [Activity] to present on, or null if presentation is not necessary. + */ +internal suspend fun Superwall.getPresenterIfNecessary( paywallView: PaywallView, rulesOutcome: RuleEvaluationOutcome, request: PresentationRequest, @@ -71,7 +82,7 @@ suspend fun Superwall.getPresenterIfNecessary( return currentActivity } -suspend fun Superwall.attemptTriggerFire( +internal suspend fun Superwall.attemptTriggerFire( request: PresentationRequest, triggerResult: InternalTriggerResult, ) { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/LogErrors.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/LogErrors.kt index ddbf13fa..61503c55 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/LogErrors.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/LogErrors.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -suspend fun Superwall.logErrors( +internal suspend fun Superwall.logErrors( request: PresentationRequest, error: Throwable, ) { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt index 19ea8772..dd87c006 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt @@ -16,6 +16,7 @@ import com.superwall.sdk.paywall.presentation.internal.state.PaywallState import com.superwall.sdk.paywall.view.PaywallView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -88,3 +89,34 @@ suspend fun Superwall.presentPaywallView( throw error } } + +/** + * A synchronous version of `presentPaywallView` which will invoke a callback with the paywall state. + * Warning: This blocks the calling thread. + **/ + +fun Superwall.presentPaywallViewSync( + paywallView: PaywallView, + presenter: Activity, + unsavedOccurrence: TriggerRuleOccurrence?, + debugInfo: Map, + request: PresentationRequest, + onStateChanged: (PaywallState) -> Unit, +) { + mainScope.launch { + val publisher = MutableSharedFlow() + ioScope.launch { + publisher.collectLatest { + onStateChanged(it) + } + } + presentPaywallView( + paywallView = paywallView, + presenter = presenter, + unsavedOccurrence = unsavedOccurrence, + debugInfo = debugInfo, + request = request, + paywallStatePublisher = publisher, + ) + } +} \ No newline at end of file From c3c05e6fffa254afec03db2bbedc0ec4a9f44a34 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Fri, 13 Dec 2024 17:07:22 +0100 Subject: [PATCH 26/37] Change active entitlements to list --- .../main/java/com/superwall/sdk/network/device/DeviceHelper.kt | 2 +- .../paywall/view/webview/templating/models/DeviceTemplate.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index a01731fb..1f7f1833 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -480,7 +480,7 @@ class DeviceHelper( isSandbox = isSandbox.toString(), activeEntitlements = Superwall.instance.entitlements.active - .map { mapOf("identifier" to it.id) }, + .map { it.id }, isFirstAppOpen = isFirstAppOpen, sdkVersion = sdkVersion, sdkVersionPadded = sdkVersionPadded, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt index 22e82e12..688d79f4 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt @@ -44,7 +44,7 @@ data class DeviceTemplate( val utcDateTime: String, val localDateTime: String, val isSandbox: String, - val activeEntitlements: List>, + val activeEntitlements: List, val isFirstAppOpen: Boolean, val sdkVersion: String, val sdkVersionPadded: String, From ec927e77824f0449dd557c552b195d687a20b7c7 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 16 Dec 2024 10:33:45 +0100 Subject: [PATCH 27/37] Fix issues after rebase, update notifications to include subtitle --- .../com/superwall/superapp/test/UITestHandler.kt | 2 -- .../sdk/config/ConfigManagerInstrumentedTest.kt | 2 +- .../src/main/java/com/superwall/sdk/Superwall.kt | 6 +++--- .../com/superwall/sdk/config/ConfigManager.kt | 2 +- .../sdk/dependencies/DependencyContainer.kt | 9 ++++++--- .../sdk/models/paywall/LocalNotification.kt | 2 ++ .../models/paywall/PaywallPresentationStyle.kt | 2 +- .../sdk/paywall/presentation/PaywallInfo.kt | 2 -- .../get_paywall/InternalGetPaywall.kt | 4 +--- .../internal/GetPaywallComponents.kt | 2 +- .../internal/operators/PresentPaywall.kt | 2 +- .../sdk/store/transactions/TransactionManager.kt | 16 ++++++++-------- .../notifications/NotificationScheduler.kt | 1 + .../notifications/NotificationWorker.kt | 6 +++++- .../com/superwall/sdk/utilities/ErrorTracking.kt | 1 + 15 files changed, 32 insertions(+), 27 deletions(-) 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 0fa6c08d..fa4d8e41 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -19,8 +19,6 @@ import com.superwall.sdk.paywall.presentation.get_presentation_result.getPresent import com.superwall.sdk.paywall.presentation.register import com.superwall.sdk.paywall.presentation.result.PresentationResult import com.superwall.sdk.paywall.view.SuperwallPaywallActivity -import com.superwall.sdk.store.PurchasingObserverState -import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.view.fatalAssert import com.superwall.superapp.ComposeActivity import kotlinx.coroutines.CoroutineScope 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 4509b90b..b0099643 100644 --- a/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt +++ b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt @@ -36,10 +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.StoreManager 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 diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 8ab4c172..fb1f80d5 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -44,8 +44,6 @@ 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.storage.StoredEntitlementStatus -import com.superwall.sdk.store.Entitlements import com.superwall.sdk.paywall.view.PaywallView import com.superwall.sdk.paywall.view.SuperwallPaywallActivity import com.superwall.sdk.paywall.view.delegate.PaywallViewEventCallback @@ -57,6 +55,8 @@ import com.superwall.sdk.paywall.view.webview.messaging.PaywallWebEvent.Initiate 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 @@ -786,7 +786,7 @@ class Superwall( suspend fun getProducts(vararg productIds: String): Result> = withErrorTracking { - dependencyContainer.storeKitManager.getProductsWithoutPaywall(productIds.toList()) + dependencyContainer.storeManager.getProductsWithoutPaywall(productIds.toList()) }.toResult() /** diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt index 54743a86..c36ef3a1 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt @@ -33,8 +33,8 @@ import com.superwall.sdk.storage.DisableVerboseEvents import com.superwall.sdk.storage.LatestConfig import com.superwall.sdk.storage.LatestGeoInfo import com.superwall.sdk.storage.Storage -import com.superwall.sdk.store.StoreManager import com.superwall.sdk.store.Entitlements +import com.superwall.sdk.store.StoreManager import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index e16c4a5a..3e485153 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -200,12 +200,15 @@ class DependencyContainer( appLifecycleObserver = appLifecycleObserver, this, ) - storage = LocalStorage(context = context, ioScope = ioScope(), factory = this, json = json()) + storage = + LocalStorage(context = context, ioScope = ioScope(), factory = this, json = json()) entitlements = Entitlements(storage) var purchaseController = InternalPurchaseController( - kotlinPurchaseController = purchaseController ?: AutomaticPurchaseController(context, ioScope, entitlements), + kotlinPurchaseController = + purchaseController + ?: AutomaticPurchaseController(context, ioScope, entitlements), javaPurchaseController = null, context, ) @@ -520,7 +523,7 @@ class DependencyContainer( override fun makeHasExternalPurchaseController(): Boolean = storeManager.purchaseController.hasExternalPurchaseController - override fun makeHasInternalPurchaseController(): Boolean = storeKitManager.purchaseController.hasInternalPurchaseController + override fun makeHasInternalPurchaseController(): Boolean = storeManager.purchaseController.hasInternalPurchaseController override suspend fun didUpdateAppSession(appSession: AppSession) { } diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/LocalNotification.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/LocalNotification.kt index f0e978aa..69fe01d6 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/LocalNotification.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/LocalNotification.kt @@ -16,6 +16,8 @@ class LocalNotification( val type: LocalNotificationType, @SerialName("title") val title: String, + @SerialName("subtitle") + val subtitle: String? = null, @SerialName("body") val body: String, @SerialName("delay") diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt index 234670b7..ce6b7eac 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallPresentationStyle.kt @@ -14,7 +14,7 @@ enum class PaywallPresentationStyle( FULLSCREEN("FULLSCREEN"), @SerialName("NO_ANIMATION") - FULLSCREEN_NO_ANIMATION("FULLSCREEN_NO_ANIMATION"), + FULLSCREEN_NO_ANIMATION("NO_ANIMATION"), @SerialName("PUSH") PUSH("PUSH"), diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt index 739a99d1..7b16ac0f 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/PaywallInfo.kt @@ -325,8 +325,6 @@ data class PaywallInfo( buildId = "", cacheKey = "", isScrollEnabled = true, - shimmerLoadCompleteTime = null, - shimmerLoadStartTime = null, ) } } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt index e70317c3..c0982770 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/get_paywall/InternalGetPaywall.kt @@ -53,13 +53,11 @@ internal suspend fun Superwall.getPaywall( * @param request A presentation request of type [PresentationRequest] to feed into a presentation pipeline. * @param onStateChanged A callback function that receives [PaywallState] updates. * @return A [PaywallView] to present. - * @throws Throwable if an error occurs during the process. */ -@Throws(Throwable::class) fun Superwall.getPaywallSync( request: PresentationRequest, onStateChanged: (PaywallState) -> Unit = {}, -): PaywallView { +): Either { val scope = Superwall.instance.ioScope val publisher = MutableSharedFlow() scope.launch { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt index 077c3e91..7acbab70 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/GetPaywallComponents.kt @@ -96,7 +96,7 @@ internal suspend fun Superwall.confirmAssignment(request: PresentationRequest): fun Superwall.getPaywallComponentsSync( request: PresentationRequest, publisher: MutableSharedFlow? = null, -): PaywallComponents = +): Result = runBlocking { getPaywallComponents(request, publisher) } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt index dd87c006..5cec969d 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/PresentPaywall.kt @@ -119,4 +119,4 @@ fun Superwall.presentPaywallViewSync( paywallStatePublisher = publisher, ) } -} \ No newline at end of file +} diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index d8b155c8..566bff05 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -103,7 +103,7 @@ class TransactionManager( if (shouldObserveTransactionFinishingAutomatically ) { ioScope.launch { - storeKitManager.billing.purchaseResults + storeManager.billing.purchaseResults .asSharedFlow() .dropWhile { transactionsInProgress.isEmpty() @@ -572,13 +572,13 @@ class TransactionManager( factory = factory, ) - if (purchase != null) { - factory.makeStoreTransaction(purchase) - } else { - transactionVerifier.getLatestTransaction( - factory = factory, - ) - } + if (purchase != null) { + factory.makeStoreTransaction(purchase) + } else { + transactionVerifier.getLatestTransaction( + factory = factory, + ) + } storeManager.loadPurchasedProducts() trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial) diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt index 75d5b1b3..fa92025d 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationScheduler.kt @@ -27,6 +27,7 @@ internal class NotificationScheduler { "id" to notification.id, "title" to notification.title, "body" to notification.body, + "subtitle" to notification.subtitle, ) var delay = notification.delay // delay in milliseconds diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt index 3ab9cbde..699baa3c 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/notifications/NotificationWorker.kt @@ -24,7 +24,11 @@ internal class NotificationWorker( .Builder(applicationContext, SuperwallPaywallActivity.NOTIFICATION_CHANNEL_ID) .setSmallIcon(context.applicationInfo.icon) .setContentTitle(title) - .setContentText(text) + .let { + inputData.getString("subtitle")?.let { subtitle -> + it.setSubText(subtitle) + } ?: it + }.setContentText(text) .setPriority(NotificationCompat.PRIORITY_HIGH) with(NotificationManagerCompat.from(applicationContext)) { diff --git a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt index e78d2fb9..d6a01109 100644 --- a/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt +++ b/superwall/src/main/java/com/superwall/sdk/utilities/ErrorTracking.kt @@ -80,6 +80,7 @@ internal class ErrorTracker( "java.lang", "net.java.dev.jna", "kotlin.", + "kotlinx.", "android.os", "androidx.os", "com.android.", From 0d708254b5047158e39bcb5d1e13be6bd0112816 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 16 Dec 2024 10:37:11 +0100 Subject: [PATCH 28/37] Bump version --- superwall/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superwall/build.gradle.kts b/superwall/build.gradle.kts index 9f3d6144..230ecc1a 100644 --- a/superwall/build.gradle.kts +++ b/superwall/build.gradle.kts @@ -23,7 +23,7 @@ plugins { id("signing") } -version = "1.5.0" +version = "2.0.0-alpha.1" android { compileSdk = 34 From 0b5015f7b4daa29d4c5960c8595c38367f10663b Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 16 Dec 2024 14:26:29 +0100 Subject: [PATCH 29/37] Remove JS evaluator --- gradle/libs.versions.toml | 3 - superwall/build.gradle.kts | 1 - ...pressionEvaluatorParamsInstrumentedTest.kt | 155 ------- ...inedExpressionEvaluatorInstrumentedTest.kt | 424 ------------------ .../DefaultJavascriptEvaluatorTest.kt | 74 --- .../com/superwall/sdk/config/ConfigManager.kt | 4 +- .../superwall/sdk/config/PaywallPreload.kt | 4 +- .../sdk/dependencies/DependencyContainer.kt | 4 +- .../ExpressionEvaluatorParams.kt | 54 --- .../javascript/DefaultJavascriptEvalutor.kt | 140 ------ .../javascript/NoSupportedEvaluator.kt | 27 -- ...avascriptEvaluator.kt => RuleEvaluator.kt} | 11 +- .../javascript/SandboxJavascriptEvaluator.kt | 55 --- .../javascript/WebviewJavascriptEvaluator.kt | 69 --- .../ExpressionEvaluatorParamsTest.kt | 153 ------- 15 files changed, 5 insertions(+), 1173 deletions(-) delete mode 100644 superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsInstrumentedTest.kt delete mode 100644 superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/JavascriptCombinedExpressionEvaluatorInstrumentedTest.kt delete mode 100644 superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvaluatorTest.kt delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/NoSupportedEvaluator.kt rename superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/{JavascriptEvaluator.kt => RuleEvaluator.kt} (53%) delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/SandboxJavascriptEvaluator.kt delete mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/WebviewJavascriptEvaluator.kt delete mode 100644 superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe662d66..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" @@ -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" } diff --git a/superwall/build.gradle.kts b/superwall/build.gradle.kts index 230ecc1a..cc5143fd 100644 --- a/superwall/build.gradle.kts +++ b/superwall/build.gradle.kts @@ -182,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) 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 c404358f..00000000 --- a/superwall/src/androidTest/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/JavascriptCombinedExpressionEvaluatorInstrumentedTest.kt +++ /dev/null @@ -1,424 +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, - ), - shouldTraceResults = false, - ) - - 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, - ), - shouldTraceResults = false, - ) - - 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, - ), - shouldTraceResults = false, - ) - - 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, - ), - shouldTraceResults = false, - ) - - 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 f1028930..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(), coroutineScope = this) - 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/main/java/com/superwall/sdk/config/ConfigManager.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt index c36ef3a1..01f40214 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt @@ -28,7 +28,6 @@ import com.superwall.sdk.network.SuperwallAPI import com.superwall.sdk.network.awaitUntilNetworkExists import com.superwall.sdk.network.device.DeviceHelper import com.superwall.sdk.paywall.manager.PaywallManager -import com.superwall.sdk.paywall.presentation.rule_logic.javascript.JavascriptEvaluator import com.superwall.sdk.storage.DisableVerboseEvents import com.superwall.sdk.storage.LatestConfig import com.superwall.sdk.storage.LatestGeoInfo @@ -68,8 +67,7 @@ open class ConfigManager( RequestFactory, DeviceInfoFactory, RuleAttributesFactory, - DeviceHelperFactory, - JavascriptEvaluator.Factory + DeviceHelperFactory // The configuration of the Superwall dashboard internal val configState = MutableStateFlow(ConfigState.None) diff --git a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt index 145eb5ff..fbf0c205 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt @@ -10,7 +10,6 @@ import com.superwall.sdk.models.paywall.CacheKey import com.superwall.sdk.models.paywall.PaywallIdentifier import com.superwall.sdk.models.triggers.Trigger import com.superwall.sdk.paywall.manager.PaywallManager -import com.superwall.sdk.paywall.presentation.rule_logic.javascript.JavascriptEvaluator import com.superwall.sdk.paywall.request.ResponseIdentifiers import com.superwall.sdk.paywall.view.webview.webViewExists import com.superwall.sdk.storage.LocalStorage @@ -28,8 +27,7 @@ class PaywallPreload( ) { interface Factory : RequestFactory, - RuleAttributesFactory, - JavascriptEvaluator.Factory + RuleAttributesFactory private var currentPreloadingTask: Job? = null diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index 3e485153..c2534142 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -57,7 +57,7 @@ import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.presentation.rule_logic.cel.SuperscriptEvaluator import com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator.CombinedExpressionEvaluator import com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator.ExpressionEvaluating -import com.superwall.sdk.paywall.presentation.rule_logic.javascript.JavascriptEvaluator +import com.superwall.sdk.paywall.presentation.rule_logic.javascript.RuleEvaluator import com.superwall.sdk.paywall.request.PaywallRequest import com.superwall.sdk.paywall.request.PaywallRequestManager import com.superwall.sdk.paywall.request.PaywallRequestManagerDepFactory @@ -119,7 +119,7 @@ class DependencyContainer( ConfigManager.Factory, AppSessionManager.Factory, DebugView.Factory, - JavascriptEvaluator.Factory, + RuleEvaluator.Factory, JsonFactory, ConfigAttributesFactory, PaywallPreload.Factory, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt deleted file mode 100644 index a71043a1..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParams.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator - -import android.util.Base64 -import com.superwall.sdk.logger.LogLevel -import com.superwall.sdk.logger.LogScope -import com.superwall.sdk.logger.Logger -import kotlinx.serialization.SerializationException -import org.json.JSONObject - -data class LiquidExpressionEvaluatorParams( - val expression: String, - val values: JSONObject, -) { - fun toJson(): String { - var obj = JSONObject() - obj.put("expression", expression) - obj.put("values", values) - return obj.toString() - } - - fun toBase64Input(): String? = - try { - val jsonString = toJson() - Logger.debug( - LogLevel.debug, - LogScope.all, - "!! jsonString: $jsonString", - ) - jsonString.encodeToByteArray().toBase64() - } catch (e: SerializationException) { - null - } -} - -data class JavascriptExpressionEvaluatorParams( - val expressionJs: String, - val values: JSONObject, -) { - fun toJson(): String { - var obj = JSONObject() - obj.put("expressionJS", expressionJs) - obj.put("values", values) - return obj.toString() - } - - fun toBase64Input(): String? = - try { - toJson().encodeToByteArray().toBase64() - } catch (e: SerializationException) { - null - } -} - -fun ByteArray.toBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt deleted file mode 100644 index 4d49d42e..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/DefaultJavascriptEvalutor.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.superwall.sdk.paywall.presentation.rule_logic.javascript - -import android.content.Context -import android.webkit.WebView -import androidx.javascriptengine.JavaScriptSandbox -import androidx.javascriptengine.SandboxDeadException -import com.superwall.sdk.logger.LogLevel -import com.superwall.sdk.logger.LogScope -import com.superwall.sdk.logger.Logger -import com.superwall.sdk.misc.IOScope -import com.superwall.sdk.misc.MainScope -import com.superwall.sdk.misc.asEither -import com.superwall.sdk.misc.toResult -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.paywall.view.webview.webViewExists -import com.superwall.sdk.storage.LocalStorage -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.guava.await -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex - -class DefaultJavascriptEvalutor( - private val ioScope: IOScope, - private val uiScope: MainScope, - private val context: Context, - private val storage: LocalStorage, - private val createSandbox: suspend (ctx: Context) -> Result = { - asEither { - JavaScriptSandbox.createConnectedInstanceAsync(it).await() - }.toResult() - }, -) : JavascriptEvaluator { - private val mutex = Mutex() - private var eval: Deferred? = null - - /* - * Tries to evaluate JS using existing evaluator. If it is broken, tears it down and creates - * tries to execute it again, falling back to a WebView if that fails. - * */ - override suspend fun evaluate( - base64params: String, - rule: TriggerRule, - ): TriggerRuleOutcome = - try { - // Try to evaluate with the existing evaluator - createEvaluatorIfDoesntExist().evaluate(base64params, rule) - } catch (throwable: SandboxDeadException) { - // If evaluation failed, try teardown and recreate evaluator - teardown() - tryEvaluateWithFallback(base64params, rule) - } catch (e: Exception) { - Logger.debug( - logLevel = LogLevel.error, - scope = LogScope.superwallCore, - message = "Failed to evaluate rule with fallback: ${e.message}", - ) - TriggerRuleOutcome.noMatch(UnmatchedRule.Source.EXPRESSION, rule.experiment.id) - } - - override fun teardown() { - runBlocking { - try { - eval?.await()?.teardown() - } catch (e: Exception) { - Logger.debug( - logLevel = LogLevel.error, - scope = LogScope.superwallCore, - message = "Failed to teardown evaluator: ${e.message}", - ) - } - // Clear the existing evaluator and try with fallback to webview - eval = null - } - } - - private suspend fun createNewEvaluator(context: Context): JavascriptEvaluator = - when { - JavaScriptSandbox.isSupported() -> createSandboxEvaluator(context) - webViewExists() -> createWebViewEvaluator(context) - else -> NoSupportedEvaluator - } - - private suspend fun createSandboxEvaluator(context: Context): JavascriptEvaluator = - createSandbox(context) - .fold(onSuccess = { - SandboxJavascriptEvaluator(it, ioScope, storage.coreDataManager) - }, onFailure = { - Logger.debug( - logLevel = LogLevel.error, - scope = LogScope.superwallCore, - message = "Failed to create javascript sandbox evaluator: ${it.message}", - ) - createWebViewEvaluator(context) // Fallback to WebView - }) - - private suspend fun createWebViewEvaluator(context: Context): JavascriptEvaluator = - uiScope - .async { - WebviewJavascriptEvaluator(WebView(context), uiScope, storage.coreDataManager) - }.await() - - // Tries to create a JSSandbox and if it fails, it falls back to a WebView - private suspend fun tryEvaluateWithFallback( - base64params: String, - rule: TriggerRule, - ): TriggerRuleOutcome = - try { - createEvaluatorIfDoesntExist().evaluate(base64params, rule) - } catch (e: Exception) { - teardown() - createEvaluatorIfDoesntExist { - createWebViewEvaluator(context) - }.evaluate(base64params, rule) - } - - private suspend fun createEvaluatorIfDoesntExist( - invoke: suspend () -> JavascriptEvaluator = { - createNewEvaluator(context) - }, - ): JavascriptEvaluator { - mutex.lock() - val current = eval - val evaluator = - if (current == null) { - val call = - ioScope.async { - invoke() - } - eval = call - call.await() - } else { - current.await() - } - mutex.unlock() - return evaluator - } -} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/NoSupportedEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/NoSupportedEvaluator.kt deleted file mode 100644 index c24e92e5..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/NoSupportedEvaluator.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.superwall.sdk.paywall.presentation.rule_logic.javascript - -import com.superwall.sdk.logger.LogLevel -import com.superwall.sdk.logger.LogScope -import com.superwall.sdk.logger.Logger -import com.superwall.sdk.models.triggers.TriggerRule -import com.superwall.sdk.models.triggers.TriggerRuleOutcome -import com.superwall.sdk.models.triggers.UnmatchedRule - -object NoSupportedEvaluator : JavascriptEvaluator { - override suspend fun evaluate( - base64params: String, - rule: TriggerRule, - ): TriggerRuleOutcome { - Logger.debug( - LogLevel.warn, - LogScope.jsEvaluator, - "Javascript sandbox and Webview are unsupported, nothing was evaluated.", - ) - return TriggerRuleOutcome.noMatch( - UnmatchedRule.Source.EXPRESSION, - rule.experiment.id, - ) - } - - override fun teardown() {} -} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/JavascriptEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/RuleEvaluator.kt similarity index 53% rename from superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/JavascriptEvaluator.kt rename to superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/RuleEvaluator.kt index 79c3561f..61dc724e 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/JavascriptEvaluator.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/RuleEvaluator.kt @@ -1,18 +1,9 @@ package com.superwall.sdk.paywall.presentation.rule_logic.javascript import android.content.Context -import com.superwall.sdk.models.triggers.TriggerRule -import com.superwall.sdk.models.triggers.TriggerRuleOutcome import com.superwall.sdk.paywall.presentation.rule_logic.expression_evaluator.ExpressionEvaluating -interface JavascriptEvaluator { - suspend fun evaluate( - base64params: String, - rule: TriggerRule, - ): TriggerRuleOutcome - - fun teardown() - +interface RuleEvaluator { fun interface Factory { suspend fun provideRuleEvaluator(context: Context): ExpressionEvaluating } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/SandboxJavascriptEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/SandboxJavascriptEvaluator.kt deleted file mode 100644 index b0cce428..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/SandboxJavascriptEvaluator.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.superwall.sdk.paywall.presentation.rule_logic.javascript - -import androidx.javascriptengine.JavaScriptSandbox -import com.superwall.sdk.logger.LogLevel -import com.superwall.sdk.logger.LogScope -import com.superwall.sdk.logger.Logger -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.paywall.presentation.rule_logic.expression_evaluator.SDKJS -import com.superwall.sdk.paywall.presentation.rule_logic.tryToMatchOccurrence -import com.superwall.sdk.storage.core_data.CoreDataManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.guava.await -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext - -internal class SandboxJavascriptEvaluator( - private val jsSandbox: JavaScriptSandbox, - private val ioScope: CoroutineScope, - private val storage: CoreDataManager, -) : JavascriptEvaluator { - override suspend fun evaluate( - base64params: String, - rule: TriggerRule, - ): TriggerRuleOutcome = - withContext(ioScope.coroutineContext) { - val jsIsolate = jsSandbox.createIsolate() - jsIsolate.addOnTerminatedCallback { - Logger.debug( - logLevel = LogLevel.error, - scope = LogScope.superwallCore, - message = "$it", - ) - } - - val resultFuture = jsIsolate.evaluateJavaScriptAsync("$SDKJS\n $base64params") - - val result = resultFuture.await() - jsIsolate.close() - - if (result.isNullOrEmpty()) { - TriggerRuleOutcome.noMatch(UnmatchedRule.Source.EXPRESSION, rule.experiment.id) - } else { - val expressionMatched = result == "true" - rule.tryToMatchOccurrence(storage, expressionMatched) - } - } - - override fun teardown() { - runBlocking { - jsSandbox.close() - } - } -} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/WebviewJavascriptEvaluator.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/WebviewJavascriptEvaluator.kt deleted file mode 100644 index 63364180..00000000 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/rule_logic/javascript/WebviewJavascriptEvaluator.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.superwall.sdk.paywall.presentation.rule_logic.javascript - -import android.webkit.ConsoleMessage -import android.webkit.WebChromeClient -import android.webkit.WebView -import com.superwall.sdk.logger.LogLevel -import com.superwall.sdk.logger.LogScope -import com.superwall.sdk.logger.Logger -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.paywall.presentation.rule_logic.expression_evaluator.SDKJS -import com.superwall.sdk.paywall.presentation.rule_logic.tryToMatchOccurrence -import com.superwall.sdk.storage.core_data.CoreDataManager -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.launch - -internal class WebviewJavascriptEvaluator( - private val webView: WebView, - private val mainScope: CoroutineScope, - private val storage: CoreDataManager, -) : JavascriptEvaluator { - init { - webView.settings.javaScriptEnabled = true - webView.webChromeClient = - object : WebChromeClient() { - override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean = true - } - } - - override suspend fun evaluate( - base64params: String, - rule: TriggerRule, - ): TriggerRuleOutcome { - val deferred: CompletableDeferred = CompletableDeferred() - mainScope.async { - webView!!.evaluateJavascript( - "$SDKJS\n $base64params", - ) { result -> - Logger.debug(LogLevel.debug, LogScope.jsEvaluator, "!! evaluateJavascript result: $result") - - if (result == null) { - deferred.complete( - TriggerRuleOutcome.noMatch( - UnmatchedRule.Source.EXPRESSION, - rule.experiment.id, - ), - ) - } else { - val expressionMatched = result.replace("\"", "") == "true" - - CoroutineScope(Dispatchers.IO).launch { - val ruleMatched = rule.tryToMatchOccurrence(storage, expressionMatched) - deferred.complete(ruleMatched) - } - } - } - } - - return deferred.await() - } - - override fun teardown() { - webView.destroy() - } -} diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsTest.kt deleted file mode 100644 index d09853c4..00000000 --- a/superwall/src/test/java/com/superwall/sdk/paywall/presentation/rule_logic/expression_evaluator/ExpressionEvaluatorParamsTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -import java.util.* - -class ExpressionEvaluatorParamsTest { -/* - @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", mapOf("id" to "123", "email" to "test@gmail.com")) - jsonValues.put("device", emptyMap()) - jsonValues.put("params", 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") - } -*/ -} From 3ccfb2c1fbe877658b62836b372ab30ee526c48c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 16 Dec 2024 14:43:28 +0100 Subject: [PATCH 30/37] Fix issue with RuleEvaluator name change --- .../src/main/java/com/superwall/sdk/config/PaywallPreload.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt index fbf0c205..49b6cde2 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/PaywallPreload.kt @@ -10,6 +10,7 @@ import com.superwall.sdk.models.paywall.CacheKey import com.superwall.sdk.models.paywall.PaywallIdentifier import com.superwall.sdk.models.triggers.Trigger import com.superwall.sdk.paywall.manager.PaywallManager +import com.superwall.sdk.paywall.presentation.rule_logic.javascript.RuleEvaluator import com.superwall.sdk.paywall.request.ResponseIdentifiers import com.superwall.sdk.paywall.view.webview.webViewExists import com.superwall.sdk.storage.LocalStorage @@ -27,7 +28,8 @@ class PaywallPreload( ) { interface Factory : RequestFactory, - RuleAttributesFactory + RuleAttributesFactory, + RuleEvaluator.Factory private var currentPreloadingTask: Job? = null From a1149b816db9fde5cec2f318410f1870327a691c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Mon, 16 Dec 2024 15:32:33 +0100 Subject: [PATCH 31/37] Move back to release --- .../java/com/superwall/sdk/config/options/SuperwallOptions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt index 9f886612..12b3a2bf 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/options/SuperwallOptions.kt @@ -51,7 +51,7 @@ class SuperwallOptions { // **WARNING:**: Determines which network environment your SDK should use. // Defaults to `.release`. You should under no circumstance change this unless you // received the go-ahead from the Superwall team. - var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Developer() + var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Release() // Enables the sending of non-Superwall tracked events and properties back to the Superwall servers. // Defaults to `true`. From 9817ffdc3db2eb2515beb08cb232da867856c3ed Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Wed, 18 Dec 2024 13:22:14 +0100 Subject: [PATCH 32/37] Add purchase token & fix minor issues with entitlements --- .../com/superwall/superapp/MainApplication.kt | 1 + .../purchase/RevenueCatPurchaseController.kt | 2 +- .../config/ConfigManagerInstrumentedTest.kt | 2 +- .../main/java/com/superwall/sdk/Superwall.kt | 2 +- .../trackable/TrackableSuperwallEvent.kt | 8 ++++- .../sdk/billing/GoogleBillingWrapper.kt | 36 +++++++++++-------- .../models/entitlements/EntitlementStatus.kt | 2 +- .../operators/WaitForSubsStatusAndConfig.kt | 2 +- .../com/superwall/sdk/store/Entitlements.kt | 4 +-- .../GoogleBillingPurchaseTransaction.kt | 3 ++ .../transactions/StoreTransaction.kt | 2 ++ .../transactions/StoreTransactionType.kt | 1 + .../store/transactions/TransactionManager.kt | 4 +-- .../superwall/sdk/store/EntitlementsTest.kt | 4 +-- 14 files changed, 46 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/superwall/superapp/MainApplication.kt b/app/src/main/java/com/superwall/superapp/MainApplication.kt index ac66945e..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 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 225c36de..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 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 b0099643..845e23a5 100644 --- a/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt +++ b/superwall/src/androidTest/java/com/superwall/sdk/config/ConfigManagerInstrumentedTest.kt @@ -91,7 +91,7 @@ class ConfigManagerUnderTest( entitlements = Entitlements( mockk(relaxUnitFun = true) { - every { read(StoredEntitlementStatus) } returns EntitlementStatus.Unkown + every { read(StoredEntitlementStatus) } returns EntitlementStatus.Unknown every { read(StoredEntitlementsByProductId) } returns emptyMap() }, ), diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index fb1f80d5..39be2734 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -435,7 +435,7 @@ class Superwall( val cachedEntitlementStatus = dependencyContainer.storage.read(StoredEntitlementStatus) - ?: EntitlementStatus.Unkown + ?: EntitlementStatus.Unknown setEntitlementStatus(cachedEntitlementStatus) addListeners() diff --git a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt index 9ad1ffe5..15d7bb5d 100644 --- a/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt +++ b/superwall/src/main/java/com/superwall/sdk/analytics/internal/trackable/TrackableSuperwallEvent.kt @@ -261,7 +261,13 @@ sealed class InternalSuperwallEvent( ) : InternalSuperwallEvent(SuperwallEvent.EntitlementStatusDidChange()) { override suspend fun getSuperwallParameters(): HashMap = hashMapOf( - "entitlement_status" to entitlementStatus.toString(), + "entitlement_status" to { + when (entitlementStatus) { + is EntitlementStatus.Active -> "active" + is EntitlementStatus.Inactive -> "inactive" + is EntitlementStatus.Unknown -> "unknown" + } + }, ) } diff --git a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt index 91c56092..dcb8b50b 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt @@ -9,6 +9,7 @@ import com.android.billingclient.api.Purchase import com.android.billingclient.api.PurchasesUpdatedListener import com.superwall.sdk.delegate.InternalPurchaseResult import com.superwall.sdk.dependencies.HasExternalPurchaseControllerFactory +import com.superwall.sdk.dependencies.HasInternalPurchaseControllerFactory import com.superwall.sdk.dependencies.OptionsFactory import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.logger.LogLevel @@ -51,12 +52,13 @@ class GoogleBillingWrapper( interface Factory : HasExternalPurchaseControllerFactory, + HasInternalPurchaseControllerFactory, OptionsFactory private val threadHandler = Handler(ioScope) private val shouldFinishTransactions: Boolean get() = - !factory.makeHasExternalPurchaseController() && !factory.makeSuperwallOptions().shouldObservePurchases + factory.makeHasInternalPurchaseController() @get:Synchronized @set:Synchronized @@ -551,25 +553,29 @@ class GoogleBillingWrapper( */ override suspend fun getLatestTransaction(factory: StoreTransactionFactory): StoreTransaction? { // Get the latest from purchaseResults - purchaseResults.asStateFlow().filter { it != null }.first().let { purchaseResult -> - return when (purchaseResult) { - is InternalPurchaseResult.Purchased -> { - if (shouldFinishTransactions) { - return factory.makeStoreTransaction(purchaseResult.purchase) - } else { - null + purchaseResults + .asStateFlow() + .filter { it != null } + .first() + .let { purchaseResult -> + return when (purchaseResult) { + is InternalPurchaseResult.Purchased -> { + if (shouldFinishTransactions) { + return factory.makeStoreTransaction(purchaseResult.purchase) + } else { + null + } } - } - is InternalPurchaseResult.Cancelled -> { - null - } + is InternalPurchaseResult.Cancelled -> { + null + } - else -> { - null + else -> { + null + } } } - } } @Synchronized diff --git a/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt index 26dc2f15..3d11d1ae 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable @Serializable sealed class EntitlementStatus { @Serializable - object Unkown : EntitlementStatus() + object Unknown : EntitlementStatus() @Serializable object Inactive : EntitlementStatus() diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt index 12366d42..50738ce0 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt @@ -34,7 +34,7 @@ internal suspend fun Superwall.waitForEntitlementsAndConfig( try { withTimeout(5.seconds) { request.flags.entitlements - .filter { it !is EntitlementStatus.Unkown } + .filter { it !is EntitlementStatus.Unknown } .first() } } catch (e: TimeoutCancellationException) { diff --git a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt index f8a8a929..370cd356 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt @@ -21,7 +21,7 @@ class Entitlements( private val _entitlementsByProduct = ConcurrentHashMap>() private val _status: MutableStateFlow = - MutableStateFlow(EntitlementStatus.Unkown) + MutableStateFlow(EntitlementStatus.Unknown) val status: StateFlow get() = _status.asStateFlow() @@ -74,7 +74,7 @@ class Entitlements( _status.value = value } - is EntitlementStatus.Unkown -> { + is EntitlementStatus.Unknown -> { _all.clear() _active.clear() _inactive.clear() diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/GoogleBillingPurchaseTransaction.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/GoogleBillingPurchaseTransaction.kt index b03b7266..52b7c3f5 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/GoogleBillingPurchaseTransaction.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/GoogleBillingPurchaseTransaction.kt @@ -44,6 +44,8 @@ data class GoogleBillingPurchaseTransaction( @Serializable(with = UUIDSerializer::class) @SerialName("app_account_token") override val appAccountToken: UUID?, + @SerialName("purchase_token") + override val purchaseToken: String, override var payment: StorePayment, ) : StoreTransactionType { constructor(transaction: Purchase) : this( @@ -62,5 +64,6 @@ data class GoogleBillingPurchaseTransaction( revocationDate = null, // Replace with correct mapping appAccountToken = null, // Replace with correct mapping payment = StorePayment(transaction), // Replace with correct mapping + purchaseToken = transaction.purchaseToken, ) } diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransaction.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransaction.kt index 0976b419..a04bbad3 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransaction.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransaction.kt @@ -42,6 +42,8 @@ class StoreTransaction( @Serializable(with = UUIDSerializer::class) override val appAccountToken: UUID? get() = transaction.appAccountToken + override val purchaseToken: String + get() = transaction.purchaseToken // fun toDictionary(): Map { // val json = Json { encodeDefaults = true } diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransactionType.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransactionType.kt index 8572f4af..74b46bbf 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransactionType.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/transactions/StoreTransactionType.kt @@ -26,6 +26,7 @@ interface StoreTransactionType { val offerId: String? val revocationDate: Date? val appAccountToken: UUID? + val purchaseToken: String } // Custom serializer diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index 566bff05..d9ba5aed 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -203,7 +203,7 @@ class TransactionManager( val result = state as PurchasingObserverState.PurchaseResult result.purchases?.forEach { purchase -> purchase.products.map { - storeManager.productsByFullId[it] ?.let { product -> + storeManager.productsByFullId[it]?.let { product -> handlePendingTransaction(PurchaseSource.ObserverMode(product)) } } @@ -214,7 +214,7 @@ class TransactionManager( val state = state as PurchasingObserverState.PurchaseResult state.purchases?.forEach { purchase -> purchase.products.map { - storeManager.productsByFullId[it] ?.let { product -> + storeManager.productsByFullId[it]?.let { product -> didRestore(product, PurchaseSource.ObserverMode(product)) } } diff --git a/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt b/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt index 47604e01..79c5860d 100644 --- a/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/store/EntitlementsTest.kt @@ -133,13 +133,13 @@ class EntitlementsTest { every { storage.read(StoredEntitlementsByProductId) } returns null entitlements = Entitlements(storage) When("setting Unknown status") { - entitlements.setEntitlementStatus(EntitlementStatus.Unkown) + entitlements.setEntitlementStatus(EntitlementStatus.Unknown) Then("it should clear all collections") { assertTrue(entitlements.active.isEmpty()) assertTrue(entitlements.inactive.isEmpty()) assertTrue(entitlements.all.isEmpty()) - assertTrue(entitlements.status.value is EntitlementStatus.Unkown) + assertTrue(entitlements.status.value is EntitlementStatus.Unknown) } } } From eb6167247df16675778090ae4b2a9e8edae82690 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Wed, 18 Dec 2024 13:48:03 +0100 Subject: [PATCH 33/37] Add activeProducts to device template --- .../main/java/com/superwall/sdk/Superwall.kt | 12 +------ .../sdk/billing/GoogleBillingWrapper.kt | 33 +++++++++++++++++++ .../sdk/dependencies/DependencyContainer.kt | 6 ++-- .../sdk/dependencies/FactoryProtocols.kt | 2 ++ .../sdk/network/device/DeviceHelper.kt | 5 ++- .../templating/models/DeviceTemplate.kt | 1 + .../com/superwall/sdk/store/Entitlements.kt | 5 +++ .../store/transactions/TransactionManager.kt | 2 +- 8 files changed, 51 insertions(+), 15 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 39be2734..9e3720c0 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -70,7 +70,6 @@ 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 @@ -273,15 +272,6 @@ class Superwall( dependencyContainer.entitlements } - /** - * A `StateFlow` of the entitlement status of the user. Set this using - * [setEntitlementStatus]. - */ - - val entitlementStatus: StateFlow by lazy { - entitlements.status - } - /** * A property that indicates current configuration state of the SDK. * @@ -471,7 +461,7 @@ class Superwall( // / Listens to config and the subscription status private fun addListeners() { ioScope.launchWithTracking { - entitlementStatus // Removes duplicates by default + entitlements.status // Removes duplicates by default .drop(1) // Drops the first item .collect { newValue -> // Save and handle the new value diff --git a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt index dcb8b50b..f48ec946 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt @@ -7,6 +7,7 @@ import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingResult import com.android.billingclient.api.Purchase import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.QueryPurchasesParams import com.superwall.sdk.delegate.InternalPurchaseResult import com.superwall.sdk.dependencies.HasExternalPurchaseControllerFactory import com.superwall.sdk.dependencies.HasInternalPurchaseControllerFactory @@ -20,6 +21,7 @@ import com.superwall.sdk.misc.Either import com.superwall.sdk.misc.IOScope import com.superwall.sdk.store.abstractions.product.StoreProduct import com.superwall.sdk.store.abstractions.transactions.StoreTransaction +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -122,6 +124,12 @@ class GoogleBillingWrapper( } } + internal suspend fun queryAllPurchases(): List { + val apps = billingClient?.queryType(ProductType.INAPP) ?: emptyList() + val subs = billingClient?.queryType(ProductType.SUBS) ?: emptyList() + return apps + subs + } + fun startConnectionOnMainThread(delayMilliseconds: Long = 0) { threadHandler.postDelayed( { startConnection() }, @@ -617,3 +625,28 @@ fun Pair?>.toInternalResult(): List { + val deferred = CompletableDeferred>() + + val params = + QueryPurchasesParams + .newBuilder() + .setProductType(type) + .build() + + queryPurchasesAsync(params) { billingResult, purchasesList -> + if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) { + Logger.debug( + logLevel = LogLevel.error, + scope = LogScope.nativePurchaseController, + message = "Unable to query for purchases.", + ) + return@queryPurchasesAsync + } + + deferred.complete(purchasesList) + } + + return deferred.await() +} diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index c2534142..fcb9ee7e 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -418,7 +418,7 @@ class DependencyContainer( "X-Low-Power-Mode" to deviceHelper.isLowPowerModeEnabled.toString(), "X-Is-Sandbox" to deviceHelper.isSandbox.toString(), "X-Entitlement-Status" to - Superwall.instance.entitlementStatus.value + Superwall.instance.entitlements.status.value .toString(), "Content-Type" to "application/json", "X-Current-Time" to dateFormat(DateUtils.ISO_MILLIS).format(Date()), @@ -565,7 +565,7 @@ class DependencyContainer( PresentationRequest.Flags( isDebuggerLaunched = isDebuggerLaunched ?: debugManager.isDebuggerLaunched, // TODO: (PresentationCritical) Fix subscription status - entitlements = entitlementStatus ?: Superwall.instance.entitlementStatus, + entitlements = entitlementStatus ?: Superwall.instance.entitlements.status, // subscriptionStatus = subscriptionStatus!!, isPaywallPresented = isPaywallPresented, type = type, @@ -647,6 +647,8 @@ class DependencyContainer( appSessionId = appSessionManager.appSession.id, ) + override suspend fun queryAllPurchases(): List = googleBillingWrapper.queryAllPurchases() + override fun makeTransactionVerifier(): GoogleBillingWrapper = googleBillingWrapper override fun makeSuperwallOptions(): SuperwallOptions = configManager.options diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt index 12a15382..0d0d5b43 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt @@ -166,6 +166,8 @@ interface ConfigManagerFactory { interface StoreTransactionFactory { suspend fun makeStoreTransaction(transaction: Purchase): StoreTransaction + + suspend fun queryAllPurchases(): List } interface OptionsFactory { diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index 1f7f1833..f3825a03 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -15,6 +15,7 @@ import com.superwall.sdk.BuildConfig import com.superwall.sdk.Superwall import com.superwall.sdk.dependencies.IdentityInfoFactory import com.superwall.sdk.dependencies.LocaleIdentifierFactory +import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger @@ -62,7 +63,8 @@ class DeviceHelper( interface Factory : IdentityInfoFactory, LocaleIdentifierFactory, - JsonFactory + JsonFactory, + StoreTransactionFactory private val json = Json { @@ -481,6 +483,7 @@ class DeviceHelper( activeEntitlements = Superwall.instance.entitlements.active .map { it.id }, + activeProducts = factory.queryAllPurchases().flatMap { it.products.map { it } }, isFirstAppOpen = isFirstAppOpen, sdkVersion = sdkVersion, sdkVersionPadded = sdkVersionPadded, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt index 688d79f4..ef0d04ae 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt @@ -45,6 +45,7 @@ data class DeviceTemplate( val localDateTime: String, val isSandbox: String, val activeEntitlements: List, + val activeProducts: List, val isFirstAppOpen: Boolean, val sdkVersion: String, val sdkVersionPadded: String, diff --git a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt index 370cd356..ba2950e0 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt @@ -23,6 +23,11 @@ class Entitlements( private val _status: MutableStateFlow = MutableStateFlow(EntitlementStatus.Unknown) + /** + * A `StateFlow` of the entitlement status of the user. Set this using + * [Superwall.instance.setEntitlementStatus]. + */ + val status: StateFlow get() = _status.asStateFlow() diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index d9ba5aed..b9469884 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -63,7 +63,7 @@ class TransactionManager( }, private val dismiss: suspend (paywallView: PaywallView, result: PaywallResult) -> Unit, private val entitlementStatus: () -> EntitlementStatus = { - Superwall.instance.entitlementStatus.value + Superwall.instance.entitlements.status.value }, ) { sealed class PurchaseSource { From 163655b44df989426bb022682f131759c9576b45 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Wed, 18 Dec 2024 14:46:35 +0100 Subject: [PATCH 34/37] Minor fixes to observer mode billing listening, adds to DeviceTemplate --- .../java/com/superwall/sdk/billing/Billing.kt | 3 + .../sdk/billing/GoogleBillingWrapper.kt | 2 +- .../com/superwall/sdk/config/ConfigManager.kt | 7 +- .../sdk/dependencies/DependencyContainer.kt | 2 +- .../sdk/dependencies/FactoryProtocols.kt | 2 +- .../sdk/network/device/DeviceHelper.kt | 2 +- .../com/superwall/sdk/store/StoreManager.kt | 2 +- .../product/receipt/ReceiptManager.kt | 65 ++++--------------- .../store/transactions/TransactionManager.kt | 2 +- 9 files changed, 29 insertions(+), 58 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt b/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt index ecca8456..fcae3c8f 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/Billing.kt @@ -1,5 +1,6 @@ package com.superwall.sdk.billing +import com.android.billingclient.api.Purchase import com.superwall.sdk.delegate.InternalPurchaseResult import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.store.abstractions.product.StoreProduct @@ -12,4 +13,6 @@ interface Billing { suspend fun awaitGetProducts(identifiers: Set): Set suspend fun getLatestTransaction(factory: StoreTransactionFactory): StoreTransaction? + + suspend fun queryAllPurchases(): List } diff --git a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt index f48ec946..fd2b5761 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/GoogleBillingWrapper.kt @@ -124,7 +124,7 @@ class GoogleBillingWrapper( } } - internal suspend fun queryAllPurchases(): List { + override suspend fun queryAllPurchases(): List { val apps = billingClient?.queryType(ProductType.INAPP) ?: emptyList() val subs = billingClient?.queryType(ProductType.SUBS) ?: emptyList() return apps + subs diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt index 01f40214..baf2170c 100644 --- a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt @@ -9,6 +9,7 @@ import com.superwall.sdk.dependencies.DeviceHelperFactory import com.superwall.sdk.dependencies.DeviceInfoFactory import com.superwall.sdk.dependencies.RequestFactory import com.superwall.sdk.dependencies.RuleAttributesFactory +import com.superwall.sdk.dependencies.StoreTransactionFactory import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger @@ -67,7 +68,8 @@ open class ConfigManager( RequestFactory, DeviceInfoFactory, RuleAttributesFactory, - DeviceHelperFactory + DeviceHelperFactory, + StoreTransactionFactory // The configuration of the Superwall dashboard internal val configState = MutableStateFlow(ConfigState.None) @@ -309,6 +311,9 @@ open class ConfigManager( ConfigLogic.extractEntitlementsByProductId(config.products).let { entitlements.addEntitlementsByProductId(it) } + ioScope.launch { + storeManager.loadPurchasedProducts() + } } // Preloading Paywalls diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt index fcb9ee7e..2977d1fc 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt @@ -647,7 +647,7 @@ class DependencyContainer( appSessionId = appSessionManager.appSession.id, ) - override suspend fun queryAllPurchases(): List = googleBillingWrapper.queryAllPurchases() + override suspend fun activeProductIds(): List = storeManager.receiptManager.purchases.toList() override fun makeTransactionVerifier(): GoogleBillingWrapper = googleBillingWrapper diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt index 0d0d5b43..d990c758 100644 --- a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt +++ b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt @@ -167,7 +167,7 @@ interface ConfigManagerFactory { interface StoreTransactionFactory { suspend fun makeStoreTransaction(transaction: Purchase): StoreTransaction - suspend fun queryAllPurchases(): List + suspend fun activeProductIds(): List } interface OptionsFactory { diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index f3825a03..b37d88bb 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -483,7 +483,7 @@ class DeviceHelper( activeEntitlements = Superwall.instance.entitlements.active .map { it.id }, - activeProducts = factory.queryAllPurchases().flatMap { it.products.map { it } }, + activeProducts = factory.activeProductIds(), isFirstAppOpen = isFirstAppOpen, sdkVersion = sdkVersion, sdkVersionPadded = sdkVersionPadded, diff --git a/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt index ad3c7432..f9a50ace 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/StoreManager.kt @@ -29,7 +29,7 @@ class StoreManager( }, ) : ProductsFetcher, StoreKit { - private val receiptManager by lazy { ReceiptManager(delegate = this) } + val receiptManager by lazy { ReceiptManager(delegate = this, billing) } var productsByFullId: MutableMap = mutableMapOf() diff --git a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/receipt/ReceiptManager.kt b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/receipt/ReceiptManager.kt index 0d8645bf..98464231 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/receipt/ReceiptManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/abstractions/product/receipt/ReceiptManager.kt @@ -1,5 +1,6 @@ package com.superwall.sdk.store.abstractions.product.receipt +import com.superwall.sdk.billing.Billing import com.superwall.sdk.logger.LogLevel import com.superwall.sdk.logger.LogScope import com.superwall.sdk.logger.Logger @@ -14,63 +15,25 @@ data class InAppPurchase( class ReceiptManager( private var delegate: ProductsFetcher?, + private val billing: Billing, // private val receiptData: () -> ByteArray? = ReceiptLogic::getReceiptData ) { var purchasedSubscriptionGroupIds: Set? = null - private var purchases: MutableSet = mutableSetOf() + private var _purchases: MutableSet = mutableSetOf() private var receiptRefreshCompletion: ((Boolean) -> Unit)? = null + val purchases: Set + get() = _purchases.map { it.productIdentifier }.toSet() + @Suppress("RedundantSuspendModifier") suspend fun loadPurchasedProducts(): Set? = - coroutineScope { -// val hasPurchaseController = Superwall.instance.dependencyContainer.delegateAdapter.hasPurchaseController -// -// val payload = ReceiptLogic.getPayload(receiptData()) ?: run { -// // if (!hasPurchaseController) { -// // Superwall.instance.subscriptionStatus = SubscriptionStatus.INACTIVE -// // } -// return@coroutineScope null -// } -// -// delegate?.let { delegate -> -// val localPurchases = payload.purchases -// this@ReceiptManager.purchases = localPurchases.toMutableSet() -// -// if (!hasPurchaseController) { -// val activePurchases = localPurchases.filter { it.isActive } -// if (activePurchases.isEmpty()) { -// Superwall.instance.subscriptionStatus = SubscriptionStatus.INACTIVE -// } else { -// Superwall.instance.subscriptionStatus = SubscriptionStatus.ACTIVE -// } -// } -// -// val purchasedProductIds = localPurchases.map { it.productIdentifier }.toSet() -// -// try { -// val products = delegate.products(purchasedProductIds, null) -// val purchasedSubscriptionGroupIds = mutableSetOf() -// for (product in products) { -// product.subscriptionGroupIdentifier?.let { -// purchasedSubscriptionGroupIds.add(it) -// } -// } -// this@ReceiptManager.purchasedSubscriptionGroupIds = purchasedSubscriptionGroupIds -// products -// } catch (e: Throwable) { -// null -// } -// } ?: run { -// if (!hasPurchaseController) { -// Superwall.instance.subscriptionStatus = SubscriptionStatus.INACTIVE -// } -// return@coroutineScope null -// } - - // SW-2218 - // https://linear.app/superwall/issue/SW-2218/%5Bandroid%5D-%5Bv0%5D-replace-receipt-validation-with-google-play-billing - return@coroutineScope emptySet() - } + billing + .queryAllPurchases() + .flatMap { it.products } + .let { products -> + _purchases.addAll(products.map { InAppPurchase(it) }.toSet()) + delegate?.products(products.toSet()) + }?.toSet() suspend fun refreshReceipt() { Logger.debug( @@ -85,5 +48,5 @@ class ReceiptManager( // https://linear.app/superwall/issue/SW-2218/%5Bandroid%5D-%5Bv0%5D-replace-receipt-validation-with-google-play-billing } - fun hasPurchasedProduct(productId: String): Boolean = purchases.firstOrNull { it.productIdentifier == productId } != null + fun hasPurchasedProduct(productId: String): Boolean = _purchases.firstOrNull { it.productIdentifier == productId } != null } diff --git a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt index b9469884..858bbc40 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/transactions/TransactionManager.kt @@ -720,7 +720,7 @@ class TransactionManager( val hasRestored = restorationResult is RestorationResult.Restored val hasEntitlements = entitlementStatus() is EntitlementStatus.Active - + storeManager.loadPurchasedProducts() if (hasRestored && hasEntitlements) { log(message = "Transactions Restored") track( From 38f0c008359b33879f0a173696d90d20c2acc616 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Wed, 18 Dec 2024 15:05:48 +0100 Subject: [PATCH 35/37] Improve documentation on entitlements, purchase and observer --- .../main/java/com/superwall/sdk/Superwall.kt | 63 ++++++++++++++----- .../LaunchBillingFlowWithSuperwall.kt | 26 ++++++++ .../models/entitlements/EntitlementStatus.kt | 22 +++++++ .../com/superwall/sdk/store/Entitlements.kt | 39 ++++++++++-- 4 files changed, 130 insertions(+), 20 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/Superwall.kt b/superwall/src/main/java/com/superwall/sdk/Superwall.kt index 9e3720c0..537e4ccd 100644 --- a/superwall/src/main/java/com/superwall/sdk/Superwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/Superwall.kt @@ -190,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. @@ -199,10 +200,10 @@ 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 [EntitlementStatus.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 @@ -212,7 +213,7 @@ class Superwall( * 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 setEntitlementStatus(entitlementStatus: EntitlementStatus) { entitlements.setEntitlementStatus(entitlementStatus) @@ -222,7 +223,9 @@ class Superwall( * 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")` + * Example: + * `setEntitlementStatus("default", "pro")` equals `EntitlementStatus.Active(setOf(Entitlement("default"), Entitlement("pro")))` + * `setEntitlementStatus()` equals `EntitlementStatus.Inactive` * * @param entitlements A list of entitlements. * */ @@ -697,7 +700,7 @@ class Superwall( } /** - *Initiates a purchase of `ProductDetails`. + * 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` @@ -709,7 +712,6 @@ class Superwall( * - 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( @@ -720,7 +722,7 @@ class Superwall( }.toResult() /** - *Initiates a purchase of `StoreProduct`. + * 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` @@ -732,7 +734,6 @@ class Superwall( * - 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( @@ -743,7 +744,7 @@ class Superwall( }.toResult() /** - *Initiates a purchase of a product with the given `productId`. + * 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` @@ -755,7 +756,6 @@ class Superwall( * - 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 { @@ -773,7 +773,6 @@ class Superwall( * @param productIds: A list of full product identifiers. * @return A map of product identifiers to `StoreProduct` objects. */ - suspend fun getProducts(vararg productIds: String): Result> = withErrorTracking { dependencyContainer.storeManager.getProductsWithoutPaywall(productIds.toList()) @@ -811,10 +810,18 @@ class Superwall( } /** - * Observe purchases made without using Paywalls + * Observe purchases made without using Paywalls. * - * */ - + * This method allows you to track purchases that happen outside of Superwall's paywall flow. + * It handles different states of the purchase process including start, completion, and errors. + * + * Note: The `shouldObservePurchases` option must be enabled in SuperwallOptions for this to work. + * + * @param state The current state of the purchase to observe, can be: + * - PurchaseWillBegin: When a purchase flow is about to start + * - PurchaseResult: When a purchase completes successfully + * - PurchaseError: When a purchase fails with an error + */ fun observe(state: PurchasingObserverState) { ioScope.launchWithTracking { if (!options.shouldObservePurchases) { @@ -856,10 +863,27 @@ class Superwall( } } + /** + * Convenience method to observe when a purchase flow begins. + * + * Call this method when a purchase is about to start to track the beginning of the transaction. + * This will trigger tracking of the Transaction Start event in Superwall's analytics. + * + * @param product The Google Play Billing ProductDetails for the product being purchased + */ fun observePurchaseStart(product: ProductDetails) { observe(PurchasingObserverState.PurchaseWillBegin(product)) } + /** + * Convenience method to observe purchase errors. + * + * Call this method when a purchase fails to track the failure in Superwall's analytics. + * This will trigger tracking of the Transaction Fail event. + * + * @param product The Google Play Billing ProductDetails for the product that failed to purchase + * @param error The error that caused the purchase to fail + */ fun observePurchaseError( product: ProductDetails, error: Throwable, @@ -867,6 +891,15 @@ class Superwall( observe(PurchasingObserverState.PurchaseError(product, error)) } + /** + * Convenience method to observe successful purchases. + * + * Call this method when a purchase completes successfully to track the completion in Superwall's analytics. + * This will trigger tracking of the Transaction Success event. + * + * @param billingResult The BillingResult from Google Play Billing containing the purchase response + * @param purchases List of completed Purchase objects from the transaction + */ fun observePurchaseResult( billingResult: BillingResult, purchases: List, diff --git a/superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt b/superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt index e0076df1..79386854 100644 --- a/superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt +++ b/superwall/src/main/java/com/superwall/sdk/billing/observer/LaunchBillingFlowWithSuperwall.kt @@ -10,6 +10,32 @@ import com.superwall.sdk.logger.Logger import com.superwall.sdk.store.PurchasingObserverState import com.superwall.sdk.store.abstractions.product.RawStoreProduct +/** + * Extension function for BillingClient that launches the Google Play billing flow while allowing Superwall to observe the purchase. + * + * This method acts as a proxy between your app's purchase flow and Google Play Billing, enabling Superwall to track the + * purchase lifecycle when observer mode is enabled. It wraps the standard [BillingClient.launchBillingFlow] method and adds + * purchase observation capabilities. + * + * The method will: + * 1. Check if Superwall SDK is initialized + * 2. Verify if purchase observation is enabled via [SuperwallOptions.shouldObservePurchases] + * 3. Notify Superwall before each product purchase begins + * 4. Launch the actual billing flow + * + * Purchase events can then be observed through [Superwall.delegate] or [Superwall.events], which will emit events like: + * - [SuperwallEvent.TransactionStart] when purchase begins + * - [SuperwallEvent.TransactionComplete] on successful purchase + * - [SuperwallEvent.TransactionFail] on purchase failure + * + * @param activity The activity that will host the billing flow + * @param params Wrapper around BillingFlowParams containing product details for the purchase + * @return BillingResult containing the response from launching the billing flow + * @throws IllegalStateException if Superwall SDK is not initialized + * + * @see SuperwallBillingFlowParams + * @see Superwall.observe + */ fun BillingClient.launchBillingFlowWithSuperwall( activity: Activity, params: SuperwallBillingFlowParams, diff --git a/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt index 3d11d1ae..debcb163 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/entitlements/EntitlementStatus.kt @@ -3,14 +3,36 @@ package com.superwall.sdk.models.entitlements import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +/** + * Represents the status of a user's entitlements. + * + * This sealed class has three possible states: + * - [Unknown]: The initial state before any entitlement status is determined + * - [Inactive]: When the user has no active entitlements + * - [Active]: When the user has one or more active entitlements + */ @Serializable sealed class EntitlementStatus { + /** + * Represents an unknown entitlement status. + * This is the initial state before any entitlement status is determined. + */ @Serializable object Unknown : EntitlementStatus() + /** + * Represents an inactive entitlement status. + * This state indicates the user has no active entitlements. + */ @Serializable object Inactive : EntitlementStatus() + /** + * Represents an active entitlement status. + * This state indicates the user has one or more active entitlements. + * + * @property entitlements A Set of active [Entitlement] objects belonging to the user + */ @Serializable data class Active( @SerialName("entitlements") diff --git a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt index ba2950e0..3d84e6f8 100644 --- a/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt +++ b/superwall/src/main/java/com/superwall/sdk/store/Entitlements.kt @@ -14,33 +14,51 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap +/** + * A class that handles the Set of Entitlement objects retrieved from + * the Superwall dashboard. + */ class Entitlements( private val storage: Storage, private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default), ) { + // MARK: - Private Properties private val _entitlementsByProduct = ConcurrentHashMap>() private val _status: MutableStateFlow = MutableStateFlow(EntitlementStatus.Unknown) /** - * A `StateFlow` of the entitlement status of the user. Set this using + * A StateFlow of the entitlement status of the user. Set this using * [Superwall.instance.setEntitlementStatus]. + * + * You can collect this flow to get notified whenever it changes. */ - val status: StateFlow get() = _status.asStateFlow() - // Mutable backing fields for entitlements + // MARK: - Backing Fields private val _all = mutableSetOf() private val _active = mutableSetOf() private val _inactive = mutableSetOf() - // Exposed properties for entitlements + // MARK: - Public Properties + + /** + * All entitlements, regardless of whether they're active or not. + */ val all: Set get() = _all.toSet() + + /** + * The active entitlements. + */ val active: Set get() = _active.toSet() + + /** + * The inactive entitlements. + */ val inactive: Set get() = _inactive.toSet() @@ -59,6 +77,9 @@ class Entitlements( } } + /** + * Sets the entitlement status and updates the corresponding entitlement collections. + */ fun setEntitlementStatus(value: EntitlementStatus) { when (value) { is EntitlementStatus.Active -> { @@ -80,7 +101,6 @@ class Entitlements( } is EntitlementStatus.Unknown -> { - _all.clear() _active.clear() _inactive.clear() _status.value = value @@ -88,6 +108,12 @@ class Entitlements( } } + /** + * Returns a Set of Entitlements belonging to a given productId. + * + * @param id A String representing a productId + * @return A Set of Entitlements + */ internal fun byProductId(id: String): Set { val decomposedProductIds = DecomposedProductIds.from(id) listOf( @@ -106,6 +132,9 @@ class Entitlements( return emptySet() } + /** + * Updates the entitlements associated with product IDs and persists them to storage. + */ internal fun addEntitlementsByProductId(idToEntitlements: Map>) { _entitlementsByProduct.putAll( idToEntitlements.mapValues { (_, entitlements) -> From f5f7a20768bc4437ad601b878a27dce61602a14c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Wed, 18 Dec 2024 15:51:36 +0100 Subject: [PATCH 36/37] Add activeEntitlementsObject to device template --- .../com/superwall/sdk/models/entitlements/Entitlement.kt | 6 ++++-- .../java/com/superwall/sdk/network/device/DeviceHelper.kt | 3 +++ .../view/webview/templating/models/DeviceTemplate.kt | 1 + .../view/webview/templating/models/DeviceTemplateTest.kt | 4 +++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt b/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt index f19b9fee..927027bc 100644 --- a/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt +++ b/superwall/src/main/java/com/superwall/sdk/models/entitlements/Entitlement.kt @@ -11,8 +11,10 @@ data class Entitlement( val type: Type = Type.SERVICE_LEVEL, ) { @Serializable - enum class Type { + enum class Type( + val raw: String, + ) { @SerialName("SERVICE_LEVEL") - SERVICE_LEVEL, + SERVICE_LEVEL("SERVICE_LEVEL"), } } diff --git a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt index b37d88bb..3ce1195e 100644 --- a/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt +++ b/superwall/src/main/java/com/superwall/sdk/network/device/DeviceHelper.kt @@ -483,6 +483,9 @@ class DeviceHelper( activeEntitlements = Superwall.instance.entitlements.active .map { it.id }, + activeEntitlementsObject = + Superwall.instance.entitlements.active + .map { mapOf("identifier" to it.id, "type" to it.type.raw) }, activeProducts = factory.activeProductIds(), isFirstAppOpen = isFirstAppOpen, sdkVersion = sdkVersion, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt index ef0d04ae..d0e4c885 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplate.kt @@ -45,6 +45,7 @@ data class DeviceTemplate( val localDateTime: String, val isSandbox: String, val activeEntitlements: List, + val activeEntitlementsObject: List>, val activeProducts: List, val isFirstAppOpen: Boolean, val sdkVersion: String, diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt index c7f455ba..7dfac885 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/templating/models/DeviceTemplateTest.kt @@ -51,7 +51,8 @@ class DeviceTemplateTest { utcDateTime = "2024-03-20T10:00:00", localDateTime = "2024-03-20T02:00:00", isSandbox = "true", - activeEntitlements = listOf(mapOf("identifier" to "active")), + activeEntitlements = listOf("active"), + activeEntitlementsObject = listOf(mapOf("identifier" to "active", "type" to "SERVICE_LEVEL")), isFirstAppOpen = false, sdkVersion = "1.0.0", sdkVersionPadded = "001.000.000", @@ -129,6 +130,7 @@ class DeviceTemplateTest { localDateTime = "2024-03-20T02:00:00", isSandbox = "true", activeEntitlements = listOf(), + activeEntitlementsObject = listOf(), isFirstAppOpen = true, sdkVersion = "1.0.0", sdkVersionPadded = "001.000.000", From bb5a9bdcde64514e2a791e3c814f0ff49646ae16 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Wed, 18 Dec 2024 17:06:37 +0100 Subject: [PATCH 37/37] Fix testing utils --- .../java/com/example/superapp/utils/TestingUtils.kt | 2 +- .../main/java/com/superwall/superapp/test/UITestHandler.kt | 5 ++--- .../internal/operators/WaitForSubsStatusAndConfig.kt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) 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 20e4a000..65b6c672 100644 --- a/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt +++ b/app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt @@ -18,7 +18,7 @@ import com.dropbox.dropshots.Dropshots import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.superwall.SuperwallEvent import com.superwall.sdk.config.models.ConfigurationStatus -import com.superwall.sdk.paywall.vc.ShimmerView +import com.superwall.sdk.paywall.view.ShimmerView import com.superwall.superapp.MainActivity import com.superwall.superapp.test.UITestInfo import kotlinx.coroutines.CoroutineScope 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 fa4d8e41..80fe7165 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -545,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( @@ -729,7 +728,7 @@ object UITestHandler { subscribed: Boolean, gated: Boolean, ) { - val currentSubscriptionStatus = Superwall.instance.entitlementStatus.value + val currentSubscriptionStatus = Superwall.instance.entitlements.status.value if (subscribed) { // Set user subscribed diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt index 50738ce0..4c07e33d 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/presentation/internal/operators/WaitForSubsStatusAndConfig.kt @@ -54,7 +54,7 @@ internal suspend fun Superwall.waitForEntitlementsAndConfig( logLevel = LogLevel.info, scope = LogScope.paywallPresentation, message = - "Timeout: Superwall.instance.entitlementStatus has been \"unknown\" for " + + "Timeout: Superwall.instance.entitlement.status has been \"unknown\" for " + "over 5 seconds resulting in a failure.", ) val error =