Skip to content

Commit

Permalink
Update paymentMethods to StateFlow (#6009)
Browse files Browse the repository at this point in the history
  • Loading branch information
jameswoo-stripe authored Jan 12, 2023
1 parent 40adb8e commit 9634137
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 19 deletions.
1 change: 0 additions & 1 deletion paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@
<ID>VariableNaming:BaseSheetViewModel.kt$BaseSheetViewModel$// a fatal error protected val _fatal = MutableLiveData&lt;Throwable>()</ID>
<ID>VariableNaming:BaseSheetViewModel.kt$BaseSheetViewModel$@VisibleForTesting internal val _isGooglePayReady = savedStateHandle.getLiveData&lt;Boolean>(SAVE_GOOGLE_PAY_READY)</ID>
<ID>VariableNaming:BaseSheetViewModel.kt$BaseSheetViewModel$@VisibleForTesting internal val _isLinkEnabled = MutableLiveData&lt;Boolean>()</ID>
<ID>VariableNaming:BaseSheetViewModel.kt$BaseSheetViewModel$@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) internal val _paymentMethods = savedStateHandle.getLiveData&lt;List&lt;PaymentMethod>>(SAVE_PAYMENT_METHODS)</ID>
<ID>VariableNaming:BaseSheetViewModel.kt$BaseSheetViewModel$protected val _linkConfiguration = savedStateHandle.getLiveData&lt;LinkPaymentLauncher.Configuration>(LINK_CONFIGURATION)</ID>
<ID>VariableNaming:PaymentOptionsViewModel.kt$PaymentOptionsViewModel$@VisibleForTesting internal val _paymentOptionResult = MutableLiveData&lt;PaymentOptionResult>()</ID>
<ID>VariableNaming:PaymentSheetViewModel.kt$PaymentSheetViewModel$@VisibleForTesting internal val _viewState = MutableLiveData&lt;PaymentSheetViewState>(null)</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal abstract class BasePaymentMethodsListFragment(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setHasOptionsMenu(!sheetViewModel.paymentMethods.value.isNullOrEmpty())
setHasOptionsMenu(sheetViewModel.paymentMethods.value.isNotEmpty())
sheetViewModel.eventReporter.onShowExistingPaymentOptions(
linkEnabled = sheetViewModel.isLinkEnabled.value ?: false,
activeLinkSession = sheetViewModel.activeLinkSession.value ?: false
Expand Down Expand Up @@ -136,7 +136,7 @@ internal abstract class BasePaymentMethodsListFragment(
}
}

sheetViewModel.paymentMethods.observe(viewLifecycleOwner) { paymentMethods ->
sheetViewModel.paymentMethods.launchAndCollectIn(viewLifecycleOwner) { paymentMethods ->
if (isEditing && paymentMethods.isEmpty()) {
isEditing = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,15 @@ internal class PaymentOptionsViewModel @Inject constructor(
_paymentOptionResult.value =
PaymentOptionResult.Failed(
error = throwable,
paymentMethods = _paymentMethods.value
paymentMethods = paymentMethods.value
)
}

override fun onUserCancel() {
_paymentOptionResult.value =
PaymentOptionResult.Canceled(
mostRecentError = mostRecentError,
paymentMethods = _paymentMethods.value
paymentMethods = paymentMethods.value
)
}

Expand Down Expand Up @@ -266,7 +266,7 @@ internal class PaymentOptionsViewModel @Inject constructor(
_paymentOptionResult.value =
PaymentOptionResult.Succeeded(
paymentSelection = paymentSelection,
paymentMethods = _paymentMethods.value
paymentMethods = paymentMethods.value
)
}

Expand All @@ -275,7 +275,7 @@ internal class PaymentOptionsViewModel @Inject constructor(
_paymentOptionResult.value =
PaymentOptionResult.Succeeded(
paymentSelection = paymentSelection,
paymentMethods = _paymentMethods.value
paymentMethods = paymentMethods.value
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ internal class PaymentSheetViewModel @Inject internal constructor(
}

override fun transitionToFirstScreen() {
val target = if (paymentMethods.value.isNullOrEmpty()) {
val target = if (paymentMethods.value.isEmpty()) {
updateSelection(null)
TransitionTarget.AddFirstPaymentMethod
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,12 @@ internal abstract class BaseSheetViewModel(
} ?: emptyList()
set(value) = savedStateHandle.set(SAVE_SUPPORTED_PAYMENT_METHOD, value.map { it.code })

@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
internal val _paymentMethods =
savedStateHandle.getLiveData<List<PaymentMethod>>(SAVE_PAYMENT_METHODS)

/**
* The list of saved payment methods for the current customer.
* Value is null until it's loaded, and non-null (could be empty) after that.
*/
internal val paymentMethods: LiveData<List<PaymentMethod>> = _paymentMethods
internal val paymentMethods: StateFlow<List<PaymentMethod>> = savedStateHandle
.getStateFlow(SAVE_PAYMENT_METHODS, listOf())

internal val amount: StateFlow<Amount?> = savedStateHandle
.getStateFlow(SAVE_AMOUNT, null)
Expand Down Expand Up @@ -230,7 +227,7 @@ internal abstract class BaseSheetViewModel(

private val paymentOptionsStateMapper: PaymentOptionsStateMapper by lazy {
PaymentOptionsStateMapper(
paymentMethods = paymentMethods,
paymentMethods = paymentMethods.asLiveData(),
initialSelection = savedSelection,
currentSelection = selection,
googlePayState = googlePayState.asLiveData(),
Expand Down Expand Up @@ -295,7 +292,7 @@ internal abstract class BaseSheetViewModel(
listOf(
savedSelection,
stripeIntent.asLiveData(),
paymentMethods,
paymentMethods.asLiveData(),
googlePayState.asLiveData(),
isResourceRepositoryReady,
isLinkEnabled
Expand Down Expand Up @@ -442,7 +439,7 @@ internal abstract class BaseSheetViewModel(
_selection.value = null
}

savedStateHandle[SAVE_PAYMENT_METHODS] = _paymentMethods.value?.filter {
savedStateHandle[SAVE_PAYMENT_METHODS] = paymentMethods.value.filter {
it.id != paymentMethodId
}

Expand All @@ -453,7 +450,7 @@ internal abstract class BaseSheetViewModel(
)
}

val hasNoBankAccounts = paymentMethods.value.orEmpty().all { it.type != USBankAccount }
val hasNoBankAccounts = paymentMethods.value.all { it.type != USBankAccount }
if (hasNoBankAccounts) {
updatePrimaryButtonUIState(
primaryButtonUIState.value?.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,68 @@ internal class PaymentOptionsViewModelTest {
}
}

@Test
fun `paymentMethods is not empty if customer has payment methods`() = runTest {
val viewModel = createViewModel(
args = PAYMENT_OPTION_CONTRACT_ARGS.updateState(
paymentMethods = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD)
)
)

viewModel.paymentMethods.test {
assertThat(awaitItem()).isNotEmpty()
}
}

@Test
fun `paymentMethods is empty if customer has no payment methods`() = runTest {
val viewModel = createViewModel(
args = PAYMENT_OPTION_CONTRACT_ARGS.updateState(
paymentMethods = listOf()
)
)

viewModel.paymentMethods.test {
assertThat(awaitItem()).isEmpty()
}
}

@Test
fun `transition target is AddFirstPaymentMethod if payment methods is empty`() = runTest {
val viewModel = createViewModel(
args = PAYMENT_OPTION_CONTRACT_ARGS.updateState(
paymentMethods = listOf(),
isGooglePayReady = false,
linkState = null
)
)

val observedTransitions = mutableListOf<TransitionTarget>()
viewModel.transition.observeEventsForever { observedTransitions.add(it) }

viewModel.transitionToFirstScreen()

assertThat(observedTransitions).containsExactly(TransitionTarget.AddFirstPaymentMethod)
}

@Test
fun `transition target is SelectSavedPaymentMethods if payment methods is not empty`() = runTest {
val viewModel = createViewModel(
args = PAYMENT_OPTION_CONTRACT_ARGS.updateState(
paymentMethods = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD),
isGooglePayReady = false,
linkState = null
)
)

val observedTransitions = mutableListOf<TransitionTarget>()
viewModel.transition.observeEventsForever { observedTransitions.add(it) }

viewModel.transitionToFirstScreen()

assertThat(observedTransitions).containsExactly(TransitionTarget.SelectSavedPaymentMethods)
}

private fun createViewModel(
args: PaymentOptionContract.Args = PAYMENT_OPTION_CONTRACT_ARGS,
linkState: LinkState? = args.state.linkState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.stripe.android.paymentsheet.model.SavedSelection
import com.stripe.android.paymentsheet.navigation.TransitionTarget
import com.stripe.android.paymentsheet.state.GooglePayState
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_GOOGLE_PAY_STATE
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_PAYMENT_METHODS
import com.stripe.android.paymentsheet.viewmodels.BaseSheetViewModel.Companion.SAVE_SAVED_SELECTION
import com.stripe.android.utils.TestUtils.idleLooper
import org.junit.After
Expand Down Expand Up @@ -286,7 +287,7 @@ internal class PaymentSheetListFragmentTest : PaymentSheetViewModelTestInjection
isLinkEnabled: Boolean = false,
savedSelection: SavedSelection = SavedSelection.None,
) {
sheetViewModel._paymentMethods.value = paymentMethods
sheetViewModel.savedStateHandle[SAVE_PAYMENT_METHODS] = paymentMethods
sheetViewModel.savedStateHandle[SAVE_GOOGLE_PAY_STATE] = isGooglePayReady
sheetViewModel._isLinkEnabled.value = isLinkEnabled
sheetViewModel.savedStateHandle[SAVE_SAVED_SELECTION] = savedSelection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ internal class PaymentSheetViewModelTest {
viewModel.transitionToFirstScreenWhenReady()
assertThat(observedTransitions).isEmpty()

viewModel.savedStateHandle[BaseSheetViewModel.SAVE_GOOGLE_PAY_STATE] =
viewModel.savedStateHandle[SAVE_GOOGLE_PAY_STATE] =
GooglePayState.Available
assertThat(observedTransitions).containsExactly(TransitionTarget.AddFirstPaymentMethod)
}
Expand Down Expand Up @@ -920,12 +920,63 @@ internal class PaymentSheetViewModelTest {
}
}

@Test
fun `paymentMethods is not empty if customer has payment methods`() = runTest {
val viewModel = createViewModel(
customerPaymentMethods = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD)
)

viewModel.paymentMethods.test {
assertThat(awaitItem()).isNotEmpty()
}
}

@Test
fun `paymentMethods is empty if customer has no payment methods`() = runTest {
val viewModel = createViewModel(
customerPaymentMethods = listOf()
)

viewModel.paymentMethods.test {
assertThat(awaitItem()).isEmpty()
}
}

@Test
fun `transition target is AddFirstPaymentMethod if payment methods is empty`() = runTest {
val viewModel = createViewModel(
customerPaymentMethods = listOf()
)

val observedTransitions = mutableListOf<TransitionTarget>()
viewModel.transition.observeEventsForever { observedTransitions.add(it) }

viewModel.transitionToFirstScreen()

assertThat(observedTransitions).containsExactly(TransitionTarget.AddFirstPaymentMethod)
}

@Test
fun `transition target is SelectSavedPaymentMethods if payment methods is not empty`() = runTest {
val viewModel = createViewModel(
customerPaymentMethods = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD)
)

val observedTransitions = mutableListOf<TransitionTarget>()
viewModel.transition.observeEventsForever { observedTransitions.add(it) }

viewModel.transitionToFirstScreen()

assertThat(observedTransitions).containsExactly(TransitionTarget.SelectSavedPaymentMethods)
}

private fun createViewModel(
args: PaymentSheetContract.Args = ARGS_CUSTOMER_WITH_GOOGLEPAY,
stripeIntent: StripeIntent = PAYMENT_INTENT,
customerRepository: CustomerRepository = FakeCustomerRepository(PAYMENT_METHODS),
shouldFailLoad: Boolean = false,
linkState: LinkState? = null,
customerPaymentMethods: List<PaymentMethod> = listOf()
): PaymentSheetViewModel {
val paymentConfiguration = PaymentConfiguration(ApiKeyFixtures.FAKE_PUBLISHABLE_KEY)
return PaymentSheetViewModel(
Expand All @@ -939,6 +990,7 @@ internal class PaymentSheetViewModelTest {
stripeIntent = stripeIntent,
shouldFail = shouldFailLoad,
linkState = linkState,
customerPaymentMethods = customerPaymentMethods
),
customerRepository,
prefsRepository,
Expand Down

0 comments on commit 9634137

Please sign in to comment.