Skip to content

Commit

Permalink
Paywalls: display purchase/restore errors
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Oct 20, 2023
1 parent a1f8278 commit 2f0c52a
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.lifecycle.viewModelScope
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.awaitOfferings
import com.revenuecat.purchases.models.StoreTransaction
Expand Down Expand Up @@ -66,16 +65,7 @@ class PaywallScreenViewModelImpl(
}
}

override fun onRestoreError(error: PurchasesError) {
val value = _state.value
if (value is PaywallScreenState.Loaded) {
_state.update {
value.copy(
dialogText = "There was an error restoring purchases:\n${error.message}",
)
}
}
}
override fun onRestoreError(error: PurchasesError) = Unit

override fun onPurchaseCompleted(customerInfo: CustomerInfo, storeTransaction: StoreTransaction) {
val value = _state.value
Expand All @@ -88,18 +78,7 @@ class PaywallScreenViewModelImpl(
}
}

override fun onPurchaseError(error: PurchasesError) {
val value = _state.value
if (value is PaywallScreenState.Loaded) {
if (error.code != PurchasesErrorCode.PurchaseCancelledError) {
_state.update {
value.copy(
dialogText = "There was an error purchasing:\n${error.message}",
)
}
}
}
}
override fun onPurchaseError(error: PurchasesError) = Unit

