Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

in app messages support #1290

Merged
merged 7 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Some fixes and tests
  • Loading branch information
tonidero committed Sep 26, 2023
commit 151334ae6b1cbafc94e3375ff58e382c5e7d7e40
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ internal class AppConfig(
if (forceServerErrors != other.forceServerErrors) return false
if (forceSigningErrors != other.forceSigningErrors) return false
if (baseURL != other.baseURL) return false
if (showDeclinedPaymentMessagesAutomatically != other.showDeclinedPaymentMessagesAutomatically) return false

return true
}
Expand All @@ -72,6 +73,7 @@ internal class AppConfig(
result = 31 * result + forceServerErrors.hashCode()
result = 31 * result + forceSigningErrors.hashCode()
result = 31 * result + baseURL.hashCode()
result = 31 * result + showDeclinedPaymentMessagesAutomatically.hashCode()
return result
}

Expand All @@ -84,6 +86,7 @@ internal class AppConfig(
"versionName='$versionName', " +
"packageName='$packageName', " +
"finishTransactions=$finishTransactions, " +
"showDeclinedPaymentMessagesAutomatically=$showDeclinedPaymentMessagesAutomatically, " +
"baseURL=$baseURL)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -763,18 +763,26 @@ internal class BillingWrapper(
.build()
val weakActivity = WeakReference(activity)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using a weak reference here since I want to avoid any chance of leaking the activity if there is a problem connecting to the billing client.


withConnectedClient {
val activity = weakActivity.get() ?: run {
debugLog("Activity is null, not showing Google Play in-app message.")
return@withConnectedClient
executeRequestOnUIThread { error ->
if (error != null) {
errorLog(BillingStrings.BILLING_CONNECTION_ERROR_INAPP_MESSAGES.format(error))
return@executeRequestOnUIThread
}
showInAppMessages(activity, inAppMessageParams) { inAppMessageResult ->
if (inAppMessageResult.responseCode == InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED) {
verboseLog("No Google Play in-app message was available.")
} else if (inAppMessageResult.responseCode
== InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED
) {
debugLog("Subscription status was updated from In-App Message.")
withConnectedClient {
val activity = weakActivity.get() ?: run {
debugLog("Activity is null, not showing Google Play in-app message.")
return@withConnectedClient
}
showInAppMessages(activity, inAppMessageParams) { inAppMessageResult ->
when (val responseCode = inAppMessageResult.responseCode) {
InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED -> {
verboseLog(BillingStrings.BILLING_INAPP_MESSAGE_NONE)
}
InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED -> {
debugLog(BillingStrings.BILLING_INAPP_MESSAGE_UPDATE)
}
else -> errorLog(BillingStrings.BILLING_INAPP_MESSAGE_UNEXPECTED_CODE.format(responseCode))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ internal object BillingStrings {
const val BILLING_CLIENT_RETRY = "Retrying BillingClient connection after backoff of %s milliseconds."
const val ILLEGAL_STATE_EXCEPTION_WHEN_CONNECTING = "There was an IllegalStateException when connecting to " +
"BillingClient. This has been reported to occur on Samsung devices on unknown circumstances.\nException: %s"
const val BILLING_CONNECTION_ERROR_INAPP_MESSAGES = "Error connecting to billing client to display " +
"inapp messages: %s"
const val BILLING_INAPP_MESSAGE_NONE = "No Google Play in-app message was available."
const val BILLING_INAPP_MESSAGE_UPDATE = "Subscription status was updated from In-App Message."
const val BILLING_INAPP_MESSAGE_UNEXPECTED_CODE = "Unexpected billing code: %s"
}
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,12 @@ internal open class BasePurchasesTest {
anonymous: Boolean,
autoSync: Boolean = true,
customEntitlementComputation: Boolean = false,
showDeclinedPaymentMessagesAutomatically: Boolean = false,
) {
val appConfig = AppConfig(
context = mockContext,
observerMode = false,
showDeclinedPaymentMessagesAutomatically = showDeclinedPaymentMessagesAutomatically,
platformInfo = PlatformInfo("native", "3.2.0"),
proxyURL = null,
store = Store.PLAY_STORE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() {
purchases.purchasesOrchestrator.appConfig = AppConfig(
mockContext,
false,
false,
PlatformInfo("", null),
null,
Store.AMAZON
Expand Down Expand Up @@ -1665,88 +1666,6 @@ internal class PurchasesCommonTest: BasePurchasesTest() {

// endregion

// region app lifecycle

@Test
fun `state appInBackground is updated when app foregrounded`() {
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = true)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
assertThat(purchases.purchasesOrchestrator.state.appInBackground).isFalse()
}

@Test
fun `state appInBackground is updated when app backgrounded`() {
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false)
Purchases.sharedInstance.purchasesOrchestrator.onAppBackgrounded()
assertThat(purchases.purchasesOrchestrator.state.appInBackground).isTrue()
}

@Test
fun `force update of caches when app foregrounded for the first time`() {
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false, firstTimeInForeground = true)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
assertThat(purchases.purchasesOrchestrator.state.firstTimeInForeground).isFalse()
verify(exactly = 1) {
mockCustomerInfoHelper.retrieveCustomerInfo(
appUserId,
CacheFetchPolicy.FETCH_CURRENT,
false,
any()
)
}
verify(exactly = 0) {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
}
}

@Test
fun `don't force update of caches when app foregrounded not for the first time`() {
every {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
} returns false
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false, firstTimeInForeground = false)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
assertThat(purchases.purchasesOrchestrator.state.firstTimeInForeground).isFalse()
verify(exactly = 0) {
mockCustomerInfoHelper.retrieveCustomerInfo(
appUserId,
CacheFetchPolicy.FETCH_CURRENT,
false,
any()
)
}
verify(exactly = 1) {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
}
}

@Test
fun `update of caches when app foregrounded not for the first time and caches stale`() {
every {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
} returns true
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false, firstTimeInForeground = false)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
assertThat(purchases.purchasesOrchestrator.state.firstTimeInForeground).isFalse()
verify(exactly = 1) {
mockCustomerInfoHelper.retrieveCustomerInfo(
appUserId,
CacheFetchPolicy.FETCH_CURRENT,
false,
any()
)
}
verify(exactly = 1) {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
}
}

// endregion

// region Private Methods
private fun mockSynchronizeSubscriberAttributesForAllUsers() {
every {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class PurchasesConfigurationTest {
assertThat(purchasesConfiguration.diagnosticsEnabled).isFalse
assertThat(purchasesConfiguration.verificationMode).isEqualTo(EntitlementVerificationMode.DISABLED)
assertThat(purchasesConfiguration.dangerousSettings).isEqualTo(DangerousSettings(autoSyncPurchases = true))
assertThat(purchasesConfiguration.showDeclinedPaymentMessagesAutomatically).isFalse
}

@Test
Expand All @@ -52,6 +53,12 @@ class PurchasesConfigurationTest {
assertThat(purchasesConfiguration.observerMode).isTrue
}

@Test
fun `PurchasesConfiguration sets showDeclinedPaymentMessagesAutomatically correctly`() {
val purchasesConfiguration = builder.showDeclinedPaymentMessagesAutomatically(true).build()
assertThat(purchasesConfiguration.showDeclinedPaymentMessagesAutomatically).isTrue
}

@Test
fun `PurchasesConfiguration sets service correctly`() {
val serviceMock: ExecutorService = mockk()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.revenuecat.purchases

import android.app.Activity
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.revenuecat.purchases.common.AppConfig
import com.revenuecat.purchases.common.PlatformInfo
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import org.assertj.core.api.Assertions
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
@Config(manifest = Config.NONE)
internal class PurchasesLifecycleTest: BasePurchasesTest() {

// region app lifecycle

@Test
fun `state appInBackground is updated when app foregrounded`() {
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = true)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
Assertions.assertThat(purchases.purchasesOrchestrator.state.appInBackground).isFalse
}

@Test
fun `state appInBackground is updated when app backgrounded`() {
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false)
Purchases.sharedInstance.purchasesOrchestrator.onAppBackgrounded()
Assertions.assertThat(purchases.purchasesOrchestrator.state.appInBackground).isTrue
}

@Test
fun `force update of caches when app foregrounded for the first time`() {
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false, firstTimeInForeground = true)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
Assertions.assertThat(purchases.purchasesOrchestrator.state.firstTimeInForeground).isFalse
verify(exactly = 1) {
mockCustomerInfoHelper.retrieveCustomerInfo(
appUserId,
CacheFetchPolicy.FETCH_CURRENT,
false,
any()
)
}
verify(exactly = 0) {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
}
}

@Test
fun `don't force update of caches when app foregrounded not for the first time`() {
every {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
} returns false
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false, firstTimeInForeground = false)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
Assertions.assertThat(purchases.purchasesOrchestrator.state.firstTimeInForeground).isFalse
verify(exactly = 0) {
mockCustomerInfoHelper.retrieveCustomerInfo(
appUserId,
CacheFetchPolicy.FETCH_CURRENT,
false,
any()
)
}
verify(exactly = 1) {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
}
}

@Test
fun `update of caches when app foregrounded not for the first time and caches stale`() {
every {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
} returns true
mockOfferingsManagerAppForeground()
purchases.purchasesOrchestrator.state = purchases.purchasesOrchestrator.state.copy(appInBackground = false, firstTimeInForeground = false)
Purchases.sharedInstance.purchasesOrchestrator.onAppForegrounded()
Assertions.assertThat(purchases.purchasesOrchestrator.state.firstTimeInForeground).isFalse
verify(exactly = 1) {
mockCustomerInfoHelper.retrieveCustomerInfo(
appUserId,
CacheFetchPolicy.FETCH_CURRENT,
false,
any()
)
}
verify(exactly = 1) {
mockCache.isCustomerInfoCacheStale(appInBackground = false, appUserID = appUserId)
}
}

// endregion

// region activity lifecycle

@Test
fun `activity on start does not show inapp messages if option disabled`() {
resetShowDeclinedPaymentMessagesAutomatically(false)
purchases.purchasesOrchestrator.onActivityStarted(mockk())
verify(exactly = 0) { mockBillingAbstract.showInAppMessagesIfNeeded(any()) }
}

@Test
fun `activity on start shows inapp messages if option enabled`() {
resetShowDeclinedPaymentMessagesAutomatically(true)
val activity = mockk<Activity>()
every { mockBillingAbstract.showInAppMessagesIfNeeded(activity) } just Runs
purchases.purchasesOrchestrator.onActivityStarted(activity)
verify(exactly = 1) { mockBillingAbstract.showInAppMessagesIfNeeded(activity) }
}

// endregion activity lifecycle

// region Private

private fun resetShowDeclinedPaymentMessagesAutomatically(showDeclinedPaymentMessagesAutomatically: Boolean) {
purchases.purchasesOrchestrator.appConfig = AppConfig(
mockContext,
observerMode = false,
showDeclinedPaymentMessagesAutomatically = showDeclinedPaymentMessagesAutomatically,
PlatformInfo("", null),
proxyURL = null,
Store.AMAZON
)
}

// endregion Private
}
Loading