diff --git a/api-tester/src/defaults/java/com/revenuecat/apitester/java/PurchasesAPI.java b/api-tester/src/defaults/java/com/revenuecat/apitester/java/PurchasesAPI.java index d98b1c68ce..6edeecf4cd 100644 --- a/api-tester/src/defaults/java/com/revenuecat/apitester/java/PurchasesAPI.java +++ b/api-tester/src/defaults/java/com/revenuecat/apitester/java/PurchasesAPI.java @@ -100,6 +100,8 @@ public void onSuccess(@NonNull AmazonLWAConsentStatus contentStatus) { final Store store = purchases.getStore(); final String storefrontCountryCode = purchases.getStorefrontCountryCode(); + + final PurchasesConfiguration configuration = purchases.getCurrentConfiguration(); } static void check(final Purchases purchases, final Map attributes) { diff --git a/api-tester/src/defaults/kotlin/com/revenuecat/apitester/kotlin/PurchasesAPI.kt b/api-tester/src/defaults/kotlin/com/revenuecat/apitester/kotlin/PurchasesAPI.kt index 9746fda470..107d207a75 100644 --- a/api-tester/src/defaults/kotlin/com/revenuecat/apitester/kotlin/PurchasesAPI.kt +++ b/api-tester/src/defaults/kotlin/com/revenuecat/apitester/kotlin/PurchasesAPI.kt @@ -90,6 +90,8 @@ private class PurchasesAPI { val store: Store = purchases.store val countryCode = purchases.storefrontCountryCode + + val configuration: PurchasesConfiguration = purchases.currentConfiguration } @Suppress("LongMethod", "LongParameterList") diff --git a/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt b/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt index e5cc9754dc..51923391ae 100644 --- a/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt +++ b/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt @@ -38,6 +38,11 @@ import java.net.URL class Purchases internal constructor( @get:JvmSynthetic internal val purchasesOrchestrator: PurchasesOrchestrator, ) : LifecycleDelegate { + /** + * The current configuration parameters of the Purchases SDK. + */ + val currentConfiguration: PurchasesConfiguration + get() = purchasesOrchestrator.currentConfiguration /** * Default to TRUE, set this to FALSE if you are consuming and acknowledging transactions @@ -62,7 +67,9 @@ class Purchases internal constructor( @Synchronized get() = if (purchasesOrchestrator.finishTransactions) { PurchasesAreCompletedBy.REVENUECAT - } else PurchasesAreCompletedBy.MY_APP + } else { + PurchasesAreCompletedBy.MY_APP + } @Synchronized set(value) { purchasesOrchestrator.finishTransactions = when (value) { @@ -882,7 +889,12 @@ class Purchases internal constructor( configuration: PurchasesConfiguration, ): Purchases { if (isConfigured) { - infoLog(ConfigureStrings.INSTANCE_ALREADY_EXISTS) + if (backingFieldSharedInstance?.purchasesOrchestrator?.currentConfiguration == configuration) { + infoLog(ConfigureStrings.INSTANCE_ALREADY_EXISTS_WITH_SAME_CONFIG) + return sharedInstance + } else { + infoLog(ConfigureStrings.INSTANCE_ALREADY_EXISTS) + } } return PurchasesFactory().createPurchases( configuration, diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt index ebb5286560..66c952f6d0 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/DangerousSettings.kt @@ -1,8 +1,12 @@ package com.revenuecat.purchases +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + /** * Only use a Dangerous Setting if suggested by RevenueCat support team. */ +@Parcelize data class DangerousSettings internal constructor( /** * Disable or enable syncing purchases automatically. If this is disabled, RevenueCat will not sync any purchase @@ -12,6 +16,6 @@ data class DangerousSettings internal constructor( val autoSyncPurchases: Boolean = true, internal val customEntitlementComputation: Boolean = false, -) { +) : Parcelable { constructor(autoSyncPurchases: Boolean = true) : this(autoSyncPurchases, false) } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt index 9bce868577..b937416b13 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt @@ -36,7 +36,7 @@ open class PurchasesConfiguration(builder: Builder) { val pendingTransactionsForPrepaidPlansEnabled: Boolean init { - this.context = builder.context + this.context = builder.context.applicationContext this.apiKey = builder.apiKey.trim() this.appUserID = builder.appUserID this.purchasesAreCompletedBy = builder.purchasesAreCompletedBy @@ -49,6 +49,25 @@ open class PurchasesConfiguration(builder: Builder) { this.pendingTransactionsForPrepaidPlansEnabled = builder.pendingTransactionsForPrepaidPlansEnabled } + internal fun copy( + appUserID: String? = this.appUserID, + service: ExecutorService? = this.service, + ): PurchasesConfiguration { + var builder = Builder(context, apiKey) + .appUserID(appUserID) + .purchasesAreCompletedBy(purchasesAreCompletedBy) + .store(store) + .diagnosticsEnabled(diagnosticsEnabled) + .entitlementVerificationMode(verificationMode) + .dangerousSettings(dangerousSettings) + .showInAppMessagesAutomatically(showInAppMessagesAutomatically) + .pendingTransactionsForPrepaidPlansEnabled(pendingTransactionsForPrepaidPlansEnabled) + if (service != null) { + builder = builder.service(service) + } + return builder.build() + } + @SuppressWarnings("TooManyFunctions") open class Builder( @get:JvmSynthetic internal val context: Context, @@ -117,7 +136,9 @@ open class PurchasesConfiguration(builder: Builder) { purchasesAreCompletedBy( if (observerMode) { PurchasesAreCompletedBy.MY_APP - } else PurchasesAreCompletedBy.REVENUECAT, + } else { + PurchasesAreCompletedBy.REVENUECAT + }, ) } @@ -238,4 +259,36 @@ open class PurchasesConfiguration(builder: Builder) { return PurchasesConfiguration(this) } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PurchasesConfiguration + + if (apiKey != other.apiKey) return false + if (appUserID != other.appUserID) return false + if (purchasesAreCompletedBy != other.purchasesAreCompletedBy) return false + if (showInAppMessagesAutomatically != other.showInAppMessagesAutomatically) return false + if (store != other.store) return false + if (diagnosticsEnabled != other.diagnosticsEnabled) return false + if (dangerousSettings != other.dangerousSettings) return false + if (verificationMode != other.verificationMode) return false + if (pendingTransactionsForPrepaidPlansEnabled != other.pendingTransactionsForPrepaidPlansEnabled) return false + + return true + } + + override fun hashCode(): Int { + var result = apiKey.hashCode() + result = 31 * result + (appUserID?.hashCode() ?: 0) + result = 31 * result + purchasesAreCompletedBy.hashCode() + result = 31 * result + showInAppMessagesAutomatically.hashCode() + result = 31 * result + store.hashCode() + result = 31 * result + diagnosticsEnabled.hashCode() + result = 31 * result + dangerousSettings.hashCode() + result = 31 * result + verificationMode.hashCode() + result = 31 * result + pendingTransactionsForPrepaidPlansEnabled.hashCode() + return result + } } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt index a85121d84c..50df234d8e 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt @@ -284,6 +284,7 @@ internal class PurchasesFactory( paywallPresentedCache, purchasesStateProvider, dispatcher = dispatcher, + initialConfiguration = configuration, ) return Purchases(purchasesOrchestrator) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt index 3ed611286d..9cbc2ae8a8 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt @@ -101,6 +101,7 @@ internal class PurchasesOrchestrator( // This is nullable due to: https://github.com/RevenueCat/purchases-flutter/issues/408 private val mainHandler: Handler? = Handler(Looper.getMainLooper()), private val dispatcher: Dispatcher, + private val initialConfiguration: PurchasesConfiguration, ) : LifecycleDelegate, CustomActivityLifecycleHandler { internal var state: PurchasesState @@ -110,6 +111,13 @@ internal class PurchasesOrchestrator( purchasesStateCache.purchasesState = value } + val currentConfiguration: PurchasesConfiguration + get() = if (initialConfiguration.appUserID == null) { + initialConfiguration + } else { + initialConfiguration.copy(appUserID = this.appUserID) + } + var finishTransactions: Boolean @Synchronized get() = appConfig.finishTransactions diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/strings/ConfigureStrings.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/strings/ConfigureStrings.kt index ffeac4a4d2..9e86180abc 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/strings/ConfigureStrings.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/strings/ConfigureStrings.kt @@ -36,4 +36,6 @@ internal object ConfigureStrings { "purchase." const val INSTANCE_ALREADY_EXISTS = "Purchases instance already set. " + "Did you mean to configure two Purchases objects?" + const val INSTANCE_ALREADY_EXISTS_WITH_SAME_CONFIG = "Purchases instance already set with the same " + + "configuration. Ignoring duplicate call." } diff --git a/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt b/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt index 8cc1adb6d1..1ddd0ec145 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt @@ -55,7 +55,9 @@ internal open class BasePurchasesTest { protected val mockBackend: Backend = mockk() protected val mockCache: DeviceCache = mockk() protected val updatedCustomerInfoListener: UpdatedCustomerInfoListener = mockk() - private val mockApplication = mockk(relaxed = true) + private val mockApplication = mockk(relaxed = true).apply { + every { applicationContext } returns this + } protected val mockContext = mockk(relaxed = true).apply { every { applicationContext @@ -408,6 +410,7 @@ internal open class BasePurchasesTest { paywallPresentedCache = paywallPresentedCache, purchasesStateCache = purchasesStateProvider, dispatcher = SyncDispatcher(), + initialConfiguration = PurchasesConfiguration.Builder(mockContext, "api_key").build() ) purchases = Purchases(purchasesOrchestrator) Purchases.sharedInstance = purchases diff --git a/purchases/src/test/java/com/revenuecat/purchases/PurchasesConfigurationTest.kt b/purchases/src/test/java/com/revenuecat/purchases/PurchasesConfigurationTest.kt index a528811df9..859506b488 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/PurchasesConfigurationTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/PurchasesConfigurationTest.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.PurchasesAreCompletedBy.MY_APP import com.revenuecat.purchases.PurchasesAreCompletedBy.REVENUECAT +import io.mockk.every import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -17,12 +18,15 @@ class PurchasesConfigurationTest { private val apiKey = "test-api-key" private lateinit var context: Context + private lateinit var applicationContext: Context private lateinit var builder: PurchasesConfiguration.Builder @Before fun setup() { context = mockk() + applicationContext = mockk() + every { context.applicationContext } returns applicationContext builder = PurchasesConfiguration.Builder(context, apiKey) } @@ -31,7 +35,7 @@ class PurchasesConfigurationTest { fun `PurchasesConfiguration has expected default parameters`() { val purchasesConfiguration = builder.build() assertThat(purchasesConfiguration.apiKey).isEqualTo(apiKey) - assertThat(purchasesConfiguration.context).isEqualTo(context) + assertThat(purchasesConfiguration.context).isEqualTo(applicationContext) assertThat(purchasesConfiguration.appUserID).isNull() assertThat(purchasesConfiguration.observerMode).isFalse assertThat(purchasesConfiguration.purchasesAreCompletedBy).isEqualTo(REVENUECAT) diff --git a/purchases/src/test/java/com/revenuecat/purchases/PurchasesFactoryTest.kt b/purchases/src/test/java/com/revenuecat/purchases/PurchasesFactoryTest.kt index 15db5f8a1d..3ebaf61e50 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/PurchasesFactoryTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/PurchasesFactoryTest.kt @@ -20,7 +20,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PurchasesFactoryTest { - private val contextMock = mockk() + private val applicationMock = mockk() + private val contextMock = mockk().apply { + every { applicationContext } returns applicationMock + } private val apiKeyValidatorMock = mockk() private lateinit var purchasesFactory: PurchasesFactory @@ -41,7 +44,7 @@ class PurchasesFactoryTest { fun `creating purchase checks context has INTERNET permission`() { val configuration = createConfiguration() every { - contextMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) + applicationMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) } returns PackageManager.PERMISSION_DENIED assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { purchasesFactory.validateConfiguration(configuration) @@ -52,7 +55,7 @@ class PurchasesFactoryTest { fun `creating purchase checks api key is not empty`() { val configuration = createConfiguration(testApiKey = "") every { - contextMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) + applicationMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) } returns PackageManager.PERMISSION_GRANTED assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { purchasesFactory.validateConfiguration(configuration) @@ -64,10 +67,10 @@ class PurchasesFactoryTest { val configuration = createConfiguration() val nonApplicationContextMock = mockk() every { - contextMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) + applicationMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) } returns PackageManager.PERMISSION_GRANTED every { - contextMock.applicationContext + applicationMock.applicationContext } returns nonApplicationContextMock assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { purchasesFactory.validateConfiguration(configuration) @@ -79,10 +82,10 @@ class PurchasesFactoryTest { val configuration = createConfiguration() val applicationContextMock = mockk() every { - contextMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) + applicationMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET) } returns PackageManager.PERMISSION_GRANTED every { - contextMock.applicationContext + applicationMock.applicationContext } returns applicationContextMock purchasesFactory.validateConfiguration(configuration) verify(exactly = 1) { apiKeyValidatorMock.validateAndLog("fakeApiKey", Store.PLAY_STORE) } diff --git a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt index 2256223faa..01c87e4cc1 100644 --- a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt +++ b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt @@ -5,10 +5,13 @@ package com.revenuecat.purchases +import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.PurchasesAreCompletedBy.MY_APP import com.revenuecat.purchases.PurchasesAreCompletedBy.REVENUECAT import com.revenuecat.purchases.common.PlatformInfo +import io.mockk.every +import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -69,4 +72,100 @@ internal class PurchasesConfigureTest : BasePurchasesTest() { Purchases.configure(builder.build()) assertThat(Purchases.sharedInstance.store).isEqualTo(Store.PLAY_STORE) } + + @Test + fun `Calling configure multiple times with same configuration does not create a new instance`() { + val builder = PurchasesConfiguration.Builder(mockContext, "api_key") + val instance1 = Purchases.configure(builder.build()) + assertThat(Purchases.sharedInstance).isEqualTo(instance1) + val instance2 = Purchases.configure(builder.build()) + assertThat(Purchases.sharedInstance).isEqualTo(instance2) + assertThat(instance2).isEqualTo(instance1) + } + + @Test + fun `Calling configure multiple times with different configuration does create a new instance`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key").build() + val instance1 = Purchases.configure(config1) + assertThat(Purchases.sharedInstance).isEqualTo(instance1) + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key2").build() + val instance2 = Purchases.configure(config2) + assertThat(Purchases.sharedInstance).isEqualTo(instance2) + assertThat(instance2).isNotEqualTo(instance1) + } + + @Test + fun `configurations with same properties are equal`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key").build() + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key").build() + + assertThat(config1).isEqualTo(config2) + assertThat(config1.hashCode()).isEqualTo(config2.hashCode()) + } + + @Test + fun `configurations with different api keys are not equal`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key1").build() + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key2").build() + + assertThat(config1).isNotEqualTo(config2) + } + + @Test + fun `configurations with different app user IDs are not equal`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key").appUserID("user1").build() + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key").appUserID("user2").build() + + assertThat(config1).isNotEqualTo(config2) + } + + @Test + fun `configurations with different purchasesAreCompletedBy are not equal`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key").purchasesAreCompletedBy(MY_APP).build() + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key").purchasesAreCompletedBy(REVENUECAT).build() + + assertThat(config1).isNotEqualTo(config2) + } + + @Test + fun `configurations with different stores are not equal`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key").store(Store.PLAY_STORE).build() + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key").store(Store.AMAZON).build() + + assertThat(config1).isNotEqualTo(config2) + } + + @Test + fun `configurations with different contexts are equal`() { + val context1 = mockContext + val context2 = mockk().apply { every { applicationContext } returns mockk() } + val config1 = PurchasesConfiguration.Builder(context1, "api_key").build() + val config2 = PurchasesConfiguration.Builder(context2, "api_key").build() + + assertThat(config1).isEqualTo(config2) + } + + @Test + fun `configurations with different verificationMode are not equal`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key") + .entitlementVerificationMode(EntitlementVerificationMode.DISABLED) + .build() + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key") + .entitlementVerificationMode(EntitlementVerificationMode.INFORMATIONAL) + .build() + + assertThat(config1).isNotEqualTo(config2) + } + + @Test + fun `configurations with different dangerousSettings are not equal`() { + val config1 = PurchasesConfiguration.Builder(mockContext, "api_key") + .dangerousSettings(DangerousSettings(autoSyncPurchases = true)) + .build() + val config2 = PurchasesConfiguration.Builder(mockContext, "api_key") + .dangerousSettings(DangerousSettings(autoSyncPurchases = false)) + .build() + + assertThat(config1).isNotEqualTo(config2) + } } diff --git a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/attributes/SubscriberAttributesPurchasesTests.kt b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/attributes/SubscriberAttributesPurchasesTests.kt index 1353d38ef1..c851ac0b25 100644 --- a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/attributes/SubscriberAttributesPurchasesTests.kt +++ b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/attributes/SubscriberAttributesPurchasesTests.kt @@ -11,6 +11,7 @@ import com.revenuecat.purchases.PostTransactionWithProductDetailsHelper import com.revenuecat.purchases.Purchases import com.revenuecat.purchases.PurchasesAreCompletedBy import com.revenuecat.purchases.PurchasesAreCompletedBy.REVENUECAT +import com.revenuecat.purchases.PurchasesConfiguration import com.revenuecat.purchases.PurchasesOrchestrator import com.revenuecat.purchases.PurchasesState import com.revenuecat.purchases.PurchasesStateCache @@ -84,8 +85,10 @@ class SubscriberAttributesPurchasesTests { postTransactionHelper, ) + val context = mockk(relaxed = true).also { applicationMock = it } + val purchasesOrchestrator = PurchasesOrchestrator( - application = mockk(relaxed = true).also { applicationMock = it }, + application = context, backingFieldAppUserID = appUserId, backend = backendMock, billing = billingWrapperMock, @@ -106,6 +109,7 @@ class SubscriberAttributesPurchasesTests { paywallPresentedCache = PaywallPresentedCache(), purchasesStateCache = PurchasesStateCache(PurchasesState()), dispatcher = SyncDispatcher(), + initialConfiguration = PurchasesConfiguration.Builder(context, "mock-api-key").build(), ) underTest = Purchases(purchasesOrchestrator) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/activity/PaywallActivity.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/activity/PaywallActivity.kt index 1e072d93d0..54c8a374b1 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/activity/PaywallActivity.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/activity/PaywallActivity.kt @@ -3,6 +3,7 @@ package com.revenuecat.purchases.ui.revenuecatui.activity import android.content.Intent import android.os.Build import android.os.Bundle +import android.os.Parcelable import android.view.Window import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -18,8 +19,14 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.googlefonts.Font import androidx.compose.ui.text.googlefonts.GoogleFont import com.revenuecat.purchases.CustomerInfo +import com.revenuecat.purchases.DangerousSettings +import com.revenuecat.purchases.EntitlementVerificationMode +import com.revenuecat.purchases.Purchases +import com.revenuecat.purchases.PurchasesAreCompletedBy +import com.revenuecat.purchases.PurchasesConfiguration import com.revenuecat.purchases.PurchasesError import com.revenuecat.purchases.PurchasesErrorCode +import com.revenuecat.purchases.Store import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.ui.revenuecatui.Paywall import com.revenuecat.purchases.ui.revenuecatui.PaywallListener @@ -28,14 +35,18 @@ import com.revenuecat.purchases.ui.revenuecatui.fonts.FontProvider import com.revenuecat.purchases.ui.revenuecatui.fonts.GoogleFontProvider import com.revenuecat.purchases.ui.revenuecatui.fonts.PaywallFont import com.revenuecat.purchases.ui.revenuecatui.fonts.TypographyType +import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger +import kotlinx.parcelize.Parcelize /** * Wrapper activity around [Paywall] that allows using it when you are not using Jetpack Compose directly. * It receives the [PaywallActivityArgs] as an extra and returns the [PaywallResult] as a result. */ +@Suppress("TooManyFunctions") internal class PaywallActivity : ComponentActivity(), PaywallListener { companion object { const val ARGS_EXTRA = "paywall_args" + const val SDK_CONFIG_EXTRA = "sdk_config_args" const val RESULT_EXTRA = "paywall_result" } @@ -49,6 +60,24 @@ internal class PaywallActivity : ComponentActivity(), PaywallListener { } } + @Parcelize + private data class SdkConfigArgs( + val apiKey: String, + val appUserId: String?, + val purchasesAreCompletedBy: PurchasesAreCompletedBy, + val showInAppMessagesAutomatically: Boolean, + val store: Store, + val diagnosticsEnabled: Boolean, + val verificationMode: EntitlementVerificationMode, + val dangerousSettings: DangerousSettings, + val pendingTransactionsForPrepaidPlansEnabled: Boolean, + ) : Parcelable + + private fun getSdkConfigArgs(savedInstanceState: Bundle): SdkConfigArgs? { + @Suppress("DEPRECATION") + return savedInstanceState.getParcelable(SDK_CONFIG_EXTRA) + } + private fun getFontProvider(): FontProvider? { val googleFontProviders = mutableMapOf() val fontsMap = getArgs()?.fonts?.mapValues { (_, fontFamily) -> @@ -79,6 +108,9 @@ internal class PaywallActivity : ComponentActivity(), PaywallListener { override fun onCreate(savedInstanceState: Bundle?) { requestWindowFeature(Window.FEATURE_NO_TITLE) super.onCreate(savedInstanceState) + if (!Purchases.isConfigured && savedInstanceState != null) { + configureSdkWithSavedData(savedInstanceState) + } val args = getArgs() val paywallOptions = PaywallOptions.Builder(dismissRequest = ::finish) .setOfferingId(args?.offeringId) @@ -97,6 +129,27 @@ internal class PaywallActivity : ComponentActivity(), PaywallListener { } } + override fun onSaveInstanceState(outState: Bundle) { + if (Purchases.isConfigured) { + val configuration = Purchases.sharedInstance.currentConfiguration + outState.putParcelable( + SDK_CONFIG_EXTRA, + SdkConfigArgs( + configuration.apiKey, + configuration.appUserID, + configuration.purchasesAreCompletedBy, + configuration.showInAppMessagesAutomatically, + configuration.store, + configuration.diagnosticsEnabled, + configuration.verificationMode, + configuration.dangerousSettings, + configuration.pendingTransactionsForPrepaidPlansEnabled, + ), + ) + } + super.onSaveInstanceState(outState) + } + override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) { setResult(RESULT_OK, createResultIntent(PaywallResult.Purchased(customerInfo))) finish() @@ -123,6 +176,26 @@ internal class PaywallActivity : ComponentActivity(), PaywallListener { setResult(RESULT_OK, createResultIntent(PaywallResult.Error(error))) } + private fun configureSdkWithSavedData(savedInstanceState: Bundle) { + val sdkConfigArgs = getSdkConfigArgs(savedInstanceState) + if (sdkConfigArgs == null) { + Logger.e("Missing SDK configuration arguments while restoring PaywallActivity") + return + } + Purchases.configure( + PurchasesConfiguration.Builder(this, sdkConfigArgs.apiKey) + .appUserID(sdkConfigArgs.appUserId) + .purchasesAreCompletedBy(sdkConfigArgs.purchasesAreCompletedBy) + .showInAppMessagesAutomatically(sdkConfigArgs.showInAppMessagesAutomatically) + .store(sdkConfigArgs.store) + .diagnosticsEnabled(sdkConfigArgs.diagnosticsEnabled) + .entitlementVerificationMode(sdkConfigArgs.verificationMode) + .dangerousSettings(sdkConfigArgs.dangerousSettings) + .pendingTransactionsForPrepaidPlansEnabled(sdkConfigArgs.pendingTransactionsForPrepaidPlansEnabled) + .build(), + ) + } + private fun createResultIntent(result: PaywallResult): Intent { return Intent().putExtra(RESULT_EXTRA, result) }