Skip to content

Commit

Permalink
Save SDK configuration on paywall activity so it can reconfigure auto…
Browse files Browse the repository at this point in the history
…matically (#1872)

### Description
In here we store the latest known state for `Purchases` so we can
reconfigure when displaying the `PaywallActivity` and the SDK is not
configured. This may happen if the app is killed while displaying the
`PaywallActivity`, the developer is not configuring in the application
but on an activity and the user returns through the app through the
recent apps menu.

Additionally, it includes the changes from #1866 so we avoid
reconfiguring the SDK if the configuration parameters are the same.
  • Loading branch information
tonidero authored Oct 8, 2024
1 parent 256b7ee commit f889c76
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> attributes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ private class PurchasesAPI {
val store: Store = purchases.store

val countryCode = purchases.storefrontCountryCode

val configuration: PurchasesConfiguration = purchases.currentConfiguration
}

@Suppress("LongMethod", "LongParameterList")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -117,7 +136,9 @@ open class PurchasesConfiguration(builder: Builder) {
purchasesAreCompletedBy(
if (observerMode) {
PurchasesAreCompletedBy.MY_APP
} else PurchasesAreCompletedBy.REVENUECAT,
} else {
PurchasesAreCompletedBy.REVENUECAT
},
)
}

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ internal class PurchasesFactory(
paywallPresentedCache,
purchasesStateProvider,
dispatcher = dispatcher,
initialConfiguration = configuration,
)

return Purchases(purchasesOrchestrator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Application>(relaxed = true)
private val mockApplication = mockk<Application>(relaxed = true).apply {
every { applicationContext } returns this
}
protected val mockContext = mockk<Context>(relaxed = true).apply {
every {
applicationContext
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PurchasesFactoryTest {

private val contextMock = mockk<Context>()
private val applicationMock = mockk<Context>()
private val contextMock = mockk<Context>().apply {
every { applicationContext } returns applicationMock
}
private val apiKeyValidatorMock = mockk<APIKeyValidator>()

private lateinit var purchasesFactory: PurchasesFactory
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -64,10 +67,10 @@ class PurchasesFactoryTest {
val configuration = createConfiguration()
val nonApplicationContextMock = mockk<Context>()
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)
Expand All @@ -79,10 +82,10 @@ class PurchasesFactoryTest {
val configuration = createConfiguration()
val applicationContextMock = mockk<Application>()
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) }
Expand Down
Loading

0 comments on commit f889c76

Please sign in to comment.