From 663112b144a797ba5e3d4789c416bd4525863acc Mon Sep 17 00:00:00 2001 From: Toni Rico Date: Wed, 14 Feb 2024 10:49:55 +0100 Subject: [PATCH] Paywalls: Call `PaywallDialog` dismiss handler after successful restore if needed (#1610) ### Description When using `PaywallDialog` composable, if the user restored and matched the `requiredEntitlementIdentifier` or condition to display the paywall given by the developer, we were not dismissing the paywall automatically. This fixes that, and auto-dismisses the paywall after a successful restore if it matches the given condition by the developer. --- .../ui/revenuecatui/InternalPaywall.kt | 3 ++ .../ui/revenuecatui/PaywallDialog.kt | 5 ++- .../ui/revenuecatui/data/PaywallViewModel.kt | 13 ++++++- .../data/PaywallViewModelFactory.kt | 3 ++ .../revenuecatui/data/PaywallViewModelTest.kt | 35 +++++++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/InternalPaywall.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/InternalPaywall.kt index 438ac2e54c..f239efe349 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/InternalPaywall.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/InternalPaywall.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.viewmodel.compose.viewModel +import com.revenuecat.purchases.CustomerInfo import com.revenuecat.purchases.ui.revenuecatui.UIConstant.defaultAnimation import com.revenuecat.purchases.ui.revenuecatui.composables.CloseButton import com.revenuecat.purchases.ui.revenuecatui.data.PaywallState @@ -160,6 +161,7 @@ private fun TemplatePaywall(state: PaywallState.Loaded, viewModel: PaywallViewMo @Composable internal fun getPaywallViewModel( options: PaywallOptions, + shouldDisplayBlock: ((CustomerInfo) -> Boolean)? = null, ): PaywallViewModel { val applicationContext = LocalContext.current.applicationContext val viewModel = viewModel( @@ -168,6 +170,7 @@ internal fun getPaywallViewModel( options, MaterialTheme.colorScheme, isSystemInDarkTheme(), + shouldDisplayBlock = shouldDisplayBlock, preview = isInPreviewMode(), ), ) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialog.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialog.kt index 16ae9538e3..8ff58df470 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialog.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PaywallDialog.kt @@ -51,7 +51,10 @@ fun PaywallDialog( } val paywallOptions = paywallDialogOptions.toPaywallOptions(dismissRequest) - val viewModel = getPaywallViewModel(options = paywallOptions) + val viewModel = getPaywallViewModel( + options = paywallOptions, + shouldDisplayBlock = paywallDialogOptions.shouldDisplayBlock, + ) Dialog( onDismissRequest = { diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt index 0df75b8895..951eaa1778 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt @@ -61,13 +61,14 @@ internal interface PaywallViewModel { } @OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class, ExperimentalPreviewRevenueCatPurchasesAPI::class) -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LongParameterList") internal class PaywallViewModelImpl( override val resourceProvider: ResourceProvider, private val purchases: PurchasesType = PurchasesImpl(), private var options: PaywallOptions, colorScheme: ColorScheme, private var isDarkMode: Boolean, + private val shouldDisplayBlock: ((CustomerInfo) -> Boolean)?, preview: Boolean = false, ) : ViewModel(), PaywallViewModel { private val variableDataProvider = VariableDataProvider(resourceProvider, preview) @@ -167,9 +168,18 @@ internal class PaywallViewModelImpl( viewModelScope.launch { try { listener?.onRestoreStarted() + val customerInfo = purchases.awaitRestore() + Logger.i("Restore purchases successful: $customerInfo") listener?.onRestoreCompleted(customerInfo) + + shouldDisplayBlock?.let { + if (!it(customerInfo)) { + Logger.d("Dismissing paywall after restore since display condition has not been met") + options.dismissRequest() + } + } } catch (e: PurchasesException) { Logger.e("Error restoring purchases: $e") listener?.onRestoreError(e.error) @@ -201,6 +211,7 @@ internal class PaywallViewModelImpl( PurchaseParams.Builder(activity, packageToPurchase), ) listener?.onPurchaseCompleted(purchaseResult.customerInfo, purchaseResult.storeTransaction) + Logger.d("Dismissing paywall after purchase") options.dismissRequest() } catch (e: PurchasesException) { if (e.code == PurchasesErrorCode.PurchaseCancelledError) { diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelFactory.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelFactory.kt index 9ae351cfae..1c0c17026d 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelFactory.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelFactory.kt @@ -3,6 +3,7 @@ package com.revenuecat.purchases.ui.revenuecatui.data import androidx.compose.material3.ColorScheme import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.revenuecat.purchases.CustomerInfo import com.revenuecat.purchases.ui.revenuecatui.ExperimentalPreviewRevenueCatUIPurchasesAPI import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions import com.revenuecat.purchases.ui.revenuecatui.helpers.ResourceProvider @@ -13,6 +14,7 @@ internal class PaywallViewModelFactory( private val options: PaywallOptions, private val colorScheme: ColorScheme, private val isDarkMode: Boolean, + private val shouldDisplayBlock: ((CustomerInfo) -> Boolean)?, private val preview: Boolean = false, ) : ViewModelProvider.NewInstanceFactory() { @Suppress("UNCHECKED_CAST") @@ -23,6 +25,7 @@ internal class PaywallViewModelFactory( colorScheme = colorScheme, isDarkMode = isDarkMode, preview = preview, + shouldDisplayBlock = shouldDisplayBlock, ) as T } } diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt index 21799ecab2..15005a1744 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt @@ -128,6 +128,7 @@ class PaywallViewModelTest { options, TestData.Constants.currentColorScheme, isDarkMode = false, + shouldDisplayBlock = null, ) coVerify(exactly = 1) { purchases.awaitOfferings() } model.updateOptions(options) @@ -148,6 +149,7 @@ class PaywallViewModelTest { options1, TestData.Constants.currentColorScheme, isDarkMode = false, + shouldDisplayBlock = null, ) coVerify(exactly = 1) { purchases.awaitOfferings() } model.updateOptions(options1) @@ -357,6 +359,37 @@ class PaywallViewModelTest { assertThat(dismissInvoked).isFalse } + @Test + fun `restorePurchases calls onDismiss if shouldDisplayBlock condition false`() { + val model = create { + false + } + + coEvery { + purchases.awaitRestore() + } returns customerInfo + + model.restorePurchases() + + assertThat(dismissInvoked).isTrue() + } + + @Test + fun `restorePurchases does not call onDismiss if shouldDisplayBlock condition true`() { + val model = create { + true + } + + coEvery { + purchases.awaitRestore() + } returns customerInfo + + model.restorePurchases() + + assertThat(dismissInvoked).isFalse() + } + + @Test fun `restorePurchases fails`() { val model = create() @@ -536,6 +569,7 @@ class PaywallViewModelTest { offering: Offering? = null, activeSubscriptions: Set = setOf(), nonSubscriptionTransactionProductIdentifiers: Set = setOf(), + shouldDisplayBlock: ((CustomerInfo) -> Boolean)? = null, ): PaywallViewModelImpl { mockActiveSubscriptions(activeSubscriptions) mockNonSubscriptionTransactions(nonSubscriptionTransactionProductIdentifiers) @@ -549,6 +583,7 @@ class PaywallViewModelTest { .build(), TestData.Constants.currentColorScheme, isDarkMode = false, + shouldDisplayBlock = shouldDisplayBlock, ) }