override fun onDialogDismissed() {
val value = _state.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ internal fun InternalPaywall(

is PaywallState.Loaded -> {
LoadedPaywall(state = state, viewModel = viewModel)

viewModel.actionError.value?.let {
ErrorDialog(
dismissRequest = viewModel::clearActionError,
error = it.message,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PackageType
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.models.Period
import com.revenuecat.purchases.models.Price
import com.revenuecat.purchases.models.TestStoreProduct
Expand Down Expand Up @@ -155,6 +156,7 @@ private class LoadingViewModel(
get() = _state.asStateFlow()

override val actionInProgress: State<Boolean> = mutableStateOf(false)
override val actionError: State<PurchasesError?> = mutableStateOf(null)

override fun refreshStateIfLocaleChanged() = Unit
override fun refreshStateIfColorsChanged(colorScheme: ColorScheme) = Unit
Expand All @@ -176,6 +178,8 @@ private class LoadingViewModel(
override fun openURL(url: URL, context: Context) {
error("Can't open URL")
}

override fun clearActionError() = Unit
}

@Preview(showBackground = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PurchaseParams
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
import com.revenuecat.purchases.ui.revenuecatui.PaywallMode
Expand All @@ -36,6 +37,7 @@ import java.net.URL
internal interface PaywallViewModel {
val state: StateFlow<PaywallState>
val actionInProgress: State<Boolean>
val actionError: State<PurchasesError?>

fun refreshStateIfLocaleChanged()
fun refreshStateIfColorsChanged(colorScheme: ColorScheme)
Expand All @@ -49,6 +51,8 @@ internal interface PaywallViewModel {

fun restorePurchases()

fun clearActionError()

fun openURL(url: URL, context: Context)
}

Expand All @@ -66,9 +70,12 @@ internal class PaywallViewModelImpl(
get() = _state.asStateFlow()
override val actionInProgress: State<Boolean>
get() = _actionInProgress
override val actionError: State<PurchasesError?>
get() = _actionError

private val _state: MutableStateFlow<PaywallState> = MutableStateFlow(PaywallState.Loading)
private val _actionInProgress: MutableState<Boolean> = mutableStateOf(false)
private val _actionError: MutableState<PurchasesError?> = mutableStateOf(null)
private val _lastLocaleList = MutableStateFlow(getCurrentLocaleList())
private val _colorScheme = MutableStateFlow(colorScheme)

Expand Down Expand Up @@ -138,6 +145,7 @@ internal class PaywallViewModelImpl(
} catch (e: PurchasesException) {
Logger.e("Error restoring purchases: $e")
listener?.onRestoreError(e.error)
_actionError.value = e.error
}

finishAction()
Expand All @@ -149,6 +157,10 @@ internal class PaywallViewModelImpl(
context.startActivity(intent)
}

override fun clearActionError() {
_actionError.value = null
}

private fun purchasePackage(activity: Activity, packageToPurchase: Package) {
if (verifyNoActionInProgressOrStartAction()) { return }

Expand All @@ -161,6 +173,7 @@ internal class PaywallViewModelImpl(
listener?.onPurchaseCompleted(purchaseResult.customerInfo, purchaseResult.storeTransaction)
} catch (e: PurchasesException) {
listener?.onPurchaseError(e.error)
_actionError.value = e.error
}

finishAction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package com.revenuecat.purchases.ui.revenuecatui.data.testdata
import android.content.Context
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PackageType
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.models.Period
import com.revenuecat.purchases.models.Price
import com.revenuecat.purchases.models.TestStoreProduct
Expand Down Expand Up @@ -261,7 +261,9 @@ internal class MockViewModel(
override val state: StateFlow<PaywallState>
get() = _state.asStateFlow()
override val actionInProgress: State<Boolean>
get() = derivedStateOf { _actionInProgress.value }
get() = _actionInProgress
override val actionError: State<PurchasesError?>
get() = _actionError

fun loadedState(): PaywallState.Loaded? {
return when (val state = state.value) {
Expand All @@ -283,6 +285,7 @@ internal class MockViewModel(
)

private val _actionInProgress = mutableStateOf(false)
private val _actionError = mutableStateOf<PurchasesError?>(null)

override fun refreshStateIfLocaleChanged() = Unit
override fun refreshStateIfColorsChanged(colorScheme: ColorScheme) = Unit
Expand Down Expand Up @@ -311,6 +314,8 @@ internal class MockViewModel(
error("Can't open URL")
}

override fun clearActionError() = Unit

private fun simulateActionInProgress() {
viewModelScope.launch {
_actionInProgress.value = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.Offerings
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PurchaseResult
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.models.Transaction
import com.revenuecat.purchases.paywalls.PaywallData
Expand Down Expand Up @@ -204,6 +207,38 @@ class PaywallViewModelTest {
assertThat(model.actionInProgress.value).isFalse
}

@Test
fun `purchasePackage fails`() {
val model = create()

val state = model.state.value
if (state !is PaywallState.Loaded) {
fail("Invalid state")
return
}

val selectedPackage = state.selectedPackage.value
val expectedError = PurchasesError(PurchasesErrorCode.ProductNotAvailableForPurchaseError)

coEvery {
purchases.awaitPurchase(any())
} throws PurchasesException(expectedError)

model.purchaseSelectedPackage(context)

coVerify {
purchases.awaitPurchase(any())
}

verifyOrder {
listener.onPurchaseStarted(selectedPackage.rcPackage)
listener.onPurchaseError(expectedError)
}

assertThat(model.actionInProgress.value).isFalse
assertThat(model.actionError.value).isEqualTo(expectedError)
}

@Test
fun `restorePurchases`() {
val model = create()
Expand Down Expand Up @@ -232,6 +267,58 @@ class PaywallViewModelTest {
assertThat(model.actionInProgress.value).isFalse
}

@Test
fun `restorePurchases fails`() {
val model = create()

val state = model.state.value
if (state !is PaywallState.Loaded) {
fail("Invalid state")
return
}

val expectedError = PurchasesError(PurchasesErrorCode.NetworkError)

coEvery {
purchases.awaitRestore()
} throws PurchasesException(expectedError)

model.restorePurchases()

coVerify {
purchases.awaitRestore()
}

verifyOrder {
listener.onRestoreStarted()
listener.onRestoreError(expectedError)
}

assertThat(model.actionInProgress.value).isFalse
assertThat(model.actionError.value).isEqualTo(expectedError)
}

@Test
fun `clearActionError`() {
val model = create()

val state = model.state.value
if (state !is PaywallState.Loaded) {
fail("Invalid state")
return
}

coEvery {
purchases.awaitRestore()
} throws PurchasesException(PurchasesError(PurchasesErrorCode.NetworkError))

model.restorePurchases()

assertThat(model.actionError.value).isNotNull
model.clearActionError()
assertThat(model.actionError.value).isNull()
}

private fun create(
offering: Offering? = null,
activeSubscriptions: Set<String> = setOf(),
Expand Down

0 comments on commit 2f0c52a

Please sign in to comment.