From 18573334e5c34c050f0d7606fb2c104b9fd7fd97 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 4 Oct 2024 09:55:07 +0200 Subject: [PATCH 1/3] Avoid recreating Purchases instance if same config --- .../com/revenuecat/purchases/Purchases.kt | 8 +- .../purchases/PurchasesConfiguration.kt | 38 +++++++ .../revenuecat/purchases/PurchasesFactory.kt | 1 + .../purchases/PurchasesOrchestrator.kt | 1 + .../purchases/strings/ConfigureStrings.kt | 2 + .../revenuecat/purchases/BasePurchasesTest.kt | 1 + .../purchases/PurchasesConfigureTest.kt | 98 +++++++++++++++++++ .../SubscriberAttributesPurchasesTests.kt | 6 +- 8 files changed, 153 insertions(+), 2 deletions(-) diff --git a/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt b/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt index e5cc9754dc..f1e1763915 100644 --- a/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt +++ b/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import androidx.annotation.VisibleForTesting +import com.revenuecat.purchases.common.Config import com.revenuecat.purchases.common.LogIntent import com.revenuecat.purchases.common.PlatformInfo import com.revenuecat.purchases.common.infoLog @@ -882,7 +883,12 @@ class Purchases internal constructor( configuration: PurchasesConfiguration, ): Purchases { if (isConfigured) { - infoLog(ConfigureStrings.INSTANCE_ALREADY_EXISTS) + if (backingFieldSharedInstance?.purchasesOrchestrator?.configuration == 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/PurchasesConfiguration.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt index 9bce868577..63f49ade9c 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt @@ -49,6 +49,8 @@ open class PurchasesConfiguration(builder: Builder) { this.pendingTransactionsForPrepaidPlansEnabled = builder.pendingTransactionsForPrepaidPlansEnabled } + + @SuppressWarnings("TooManyFunctions") open class Builder( @get:JvmSynthetic internal val context: Context, @@ -238,4 +240,40 @@ 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 (context != other.context) return false + 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 (service != other.service) 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 = context.hashCode() + result = 31 * result + apiKey.hashCode() + result = 31 * result + (appUserID?.hashCode() ?: 0) + result = 31 * result + purchasesAreCompletedBy.hashCode() + result = 31 * result + showInAppMessagesAutomatically.hashCode() + result = 31 * result + (service?.hashCode() ?: 0) + 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..990e9ef0e4 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, + configuration = 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..0cc4e5ed3d 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, + internal val configuration: PurchasesConfiguration, ) : LifecycleDelegate, CustomActivityLifecycleHandler { internal var state: PurchasesState 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..0f09bde0cc 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..1ae0917704 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt @@ -408,6 +408,7 @@ internal open class BasePurchasesTest { paywallPresentedCache = paywallPresentedCache, purchasesStateCache = purchasesStateProvider, dispatcher = SyncDispatcher(), + configuration = PurchasesConfiguration.Builder(mockContext, "api_key").build() ) purchases = Purchases(purchasesOrchestrator) Purchases.sharedInstance = purchases diff --git a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt index 2256223faa..46635a7757 100644 --- a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt +++ b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesConfigureTest.kt @@ -5,10 +5,12 @@ 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.mockk import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -69,4 +71,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 not equal`() { + val context1 = mockContext + val context2 = mockk() + val config1 = PurchasesConfiguration.Builder(context1, "api_key").build() + val config2 = PurchasesConfiguration.Builder(context2, "api_key").build() + + assertThat(config1).isNotEqualTo(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..9c9d7d9f2e 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(), + configuration = PurchasesConfiguration.Builder(context, "mock-api-key").build(), ) underTest = Purchases(purchasesOrchestrator) From 8ab9ad7b8017e5b22c30316009eeb09bbf4fbb78 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Fri, 4 Oct 2024 10:09:58 +0200 Subject: [PATCH 2/3] Detekt fixes --- .../defaults/kotlin/com/revenuecat/purchases/Purchases.kt | 5 +++-- .../com/revenuecat/purchases/PurchasesConfiguration.kt | 6 +++--- .../com/revenuecat/purchases/strings/ConfigureStrings.kt | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt b/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt index f1e1763915..e3c1b83066 100644 --- a/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt +++ b/purchases/src/defaults/kotlin/com/revenuecat/purchases/Purchases.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import androidx.annotation.VisibleForTesting -import com.revenuecat.purchases.common.Config import com.revenuecat.purchases.common.LogIntent import com.revenuecat.purchases.common.PlatformInfo import com.revenuecat.purchases.common.infoLog @@ -63,7 +62,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) { diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt index 63f49ade9c..db1eec1494 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt @@ -49,8 +49,6 @@ open class PurchasesConfiguration(builder: Builder) { this.pendingTransactionsForPrepaidPlansEnabled = builder.pendingTransactionsForPrepaidPlansEnabled } - - @SuppressWarnings("TooManyFunctions") open class Builder( @get:JvmSynthetic internal val context: Context, @@ -119,7 +117,9 @@ open class PurchasesConfiguration(builder: Builder) { purchasesAreCompletedBy( if (observerMode) { PurchasesAreCompletedBy.MY_APP - } else PurchasesAreCompletedBy.REVENUECAT, + } else { + PurchasesAreCompletedBy.REVENUECAT + }, ) } 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 0f09bde0cc..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,6 +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." + const val INSTANCE_ALREADY_EXISTS_WITH_SAME_CONFIG = "Purchases instance already set with the same " + + "configuration. Ignoring duplicate call." } From 5b57b1654ce41ad82135cc233721056198efa0d7 Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Mon, 7 Oct 2024 16:52:44 +0200 Subject: [PATCH 3/3] Make sure we only keep a reference to the application context --- .../kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesConfiguration.kt index db1eec1494..17ea1a9517 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